/* "The comfort science": Chroma Super's six film layers, on the paid page
   (window-tint-killeen-quote) and the homepage. The default layout below is static and fully readable, and is what visitors without
   JavaScript or with reduced motion get. js/comfort-science.js adds .is-live, which pins the
   section and hands every moving value (--gap, --lift, --o, --tone, --sheen, --p, --rx, --rz,
   --top, --len, --bo, --z, --ho) to the script. --gap, --sheen, --rx and --rz are written once
   on .cs-stack and inherited by every .cs-ly/.cs-beam/.cs-axis; --lift/--o/--tone are per
   layer and --top/--len/--bo per beam and --z/--ho per hit, written only when the active step
   changes (or, for the beams and hits, every frame). Only transform and opacity animate, except
   the ceramic dots (filter). --gap, --lift, --top, --len and --z are unitless design pixels: the
   stack's lengths are multiples of --cs-u (across the sheets) and --cs-uz (depth), both 1px
   except in the phone layout (see below).
   --cs-headroom, --cs-lift and --cs-drift are per-breakpoint tuning numbers the script reads
   from the section at mount and on resize (readTuning) rather than computing itself. */

/* The backdrop's light colour, set per caption by js/comfort-science.js. Registered, so a change
   fades instead of snapping; browsers that can't animate it simply switch. */
@property --cs-glow {
  syntax: "<color>";
  inherits: true;
  initial-value: #fde6c4;
}

.comfort-science {
  --cs-uv: #8b5cf6;
  --cs-uv-hot: #a78bfa;
  --cs-ice: #e0f2fe;
  --cs-heat: #fb923c;
  --cs-sun: #fde6c4;
  --cs-u: 1px;
  --cs-uz: 1px;
  /* The live pin must clear the page's sticky header (and, on phones, its fixed mobile
     bottom bar) so nothing sits under either. Sourced from css/window-tint-killeen-quote.css:
     .lp-header-inner min-height 68px + .lp-header's 1px border-bottom = 69px. */
  --cs-top: 69px;
  --cs-bottom: 0px;
  /* Per-breakpoint numbers js/comfort-science.js reads at mount and on resize (readTuning):
     how far above the top sheet the light starts, how far the active sheet lifts (both unitless
     design px), and how much the camera drifts across the story (0-1). Phones override them
     below. */
  --cs-headroom: 70;
  --cs-lift: 18;
  --cs-drift: 1;
  position: relative;
  background: var(--bg);
}

/* Matches window-tint-killeen-quote.css's own header/mobile-bar breakpoint (767px), which is
   not the same breakpoint the stack's phone layout switches at (820px) below. */
@media (max-width: 767px) {
  .comfort-science {
    /* .lp-header-inner min-height 62px + 1px border-bottom = 63px. */
    --cs-top: 63px;
    /* .lp-mobile-bar: 1px border-top + 10px padding-top + 48px min-height buttons
       + 10px padding-bottom + the safe-area inset the bar itself pads for. */
    --cs-bottom: calc(69px + env(safe-area-inset-bottom));
  }
}

.cs-pin {
  display: grid;
  grid-template-columns: 1.08fr 1fr;
  align-items: center;
  gap: 48px;
  max-width: 1180px;
  margin: 0 auto;
  padding: 96px 24px;
}

/* ---- the 3D stack ---- */

/* The static layout (no JS, reduced motion) draws the light at its resting length, rising
   about 130px above the sheets: the padding is that headroom, so the beams and the axis stay
   below the Sunlight label and inside the section. min-height counts the padding
   (border-box), so it is the sheets' 460px plus the headroom. The live layout drops both. */
.cs-visual {
  position: relative;
  display: grid;
  place-items: center;
  min-height: calc(460px + 90px);
  padding-top: 90px;
}

.cs-edge {
  position: absolute;
  left: 50%;
  color: var(--faint);
  font: 600 0.64rem/1 var(--body);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  white-space: nowrap;
  transform: translateX(-50%);
}

.cs-edge-top {
  top: 0;
}

.cs-edge-bottom {
  bottom: 0;
}

.cs-iso {
  position: relative;
  width: min(30vw, 300px);
  aspect-ratio: 1;
  perspective: calc(1400 * var(--cs-uz));
}

.cs-stack {
  --gap: 32;
  /* Same value on all six layers, so the script sets it once here and every .cs-ly::after
     inherits it, instead of writing it six times per frame. */
  --sheen: 0;
  /* The camera. The script drifts these across the story on desktop; the static layout and
     phones keep today's angles. Standing planes (beams, axis) undo --rz so they face the camera. */
  --rx: 62deg;
  --rz: -42deg;
  position: absolute;
  inset: 0;
  transform: rotateX(var(--rx)) rotateZ(var(--rz));
  transform-style: preserve-3d;
}

