/* bubbleup.gg/phone — the app's page.
 *
 * An ADDENDUM to style.css, exactly like demo.css and pricing.css: the palette,
 * the mono stack, the fixed ground (two radial glows + hairline grid), the
 * three-dot mark and the chip all come from there unchanged, so this page
 * cannot drift away from the other three by being edited on its own.
 *
 * WHY THE DOCUMENT PRIMITIVES ARE REPEATED HERE, AGAIN. `.wrap`, `.head`,
 * `.brand`, `.sec`, `.legend`, `.h` and `.copy` are now in demo.css AND
 * pricing.css AND this file, and the hoist into a shared page.css is the right
 * shape and is still not done here. pricing.css spells out why: demo.css's
 * `.wrap` carries a max-width clamp derived from 16:9 pane maths and a fixed
 * contact pill, which is that page's geometry and not a page's geometry, so the
 * hoist is a REWRITE of a walked surface with a browser gate on it — its own
 * change, on its own evidence. A third copy makes that hoist MORE worth doing,
 * not less; it is written down in the lane report rather than smuggled in here.
 * Every value below is copied from pricing.css rather than re-invented.
 *
 * NO COPY IS IN THIS FILE, and there is no `content:` string anywhere in it.
 * Every user-facing word on this page is in phone.html, authored by Fable and
 * pasted verbatim. A stylesheet that can print a word is a stylesheet that can
 * add one.
 *
 * NO PRICE, either. `tools/pricing/test` fails on a `$<digit>` anywhere under
 * `site/`, and this page no longer says anything about what the app costs at
 * all: iteration 3 deleted the lede that used to point at the Founding seat.
 * The number lives on /pricing, which fetches it from the SKU spec. */

/* style.css locks the landing page to one viewport with a physics field in it.
   This is a page. */
html,
body {
  height: auto;
  min-height: 100%;
}
body {
  overflow: visible;
}

.wrap {
  position: relative;
  /* pricing.css's reading width, unchanged — and iteration 3 spends it on
     three phone frames standing side by side, which is very nearly exactly
     what it holds (3 x 310 + 2 x 60 = 1050 of 1056 at 1920). See `--frame-h`. */
  max-width: min(1100px, 92vw);
  margin: 0 auto;
  /* The bottom was `clamp(80px, 5vw, 120px)` when the page was four screens of
     stacked sections and the reader needed to be told it had ended. The page
     now ENDS at the third caption, one screen down; what the padding still owes
     is a lane for the fixed contact pill (contact.css, 52px tall at the bottom
     right) and nothing more. That debt is now stated as a number rather than
     estimated — see the `max()` below, which is what actually pays it at every
     unit. This clamp is the READING decision that remains: how much air a page
     that ends at a caption wants under it. */
  /* FINDING 98 ROUND 2: 0 at the top, because the head is no longer in here.
     The air above it is `.head-wrap`'s now (style.css), the same
     `clamp(28px, 2.4vw, 52px)` this line used to spend, so nothing on the page
     moved by a pixel. */
  /* FINDING 101 (Kevin, walk 9, 2026-09-13): THE PILL'S LANE, as a floor under
     whatever this page's own bottom padding happens to be. The `like what you
     see?` pill is fixed to the bottom right of the VIEWPORT (contact.css), so
     it does not scroll and the page's last in-flow content scrolls under it —
     which on a wide window is how it came to sit on top of `/demo`'s
     `Add to OBS` button. `/demo` answered by taking the pill into the flow;
     every page that KEEPS it fixed answers by reserving the corner instead:
     6 x `--ui` of nothing below the last line, which is 78px at the unit's
     floor (a phone, a 1440x900 laptop) against a pill that is ~34px tall and
     18px off the bottom edge, and grows with the pill on a 4K screen where
     `--ui` is 26.11px and the flat clamp below would have stopped at its
     ceiling. `max(…)` and not a replacement: the page's own padding is a
     reading decision and wins wherever it is already the larger of the two. */
  padding: 0 22px max(clamp(58px, 3.4vw, 84px), calc(var(--ui) * 6));
}

