/* Kid wall layout. Every colour and size comes from tokens.css.
   No inline style attribute appears anywhere in this page: the CSP in
   wall.html grants style-src 'self' with no 'unsafe-inline', so a state
   that needs to look different gets a class, never a style attribute. */

#demobanner{background:var(--brand-fill);color:var(--brand-fill-ink);
  padding:var(--space-3) var(--space-5);text-align:center;font-size:var(--text-s)}
#demobanner p{margin:0;max-width:960px;margin-inline:auto}
#demobanner a{color:inherit;font-weight:600}
#demobanner a.cta{display:inline-block;padding:.15em .75em;margin-right:.5em;border-radius:999px;background:var(--brand-fill-ink);color:var(--brand-fill);text-decoration:none}

header{display:flex;align-items:center;gap:var(--space-4);
  padding:var(--space-3) var(--space-5);position:sticky;top:0;background:var(--bg);
  border-bottom:1px solid var(--line);z-index:5}
/* The related rail (2026-08-28) put real scrollable content below the stage
   for the first time, so the page CAN scroll while a video is on screen. A
   sticky header pinned at y:0 through that scroll would pass in front of
   the still-visible bottom of the stage mid-scroll: HB-7 forbids that. So
   the header drops out of sticky (and scrolls away like everything else)
   for exactly as long as the player is up, rather than trying to bound the
   rail's height to keep the old "the page never scrolls" invariant, which
   a related rail cannot honestly promise. tests/browser-check.mjs measures
   the result at full scroll rather than trusting this rule alone. */