/* A sheet. overflow must stay visible and the sheet must preserve-3d: its underside (::before),
   its shadow onto the sheet below and its beams' landing glows sit at their own depths. A
   dimmed sheet (--o 0.42 during the steps) is flattened by its opacity, so its dark ::before
   underside would paint over its face and turn the inactive sheets into dark slabs (the
   prototype's look, which Michael saw live and replaced on 2026-09-24). So the underside is
   hidden on inactive sheets during the story: their ice/amber/violet tints, the ceramic dots and
   the cones stay visible through the faded stack, and only the active sheet shows its edge. */
.cs-ly {
  position: absolute;
  inset: 0;
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 4px;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.17), rgba(255, 255, 255, 0.035) 55%, rgba(255, 255, 255, 0.1));
  opacity: var(--o, 1);
  transform: translateZ(calc((var(--i) * var(--gap) + var(--lift, 0)) * var(--cs-uz)));
  transform-style: preserve-3d;
  transition: opacity 0.35s, border-color 0.35s, box-shadow 0.35s, background-color 0.35s;
}

/* The underside: 3 design px of thickness. */
.cs-ly::before {
  content: "";
  position: absolute;
  inset: -1px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  background: rgba(6, 6, 10, 0.85);
  transform: translateZ(calc(-3 * var(--cs-uz)));
}

.is-live .cs-ly:not(.active)::before {
  visibility: hidden;
}

.is-live .cs-ly:not(.active) {
  border-color: rgba(255, 255, 255, 0.4);
}

/* A light sheen that travels across every sheet as you scroll (background-position, not an
   oversized translated box, because the sheet can no longer clip with overflow: hidden). */
.cs-ly::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 4px;
  pointer-events: none;
  background: linear-gradient(115deg, transparent 42%, rgba(255, 255, 255, 0.16) 50%, transparent 58%);
  background-size: 180% 100%;
  background-position: calc(var(--sheen, 0) * 100%) 0;
}

/* The shadow a sheet casts onto the one below: a blurred disc just above the next sheet, that
   fades as the sheets close (opacity is a ratio of --gap, not a length). */
.cs-shadow {
  position: absolute;
  inset: 6%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
  filter: blur(6px);
  opacity: calc(var(--gap) / 32);
  pointer-events: none;
  transform: translateZ(calc((3 - var(--gap)) * var(--cs-uz)));
}

/* Exploded-view corner ticks. */
.cs-corner {
  position: absolute;
  inset: 6px;
  pointer-events: none;
  background:
    linear-gradient(rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.45)) 0 0 / 10px 1px no-repeat,
    linear-gradient(rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.45)) 0 0 / 1px 10px no-repeat,
    linear-gradient(rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.45)) 100% 100% / 10px 1px no-repeat,
    linear-gradient(rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.45)) 100% 100% / 1px 10px no-repeat;
}

/* Materials. */
.cs-ly.cs-poly {
  background: linear-gradient(135deg, rgba(224, 242, 254, 0.2), rgba(224, 242, 254, 0.04) 55%, rgba(224, 242, 254, 0.12));
}

.cs-ly.cs-adh {
  background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(251, 191, 36, 0.05) 55%, rgba(251, 191, 36, 0.12));
}

.cs-ly.cs-uvl {
  background: linear-gradient(135deg, rgba(139, 92, 246, 0.26), rgba(139, 92, 246, 0.06) 55%, rgba(139, 92, 246, 0.16));
}

.cs-ly.cs-ceramic {
  background:
    radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.36) 1.3px, transparent 1.8px) 0 0 / 11px 11px,
    linear-gradient(135deg, rgba(20, 20, 28, 0.6), rgba(60, 60, 72, 0.35));
}

/* The nanoparticles light up warm while the ceramic sheet is the active one. */
.cs-dots {
  position: absolute;
  inset: 0;
  border-radius: 4px;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(circle at 20% 30%, #fed7aa 1.6px, transparent 2.4px) 0 0 / 11px 11px;
  transition: opacity 0.4s;
}

.cs-ly.active .cs-dots {
  opacity: 1;
}

.in-view .cs-ly.active .cs-dots {
  animation: cs-dots 1.6s ease-in-out infinite alternate;
}

@keyframes cs-dots {
  from { filter: brightness(0.7); }
  to { filter: brightness(1.6) drop-shadow(0 0 4px #fb923c); }
}

.cs-ly.cs-hard {
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.08) 55%, rgba(255, 255, 255, 0.2));
}

/* A gloss sweeps across the hardcoat while it is the active sheet. */
.cs-gloss {
  position: absolute;
  inset: 0;
  border-radius: 4px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s;
}

.cs-gloss::before {
  content: "";
  position: absolute;
  inset: -40%;
  background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, 0.55) 50%, transparent 60%);
  transform: translateX(-60%);
}

.cs-ly.active .cs-gloss {
  opacity: 1;
}

.in-view .cs-ly.active .cs-gloss::before {
  animation: cs-gloss 2.2s ease-in-out infinite;
}

@keyframes cs-gloss {
  to { transform: translateX(60%); }
}

.cs-ly.active {
  border-color: var(--tone, var(--red));
  background-color: color-mix(in srgb, var(--tone, var(--red)) 18%, transparent);
  box-shadow: 0 0 34px color-mix(in srgb, var(--tone, var(--red)) 70%, transparent);
}

