/* Pass — landing page
   ------------------------------------------------------------------
   Two presentations, one set of content. `html.operator` strips motion
   and tightens density; everything else is identical. That toggle is
   what earns the right to make the default version indulgent.

   No framework, no build step, no CDN. A page whose entire argument is
   reliability should not depend on somebody else's server to render. */

/* ══ EVERY COLOUR ON THE PAGE IS DEFINED HERE ══════════════════════════════
   Nothing below this block may contain a literal colour. If a rule needs a
   shade that does not exist yet, add a token — do not inline a hex, because
   that is how the last palette ended up with thirty-six separate greens
   scattered across five hundred lines and impossible to change as a set.
   (Two deliberate exceptions, both marked where they appear: the `#000` inside
   `mask-image` gradients, which is opacity maths and not a colour, and the
   `rgba(0,0,0,…)` ramps in the hero scrim, which are neutral density over
   footage.)

   The palette is taken from the hero footage: a gas ring in a dark kitchen.
   Black, off-white, the ORANGE of the ignition flare, and the BLUE of the lit
   ring. There is no green anywhere — the page used to be forest green, which
   fought both the steel and the flame the moment real footage went in.

   Naming: `--ink*` is text on light, `--on-dark*` is text on dark, `--coal*`
   is a dark background, `--panel*` is a raised surface *on* dark (the iPad
   mock and the network diagram live on these), `--flame*` is the orange and
   `--gas*` is the blue. */
:root {
  /* ── light ground ─────────────────────────────────────────────── */
  --paper:   #faf7f2;   /* off-white, never pure white */
  --card:    #ffffff;
  --ink:     #14120f;
  --ink-2:   #55504a;
  --ink-3:   #8b847b;
  --line:    #e5dfd5;
  --hair:    #f0ece3;

  /* ── dark ground ──────────────────────────────────────────────── */
  /* Channels, not a hex, because two gradients need this colour with an alpha
     and CSS cannot apply one to a hex custom property. --coal is built from the
     same three numbers, so there is still exactly one place to change it. */
  --coal-rgb:  11 13 10;
  --coal:      rgb(var(--coal-rgb));   /* the dark sections */
  --hero-bg:   #070906;  /* the hero, darkest — the footage arrives on this,
                            so it must match the clip's own black */
  --on-dark:   #ffffff;
  --on-dark-2: #c9c6c1;
  --on-dark-3: #8e8a85;

  /* ── raised surfaces on dark, three ascending steps ───────────── */
  --panel:      #101310;  /* the device body */
  --panel-2:    #171b17;  /* its screen */
  --panel-3:    #1e231e;  /* a ticket on the screen */
  --panel-line: #2b322c;
  --panel-line-2: #3b433d; /* hover, and anything that must read as raised */

  /* ── flame: the orange of the ignition flare. The brand accent. ── */
  --flame:       #c2542a;
  --flame-2:     #e0703f;
  --flame-wash:  #fdf3ee;  /* tint behind our own row in the price table */
  --flame-flash: #fbe3d6;  /* the change-flash keyframe */
  --flame-rule:  rgba(224,112,63,.32);

  /* ── gas: the blue of the lit ring. Everything that means working. ── */
  --gas:   #4a90d9;
  --gas-2: #7fb6ea;

  /* ── shadow, edge and glass. A shadow is a colour choice too, and so is the
        white that catches an aluminium edge, so they live here with everything
        else rather than being sprinkled through the device rules. ── */
  --shadow-panel:   rgba(0,0,0,.6);   /* the cast shadow under the iPad */
  --shadow-contact: rgba(0,0,0,.45);  /* the tighter one where it meets a surface */
  --edge-line:      rgba(0,0,0,.5);   /* the 1px outline around hardware */
  --edge-inset:     rgba(0,0,0,.8);   /* recess, for the screen sitting in its bezel */
  --lens:           #000000;          /* the camera */
  --glint:          rgba(255,255,255,.10);  /* glass catching light */
  --glint-2:        rgba(255,255,255,.04);
  --glint-hair:     rgba(255,255,255,.07);  /* a machined edge */
  --glint-hair-2:   rgba(255,255,255,.05);

  /* ── status. `good` is the gas blue, because on this palette a working
        system should be the same colour as a lit ring. `warn` and `bad` stay
        warm but are kept clear of --flame so an alert never reads as a
        button. ─────────────────────────────────────────────────── */
  --good:   var(--gas);
  --warn:   #b5751f;
  --warn-2: #e2b565;   /* on dark */
  --bad:    #b2372e;
  --bad-2:  #ef8b73;   /* on dark */

  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;

  --gap: clamp(56px, 9vw, 128px);
  --pad: clamp(20px, 5vw, 64px);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* `overflow-x: clip` at the root, not just `hidden` on the body. The canvas
   handoff moves whole sections sideways by design, so they genuinely extend past
   the viewport and something has to clip them. `hidden` on the body was a safety
   net that no longer covers it — and `hidden` here would turn html into a scroll
   container, which breaks `position: sticky` on the nav. `clip` clips without
   creating one. */
html { -webkit-text-size-adjust: 100%; overflow-x: clip; }


body {
  font-family: var(--sans);
  background: var(--paper);
  color: var(--ink);
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  /* NOT `overflow-x: hidden` here. `hidden` on one axis computes the other to
     `auto`, which makes the body a scroll container — and `position: sticky`
     then resolves against that box instead of the viewport, so every sticky
     stage silently stops sticking and scrolls away with the page. The root's
     `overflow-x: clip` already does the clipping without creating one. */
}

.wrap { width: min(1180px, 100%); margin-inline: auto; padding-inline: var(--pad); }

h1, h2, h3 { line-height: 1.05; letter-spacing: -0.02em; font-weight: 600; }
h1 { font-size: clamp(44px, 8.5vw, 104px); letter-spacing: -0.035em; }
h2 { font-size: clamp(30px, 4.6vw, 54px); }
h3 { font-size: clamp(19px, 2vw, 23px); letter-spacing: -0.01em; }
p  { color: var(--ink-2); }
a  { color: inherit; }

.eyebrow {
  font-size: 12px; letter-spacing: 0.17em; text-transform: uppercase;
  font-weight: 600; color: var(--ink-3);
}
.lede { font-size: clamp(18px, 2.1vw, 23px); line-height: 1.45; max-width: min(100%, 34ch); }
.muted { color: var(--ink-3); }
.num { font-variant-numeric: tabular-nums; }

/* ── motion, and the switch that removes it ─────────────────────────── */
/* No transition here on purpose. The timeline writes opacity and transform
   inline on every frame, and a CSS transition on a property being written 60
   times a second does not smooth it — it lags behind it, and the two curves
   beat against each other. Everything is visible by default, so a page whose
   script never runs is a readable page. */
.reveal { opacity: 1; transform: none; }
html.operator .reveal { opacity: 1 !important; transform: none !important; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  html { scroll-behavior: auto; }
}

/* ── #breaks and #the-pass: two sections, not one ────────────────────────
   These were briefly a single sticky stage with the question, the diagram and
   the iPad as beats sharing one chip rail. That was wrong twice over: the
   instruction ("pick the one that has already cost you a service") sat on a
   different scroll tick from the buttons it referred to, and the iPad still had
   the failure buttons under it three beats later, which made showing off the
   app and breaking the network read as one confused section.

   They are ordinary sections now. #breaks is one composition — read, press,
   watch the diagram answer. #the-pass is the app, on its own, with nothing to
   operate. */
#breaks .head p { max-width: min(100%, 46ch); }
#breaks .picker { margin-top: clamp(20px, 3vh, 34px); }
#breaks .net-wrap { width: min(100%, 860px); margin: clamp(26px, 5vh, 56px) auto 0; }
#breaks .outcome { margin: clamp(22px, 3.4vh, 40px) auto 0; max-width: 64ch; }

/* ── #the-pass: six screens on one device ────────────────────────────────
   The device is fixed; the screens slide horizontally underneath it, driven by
   the scroll. Every view is live DOM rather than a screenshot, so the clocks in
   the pass view keep ticking while you are three screens away from it. */
/* The whole composition has to fit one screen, because the section holds the
   screen while you scroll through the six views. Everything here is sized
   against the viewport for that reason — head, device and rail together, not
   the device on its own. */
#the-pass { padding: 0; display: flex; align-items: center; min-height: 100svh; }
#the-pass .head { text-align: center; margin-inline: auto; }
#the-pass .head h2 { font-size: clamp(26px, 3.2vw, 40px); max-width: 20ch; margin-inline: auto; }
#the-pass .head p  { max-width: 54ch; margin-inline: auto; margin-top: 12px;
                     font-size: clamp(15px, 1.15vw, 17px); }