body:has(#player.on) header{position:static}
header .kidicon{width:32px;height:32px;flex:0 0 auto;border-radius:var(--radius-s);
  object-fit:cover;background:var(--surface-sunken);display:block}
/* Author CSS always outranks the UA stylesheet's [hidden]{display:none},
   regardless of specificity, so the display:block above wins over the
   hidden attribute wall.js sets on a dead token or a load error and the
   empty bordered square shows anyway. This rule must stay in lockstep
   with any change to the selector above. */
header .kidicon[hidden]{display:none}

/* The drawn fallback for a missing or broken kidicon (Design 4). Colour and
   motif arrive as classes -- c0..c5, m0..m3 -- from LanternAvatar.svg
   (assets/wall/avatar.js), never as a hex baked into the markup, and the
   fill comes from the same --kid-* tokens assets/guardian/guardian.css
   defines, so the wall and the guardian pages can never draw two different
   colours for the same index. */
.avatar.c0 .base{fill:var(--kid-teal)}
.avatar.c1 .base{fill:var(--kid-coral)}
.avatar.c2 .base{fill:var(--kid-plum)}
.avatar.c3 .base{fill:var(--kid-sea)}
.avatar.c4 .base{fill:var(--kid-moss)}
.avatar.c5 .base{fill:var(--kid-rose)}
.avatar text{fill:#fff}
.avatar .deco{fill:#fff}
.avatar.m0 .deco{opacity:.18}
.avatar.m1 .deco{opacity:.14}
.avatar .ring1,.avatar .ring2{fill:none;stroke:#fff;stroke-width:6}
.avatar.m2 .ring1{stroke-opacity:.22}
.avatar.m2 .ring2{stroke-opacity:.16}
.avatar.m3 .ring1{stroke-opacity:.2}

/* No `header h1` rule and no `header .mark`: Jeff's 2026-09-10 ruling took
   the Lantern mark and wordmark off the kid wall's masthead, so the row is
   the child's icon and their name and nothing else. */
/* The name and the icon both arrive from the queue, so an unreserved
   masthead is 0px tall at first paint and grows twice -- once when the name
   lands and again when the icon does -- shoving the nav, the searchbar and
   the whole wall down the screen under each. Reserving the icon's own 32px
   here (the .kidicon width/height below, the tallest thing this row can
   hold) makes the masthead's height the same before and after the queue
   lands. `line-height` rather than a flex centring, because ellipsis only
   renders on a block container and this name is one nowrap line. */
header h2{font-family:var(--font-display);font-weight:600;font-size:var(--text-l);
  color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  min-height:32px;line-height:32px}

/* ---- nav + channel view (Jeff's ruling, 2026-08-28) ----
   Plain flow, never positioned: same "nothing sits over the player" rule as
   everything else here, and #player.on hides both exactly like it hides
   #grid and #searchbar (see that rule below). */
/* Wraps rather than scrolls (2026-09-19, when Shuffle made it five pills).
   Measured in headless Chrome at 390px, the width of the phone a child
   actually holds: the four mode pills come to 347.3px inside a 350px content
   box -- the row was already full to 3px. A fifth pill under the old
   `overflow-x:auto` therefore sat ~90px off the right edge, invisible until
   swiped, which is no control at all for a non-reader. Wrapping drops it
   onto a second row at full size instead: nothing clips, nothing shrinks
   below the ruled 64px kid floor, and `documentElement.scrollWidth` never
   grows (browser-check's "no horizontal scroll at 390 px"). One row again
   from ~480px up, where all five fit. */
#navbar{display:flex;flex-wrap:wrap;gap:var(--space-3);
  padding:var(--space-5) var(--space-5) 0;max-width:1280px;margin:0 auto}
/* Icon-first pills (Jeff's directive, 2026-09-01: "All pills need
   iconography," "this needs to work for a 2 year old and non reader to
   grok"). The glyph is the primary read; the label rides underneath for a
   parent glancing over a shoulder, never the other way around. Still a
   real >=44px touch target (var(--tap-kid) is 64px) at the toddler-poke
   sizes the directive calls for. */
.navbtn{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:2px;min-width:72px;min-height:var(--tap-kid);
  padding:var(--space-2) var(--space-4);border:1px solid var(--line-strong);
  border-radius:var(--radius-l);background:var(--surface-raised);color:var(--ink);
  cursor:pointer;font-family:var(--font-display)}
.navicon{width:30px;height:30px;flex:0 0 auto;display:block;color:var(--brand-fill)}
.navlabel{font-size:var(--text-s);font-weight:700;white-space:nowrap}
.navbtn.on{background:var(--brand-fill);color:var(--brand-fill-ink);border-color:var(--brand-fill)}
/* Solid-toy glyphs (Jeff's pick, 2026-09-01: icons C on Forest & Butter)
   are single-color fill="currentColor" silhouettes, which retires the
   #225-era [fill]/[stroke] override hack: the selected pill flips its
   glyph to the ink color through .on's own `color` plus the one rule
   below. The lone two-tone detail is Watched's check, a `.cut` stroked
   in the surface behind the glyph so it reads as a cutout -- on .on that
   surface IS --brand-fill, hence the paired rule. */
.navbtn.on .navicon{color:var(--brand-fill-ink)}
.navicon .cut{stroke:var(--surface-raised)}
.navbtn.on .navicon .cut{stroke:var(--brand-fill)}
.navbtn:active{transform:scale(.97)}
/* `display:flex` above outranks the UA's own [hidden] rule, so a pill hidden
   in JS (updateNavUI hides Shuffle in the views it cannot reorder) would
   otherwise stay on screen -- same reason #navbar[hidden] needs its own rule
   below. */
.navbtn[hidden]{display:none}
/* Off-screen for the eye, present for a screen reader (#shuffleSaid). A
   control that silently reorders every card on the page is a control a
   non-sighted child cannot tell fired. Not `display:none` and not `hidden`:
   either would stop the announcement entirely. */
.said{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}

#channelbar{display:flex;align-items:center;gap:var(--space-4);
  padding:var(--space-4) var(--space-5) 0;max-width:1280px;margin:0 auto}
#channelbar[hidden]{display:none}
#navbar[hidden]{display:none}
/* Same reason as the two rules above: #searchbar{display:flex} outranks the
   UA's own [hidden]{display:none}, so showLimitCard()/showFailure() setting
   $('searchbar').hidden = true did nothing visible -- the search bar (and
   its input) stayed on screen over the daily-limit card and the failure
   card alike. wall.test.mjs's own hidden-elements test pins every id those
   two functions hide against a rule (or default display) that actually
   hides it. */
#searchbar[hidden]{display:none}
#channelBack{flex:0 0 auto;min-height:var(--tap-kid);padding:0 var(--space-6);
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  background:var(--surface-raised);color:var(--ink);cursor:pointer;
  font-family:var(--font-display);font-size:var(--text-m);font-weight:700}
#channelbar h2{font-family:var(--font-display);font-size:var(--text-l);font-weight:700;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ---- search ----
   In normal flow, never positioned: the same "nothing sits over the player"
   rule as everything else on this page, and it costs nothing extra here
   because #player.on hides this row exactly like it hides #grid. */
#searchbar{display:flex;gap:var(--space-3);align-items:center;
  padding:var(--space-5) var(--space-5) 0;max-width:1280px;margin:0 auto}