/* ---- the light ---- */

/* A cone of light is a standing plane: rotateX(-90deg) about its top edge turns its height into
   depth, so it hangs from --top (design px above the plane) down to the sheet that stops it,
   and rotateZ(-rz) keeps it facing the camera. Every background layer is an ellipse that
   reaches zero before the element's edge: a layer that lights the top row or a side makes the
   beam read as a box (four rounds of the prototype). The defaults draw the static, no-JS and
   reduced-motion layout at gap 32; the script overwrites them on its first frame. */
.cs-beam {
  --c: 255, 255, 255;
  --top: 264;
  --len: 0;
  --bo: 0.72;
  position: absolute;
  left: calc(var(--x) - 58 * var(--cs-u));
  top: var(--y);
  width: calc(116 * var(--cs-u));
  height: calc(var(--len) * var(--cs-uz));
  opacity: var(--bo);
  pointer-events: none;
  transform-origin: 50% 0;
  transform: translateZ(calc(var(--top) * var(--cs-uz))) rotateZ(calc(-1 * var(--rz))) rotateX(-90deg);
  background:
    radial-gradient(50% 100% at 50% 100%, rgba(var(--c), 1), rgba(var(--c), 0.6) 24%, rgba(var(--c), 0.22) 56%, rgba(var(--c), 0)),
    radial-gradient(18% 80% at 50% 55%, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0));
}

.cs-beam-uv {
  --c: 167, 139, 250;
  --lc: 196, 181, 253;
  --len: 168;
}

.cs-beam-vis {
  --c: 255, 250, 235;
  --lc: 255, 250, 235;
  --len: 339;
}

.cs-beam-ir {
  --c: 251, 146, 60;
  --lc: 253, 186, 116;
  --len: 200;
}

/* The heat the ceramic layer turns back: rises from that sheet and leans away. */
.cs-beam-refl {
  --c: 239, 68, 68;
  --top: 64;
  --len: 130;
  --bo: 0.6;
  left: calc(var(--x) - 42 * var(--cs-u));
  width: calc(84 * var(--cs-u));
  transform: translateZ(calc(var(--top) * var(--cs-uz))) rotateZ(calc(-1 * var(--rz))) rotateX(90deg) rotateZ(28deg);
  background: radial-gradient(50% 100% at 50% 0%, rgba(var(--c), 1), rgba(var(--c), 0.6) 24%, rgba(var(--c), 0.22) 56%, rgba(var(--c), 0));
}

/* Two bright pulses travel down each cone (up the bounce) so the path of the light is visible.
   They run only while their beam shows (.lit, set by the script). */
.cs-pulse {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: calc(70 * var(--cs-uz));
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(40% 50% at 50% 50%, rgba(255, 255, 255, 0.75), rgba(var(--c), 0.35) 45%, rgba(var(--c), 0));
}

.in-view .cs-beam.lit .cs-pulse {
  animation: cs-travel 2.6s linear infinite;
}

.in-view .cs-beam.lit .cs-pulse.p2 {
  animation-delay: -1.3s;
}

.in-view .cs-beam-refl.lit .cs-pulse {
  animation-direction: reverse;
  animation-duration: 3s;
}

@keyframes cs-travel {
  from { transform: translateY(calc(-0.12 * var(--len) * var(--cs-uz))); opacity: 0; }
  12% { opacity: 1; }
  88% { opacity: 1; }
  to { transform: translateY(calc(0.96 * var(--len) * var(--cs-uz))); opacity: 0; }
}

/* Each cone is named where it enters, at its top in the open space under "Sunlight · outside",
   in its own colour (--lc, a lighter tint of --c): lower down, the names sat on the top sheet
   and its glows, and "Visible light" hid behind that sheet once the stack opened. 4px inside
   is as high as it goes: a browser paints nothing above the cone's top edge, the edge the
   plane is rotated on, so the light starts 70 design px above the top sheet (--cs-headroom):
   at 50 the Clarity step's lifted top sheet reached "Visible light" and the adhesive step's
   breathing ran the sheet's corner into it. On the closed stack the cone tops still sit over
   the film, so the names fade in with the gap: hidden at 19, full at 23, the tightest the
   adhesive step breathes, so that step never dims them. Measured, the closing stack's corner
   then touches "Visible light" by 2px at most, while it shows at 12%. The names are 11px at
   full size and shrink with the picture on a short screen (never below 9px): at a fixed 11px
   the neighbours ran into each other once the stack shrank. */
.cs-beam-label {
  position: absolute;
  top: 4px;
  left: 50%;
  color: rgb(var(--lc));
  font: 700 max(9px, calc(11 * var(--cs-u)))/1 var(--body);
  letter-spacing: 0.2em;
  text-shadow: 0 0 10px rgba(0, 0, 0, 0.95), 0 0 3px rgba(0, 0, 0, 0.95);
  text-transform: uppercase;
  white-space: nowrap;
  opacity: clamp(0, (var(--gap) - 19) / 4, 1);
  transform: translateX(-50%);
}