#the-pass .device { margin: clamp(22px, 3.6vh, 44px) auto 0; max-width: min(100%, 720px); }
#the-pass .screen { overflow: hidden; }

/* The filmstrip. Each view is exactly one screen wide, so the transform the
   timeline writes is simply `-index × 100%` and no measurement is involved. */
.views { display: flex; will-change: transform; }
.view {
  flex: 0 0 100%; min-width: 100%;
  height: clamp(260px, 41vh, 390px);
  padding: 12px; overflow: hidden;
}
/* The pass view owns the tickets grid it already had, tightened to the
   filmstrip's frame — the standalone version was sized for a section of its
   own and overflowed this one. */
.view .tickets { padding: 0; height: 100%; grid-template-rows: 1fr 1fr; gap: 4px; }
.view .ticket { padding: 7px 9px; }
.view .ticket .top { margin-bottom: 3px; padding-bottom: 5px; }
/* The table number and cover count are two inline spans in a div, which wraps
   to three lines at the width a ticket gets inside the filmstrip — 68px of
   header on a 118px row. One line, always. */
.view .ticket .top > div:first-child {
  display: flex; align-items: baseline; gap: 6px; white-space: nowrap;
}
.view .ticket li { font-size: 12px; padding: 1px 0; }
.view .ticket ul { gap: 4px; }
.view .ticket li small { font-size: 10px; }
.view .ticket .tbl { font-size: 14px; }
.view .ticket .clock { font-size: 13px; }

/* ── the floor ── */
.floor { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; height: 100%; }
.tbl {
  grid-column: var(--c); grid-row: var(--r);
  border-radius: 9px; padding: 9px 10px; display: flex; flex-direction: column;
  justify-content: center; gap: 2px; border: 1px solid var(--panel-line);
  background: var(--panel-3);
}
.tbl b { font-size: 16px; color: var(--on-dark); line-height: 1; }
.tbl span { font-size: 10px; color: var(--on-dark-3); }
.tbl.free   { background: transparent; border-style: dashed; }
.tbl.free b { color: var(--on-dark-3); }
.tbl.seated { border-color: var(--gas); }
.tbl.seated b { color: var(--gas-2); }
.tbl.bill   { border-color: var(--flame); }
.tbl.bill b { color: var(--flame-2); }
.tbl.booked { border-color: var(--panel-line-2); }

/* ── the kitchen ── */
.stations { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; height: 100%; }
.station { background: var(--panel-3); border: 1px solid var(--panel-line); border-radius: 9px; padding: 9px 10px; }
.st-head {
  display: flex; justify-content: space-between; align-items: center;
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--on-dark-2); border-bottom: 1px solid var(--panel-line);
  padding-bottom: 6px; margin-bottom: 7px;
}
.st-head span { color: var(--gas-2); font-variant-numeric: tabular-nums; }
.station ul { list-style: none; display: grid; gap: 5px; }
.station li { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--on-dark-2); }
.station li em { font-style: normal; color: var(--on-dark-3); font-size: 11px; }
.station li.late span { color: var(--bad-2); }

/* ── taking an order ── */
.order { height: 100%; display: flex; flex-direction: column;
         background: var(--panel-3); border: 1px solid var(--panel-line); border-radius: 9px; }
.ord-head { display: flex; justify-content: space-between; align-items: baseline;
            padding: 10px 12px; border-bottom: 1px solid var(--panel-line); }
.ord-head b { color: var(--on-dark); font-size: 15px; }
.ord-head span { font-size: 11px; color: var(--on-dark-3); }
.ord-list { list-style: none; flex: 1; min-height: 0; overflow: hidden;
            padding: 6px 12px; display: grid; gap: 2px; align-content: start; }