#searchbar input{flex:1;min-width:0;min-height:var(--tap-kid);
  background:var(--surface-sunken);border:1px solid var(--line);
  border-radius:var(--radius-pill);padding:0 var(--space-5);
  font-family:var(--font-body);font-size:var(--text-l);color:var(--ink)}
#searchbar input::placeholder{color:var(--ink-faint)}
/* -webkit-appearance:none strips the iOS Safari factory chrome (the tiny grey
   clear-x and magnifier) so the one deliberate control this page ships,
   #qClear, is the only one visible. */
#searchbar input[type=search]{-webkit-appearance:none;appearance:none}
/* Chrome and Safari both grow their own cancel-x inside a type=search field
   independent of `appearance:none` on the input itself, and it does not run
   our clearSearch(): a tap on it empties the box but leaves searchQuery and
   the filtered grid exactly as they were, which is a broken control on a kid
   surface. #qClear is the ONE clear affordance this page ships. */
#searchbar input[type=search]::-webkit-search-cancel-button,
#searchbar input[type=search]::-webkit-search-decoration{-webkit-appearance:none;display:none}
#qClear{flex:0 0 auto;width:var(--tap-kid);height:var(--tap-kid);
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  background:var(--surface-raised);color:var(--ink);font-size:var(--text-l);
  font-family:var(--font-body);cursor:pointer}
#qClear:active{transform:scale(.94)}
#qClear[hidden]{display:none}

/* ---- the friendly result count (unit 2, Jeff's directive 2026-09-01) ----
   For a parent glancing over a shoulder, not a control a child has to
   read: the cards in #grid below are the real signal. Small, quiet type;
   the icon carries "these are videos" without asking anyone to read a
   word. */
#searchCount{display:flex;align-items:center;gap:var(--space-2);
  max-width:1280px;margin:0 auto;padding:var(--space-3) var(--space-5) 0;
  color:var(--ink-soft);font-family:var(--font-body);font-size:var(--text-s);
  font-weight:700}
#searchCount[hidden]{display:none}
.counticon{width:16px;height:16px;flex:0 0 auto}

/* ---- the wall ---- */
#grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-5);
  padding:var(--space-6) var(--space-5) var(--space-7);max-width:1280px;margin:0 auto}
@media (min-width:720px){#grid{grid-template-columns:repeat(3,1fr);
  gap:var(--space-5);padding:var(--space-6) var(--space-6) var(--space-8)}}