.cs-beam-label::after {
  content: "";
  display: block;
  width: 1px;
  height: 16px;
  margin: 5px auto 0;
  background: linear-gradient(rgba(var(--c), 0.9), rgba(var(--c), 0));
}

/* The glow where a beam lands on its sheet, 2 design px above it. */
.cs-hit {
  --z: 0;
  --ho: 0;
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: calc(150 * var(--cs-u));
  height: calc(150 * var(--cs-u));
  margin: calc(-75 * var(--cs-u)) 0 0 calc(-75 * var(--cs-u));
  border-radius: 50%;
  opacity: var(--ho);
  pointer-events: none;
  background: radial-gradient(closest-side, var(--hc), transparent 70%);
  transform: translateZ(calc((var(--z) + 2) * var(--cs-uz)));
}

.in-view .cs-hit.pulse {
  animation: cs-hit 1.4s ease-in-out infinite alternate;
}

@keyframes cs-hit {
  from { transform: translateZ(calc((var(--z) + 2) * var(--cs-uz))) scale(0.85); }
  to { transform: translateZ(calc((var(--z) + 2) * var(--cs-uz))) scale(1.15); }
}

/* ---- the exploded-view diagram ---- */

/* The assembly axis every sheet is threaded on: a dashed standing line with a ring at each end. */
.cs-axis {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: calc((7.2 * var(--gap) + 120) * var(--cs-uz));
  border-left: 1px dashed rgba(255, 255, 255, 0.28);
  pointer-events: none;
  transform-origin: 50% 0;
  transform: translateZ(calc((6.8 * var(--gap) + 70) * var(--cs-uz))) rotateZ(calc(-1 * var(--rz))) rotateX(-90deg);
}

.cs-axis::before,
.cs-axis::after {
  content: "";
  position: absolute;
  left: -4px;
  width: 7px;
  height: 7px;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 50%;
}

.cs-axis::before {
  top: -4px;
}

.cs-axis::after {
  bottom: -4px;
}

/* A faint grid under the stack. */
.cs-grid {
  position: absolute;
  inset: -40%;
  opacity: 0.5;
  pointer-events: none;
  background:
    linear-gradient(90deg, rgba(255, 255, 255, 0.07) 1px, transparent 1px) 0 0 / calc(30 * var(--cs-u)) calc(30 * var(--cs-u)),
    linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px) 0 0 / calc(30 * var(--cs-u)) calc(30 * var(--cs-u));
  -webkit-mask: radial-gradient(closest-side, #000, transparent);
  mask: radial-gradient(closest-side, #000, transparent);
  transform: translateZ(calc((-1.6 * var(--gap) - 30) * var(--cs-uz)));
}

/* ---- the copy ---- */

.cs-copy {
  position: relative;
}

.cs-caps,
.cs-layers {
  display: grid;
  gap: 40px;
}

.cs-layers {
  margin: 0;
  padding: 0;
  list-style: none;
}

.cs-cap h2,
.cs-cap h3 {
  margin: 16px 0 14px;
  font: 700 clamp(2.2rem, 3.8vw, 3.4rem)/0.95 var(--disp);
  text-transform: uppercase;
}

.cs-cap em {
  color: var(--red-hot);
  font-style: normal;
}

.cs-cap p {
  max-width: 40ch;
  margin: 0;
  color: var(--muted);
  font-size: 1.02rem;
}

.cs-cap .cs-stat {
  max-width: none;
  margin-top: 14px;
  font: 700 clamp(3.6rem, 7vw, 6rem)/0.85 var(--disp);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  text-transform: uppercase;
}

.cs-cap .cs-stat small {
  margin-right: 0.18em;
  font-size: 0.42em;
  letter-spacing: 0.04em;
  vertical-align: 0.9em;
}

.cs-cap .cs-stat-uv {
  color: var(--cs-uv-hot);
}

.cs-cap .cs-stat-ir {
  color: var(--red-hot);
}

.cs-cta {
  margin-top: 26px;
}

/* A layer's name with its icon: the card's kicker, minus the number. */
.cs-name {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--red-hot);
  font: 600 11px/1 var(--body);
  letter-spacing: 0.3em;
  text-transform: uppercase;
}

.cs-name svg {
  width: 26px;
  height: 26px;
  padding: 4px;
  border: 1px solid rgba(239, 68, 68, 0.45);
  border-radius: 6px;
  box-shadow: 0 0 18px rgba(220, 38, 38, 0.25);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The progress line: six segments under the card, one per layer, shown during the layer steps.
   Its buttons carry no text (the numbers live only in their aria-labels) and are a jump to
   that layer. It only makes sense while the section is pinned. */
.cs-steps {
  display: none;
}

.is-live .cs-steps {
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: repeat(6, 1fr);
  gap: 6px;
  width: min(100%, 300px);
  margin: 22px 0 0;
  padding: 0;
  list-style: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.4s, visibility 0s 0.4s;
}

.is-live .cs-steps.on {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.4s;
}

.cs-steps button {
  all: unset;
  display: block;
  width: 100%;
  height: 14px;
  cursor: pointer;
  background: linear-gradient(rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.18)) 0 50% / 100% 2px no-repeat;
  transition: background 0.3s;
}