.ord-list li { display: flex; justify-content: space-between; gap: 10px;
               padding: 7px 0; border-bottom: 1px solid var(--panel-line);
               font-size: 13px; color: var(--on-dark-2); }
.ord-list li small { display: block; font-size: 10px; color: var(--flame-2); }
.ord-list li em { font-style: normal; color: var(--on-dark); font-variant-numeric: tabular-nums; }
.ord-foot { display: flex; justify-content: space-between; padding: 10px 12px;
            border-top: 1px solid var(--panel-line); font-size: 12px; color: var(--on-dark-3); }
.ord-foot b { color: var(--on-dark); font-size: 16px; font-variant-numeric: tabular-nums; }

/* ── the menu ── */
.menu { height: 100%; overflow: hidden; }
.m-sec { font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
         color: var(--on-dark-3); margin: 6px 0 4px; }
.m-sec:first-child { margin-top: 0; }
.m-row { display: flex; justify-content: space-between; gap: 12px;
         padding: 4px 0; border-bottom: 1px solid var(--panel-line);
         font-size: 13px; color: var(--on-dark-2); }
.m-row em { font-style: normal; color: var(--on-dark); font-variant-numeric: tabular-nums; }
.m-row.off span { color: var(--on-dark-3); text-decoration: line-through; }
.m-row.off em { color: var(--bad-2); font-size: 11px; text-decoration: none; }

/* ── allergens ── */
.alg { height: 100%; }
.alg-row { display: grid; grid-template-columns: 1fr repeat(5, 26px); gap: 4px;
           align-items: center; padding: 6px 0; border-bottom: 1px solid var(--panel-line);
           font-size: 12px; color: var(--on-dark-2); }
.alg-row i { font-style: normal; text-align: center; color: var(--panel-line-2); font-size: 11px; }
.alg-row i.y { color: var(--gas-2); }
.alg-row i.q { color: var(--warn-2); }
.alg-head { color: var(--on-dark-3); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; }
.alg-row.ask span { color: var(--warn-2); }

/* ── which screen you are on ── */
.viewbar { max-width: min(100%, 760px); margin: clamp(16px, 2.4vh, 26px) auto 0; }
.viewname { font-size: 13px; letter-spacing: .16em; text-transform: uppercase;
            font-weight: 600; color: var(--on-dark-2); text-align: center; }
.viewrail { display: flex; gap: 5px; margin-top: 10px; }
.viewrail i { flex: 1; height: 2px; border-radius: 1px; background: var(--panel-line); position: relative; overflow: hidden; }
.viewrail i b { position: absolute; inset: 0; background: var(--flame);
                transform: scaleX(0); transform-origin: left; display: block; }

@media (max-width: 899px) {
  .view { height: clamp(260px, 54vh, 420px); }
  .floor, .stations { grid-template-columns: repeat(2, 1fr); }
}


/* ── sticky stages ───────────────────────────────────────────────────────
   Two sections hold the screen while the reader scrolls through them: the hero
   and the burner. This used to be ScrollTrigger's `pin`, which fixes the
   element and injects a spacer to replace the height it stopped occupying —
   scroll distance with nothing drawn in it, which is where the long blank
   stretches came from.

   `position: sticky` has no spacer. The wrapper's height IS the beat's length,
   set by the timeline in app.js from the same `SPAN` number the timeline reads,
   and the sticky child simply stays put inside it. */
.stage-wrap { position: relative; }
.stage-wrap > .hero,
.stage-wrap > section { position: sticky; top: 0; min-height: 100svh; }
/* A sticky child must be at least a full screen tall, or the next section shows
   underneath it — the band-of-white the pins had. That is the `min-height`
   above; nothing here sets `overflow`, because anything other than `visible` on
   an ancestor changes what `sticky` resolves against. */

/* Without a script the wrapper has no inline height, so these are ordinary
   full-height sections stacked down the page and everything still reads. */
html:not(.js-anim) .stage-wrap > .hero,
html:not(.js-anim) .stage-wrap > section { position: static; }
html.operator .stage-wrap { height: auto !important; }
html.operator .stage-wrap > .hero,
html.operator .stage-wrap > section { position: static; }


/* Operator view: a filmstrip is meaningless without the scroll that drives it,
   so the six screens stack and are all readable at once — which is the whole
   promise of this view. Each one labels itself from the attribute the timeline
   otherwise reads. */
html.operator #the-pass { min-height: 0; display: block; padding: var(--gap) 0; }
html.operator .views { display: block; transform: none !important; }
html.operator .view { width: 100%; height: auto; padding: 14px 12px; }
html.operator .view + .view { border-top: 1px solid var(--panel-line); }
html.operator .view::before {
  content: attr(data-view); display: block; margin-bottom: 10px;
  font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--on-dark-3);
}
html.operator .view .tickets { height: auto; }
html.operator .viewbar { display: none; }


/* ── section visuals ─────────────────────────────────────────────────────
   A scrubbed still sequence beside the HEADING, with the section's real content
   — price rows, cards, tables — spanning the full measure underneath.

   Two earlier attempts were wrong, and both were visible as emptiness:

   1. Picture centred in a full-height column. The content column is far taller
      than the picture (1225px of price rows against a 550px panel), so
      centring left the difference as dead space above and below it — 440-740px
      of nothing per section.
   2. Picture sticky down the side. That removed the void, but it forced every
      section into two columns for its whole height, which squeezed the three
      proof cards into a 120px-wide gutter each.

   Beside the heading is the answer: a heading and a picture are about the same
   height, so there is nothing left over, and the wide content keeps its full
   measure. */
section:has(> .wrap > .shot) > .wrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
  gap: clamp(24px, 3.5vw, 56px) clamp(28px, 5vw, 72px);
  align-items: center;
}
/* The heading sits in the first cell, the picture in the second, and anything
   else drops below and spans both. */
section:has(> .wrap > .shot) > .wrap > .head { grid-column: 1; grid-row: 1; }
section:has(> .wrap > .shot) > .wrap > .shot { grid-column: 2; grid-row: 1; }
section:has(> .wrap > .shot) > .wrap > *:not(.head):not(.shot) { grid-column: 1 / -1; }