/* ---------- the head: the mark, home, and what this page is ---------- */
.head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding-bottom: clamp(18px, 1.5vw, 30px);
  border-bottom: 1px solid var(--line);
}
/* FINDING 94: the gap, the wordmark and the mark are `style.css`'s `.brand`
   rules now — one size for this header on all four document pages, off `--ui`.
   What used to be here was a flat 10px gap, a 17px mark and a
   `clamp(19px, 1.1vw, 26px)` wordmark, which is why a 5K screen got this page's
   1280x800 header. */
.brand {
  display: inline-flex;
  align-items: center;
  color: var(--fg);
  text-decoration: none;
}
.brand:hover {
  color: #fff;
}

/* The head's right cluster: the two nav doors (`a.chip.to`, style.css) and, at
   the end of them since finding 98, the purple door into the live demo
   (`a.chip.go`). Held together so `.head`'s `space-between` has two children to
   push apart rather than four to strand — the `Beta` badge left this cluster in
   finding 98 and stands beside the wordmark now, in `.head-start`. The gap is
   `style.css`'s (finding 94): 1.2 of `--ui`, which is the flat 12px that stood
   here at the old floor — `.head`'s own 16px tightened one step, because the
   doors belong to each other more than any of them belongs to the brand across
   the rule. The wrap and the right-alignment are style.css's (finding 98). */
.head-end {
  display: flex;
  align-items: center;
}