.cs-steps li.done button {
  background-image: linear-gradient(rgba(220, 38, 38, 0.55), rgba(220, 38, 38, 0.55));
}

.cs-steps li.now button {
  background-image: linear-gradient(var(--red), var(--red));
  background-size: 100% 4px;
}

.cs-steps button:focus-visible {
  outline: 2px solid var(--red);
  outline-offset: 4px;
}

/* ---- live: pinned and scroll-driven (js/comfort-science.js adds .is-live) ---- */

.comfort-science.is-live {
  height: 820vh;
}

.is-live .cs-pin {
  position: sticky;
  top: var(--cs-top);
  height: calc(100svh - var(--cs-top));
  max-width: none;
  gap: 0;
  padding: 0;
  overflow: hidden;
  background: radial-gradient(70% 70% at 30% 40%, #191922 0%, var(--bg) 70%);
  /* The active layer's light behind the stack, and a faint warm edge on the sunlight side.
     Browsers without color-mix keep the plain background above. */
  background:
    radial-gradient(38% 52% at 27% 50%, color-mix(in srgb, var(--cs-glow) 18%, transparent), transparent 72%),
    radial-gradient(90% 38% at 30% 0%, color-mix(in srgb, var(--cs-sun) 6%, transparent), transparent 70%),
    radial-gradient(70% 70% at 30% 40%, #191922 0%, var(--bg) 70%);
  transition: --cs-glow 0.6s ease;
}

.is-live .cs-pin::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.5;
  pointer-events: none;
  background: radial-gradient(120% 90% at 50% 50%, transparent 55%, rgba(0, 0, 0, 0.6) 100%);
}

/* ---- The backdrop: a blueprint sheet lit by the active layer (2026-09-27) ------------------
   Decoration only, and only while the story runs: the static layout (no JS, reduced motion)
   keeps its plain background. What moves does so by transform or opacity, and the full-screen
   grid holds still. No words here: a title block was tried and cut as filler. A scan line that
   swept down the sheet on each layer change was cut too: it pulled the eye off the story's own light. */
.cs-backdrop {
  display: none;
}

/* z-index -1 paints it under the in-flow copy but above the pin's own background (the sticky
   pin is a stacking context). */
.is-live .cs-backdrop {
  display: block;
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

.is-live .cs-bd-grid {
  position: absolute;
  inset: 0;
  overflow: hidden;
  -webkit-mask: radial-gradient(75% 85% at 32% 50%, #000 35%, transparent 90%);
  mask: radial-gradient(75% 85% at 32% 50%, #000 35%, transparent 90%);
}

/* The grid holds still: drifting it under the mask re-rendered the whole masked screen every
   scroll frame and nearly doubled the frame time (measured 2026-09-27); still, it costs almost
   nothing. The rest of the scene (camera, light, sweep, ripples, rulers) carries the motion. */
.is-live .cs-bd-grid > i {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px) 0 0 / 120px 120px,
    linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px) 0 0 / 120px 120px,
    linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px) 0 0 / 24px 24px,
    linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px) 0 0 / 24px 24px;
}

/* Near the stack the grid takes the layer's colour, as if its light falls on the drawing. */
.is-live .cs-bd-lit {
  -webkit-mask: radial-gradient(30% 40% at 27% 50%, rgba(0, 0, 0, 0.9), transparent 75%);
  mask: radial-gradient(30% 40% at 27% 50%, rgba(0, 0, 0, 0.9), transparent 75%);
}

.is-live .cs-bd-lit > i {
  background:
    linear-gradient(color-mix(in srgb, var(--cs-glow) 55%, transparent) 1px, transparent 1px) 0 0 / 120px 120px,
    linear-gradient(90deg, color-mix(in srgb, var(--cs-glow) 55%, transparent) 1px, transparent 1px) 0 0 / 120px 120px,
    linear-gradient(color-mix(in srgb, var(--cs-glow) 30%, transparent) 1px, transparent 1px) 0 0 / 24px 24px,
    linear-gradient(90deg, color-mix(in srgb, var(--cs-glow) 30%, transparent) 1px, transparent 1px) 0 0 / 24px 24px;
}

.is-live .cs-bd-corner {
  position: absolute;
  width: 22px;
  height: 22px;
  border: 0 solid rgba(255, 255, 255, 0.28);
}

.is-live .cs-bd-corner:nth-of-type(1) {
  top: 28px;
  left: 28px;
  border-top-width: 1px;
  border-left-width: 1px;
}

.is-live .cs-bd-corner:nth-of-type(2) {
  top: 28px;
  right: 28px;
  border-top-width: 1px;
  border-right-width: 1px;
}

.is-live .cs-bd-corner:nth-of-type(3) {
  bottom: 28px;
  left: 28px;
  border-bottom-width: 1px;
  border-left-width: 1px;
}

.is-live .cs-bd-corner:nth-of-type(4) {
  right: 28px;
  bottom: 28px;
  border-right-width: 1px;
  border-bottom-width: 1px;
}