@media (min-width:1100px){#grid{grid-template-columns:repeat(4,1fr)}}

/* .card is a plain container holding two direct children: .cardplay (a real
   <button> around the thumbnail) and .info. Never a button inside a button:
   a nested control is invalid markup and is unreachable from a keyboard or
   a TV remote. */
.card{background:var(--surface-raised);border:1px solid var(--line);
  border-radius:var(--radius-tile);overflow:hidden;text-align:left;color:var(--ink);
  box-shadow:var(--shadow-1);display:block;width:100%;
  transition:box-shadow var(--dur-2) var(--ease),transform var(--dur-1) var(--ease)}
.card:hover{box-shadow:var(--shadow-2);border-color:var(--line-strong)}
.card:has(.cardplay:active){transform:scale(.97)}
/* width:100% for the same reason .ch carries it below: a <button> keeps its
   native shrink-to-fit sizing under `all:unset; display:block`, and this
   one's only content is .thumb, an aspect-ratio box whose <img> has no
   intrinsic size until the bytes arrive. Left to shrink-to-fit the button
   measured 0 x 64 for the whole time a thumbnail was in flight -- on a cold
   400 kbit/s phone that is seconds of a kid tapping a video and getting
   nothing, because elementFromPoint at the middle of the tile returned the
   .card behind the button, and seconds of the grid jumping under the finger
   as each image finally landed (CLS 0.352 measured in the rig at 390x844,
   0.081 with this line in). A percentage width gives .thumb a definite
   width, and 16/9 then gives it a definite height, both before the image
   exists. min-height stays: it is the kid tap floor for the degenerate case
   of a card narrower than 114px. Asserted with the thumbnails blocked in
   tests/browser-check.mjs (WALL-TAPBOX). */
.cardplay{all:unset;display:block;width:100%;box-sizing:border-box;
  cursor:pointer;min-height:var(--tap-kid)}
.thumb{display:block;position:relative;aspect-ratio:16/9;background:var(--surface-sunken)}
.thumb img{width:100%;height:100%;object-fit:cover;display:block}
.thumb img.missing{display:none}
.dur{position:absolute;right:8px;bottom:8px;background:var(--scrim);color:#fff;
  border-radius:6px;padding:2px 8px;font-family:var(--font-display);font-weight:600;
  font-size:var(--text-xs);font-variant-numeric:tabular-nums}
/* The "short" text overlay is retired (Jeff's directive, 2026-09-01: "this
   then means we can replace the 'short' overlay with the lightning too"):
   the badge is now the same lightning glyph as the Shorts nav pill
   (wall.js's LIGHTNING_SVG), never the word "short". */
.badge{position:absolute;left:8px;top:8px;background:var(--brand-fill);
  color:var(--brand-fill-ink);border-radius:6px;padding:4px;display:flex}
.badge svg{width:14px;height:14px;display:block}
/* The clamped title carries NO padding of its own. -webkit-line-clamp clips at
   the CONTENT box, so bottom padding on the clamped element lets a sliver of
   line 3 bleed through under the clamp (seen on the first Chrome pass here, and
   the same D8 bug the lab viewer fixed). All the spacing lives on .info. */
.info{padding:var(--space-3) var(--space-4) var(--space-4)}
.card h3{font-size:var(--text-l);font-weight:800;line-height:var(--leading-tight);
  cursor:pointer;display:-webkit-box;-webkit-line-clamp:2;
  -webkit-box-orient:vertical;overflow:hidden}
/* The channel name is a real second control (channel view, 2026-08-28's
   ruling), a sibling of .cardplay rather than nested inside it, same reason
   as the lab's own two-sibling-buttons fix: a <p> inside a <button> is
   invalid markup and unreachable from a keyboard or a TV remote. */
/* S3 fix (synth-user-swarm-2026-09-03.md): a <button> keeps its native
   shrink-to-fit sizing even under `all:unset; display:block` -- unlike a
   plain block box, it never grows to the card's own width on its own, so a
   long channel name was never actually clipped by ITS OWN overflow:hidden
   below (its box was always exactly as wide as the unwrapped text); the
   card's own `overflow:hidden` (line 156) was the thing doing the
   clipping, mid-word, at the card edge, with no ellipsis in sight because
   ellipsis only renders for overflow inside the element that declares it.
   width:100% forces .ch to the card's actual width so its own
   overflow/ellipsis pair finally has something to do. */
.card .ch{all:unset;display:block;width:100%;box-sizing:border-box;
  cursor:pointer;font-size:var(--text-s);
  color:var(--ink-soft);padding-top:var(--space-2);min-height:var(--space-7);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* "+ Add to my list" is retired from the kid UI entirely (Jeff's directive,
   2026-09-01: "though let's just get rid of the list" -- the .mylist
   button, onToggleMyList/sendListToggle and their card wiring are gone
   from wall.js; control/src/lib/list.ts's own getList/postList door is
   untouched, just unused from here). This rule is deleted with it, not
   left as dead CSS. */
/* Liked (Jeff's ruling, 2026-08-31, board T-1813; icon-first per the
   2026-09-01 directive: "Like this video" is now a thumbs-up glyph, not a
   text pill). A plain toggle -- a heart/like is a keep a kid can give and
   take back, never a rating, and there is still no thumbs-down anywhere on
   this surface -- but the button itself is a compact icon target now
   rather than a full-width text pill: the accessible name (aria-label)
   carries "Like this video"/"Unlike this video" for anyone who needs it
   read aloud, since there is no visible text left to read. */
.card .heart{display:inline-flex;align-items:center;justify-content:center;
  margin-top:var(--space-2);width:var(--tap-min);height:var(--tap-min);padding:0;
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  background:var(--surface-raised);color:var(--ink);cursor:pointer}
.card .heart svg{width:22px;height:22px;display:block;color:var(--brand-fill)}
/* "Did my like stick?" is the card's most important feedback: the lit
   state inverts butter-on-green to green-ink-on-butter (currentColor,
   same design as .navbtn.on above). */
.card .heart.on{background:var(--brand-fill);border-color:var(--brand-fill)}
.card .heart.on svg{color:var(--brand-fill-ink)}
/* Demo mode's own link-out state (makeCard, wall.js): "Sign up to like
   videos" is a real sentence for an adult visitor, not an icon, so it keeps
   the old full-width text-pill shape instead of the icon circle above. */
.card .heart.demolink{display:block;width:auto;height:auto;min-height:var(--space-7);
  padding:0 var(--space-3);font-family:var(--font-body);font-size:var(--text-s);font-weight:600}

/* Channel + like share one row (Jeff's directive, 2026-09-04, from a photo
   of the kid's wall: the like button sat on its own row under the channel
   name, wasting height on every card -- fewer cards fit on screen for it).
   Grid, not flex, and scoped to a real card (:not(.askcard)): explicit line
   placement puts h3 in row 1 and drops .ch/.heart into row 2 by grid line
   number, not DOM order, so the heart lands in the same column-2/row-2 cell
   whether or not a .ch element is even in the DOM -- a card with no channel
   yet (title fills in after render) already parks the button at that exact
   spot, and nothing shifts when the channel arrives. .ch keeps the same
   overflow/ellipsis pair above, just narrowed to 1fr so it truncates before
   it can ever reach the button. askCardHTML keeps its old block .info: its
   askbtn wants the full row width, not a shared column, so it is excluded
   here rather than reworked. Demo mode's full-sentence link-out (.demolink,
   above) also opts back out, into its own row 3, so it keeps the same
   full-width text-pill shape it always had. */
.card:not(.askcard) .info{display:grid;grid-template-columns:1fr auto;
  column-gap:var(--space-3);row-gap:var(--space-2);align-items:center}
.card:not(.askcard) h3{grid-column:1/-1;grid-row:1}
.card:not(.askcard) .ch{grid-column:1;grid-row:2;padding-top:0}
.card:not(.askcard) .heart{grid-column:2;grid-row:2;margin-top:0}
.card:not(.askcard) .heart.demolink{grid-column:1/-1;grid-row:3;margin-top:var(--space-2)}

/* ---- Look further, the deliberate second tap (unit 2, Jeff's directive
   2026-09-01) ----
   Sits right after #searchCount, ABOVE #grid (PR #226 review, P1: moved off
   its original after-#grid spot, which measured below the fold on a
   390x844 phone in every state tried -- see wall.html's own comment on
   #lookFurtherWrap for the measurement and the reasoning). A big, chunky,
   icon-first tap target -- the same tap-kid floor every other kid control
   on this page uses -- never a bare text link. */
#lookFurtherWrap{display:flex;justify-content:center;padding:0 var(--space-5) var(--space-6)}
#lookFurtherWrap[hidden]{display:none}
#lookFurtherBtn{display:flex;align-items:center;gap:var(--space-3);
  min-height:var(--tap-kid);padding:0 var(--space-6);
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  background:var(--surface-raised);color:var(--ink);cursor:pointer;
  font-family:var(--font-display);font-size:var(--text-l);font-weight:700}
#lookFurtherBtn:active{transform:scale(.97)}
#lookFurtherBtn svg{width:26px;height:26px;flex:0 0 auto;display:block}

/* ---- K1: the search-miss "look further" grid ----
   `.askcard` reuses .card's box but carries no .cardplay: there is nothing
   to press on the tile itself, only the ask button below it. */
#askwrap{max-width:1280px;margin:0 auto;padding:0 var(--space-5) var(--space-6)}
#askgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-4)}
@media (min-width:1100px){#askgrid{grid-template-columns:repeat(4,1fr)}}
.askcard{background:var(--surface-raised);border:1px solid var(--line);
  border-radius:var(--radius-tile);overflow:hidden;text-align:left;color:var(--ink);
  box-shadow:var(--shadow-1);display:block;width:100%}
.askbtn{display:block;width:100%;margin-top:var(--space-2);min-height:var(--tap-kid);
  padding:0 var(--space-3);border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  background:var(--surface-raised);color:var(--ink);cursor:pointer;
  font-family:var(--font-body);font-size:var(--text-s);font-weight:600}
.askbtn:disabled{cursor:default;opacity:.7}
/* Unit 2's own fail-closed state (wall.js's LOOK_FURTHER_CLOSED_MSG):
   a plain, calm sentence in place of the candidate grid, same voice as
   .boot p but sized to sit inside #askgrid's own column span rather than
   taking over the whole viewport like a boot failure does. */
.lfclosed{grid-column:1/-1;color:var(--ink-soft);font-family:var(--font-body);
  font-size:var(--text-m);font-weight:600;padding:var(--space-4) 0;text-align:center}

/* ---- more ---- */
#morewrap{display:flex;justify-content:center;padding:0 var(--space-5) var(--space-8)}
#morewrap[hidden]{display:none}
#more{background:var(--brand-fill);color:var(--brand-fill-ink);
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  min-height:var(--tap-kid);padding:0 var(--space-7);cursor:pointer;
  font-family:var(--font-display);font-size:var(--text-l);font-weight:700}
#more:active{transform:scale(.97)}
#sentinel{height:1px}

/* ---- the YouTube Brand Feature (see wall.html) ----
   Sized and spaced against the brand site's own numbers for the real mark,
   so swapping the official asset in later changes the picture and not the
   layout: 20dp is its minimum height, and its clear space has to be at
   least that same height on every side. 15px type on a 20px line clears the
   first, and 24px of padding all round clears the second with room to
   spare. Full-width and centred so it reads as the page's own footer rather
   than a caption belonging to the last card above it. */
#ytbrand{padding:var(--space-6) var(--space-5);text-align:center;
  font-family:var(--font-body);font-size:var(--text-m);line-height:20px;
  color:var(--ink-soft)}