.shot {
  width: 100%;
  /* Sized to the heading beside it rather than to the footage's own aspect —
     `object-fit: cover` crops the frame instead of letting a tall clip set the
     row height and reopen the gap. */
  height: clamp(240px, 32vh, 360px);
  object-fit: cover;
  display: block;
  border-radius: 14px; overflow: hidden;
  background: var(--coal);
  box-shadow: 0 26px 54px -30px var(--shadow-panel);
  will-change: transform;
}
/* Alternate the side, so four sections in a row do not read as a template. */
#room-section > .wrap, #proof-section > .wrap {
  grid-template-columns: minmax(0, 0.78fr) minmax(0, 1fr);
}
#room-section > .wrap > .head, #proof-section > .wrap > .head { grid-column: 2; }
#room-section > .wrap > .shot, #proof-section > .wrap > .shot { grid-column: 1; }

@media (max-width: 899px) {
  section:has(> .wrap > .shot) > .wrap,
  #room-section > .wrap, #proof-section > .wrap { grid-template-columns: minmax(0,1fr); }
  section:has(> .wrap > .shot) > .wrap > .head,
  section:has(> .wrap > .shot) > .wrap > .shot,
  #room-section > .wrap > .head, #proof-section > .wrap > .head,
  #room-section > .wrap > .shot, #proof-section > .wrap > .shot { grid-column: 1; }
  section:has(> .wrap > .shot) > .wrap > .shot { grid-row: 1; }
  section:has(> .wrap > .shot) > .wrap > .head { grid-row: 2; }
  .shot { height: clamp(200px, 30vh, 300px); }
}
html.operator .shot { display: none; }

/* ── nav ────────────────────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 50;
  /* A more opaque ground lets the blur be smaller for the same frosted look.
     This is sticky, so the backdrop is re-sampled on every scroll frame and
     the blur radius is most of the cost — the saturate bought almost nothing
     visible through 92% paper and is gone. */
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid transparent;
  transition: border-color .3s ease;
}
.nav.stuck { border-bottom-color: var(--line); }
.nav .wrap { display: flex; align-items: center; gap: 16px; height: 66px; }
.brand { font-family: var(--serif); font-size: clamp(19px, 2.4vw, 25px); letter-spacing: -0.02em; text-decoration: none; white-space: nowrap; }
.brand i { color: var(--flame); font-style: normal; }
.nav .spacer { flex: 1; }

.btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 11px 18px; border-radius: 999px; border: 1px solid var(--ink);
  background: var(--ink); color: var(--paper);
  font-size: 15px; font-weight: 550; text-decoration: none; cursor: pointer;
  transition: transform .18s ease, background .18s ease;
}
.btn:hover { transform: translateY(-1px); }
.btn.ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn.ember { background: var(--flame); border-color: var(--flame); color: var(--on-dark); }
.btn.sm { padding: 8px 14px; font-size: 14px; }
html.operator .btn { transition: none; }
html.operator .btn:hover { transform: none; }

.viewtoggle {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; color: var(--ink-3); background: transparent;
  border: 1px solid var(--line); border-radius: 999px; padding: 7px 13px;
  cursor: pointer; font-family: inherit;
}
.viewtoggle .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); }
html.operator .viewtoggle { border-color: var(--ink); color: var(--ink); }
html.operator .viewtoggle .dot { background: var(--good); }

/* ── hero ───────────────────────────────────────────────────────────── */
/* One step darker than --coal, which every other dark section uses. The hero is
   an unlit kitchen and it is what shows before the first frame paints, so it has
   to match the black the footage itself arrives on. */
/* A full viewport, and NOT `100svh - 67px` for the nav. Once ScrollTrigger pins
   this, it fixes the hero at the top of the viewport — the 67px the nav was
   occupying goes away, but the hero's height does not, so a 67px strip of the
   page background showed as a band of white under the shot for the whole pin.
   At full height the pinned hero fills the screen exactly. Before the pin
   engages it overhangs by the nav's height, which costs nothing: the pin starts
   at 67px of scroll.
   `svh` not `vh` so a mobile browser's collapsing toolbar cannot clip the copy. */
.hero {
  background: var(--hero-bg); color: var(--paper);
  min-height: 100svh;
  display: flex; align-items: center;
  padding: clamp(48px,7vw,88px) 0;
}
.hero .eyebrow { color: var(--on-dark-3); }
/* Bigger than the page's other h1 scale, and allowed to run wider. On a dark
   full-bleed shot the headline is the only thing competing with the flame, so it
   has to win. 11ch keeps "It doesn't / stop." breaking after the first word,
   which is what the two-line split animation expects. */
.hero h1 {
  color: var(--on-dark); margin: 18px 0 24px;
  font-size: clamp(52px, 11vw, 148px);
  letter-spacing: -0.045em;
  max-width: min(100%, 11ch);
}
.hero .lede { color: var(--on-dark-2); max-width: min(100%, 44ch); }
.hero .ctas { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
.hero .btn.ghost { color: var(--on-dark); border-color: var(--panel-line); }
/* any ghost button on a dark section — it was invisible on the closing CTA */
.dark .btn.ghost { color: var(--on-dark); border-color: var(--panel-line); }
.dark .btn.ghost:hover { border-color: var(--panel-line-2); }
/* One column. The iPad mock used to sit in the right-hand half, which put the
   most information-dense thing on the page above the fold and left nothing to
   discover by scrolling. It now lives in #breaks, next to the failure chips that
   drive it — see the note on the hero canvas in index.html. The hero is type on
   moving light and nothing else. */
.hero-grid { display: grid; grid-template-columns: minmax(0,1fr); gap: clamp(32px,5vw,64px); align-items: center; }

.assure { margin-top: 26px; font-size: 14px; color: var(--on-dark-3); display: flex; gap: 18px; flex-wrap: wrap; }
.assure span { display: flex; align-items: center; gap: 7px; }
.assure .tick { color: var(--flame-2); }

/* ── the pass mock ──────────────────────────────────────────────────── */
/* It used to be constrained by sitting in one half of the hero's two-column
   grid. Standing on its own in #breaks it needs its own ceiling, or an iPad mock
   stretches to the full measure and stops reading as a device. */
/* An actual iPad, not a rounded box. The screen inside has to stay live DOM —
   the clocks tick, the status dot follows the failure chips — so this cannot be
   a flat photograph; it is a device frame built around real content.
   What makes it read as hardware rather than a card: a thick, even bezel (a real
   iPad's is ~10mm on every side, and getting that wrong is the main tell), an
   aluminium edge caught by a light gradient, the front camera, a diagonal sheet
   of glass glare across the display, and a shadow soft enough to sit on a
   surface rather than float. */
.device {
  position: relative;
  background:
    linear-gradient(148deg, var(--panel-line-2) 0%, var(--panel) 26%,
                    var(--panel) 74%, var(--panel-line) 100%);
  border-radius: 26px;
  padding: 26px 20px;
  box-shadow:
    0 1px 0 var(--glint-hair) inset,          /* the machined top edge */
    0 0 0 1px var(--edge-line),
    0 34px 70px -28px var(--shadow-panel),
    0 6px 18px -8px var(--shadow-contact);
  max-width: 620px; margin: 34px auto 0;
}
/* The front camera, centred in the top bezel. Small, and the reason the top
   padding is deeper than the sides. */
.device::before {
  content: ""; position: absolute; top: 11px; left: 50%;
  width: 6px; height: 6px; margin-left: -3px; border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, var(--panel-line-2), var(--lens) 70%);
  box-shadow: 0 0 0 1px var(--glint-hair-2);
}
/* A sheet of glass over the whole face. `pointer-events: none` so it never eats
   a click, and it sits above the screen but below nothing else. */