/* Tick rulers with no numbers; a red marker on each follows the story's progress (--p). */
.is-live .cs-bd-ruler {
  position: absolute;
}

.is-live .cs-bd-ruler-y {
  top: 130px;
  bottom: 130px;
  left: 30px;
  width: 14px;
  background:
    repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.26) 0 1px, transparent 1px 12px) left / 6px 100% no-repeat,
    repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.36) 0 1px, transparent 1px 60px) left / 14px 100% no-repeat;
}

.is-live .cs-bd-ruler-x {
  right: 130px;
  bottom: 30px;
  left: 130px;
  height: 14px;
  background:
    repeating-linear-gradient(to right, rgba(255, 255, 255, 0.26) 0 1px, transparent 1px 12px) bottom / 100% 6px no-repeat,
    repeating-linear-gradient(to right, rgba(255, 255, 255, 0.36) 0 1px, transparent 1px 60px) bottom / 100% 14px no-repeat;
}

/* The marker's box is the ruler's own size, so translating it by --p × 100% walks its tip from
   one end of the ruler to the other. */
.is-live .cs-bd-ruler > i {
  position: absolute;
  inset: 0;
}

.is-live .cs-bd-ruler-y > i {
  transform: translateY(calc(var(--p, 0) * 100%));
}

.is-live .cs-bd-ruler-x > i {
  transform: translateX(calc(var(--p, 0) * 100%));
}

.is-live .cs-bd-ruler-y > i::before,
.is-live .cs-bd-ruler-x > i::before {
  position: absolute;
  border: 6px solid transparent;
  content: "";
}

.is-live .cs-bd-ruler-y > i::before {
  top: -6px;
  left: 18px;
  border-left: 9px solid var(--red);
}

.is-live .cs-bd-ruler-x > i::before {
  bottom: 18px;
  left: -6px;
  border-bottom: 9px solid var(--red);
}

/* Where the film stops UV or heat, rings spread from that spot (placed by the script). The
   0.55 vertical squash lays them roughly in the sheets' plane. */
.is-live .cs-bd-ripple {
  position: absolute;
  top: 0;
  left: 0;
  width: 560px;
  height: 560px;
  margin: -280px 0 0 -280px;
  border: 1px solid color-mix(in srgb, var(--cs-glow) 70%, transparent);
  border-radius: 50%;
  opacity: 0;
  box-shadow: inset 0 0 30px color-mix(in srgb, var(--cs-glow) 25%, transparent);
}

.is-live.in-view .cs-bd-ripple.go {
  animation: cs-bd-ripple 2.2s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
}

.is-live.in-view .cs-bd-ripple.go + .cs-bd-ripple.go {
  animation-delay: -1.1s;
}

@keyframes cs-bd-ripple {
  from {
    transform: scale(0.08) scaleY(0.55);
    opacity: 0.7;
  }
  to {
    transform: scale(1) scaleY(0.55);
    opacity: 0;
  }
}


.is-live .cs-visual {
  height: 100%;
  min-height: 0;
  padding-top: 0;
}

/* Centre the composition (beams, axis and sheets), not the sheet plane: the stack drops by
   about half the height the light and the axis stand above the top sheet (raising the light
   from 50 to 70 moved that centre by 2px, the axis ring standing higher, so it stays 26).
   Then fit a short screen. At full size the picture reaches 326px above the visual's centre,
   at the adhesive step's widest breath, and only 157px below it, so on most laptops the beam
   names ran into "Sunlight · outside". The room is the distance from the centre up to that
   label (9vh down the pin, 0.64rem tall) less 16px; "Your cabin" mirrors it below. When 326px
   is more than the room, the picture slides down into the space under the stack; when the
   whole 483px is more than twice the room, it also shrinks, depth and sheets together, so no
   clearance closes up. A big monitor needs neither and keeps its picture exactly. The
   numbers come from npm run comfort-science:measure; re-run it after any change to the stack's
   geometry, light or timeline. Phones replace this rule below. */
.is-live .cs-iso {
  --cs-u: min(1px, 2 * (50svh - var(--cs-top) / 2 - 9vh - 0.64rem - 16px) / 483);
  --cs-uz: var(--cs-u);
  width: min(30vw, calc(300 * var(--cs-u)));
  translate: 0 calc(26 * var(--cs-uz) + max(0px, 326 * var(--cs-u) - (50svh - var(--cs-top) / 2 - 9vh - 0.64rem - 16px)));
}

.is-live .cs-edge-top {
  top: 9vh;
}

.is-live .cs-edge-bottom {
  bottom: 9vh;
}

.is-live .cs-copy {
  display: grid;
  align-content: start;
  justify-items: start;
  height: 100%;
  /* The card is anchored a fifth of the way down the pin, level with the beam labels, so every
     card's icon row sits on one line and the headline hangs below it. Centring the card on
     itself read as "the left side sits higher than the right". */
  padding: calc(0.2 * (100svh - var(--cs-top))) 9vw 0 0;
}