/* ---------- sections, and the type the site already uses ---------- */
.sec {
  padding-top: clamp(34px, 2.6vw, 56px);
}
.legend {
  color: var(--fg-3);
  font-size: clamp(10px, 0.6vw, 13px);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.h {
  margin: 9px 0 10px;
  font-size: clamp(19px, 2.6vw, 27px);
  font-weight: 700;
  letter-spacing: 0.01em;
}
.copy {
  max-width: 68ch;
  color: var(--fg-2);
  font-size: clamp(13px, 0.7vw, 16px);
  line-height: 1.62;
}

/* The title block is an eyebrow and an h1 and there is nothing under it any
   more — the lede was deleted (Kevin: *"is a dumb statement we don't need"*),
   so the h1's own 10px bottom margin is the only air the frames need above
   them. `.sec.frames` supplies the rest. */
.title .h {
  margin-bottom: 0;
}

/* ---------- the frames ----------
 *
 * ITERATION 3, and the whole of what changed. Five sections of stills became
 * THREE LOOPING CLIPS and no prose. Kevin: *"just static section and image is
 * boring … maybe we just need less and so the images are more exciting for
 * what's there and to come"*. A still of a bubble field is a diagram of one;
 * every argument this page makes — bubbles growing as a burst lands, rows
 * flying, emotes animating — is an argument about MOTION, and a screenshot is
 * the one medium that cannot carry it.
 *
 * The row is three columns on a laptop and one column on a phone. There is no
 * middle layout: two-across leaves a hole where the third was, and these are
 * 0.46-aspect portrait screens, so three of them side by side is narrower than
 * two of anything else. */
.sec.frames {
  display: grid;
  /* Each column is EXACTLY its clip's width, and that is a fix rather than a
     tidy-up: with `auto` tracks the column takes its content's max-content
     width, which for two of the three is the CAPTION and not the clip — so the
     lines ran 10-30px past the frame they belong to, and the row filled the
     wrap instead of sitting under the h1. */
  grid-template-columns: repeat(3, var(--clip-w));
  /* …and left-aligned with the h1, not centred in the wrap. The type above is
     left-aligned; a row of frames indented 20px from it reads as a mistake. */
  justify-content: start;
  align-items: start;
  gap: var(--gap);
  padding-top: clamp(26px, 2.2vw, 44px);
}
.frame {
  /* A `<figure>` has a UA margin. `* { margin: 0 }` in style.css already takes
     it, and this says so on purpose so a future reset change cannot silently
     put 40px back on either side of every clip. */
  margin: 0;
  min-width: 0;
  /* Nothing is positioned against the frame any more. Iteration 6's play control
     was — it stood on the poster, and `.frame` is exactly the clip's width in
     both layouts, so `left: 50%` of it was the centre of the clip. Iteration 7
     took the control off the posters entirely (finding 86: a round purple thing
     on a field of round purple things is a bubble, not a button), and this line
     is kept because a `<figure>` that is a positioning context costs nothing and
     is what any future overlay on a clip would need. */
  position: relative;
}
.frame figcaption {
  margin-top: clamp(12px, 1.1vw, 20px);
}
.frame .h {
  /* A rung below the page's own h1, exactly as the old section headings were. */
  margin: 0 0 6px;
  font-size: clamp(16px, 1.5vw, 20px);
}
.frame .copy {
  /* The caption is as wide as the frame it captions and no wider. `68ch` is a
     reading measure for a paragraph, and these are two wrapped lines under a
     276-331px column; the width that makes them end where the clip does is the
     column's, which `--clip-w` now sets. */
  max-width: none;
  font-size: clamp(12px, 0.68vw, 14px);
  line-height: 1.55;
}

/* ---------- how big a frame is ----------
 *
 * The brief: *"each frame the height of the viewport minus chrome, phone-shaped,
 * `object-fit: cover`"*. So HEIGHT is the governor — as it was in iteration 2,
 * and for the same reason: a 1206x2622 portrait screen laid out on its width
 * alone is a column of pixels three screens tall — but the height is now taken
 * from the viewport rather than from a fixed 600px, so the page is one screen
 * on a laptop and one screen on a 27-inch monitor instead of one and a bit.
 *
 * Four terms, and they are not interchangeable — read the nesting, not the
 * order:
 *
 *   1. `100dvh - var(--chrome)`, the brief's own rule. `--chrome` is the sum of
 *      everything on the page that is NOT a clip, MEASURED on the built page
 *      rather than estimated: at 1280x800 it is 30.72 wrap padding-top + 49.72
 *      head + 33.28 section padding + 16 eyebrow + 9 h1 margin + 33.28 h1 +
 *      28.6 frames padding + 15.36 figcaption margin + 25.6 title + 6 + 21.7
 *      line + 58 wrap padding-bottom = 330.0 exactly, and at 1920 the same
 *      reading is 402.7. It is a `clamp` and not a constant because every term
 *      in that sum is itself a clamp; `24vw` is the slope that covers both
 *      readings without ever coming in UNDER one (330 at 1280, 345.6 at 1440
 *      against a measured 341, 405 at 1920 against a measured 402.7).
 *   2. 720px, a LAYOUT number and no longer an encode number. It was both
 *      until iteration 4: the clips were encoded 720px tall, so a frame drawn
 *      taller than that was a frame drawn out of focus. What that missed is
 *      that CSS pixels are not device pixels — Kevin, 2026-09-10: *"you can
 *      tell the quality is low(er) on the video when viewed on my retina
 *      macbook pro, does it need to be?"*. On a 2x display a 720px-tall CSS
 *      box is 1440 device pixels, so a 720px encode was being upscaled 2x on
 *      every Retina screen there is. **The clips are now encoded at 1440px,
 *      which is 2x this ceiling, and this ceiling did not move**: it is where
 *      a phone frame stops growing on the page, and the encode is what feeds
 *      it one source pixel per device pixel. Move one and the other has to
 *      move with it — a 1440px ceiling here would want a 2880px encode.
 *   3. a 600px FLOOR under those two, and it is the one number here that came
 *      from the old page rather than from this one. 600px is exactly what
 *      iteration 2's frames were — `--frame: 600px`, singles at 277x602 — so
 *      this is the promise that a page which is now ONE SCREEN never bought
 *      that screen by shrinking the product. Without it, a 1280x800 laptop
 *      (dvh - chrome = 470) would show frames at 216x470: smaller than what
 *      shipped, on the change whose whole point was *"the images are more
 *      exciting"*. With it that laptop scrolls 125px and keeps 276x600, and
 *      every viewport 1080 tall or better is still one screen with BIGGER
 *      frames than before (675px at 1080, the 720px ceiling from 1440 up).
 *   4. the WIDTH the row can afford, applied OUTSIDE the floor and therefore
 *      last. Three frames and two gaps have to fit inside `.wrap`'s content
 *      box, and on a tall, narrow window (a 1000px browser on a 1440px display,
 *      or anything just above the 900px stacking point) height is not the
 *      binding constraint. If the floor were allowed to win here the row would
 *      overflow the wrap and the DOCUMENT would scroll sideways — the exact
 *      failure iteration 2's `min-width: 0` bug produced, and the one thing
 *      `site-phone.spec.ts` has asserted against since. So it is a hard cap on
 *      the result, not another value in the same `clamp`. */
.wrap {
  /* Kevin, 2026-09-10, on the merged page: "you could maybe space the 3 column
     vids/captions wider still within the width of the window, just a bit". Was
     clamp(18px, 2.2vw, 40px): 28px at 1280, 40px from 1820 up. Now ~41px at
     1280, 46px at 1440, 60px at 1920 — and 60 is the ceiling because at 1920
     the row is 3 x 310 + 2 x 60 = 1050 of the wrap's 1056, so the width term
     (4) still does not bind and the frames keep their size. */
  --gap: clamp(24px, 3.2vw, 60px);
  /* FINDING 94 split this in two. It is still the same measured sum — see term
     1 above — but the HEAD is now a multiple of `--ui` rather than a fixed
     17px mark beside a `clamp(19px, 1.1vw, 26px)` wordmark, so the term that
     carries it has to move with the unit or the frames outgrow the screen. The
     head's content box is 2.3 of the unit (measured: 30.0px at the 13px floor,
     39.4 at 17.41, 58.1 at 26.11, 67 at 30 — the chip's 2.15u + 2 and the
     wordmark's 2.23u, whichever is taller), against the flat 23px it used to
     be, so the vw fit loses 23 and the unit term adds it back with the growth:
     `clamp(330, 24vw, 405)` became `clamp(307, 24vw - 23, 382) + 2.3 x --ui`.
     At the floor that is 337 / 352.6 / 412 where the old one read 330 / 345.6 /
     405 at 1280 / 1440 / 1920, which is the +7px the head actually grew — and
     it restores the one-screen verdict at 1920x1080 and 1920x1200 that the
     bigger head had taken away (both measured, lane report §4). Above ~1920 the
     clip is bound by the ROW's width rather than by this term, so the two
     readings drift apart there without deciding anything, exactly as the flat
     405 already did. */
  --chrome: calc(clamp(307px, 24vw - 23px, 382px) + var(--ui) * 2.3);
  /* `.wrap`'s content box: its max-width less its 22px of side padding. Below
     759px the padding is 16px, so this UNDER-states the room by 12px — which is
     the safe direction, and in that range the page is one column and this term
     never binds anyway. */
  --row: calc(min(1100px, 92vw) - 44px);
  --frame-w: calc((var(--row) - 2 * var(--gap)) / 3);
  /* Zero unless the reduced-motion control is on the page — see `.motion`. It is
     a separate term rather than another value baked into `--chrome` because
     `--chrome` is overridden twice below, at 900px and at 759px, and one of the
     two conditions is a width while the other is a preference: folding them
     together would mean four numbers where there are two. */
  --motion-chrome: 0px;
  --frame-h: min(
    max(min(calc(100dvh - var(--chrome) - var(--motion-chrome)), 720px), 600px),
    calc(var(--frame-w) * 2622 / 1206)
  );
  /* The height decided above, back in width — the clip's own box, and the width
     of the column and the caption under it. */
  --clip-w: calc(var(--frame-h) * 1206 / 2622);
}
.clip {
  display: block;
  height: var(--frame-h);
  /* Width follows from the shape, so there is one number on this page and not
     two. `max-width` is the guard for the one case the four terms above cannot
     see — a viewport narrow enough that a single column is still wider than the
     screen — and it is why `cover` is the fit: when it binds, the box stops
     being phone-shaped and the crop takes the difference instead of a stretch.
     It is also what absorbs the 0.4px the encode rounds off (1440 x 1206/2622
     = 662.4, and H.264 wants an even width, so the file is 662 x 1440 — which
     is 331.2 x 720 of CSS at 2x, the same 0.8px as before, halved by being
     rounded at twice the resolution). */
  aspect-ratio: 1206 / 2622;
  width: auto;
  max-width: 100%;
  object-fit: cover;
  /* The site's glass, on a phone's corner radius — the same border, radius and
     shadow the stills carried, so the medium changed and the furniture did not.
     `background` is what is painted in the instant before the poster decodes,
     and under `prefers-reduced-motion` it is what sits behind a poster that is
     never replaced. */
  border: 1px solid var(--line);
  border-radius: 24px;
  background: rgba(10, 10, 13, 0.82);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 14px 44px rgba(8, 8, 11, 0.55);
}

/* `prefers-reduced-motion: reduce` -> the POSTER, and no autoplay.
 *
 * The stopping is `site/phone.js`'s — an attribute a media element acts on
 * cannot be revoked by a stylesheet, and that file's header says so at length.
 * What belongs here is the rest of the promise: the paused element must not
 * animate its own arrival either. */
@media (prefers-reduced-motion: reduce) {
  .clip {
    transition: none;
  }
}

/* ---------- the one play control, as a CARD (iteration 8, finding 91) -------
 *
 * ITERATION 7 (finding 86) made this ONE pill with words on it, above the
 * frames. Kevin, walk 8: *"the 'motion is off on this device' message with the
 * play the clips button needs to be like in a big red opaque overlay pill thing
 * like a popup like a warning almost so people click it bc it just is hard to
 * really tell.. ok maybe not that crazy since we do want to respect the no
 * motion people but maybe a bit more than that for those like me that just want
 * high FPS on their gaming computers"*.
 *
 * He asked for a warning and then walked it back inside the same sentence, and
 * Fable's ruling took the walked-back half: **a CARD, not a warning.** No red,
 * no opaque overlay, no popup, and above all NO MOTION — a control that animates
 * itself onto a page for a reader who asked the operating system for stillness
 * is the one answer the setting exists to refuse.
 *
 * What it IS is the site's own notice idiom, lifted verbatim from
 * `demo.css`'s `body.down .fallback` — the card the demo page shows when the box
 * is down: a hairline `--line` border, a 3px `--amber` rule down the left, the
 * `3px 9px 9px 3px` radius that rule produces, and the dark gradient with the
 * inset top highlight. AMBER IS THE SITE'S STATE COLOUR (the Beta chip's dot is
 * the same token) and it is the ONLY notice signal here: nothing on this page
 * flashes, pulses, slides or fades in.
 *
 * The two elements inside it and their two strings are byte for byte what
 * iteration 7 shipped. Nothing was authored for this change.
 *
 * Kevin's own "a bit more than that" is spent on SIZE rather than on alarm: the
 * line steps up from `--fg-2` to `--fg` and from the caption's type to
 * 15-18px, and the button becomes the large purple door — 52px tall, 14/26
 * padding, 15px type — instead of the 44px chip it was. A reader who wants high
 * FPS on a gaming computer should be able to see the way in from across the
 * room; a reader who meant the setting should not be shouted at. A bordered card
 * with a big door in it does both.
 *
 * Kevin's Windows machine shows three posters and nothing else: Windows
 * "animation effects" off IS `prefers-reduced-motion: reduce`, and this page
 * honours it. It should keep honouring it — a reader who asked the operating
 * system for stillness gets stillness — but "honour the setting" and "give that
 * reader no way to see the product" are two different promises, and only the
 * first one is owed. Iteration 6 answered that with a round control on each of
 * the three posters. Kevin, walk 8: *"the play buttons are more purple so easier
 * to see but not much bigger. I think you make them like at least twice as big
 * if not more … problem is with your purple shading and at that size it will
 * just look like a bubble itself"*.
 *
 * He is describing the failure mode exactly. The thing this page is a picture of
 * IS a field of round purple things that grow; a round purple control drawn at
 * twice 64px, standing on that field, is not a button that reads as a button —
 * it is the biggest bubble on the poster. Making it bigger makes it worse.
 *
 * So the control stopped being round, stopped being three, and came off the
 * posters: ONE PILL WITH WORDS ON IT, above the frames, where nothing it can be
 * confused with is drawn. That is Fable's ruling on finding 86 and it is also
 * Kevin's own second option — *"a normally invis pill appears v obvious that
 * reduced motion has been detected … and then just a oneliner but then a play
 * for all"*. Iteration 8 keeps every word of that and puts a card around it.
 *
 * EVERY VALUE BELOW IS THE SITE'S, AND MOST OF THEM ARE NOT HERE AT ALL. The
 * button carries `class="chip play"`, so `style.css`'s `.chip` draws the pill —
 * the radius, the inline-flex, the uppercase tracking, the glass — and this file
 * only raises it to the large purple door: 14/26 padding, the 9px gap, 15px at
 * 0.1em, `--purple` on the border, `--fg` on the ink and the lift. The one
 * deliberate difference from `.chip.go` is the resting FILL, and it is finding
 * 77's ruling carried forward unchanged: the gradient that chip keeps for its
 * hover is on at rest here, because a control a reader has to FIND cannot spend
 * its legibility waiting for a pointer. Hover and focus then have somewhere to
 * go.
 *
 * `display: none` here is belt to `phone.js`'s braces: the script detaches the
 * whole block from the document when motion is allowed, and this is what keeps a
 * page whose script failed to load from drawing a control that does nothing.
 * Both layers agree on the same condition, in the same direction. */
.motion {
  display: none;
  align-items: center;
  flex-wrap: wrap;
  gap: clamp(12px, 1.1vw, 20px);
  /* The air between the h1 and the card. It was `padding-top` when the block
     had no box of its own; a card's padding is the card's INSIDE, so the
     separation had to become a margin for the two to stop being one number.
     The VALUE is unchanged, and `--motion-chrome` below still counts it. */
  margin-top: clamp(20px, 1.6vw, 30px);
  /* demo.css's `body.down .fallback`, value for value — the site's one notice
     idiom, and the reason this lane did not invent a second one. The left rule
     is `--amber`, which is the same token the Beta chip's dot carries: on this
     site amber means "a state you should know about", and it is the whole of
     the signal. The asymmetric radius is what that 3px rule wants — square
     against the rule, 9px on the three free corners. */
  padding: 18px 22px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--amber);
  border-radius: 3px 9px 9px 3px;
  background: linear-gradient(180deg, rgba(16, 16, 20, 0.82), rgba(10, 10, 13, 0.82));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
/* The line, a step up from the captions rather than a warning's voice: `--fg`
   (the page's brightest neutral, what the headings are in) instead of `--fg-2`,
   and 15-18px instead of the caption's 13-16. It is NOT `class="copy"`:
   `site-phone.spec.ts` asserts that `p.copy` is exactly the three frame lines
   and nothing else, which is the assertion that keeps a fourth sentence off this
   page. */
.motion-line {
  color: var(--fg);
  font-size: clamp(15px, 0.85vw, 18px);
  line-height: 1.5;
}
/* The large purple door, on a <button>. A button inherits neither the font nor
   the cursor, so those two lines are not decoration. `margin-left: auto` puts it
   at the END of the card's row; when the row cannot hold both it wraps under the
   line, and at <=560px it takes the whole width (below). */
.play {
  /* FINDING 94: on `--ui`, in the whole tenths finding 91 left here — 1.4/2.6 of
     padding, a 0.9 gap, 1.5 of type and a 5.2 floor under the box, which at the
     old 10px unit are the 14/26, 9, 15 and 52 that shipped. */
  padding: calc(var(--ui) * 1.4) calc(var(--ui) * 2.6);
  /* 5.2 of the unit, and it is a FLOOR rather than a height: at the unit's own
     floor the 1.4/2.6 padding puts the type's own box at 51px and the rounding
     has to land on the safe side of the large-target guidance. Iteration 7's
     floor was 44 — the pointer minimum — and finding 91 is Kevin asking for "a
     bit more than that"; on the shared unit it is 67.6px on a laptop and grows
     from there, so finding 91's ask can only be over-met, never under. */
  min-height: calc(var(--ui) * 5.2);
  margin-left: auto;
  gap: calc(var(--ui) * 0.9);
  border-color: var(--purple);
  color: var(--fg);
  font: inherit;
  font-size: calc(var(--ui) * 1.5);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  /* Finding 77: the gradient `.chip.go` keeps for hover, on at rest. */
  background: linear-gradient(180deg, rgba(145, 71, 255, 0.28), rgba(145, 71, 255, 0.14));
  /* Lifted off the page the way `.chip.go` is lifted off the field. */
  box-shadow: 0 10px 30px rgba(8, 8, 11, 0.5), 0 0 18px rgba(145, 71, 255, 0.18);
  /* OPACITY AND COLOUR ONLY, and 180ms of it. A cross-fade is what the
     reduced-motion guidance names as the SUBSTITUTE for motion rather than an
     instance of it, and this one is feedback on a button the reader just
     pressed. Nothing here translates, scales or moves. */
  transition: background 180ms linear, color 180ms linear;
}
/* Brighter, not different: the same two stops, further up, and the ink to white
   — the move `.chip.go` makes on the landing, one step along. */
.play:hover,
.play:focus-visible {
  color: #fff;
  background: linear-gradient(180deg, rgba(145, 71, 255, 0.46), rgba(145, 71, 255, 0.26));
}
.play:focus-visible {
  outline: 2px solid var(--purple);
  outline-offset: 2px;
}
/* Playing — THE CARD GOES AWAY. Kevin, walk 8, 2026-09-12 (verbatim): *"if on
   win/noMotion you click 'play the clips' on the iphone tab then that thing
   should be hidden while it's running like if you viewed normally... refresh
   brings it back anywahs so.."* So once the reader has pressed the door the
   page is the normal-motion page: the card is gone (`display: none` — the
   whole `.motion` block, read off the button's own `aria-pressed`, which is
   the one copy of the state) and the frames take the room back (the
   `--motion-chrome` term returns to 0 in the reduced-motion block below). The
   way back to stills is a reload, which is what the reader would do anyway.
   The two pressed rules under this one still apply for the instant between the
   press and the hide, and for an engine without `:has()`, where the card stays
   and reads as "playing". */
.motion:has(.play[aria-pressed="true"]) {
  display: none;
}
.play[aria-pressed="true"] {
  color: var(--fg-2);
  background: linear-gradient(180deg, rgba(35, 35, 44, 0.8), rgba(22, 22, 28, 0.8));
}
.play[aria-pressed="true"]:hover,
.play[aria-pressed="true"]:focus-visible {
  color: #fff;
  background: linear-gradient(180deg, rgba(145, 71, 255, 0.46), rgba(145, 71, 255, 0.26));
}

@media (prefers-reduced-motion: reduce) {
  .motion {
    display: flex;
  }
  .wrap {
    /* The control is furniture the frames have to make room for, so it goes into
       the `--chrome` sum the same way the head and the captions do — otherwise a
       page that is one screen under normal motion is one screen plus a control
       under reduced motion, and the reader who can least afford to scroll for the
       product is the one asked to.

       It is `.motion`'s own terms and nothing else, which is why it is not a
       measured constant like `--chrome`. ITERATION 8 recomputed it from the
       CARD's real height, which is four things and not two:

         margin-top   clamp(20px, 1.6vw, 30px)   the air above the card
         border       1 + 1                      the card's hairline, top+bottom
         padding      18 + 18                    the card's inside, top+bottom
         content      var(--ui) * 5.2            the button's `min-height`, the
                                                 tallest thing on the row (the
                                                 line is 27px at its ceiling)

       = clamp(20px, 1.6vw, 30px) + 38 + 5.2 of the unit. FINDING 94 moved the
       LAST of those four terms and nothing else: `.play`'s floor was a flat 52px
       and is now 5.2 of `--ui`, so the reservation follows it — which is the
       whole reason this was written as arithmetic over the block's own values
       rather than as a measured constant (`docs/knobs.md` says so in its row).
       It was `clamp(110px, calc(1.6vw + 90px), 120px)`, i.e. the same sum with
       52 in place of the unit term, reserving 110.5px at 1280 and 120px from
       1875 up; it now reserves more at every width because the button is bigger
       at every width. Measured against the live page in the lane report. */
    --motion-chrome: calc(clamp(20px, 1.6vw, 30px) + 38px + var(--ui) * 5.2);
  }
  /* …and 0 again once the clips are playing and the card has gone (the rule by
     `.motion:has(...)` above): the frames are sized as they are under normal
     motion, because that is now the page (Kevin, 2026-09-12). */
  .wrap:has(.motion .play[aria-pressed="true"]) {
    --motion-chrome: 0px;
  }
}

/* ---------- tablets and phones ----------
 *
 * One column, and the frame gets its height from the viewport rather than from
 * a third of the row. There is no two-across step on the way: with three frames
 * that is a row of two and an orphan, and the orphan is always the one the page
 * ends on.
 *
 * 900px is where three 0.46-aspect frames stop being frames — at that width the
 * row's own term (2) caps each one at ~390px tall and ~180px wide, which is
 * narrower than the chat rows inside it are legible at. */
@media (max-width: 900px) {
  .sec.frames {
    grid-template-columns: 1fr;
    justify-items: center;
    gap: clamp(34px, 5vw, 56px);
  }
  .frame {
    /* One column, and the frame is centred in it while its caption is not —
       the caption still ends exactly where the clip does. */
    width: min(100%, var(--clip-w));
  }
  .wrap {
    /* Term 4 above is about a THREE-across row and says nothing here: one column
       can afford the whole width, so the width cap stops binding and the height
       terms decide alone. The chrome is smaller too — the same furniture, but
       every clamp in it is near its floor at this width. */
    --frame-w: var(--row);
    --chrome: clamp(300px, 34vw, 340px);
  }
}

@media (max-width: 759px) {
  /* FINDING 98 ROUND 2: the head's band tightens with the column it sits over,
     or the brand would stand 6px further out than everything under it. */
  .head-wrap {
    padding-left: 16px;
    padding-right: 16px;
  }
  .wrap {
    padding-left: 16px;
    padding-right: 16px;
    /* A phone screen shown on a phone screen, with the caption under it and the
       next one just far enough below to be a separate thing. */
    --chrome: 300px;
  }
  /* The card tightens with the page: `.wrap`'s own side padding drops from 22
     to 16 at this width, so a card that kept 22px inside it would have less air
     outside than in. Everything else about it is unchanged. */
  .motion {
    padding: 16px 18px;
  }
}

/* The card's row has stopped being a row: the line took the whole width and the
   button is underneath it. A 200px pill floating at the right end of a 300px
   card reads as an afterthought, so below here the door is the width of the card
   — which is also the shape a thumb expects at this size. Nothing else changes:
   same two elements, same two strings, same colours. */
@media (max-width: 560px) {
  .play {
    margin-left: 0;
    width: 100%;
    justify-content: center;
  }
}