.device::after {
  content: ""; position: absolute; inset: 0; border-radius: 26px;
  pointer-events: none;
  background: linear-gradient(126deg,
    var(--glint) 0%, var(--glint-2) 18%,
    transparent 42%, transparent 100%);
}
.screen {
  background: var(--panel-2); border-radius: 8px; overflow: hidden;
  box-shadow: 0 0 0 1px var(--shadow-panel), 0 2px 10px -4px var(--edge-inset) inset;
}
.screen-bar {
  display: flex; justify-content: space-between; align-items: center;
  padding: 9px 13px; border-bottom: 1px solid var(--panel-line);
  font-size: 11px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--on-dark-3);
}
.status { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; }
.status .led { width: 8px; height: 8px; border-radius: 50%; background: var(--gas); transition: background .5s ease; }
.status[data-state="lan"]   .led { background: var(--warn-2); }
.status[data-state="mesh"]  .led { background: var(--warn-2); }
.status[data-state="alone"] .led { background: var(--warn-2); }
.status[data-state="vendor"] .led { background: var(--warn-2); }
.status[data-state="lan"], .status[data-state="mesh"],
.status[data-state="alone"], .status[data-state="vendor"] { color: var(--warn-2); }

.tickets { display: grid; grid-template-columns: minmax(0,1fr); gap: 9px; padding: 11px; }
@media (min-width: 520px) { .tickets { grid-template-columns: repeat(2, minmax(0,1fr)); } }
.ticket { background: var(--panel-3); border: 1px solid var(--panel-line); border-radius: 9px; padding: 10px 11px; }
.ticket .top { display: flex; justify-content: space-between; align-items: baseline; border-bottom: 1px solid var(--panel-line); padding-bottom: 7px; margin-bottom: 8px; }
.ticket .tbl { font-size: 18px; font-weight: 700; color: var(--on-dark); }
.ticket .cv { font-size: 11px; color: var(--on-dark-3); }
.ticket .clock { font-variant-numeric: tabular-nums; font-weight: 650; color: var(--gas-2); font-size: 15px; }
.ticket.warm .clock { color: var(--warn-2); }
.ticket.hot .clock { color: var(--bad-2); }
.ticket ul { list-style: none; display: grid; gap: 6px; font-size: 13px; color: var(--on-dark-2); }
.ticket li { display: flex; justify-content: space-between; gap: 9px; }
.ticket li small { color: var(--on-dark-3); font-size: 11px; display: block; }
.ticket li .go { font-size: 10px; letter-spacing: .06em; border: 1px solid var(--panel-line-2); border-radius: 4px; padding: 2px 7px; color: var(--gas-2); }

.screen-foot { border-top: 1px solid var(--panel-line); padding: 9px 13px; font-size: 12px; color: var(--on-dark-3); display: flex; justify-content: space-between; gap: 12px; }
.screen-foot b { color: var(--on-dark-2); font-weight: 600; }

/* ── generic section ────────────────────────────────────────────────── */
section { padding: var(--gap) 0; }
/* .tight used to be a second, shorter rhythm. Two spacing scales on one
   page read as inconsistency, not as variety, so it is the same rhythm now
   and the class is kept only because markup and other rules reference it. */
section.tight { padding: var(--gap) 0; }
.dark { background: var(--coal); color: var(--paper); }
.dark h2, .dark h3 { color: var(--on-dark); }
.dark p { color: var(--on-dark-2); }
.dark .eyebrow { color: var(--on-dark-3); }

.head h2 { max-width: min(100%, 22ch); }
.head p { margin-top: 16px; max-width: min(100%, 52ch); }

/* ── failure picker ─────────────────────────────────────────────────── */
.picker { display: flex; flex-wrap: wrap; gap: 9px; margin: 26px 0 0; }
.chip {
  border: 1px solid var(--panel-line); background: transparent; color: var(--on-dark-2);
  border-radius: 999px; padding: 10px 17px; font: inherit; font-size: 15px;
  cursor: pointer; transition: background .2s ease, border-color .2s ease;
}
.chip:hover { border-color: var(--panel-line-2); }
.chip[aria-pressed="true"] { background: var(--flame); border-color: var(--flame); color: var(--on-dark); }
html.operator .chip { transition: none; }

.outcome {
  margin-top: 22px; border-left: 3px solid var(--flame); padding: 4px 0 4px 18px;
  min-height: 84px;
}
.outcome h3 { font-size: 20px; color: var(--on-dark); }
.outcome p { margin-top: 7px; max-width: min(100%, 48ch); }