/* S1 finding 6 (Codex review, 2026-09-03): a grown-up-facing notice, shown
   only when wall.js has confirmed storage is actually blocked -- see
   onStorageBlocked there. Plain [hidden] default:none is enough; nothing
   here sets a competing display value, unlike header .kidicon above. */
#storagewarn{padding:var(--space-5);text-align:center;max-width:520px;
  margin:0 auto;font-family:var(--font-body);font-size:var(--text-s);
  line-height:18px;color:var(--ink-soft)}

/* ---- player ----
   The stage holds the iframe and nothing else. No element is ever positioned
   in front of the player: the back control sits ABOVE the stage, in normal
   flow, so the player, its controls and all YouTube branding stay visible
   and untouched. */
#player{display:none}
#player.on{display:block}
#player.on ~ #navbar,#player.on ~ #channelbar,
#player.on ~ #searchbar,#player.on ~ #searchCount,#player.on ~ #grid,
#player.on ~ #lookFurtherWrap,#player.on ~ #askwrap,
#player.on ~ #morewrap,#player.on ~ #sentinel{display:none}
.pbar{display:flex;align-items:center;gap:var(--space-3);
  padding:var(--space-3) var(--space-5) 0;max-width:1280px;margin:0 auto}
#back{background:var(--surface-raised);color:var(--ink);
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  min-height:var(--tap-kid);padding:0 var(--space-6);cursor:pointer;
  font-family:var(--font-display);font-size:var(--text-l);font-weight:700}
