/* index.html carries this same rule; without it here too, a click from
   index.html into start.html raises an unhandled AbortError in Chrome. */
@view-transition{ navigation: auto }

/* ---------------------------------------------------------------
   KidBD / funnel.css
   The step by step launcher, start.html. Ten screens, one table.

   Every value in here is recorded in DESIGN-R14.md with the reason
   it is that number. If you are about to invent a spacing value,
   it is already in the ladder.

   Borderless: no rim on an object, a chip, an input, a button or an
   icon, and no border reaction on hover, focus or active. Fills and
   the offset ink shadow do the separating. Focus is an outline on
   links and buttons and a spread halo on text inputs.

   No blink: all ten screens ship in the markup and are shown by a
   class. Nothing here is painted by rebuilding a container.

   Nexus V22 / Kavalsia Inc.
   --------------------------------------------------------------- */

/* ---------------------------------------------------------------
   The faces. Self hosted, because this page makes no external
   request of any kind. Both files are the VARIABLE instance, so one
   file covers every weight of the family and a heading going to 700
   costs no second request.

   latin-ext is its own face with its own unicode-range, so it is
   fetched only when a guest's name actually needs it.
   --------------------------------------------------------------- */
@font-face{
  font-family:"Fredoka"; font-style:normal; font-weight:400 700; font-display:swap;
  src:url("fonts/fredoka-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:"Fredoka"; font-style:normal; font-weight:400 700; font-display:swap;
  src:url("fonts/fredoka-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:"Lexend"; font-style:normal; font-weight:400 700; font-display:swap;
  src:url("fonts/lexend-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:"Lexend"; font-style:normal; font-weight:400 700; font-display:swap;
  src:url("fonts/lexend-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* ---------------------------------------------------------------
   Tokens. The palette is index.html's, unchanged: this is the same
   product. Not one new colour is introduced here. --tone is the
   only thing that moves, and it holds the `gold` field of whichever
   theme in lib.js is selected.
   --------------------------------------------------------------- */
:root{
  --bg:#fff5d6; --su:#fffdf4; --ink:#2a2350; --mut:#4d4383; --ln:#f0e6bf;
  --red:#ff5a5f; --tang:#ff9f1c; --sun:#ffce3a; --sky:#3aa0ff;
  --mint:#1fc08a; --pink:#ff77c2;

  /* the SYSTEM colour: the focus ring, and nothing else. It is never a
     ground under a paragraph, because AAA is unreachable on it at any text
     colour. --grape-d is the same hue deep enough to carry white (7.51:1). */
  --grape:#6a49f2; --grape-d:#5535cf;

  /* the ONE action colour. #ffa41f under --ink is 7.28:1; #ff9f1c is 7.03:1
     and rounds to the threshold in a report. Ink on it, never white: white on
     this orange is 2.2:1. */
  --cta:#ffa41f; --cta-hi:#ffb03d; --cta-ink:var(--ink);

  /* the party colour currently selected, read out of THEMES. Fills only.
     TEXT ON A --tone FILL IS ALWAYS --ink: --mut on a 62% tone falls to
     5.81:1 on Midnight and misses AAA. Measured, not assumed. */
  --tone:#ffc53d;

  --disp:"Fredoka","Trebuchet MS",sans-serif;
  --body:"Lexend","Helvetica Neue",Arial,sans-serif;

  /* ---- the type scale, solved for both ends, 320 to 1440 ----
       slope = (max - min) / 1120 * 100      -> the vw term
       base  = min - slope / 100 * 320       -> the px term
     so each one is exactly its min at 320 and exactly its max at 1440.
     A clamp that parks on its floor for half the range renders a phone at
     390 and a tablet at 780 with identical type while everything around
     them changes; that is the defect this arithmetic exists to prevent. */
  --t-q:clamp(30px,21.43px + 2.679vw,60px);
  --t-h3:clamp(19px,17.29px + .536vw,25px);
  --t-lead:clamp(17px,15.86px + .357vw,21px);
  --t-body:clamp(15px,14.43px + .179vw,17px);
  --t-sm:clamp(14px,13.71px + .089vw,15px);
  --t-btn:clamp(17px,16.43px + .179vw,19px);
  --t-num:clamp(34px,28.86px + 1.607vw,52px);

  /* ---- the one spacing ladder ---- */
  --s1:6px; --s2:10px; --s3:16px; --s4:24px; --s5:36px; --s6:56px; --s7:88px;
  --pad-x:clamp(20px,5vw,72px);
  --gap-col:clamp(32px,4.4vw,72px);
  --strip-h:68px;

  --r-sm:14px; --r:22px; --r-lg:34px; --r-pill:999px;

  /* Zero blur, offset. The committed world chose this (index.html:36) and it
     is the only thing separating surfaces once the rims are gone. */
  --sh:6px 8px 0 var(--ink);
  --sh-sm:4px 5px 0 var(--ink);
  --sh-xs:3px 4px 0 color-mix(in srgb,var(--ink) 16%,transparent);

  --ease:cubic-bezier(.16,1,.3,1);
  color-scheme:light;

  /* the browser surfaces we did not draw, themed from the palette */
  accent-color:var(--grape);
  caret-color:var(--grape-d);
}
@media (min-width:900px){ :root{ --strip-h:84px } }

*,*::before,*::after{ box-sizing:border-box }

/* The attribute loses to any display: declaration, silently. Every element
   this page hides with .hidden also carries the rule beside its display. */
[hidden]{ display:none !important }

html{ -webkit-text-size-adjust:100% }

body{
  margin:0;
  background-color:var(--bg);
  color:var(--ink);
  font-family:var(--body);
  font-size:var(--t-body);
  line-height:1.62;
  font-weight:400;
  min-height:100svh;
  overflow-x:hidden;
}

/* The table. A very low contrast wash of the party's own colour, laid over a
   SOLID ground colour: a gradient in the `background` shorthand leaves
   background-color transparent, so the ground is stated first and the ramp
   is background-image. */
body::before{
  content:"";
  position:fixed; inset:0; z-index:0; pointer-events:none;
  background-color:var(--bg);
  background-image:
    radial-gradient(120% 78% at 50% -14%, color-mix(in srgb,var(--tone) 13%,transparent) 0%, transparent 62%),
    radial-gradient(90% 60% at 8% 104%, color-mix(in srgb,var(--tone) 9%,transparent) 0%, transparent 58%);
  transition:background-image 420ms var(--ease);
}
@media (prefers-reduced-motion:reduce){ body::before{ transition:none } }

::selection{ background:color-mix(in srgb,var(--grape) 22%,transparent); color:var(--ink) }

a{ color:var(--grape-d); text-underline-offset:.18em; text-decoration-thickness:.08em }

/* ---------------------------------------------------------------
   Focus. Not decoration and not optional: deleting the author rims
   also deletes the browser's own focus ring on most controls, so the
   ring is rebuilt here in the same file that removed them.
   :focus-visible is NOT keyboard only; it fires for a mouse press on
   a text input in every engine, so nothing may read it that way.
   --------------------------------------------------------------- */
:where(a,button,summary,[role=button],[tabindex]):focus-visible{
  outline:3px solid var(--grape);
  outline-offset:3px;
  border-radius:var(--r-sm);
}
:where(input,select,textarea):focus-visible{
  outline:none;
  /* spread 4px: the borderless law permits 3px and up as a halo. A 1px ring
     would be a rim and is banned. */
  box-shadow:var(--sh-xs),0 0 0 4px color-mix(in srgb,var(--grape) 34%,transparent);
}

/* Every inline SVG gets an intrinsic size and flex:none. An SVG with none
   stretches to fill a flex parent and renders card sized; that has bitten
   four separate containers in this house. Drawn scenes opt out BY NAME. */
.tbl svg{ width:20px; height:20px; flex:none }
.tbl svg.scene,.tbl svg.cstage,.tbl svg.brandmark,.tbl svg.big{ width:auto; height:auto }

/* ---------------------------------------------------------------
   The top strip. The whole navigation of this page: the mark, back,
   and the cake whose candles are the progress. It is never a tab bar
   and there is no rail of any kind at any width.
   --------------------------------------------------------------- */
.strip{
  position:sticky; top:0; z-index:8;
  height:var(--strip-h);
  display:flex; align-items:center; gap:var(--s3);
  padding-inline:var(--pad-x);
  background-color:var(--bg);
}
.strip::after{
  /* the table edge: a soft fall off under the strip, not a line.
     A 1px rule here would be a rim. */
  content:""; position:absolute; left:0; right:0; top:100%; height:18px;
  background-image:linear-gradient(color-mix(in srgb,var(--ink) 9%,transparent),transparent);
  pointer-events:none;
}
/* an <a>, so it is a control and the 48px floor applies to it */
.strip .mk{
  width:48px; height:48px; flex:none;
  border-radius:14px; overflow:hidden; display:block;
}
.strip .mk svg{ width:100%; height:100%; display:block }
.strip .wd{
  font-family:var(--disp); font-weight:600; font-size:19px; letter-spacing:-.01em;
  line-height:1; margin:0;
}
.strip .spacer{ flex:1 1 auto; min-width:var(--s2) }

/* back. A real control on every screen, 48px, and it says where it goes.
   ROUND 79 moved it OUT OF THE STRIP and into the screen's own action row
   (start.page.js `buildBack`, and the mount in `show()`), so the two halves
   of one decision - onwards and backwards - are the same row on the page
   rather than one at the top of the viewport and one at the bottom of the
   content. Nothing about the control itself changed; what follows is where
   it now sits, which is the only new thing in this block. */
.back{
  /* ROUND 80. A SUBTLE FILL, not transparent. In the strip it was chrome and
     a transparent ground was right there; in the action row it is one of two
     controls on a line, and the house law is that a button is solid or
     subtle-filled and never a ghost. The same 7% of the page's own ink that
     `.btn.ghost` beside it carries, so the two agree; the rank between them
     is height and weight, which is where it belongs. `gates.mjs --controls`
     counted it as the page's one ghost control until this line. */
  appearance:none; color:var(--ink);
  background-color:color-mix(in srgb,var(--ink) 7%,transparent);
  font-family:var(--body); font-size:var(--t-sm); font-weight:600;
  min-height:48px; padding-inline:var(--s3); border:0; border-radius:var(--r-pill);
  display:inline-flex; align-items:center; gap:var(--s1); cursor:pointer;
  transition:background-color 180ms var(--ease),transform 180ms var(--ease);
}
.back:hover{ background-color:color-mix(in srgb,var(--ink) 13%,transparent) }
.back:active{ transform:translateY(1px) }
.back svg{ width:16px; height:16px }
.back[hidden]{ display:none !important }
/* In the row: full width UNDER the action on a phone, where `.acts` is a
   column and a content width pill would sit ragged against a 100% button;
   beside it from 640 up, where the row turns horizontal. It is second in
   the DOM and nothing here sets `order`, so the eye and the tab key walk
   the same two controls in the same two places. */
.acts .back{ width:100%; justify-content:center }
@media (min-width:640px){ .acts .back{ width:auto } }

/* the cake. Drawn ONCE at boot with all seven candles; lighting one sets
   opacity on that flame and its smoke. Rebuilding this from a string on
   every screen would re-parse seven flames and three gradients. */
/* QUEUE #83, 2026-09-06. `width:96px; flex:none` put this element's right
   edge at 377.3px WHATEVER the viewport was, so the document scrolled
   sideways by 57px at 320, 17px at 360 and 2px at 375 - identical numbers on
   the round-20 backup and the round-21 tree, so it was standing, not a
   round's. Nothing in the suite saw it: width-sweep swept index.html and
   funnel-proof only measures 1440 and 390.

   `flex:0 1 96px` with a floor is the durable half of the fix: the cake now
   gives width back to the row before the row gives it to the document, and it
   can never again be the fixed element that decides the page's scroll width.
   The floor is 64px because below that the seven candles stop reading as
   seven. `aspect-ratio` rather than a second fixed height, or the drawing
   letterboxes inside its own box as it narrows. */
.cake{ width:96px; height:auto; aspect-ratio:96/60; flex:0 1 96px; min-width:64px;
       display:block }
.cake svg.cstage{ width:100%; height:100%; display:block }
.cake .flame,.cake .glow{ transition:opacity 320ms var(--ease) }
.cake .smoke{ transition:opacity 520ms var(--ease) }
@media (min-width:900px){ .cake{ width:132px; flex-basis:132px; aspect-ratio:132/83 } }

/* The other half. Shrinking the cake alone could not fix this: the row's
   min-content at 320 was pad 20 + mk 48 + 16 + wordmark 49.6 + 16 + back 89.7
   + 16 + spacer 10 + 16 + cake 96 + pad 20 = 397.3, so a cake small enough to
   fit would have been 18.7px - a candle stage the size of a favicon. Below
   390 the lockup drops its TEXT half and keeps its mark: the mark is a 48px
   <a> that goes to the front page and carries the name in its aria-label, the
   page's own h1 says what the product is one line below it, and the visitor
   arrived here from that front page. The gap drops one rung at the same time.
   Measured after: 293.7px of content in 320, a 26.3px right margin, and the
   cake never has to leave 96px. */
/* ROUND 79 took the 89.7px back button out of this row and the rule below
   STAYS. The lockup still drops its text half under 390: the measurement
   above is why the rule exists and the freed width is not why it would be
   removed - the wordmark is redundant beside a mark that carries the name
   in its aria-label, at the width where the cake needs every pixel. */
@media (max-width:389.98px){
  .strip{ gap:var(--s2) }
  .strip .wd{ display:none }
}

.lit-say{
  font-family:var(--body); font-size:var(--t-sm); font-weight:500; color:var(--mut);
  white-space:nowrap;
}
@media (max-width:539px){ .lit-say{ display:none } }

/* ---------------------------------------------------------------
   A screen. A whole viewport at 1440 and a whole scroll at 390.
   Top aligned, never centred: a centred multi step form lands its
   heading at a different height on every step, so it jumps as the
   parent advances and short steps open a hole under the strip.
   --------------------------------------------------------------- */
.tbl{ position:relative; z-index:1 }

.screen{ display:none }
.screen.on{ display:block }

.screen > .in{
  display:grid;
  grid-template-columns:minmax(0,1fr);
  gap:var(--s6);
  align-items:start;
  padding-inline:var(--pad-x);
  padding-block:var(--s5) var(--s7);
  max-width:1560px;
  margin-inline:auto;
  min-height:calc(100svh - var(--strip-h));
}
@media (min-width:900px){
  .screen > .in{
    grid-template-columns:minmax(0,1fr) minmax(0,.85fr);
    gap:var(--gap-col);
    padding-block:var(--s6) var(--s7);
  }
  /* screens 1 to 3 are Persuade: the table gets the larger half, and they were
     the only screens allowed to sit centred, because they carry no growing
     field list and so cannot make a heading jump between steps. */
  .screen.tell > .in{ grid-template-columns:minmax(0,.92fr) minmax(0,1.15fr);
                      align-content:center; padding-block:var(--s6) }

  /* ROUND 78, Siavash: kill the empty space under the steps. `.in` carries
     `min-height:calc(100svh - var(--strip-h))`, so on a tall screen every
     step that is shorter than that hugs the top and leaves the whole
     remainder as a void beneath it - which is what a 1456x840 screenshot of
     the theme picker is mostly made of.

     WHICH SCREENS, AND WHY NOT ALL OF THEM. The rule above this one already
     records the constraint and it still holds: centring ties the heading's
     position to the content's height, so a screen whose content GROWS makes
     the heading jump. That is 4, 5 and 6 - the kid, the day, and the room,
     whose guest chips wrap into new rows as they are added - and they stay
     top-aligned. 7 grows once, when "Show every theme" opens the set from two
     cards to six, and that is a jump on a press the parent made and can see
     the cause of, not one between steps; it is centred with the rest.

     `safe center` and not bare `center`: when the content IS taller than the
     container, centred overflow spills BOTH ways and the top of the heading
     goes off the top of the screen where nothing can scroll it back. `safe`
     falls back to start alignment in exactly that case. */
  /* ROUND 82 takes #s9 out of this list. Its stage is taller than the
     viewport on every step, so there is no hole to centre away, and the
     sticky bar below it needs a row it can travel inside - which a centred
     `align-content` collapses. It sets its own `align-content:start` in the
     screen 9 block near the foot of this file. */
  #s1 > .in, #s7 > .in, #s8 > .in, #s10 > .in{ align-content:safe center }
  /* the lone-object centring that used to live here moved to `.objs` /
     `.set > .obj` in the flagship block, unconditional: round 82 wanted it on
     a phone as well and two rules saying the same thing at two widths is how
     one of them gets edited alone. */
}

/* at 390 the question leads, except where the object IS the point */
.screen .ask{ order:1 }
.screen .set{ order:2 }
.screen.obj-first .ask{ order:2 }
.screen.obj-first .set{ order:1 }
@media (min-width:900px){
  .screen .ask,.screen.obj-first .ask{ order:0 }
  .screen .set,.screen.obj-first .set{ order:0 }
}

.ask{ min-width:0 }
.set{ min-width:0 }

/* the entrance. From an already visible default: nothing here is hidden by a
   class a scroll has to remove, because a headless renderer and a background
   tab both fail scroll triggers and the screen would ship blank. */
/* `backwards`, never `both`, and the resting state is the visible one: an
   element parked at opacity 0 waiting for an animation is how a section
   ships blank on a headless renderer or a background tab. */
.screen.on > .in{ animation:rise 260ms var(--ease) backwards }
@keyframes rise{ from{ opacity:0; transform:translateY(14px) } to{ opacity:1; transform:none } }

/* ---------------------------------------------------------------
   The question. One per screen, never two.
   --------------------------------------------------------------- */
.q{
  font-family:var(--disp); font-weight:600;
  font-size:var(--t-q); line-height:1.02; letter-spacing:-.022em;
  margin:0 0 var(--s3);
  /* the measure is on the block whose type it describes. A ch measure
     resolves in the ELEMENT'S OWN font, so this same declaration on the
     wrapper would clamp the column to about 260px of Lexend. */
  max-width:16ch;
  text-wrap:balance;
}
.lede{
  font-size:var(--t-lead); line-height:1.5; font-weight:400; color:var(--mut);
  margin:0 0 var(--s5); max-width:34ch;
}
.h3{
  font-family:var(--disp); font-weight:600; font-size:var(--t-h3);
  line-height:1.16; letter-spacing:-.012em; margin:0 0 var(--s2);
}
.p{ margin:0 0 var(--s3); max-width:62ch }
.sm{ font-size:var(--t-sm); line-height:1.45; font-weight:500; color:var(--mut); margin:0 }
.strong{ font-weight:600; color:var(--ink) }
.num{ font-family:var(--disp); font-weight:600; font-size:var(--t-num);
      font-variant-numeric:tabular-nums; line-height:1; letter-spacing:-.02em }

/* ---------------------------------------------------------------
   Buttons. Ink on the action colour, never white.
   --------------------------------------------------------------- */
.btn{
  appearance:none; border:0; cursor:pointer;
  font-family:var(--body); font-size:var(--t-btn); font-weight:700;
  letter-spacing:-.006em; line-height:1.1;
  min-height:56px; padding:0 var(--s5);
  border-radius:var(--r-pill);
  background-color:var(--cta); color:var(--cta-ink);
  box-shadow:var(--sh-sm);
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s2);
  transition:transform 180ms var(--ease),background-color 180ms var(--ease),box-shadow 180ms var(--ease);
}
.btn:hover{ background-color:var(--cta-hi) }
.btn:active{ transform:translate(3px,4px); box-shadow:1px 1px 0 var(--ink) }
.btn[disabled]{
  background-color:color-mix(in srgb,var(--ink) 12%,var(--su));
  color:color-mix(in srgb,var(--ink) 58%,var(--su));
  box-shadow:none; cursor:not-allowed;
}
.btn.ghost{
  background-color:color-mix(in srgb,var(--ink) 7%,transparent);
  color:var(--ink); box-shadow:none; font-weight:600;
}
.btn.ghost:hover{ background-color:color-mix(in srgb,var(--ink) 13%,transparent) }
.btn.ghost:active{ transform:translateY(1px); box-shadow:none }
.btn svg{ width:18px; height:18px }

/* full width, stacked, on a phone. Not content width pills with a ragged edge. */
.acts{ display:flex; flex-direction:column; gap:var(--s3); margin-top:var(--s5) }
.acts .btn{ width:100% }
@media (min-width:640px){
  .acts{ flex-direction:row; align-items:center; flex-wrap:wrap }
  .acts .btn{ width:auto }
}
.gate{ font-size:var(--t-sm); color:var(--mut); margin:var(--s2) 0 0; min-height:1.45em }
.gate.warn{ color:var(--grape-d); font-weight:600 }

/* ---------------------------------------------------------------
   Fields. No rim: a soft under shadow does the well, and the focus
   halo is a 4px spread.
   --------------------------------------------------------------- */
.f{ display:block; margin-bottom:var(--s3) }
.f > .lb{
  display:block; font-size:var(--t-sm); font-weight:600; color:var(--mut);
  margin-bottom:var(--s2);
}
.f input,.f select,.f textarea{
  appearance:none; -webkit-appearance:none;
  width:100%; min-height:56px;
  padding:0 var(--s4);
  border:0; border-radius:var(--r-sm);
  background-color:var(--su); color:var(--ink);
  font-family:var(--body); font-size:var(--t-body); font-weight:500;
  box-shadow:var(--sh-xs);
  transition:box-shadow 180ms var(--ease);
}
.f input::placeholder{ color:color-mix(in srgb,var(--mut) 78%,var(--su)); font-weight:400 }
.f input[type=date],.f input[type=time]{ padding-right:var(--s3) }
.row2{ display:grid; grid-template-columns:minmax(0,1fr); gap:var(--s3) }
@media (min-width:560px){ .row2{ grid-template-columns:minmax(0,1fr) minmax(0,1fr) } }

/* ---------------------------------------------------------------
   Chips. A chip reads as a chip because of a faint fill taken from
   the party's own tone. No rim, and no border reaction on hover or
   focus. Selection is a stronger fill, the offset shadow, and a
   DRAWN tick, so it never depends on colour alone.
   --------------------------------------------------------------- */
.chips{ display:flex; flex-wrap:wrap; gap:var(--s2); margin:0; padding:0; list-style:none }
.chip{
  appearance:none; border:0; cursor:pointer;
  min-height:48px; padding:0 var(--s4);
  border-radius:var(--r-pill);
  background-color:color-mix(in srgb,var(--tone) 18%,var(--su));
  color:var(--ink);
  font-family:var(--body); font-size:var(--t-body); font-weight:600;
  display:inline-flex; align-items:center; gap:var(--s2);
  box-shadow:none;
  transition:background-color 180ms var(--ease),transform 180ms var(--ease),box-shadow 180ms var(--ease);
}
.chip:hover{ background-color:color-mix(in srgb,var(--tone) 34%,var(--su)) }
.chip:active{ transform:translateY(1px) }
.chip[aria-pressed=true]{
  background-color:color-mix(in srgb,var(--tone) 62%,var(--su));
  box-shadow:var(--sh-sm);
  font-weight:700;
}
/* The tick reserves its 18px at rest and animates opacity and transform only.
   It used to animate `width` from 0, which is a layout property: it reflowed
   the chip's label on every press, which is the wrong kind of movement and
   the wrong kind of cost. Reserving the space also means the label does not
   shift sideways when the chip is chosen. */
.chip .tick{ width:18px; height:18px; flex:none; opacity:0; transform:scale(.5);
             transition:opacity 180ms var(--ease),transform 180ms var(--ease) }
.chip[aria-pressed=true] .tick{ opacity:1; transform:none }
.chip .fc{ width:30px; height:30px; flex:none; border-radius:50%; overflow:hidden; display:block }
.chip .fc svg{ width:100%; height:100%; display:block }

/* the age grid. A deliberate 4 by 2, not a pill row that wrapped. */
/* ROUND 24: the wide branch was `repeat(8,...)` because AGES held exactly
   eight years. It holds eleven now (5 to 15), and eight columns split those
   into 8 + 3 - a full row and a third of one. The count is no longer typed
   here: `buildAges()` writes `--age-cols` from the number of chips it just
   made, so this rule cannot disagree with the constant again. The fallback
   in the var() is the old value, for the case where the script has not run. */
.ages{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:var(--s2) }
@media (min-width:760px){ .ages{ grid-template-columns:repeat(var(--age-cols,8),minmax(0,1fr)) } }
.ages .chip{ padding:0; justify-content:center; font-variant-numeric:tabular-nums }

/* the three vibe chips carry a drawn face each */
.vibes{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--s2) }
.vibes .chip{
  flex-direction:column; gap:var(--s1); padding:var(--s3) var(--s2);
  min-height:112px; text-align:center; font-size:var(--t-sm);
}
.vibes .chip .fc{ width:46px; height:46px }
.vibes .chip .tick{ position:absolute }
.vibes .chip{ position:relative }
.vibes .chip .tick{ top:var(--s2); right:var(--s2); width:16px; height:16px }

/* a guest chip: face, name, and the remove control is the chip itself */
.guests .chip{ padding-left:var(--s1); padding-right:var(--s3); position:relative }
.guests .chip .x{ width:16px; height:16px; opacity:.6 }
.guests .chip:hover .x{ opacity:1 }

/* ROUND 74: the two second receipt for a guest who just went in. It sits ON
   the face, absolutely, so the chip's width never changes and the row under
   it never reflows - the same refusal the .tick rule makes further up, and it
   matters more here because these chips wrap. The fill is the pressed chip's
   own fill, so the tick reads as the chip confirming rather than as a new
   colour arriving, and the stroke is currentColor, which is --ink. */
.guests .chip .ok{
  position:absolute; left:var(--s1); top:50%; margin-block-start:-15px;
  width:30px; height:30px; border-radius:50%; padding:5px;
  background-color:color-mix(in srgb,var(--tone) 62%,var(--su));
  color:var(--ink);
  animation:guest-ok 2320ms var(--ease) both;
}
@keyframes guest-ok{
  0%{ opacity:0; transform:scale(.5) }
  8%{ opacity:1; transform:none }
  86%{ opacity:1; transform:none }
  100%{ opacity:0; transform:scale(.92) }
}
/* the tick is a state receipt, not decoration, so reduced motion keeps it and
   drops only the movement */
@media (prefers-reduced-motion:reduce){
  @keyframes guest-ok{ 0%{ opacity:0 } 8%{ opacity:1 } 86%{ opacity:1 } 100%{ opacity:0 } }
}

/* the theme swatches. A ramp NEEDS a solid colour under it: a gradient in the
   `background` shorthand leaves background-color transparent. */
.themes{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--s3) }
@media (min-width:560px){ .themes{ grid-template-columns:repeat(3,minmax(0,1fr)) } }
.th{
  appearance:none; border:0; cursor:pointer; padding:0;
  border-radius:var(--r); overflow:hidden; text-align:left;
  background-color:var(--su); color:var(--ink);
  display:block; box-shadow:none;
  container-type:inline-size;
  transition:transform 180ms var(--ease),box-shadow 180ms var(--ease);
}
.th:hover{ transform:translateY(-2px) }
.th[aria-pressed=true]{ box-shadow:var(--sh-sm) }
.th .sw{
  height:118px; display:block;
  background-color:var(--th-gold,#ffc53d);
  background-image:linear-gradient(128deg,var(--th-a,#f0f) 0%,var(--th-gold,#ffc53d) 52%,var(--th-b,#0ff) 100%);
  position:relative; overflow:hidden;
}
/* ROUND 83. The theme's own furniture, drawn WHOLE on its own ramp. Read the
   law paragraph over `themeButton()` in start.page.js before changing a
   colour here. The three regions never meet: balloons rise on the left out
   of the bottom edge, the invitation's icon sits bottom right, the tick is
   the top right corner and nothing else is ever there. The container query
   is what keeps that true - the strip is two columns under 560 and THREE
   from 560, so a 560px window hands this button a NARROWER box than a 559px
   window does, and a viewport media query would size the art for the wrong
   one. 200px is the width at which the two pieces would start to touch. */
.th .sw .blns{
  position:absolute; left:14px; bottom:0; width:84px; height:104px;
  display:block; color:var(--th-ink,#2a1538);
}
.th .sw .thart{
  position:absolute; right:10px; bottom:8px; width:78px; height:78px; display:block;
}
.th .sw .thart svg{ width:100%; height:100%; display:block }
.th .sw .dot{
  position:absolute; right:10px; top:10px; width:20px; height:20px;
  opacity:0; transform:scale(.6); transition:opacity 180ms var(--ease),transform 180ms var(--ease);
}
.th[aria-pressed=true] .sw .dot{ opacity:1; transform:none }
@container (max-width:200px){
  .th .sw{ height:94px }
  .th .sw .blns{ left:8px; width:52px; height:66px }
  .th .sw .thart{ right:6px; bottom:4px; width:52px; height:52px }
  .th .sw .dot{ right:8px; top:8px; width:17px; height:17px }
}
.th .nm{
  display:block; min-height:48px; padding:var(--s2) var(--s3);
  font-size:var(--t-sm); font-weight:600;
  background-color:color-mix(in srgb,var(--tone) 12%,var(--su));
}
.th[aria-pressed=true] .nm{ font-weight:700 }

/* ---------------------------------------------------------------
   THE FLAGSHIP COMPONENT: the table object.
   Everything the parent makes is an object on the table. The shape,
   the size and the paper are the information, so no two objects on
   one screen are the same size and this never becomes a card grid.

   container-type sits on the OUTER element. Nothing inside an object
   may use a vw unit: an object inside a container query that reads vw
   sizes against the viewport, not against the object.
   --------------------------------------------------------------- */
/* ROUND 82, Siavash: "in mobile view they should be centered". The objects
   were packed to the start of a column they rarely fill, so a 284px card in a
   350px gutter sat 66px off-centre with the whole gap on one side. Centred at
   every width; `.set > .obj` below already did the same for a lone object
   from 900 up, and now does it on a phone too. */
.objs{ display:flex; flex-wrap:wrap; justify-content:center; align-items:flex-start;
       gap:var(--s6) var(--s5) }
.objs.stack{ flex-direction:column; align-items:center }
.set > .obj{ margin-inline:auto }

.obj{
  container-type:inline-size;
  margin:0; min-width:0;
  transform:rotate(var(--tilt,0deg));
  transition:transform 260ms var(--ease),opacity 260ms var(--ease);
}
.obj-face{
  position:relative;
  border-radius:var(--r);
  background-color:var(--su);
  box-shadow:var(--sh);
  overflow:hidden;
}
/* THE CAPTION UNDER AN OBJECT.
   ROUND 82, Siavash: the line under each mock "should look better and more
   professional". It was body copy at body weight, ragged against the bottom
   of a tilted object, and at 30ch it broke a two-sentence caption into five
   short lines that looked like a wrap accident.

   What it is now is a MUSEUM LABEL, which is what it always was: a small
   plate of the page's own paper, set in from the object's edge, with the
   first sentence carrying the weight and the rest of it reading as the note.
   The rank comes from size, weight and a fill - never from a rim, and never
   from a lower contrast on the part that matters. `--mut` on `--bg` is the
   page's own 7:1 pair and it does not move here.

   The counter-rotation stays: the object is tilted and the words on the
   label are not, because a tilted sentence is a sentence somebody has to
   turn their head to read. */
.obj-say{
  font-size:var(--t-sm); line-height:1.5; font-weight:500; color:var(--mut);
  margin:var(--s3) 0 0; max-width:46ch;
  padding:var(--s2) var(--s3);
  border-radius:var(--r-sm);
  background-color:color-mix(in srgb,var(--ink) 3%,transparent);
  text-wrap:pretty;
  transform:rotate(calc(var(--tilt,0deg) * -1));
}
/* the first sentence is the claim and the rest of it is the note. `.lead`
   is written by nothing: the markup carries it where a caption has two
   parts, and a caption with one part reads exactly as it did. */
.obj-say .lead{ color:var(--ink); font-weight:700; display:block }

/* the television */
.obj[data-kind=tv]{ --tilt:0deg; flex:1 1 100%; width:100%; max-width:640px }
.obj[data-kind=tv] .obj-face{
  border-radius:var(--r-lg);
  padding:14px 14px 18px;
  background-color:var(--su);
  background-image:linear-gradient(180deg,color-mix(in srgb,var(--tone) 26%,var(--su)) 0%,var(--su) 42%);
}
.tv-screen{
  position:relative;
  aspect-ratio:16/9;
  border-radius:22px;
  overflow:hidden;
  /* SOLID ground first: a ramp in the `background` shorthand would leave
     background-color transparent and the screen would show the table through
     it.

     ROUND 78, Siavash: "when you change the theme it should change the
     overall design in the right side preview". The ground WAS a fixed
     #1d1738 for all six themes, with only a 10 percent tone glow and the
     category label moving - so picking Jungle over Party swapped one word
     and a hint of haze, under a caption that promises "the theme colours the
     whole game". Measured on the real page before the change: the ground
     pixel was byte-identical across all six.

     It is now the THEME's own ground, built from the same two hues play.html
     rotates the game world through (lib.js THEMES h1/h2), which is why this
     is honesty rather than decoration - the preview paints what the game
     paints. start.page.js applyTone() sets the pair; the fallbacks keep the
     old colour if anything ever fails to set them.

     CONTRAST, computed over all six themes against the LIGHTEST pixel the
     ramp paints, not sampled from one: the tone-coloured label floors at
     8.88:1 (Midnight) and the muted ink at 9.97:1 (Jungle). Both are ABOVE
     the 7.45:1 this rule used to carry, so the floor went up, not down. The
     glow stays 10 percent for the reason the old note gives. */
  background-color:var(--tv-g1,#1d1738);
  background-image:
    radial-gradient(78% 70% at 50% 8%,color-mix(in srgb,var(--tone) 10%,transparent) 0%,transparent 66%),
    linear-gradient(162deg,var(--tv-g1,#1d1738) 0%,var(--tv-g2,#241a44) 100%);
  display:grid; place-items:center;
  color:#fffdf4;
}
.tv-screen iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; display:block }
.tv-feet{
  display:flex; justify-content:center; gap:96px; margin-top:12px;
}
.tv-feet i{ display:block; width:38px; height:8px; border-radius:0 0 6px 6px;
            background-color:color-mix(in srgb,var(--ink) 22%,var(--su)) }

/* the run playing inside the television. Every node here repaints alone. */
.run{ position:absolute; inset:0; display:grid; grid-template-rows:auto 1fr auto;
      gap:10px; padding:18px 20px; text-align:left; align-content:stretch }
.run .top{ display:flex; align-items:center; justify-content:space-between; gap:10px }
.run .cat{ font-family:var(--body); font-size:13px; font-weight:700; letter-spacing:.02em;
           color:var(--tone) }
.run .of{ font-size:12px; font-weight:500; color:#efeafc }
.run .mid{ display:flex; align-items:center; gap:16px; min-width:0 }
.run .ico{ width:76px; height:76px; flex:none }
.run .ico svg{ width:100%; height:100%; display:block }
.run .tx{ min-width:0 }
.run .ti{ font-family:var(--disp); font-weight:600; font-size:21px; line-height:1.1;
          margin:0 0 6px; color:#fffdf4 }
.run .le{ font-size:13.5px; line-height:1.45; margin:0; color:#efeafc;
          display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden }
/* `min-width:0`, and it is load bearing. A grid item's automatic minimum is
   its CONTENT, so without this an over-long answer chip does not ellipsise -
   it widens this whole row past the television, which then clips it with
   `overflow:hidden` and no ellipsis and no scrollbar, so nothing on the page
   reports it. Caught by the round 21 fit probe, 2026-09-05: a 49 character
   label read scrollWidth === clientWidth (no clipping) while the row it sat
   in had grown from 224px to 340px inside a 252px screen. */
.run .bot{ display:flex; align-items:center; gap:12px; min-width:0 }
.run .dial{ width:44px; height:44px; flex:none; position:relative; display:grid; place-items:center }
.run .dial svg{ width:44px; height:44px; position:absolute; inset:0 }
.run .dial .n{ font-family:var(--disp); font-weight:600; font-size:16px;
               font-variant-numeric:tabular-nums; color:#fffdf4; position:relative }
.run .dial circle{ transition:stroke-dashoffset 200ms linear }
.run .go{ font-size:13px; font-weight:600; color:#efeafc }
@media (prefers-reduced-motion:reduce){ .run .dial circle{ transition:none } }

/* ---------- the second card shape: answered, not counted ----------
   Four of the library's six mechanics have no number to count down, so the
   dial cannot draw them and the television simply never showed them. The
   cost of that was the CHIP, not the mechanic: `.cat` prints the challenge's
   group, and the counted filter's eligible pool here is 41 cards drawn from
   SIX of the library's nine groups, so Brain, Luck and Questions were never
   on this screen at all. `choice`, `quiz` and `wheel` are answered rather
   than counted, and they render their options here instead of a dial.

   The options take the WHOLE bottom row, and that is the measurement
   talking. `.bot` is 572px wide at 1440 and 768, 419 at 1024, 294 at 390 and
   224 at 320. Reserving the forward label beside them - "They guessed it" is
   94px at the small branch - leaves 130px at 320 for a strip whose narrowest
   honest four words measure 168, so it does not fit at 320, 360 or 390; and
   the label cannot be revealed after the answer without moving every chip,
   which is the no-blink rule. So the dial AND the label are both out in this
   mode and the row is one thing: in dial mode the row is what the room does
   next, in pick mode it is what the room is choosing between. The lit chip
   is the answer, the way `play.html`'s own option turns gold.

   This mode is strictly SHORTER than the dial mode it replaces - a 20px row
   at 390 against a 38px one - so `.mid`, the 1fr row, only ever gains and
   the 16/9 bezel is never in question. Do not add an avatar or a lettered
   disc without re-running the fit probe: measured 2026-09-04, four chips
   carrying a 14px face want 250px at 390 (fits) and the same four want 250
   in a 224px row at 320 (does not).

   The lede stays. Index's phone hides it because its grid takes the lede's
   own row; this row is a different one, `.mid` has 147px of slack at 1440
   and 28px at 390, and the card's own voice costs nothing here.

   The chips are sized by their LABELS and then grown to fill the row, never
   given an equal quarter each: `flex:1 1 auto` bases each on its own text
   and shares the slack, so "One big one" beside "8", "9" and "10" is not
   handed a quarter and ellipsised. Content-sized, so a longer answer added
   to the library later cannot defeat it either. */
.run .pick{ display:none }
.run.k-pick .pick{ display:flex; flex:1 1 auto; min-width:0; gap:7px }
.run.k-pick .dial,.run.k-pick .go{ display:none }
/* Stated as a colour, never as an opacity: a contrast probe reads the
   computed colour and never the alpha it is multiplied by, so a chip faded
   with `opacity` measures as though it were full strength and passes a check
   it should fail. The ground is `.tv-screen`'s own #1d1738 - the tone glow
   is a radial that has run out well above this row - lifted the same 15% the
   real card lifts a resting option. */
.run .pick .pc{ flex:1 1 auto; min-width:0; text-align:center;
                overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
                background:rgba(255,255,255,.15); border-radius:6px;
                padding:4.5px 9px; font-size:12px; line-height:1.3;
                font-weight:600; color:#fff6e9;
                transition:background .22s ease,color .22s ease }
/* the answer, in the same gold the dial's ring is drawn in */
.run .pick .pc.on{ background:#ffc53d; color:#2a1508 }
@media (prefers-reduced-motion:reduce){ .run .pick .pc{ transition:none } }

@container (max-width:420px){
  .run{ padding:13px 14px; gap:8px }
  .run .ico{ width:52px; height:52px }
  .run .ti{ font-size:16px }
  .run .le{ -webkit-line-clamp:2; font-size:12.5px }
  .run .dial,.run .dial svg{ width:38px; height:38px }
  .run.k-pick .pick{ gap:5px }
  .run .pick .pc{ padding:3.5px 6px; font-size:10px; border-radius:5px }
}
/* QUEUE #82, 2026-09-06. The second branch. At 320 this screen is 252 x 141.8
   and the branch above spends 26 of its 141.8 on padding and 16 on two row
   gaps, which left 99.8px for three rows that wanted up to 127. SIX of the
   nine cards in HERO_RUN were clipped by `.tv-screen`'s own `overflow:hidden`
   - by 5, 5, 6, 6, 23 and 27px - and the clip is silent: `bezelBottom` reads
   0.0 because nothing crosses the bezel, the lede's last line is simply gone.
   Measured before this rule, at 320:

     Around the house / timer  27   Kindness / count  23   Luck / wheel     5
     Dancing / timer            6   Dares / timer      6   Questions        5
     Talking / count            6

   The 27 was not the type scale at all. `.top` is a flex row of the group
   chip and "Challenge n of 12", and "Around the house" WRAPPED there: that
   row measured 42.1px against every other card's 21.0, so 21 of the 27 was
   one heading breaking in two. It is nowrap here, at a size that fits: the
   two labels want 100 and 88 of the 228px row.

   The rest is the budget. This branch takes the padding to 10/12 and the gaps
   to 6, which buys 16px back, then spends the three rows inside it:
   top 15 + mid (icon 48, or a 2-line title over a 1-line lede at 51.6) +
   bot 32. The lede's third and second lines are what go, in that order, and
   they go at 320 and 360 ONLY - the container is the `.obj`, 280px wide at a
   320 viewport and 320px at a 360 one, so 375 and up never see this rule and
   keep the 2-line lede the comment above defends.

   The trigger is 330px of CONTAINER, not of viewport: `container-type` sits
   on `.obj`, and `.obj` is the screen plus 28px of bezel. */
@container (max-width:330px){
  .run{ padding:10px 12px; gap:6px }
  .run .top{ gap:8px }
  .run .cat,.run .of{ white-space:nowrap; line-height:1.25 }
  .run .cat{ font-size:11.5px }
  .run .of{ font-size:11px }
  .run .ico{ width:48px; height:48px }
  .run .ti{ font-size:14px; margin-bottom:4px;
            display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
            overflow:hidden }
  .run .le{ -webkit-line-clamp:1; font-size:12px; line-height:1.4 }
  .run .dial,.run .dial svg{ width:32px; height:32px }
  .run .dial .n{ font-size:13px }
  .run .go{ font-size:12px }
}

/* ---------------------------------------------------------------
   the invitation card

   ROUND 74. This preview was its own object and not the thing it was
   previewing: a 9px band, a left aligned kicker, a cake floating in the
   1fr row of a 5:7 box, and the two facts as loose lines at the foot. A
   card with four short strings on it inside a fixed ratio is mostly
   hole, and the hole was the middle of it.

   It is now the card a guest actually opens. invite.html's composition,
   at this width: a deep drawn band, the tag pinned off the centre axis,
   the stamp across the fold, the name centred over an accent rule, and
   the facts as rows with their own pictures on the same row fill. Every
   rule below has a counterpart in invite.html between its `.inv` and
   `.inv-row` blocks, and the two move together.

   The 5:7 box went with it. invite.html's card is sized by what is on it
   and so is this one.

   THE ACCENT CARRIES NO TEXT, in either file. `--tone` is a ground and a
   rule here; the tag that sits on the band is cut from the card's own
   paper for exactly that reason.

   cqw and px only. `.obj` is the container, and a vw unit in here would
   measure the viewport instead of the card.
   --------------------------------------------------------------- */
/* ROUND 76. THE CARD WEARS THE TEMPLATE THAT GETS SENT, NOT THE FUNNEL.
   Round 75 joined the game theme to the invitation template so the right
   card is POSTED. It did not change what the parent WATCHES: this card went
   on painting `--tone` (lib.js's game gold) on `--su` (funnel cream) in all
   six themes, and measured against the template that would actually be sent,
   six of six disagreed - party previewed amber and sent red, night previewed
   periwinkle on cream and sent amber on near black. That is the same defect
   #159 was filed to fix, one layer further in.

   start.page.js `applyTone()` now writes the resolved template's own five
   values onto THIS element as inline custom properties. They are the funnel's
   OWN token names on purpose: `--su`, `--ink` and `--tone` inherit, so every
   rule below, and `cardBandArt()`'s `var(--su)` / `var(--ink)` fills, follow
   the template without one selector being rewritten. The funnel around the
   card keeps the root values and is untouched.

   The two that are NOT funnel tokens are the two invite.page.js derives
   rather than stores, and they carry the same derivation here:
     --c-well  the paper lifted 18% toward white. Every well inside the card
               (the tag, the stamp, the row pictures) is cut from it, never
               from the raw paper: on `night` and `ink` the paper is near
               black and a well cut from it swallows the drawing in it.
     --c-row   the template's own paper, one step away in lightness with a
               little more colour in it. lib.js's cardRowFill() owns that
               derivation for every surface that draws a card; round 77
               replaced a 13% mix of the second colour, which went through
               grey on the four styles whose pair is complementary.
   Both fall back to the cream-funnel value, so a card with no JS is the card
   this file drew before round 76.

   NO TEXT ON THIS CARD IS `--mut` ANY MORE. invite.html's law is that text is
   always --c-ink on --c-paper or on --c-row, and a muted funnel ink is
   unreadable on the four dark or tinted papers. The kicker earns its rank
   from case, tracking and size, the way `.card-row .k` beside it already
   does. */
.obj[data-kind=card]{ --tilt:-2.4deg; flex:0 1 268px; width:100%; max-width:284px }
.obj[data-kind=card] .obj-face{ padding:0; color:var(--ink) }

.card-band{
  position:relative; height:clamp(76px,30cqw,98px);
  background-color:var(--tone);
}
.card-band .bandart{ position:absolute; inset:0; width:100%; height:100%; display:block }
/* off the centre axis: the stamp owns the middle of the band, so two
   centred objects would stack on one line. Pinned top left, the way a
   label is put on a thing. */
.card-tag{
  position:absolute; left:clamp(11px,4.4cqw,15px); top:clamp(10px,3.8cqw,14px);
  transform:rotate(-2.2deg); transform-origin:left center;
  background-color:var(--c-well,var(--su)); color:var(--ink);
  font-family:var(--disp); font-weight:700;
  font-size:clamp(9.5px,3.3cqw,11px); line-height:1;
  letter-spacing:.11em; text-transform:uppercase; white-space:nowrap;
  margin:0; padding:7px 11px; border-radius:var(--r-pill);
  box-shadow:var(--sh-xs);
}
/* across the fold. A translate and not a negative `bottom`: the overhang
   has to be a share of the stamp's OWN size, and that size clamps.
   z-index because `.card-in` is positioned and comes after it in the
   flow; without it the copy's stacking context paints over the overhang. */
.card-stamp{
  position:absolute; left:50%; bottom:0; z-index:1;
  transform:translate(-50%,38%);
  width:clamp(56px,24cqw,72px); aspect-ratio:1;
  border-radius:var(--r); overflow:hidden;
  background-color:var(--c-well,var(--su)); display:grid; place-items:center;
}
.card-stamp .scene,.card-stamp svg{ width:100%; height:100%; display:block; flex:none }

/* the top pad clears the stamp's overhang and then gives it air */
.card-in{
  position:relative; isolation:isolate; text-align:center;
  padding:clamp(34px,12.5cqw,44px) clamp(14px,5cqw,20px) clamp(16px,5cqw,20px);
}
/* the house 26px dot lattice under the copy, in the card's own ink at 7%,
   the same paper invite.html draws. A 7% dot under 12:1 ink is still AAA. */
.card-in::before{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  color:var(--ink); opacity:.07;
  background-image:radial-gradient(currentColor 1.5px,transparent 1.5px);
  background-size:26px 26px;
}
@media(prefers-reduced-data:reduce){ .card-in::before{ display:none } }

.card-in .kicker{ font-size:clamp(9.5px,3.2cqw,11px); font-weight:700; letter-spacing:.1em;
                  text-transform:uppercase; color:var(--ink); margin:0 }
.card-in .who{ font-family:var(--disp); font-weight:700; font-size:clamp(24px,8.4cqw,32px);
               line-height:1.04; letter-spacing:-.022em; margin:var(--s1) auto 0;
               max-width:15ch; text-wrap:balance; overflow-wrap:anywhere }
/* the accent as a rule under the name: a drawn line, no text on it */
.card-rule{ display:block; width:52px; height:5px; border-radius:var(--r-pill);
            background-color:var(--tone); margin:var(--s3) auto }

.card-rows{ display:grid; gap:var(--s2); text-align:left }
/* 16% of the tone over the card's own paper. The label and the value are
   BOTH --ink on it: --mut on this fill runs to 7.25 on Ocean, which is a
   rounding error away from the 7:1 the house holds, and the label earns
   its rank from case and tracking instead. */
.card-row{
  background-color:var(--c-row,color-mix(in srgb,var(--tone) 16%,var(--su)));
  border-radius:var(--r-sm); padding:9px 11px;
  display:flex; align-items:center; gap:9px;
}
.card-row .ra{ width:30px; height:30px; flex:none; border-radius:10px; overflow:hidden;
               background-color:var(--c-well,var(--su)); display:grid; place-items:center }
.card-row .ra .scene,.card-row .ra svg{ width:100%; height:100%; display:block; flex:none }
.card-row .rt{ display:grid; gap:1px; min-width:0 }
.card-row .k{ font-family:var(--disp); font-weight:700; font-size:10px;
              letter-spacing:.1em; text-transform:uppercase; color:var(--ink) }
.card-row .v{ font-size:13px; font-weight:600; line-height:1.35; color:var(--ink);
              margin:0; min-width:0; overflow-wrap:anywhere; text-wrap:pretty }
/* invite.html drops the row pictures under 390 because its card is nearly
   the whole gutter there. This one is NOT that card: `.obj` caps at 284 and
   the narrowest it renders is 280, at a 320 viewport, so the same rule here
   would be a container query that can never fire. It is not written. The
   address wraps inside its own column instead, and the row grows with it. */

/* ---------- the card at half a phone gutter (round 82) ----------
   The comment two rules up says invite.html drops its row pictures under 390
   and that the same rule here "would be a container query that can never
   fire, [so] it is not written". Round 82 is when it fires: the tour's
   invitation now sits beside the reply block on a phone and the card is
   handed about 170px.

   EVERY NUMBER BELOW IS THE 268px ONE TIMES THE WIDTH RATIO, rounded to
   something the type scale can hold, and nothing here changes the card's
   composition: the band, the tag over the fold, the stamp on the axis, the
   rule under the name and the rows are the same objects in the same order.
   The row picture is the one thing removed, for invite.html's own reason -
   a 30px well beside an 11px value at this width is a picture with a caption
   stuck to it rather than a row with a picture on it.

   200px of CONTAINER and not of viewport: `container-type` is on `.obj`, so
   this reads the card's own width and fires wherever the card is that narrow,
   including a future layout nobody has written yet. */
@container (max-width:200px){
  .card-band{ height:clamp(48px,30cqw,98px) }
  .card-tag{ font-size:clamp(8px,4.6cqw,11px); padding:4px 7px; letter-spacing:.07em;
             left:clamp(6px,4.4cqw,15px); top:clamp(6px,3.8cqw,14px) }
  .card-stamp{ width:clamp(38px,26cqw,72px) }
  .card-in{ padding:clamp(24px,15cqw,44px) clamp(9px,6cqw,20px) clamp(11px,6cqw,20px) }
  .card-in::before{ background-size:17px 17px }
  .card-in .kicker{ font-size:clamp(7.5px,4.4cqw,11px); letter-spacing:.07em }
  .card-in .who{ font-size:clamp(15px,9.6cqw,32px) }
  .card-rule{ width:32px; height:3px; margin-block:var(--s2) }
  .card-rows{ gap:6px }
  .card-row{ padding:5px 7px; gap:0 }
  /* the picture goes and the text takes the whole row, which is
     invite.html's own rule under 390 */
  .card-row .ra{ display:none }
  .card-row .k{ font-size:7.5px; letter-spacing:.07em }
  .card-row .v{ font-size:9.5px; line-height:1.3 }
  .card-msg{ margin-top:var(--s2); padding:6px 7px; font-size:9px; line-height:1.35 }
  .card-note{ margin-top:var(--s2); padding:4px 8px 4px 4px; gap:5px }
  .card-note .na{ width:18px; height:18px }
  .card-note .nt{ font-size:8.5px }
}

/* the reply block at the same width, for the same reason. It keeps all three
   answers and all three fields - the allergy field is the point of the step
   and a mock that drops it is a mock of a different product. */
@container (max-width:200px){
  .pn-h{ font-size:12px; margin-bottom:var(--s2) }
  .pn-ans{ margin-bottom:var(--s3); gap:5px }
  .pn-ans li{ padding:6px 7px; font-size:9.5px; gap:6px }
  .pn-ans li .bx{ width:13px; height:13px; border-radius:5px }
  .pn-f{ margin-bottom:var(--s2); gap:2px }
  .pn-f .fl{ font-size:8px }
  .pn-f .fw{ font-size:9.5px; padding:6px 7px; min-height:26px }
  .pn-go{ font-size:9.5px; padding:8px 10px }
}

/* the receipt */
.obj[data-kind=receipt]{ --tilt:1.6deg; flex:0 1 214px; width:100%; max-width:224px }
.obj[data-kind=receipt] .obj-face{
  border-radius:var(--r-sm) var(--r-sm) 0 0;
  padding:var(--s4) var(--s4) 26px;
  /* the torn edge is a mask, not a border image */
  -webkit-mask-image:linear-gradient(#000 calc(100% - 12px),transparent calc(100% - 12px)),
    radial-gradient(7px 12px at 7px 100%,#000 98%,transparent 100%);
  -webkit-mask-size:100% 100%,14px 12px;
  -webkit-mask-position:0 0,0 100%;
  -webkit-mask-repeat:no-repeat,repeat-x;
  mask-image:linear-gradient(#000 calc(100% - 12px),transparent calc(100% - 12px)),
    radial-gradient(7px 12px at 7px 100%,#000 98%,transparent 100%);
  mask-size:100% 100%,14px 12px;
  mask-position:0 0,0 100%;
  mask-repeat:no-repeat,repeat-x;
}
.rc-h{ font-family:var(--disp); font-weight:600; font-size:16px; margin:0 0 var(--s3);
       letter-spacing:-.01em }
.rc-l{ list-style:none; margin:0; padding:0 }
.rc-l li{ display:flex; justify-content:space-between; gap:var(--s2);
          font-size:13px; line-height:1.5; padding:5px 0 }
.rc-l li b{ font-weight:600 }
.rc-l li span{ color:var(--mut); font-variant-numeric:tabular-nums; flex:none }
.rc-t{ display:flex; justify-content:space-between; gap:var(--s2); margin-top:var(--s3);
       padding-top:var(--s2); font-size:14px; font-weight:700 }

/* the run sheet, as a strip of tape */
.obj[data-kind=tape]{ --tilt:-1.1deg; flex:1 1 100%; width:100%; max-width:520px }
.obj[data-kind=tape] .obj-face{
  border-radius:var(--r-sm);
  padding:var(--s3) var(--s4);
  background-color:color-mix(in srgb,var(--sun) 46%,var(--su));
  box-shadow:var(--sh-sm);
}
.tape-l{ list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:var(--s2) var(--s4) }
.tape-l li{ font-size:13px; font-weight:600; display:flex; align-items:baseline; gap:6px }
.tape-l li i{ font-style:normal; font-variant-numeric:tabular-nums; color:var(--mut); font-weight:500 }

/* Screen 9 holds two objects and no growing field list, so it gets the wide
   object column and sits them side by side rather than stacking to 1,200px
   with an empty column beside it.

   The card's track is a DEFINITE 268px, not `auto`. With `auto` the track
   sized to the card's max-content, the card's own `width:100%` resolved
   against that to zero, and its absolutely positioned face then painted 120px
   past the right edge of the page: one horizontal scrollbar and fourteen
   offscreen elements, from a track that measured 0px. */
/* The preview is a real page, not a still: it needs the shape of the thing a
   kid actually holds. At 16/9 the game's own intro copy was clipped mid
   sentence inside the frame.

   ROUND 63: EVERY SELECTOR IN THIS BLOCK MOVED FROM #s9 TO #s10, and this is
   the reason the round's first build of the new screen came out wrong rather
   than a note about tidiness. "Keep it" was screen 9 and is screen 10 now; the
   price screen took the number. These six rules stayed pointed at #s9 and
   silently landed on a screen they were never written for: a 3/4 television
   where the new one wants 16/9, a .72/1.28 column split, and a 268px card
   track for a card this screen does not have. The television filled the whole
   right column and ran off the bottom of the page, and nothing errored. An id
   selector does not know a screen was renumbered under it.

   ROUND 78 MOVED THEM AGAIN, #s10 -> #s11, for the same reason and with this
   paragraph already on the page to say what happens if they are left behind.
   The tour screen is the new 9, so the price is 10 and "Keep it" is 11; these
   rules belong to KEEP - the card and the receipt - and the price screen has
   its own television that wants none of them. */
#s11 .tv-screen{ aspect-ratio:3/4 }
@media (min-width:1200px){
  #s11 > .in{ grid-template-columns:minmax(0,.72fr) minmax(0,1.28fr); align-content:center }
  #s11 .obj[data-kind=tv]{ max-width:440px }
  /* ROUND 74. The card track was `minmax(0,1fr)` and the card was let to
     max-width:100% in it, which was safe only while a 5:7 box kept the card
     card shaped at any width it was handed. The card is sized by its content
     now, and 100% of this track drew it 487px wide and 390 tall: a landscape
     banner with the same four strings on it. The track is the card's OWN
     width, the one screens 4 and 5 give it, and the block sits left rather
     than stretching to fill a column. */
  #s11 .objs{ display:grid; grid-template-columns:minmax(0,284px) minmax(0,268px);
             gap:var(--s5); align-items:start; justify-content:start }
  #s11 .obj[data-kind=tv]{ max-width:none }
  #s11 .obj[data-kind=card]{ max-width:100% }
}

/* the reply cards, as a loose stack */
.obj[data-kind=reply]{ --tilt:0deg; flex:1 1 100% }
.obj[data-kind=reply] .obj-face{ background:transparent; box-shadow:none; overflow:visible;
                                 display:flex; flex-wrap:wrap; gap:var(--s3) }
.rep{
  flex:0 0 auto; width:auto; min-width:126px; max-width:100%;
  background-color:var(--su); border-radius:var(--r-sm);
  box-shadow:var(--sh-sm);
  padding:var(--s3);
  transform:rotate(var(--rt,0deg));
  display:flex; align-items:center; gap:var(--s2);
}
.rep .fc{ width:34px; height:34px; flex:none; border-radius:50%; overflow:hidden }
.rep .fc svg{ width:100%; height:100%; display:block }
.rep b{ font-size:13px; font-weight:600; line-height:1.2; overflow-wrap:anywhere }

/* the cake, as an object on the table rather than as progress */
.obj[data-kind=cake]{ --tilt:0deg; flex:0 1 320px; width:100%; max-width:340px }
.obj[data-kind=cake] .obj-face{ background:transparent; box-shadow:none; overflow:visible }
.obj[data-kind=cake] .obj-face svg.cstage{ width:100%; height:auto; display:block }

/* the three steps of screen 2, arriving in order */
.beats{ display:flex; flex-direction:column; gap:var(--s6) }
@media (min-width:900px){ .beats{ gap:var(--s7) } }
.beat{ animation:beat 520ms var(--ease) backwards }
.beat:nth-child(1){ animation-delay:60ms }
.beat:nth-child(2){ animation-delay:340ms }
.beat:nth-child(3){ animation-delay:620ms }
@keyframes beat{ from{ opacity:0; transform:translateY(16px) } to{ opacity:1; transform:none } }
@media (prefers-reduced-motion:reduce){
  .beat{ animation-duration:1ms; animation-delay:0ms }
}

/* ---------------------------------------------------------------
   Screen 3, the paid outcome. Plain words, the price named once,
   read out of TIERS.
   --------------------------------------------------------------- */
.price{ display:flex; align-items:baseline; gap:var(--s2); margin:0 0 var(--s2) }
.price .num{ color:var(--ink) }
.price .per{ font-size:var(--t-sm); font-weight:600; color:var(--mut) }
/* Round 20, the second price. The monthly sits UNDER the forever price and
   reads as the alternative, not as a competing headline: same row shape, one
   step down in size and in ink. No rule, no box and no chip separates them -
   the size step is the separation (borderless law). */
.price-alt{ margin:calc(var(--s2) * -0.5) 0 var(--s2) }
.price-alt .num.sm{ font-size:calc(var(--t-num) * 0.52); color:var(--mut) }
/* 2026-09-06, the sale. The struck regular price sits INSIDE the price row,
   before the charged number and on the same baseline, at the same step down in
   size and ink the alternative price used. It separates by size and colour and
   nothing else: no chip, no box and no rule (borderless law). `line-through`
   is the strike and not a pseudo-element rule across the box, because a rule
   drawn over a number IS a border and it does not follow the glyphs. */
.price .was{ font-size:calc(var(--t-num) * 0.46); font-weight:700; color:var(--mut);
  text-decoration:line-through; text-decoration-thickness:.08em }
.gets{ list-style:none; margin:0 0 var(--s4); padding:0; display:grid; gap:var(--s3) }
.gets li{ display:flex; gap:var(--s3); align-items:flex-start }
.gets li .ic{ width:34px; height:34px; flex:none; border-radius:10px;
              background-color:color-mix(in srgb,var(--tone) 30%,var(--su));
              display:grid; place-items:center }
.gets li .ic svg{ width:18px; height:18px }
.gets li b{ display:block; font-weight:600; font-size:var(--t-body) }
.gets li span{ display:block; font-size:var(--t-sm); color:var(--mut); line-height:1.45 }
/* ROUND 18. This was a filled pill: `border-radius:var(--r-pill)`, a mint
   fill, 48px min-height, sitting DIRECTLY ABOVE the real button on screen 3.
   Measured, the two read as the same object: the paragraph painted 271x52 at
   999px radius and the button 205x56 at 999px radius, so the bigger, first,
   pill-shaped thing on the screen was the one that could not be pressed.
   Siavash, 2026-09-04, on this screen: "this is confusing not easy to
   navigate."
   It keeps its exact words - the REPO-MAP records that the no-charge step
   reads "No card to build and preview" and nothing else - and gives up the
   only thing that made it look pressable, the fill and the pill. It is now
   the tick-and-text reassurance `index.html`'s hero already uses under its
   own button, so the two surfaces say this the same way. The markup moves it
   BELOW `.acts` for the same reason: reassurance follows the action. */
.nocard{
  display:inline-flex; align-items:center; gap:.5em; margin:var(--s3) 0 0;
  padding:0; min-height:0;
  font-size:var(--t-sm); font-weight:600; color:var(--mut);
}
/* `var(--mint)`, which is what `index.html:450` paints its own tick with. The
   first draft said `var(--good)`; that token is in DESIGN.md but is NOT
   declared in funnel.css, so it computed to nothing and the tick silently
   inherited the body colour. An undefined custom property does not warn. */
.nocard svg{ width:1.05em; height:1.05em; flex:none; color:var(--mint) }

/* ---------------------------------------------------------------
   Screen 8, the build. The one authored moment on this page.
   --------------------------------------------------------------- */
.walkers{ display:flex; flex-wrap:wrap; gap:10px; justify-content:center; padding:0 8px }
.walker{ width:46px; height:46px; flex:none; border-radius:50%; overflow:hidden;
         animation:walkin 420ms var(--ease) backwards }
/* specificity: `.tbl svg` is (0,1,1) and would win over `.walker svg`,
   so the walker's own rule names its container too. */
.walkers .walker svg{ width:100%; height:100%; display:block }
@keyframes walkin{ from{ opacity:0; transform:translateY(18px) scale(.8) } to{ opacity:1; transform:none } }
.build-n{ font-family:var(--disp); font-weight:600; font-size:44px; line-height:1;
          font-variant-numeric:tabular-nums; color:#fffdf4; margin:0 }
.build-l{ font-size:13px; font-weight:500; color:#efeafc; margin:6px 0 0 }
@media (prefers-reduced-motion:reduce){ .walker{ animation-duration:1ms } }

/* ---------------------------------------------------------------
   Screen 10. The no recovery sentence sits ABOVE the password field,
   never below it and never in a tooltip.
   --------------------------------------------------------------- */
.keep{ max-width:520px }
.norec{
  margin:var(--s4) 0 var(--s3);
  padding:var(--s4);
  border-radius:var(--r);
  background-color:color-mix(in srgb,var(--sun) 30%,var(--su));
  font-size:var(--t-body); line-height:1.55;
}
.norec b{ font-weight:700 }
.honest{
  margin:0 0 var(--s4);
  padding:var(--s4);
  border-radius:var(--r);
  background-color:var(--su);
  box-shadow:var(--sh-xs);
  font-size:var(--t-body); line-height:1.55;
}
.acct-live-only{ display:none }
.screen.acct-live .acct-live-only{ display:block }
.screen.acct-live .acct-off-only{ display:none }
.said{ font-size:var(--t-sm); color:var(--grape-d); font-weight:600; margin:var(--s3) 0 0;
       min-height:1.45em }

/* ---------------------------------------------------------------
   Reduced motion, walked end to end rather than asserted.
   --------------------------------------------------------------- */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:1ms !important;
    animation-iteration-count:1 !important;
    transition-duration:1ms !important;
    scroll-behavior:auto !important;
  }
}

/* the visually hidden label, for a control whose meaning is drawn */
.vh{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}

/* ---------------------------------------------------------------
   ROUND 63. The assistant's opening, and the screen where the price
   finally appears.

   Every rule below is an existing token arrangement, not a new one:
   no colour is introduced, every radius is --r-sm / --r / --r-lg /
   --r-pill, and the one shadow is --sh, which is the flat offset the
   paper world uses everywhere. Nothing here draws a border, a rim or
   a rest-state outline (borderless law); the lid is a picture painted
   over the television and is the only thing on this page that covers
   an object rather than sitting beside it.
   --------------------------------------------------------------- */

/* the assistant's own line, above the question it is asking. One rank
   below the question and in --mut, so it introduces and never competes.
   NOT an eyebrow label: it is a sentence naming who is speaking, which
   is the one thing a greeting screen has to establish before it asks a
   parent for their child's name. */
.hail{
  margin:0 0 var(--s2);
  font-family:var(--disp); font-weight:700;
  font-size:var(--t-sm); color:var(--mut);
}

/* "optional" beside a field label. It has to be legible - a parent who
   cannot read it will fill the field in anyway and the screen has
   gained a gate it does not have - so it is the same size as the label
   and separated by weight and ink, never by a chip or a rule. */
.f > .lb .opt{
  font-style:normal; font-weight:500; color:var(--mut);
  margin-left:.45em;
}

/* the line over the included list on screen 9. The rank between the
   reassurance and the list, so the list is read as a consequence of the
   payment above it rather than as a second offer. */
.alsoh{
  margin:var(--s5) 0 var(--s3);
  font-family:var(--disp); font-weight:700;
  font-size:var(--t-sm); color:var(--mut);
}
/* screen 9 carries the price, the button, the reassurance, this list and
   a second control, so the list gives back the vertical room screen 3
   can afford to spend. Same rows, closer together. */
.gets-tight{ gap:var(--s2); margin-bottom:0 }

/* ---------- screen 9, the tour ----------
   ROUND 78. Five features, one at a time, between the build and the price.
   Every value here is a token or a mix of two: the mark reuses `.gets li .ic`'s
   own tone mix at a larger size so the tour and the list beside the price are
   visibly the same five things, and nothing in this block sets a border, a
   rest-state outline or a blurred shadow - the paper world's offset shadow is
   the only one on the page. */
.tourdots{ list-style:none; display:flex; gap:7px; margin:0 0 var(--s4); padding:0 }
.tourdots li{ width:26px; height:6px; border-radius:999px; flex:none;
              background-color:color-mix(in srgb,var(--ink) 14%,var(--su));
              transition:background-color 220ms ease, width 220ms ease }
.tourdots li.past{ background-color:color-mix(in srgb,var(--tone) 62%,var(--su)) }
/* The live step is WIDER as well as darker. Colour alone carries the position
   for everybody who can see the hue and for nobody else; the width says it a
   second way, which is the same reason the rank is written out in words. */
.tourdots li.on{ width:40px; background-color:var(--ink) }
@media (prefers-reduced-motion:reduce){ .tourdots li{ transition:none } }

.tourstep{ margin:0 0 var(--s4) }
.tourmark{ width:56px; height:56px; display:grid; place-items:center; border-radius:18px;
           background-color:color-mix(in srgb,var(--tone) 30%,var(--su));
           box-shadow:var(--shadow-sm); margin-bottom:var(--s3) }
.tourmark svg{ width:30px; height:30px }
.tourrank{ margin:0 0 4px; font-size:var(--t-sm); font-weight:600; color:var(--mut) }
.tourti{ font-family:var(--disp); font-weight:600; line-height:1.05; margin:0 0 6px;
         font-size:clamp(25px,3.4vw,36px) }
/* THREE LINES OF ROOM, always. The five sentences are different lengths and
   they wrap differently at every width, so without a floor the step below
   this - the two buttons - moves as the tour advances, and the parent is
   pressing Next at a target that walks up and down the page. Measured at
   1456 all five already render 195px tall; at 390 they do not, which is the
   width this rule is for. `min-height` and not a fixed height: a fourth line
   at some future width grows the block rather than clipping it. */
.toursub{ margin:0; color:var(--mut); line-height:1.5; max-width:38ch;
          font-size:var(--t-body); min-height:calc(1.5em * 3) }
.gets-tight li span{ font-size:calc(var(--t-sm) * .94) }

/* the second control on screen 9 is a way out, not an offer. It sits
   below the list at the bottom of the reading order and is quieter than
   everything above it by rank alone. */
.acts-quiet{ margin-top:var(--s4) }

/* the quiet line under the tour's paragraph: the thing the feature also
   does, at the rank of a footnote and never at the rank of the promise. */
/* TWO LINES OF ROOM, for the reason `.toursub` above carries three: this
   line is the last thing above the buttons, so a step whose footnote runs to
   one line and a step whose footnote runs to two move the Next button
   between them. */
.tourfoot{ margin:var(--s2) 0 0; font-size:var(--t-sm); line-height:1.45;
           font-weight:600; color:var(--mut); max-width:38ch;
           min-height:calc(1.45em * 2) }

/* ---------- screen 9: the rail, the stage and the pinned pair ----------
   ROUND 82. Three of Siavash's calls, 2026-09-14, and all three are about
   the same thing: the tour was a narrow picture parked in a wide column,
   with the control that advances it 1,500px away from it on a phone.

   THE BAR THE PASS WAS MEASURED AGAINST is arc.net's own feature walk: the
   artifact is the stage, the step copy is a small fixed rail beside it, and
   the control that advances the walk never leaves the glass. The thing it
   does that this screen did not is the last one - there, the picture scrolls
   under a pinned control; here, the control scrolled away with the picture
   and the parent had to come back up the page to press it.

   THE SHAPE.
     phone    one column. Rail, then stage, then the bar pinned to the foot
              of the viewport for the whole of the scroll.
     >=900    two columns. The rail is column 1 row 1, the stage is column 2
              across both rows, and the bar is column 1 row 2 - which is a
              tall row, because the stage is what sizes it - pinned to the
              foot of the viewport while the stage scrolls past it.

   `.acts` is a plain flex row and stays one; everything below adds where it
   sits and what is under it. */
/* the bar is the last thing in the screen and it carries its own padding, so
   the 88px tail every other screen ends on is 88px of scroll under a pinned
   control with nothing in it */
#s9 > .in{ position:relative; padding-bottom:var(--s3) }

/* THE BAR. Opaque, and opaque on the EXACT ground under it.

   `body::before` is the page's wash and it is `position:fixed`, so the two
   tone radials on it are registered to the VIEWPORT and do not move when the
   page scrolls. A sticky bar painting flat `--bg` therefore reads as a
   lighter rectangle wherever the wash has tone in it, which at the foot of
   the viewport is the second radial's corner. The ground below is the same
   colour and the same two images with `background-attachment:fixed`, which
   registers them to the viewport the way `position:fixed` does: the bar and
   the page under it resolve to the same pixel, at any scroll, in any theme,
   because both read `--tone`.

   Above it, a fade and never a rule: a hairline under a sticky bar is a
   border by another name and this file draws none. It is the same
   construction `.strip` uses at the top of the page, flipped.

   Both pseudo elements bleed by `--pad-x` so the ground and the fade cover
   the gutter the bar sits in rather than stopping at the button's own box. */
.tourbar{
  position:sticky; z-index:4;
  /* `.ask` and `.set` carry `order` from the obj-first block, so a row with
     no order of its own sorts FIRST and the pinned pair lands above the
     heading. Last, at every width, in the eye and in the tab key both. */
  order:3;
  bottom:0;
  margin-top:var(--s5);
  padding-block:var(--s3);
  /* ONE ROW, never the column `.acts` is under 640. Two stacked 48px
     controls take 120px of a phone viewport and never give it back, and the
     two directions of one walk read as one control and a consolation prize
     when they are stacked. Next takes the width it deserves and Back is the
     pill beside it. */
  flex-direction:row; align-items:center; gap:var(--s2);
}
.tourbar .btn{ flex:1 1 auto; width:auto }
.tourbar .back{ flex:0 0 auto; width:auto }
.tourbar::after{
  content:""; position:absolute; z-index:-1;
  inset:0 calc(var(--pad-x) * -1);
  background-color:var(--bg);
  background-image:
    radial-gradient(120% 78% at 50% -14%, color-mix(in srgb,var(--tone) 13%,transparent) 0%, transparent 62%),
    radial-gradient(90% 60% at 8% 104%, color-mix(in srgb,var(--tone) 9%,transparent) 0%, transparent 58%);
  background-attachment:fixed;
  pointer-events:none;
}
.tourbar::before{
  content:""; position:absolute; z-index:-1;
  left:calc(var(--pad-x) * -1); right:calc(var(--pad-x) * -1);
  bottom:100%; height:24px;
  background-image:linear-gradient(transparent,color-mix(in srgb,var(--ink) 9%,transparent));
  pointer-events:none;
}
/* the home bar on a phone, and nowhere else: the tour bar is the last thing
   above it and a 48px control half under a home indicator is a 24px control */
@supports (padding:env(safe-area-inset-bottom)){
  .tourbar{ padding-bottom:calc(var(--s3) + env(safe-area-inset-bottom)) }
}

@media (min-width:900px){
  /* the stage takes the larger half. Screen 9 carries no growing field list
     and its rail is five fixed blocks, so the column that holds the artifact
     is the one that should have the room - the reverse of every screen that
     is still asking questions. */
  #s9 > .in{
    grid-template-columns:minmax(0,.82fr) minmax(0,1.18fr);
    grid-template-rows:auto 1fr;
    align-content:start;
  }
  #s9 .ask{ grid-column:1; grid-row:1 }
  #s9 .set{ grid-column:2; grid-row:1 / span 2 }
  /* row 2 is as tall as the stage makes it, so the bar has somewhere to
     travel: `align-self:end` parks it at the foot of that row and `sticky`
     lifts it to the foot of the viewport for everything above that. */
  #s9 .tourbar{ grid-column:1; grid-row:2; align-self:end; bottom:var(--s4) }
  /* NO GROUND AND NO FADE FROM 900 UP. The bar lives in column 1, row 2, and
     the only thing in that column is `.ask` in row 1 - which is ABOVE the
     bar's own grid area and so never travels under it. A ground painted here
     is a cream rectangle with a visible edge where the column ends, which is
     what the first build of this drew. The stage scrolls in column 2 and the
     bar is not over it. */
  #s9 .tourbar::after,#s9 .tourbar::before{ display:none }
  /* the pair sits at the foot of the rail, not stretched across it */
  #s9 .tourbar{ justify-content:flex-start; flex-wrap:wrap }
  #s9 .tourbar .btn{ flex:0 0 auto }
}

/* ---------- screen 9's five mocks ----------
   ROUND 79. Every step shows the thing it names. Four of the five reuse an
   object this page already owns - the invitation card, the receipt, the tape
   and the reply stack - so the tour is furnished out of the same paper world
   the rest of the funnel is, and only the plate and the running order needed
   a new one.

   `sheet` is that one: a plain leaf of paper with a heading and rows. It is
   NOT a second receipt (no torn edge, no tabular tail) because the two would
   then be the same object carrying different words, which is the whole defect
   this round exists to fix.

   Nothing in this block draws a border, a rest-state outline or a blurred
   shadow. The row separation is a fill one step off the paper, the same
   construction lib.js cardRowFill() uses on the card above it. */
.tourmock{ display:block }
.obj[data-kind=sheet]{ --tilt:-1.2deg; flex:0 1 320px; width:100%; max-width:330px }
.obj[data-kind=sheet] .obj-face{ padding:var(--s4) }
.mk-h{ font-family:var(--disp); font-weight:600; font-size:17px; letter-spacing:-.01em;
       margin:0 0 var(--s3); display:flex; align-items:baseline;
       justify-content:space-between; gap:var(--s2) }
.mk-n{ font-family:var(--body); font-size:var(--t-sm); font-weight:500; color:var(--mut);
       flex:none }
.mk-f{ margin:var(--s3) 0 0; font-size:calc(var(--t-sm) * .94); line-height:1.4;
       color:var(--mut) }

/* the running order, and the plate. One row shape, two lists: an icon well
   is the only thing the ideas add. */
.mk-ideas,.mk-rows{ list-style:none; margin:0; padding:0;
                    display:flex; flex-direction:column; gap:6px }
.mk-ideas li,.mk-rows li{
  display:flex; align-items:center; gap:var(--s2);
  padding:8px 10px; border-radius:var(--r-sm);
  background-color:color-mix(in srgb,var(--ink) 5%,var(--su));
}
.mk-ideas .ia{ width:34px; height:34px; flex:none; border-radius:11px; overflow:hidden }
.mk-ideas .ia svg{ width:100%; height:100% }
.it{ display:flex; flex-direction:column; gap:1px; min-width:0; flex:1 1 auto }
/* NOTHING IN A ROW IS EVER CUT. The first build put the challenge's own
   `l` here, clipped to one line with an ellipsis, and the lines are
   sentences up to 149 characters long: "Twelve seconds. All attitu..." reads
   as a rendering fault, not as a summary, and no clamp fixes that - at three
   lines a real running order still ended three of its eleven rows in a cut
   word. So the long line is not in the row at all. What is here instead is
   the row's own METADATA, which is short by construction: the group the
   challenge came from and how it is timed or counted. A complete small fact
   beats a truncated large one, and the whole sentence is one tap away on the
   dashboard this is a picture of. */
.it b{ font-size:13px; font-weight:600; line-height:1.25; overflow-wrap:anywhere }
.it i{ font-style:normal; font-size:11.5px; line-height:1.3; font-weight:600;
       color:var(--mut); overflow-wrap:anywhere }

/* the swap tag is a PICTURE of the control the planner has, at the size the
   planner draws it. It is a span, never a button: a control on a mock that
   does nothing when it is pressed is worse than no control at all. */
.sw{ flex:none; font-size:11px; font-weight:700; letter-spacing:.01em;
     padding:5px 9px; border-radius:var(--r-pill);
     background-color:color-mix(in srgb,var(--tone) 34%,var(--su)); color:var(--ink) }
.qt{ flex:none; font-size:14px; font-weight:700; font-variant-numeric:tabular-nums }

/* the aisles that are already in the basket. The tick is art.js's own check,
   inline, because a CSS glyph is not a picture this house is allowed to draw. */
.rc-tick li{ align-items:center }
.rc-tick .tk{ width:15px; height:15px; flex:none; display:inline-flex;
              color:var(--grape-d); margin-right:5px }
.rc-tick .tk svg{ width:100%; height:100% }
.rc-tick li b{ display:flex; align-items:center; min-width:0 }
.rc-tick li.got b{ color:var(--mut); text-decoration:line-through;
                   text-decoration-thickness:1.5px }

/* ---------- the invitation, whole (round 80) ----------
   The card's own law still holds over every line here: no text on this card
   is ever `--mut`, because --mut is a funnel ink and four of the seven
   templates are dark or tinted paper. The message and the note earn their
   rank from size and fill, never from a lower contrast.

   THE PAIR. At 900 and up the card and the reply block sit side by side, the
   way invite.html itself puts them in two columns, because that is the shape
   of the thing being shown: a card, and the answer under the guest's thumb.
   Below that they stack, card first. */
/* ROUND 82, Siavash: "in mobile put both columns like desktop beside each
   other". Stacked, this pair was 1,528px tall at 390 - the card alone filled
   two and a half phone screens, and the reply block, which is the half of the
   step the sentence beside it is actually about, sat below the fold of the
   fold. Side by side it is 695px and the relationship between the two - a
   card, and the answer under the guest's thumb - is one look, which is the
   whole reason invite.html puts them in two columns.

   THE CARD IS DRAWN FOR 268px AND THIS HANDS IT 170. Every fixed px size
   inside it - the row label at 10, the value at 13, the 30px row picture, the
   76px band floor - was chosen for that width, so at 170 it does not become a
   smaller card, it becomes the same card with every row wrapped to three
   lines. The container branch under `.obj[data-kind=card]` below is what
   makes 170 a size this object HAS rather than a size it survives, and
   `container-type` is already on `.obj`, so the query reads the card's own
   width and fires at exactly the widths the pair creates.

   `zoom` was tried first and reverted in the same round: it scales the
   captions with the objects, and a caption counter-scaled back to 14px is
   14px inside a 170px column, which is where the readable half of this step
   went. The captions keep the full type scale and their own column. */
.invpair{ gap:var(--s5) }
/* 380px OF VIEWPORT IS THE FLOOR, and it is the card that sets it. Swept at
   every width on screen 9: at 375 the pair hands the card 177px against a
   632px height, which is 0.280 - the exact value of the whole-invitation
   floor, with nothing left - and at 360 and 320 it is 0.270 and 0.227 with
   four and six elements pushed off the gutter. A 320px phone does not have
   the room for two columns and a card that is still a card, so it keeps the
   stack it had. Everything from 380 up gets the pair. */
@media (min-width:380px) and (max-width:899px){
  .invpair{ flex-wrap:nowrap; align-items:flex-start; gap:var(--s3) }
  /* two even columns, and `min-width:0` on both: a `nowrap` run sets the
     min-content of its whole row, and the card's own longest unbreakable
     string would otherwise push the pair past the gutter.

     `[data-kind]` and not a bare `.obj`: every per-kind rule in this file is
     `.obj[data-kind=x]`, which is one class plus one attribute, and a
     two-class selector ties with it and loses on source order. The first
     build of this line read `.invpair > .obj` and the panel kept its own
     `flex:0 1 300px` while the card took `1 1 0` - a 34px card beside a
     300px panel. The attribute puts this rule one step above both. */
  .invpair > .obj[data-kind]{ flex:1 1 0; min-width:0; max-width:none; width:auto }
  /* NOT AN EVEN SPLIT. The card carries six rows, a message and a note; the
     reply block carries three answers and three short fields. Even columns
     put the card at 167x658, which funnel-proof reads as 0.254 wide over
     tall and fails against the 0.28 floor a whole invitation holds - the
     probe is right, a card that narrow has stopped being card shaped. The
     card takes 1.2 of the row and the reply block takes 1. */
  .invpair{ gap:var(--s2) }
  /* and the same 300px ceiling the desktop pair holds: between 640 and 899
     the column is wide enough to hand the card 436px, which is 168px past
     the width it is drawn for and a different size of invitation from the
     one the desktop stage shows. The reply block takes what the cap frees. */
  .invpair > .obj[data-kind=card]{ flex:1.2 1 0; max-width:300px }
  /* HALF THE TILT IN THE NARROW PAIR. The tilt is a table-top affectation
     and it is paid for in gutter: a 2.4deg rotation on a 580px-tall card
     grows its bounding box by 24px of a 350px column, which put the card's
     own tag hard against the left edge of the phone. Halved, it costs 12 and
     the pair sits inside the gutter it is given. Nothing is straightened to
     0: two objects lying flat on this table is the one thing the flagship
     block says they never are. */
  .invpair > .obj[data-kind=card]{ --tilt:-1.2deg }
  .invpair > .obj[data-kind=panel]{ --tilt:.6deg }
}
@media (min-width:900px){
  .invpair{ flex-wrap:nowrap; align-items:flex-start }
  /* the card keeps its OWN width and the panel takes what is left. Without
     this the nowrap row shrinks both by their flex basis and the card came
     out at 250px, under the 268 it is drawn for: every row then wrapped to
     two lines and the card grew 60px taller to save 18px of width. */
  /* ROUND 82 takes the card from 268 to 300. `.invpair` is not a container -
     `container-type` sits on `.obj` itself - so a cqw here would resolve
     against the viewport and not against the pair. A definite track, for the
     reason the block below the receipt already records. */
  .invpair .obj[data-kind=card]{ flex:0 0 300px; max-width:300px }
  /* the panel takes the whole rest of the wider stage rather than stopping
     at the 320 it is capped to everywhere else on the page */
  .invpair .obj[data-kind=panel]{ flex:1 1 auto; min-width:0; max-width:none }
}
/* the host's own line, under the rows. A block, not a quote mark: the quote
   wash is invite.html's and it is derived per template there; here the same
   fill as a row keeps the card to two surfaces. */
.card-msg{
  margin:var(--s3) 0 0; text-align:left;
  background-color:var(--c-row,color-mix(in srgb,var(--tone) 16%,var(--su)));
  border-radius:var(--r-sm); padding:10px 12px;
  font-size:12.5px; line-height:1.4; font-weight:600; color:var(--ink);
  text-wrap:pretty;
}
/* the note chip, the last thing read on the card. `inline-flex`, so it is
   the width of its own sentence and reads as a thing put on the card rather
   than another band across it. */
.card-note{
  margin-top:var(--s3);
  display:inline-flex; align-items:center; gap:9px; text-align:left;
  background-color:var(--c-well,var(--su));
  border-radius:var(--r-pill); padding:6px 12px 6px 6px;
  box-shadow:var(--sh-xs);
}
.card-note .na{ width:28px; height:28px; flex:none; border-radius:50%; overflow:hidden }
.card-note .na .scene,.card-note .na svg{ width:100%; height:100%; display:block; flex:none }
.card-note .nt{ font-size:12px; font-weight:700; line-height:1.25; color:var(--ink) }

/* the reply block. A PICTURE of invite.html's own panel: three answers and
   three fields, one of them the allergy. Nothing in it is a real control -
   no <button>, no <input> - because a control on a mock that does nothing
   when it is pressed is worse than no control at all, and because a field
   here would take the keyboard away from the funnel's own. */
.obj[data-kind=panel]{ --tilt:1.1deg; flex:0 1 300px; width:100%; max-width:320px }
.obj[data-kind=panel] .obj-face{ padding:var(--s4) }
.pn-h{ font-family:var(--disp); font-weight:600; font-size:17px; letter-spacing:-.01em;
       margin:0 0 var(--s3) }
.pn-ans{ list-style:none; margin:0 0 var(--s4); padding:0;
         display:flex; flex-direction:column; gap:7px }
.pn-ans li{
  display:flex; align-items:center; gap:10px;
  padding:9px 11px; border-radius:var(--r-sm);
  background-color:color-mix(in srgb,var(--ink) 5%,var(--su));
  font-size:13px; font-weight:600; color:var(--ink);
}
.pn-ans li .bx{ width:20px; height:20px; flex:none; border-radius:7px;
                background-color:color-mix(in srgb,var(--ink) 12%,var(--su)) }
/* the answered one: the house action colour, filled, with ink on it. The
   fill IS the state; there is no ring and no rim anywhere in this block. */
.pn-ans li.on{ background-color:color-mix(in srgb,var(--cta) 30%,var(--su)) }
.pn-ans li.on .bx{ background-color:var(--cta) }
/* a field, drawn as the filled well the real one is */
.pn-f{ display:grid; gap:4px; margin-bottom:var(--s3) }
.pn-f .fl{ font-size:11.5px; font-weight:700; color:var(--mut) }
.pn-f .fw{
  font-size:13px; font-weight:600; color:var(--ink); line-height:1.35;
  background-color:color-mix(in srgb,var(--ink) 6%,var(--su));
  border-radius:var(--r-sm); padding:9px 11px; min-height:38px;
}
.pn-go{
  margin:var(--s3) 0 0; text-align:center;
  background-color:var(--cta); color:var(--cta-ink);
  font-size:13px; font-weight:700; border-radius:var(--r-pill);
  padding:11px 14px; box-shadow:var(--sh-xs);
}

/* the five diet switches over the plate. Same chip as the funnel's own,
   at the size of a tag rather than a control, because none of these is
   pressable here. */
.mk-chips{ list-style:none; display:flex; flex-wrap:wrap; gap:6px; margin:0 0 var(--s3); padding:0 }
.mk-chips li{
  font-size:11.5px; font-weight:700; padding:5px 9px; border-radius:var(--r-pill);
  background-color:color-mix(in srgb,var(--ink) 6%,var(--su)); color:var(--mut);
}
.mk-chips li.on{ background-color:color-mix(in srgb,var(--tone) 40%,var(--su)); color:var(--ink) }

/* ROUND 82. THE STAGE IS THE POINT OF THIS SCREEN, SO THE OBJECTS FILL IT.
   Siavash: "make the left side images wider for all steps". Measured before
   the change, at 1440: the mock column was 566px wide and the widest thing
   in it was 330px, so every step of the tour was an artifact floating in the
   middle of 236px of cream. The column is wider now (the grid above) and
   each of the five is taken up with it.

   Each of these is a `max-width` and a basis, never a fixed width: the same
   objects are drawn at 268 to 330 on screens 1 to 8 and that is the size
   they are drawn for there. This block widens them on the tour and nowhere
   else, which is why every selector in it is prefixed `#s9`.

   The receipt keeps its own note: it carries an OPENED aisle, and nine item
   names in a 224px till roll wrap to two lines each. The torn edge, the tilt
   and the tabular tail are unchanged. */
#s9 .obj[data-kind=receipt]{ flex-basis:400px; max-width:420px }
#s9 .obj[data-kind=sheet]{ flex-basis:100%; max-width:500px }
#s9 .obj[data-kind=tape]{ max-width:none }
/* ROUND 82. THE LABELS IN A PAIR SIT ON ONE LINE.
   Two objects on the tour's table are deliberately different heights - that
   is the flagship's own law, no two objects the same size - but their labels
   are not the objects, they are the plates under them, and two plates at two
   different heights read as one of them having fallen. The column stretches,
   the label is pushed to the foot of it, and the pair lands on one baseline
   whichever object is taller. */
#s9 .objs{ align-items:stretch }
#s9 .objs > .obj{ display:flex; flex-direction:column }
#s9 .objs > .obj > .obj-say{ margin-top:auto }

/* the stage's objects sit at the top of the column, not in the middle of it:
   the five steps are five different heights and a centred stage moves the
   artifact up and down the screen as the parent presses Next. */
#s9 .set{ align-self:start }

/* the opened aisle. The rows sit INSIDE the receipt, one step in from the
   aisle above them, and they carry no number: an aisle's suggestion has a
   quantity only once a parent has ticked it. */
.rc-l li.sub{ display:block; padding:0 0 6px }
.rc-l li.sub ul{ list-style:none; margin:0; padding:0 0 0 var(--s3);
                 display:flex; flex-direction:column; gap:3px }
.rc-l li.sub li{ display:block; font-size:12px; line-height:1.35; color:var(--mut);
                 padding:0 }

/* the candle row closes the running order and is not a row to trade */
.sw.last{ background-color:color-mix(in srgb,var(--ink) 8%,var(--su)); color:var(--mut) }

/* the run sheet on screen 9 is a real run sheet and reads DOWN. The tape's
   own wrap is right on screen 3, where five short rows are a strip of
   masking tape across the table; six rows carrying RUN_DEFAULT's own
   sentences wrap into a paragraph, which is not what a run sheet is. */
#s9 .tape-l{ flex-direction:column; gap:7px }
#s9 .tape-l li{ align-items:baseline }
#s9 .tape-l li i{ flex:none; min-width:62px }
/* ROUND 82. TWO COLUMNS ONCE THE STAGE IS WIDE ENOUGH TO WASTE. Eleven rows
   in one column across a 727px strip of tape is a list down the left third
   and 400px of empty tape beside it - the Law 4 "air" band, drawn in the
   project's own sun. A `grid` and not `columns`: a CSS column box breaks a
   row between its clock and its words when the fragmentation lands there,
   and 3:25 pm at the foot of one column with "Cake and candles" at the head
   of the next is a run sheet that says something it does not mean. */
@media (min-width:640px){
  #s9 .tape-l{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
               gap:7px var(--s5) }
}
/* the guest chips sit under the middle of the tape above them, not against
   its left edge with the whole right half open */
#s9 .obj[data-kind=reply] .obj-face{ justify-content:center }

/* a guest who has answered, and one who has not, on the reply card the room
   already uses. Two fills off the same two tokens the rest of the page reads;
   the waiting chip is the page's own ink at a low mix, which is the quietest
   surface this palette has and the right rank for "nothing has happened yet". */
.rep .yes,.rep .wait{ flex:none; font-size:11px; font-weight:700;
           padding:4px 8px; border-radius:var(--r-pill); color:var(--ink) }
.rep .yes{ background-color:color-mix(in srgb,var(--mint) 52%,var(--su)) }
.rep .wait{ background-color:color-mix(in srgb,var(--ink) 9%,var(--su));
            color:var(--mut) }

/* ---- the lid ----
   The television on screen 9 is the game they just built, shut. The lid
   is painted OVER `.obj-face` and is a picture: a translucent wash of
   the page's own ground plus one mark. It is not a border, it has no
   rim, and it never draws an outline. `.obj-shut` keeps the object's
   own shadow, so the shut television still sits on the paper the same
   way the open one does. */
.obj-shut .obj-face{ position:relative }
.obj-shut .lid{
  position:absolute; inset:0; z-index:2;
  border-radius:inherit;
  /* 78% left the television legible under the lid, which is what put five
     sentences at 1.05:1 in front of the painted-pixel probe. The words moved
     onto the lid and the wash went up with them: at 92% the screen under it
     is a colour and a shape, and --ink on it clears AAA at both widths. */
  background-color:color-mix(in srgb,var(--bg) 92%,transparent);
  /* ROUND 69, Siavash 2026-09-12: "make this also more interesting". The lid
     was one flat sheet of --bg and the television under it is invisible at
     8%, so there was nothing to look at. The two ramps below are the only
     thing added, and where they are allowed to reach was MEASURED, not
     guessed: the mark, the name and the ranks occupy 33% to 67% of the lid's
     height at 1440 and 33% to 67% at 390. The tone ramp is spent by 30% and
     the ink ramp does not start until 78%, so not one pixel under a word
     changes and the painted-pixel probe reads what it read before. Tone at
     the top and weight at the bottom is the same lighting `.obj-face`
     already uses one layer out. */
  background-image:
    linear-gradient(180deg,
      color-mix(in srgb,var(--tone) 38%,transparent) 0%,
      color-mix(in srgb,var(--tone) 14%,transparent) 16%,
      transparent 30%),
    linear-gradient(0deg,
      color-mix(in srgb,var(--ink) 9%,transparent) 0%,
      transparent 22%);
  backdrop-filter:saturate(.55) blur(1.5px);
  -webkit-backdrop-filter:saturate(.55) blur(1.5px);
  display:grid; place-items:center;
  pointer-events:none;
}
/* the mark is art.js's own padlock, drawn at one size and in one ink.
   --ink on the lid's wash, never --mut: the wash is already 22% of the
   television underneath it and a muted mark on a translucent ground is
   the exact case the painted-pixel probe exists to catch. */
.obj-shut .lidmark{
  width:clamp(38px,10cqw,58px); height:clamp(38px,10cqw,58px);
  color:var(--ink); display:block;
}
/* drop-shadow and not box-shadow: `tile()` crops a DISC, so the mark's real
   edge is the circle's alpha and not the element box. Offset, zero blur, the
   paper rule. It is on the svg, which is scoped, not on a bare `.scene`. */
.obj-shut .lidmark svg{ display:block; width:100%; height:100%;
  filter:drop-shadow(3px 4px 0 color-mix(in srgb,var(--ink) 24%,transparent)) }
/* the lid sits INSIDE .obj-face, which already clips to the object's
   radius, so `inherit` above resolves against the television's own
   --r-lg and not against the figure. */
.obj[data-kind=tv].obj-shut .lid{ border-radius:var(--r-lg) }

@media(prefers-reduced-motion:reduce){
  /* background-COLOR, not the shorthand: `background:` here would reset
     background-image and take the two ramps above with it. */
  .obj-shut .lid{ backdrop-filter:none; -webkit-backdrop-filter:none;
                  background-color:color-mix(in srgb,var(--bg) 96%,transparent) }
}

/* the lid carries the two readable lines. --ink on the wash, never --mut:
   the wash is already translucent over a dark television and a muted rank on
   a translucent ground is the exact case the painted-pixel probe exists to
   catch. Measured at both widths before this was committed. */
/* align-content, not just place-items. The lid is inset:0 over a 16/9 screen,
   so its three auto rows STRETCH across the whole height by default and the
   mark, the name and the count came out pinned to the top, the middle and the
   bottom of the television, reading as three separate things. `place-items`
   sets align-items and justify-items; it does not touch how the rows are
   distributed, which is the property that was wrong. */
.obj-shut .lid{ gap:.4em; padding:0 8cqw; text-align:center; align-content:center }
.obj-shut .lidti{
  display:block; font-family:var(--disp); font-weight:700; color:var(--ink);
  font-size:clamp(15px,4.4cqw,22px); line-height:1.15;
}
/* three ranks, not one comma string. "9 challenges, midnight, cast for 7"
   read as a sentence that had been truncated; as three facts with a dot
   between them it reads as a plate on the back of the thing. The dot is a
   drawn pseudo-element, so no character has to survive a font fallback. */
.obj-shut .lidsub{
  display:flex; flex-wrap:wrap; justify-content:center; align-items:center;
  gap:.3em .7em;
  font-style:normal; font-weight:600; color:var(--ink);
  font-size:clamp(11.5px,2.9cqw,14px); line-height:1.3;
}
.obj-shut .lidsub span{ display:inline-flex; align-items:center; gap:.7em }
.obj-shut .lidsub span + span::before{
  content:""; width:4px; height:4px; border-radius:50%; flex:none;
  background:color-mix(in srgb,var(--ink) 42%,transparent);
}
/* the card under the lid holds nothing. `.run` keeps its absolute inset so
   `.tv-screen` paints its own dark ground behind the wash; there is no text
   and no icon in it to be measured, hidden or ghosted. */
.run-mute{ display:block; padding:0 }