/* All captions share one grid cell, so the box is exactly as tall as the tallest card and the
   progress line under it always clears that card's last line (a fixed min-height was outgrown
   by the nano-ceramic card at 1366-1440px wide). Each card starts at the cell's top, so every
   icon row sits on the same line. */
.is-live .cs-caps {
  display: grid;
  position: relative;
  width: 100%;
}

.is-live .cs-layers {
  display: grid;
  position: static;
}

.is-live .cs-caps > *,
.is-live .cs-layers > * {
  grid-area: 1 / 1;
}

.is-live .cs-cap {
  position: relative;
  inset: auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  opacity: 0;
  pointer-events: none;
  transform: translateY(18px);
  transition: opacity 0.45s ease, transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.is-live .cs-cap.on {
  opacity: 1;
  pointer-events: auto;
  transform: none;
}

@media (max-width: 820px) {
  .comfort-science {
    /* Shorter light, today's lift, today's fixed camera: the phone fit is measured against a
       still composition (scripts/measure-comfort-science-fit.js), and drift would change it. */
    --cs-headroom: 30;
    --cs-lift: 10;
    --cs-drift: 0;
  }

  /* Texture the phone has no room for; the card names each beam on its step. */
  .cs-beam-label,
  .cs-grid,
  .cs-corner {
    display: none;
  }

  .cs-pin {
    grid-template-columns: 1fr;
    gap: 32px;
    padding: 72px 16px;
  }

  /* The stack is smaller here but the light keeps its desktop length, so it needs more room. */
  .cs-visual {
    min-height: calc(340px + 150px);
    padding-top: 150px;
  }

  .cs-iso {
    width: 52vw;
  }

  /* About one screen of scrolling per layer. At 600svh a layer took barely half a screen, so a
     single flick on a phone ran through the whole story. */
  .comfort-science.is-live {
    height: 900svh;
  }

  /* Browsers without container query units (iOS 15 and older) keep a fixed 260px visual row
     and a fixed-size stack that fits it: the cap is (260 − 64) / 479 ≈ 0.409px, the row less the
     two labels over the composition's height, and applies on any phone. The @supports
     block below replaces both rules everywhere else, so these must stay before it. */
  .is-live .cs-pin {
    grid-template-rows: 260px 1fr;
  }

  .is-live .cs-iso {
    --cs-u: min((100vw - 32px) / 498, 0.409px);
    --cs-uz: var(--cs-u);
    top: calc(109 * var(--cs-u));
    width: calc(300 * var(--cs-u));
    translate: none;
  }

  @supports (height: 1cqh) {
    /* The copy row is exactly as tall as the tallest caption plus the index and bar clearance;
       the visual row gets the rest. It has no floor: the stack scales to any row, so on short
       phones the captions win the space rather than running into the index. */
    .is-live .cs-pin {
      grid-template-rows: minmax(0, 1fr) auto;
    }

    /* A size container, so the stack can measure the row it has to fit. */
    .is-live .cs-visual {
      container-type: size;
    }

    /* Phones scale the desktop stack to fill its row. Measured by
       scripts/measure-comfort-science-fit.js (npm run comfort-science:measure), which sweeps
       the whole timeline in Chromium with the phone tuning above (sheets, shadows, beams, axis
       and hits, adhesive peak included): the composition is 498 × --cs-u wide and
       59 × --cs-u + 420 × --cs-uz tall — 498 × 479 design px on desktop. The beams make it
       mostly depth. Re-run the script after any change to the stack's geometry, light or
       timeline, and paste what it prints here; never guess these numbers.
       - --cs-u (the sheets) is the largest unit at which that fits the row's width less a 16px
         gutter each side, and its height less 32px at each end for the Sunlight and Your cabin
         labels; capped so tablets stay near desktop size.
       - --cs-uz (the depth: gaps, lift, beams and perspective) then spends the height the
         sheets can't use, which a tall, narrow phone has plenty of: up to 1.5 × --cs-u.
       - The box moves so that the composition, not the box, is centred (fitted from the same
         sweep: depth grows the composition upward, from the light down). */
    .is-live .cs-iso {
      --cs-u: min((100cqw - 32px) / 498, (100cqh - 64px) / 479, 1.25px);
      --cs-uz: min(1.5 * var(--cs-u), (100cqh - 64px - 59 * var(--cs-u)) / 420);
      top: calc(-29 * var(--cs-u) + 139 * var(--cs-uz));
      width: calc(300 * var(--cs-u));
    }

    /* Too little room to draw the stack legibly: leave just the captions. */
    @container (max-height: 120px) {
      .cs-edge,
      .cs-iso {
        visibility: hidden;
      }
    }
  }

  /* The fitted offset centres the open stack, so the closed stack of the intro and outro would
     rest low, just above Your cabin. Closing the sheets from 32 to 4 drops the stack's centre by
     28 × 2.2 (2.5 layers × sin 62°); this lifts it back by the same amount. The lift starts
     below a gap of 23, the tightest the adhesive step's heat cycles go (32 ± 9 in
     js/comfort-science.js), so that step still breathes from the cabin side as on desktop:
     61.6 / (23 − 4) ≈ 3.24 per unit of gap. */
  .is-live .cs-stack {
    translate: 0 calc(min(var(--gap) - 23, 0) * 3.24 * var(--cs-uz));
  }

  .is-live .cs-edge-top {
    top: 8px;
  }

  .is-live .cs-edge-bottom {
    bottom: 8px;
  }

  .is-live .cs-copy {
    align-items: start;
    /* The segments are now in flow under the card, so only the fixed mobile Get Instant Quote /
       Call bar itself (--cs-bottom) needs clearing. */
    padding: 0 7vw calc(var(--cs-bottom) + 10px);
  }

  /* The captions already share one grid cell (the shared rules above). Phones add a fixed top
     offset, so the gap to the "Your cabin" label above stays constant and content-independent
     at every step. */
  .is-live .cs-cap {
    position: relative;
    inset: auto;
    justify-content: flex-start;
    padding-top: 16px;
  }

  /* A compact type scale so the tallest caption (layer 4: kicker, stat, headline, paragraph)
     fits above the index on short phones without its text running into it (round 3). Desktop's
     sizes were never designed for this little vertical room. Floor: paragraph text stays at
     0.95rem (the readability minimum), and the headline (clamp's floor drops from 2.2rem to
     1.7rem) stays clearly larger than it. */
  .is-live .cs-cap .kicker {
    font-size: 0.6rem;
  }

  .is-live .cs-cap h2,
  .is-live .cs-cap h3 {
    margin: 9px 0 7px;
    font-size: clamp(1.7rem, 3.8vw, 3.4rem);
    line-height: 0.94;
  }

  .is-live .cs-cap p {
    font-size: 0.95rem;
    line-height: 1.3;
  }

  .is-live .cs-cap .cs-stat {
    margin-top: 7px;
    font-size: clamp(2.4rem, 7vw, 6rem);
  }

  .is-live .cs-cta {
    margin-top: 13px;
  }

  .is-live .cs-steps {
    width: 100%;
    margin-top: 10px;
  }
}

/* Phones held landscape: the pinned frame is too short for the stack (the container query
   hides it) or the progress line, and the progress line ran over the captions and Build my
   quote, so a tap on the link jumped back to a layer. Drop it there and give its room, and the
   gap kept for the hidden Your cabin label, to the captions. */
@media (max-width: 820px) and (max-height: 420px) {
  .is-live .cs-steps {
    display: none;
  }

  .is-live .cs-copy {
    padding-bottom: calc(var(--cs-bottom) + 12px);
  }

  .is-live .cs-cap {
    padding-top: 4px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cs-ly,
  .cs-cap,
  .cs-steps button {
    transition: none;
  }
}

/* Phones: the stack sits at the top, so the light and the lit grid follow it there; the bottom
   ruler goes, and the left one shrinks to a slim strip against the edge, clear of the captions. */
@media (max-width: 820px) {
  .is-live .cs-pin {
    background: radial-gradient(70% 70% at 30% 40%, #191922 0%, var(--bg) 70%);
    background:
      radial-gradient(70% 34% at 50% 28%, color-mix(in srgb, var(--cs-glow) 20%, transparent), transparent 72%),
      radial-gradient(70% 70% at 30% 40%, #191922 0%, var(--bg) 70%);
  }

  .is-live .cs-bd-grid {
    -webkit-mask: radial-gradient(90% 60% at 50% 30%, #000 30%, transparent 90%);
    mask: radial-gradient(90% 60% at 50% 30%, #000 30%, transparent 90%);
  }

  .is-live .cs-bd-lit {
    -webkit-mask: radial-gradient(60% 26% at 50% 28%, rgba(0, 0, 0, 0.9), transparent 75%);
    mask: radial-gradient(60% 26% at 50% 28%, rgba(0, 0, 0, 0.9), transparent 75%);
  }

  .is-live .cs-bd-corner:nth-of-type(1) {
    top: 14px;
    left: 14px;
  }

  .is-live .cs-bd-corner:nth-of-type(2) {
    top: 14px;
    right: 14px;
  }

  .is-live .cs-bd-corner:nth-of-type(3) {
    bottom: 14px;
    left: 14px;
  }

  .is-live .cs-bd-corner:nth-of-type(4) {
    right: 14px;
    bottom: 14px;
  }

  .is-live .cs-bd-ruler-x {
    display: none;
  }

  .is-live .cs-bd-ruler-y {
    top: 90px;
    bottom: 90px;
    left: 4px;
    width: 8px;
    background:
      repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.26) 0 1px, transparent 1px 12px) left / 4px 100% no-repeat,
      repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.36) 0 1px, transparent 1px 60px) left / 8px 100% no-repeat;
  }

  .is-live .cs-bd-ruler-y > i::before {
    top: -1px;
    left: 0;
    width: 8px;
    height: 2px;
    border: 0;
    background: var(--red);
  }

  .is-live .cs-bd-ripple {
    width: 300px;
    height: 300px;
    margin: -150px 0 0 -150px;
  }

}