/* ── the stack you're replacing ─────────────────────────────────────── */
.stack { display: grid; gap: 10px; margin-top: 30px; }
.row {
  display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 14px; align-items: baseline;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  padding: 15px 18px;
}
.row .what { font-weight: 550; }
.row .note { display: block; font-size: 13.5px; color: var(--ink-3); font-weight: 400; margin-top: 2px; }
.row .cost { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--ink-2); }
.row.total { background: var(--coal); border-color: var(--coal); color: var(--on-dark); }
.row.total .what, .row.total .cost { color: var(--on-dark); font-weight: 650; }
.row.ours { border-color: var(--flame); border-width: 1.5px; background: var(--flame-wash); }
.row.ours .cost { color: var(--flame); font-weight: 650; }

/* ── matrix ─────────────────────────────────────────────────────────── */
.matrix-wrap { overflow-x: auto; margin-top: 26px; border: 1px solid var(--line); border-radius: 12px; background: var(--card); }
table.matrix { border-collapse: collapse; width: 100%; font-size: 14px; }
table.matrix th, table.matrix td { padding: 11px 12px; text-align: left; border-bottom: 1px solid var(--hair); white-space: nowrap; }
table.matrix thead th { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3); font-weight: 600; }
table.matrix tbody tr:last-child td { border-bottom: 0; }
table.matrix td.mark { text-align: center; font-weight: 700; color: var(--ink); width: 62px; }
table.matrix td.mark.on { color: var(--flame); }
table.matrix td.mark.unknown { color: var(--warn); }
/* Names the ingredient holding the answer up, rather than leaving a wall of
   question marks with nothing to act on. */
.asking {
  margin-top: 14px; font-size: 14.5px; font-weight: 550;
  color: var(--warn); max-width: 60ch;
}
.asking[hidden] { display: none; }
.changed { animation: flash 1.1s ease; }
@keyframes flash { 0% { background: var(--flame-flash); } 100% { background: transparent; } }
html.operator .changed { animation: none; }

.control { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 24px; font-size: 15px; }
.control select {
  font: inherit; padding: 9px 13px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--card); color: var(--ink);
  max-width: 100%;            /* a long <option> will otherwise widen the page */
  text-overflow: ellipsis;
}
/* Belt and braces: nothing in flow may exceed the viewport. Cheap insurance
   against the next long word, wide table or unbreakable string. */
img, svg, video, table, pre, select, input, textarea { max-width: 100%; }

/* ── permissions ────────────────────────────────────────────────────── */
.perm { display: grid; gap: 8px; margin-top: 26px; max-width: 620px; }
.perm-row { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 14px; align-items: center; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 13px 16px; }
.tri { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.tri button { font: inherit; font-size: 13px; padding: 6px 13px; border: 0; background: transparent; color: var(--ink-3); cursor: pointer; }
.tri button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }
.tri button[aria-pressed="true"][data-v="pin"] { background: var(--warn); color: var(--on-dark); }
.tri button[aria-pressed="true"][data-v="yes"] { background: var(--good); color: var(--on-dark); }

/* ── pricing ────────────────────────────────────────────────────────── */
.price-grid { display: grid; gap: 16px; margin-top: 32px; }
@media (min-width: 860px) { .price-grid { grid-template-columns: minmax(0,1.15fr) minmax(0,1fr); } }
.price-card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: clamp(22px,3vw,34px); }
.price-card.lead { border-color: var(--ink); }
.price-big { font-size: clamp(40px,6vw,60px); font-weight: 650; letter-spacing: -0.035em; line-height: 1; }
.price-big span { font-size: 18px; font-weight: 500; color: var(--ink-3); letter-spacing: 0; }
.price-card ul { list-style: none; display: grid; gap: 9px; margin-top: 20px; font-size: 15.5px; }
.price-card li { display: flex; gap: 10px; color: var(--ink-2); }
.price-card li b { color: var(--ink); font-weight: 600; }
.tickmark { color: var(--flame); flex: none; }

/* ── proof ──────────────────────────────────────────────────────────── */
.proof { display: grid; gap: 16px; margin-top: 32px; }
@media (min-width: 860px) { .proof { grid-template-columns: repeat(3, minmax(0,1fr)); } }
.proof .card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 24px; }
.proof .card h3 { margin-bottom: 9px; }
.proof .card p { font-size: 15.5px; }
/* `.terminal` used to live here — three rules and five colours for an element
   that does not exist anywhere in index.html. Removed while auditing the
   palette: dead CSS is worse than clutter, because it makes a colour look load
   bearing when nothing renders it. */

/* ── cta / footer ───────────────────────────────────────────────────── */
.cta { text-align: center; }
.cta h2 { max-width: min(100%, 18ch); margin-inline: auto; }
.cta p { margin: 18px auto 0; max-width: min(100%, 44ch); }
.cta .ctas { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 30px; }

footer { border-top: 1px solid var(--line); padding: 40px 0; font-size: 14px; color: var(--ink-3); }
footer .wrap { display: flex; flex-wrap: wrap; gap: 16px; justify-content: space-between; }

/* operator view: less air, no decoration */
html.operator section { padding: clamp(34px,5vw,54px) 0; }
/* min-height and the flex centring are undone too, not just the padding —
   operator view exists to fit everything in about twenty seconds, and a
   full-viewport hero with nothing in it but a headline is the opposite of that. */
html.operator .hero { padding: 44px 0 40px; min-height: 0; display: block; }
html.operator h1 { font-size: clamp(38px,6vw,60px); }
html.operator .hero h1 { font-size: clamp(38px,6vw,60px); letter-spacing: -0.02em; }
html.operator .device { box-shadow: none; }


/* ══ the network, drawn and rerouting ══════════════════════════════════
   The mesh is the product's least believable claim, so it gets drawn
   rather than asserted. Links animate themselves in and out as the
   failure changes; nothing here is decoration. */
/* The margin belongs to the wrapper now, not the svg: the camera frames the
   wrapper, and a margin on the child would offset the thing being measured from
   the thing being centred. */
.net-wrap { margin-top: 30px; }
.net { width: 100%; height: auto; display: block; }
.net .node rect { fill: var(--panel-2); stroke: var(--panel-line); stroke-width: 1.5; transition: all .55s cubic-bezier(.2,.7,.3,1); }
.net .node text { fill: var(--on-dark-2); font: 500 13px var(--sans); text-anchor: middle; transition: fill .55s ease; }
.net .node.pad rect { fill: var(--panel-3); }