#back:active{transform:scale(.97)}
/* "Tell a grown-up" (C3). Same big touch target as #back, styled as a
   secondary action so it never reads as the primary control on the stage.
   The button's own text swaps to the sent state (wall.js) rather than a
   second element appearing, so there is exactly one thing on screen a child
   has to make sense of. */
#flag{background:transparent;color:var(--ink-soft);
  border:1px solid var(--line);border-radius:var(--radius-pill);
  min-height:var(--tap-kid);padding:0 var(--space-5);cursor:pointer;
  font-family:var(--font-display);font-size:var(--text-m);font-weight:600;
  margin-left:auto}
#flag:active{transform:scale(.97)}
/* The playing video's own thumb (Jeff, 2026-09-10). The same control the
   cards carry, so it takes the card thumb's shape verbatim and resets only
   what this row needs: no top margin inside a centred flex row, and no
   shrinking when "Tell a grown-up" grows. --tap-min is the wall's own tap
   floor, the same token every card thumb already uses. */
.pbar .heart{display:inline-flex;align-items:center;justify-content:center;
  width:var(--tap-min);height:var(--tap-min);padding:0;flex:0 0 auto;
  border:1px solid var(--line-strong);border-radius:var(--radius-pill);
  background:var(--surface-raised);color:var(--ink);cursor:pointer}