.net .link {
  fill: none; stroke: var(--gas); stroke-width: 2; stroke-linecap: round;
  stroke-dasharray: 1 0;
  opacity: .85;
  transition: opacity .5s ease, stroke .5s ease, stroke-dasharray .5s ease;
}
.net .link.mesh { opacity: 0; stroke: var(--flame-2); stroke-width: 2.5; }

/* a dead link greys and dashes rather than vanishing — the thing is still
   there, it just isn't carrying anything */
.net[data-state="lan"]   .link.cloud { stroke: var(--on-dark-3); opacity: .35; stroke-dasharray: 5 7; }
.net[data-state="mesh"]  .link.cloud,
.net[data-state="mesh"]  .link.lan   { stroke: var(--on-dark-3); opacity: .22; stroke-dasharray: 5 7; }
.net[data-state="mesh"]  .link.mesh  { opacity: .95; }
.net[data-state="mesh"]  .node.router rect { fill: var(--panel); stroke: var(--panel-line); }
.net[data-state="mesh"]  .node.router text,
.net[data-state="mesh"]  .node.cloud text  { fill: var(--on-dark-3); }
/* Our cloud is the thing that died. Everything from the router down is
   untouched — that is the whole point of the state, so only the cloud dims. */
.net[data-state="vendor"] .link.cloud { stroke: var(--bad-2); opacity: .5; stroke-dasharray: 5 7; }
.net[data-state="vendor"] .node.cloud rect { fill: var(--panel); stroke: var(--bad-2); stroke-dasharray: 4 5; }
.net[data-state="vendor"] .node.cloud text { fill: var(--bad-2); }
.net[data-state="alone"] .link { opacity: .12; stroke: var(--on-dark-3); stroke-dasharray: 5 7; }
.net[data-state="alone"] .node:not([data-pad="0"]) rect { fill: var(--panel); stroke: var(--panel-line); }
.net[data-state="alone"] .node:not([data-pad="0"]) text { fill: var(--on-dark-3); }
.net[data-state="alone"] .node[data-pad="0"] rect { stroke: var(--flame-2); stroke-width: 2.5; }

/* the one link still doing work gets a travelling pulse */
@keyframes flow { to { stroke-dashoffset: -28; } }
.net .link.live { stroke-dasharray: 6 8; animation: flow 1.1s linear infinite; }
html.operator .net .link { transition: none; animation: none !important; }
@media (prefers-reduced-motion: reduce) { .net .link.live { animation: none; } }

/* ══ assurance strip (replaces the terminal, which spoke to the wrong reader) */
.assurance { display: grid; gap: 14px; margin-top: 28px; }
@media (min-width: 820px) { .assurance { grid-template-columns: repeat(3, minmax(0,1fr)); } }
.assurance div { border-top: 2px solid var(--line); padding-top: 14px; }
.assurance b { display: block; font-weight: 600; margin-bottom: 5px; }
.assurance span { color: var(--ink-2); font-size: 15.5px; }

/* ══ counters and hero line reveal ════════════════════════════════════ */
.line { display: block; overflow: hidden; }
.line > span { display: block; transform: translateY(105%); transition: transform .85s cubic-bezier(.16,.84,.34,1); }
.reveal.in .line > span, html.operator .line > span { transform: none; }
html.operator .line > span { transition: none; }

/* ══ the burner ════════════════════════════════════════════════════════
   A full-bleed kitchen moment that lights as you scroll into it. The flame
   is a canvas that app.js paints one still at a time, driven by scroll
   position — see `burnerSequence`. Layout, blend and mask live here;
   transform, filter and opacity belong to GSAP, so the two never fight over
   the same property. */
/* A full viewport, like the hero, and for the same reason: this section is
   pinned, and a pinned element shorter than the screen leaves a strip of the
   next section showing underneath it for the whole pin. 92vh did exactly that. */
.burner-section {
  position: relative; overflow: hidden;
  min-height: 100svh; display: flex; align-items: center;
  padding: clamp(64px,10vw,120px) 0;
}
.burner-frames {
  /* A column down the right rather than a full-bleed background. The clip is
     portrait, and stretching it across a landscape section crops the flame to
     a middle band — it stops reading as fire, which is tall and rising, and
     starts reading as an orange smear. A column keeps the proportion and
     cuts the upscale from about 4× to 2×. */
  /* Sized explicitly, not by opposing insets: a canvas is a replaced element,
     so `width: auto` resolves to its intrinsic 480px and the right/bottom
     insets are quietly ignored. */
  position: absolute; top: -6%; left: 58%; width: 50%; height: 112%;
  /* The canvas carries its intrinsic frame size in its width/height
     attributes, which makes it a replaced element — so object-fit does the
     covering, and app.js can draw each frame 1:1 without any cover arithmetic
     of its own. */
  object-fit: cover; z-index: 0;
}
@media (max-width: 780px) { .burner-frames { left: 22%; } }
.burner-section::after {                   /* keep the type readable over flame */
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(90deg, rgb(var(--coal-rgb) / .95) 0%, rgb(var(--coal-rgb) / .82) 42%, rgb(var(--coal-rgb) / .35) 100%);
}
.burner-copy { position: relative; z-index: 2; }
html.operator .burner-section { min-height: 0; padding: clamp(34px,5vw,54px) 0; }
html.operator .burner-frames { display: none; }
html.operator .burner-section::after { background: none; }

.stats { display: grid; gap: 22px; margin-top: 42px; }
@media (min-width: 780px) { .stats { grid-template-columns: repeat(3, minmax(0,1fr)); } }
.stats div { border-top: 2px solid var(--flame-rule); padding-top: 15px; }
.stats b {
  display: block; font-size: clamp(38px,5.4vw,58px); font-weight: 650;
  letter-spacing: -.035em; line-height: 1; color: var(--on-dark);
  font-variant-numeric: tabular-nums; margin-bottom: 9px;
}
.stats span { color: var(--on-dark-2); font-size: 15.5px; }

/* ══ the stack collapsing as you scroll ═══════════════════════════════ */
.stack .row.folding {
  transition: transform .9s cubic-bezier(.2,.8,.25,1), opacity .7s ease;
}
/* The fold is written per frame by the timeline — no `.folded` class, because a
   class is a latch and this has to run backwards when you scroll back up. */
html.operator .stack .row.folding { transition: none; transform: none; opacity: 1; }

/* ══ the device tilts under the cursor ════════════════════════════════ */
.device { transition: transform .35s cubic-bezier(.2,.7,.3,1); transform-style: preserve-3d; will-change: transform; }
html.operator .device { transition: none; transform: none !important; }

/* ══ real footage ══════════════════════════════════════════════════════ */
.hero { position: relative; overflow: hidden; }
/* The hook: a gas ring being lit, full-bleed, scrubbed by scroll.
   Neither sequence uses mix-blend-mode or a mask any more, and the reason is
   worth keeping: `screen` only works on footage shot against black, where it
   drops the background and leaves the flame. Both clips here are real kitchens
   with pale walls and steel in them, and screen-blending a *light* image does
   the opposite of what it sounds like — the palest area becomes the brightest
   thing on the page and the section washes out. So both are straight
   photographic layers, and the section scrims do the legibility work.
   Sized explicitly rather than by opposing insets — a canvas is a replaced
   element, so `width: auto` would resolve to its intrinsic 720px and the
   right/bottom insets would be quietly ignored. Slightly oversized so the
   scroll-driven zoom and lateral drift never expose an edge. */
.hero-frames {
  position: absolute; top: -10%; left: -6%; width: 112%; height: 120%;
  /* 62% holds the ring and its flames as a taller viewport crops the 16:9 frame
     top and bottom — the pot above it can go, the flame cannot. */
  object-fit: cover; object-position: 50% 62%; z-index: 0;
  /* Pulled towards black-and-white so the blue of the gas is the only colour in
     the hero — but NOT dimmed. An earlier version had brightness(0.92) here on
     top of a four-stop scrim, a radial and a top gradient, and the compounding
     buried the ring: the shot is already a dark kitchen, so every layer that
     "helps legibility" is spending the one thing the hero has to show.
     Fixed — never animated, see app.js. */
  filter: saturate(0.78) contrast(1.1) brightness(1.08);
  opacity: 0;                            /* app.js fades it up on load */
  will-change: opacity, transform;
}
html.operator .hero-frames { display: none; }
/* Neutral density over the footage, weighted to the left where the type is and
   lighter on the right where the ring sits. These rgba(0,0,0,…) ramps are one of
   the two deliberate exceptions to the tokens-only rule at the top of the file:
   they are not a palette choice, they are how much of the shot to hide. */
.hero-scrim {
  position: absolute; inset: 0; z-index: 1;
  background:
    /* Heavy on the left, where the headline is, and close to clear on the right,
       where the ring is. The previous version held the right side down at .50 to
       stop a gap of pale tiled wall being the brightest thing on screen — but
       that wall is a small patch and the flame is most of the frame, so it
       bought a minor tidiness at the cost of the shot. Legibility is the left
       side's job; the right side's job is to be visible. */
    linear-gradient(100deg, rgba(0,0,0,.95) 0%, rgba(0,0,0,.84) 28%, rgba(0,0,0,.30) 58%, rgba(0,0,0,.16) 100%),
    radial-gradient(115% 85% at 10% 46%, rgba(0,0,0,.45), transparent 62%);
}
/* No width override here. `.wrap` already sets `width: min(1180px, 100%)`, which
   is an explicit width, so it holds as a flex child of the now-centred hero —
   restating it as `100%` silently threw the 1180px gutter away and let the hero
   copy run the full viewport while every other section stayed inset. */
.hero .wrap { position: relative; z-index: 2; }
html.operator .hero-scrim { display: none; }

/* A flambé in a working kitchen: a pan tossed over a gas ring and the flame
   erupting up past it. Previously this was a generic fire-on-black clip, and it
   was screen-blended so the black dropped out. That clip is gone — a real
   service kitchen is specific in a way stock fire is not, and the name on this
   page is Brigadier. */
.burner-frames {
  display: block;
  /* Fixed, never animated. Scrubbing a filter on an element this size re-renders
     it every frame and is what made the page stall; app.js animates opacity and
     transform only. Warmer and more contrasted than the hero, because here the
     orange of the flame IS the accent rather than something to mute. */
  filter: saturate(1.06) contrast(1.12);
  /* Favour the lower half: the pan and the base of the flame are what read, and
     the section is far too short to show the whole portrait frame. */
  object-position: 50% 62%;
  /* Starts out. app.js lights it; if app.js never runs, the section is simply
     dark and the copy still reads — the same thing operator view does on
     purpose. */
  opacity: 0;
  /* Feathered at the left and top so it sits in the section rather than as a
     rectangle of video pasted onto it. The mask is still the right tool even
     without the blend mode — it cuts the element itself, so it works on the
     photographic layer exactly as it did on the glowing one.
     The #000 here is opacity maths, not a colour: it is the second of the two
     exceptions to the tokens-only rule at the top of the file. */
  -webkit-mask-image:
    linear-gradient(96deg, transparent 1%, rgba(0,0,0,.30) 22%, #000 54%),
    linear-gradient(to top, #000 58%, transparent 99%);
          mask-image:
    linear-gradient(96deg, transparent 1%, rgba(0,0,0,.30) 22%, #000 54%),
    linear-gradient(to top, #000 58%, transparent 99%);
  -webkit-mask-composite: source-in; mask-composite: intersect;
}
.burner-section::after {
  background:
    linear-gradient(96deg, rgb(var(--coal-rgb) / .94) 0%, rgb(var(--coal-rgb) / .78) 30%,
                    rgb(var(--coal-rgb) / .10) 58%, rgb(var(--coal-rgb) / 0) 100%);
}

/* ══ hero type, revealed line by line ═════════════════════════════════ */
/* Masked until the timeline raises them. Gated on .js-anim so that a page whose
   script never runs shows its own headline — the previous version hid these
   unconditionally, which meant any error before the animation ran left the hero
   permanently blank. */
h1[data-split] .line > span { display: block; }
html.js-anim h1[data-split] .line > span { transform: translateY(110%);
  /* No transition: the timeline writes this transform every frame, and a
     transition on top of a per-frame write is a lag, not an ease. */
  transition: none; }
html.operator h1[data-split] .line > span { transform: none; }