.pbar .heart svg{width:22px;height:22px;display:block;color:var(--brand-fill)}
.pbar .heart.on{background:var(--brand-fill);border-color:var(--brand-fill)}
.pbar .heart.on svg{color:var(--brand-fill-ink)}
.pbar .heart:active{transform:scale(.97)}
#flag:disabled{cursor:default;opacity:.7}
/* The stage is capped by the HEIGHT the viewport has left, not only by width,
   so the whole 16/9 player and its controls sit on screen without a scroll on
   an iPad in landscape. svh rather than vh: iOS Safari's toolbars change the
   large viewport under you mid-video. */
.stage{background:#000;border-radius:var(--radius-m);overflow:hidden;
  width:calc(100% - 2 * var(--space-5));
  max-width:min(1280px, calc((100vh - 200px) * 16 / 9));
  max-width:min(1280px, calc((100svh - 200px) * 16 / 9));
  margin:var(--space-4) auto 0}
.stage iframe{width:100%;aspect-ratio:16/9;border:0;display:block}
#ptitle{max-width:1280px;margin:0 auto;padding:var(--space-4) var(--space-5) var(--space-8);
  font-family:var(--font-display);font-size:var(--text-xl);font-weight:700}

/* ---- related rail: normal flow, BELOW the stage, never in front of it ---- */
#relatedwrap{max-width:1280px;margin:0 auto;padding:0 var(--space-5) var(--space-8)}
.railhead{font-family:var(--font-display);font-size:var(--text-m);font-weight:700;
  color:var(--ink-soft);padding-bottom:var(--space-3)}
#related{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-4)}
@media (min-width:720px){#related{grid-template-columns:repeat(4,1fr)}}

/* ---- the failure states (network, token, empty -- wall.js's FAIL_COPY) ---- */
.boot{grid-column:1/-1;min-height:60vh;display:flex;align-items:center;
  justify-content:center;text-align:center;padding:var(--space-8) var(--space-5)}
.boot p{font-size:var(--text-2xl);font-weight:700;color:var(--ink);
  max-width:460px;line-height:var(--leading-tight)}
/* Codex review 2026-09-03, finding 12: the retry action failureHTML() adds
   for every kind but 'token' (a dead link a reload cannot fix). Its own
   column-stacking wrapper, scoped clear of the bare `.boot` row layout
   above so this leaves the existing two-line limit card (.boot.limit,
   which has no wrapper and no button) untouched. Same pill shape as
   .askbtn, narrowed and centered rather than the full-bleed card width
   that button owns its own grid column for. */
.boot .bootmsg{display:flex;flex-direction:column;align-items:center}
.boot .retrybtn{display:block;width:100%;max-width:220px;margin:var(--space-4) auto 0;
  min-height:var(--tap-kid);padding:0 var(--space-4);border:1px solid var(--line-strong);
  border-radius:var(--radius-pill);background:var(--surface-raised);color:var(--ink);
  cursor:pointer;font-family:var(--font-body);font-size:var(--text-s);font-weight:600}
/* The daily limit's own state: the same layout as the two failure states, a
   calmer message and a lighter second line rather than one flat block of
   bold text (docs/design/dial-model-2026-08-29.md §9). */
.boot.limit p.sub{font-size:var(--text-lg);font-weight:400;margin-top:var(--space-3)}
