/* THE DEPTH SCALE COMES FIRST. Every z-index below is a band from `layers.css`; no rule
   in this file invents a number. `@import` must precede every other rule, which is why
   it is the first line. */
@import url('./layers.css');

/* THE SETTINGS MENU'S OWN STYLES, PULLED IN HERE ON PURPOSE.
 *
 * A module can now open the universal menu about ITSELF (see comet.js), which means the menu
 * has to be styled wherever a module is mounted — not only on `kiosk.html`, which was the one
 * page that linked `settings.css`. Sixteen module-hosting pages did not, so a game opening its
 * settings on any of them would have rendered an unstyled list of rows.
 *
 * Importing it here rather than adding a <link> to sixteen files is the difference between
 * fixing this once and fixing it again for the next module that uses the menu: every host that
 * can mount a module already loads THIS stylesheet, by definition.
 *
 * `@import` must precede every rule, which is why it sits above `.mod-box` rather than next to
 * the cursor styles it is related to. Pages that also <link> settings.css directly are fine —
 * the URL is identical and the browser fetches it once. */
@import url('./settings.css');

/* *** `hidden` MUST ACTUALLY HIDE. ***
 *
 * The browser hides `[hidden]` with `display:none` from its OWN stylesheet, which any rule
 * of ours outranks. Four modules set `display:flex` on `.status` - photos, camera, youtube,
 * personal - so `el.hidden = true` set the attribute, changed nothing on screen, and left a
 * full-panel scrim sitting over the content.
 *
 * That is the "photos drawing UNDERNEATH a persistent 'Loading photos...' sheet" reported off
 * the live site on 2026-09-05. The LOGIC was fixed then - the module does hide the status the
 * moment a photo draws - and the symptom stayed, because the hiding never took effect. It read
 * as fixed in the code and broken on the screen, which is the worst combination to debug.
 *
 * Four one-off `[hidden]{display:none}` patches already exist further down this file (director,
 * wallpaper, gprogress, cur). Each one was somebody hitting this same wall and walling off
 * their own corner. This is that fix made once, for every element.
 */
[hidden] { display: none !important; }

/* A host that hands a module a fixed box adds `mod-box`, and the module can then size
   itself against that box with container-query units (1cqh = 1% of the box's height).
   Without it, cq units fall back to the viewport, which is why nothing breaks in the
   composer preview or a test page that forgets to opt in.

   THE BUG THIS FIXES: modules were written in rem, which is a fact about the ROOT FONT,
   not about the panel they landed in. Put the algebra keypad in a short cell and it
   simply overflowed and grew a scrollbar; put the clock in a wide short one and the time
   ran to the edges. Neither is a bug in the module — both are a module with no way to
   know how much room it has. */
.mod-box{container-type:size;container-name:cell;position:relative}

/* *** A PANEL MUST NOT BE ABLE TO PAINT OUTSIDE ITS OWN SLOT. ***
 *
 * `.k-mod` is the element a module is mounted into, by the kiosk and by `view.js` alike. It had
 * NO `position`, and neither did `.k-cell` — so a module whose own root is
 * `position:absolute; inset:0` (which is how every full-bleed module is written) resolved
 * against the nearest positioned ancestor, and that is `.k-stage`. **It filled the entire stage
 * instead of its cell, and painted over its neighbours.**
 *
 * Diagnosed by Mike from two screenshots of the live site: Comet's balloon field landed in the
 * SAME band at the document's top-left under two completely different layouts, and its score in
 * the top-right corner of the document. A canvas ignoring layout entirely. Measured here before
 * fixing: the comet root came back at exactly the stage's rect (800x500 at 100,80) while its
 * cell was 375x225 at 505,100.
 *
 * *** AND `container-type: size` ON `.mod-box` DID NOT CONTAIN IT. *** That is worth writing
 * down because it is the reasonable assumption and it is wrong: the cell already carried
 * `container-type:size` for container queries, which reads like it should establish a
 * containing block, and measurement said otherwise. `position:relative` is what actually does
 * it.
 *
 * Fixed on the HOST rather than in the module, deliberately. Mike: fix containment generally,
 * not Comet specifically — whatever let one module do this lets all three canvas modules do it,
 * and the next full-bleed module written would have inherited the same bug. */
.k-mod{position:relative}

/* *** THE FITTING CONTRACT: EVERY ELEMENT A MODULE IS MOUNTED INTO, ON EVERY PAGE. ***
 *
 * `mod-host` is added by `mountModule` itself, not written into any page's markup. That is the
 * point of it: `.k-mod` above fixed containment for the kiosk and `view.js`, and the SAME class
 * had to be remembered by every other host — so `modules.html`'s try stage, which is where Mike
 * actually found this, had `overflow:hidden` and no scroll of its own.
 *
 * WHAT MIKE FOUND, live, on 2026-09-06, all in one sitting: Comet's Start button below the fold
 * and unreachable; Trivia's question and answers cut off with no scrollbar; Word Forge's choices
 * invisible; a maths answer submitted and the result rendering off-screen; Sprint's controls
 * below the fold. Six modules, one defect — a host that clips.
 *
 * *** SCROLLING IS THE FLOOR, NOT THE GOAL. *** A module SHOULD fit; several of them are being
 * made to. But "fits" is a judgement that depends on the box, the theme, the font size and the
 * length of the question, and it will be wrong somewhere. What must never depend on a judgement
 * is whether the Start button can be reached at all. `auto` costs nothing when the module fits —
 * no scrollbar is drawn — and it is the difference between a cramped panel and a broken one.
 *
 * `min-width/min-height:0` because these hosts are almost always flex children, and a flex item
 * refuses to shrink below its content by default: without it a long question makes the PANEL
 * grow and push its neighbours off the grid rather than scrolling inside its own box.
 *
 * *** `!important` ON THE OVERFLOW, AND ONLY ON THE OVERFLOW. *** Same remedy, for the same
 * reason, as `[hidden]{display:none !important}` at the top of this file — and arrived at the
 * same way, by watching one-off patches pile up. A single class loses to any page rule of two,
 * and it lost TWICE in one session: `.k-stage .k-mod{overflow:hidden}` in `kiosk.html`, then
 * `.trystage{overflow:hidden}` on `modules.html` — the page whose entire job is showing modules.
 * Both were removed here; the next rounded corner somebody wants would bring it straight back.
 *
 * An attribute was tried first and does NOT work: `[attr]` counts in the same specificity group
 * as a class, so `.mod-host[data-mod-host]` merely TIES with `.rude .rude-mod` and loses on
 * source order to any page whose <style> comes after this file. Measured, not assumed — it
 * failed the check in `fit_test` that exists for exactly this.
 *
 * The escape hatch that stays: `position`, `min-width` and `min-height` are ordinary, so a host
 * with a real reason can still change those. What it cannot do is hide a control somebody needs
 * to press, which is the one thing this class is for. */
.mod-host{position:relative;overflow:auto !important;min-width:0;min-height:0}

/* *** THE FOUR MODULES THAT STILL DID NOT FIT, AND THE ONE SHAPE THEY SHARE. ***
 *
 * With the scroll floor above, `fit_test` at 420x300 — a quadrant of a small screen — left four
 * modules overflowing: `bank` by 210px, `trivia` by 43, `wordforge` by 28, `lessons` by 6.
 * Reachable, because the panel scrolls; still worse than they need to be.
 *
 * They are all built the same way, and it is a good way: a header that says where you are, a
 * body that is the actual content, and sometimes a footer with the button that moves you on.
 * When such a panel scrolls AS A WHOLE, scrolling down to the answers takes the QUESTION off
 * screen, and scrolling to the last answer takes the Next button with it. So the fix is not
 * "make the text smaller" — it is to scroll the BODY and pin the rest, which is what these
 * layouts were already describing.
 *
 * Scoped to `@container cell`, so it applies only where a host has declared a fixed box
 * (`mod-box`). On a page that lets a module be as tall as it likes — the composer preview, a
 * test harness that forgets to opt in — nothing changes and nothing needs to.
 *
 * ONE RULE, FOUR SELECTORS, ON PURPOSE. Four bespoke rules would be four things to keep in
 * step, and the next module with a header and a body would get a fifth. */
@container cell (min-height: 1px){
  .tv,.wordforge,.lessons,.bk{height:100%;min-height:0;display:flex;flex-direction:column}
  /* The body is the only part that scrolls. `min-height:0` is what lets a flex child shrink
     below its content at all — without it the list simply pushes the footer out of the box,
     which is the bug rather than the fix. */
  .tv .tv-opts,.wordforge .wf-body,.lessons .l-list{flex:1 1 auto;min-height:0;overflow:auto}
  /* `.tv-opts` is a grid, and a grid stretched by `flex:1` stretches its ROWS — four answer
     buttons in a tall panel would each become a quarter of the panel. The 58px touch target is
     deliberate (somebody pressing with poor aim) and must not grow into a slab either. */
  .tv .tv-opts{align-content:start}
  /* The bank is the same shape wearing different clothes: heading and help at the top, the
     paste area in the middle, validation output underneath. So the TEXTAREA is its body, and it
     takes whatever room is left rather than demanding a fixed 300px — which alone overflowed a
     300px panel before a word of the surrounding copy was drawn.
     `height`, not only `min-height`, and that is worth writing down: relaxing `min-height`
     first changed the measured overflow by exactly ZERO, because `<textarea rows="14">` gets
     its height from the ROW COUNT, and a floor that is no longer binding does not lower a
     height something else is setting. `resize:vertical` still works, so anybody who wants a
     bigger editor can drag it. */
  .bk .bk-text{flex:1 1 auto;min-height:90px;height:auto}
}

/* modules.css — shared component styles for module internals + the theme palette
   variables. Linked by kiosk.html (the product surface). index.html (the dev
   harness) still carries its own inline copy; consolidating it onto this file is a
   later cleanup. theme.js overrides these :root vars per active profile. */

:root{
  --text-strong:#0A3323; --accent:#839958; --on-dark:#F7F4D5; --accent-warm:#D3968C;
  --link:#105666; --bg:#F7F4D5; --surface-alt:#FBF9E9; --surface:#FFFFFF;
  --text:#0A3323; --text-soft:#3c5346; --text-muted:#5d7064; --border:#e4e0c2;
  --accent-warm-deep:#a85f52; --radius:16px;
  --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box}

/* clock */
.clock{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;position:relative}
.clock .time{font-weight:800;color:var(--text-strong);line-height:1;font-variant-numeric:tabular-nums}
.clock[data-size="s"] .time{font-size:2.2rem}
.clock[data-size="m"] .time{font-size:3.2rem}
.clock[data-size="l"] .time{font-size:4.4rem}
/* `--text-soft`, not `--link`, and it is a token fix rather than a colour preference.
   `--link` is an ACCENT: themes are free to make it whatever their accent is, and two of
   them do — forge sets it to amber `#B5651D` and warm to `#a85a2a`. Measured against the card
   the clock now sits on, that is 4.34 and 4.86 against a 4.5 floor: one failing outright and
   the other a rounding error away. `--text-soft` is the token FOR secondary text and clears
   8.34 (default), 8.85 (dusk), 9.63 (forge), 8.97 (warm), 18.88 (contrast).
   "Tuesday morning" is supporting text, not an accent, so it was reading the wrong variable. */
.clock .when{font-size:1rem;color:var(--text-soft);font-weight:600;text-transform:capitalize}
.clock .date{font-size:.9rem;color:var(--text-muted)}
/* In a sized box, ignore data-size and scale to the room available. Bounded on BOTH ends:
   a clock nobody can read from a bed is useless, and one that runs off the edge is worse.
   Width is in the min() too — "10:38 PM" in a wide, short cell would otherwise overflow
   sideways long before it ran out of height. */
@container cell (min-height: 1px){
  /* CENTER IT IN THE CELL. `.clock` stretches to the full height of the panel it is given,
     but its contents packed at the top — a clock sitting against the ceiling of an empty
     quadrant, which is what "the clock is off center" was. Scoped to a sized box on
     purpose: the kiosk's translucent clock HUD is NOT a `.mod-box`, and it deliberately
     hugs its corner. */
  .clock{height:100%;justify-content:center}
  /* `[data-size]` is not decoration: without it these lose to the
     `.clock[data-size="m"] .time` rule above on specificity, and nothing changes. */
  .clock[data-size] .time{font-size:clamp(1.4rem, min(30cqh, 16cqw), 6rem)}
  .clock[data-size] .when{font-size:clamp(.7rem, 7cqh, 1.1rem)}
  .clock[data-size] .date{font-size:clamp(.65rem, 6cqh, 1rem)}
}
.clock .gear,.clock .settings{display:none}

/* camera */
.camera{width:100%;height:100%;position:relative}
.camera .stage{position:relative;width:100%;height:100%;background:var(--letterbox);border-radius:10px;
  overflow:hidden;display:flex;align-items:center;justify-content:center}
/* CONTAIN, NOT COVER. Mike, 2026-09-02: *"You made the camera fit the screen and now the top and
   bottom are cropped off. I want it to fill so you can see everything."* `cover` fills the box by
   throwing away whatever does not fit, which on a rearview mirror means throwing away part of the
   room — and the whole point of the panel is seeing who is there. Letterboxing is the cheaper
   price. The background colour is what the bars are made of. */
.camera video{width:100%;height:100%;object-fit:contain;display:block;background:var(--letterbox)}
.camera .status{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
  color:var(--on-dark);background:rgba(10,51,35,.72);font-size:.85rem;text-align:center;padding:10px}
.camera .status button{border:1px solid rgba(247,244,213,.5);color:var(--on-dark);background:transparent;
  border-radius:8px;padding:6px 10px;cursor:pointer}
.camera .gear,.camera .settings{display:none}

/* photos */
.photos{width:100%;height:100%;position:relative}
/* TRANSPARENT, NOT `--surface` — CHANGED 2026-09-27, Mike, off the live site: "The background on
   photos won't go transparent no matter how you set it, you get the green bars on the sides."
   Root cause, confirmed before fixing: this rule painted `--surface` directly on `.stage`, a
   descendant of `.k-mod` — the new `panelSurface` setting (kiosk.js/kiosk.css, 2026-09-23) only
   ever changes `.k-mod`'s OWN background, and never touched this one, so the letterbox gaps kept
   showing a solid colour no matter what the panel setting said. Making `.stage` transparent lets
   `.k-mod`'s own CURRENT background show through instead — in `solid` mode this paints the exact
   same `--surface` colour as before (zero visual change, since that IS `.k-mod`'s own default
   background), and in `veil`/`clear` mode the letterbox gaps now correctly show the live scene
   too, which is what Mike's original 2026-09-21 ask ("just have it show the theme") means now
   that a theme can BE a moving scene rather than only a flat colour. */
.photos .stage{position:relative;width:100%;height:100%;background:transparent;border-radius:10px;
  overflow:hidden;display:flex;align-items:center;justify-content:center}
.photos .stage img,.photos .stage video{position:relative;width:100%;height:100%;object-fit:contain;
  display:block;background:transparent}
.photos .status{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
  color:var(--on-dark);background:rgba(10,51,35,.72);font-size:1rem;text-align:center;padding:12px}
.photos .status button{border:1px solid rgba(247,244,213,.5);color:var(--on-dark);background:transparent;border-radius:8px;padding:6px 10px}
.photos .nav,.photos .gear,.photos .settings{display:none}

/* A status message over a panel that HAS something to show. The full-bleed version is a
   72%-opaque sheet, which is right over an empty stage and badly wrong over the photo
   somebody is looking at — "Loading photos…" fires on every reload. As a chip it says the
   same thing without taking the picture away. Shared by photos and camera because it is
   the same rule for the same reason. */
.photos .status.chip,.camera .status.chip{inset:auto 8px 8px auto;padding:5px 11px;border-radius:99px;
  font-size:.78rem;background:rgba(10,51,35,.82);max-width:80%}
/* hide picture (2026-10-09): "Hidden. Say 'bring it back' to undo." top-left, so it never sits on the source's chip
   (bottom-right); and the two buttons a long press on the picture opens, in the middle, over the picture. */
.photos .status.photos-notice{inset:8px auto auto 8px}
.photos .photo-menu{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;
  gap:8px;padding:12px;border-radius:12px;background:rgba(10,51,35,.9);z-index:3}
.photos .photo-menu button{font-size:1rem;padding:10px 16px;border-radius:8px;border:1px solid rgba(247,244,213,.5);
  color:var(--on-dark);background:transparent;cursor:pointer}
.photos .photo-menu button:disabled{opacity:.45;cursor:default}

/* youtube */
.youtube{width:100%;height:100%;position:relative}
.youtube .stage{position:relative;width:100%;height:100%;background:var(--letterbox);border-radius:10px;
  overflow:hidden;display:flex;align-items:center;justify-content:center}
.youtube .stage iframe{width:100%;height:100%;border:0;display:block}
.youtube .status{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
  color:var(--on-dark);background:rgba(10,51,35,.72);font-size:1rem;text-align:center;padding:12px}
/* *** YOUTUBE KEEPS ITS OWN CONTROLS. IT IS THE ONLY WAY TO CHOOSE A VIDEO. ***
 *
 * G13, Mike off the live site: *"YouTube's overlay covers everything on hover and there is no
 * obvious way to choose a video from the panel."*
 *
 * There was no way at all, obvious or otherwise. This rule was `.youtube .nav,.youtube .gear,
 * .youtube .settings{display:none}` — the same line photos and camera carry, written when the
 * universal settings menu took over per-panel settings. For those two that is right: everything
 * they can be told is declared in their manifest, so the menu reaches all of it.
 *
 * YouTube is not like them. Its manifest declares exactly ONE setting (`heldNotifyMs`), while
 * **adding a video, using a playlist, the schedule, shuffle and auto-advance all live in this
 * hidden panel** — so the entire content of the module was unreachable from anywhere in the
 * product. Nothing else edits a playlist; I checked. That is A14's shape: config no UI can set.
 *
 * The arrows matter for the second half of what he said. The overlay covering the frame is
 * YouTube's own player chrome inside a cross-origin iframe and this page cannot restyle a pixel
 * of it — but it does not have to. A previous/next of our own, OUTSIDE the iframe, is a way to
 * change the video that does not involve fighting their overlay for a click.
 *
 * Small, low-contrast and at an edge, so a panel somebody is watching is still a panel
 * somebody is watching. `.settings` stays closed until the gear is pressed — it is `hidden` in
 * the markup, and `[hidden]` wins.
 *
 * *** MOVED FROM THE BOTTOM TO THE TOP, 2.7. *** It used to sit at `bottom:8px`, and the
 * kiosk's OWN transport bar (`.k-controls` in kiosk.css) floats at `bottom:2.2vmin` over
 * whatever is on the stage, with no margin reserved for it. When a youtube panel fills the
 * stage the two are in the same few pixels — proved geometrically in `transport_test.html`,
 * not eyeballed: `.youtube .nav`'s own rect landed fully inside `.k-controls`'s rect, and the
 * bar's z-index (500) sits above the nav's (304), so the bar was drawn OVER the arrows, not
 * beside them. Removing the arrows was considered and rejected — G13, above, is Mike's own
 * reason they exist as a SECOND way to change videos outside the iframe, not a redundant one,
 * and the kiosk's global Back/Next reaching the same `youtube/prev`/`next` topics does not make
 * this copy dead: the bar auto-hides after 3s and the arrows do not. A FIXED bottom offset big
 * enough to clear the bar was rejected too — the bar wraps to a second row once enough panels
 * are on screen (kiosk.css's own header measured a wrapped bar at 130px tall in one test, well
 * past a single-row guess), so any hardcoded clearance is one crowded screen away from being
 * wrong again. The top edge is not bottom-anchored by anything, ever, so pairing the arrows
 * with the gear there removes the collision by construction instead of by measurement.
 */
.youtube .nav{position:absolute;left:8px;top:8px;z-index:calc(var(--z-panel-contents) + 4);
  display:flex;align-items:center;gap:8px;padding:5px 10px;border-radius:99px;
  background:rgba(7,18,13,.72);color:#cfe0d6;max-width:calc(100% - 60px)}
.youtube .nav .yt-label{font-size:.76rem;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;max-width:26ch}
.youtube .ybtn{min-width:34px;min-height:34px;border-radius:99px;cursor:pointer;
  border:1px solid rgba(255,255,255,.28);background:transparent;color:#cfe0d6;font-size:1rem}
.youtube .ybtn:hover{background:rgba(255,255,255,.12)}
/* z-index 6, ABOVE the settings sheet (5). The panel is `inset:8px` and was covering the gear
   that opened it, so the one control somebody would reach for to get out was underneath the
   thing they wanted to leave. */
.youtube .gear{position:absolute;top:8px;right:8px;z-index:calc(var(--z-panel-contents) + 6);min-width:34px;min-height:34px;
  border-radius:99px;cursor:pointer;border:1px solid rgba(255,255,255,.28);
  background:rgba(7,18,13,.72);color:#cfe0d6}
.youtube .gear:hover{background:rgba(7,18,13,.92)}
/* The panel itself: a sheet over the video, scrolling inside its own box, because a playlist
   is a list and a panel can be a quarter of a screen. */
.youtube .settings{position:absolute;inset:8px;z-index:calc(var(--z-panel-contents) + 5);overflow:auto;padding:12px 14px;
  border-radius:12px;background:rgba(7,18,13,.96);color:#e7efe9;
  border:1px solid rgba(255,255,255,.16);font-size:.86rem}
/* The way out, first thing in the panel and full width -- it is the control somebody wants when
   they are lost, and hunting for it is the problem. */
.youtube .yt-close{display:block;width:100%;margin:0 0 10px;min-height:40px;
  border-radius:8px;border:1px solid rgba(255,255,255,.28);background:rgba(255,255,255,.08);
  color:inherit;font:inherit;cursor:pointer}
.youtube .yt-close:hover{background:rgba(255,255,255,.16)}
.youtube .settings .chk{display:flex;align-items:center;gap:8px;margin:0 0 8px}
.youtube .settings .addrow{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 8px}
.youtube .settings .addrow input{flex:1 1 140px;min-width:0;padding:7px 9px;border-radius:8px;
  border:1px solid rgba(255,255,255,.24);background:rgba(255,255,255,.06);color:inherit;
  font:inherit}
.youtube .settings .addrow button,.youtube .settings .list button{min-height:34px;padding:6px 12px;
  border-radius:8px;border:1px solid rgba(255,255,255,.28);background:transparent;color:inherit;
  cursor:pointer}
.youtube .settings .list{display:flex;flex-direction:column;gap:5px;margin-top:8px}
/* The schedule editor. Wraps rather than scrolls sideways, because a panel can be a quarter of
   a screen and four fields in a row do not fit one. */
.youtube .yt-schedadd{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 6px}
.youtube .yt-schedadd input[type=text]{flex:1 1 140px;min-width:0}
.youtube .yt-schedadd input{padding:7px 9px;border-radius:8px;
  border:1px solid rgba(255,255,255,.24);background:rgba(255,255,255,.06);color:inherit;font:inherit}
.youtube .yt-schedadd button{min-height:34px;padding:6px 12px;border-radius:8px;
  border:1px solid rgba(255,255,255,.28);background:transparent;color:inherit;cursor:pointer}
.youtube .yt-schhead{font-size:.82rem;opacity:.85;margin-bottom:4px}
/* Search results. Capped and scrolling: ten results in a quarter-screen panel would push the
   playlist and the schedule out of reach, and `mod-host` scrolling the whole panel to get past
   them is worse than the list scrolling itself. */
.youtube .yt-results{margin:4px 0 8px;max-height:34cqh;overflow:auto;
  display:flex;flex-direction:column;gap:4px;font-size:.84rem}
.youtube .yt-results:empty{display:none}
.youtube .ytrow{display:flex;align-items:center;gap:8px;justify-content:space-between;
  font-size:.84rem}
.youtube .ytrow button{min-height:30px;padding:4px 10px;border-radius:8px;
  border:1px solid rgba(255,255,255,.28);background:transparent;color:inherit;cursor:pointer}

/* PAUSED, ON PURPOSE, BY SOMEBODY IN THE ROOM.
 *
 * A held pause has to be visibly different from a broken screen. Somebody walking in — an
 * aide, a relative, the person who paused it and came back — needs to tell "this is waiting
 * for you" from "this has crashed" without asking anybody, because the one person who is
 * always there cannot answer.
 *
 * So it is a QUIET CORNER MARKER, not the full-panel scrim `.status` uses. The video is
 * still on screen, still the thing to look at; the marker only says why it is not moving.
 * A full overlay would hide the frame her family is in the middle of. */
.youtube .held,.personal .held{
  position:absolute;top:10px;left:10px;z-index:calc(var(--z-panel-contents) + 3);
  padding:5px 12px;border-radius:99px;
  background:rgba(10,51,35,.82);color:var(--on-dark);
  font-size:.85rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  pointer-events:none;
}
.youtube .held::before,.personal .held::before{content:"⏸ "}
@media (prefers-reduced-motion: no-preference){
  /* A slow breath, not a blink. It marks the screen as alive without becoming something that
     pulls at the eye of somebody who cannot look away from it. */
  .youtube .held,.personal .held{animation:heldbreath 4s ease-in-out infinite}
}
@keyframes heldbreath{0%,100%{opacity:.95}50%{opacity:.6}}

/* personal videos */
.personal{width:100%;height:100%;position:relative;display:flex;flex-direction:column}
.personal .stage{position:relative;flex:1;min-height:0;background:var(--letterbox);border-radius:10px;
  overflow:hidden;display:flex;align-items:center;justify-content:center}
.personal .stage video{width:100%;height:100%;object-fit:contain;display:block;background:var(--letterbox)}
.personal .caption{margin-top:8px;text-align:center;font-weight:700;font-size:1.3rem;color:var(--on-dark)}
.personal .status{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
  color:var(--on-dark);background:rgba(10,51,35,.72);font-size:1rem;text-align:center;padding:12px}
/* Matches `.photos .status button` — both status lines can now offer an action
   (Retry, or "Connect a folder"), same visual treatment either way. */
.personal .status button{border:1px solid rgba(247,244,213,.5);color:var(--on-dark);
  background:transparent;border-radius:8px;padding:6px 10px;cursor:pointer}
.personal .nav{display:none}

/* educational — a single live graphic, drawn in the profile theme */
.educational{width:100%;height:100%;display:flex;flex-direction:column;align-items:center}
.educational .stage{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;align-items:center;
  justify-content:center;background:var(--surface-alt);border:1px solid var(--border);border-radius:10px}
.educational .caption{margin-top:8px;text-align:center;font-weight:700;color:var(--link)}
.educational .nav{display:none}
.educational .ig-number{font-size:9rem;font-weight:800;color:var(--text-strong);line-height:1;font-variant-numeric:tabular-nums}
.educational .ig-dots{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:16px;max-width:70%}
.educational .ig-dots i{width:22px;height:22px;border-radius:50%;background:var(--accent);display:block}
.educational .ig-letters{font-size:10rem;font-weight:800;color:var(--link);line-height:1}
.educational .ig-word{display:flex;flex-direction:column;align-items:center;gap:10px}
/* Was a small caption under a big letter-spelled headline (`.ig-spell`, removed 2026-09-08 --
   see graphicHTML in modules/educational.js) -- now it IS the display, so it is sized to read
   as the primary graphic rather than a caption underneath one. */
.educational .ig-wordfull{font-size:5rem;color:var(--text-strong);font-weight:800}

/* director (Lineup) — one provider slot at a time, filling its container */
.director{width:100%;height:100%;position:relative;display:flex;flex-direction:column}
.director .d-stage{position:relative;flex:1;min-height:0}
.director .d-slot{width:100%;height:100%}
.director .d-bar{display:none}   /* the kiosk provides its own controls */

/* THE COMMUNICATION BOARD. Big targets, high contrast, and a lit card that is unmistakable
   from a bed at one to two meters — the distance the symbols were drawn for.

   *** THE BOARD CARRIES ITS OWN SURFACE AND DOES NOT FOLLOW THE PROFILE THEME (2026-09-02). ***

   It used to draw on `--surface` / `--text-strong` / `--border`, so switching a profile to Warm or
   Forge repainted the one panel somebody uses to SAY THINGS. That is the wrong thing to leave
   downstream of a decoration setting, and the numbers say the same:

     * every meaning-bearing hex in `aac_symbols.js` is a LIGHT value. Measured 2026-09-02:
       on this dark card they run 4.71 (love) to 8.24 (cold); on a white card the identical
       hexes run 1.91 to 3.35 — all eight below the 4.5 floor.
     * the eight `.ab-*` word colours did the exact reverse: 5.25–8.25 on white, 1.91–3.00 on
       a dark card. Two palettes pulling opposite ways over one set of cards.

   So the surface is pinned DARK and the words are repainted to the symbol palette. One palette
   now serves the drawing and the word under it, and a card cannot be lit one way and lettered
   the other.

   THE CARD BOUNDARY IS THE BORDER, NOT THE FILL, and that is forced rather than chosen: any
   two dark greys sit around 1.1–1.3:1 apart, so no dark-on-dark fill can reach the 3:1 non-text
   floor. `--ab-line` is therefore bright enough to clear 3:1 against BOTH the card and the
   board behind it (measured: 4.91 and 5.57), because an empty slot is drawn as the background
   showing through and "card" vs "hole" has to be unmistakable. */
.aboard{
  position:absolute;inset:0;padding:calc(var(--u,1px) * 3);box-sizing:border-box;
  --ab-bg:#12181c;      /* the board behind the cards, and what an empty slot shows */
  --ab-card:#1b2429;    /* the card face */
  --ab-ink:#e8eef0;     /* the word, and every neutral `currentColor` shape in a symbol */
  --ab-line:#7d939d;    /* the card edge — 4.91 on the card, 5.57 on the board */
  --ab-lit:#8fae63;     /* the scan highlight — 6.31 on the card */
  background:var(--ab-bg);
}
/* THEME-FOLLOWING IS A SETTING, DEFAULTING TO PINNED (`followTheme` in modules/board.js).
   A profile that wants the board to move with everything else can have it — this is a default
   about which risk is worse, not a claim that nobody should ever want the other thing. The
   contrast numbers above are only guaranteed for the pinned palette. */
.aboard.ab-themed{
  --ab-bg:var(--bg,#12181c);
  --ab-card:var(--surface,#fff);
  --ab-ink:var(--text-strong,#0A3323);
  --ab-line:var(--border,#e4e0c2);
  /* The theme's --focus, not its raw --accent (2026-10-04): --ab-lit is the lit card's edge and the board's
     focus ring as well as a fill, so it has to clear 3:1 on the theme's surfaces, which --focus is made to do
     (theme.js focusColor: the accent kept exactly where it already passes, a shade deeper where it does not). */
  --ab-lit:var(--focus, var(--accent,#839958));
}
/* THE BOARD'S OWN HIGH-CONTRAST SWITCH. It needs one BECAUSE it no longer inherits the
   `contrast` theme, and losing that for the one panel somebody talks through would be a
   straight downgrade. Black on white at 21:1, and every role collapses to the ink — colour
   was never the only cue for anything here (each card also carries its word), so dropping it
   costs nothing and buys the highest contrast available. */
.aboard.ab-hc{
  --ab-bg:#ffffff;
  --ab-card:#ffffff;
  --ab-ink:#000000;
  --ab-line:#000000;
  --ab-lit:#000000;
}
/* *** THE BOARD SWITCHER — a row of boards above the cards, not a menu item. ***
 *
 * Mike: *"Make switching boards visible rather than buried behind the menu."* He built this
 * module and still had to know the system was there to find the larger board inside it.
 *
 * It is deliberately SMALL and deliberately at the top. It is a caregiver's control on a
 * surface somebody talks through, so it must not compete with the cards for either space or
 * attention — and it is never part of the scan, because "switch board" is not a thing anybody
 * should say by accident in the middle of a sentence.
 *
 * Sized in `--u` like everything else here, so it shrinks with the board when the board is one
 * quadrant of a screen rather than the whole of it. */
.aboard{display:flex;flex-direction:column;gap:calc(var(--u,1px) * 2)}
.ab-boards{display:flex;flex-wrap:wrap;gap:calc(var(--u,1px) * 2);flex:0 0 auto;
  justify-content:center}
.ab-bchip{border:1px solid var(--ab-line);background:transparent;color:var(--ab-ink);
  border-radius:999px;padding:calc(var(--u,1px) * 1.6) calc(var(--u,1px) * 5);
  font:600 clamp(11px,calc(var(--u,1px) * 9),18px)/1.2 var(--font,system-ui,sans-serif);
  cursor:pointer;opacity:.72;min-height:34px}
.ab-bchip:hover{opacity:1}
.ab-bchip[aria-pressed="true"]{background:var(--ab-lit);border-color:var(--ab-lit);
  color:var(--ab-bg);opacity:1}
.ab-bchip:focus-visible{outline:3px solid var(--ab-lit);outline-offset:2px}
/* *** THE BOARD'S SETTINGS PANEL — the caregiver's controls, off the talking surface. ***
 *
 * Mike, 2026-09-06: the board row *"sits above the cards where somebody who can only manage
 * yes/no/other can hit it and lose their board."* So the caregiver's controls move behind a
 * deliberate press and the cards keep the surface.
 *
 * The gear is DELIBERATELY SMALL AND CORNERED and deliberately not sized in `--u`: it must not
 * grow with the cards, because the whole point is that it is not one. Its hit target still
 * clears 44px, which is the floor for anything anybody has to press on purpose.
 *
 * The panel is a plain surface at a fixed readable size, again not `--u`: it is read by
 * somebody standing at the screen, not by the person the board is for, so it wants ordinary
 * body text rather than something scaled to a card. */
.ab-gear{position:absolute;top:6px;right:6px;z-index:calc(var(--z-panel-contents) + 3);
  width:44px;height:44px;border-radius:50%;
  border:1px solid var(--ab-line);background:var(--ab-card);color:var(--ab-ink);
  font-size:19px;line-height:1;cursor:pointer;opacity:.5;
  display:flex;align-items:center;justify-content:center}
.ab-gear:hover,.ab-gear:focus-visible{opacity:1}
.ab-gear:focus-visible{outline:3px solid var(--ab-lit);outline-offset:2px}
/* `max-height` + scroll: this panel grows as the module does, and a panel taller than the
   board would put its own Done button off the bottom of a small dashboard cell — which is a
   surface somebody cannot close, on the one module where that matters most. */
.ab-panel{position:absolute;top:54px;right:6px;z-index:calc(var(--z-panel-contents) + 4);
  width:min(320px,calc(100% - 12px));max-height:calc(100% - 64px);overflow:auto;
  box-sizing:border-box;padding:12px 14px;
  /* *** THE CARD COMPOSITES OVER THE SURFACE, AND BY DEFAULT IT IS STILL OPAQUE. ***
     `--ab-card-alpha` is 1, so this is the same solid card the contrast numbers at the top of
     the board's palette were measured against. A board that turns the live surface on turns
     this down deliberately, and knowingly re-opens those measurements -- which is a choice
     somebody makes for a screen, not a default that arrives underneath them. */
  background:color-mix(in srgb, var(--ab-card) calc(var(--ab-card-alpha, 1) * 100%), transparent);
  color:var(--ab-ink);
  border:1px solid var(--ab-line);border-radius:10px;
  box-shadow:0 10px 30px rgba(0,0,0,.35);
  font:14px/1.45 var(--font,system-ui,sans-serif);
  display:flex;flex-direction:column;gap:8px}
.ab-prow{display:flex;align-items:center;gap:8px;justify-content:space-between}
.ab-prow>span{font-weight:600}
.ab-panel select{flex:1 1 auto;min-width:0;min-height:36px;padding:5px 7px;
  background:var(--ab-bg);color:var(--ab-ink);
  border:1px solid var(--ab-line);border-radius:7px;font:inherit}
.ab-pcheck{justify-content:flex-start;cursor:pointer}
.ab-pcheck input{width:20px;height:20px;flex:0 0 auto;accent-color:var(--ab-lit)}
.ab-pcheck>span{font-weight:500}
/* The notes carry the reasoning a caregiver needs to make the choice — what a lock does NOT
   do, and how to get back out of it. Quieter than the control, never hidden behind one. */
.ab-pnote{margin:-4px 0 4px 28px;font-size:12.5px;line-height:1.4;opacity:.72}
.ab-pbtns{gap:8px;flex-wrap:wrap;justify-content:flex-start}
.ab-pbtn{min-height:38px;padding:7px 12px;cursor:pointer;
  border:1px solid var(--ab-line);border-radius:8px;
  background:var(--ab-bg);color:var(--ab-ink);font:600 13.5px/1.2 var(--font,system-ui,sans-serif)}
.ab-pbtn:hover{border-color:var(--ab-lit)}
.ab-pbtn:focus-visible{outline:3px solid var(--ab-lit);outline-offset:2px}
.ab-pdone{background:var(--ab-lit);border-color:var(--ab-lit);color:var(--ab-bg)}
/* THE FALLBACKS ARE NOT DECORATION. Every colour here comes from the board's palette, which
   `.aboard` sets -- so mounted anywhere else (a test page, a settings screen later) the editor
   would render as black text on nothing, with borderless inputs. Fallbacks inside `var()`
   rather than a redeclaration on `.bedit`: redeclaring would win over the board's own palette
   and take high-contrast mode with it. */
/* *** THE BOARD EDITOR — making a board, over the board. ***
 *
 * It covers the cards, which is the one thing on this module that has to be justified: the
 * person the board belongs to cannot talk while it is open. Two things make that acceptable
 * and both are structural rather than promises — it only opens from the settings panel, which
 * only a caregiver reaches, and it CLOSES ITSELF after a few idle minutes (see
 * `board_editor.js`). A form somebody could be left behind is the failure this module could
 * least afford.
 *
 * Sized in px, not `--u`: it is read and typed into by somebody standing at the screen, so it
 * wants ordinary form text rather than something scaled to a card. */
.ab-editor{position:absolute;inset:0;z-index:calc(var(--z-panel-contents) + 6);overflow:auto;
  background:var(--ab-bg,#12181c);color:var(--ab-ink,#e8eef0)}
.bedit{padding:12px 14px 16px;box-sizing:border-box;
  background:var(--ab-bg,#12181c);color:var(--ab-ink,#e8eef0);min-height:100%;
  font:14px/1.45 var(--font,system-ui,sans-serif);
  display:flex;flex-direction:column;gap:10px}
.be-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  font-size:15px}
.be-count{opacity:.7;font-size:12.5px;font-weight:600}
.be-field{display:flex;flex-direction:column;gap:4px}
.be-field>span{font-weight:600;font-size:12.5px;opacity:.85}
.be-field>span i{font-weight:400;font-style:normal;opacity:.7}
.bedit input[type=text],.bedit input[type=number]{min-height:38px;padding:6px 9px;
  background:var(--ab-card,#1b2429);color:var(--ab-ink,#e8eef0);border:1px solid var(--ab-line,#7d939d);
  border-radius:8px;font:inherit;min-width:0}
.be-dims{flex-direction:row;gap:12px}
.be-dims label{display:flex;flex-direction:column;gap:4px;flex:1 1 0;min-width:0}
.be-dims label>span{font-weight:600;font-size:12.5px;opacity:.85}
/* The slots are a MINIATURE OF THE BOARD, in the board's own grid, because "set rows and
   columns" is a question nobody can answer from two number fields alone. Fixed row height:
   this is a map of the board, not a preview of how big the cards will be. */
.be-slots{display:grid;gap:5px;grid-auto-rows:52px}
.be-slot{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
  min-width:0;overflow:hidden;padding:3px;cursor:pointer;
  background:var(--ab-card,#1b2429);color:var(--ab-ink,#e8eef0);
  border:1px solid var(--ab-line,#7d939d);border-radius:7px;
  font:600 12px/1.15 var(--font,system-ui,sans-serif);text-align:center}
.be-slot-word{overflow:hidden;text-overflow:ellipsis;max-width:100%;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.be-slot-pic{font-size:11px;opacity:.7;line-height:1}
.be-slot-empty{border-style:dashed;opacity:.5;font-weight:400}
.be-slot-on{border-color:var(--ab-lit,#8fae63);box-shadow:0 0 0 2px var(--ab-lit,#8fae63);opacity:1}
.be-slot:focus-visible{outline:3px solid var(--ab-lit,#8fae63);outline-offset:2px}
.be-card{display:flex;flex-direction:column;gap:8px;padding-top:2px}
.be-picrow{flex-direction:row;align-items:center;gap:8px;flex-wrap:wrap}
.be-picwrap{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.be-picpath{font-size:11.5px;opacity:.75;overflow:hidden;text-overflow:ellipsis;max-width:16ch;
  white-space:nowrap}
.be-hint{margin:0;font-size:12.5px;line-height:1.4;opacity:.72}
.be-warn{margin:0;padding:7px 9px;border-radius:7px;font-size:12.5px;line-height:1.4;
  background:rgba(211,150,140,.16);border:1px solid #d3968c;color:var(--ab-ink,#e8eef0)}
.be-btn{min-height:38px;padding:7px 12px;cursor:pointer;
  border:1px solid var(--ab-line,#7d939d);border-radius:8px;
  background:var(--ab-card,#1b2429);color:var(--ab-ink,#e8eef0);
  font:600 13px/1.2 var(--font,system-ui,sans-serif)}
.be-btn:hover{border-color:var(--ab-lit,#8fae63)}
.be-btn:focus-visible{outline:3px solid var(--ab-lit,#8fae63);outline-offset:2px}
.be-primary{background:var(--ab-lit,#8fae63);border-color:var(--ab-lit,#8fae63);color:var(--ab-bg,#12181c)}
.be-picgrid-btns{display:flex;flex-wrap:wrap;gap:7px}
/* `auto-fill` with a floor, so the thumbnails stay pickable in a dashboard cell and do not
   become a wall of 30px squares nobody can tell apart. */
.be-picgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(72px,1fr));gap:6px;
  max-height:210px;overflow:auto;padding:2px}
.be-pic{padding:0;border:1px solid var(--ab-line,#7d939d);border-radius:7px;overflow:hidden;
  cursor:pointer;background:var(--ab-card,#1b2429);aspect-ratio:1;min-height:0}
.be-pic img{width:100%;height:100%;object-fit:cover;display:block}
.be-pic:hover,.be-pic:focus-visible{border-color:var(--ab-lit,#8fae63);outline:none}
.be-foot{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;padding-top:2px}
.be-idle{padding-bottom:2px}
/* THE LAST STRETCH, SAID OUT LOUD. A form that vanishes without warning is why this behaviour
   felt arbitrary; a countdown somebody can see and dismiss is the opposite of the undismissable
   gate the rule is about — it is the screen saying what it is about to do while there is still
   time to say no. Warm rather than alarming: nothing is going wrong and nothing is being lost. */
.be-idlewarn{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding:9px 11px;border-radius:8px;
  background:rgba(255,243,217,.14);border:1px solid var(--ab-lit,#8fae63);
  font-size:12.5px;line-height:1.4}
.be-idlewarn>span{flex:1 1 14ch;min-width:0}
/* `height:auto` and `flex:1`, not `height:100%`: with a switcher above it, a grid claiming the
   full height of a flex column overflows by exactly the height of the switcher. */
/* *** THE THREE LAYERS OF A BOARD. ***
 *
 * A board is a SURFACE WITH CARDS OVER IT, not 36 sealed boxes. The depths come from
 * `layers.css` -- the world band for the live layer, the floating band for things in flight --
 * so a balloon on this board and a balloon on the dashboard mean the same thing by the same
 * number.
 *
 * `.aboard` is made a stacking context (`isolation`), so these bands are relative to THE BOARD:
 * a card cannot accidentally out-rank the transport bar three layers up the page.
 */
.aboard{isolation:isolate}
/* UNDER the cards. Empty by default; whatever drifts or breathes goes here, and the cards
   composite over it. It cannot take a press, and it is `aria-hidden` in the markup -- it is
   scenery, and a communication board must never hand a screen reader scenery among the words. */
.ab-surface{position:absolute;inset:0;z-index:var(--z-world);pointer-events:none;
  border-radius:inherit;overflow:hidden}
/* OVER the cards, spanning the whole board. THIS IS WHAT LETS AN ANIMATION CROSS A CELL
   BOUNDARY: a thing in flight lives here and travels in board coordinates, rather than being
   handed from one cell to the next and clipped at every edge.
   `pointer-events:none` is load-bearing, not tidiness -- this layer covers every card, and a
   card is somebody's voice. It must never eat a press. */
.ab-flight{position:absolute;inset:0;z-index:var(--z-floating);pointer-events:none;
  overflow:visible}
.ab-grid{width:100%;height:auto;flex:1 1 auto;min-height:0;display:grid;
  position:relative;z-index:var(--z-panel-contents);
  gap:calc(var(--u,1px) * 4)}
.ab-card{min-width:0;min-height:0;display:flex;flex-direction:column;align-items:center;
  justify-content:center;overflow:hidden;
  gap:calc(var(--u,1px) * 3);padding:calc(var(--u,1px) * 4);box-sizing:border-box;cursor:pointer;
  background:var(--ab-card);color:var(--ab-ink);
  border:max(1px,calc(var(--u,1px) * 2.5)) solid var(--ab-line);
  border-radius:calc(var(--u,1px) * 8);
  font:inherit;transition:transform .12s ease,box-shadow .12s ease}
/* --u is ONE HUNDREDTH OF A CARD, set by the module from the container and the tier (see
   `setUnit` in modules/board.js). NOT vmin: a board can be one quadrant of a dashboard, and
   viewport units would size a card in a small panel as if it filled the screen. Ported from
   the private build, which learned this the same way. */
.ab-card .ab-sym{display:block;width:calc(var(--u,1px) * 44);height:calc(var(--u,1px) * 44);
  line-height:0;flex:0 0 auto}
.ab-card .ab-sym svg{width:100%;height:100%;display:block}
/* A PICTURE SOMEBODY CHOSE, in the slot a symbol would have had — same box, same unit, so a
   board of photographs and a board of drawings are the same shape and a person swapping one
   card for the other does not get their layout moved. `contain`, never `cover`: a face cropped
   to a square by the renderer is a card whose picture is no longer of the thing it names. The
   frame keeps its size while the file loads so nothing jumps when the picture arrives. */
.ab-card .ab-img{display:block;width:calc(var(--u,1px) * 44);height:calc(var(--u,1px) * 44);
  line-height:0;flex:0 0 auto}
.ab-card .ab-img img{width:100%;height:100%;display:block;object-fit:contain;
  border-radius:calc(var(--u,1px) * 3)}
/* *** `hyphens:auto` BEFORE `break-word`, AND IT IS NOT TYPOGRAPHY. ***
 * This was `overflow-wrap:anywhere`, which breaks a word at whatever character the line runs out
 * at — so the care board's "Keyboard" card rendered as "Keyboar / d". On a page of prose that is
 * ugly; on a card somebody is trying to READ in order to say a word, it is a card they have to
 * decode first. `hyphens:auto` gives "Key-board" with the hyphen that says a word is continuing,
 * and `break-word` stays underneath it as the last resort for a word no hyphenation can save.
 * (`hyphens:auto` needs a language: every page that mounts a module carries `<html lang="en">`.
 * Without one it is inert and this degrades to `break-word`, which is still better than
 * `anywhere`.) */
.ab-card .ab-word{font-size:clamp(11px,calc(var(--u,1px) * 22),96px);font-weight:800;
  line-height:1.05;text-align:center;overflow-wrap:break-word;hyphens:auto}
/* Word-only cards get the room the missing symbol would have taken. A card with no picture is
   a normal card, not a broken one. */
.ab-card:not(:has(.ab-sym)):not(:has(.ab-img)) .ab-word{font-size:clamp(12px,calc(var(--u,1px) * 30),120px)}
/* *** A CARD TOO SMALL FOR BOTH DROPS THE PICTURE, NEVER THE WORD. ***
   The same rule as color: the symbol SUPPORTS meaning, the word CARRIES it. A board squeezed
   into a corner of a dashboard is still readable; a board that shrank the word to fit a picture
   in would be neither. Set by `setUnit` in modules/board.js, which knows the card's real size. */
.ab-grid.ab-tight .ab-sym,.ab-grid.ab-tight .ab-img{display:none}
.ab-grid.ab-tight .ab-word{font-size:clamp(11px,calc(var(--u,1px) * 26),120px)}
/* An empty slot is DRAWN, faintly. A hole has to look like a hole: the cards after it did not
   move, and a gap that rendered as nothing would look like the board had rearranged. */
.ab-card.ab-empty{background:transparent;border-style:dashed;opacity:.28;cursor:default}
/* The scan highlight. Thick and bright rather than a subtle tint - it is the only thing telling
   somebody which card their next press will choose. */
.ab-card.ab-lit{border-color:var(--ab-lit);box-shadow:0 0 0 .8vmin rgba(143,174,99,.45);
  transform:scale(1.02)}
.ab-card.ab-press{transform:scale(.97);box-shadow:0 0 0 .8vmin rgba(143,174,99,.75)}
/* 'one at a time': the others are gone, not dimmed. Half-visible cards are worse than none. */
.ab-card.ab-away{visibility:hidden}

/* THE EIGHT WORD COLOURS ARE THE SYMBOL PALETTE (2026-09-02). Every hex below is lifted from
   `aac_symbols.js` — the accent already inside the drawing on that card — so the word and the
   picture above it are the same colour rather than two palettes over one card. Measured on
   `--ab-card` #1b2429: 4.71 (love, the weakest) to 8.24 (cold), all clear of the 4.5 floor.

   `social` and `nav` take the ink, and that is the rule rather than an omission: their symbols
   (`okay`, `hi`, `wait`, `keyboard`) are drawn in `currentColor` with no accent of their own,
   so the word matches what the drawing actually does. The rule generalises — A CARD'S WORD IS
   ITS SYMBOL'S ACCENT, OR THE INK WHERE THE SYMBOL IS NEUTRAL — which is also why no new hex
   was invented for either of them. (`thanks` shares love's pink, so borrowing it for `social`
   would have made two roles one colour and told nobody anything.)

   Colour still SUPPORTS meaning and never carries it: every card also has its word, so
   somebody who cannot separate these hues loses nothing. That is what lets `.ab-hc` collapse
   all eight to a single ink without taking information away. */
.ab-yes .ab-word{color:#6abf69}     /* symbol `yes`             — 6.96 */
.ab-no .ab-word{color:#d3968c}      /* symbol `no`/`stop`/`pain` — 6.39 */
.ab-need .ab-word{color:#e2b45a}    /* symbol `help`            — 8.20 */
.ab-social .ab-word{color:var(--ab-ink)}   /* neutral symbols  — 13.46 */
.ab-nav .ab-word{color:var(--ab-ink)}      /* neutral symbols  — 13.46 */
.ab-hot .ab-word{color:#d98a5f}     /* symbol `hot`            — 5.82 */
.ab-cold .ab-word{color:#7fc6d8}    /* symbol `cold`           — 8.24 */
.ab-love .ab-word{color:#cf6f86}    /* symbol `love`/`thanks`   — 4.71 */
/* In high contrast every role is the ink — see `.ab-hc` above for why that loses nothing. */
.aboard.ab-hc .ab-word{color:var(--ab-ink)}

/* THE HELD-STATE WALLPAPER. It covers the stage rather than replacing it: the paused
   segment is still mounted underneath and comes back untouched. Structural properties are
   here AND set nowhere else, so nothing depends on a second rule to be positioned. */
.director .d-wall{position:absolute;inset:0;z-index:var(--z-world)}
.director .d-wall[hidden]{display:none}

/* The wallpaper itself. Full-bleed, no chrome, nothing focusable — it is scenery. */
.wp{position:absolute;inset:0;overflow:hidden;background:var(--text-strong,#0A3323)}
.wp-ambient{position:absolute;inset:0}
.wp-media{position:absolute;inset:0}
.wp-media[hidden]{display:none}
.wp-layer{position:absolute;inset:0;opacity:0}
.wp-layer img,.wp-layer video{width:100%;height:100%;object-fit:cover;display:block}
.director .d-ph{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:10px;background:var(--surface-alt);border:1px dashed var(--border);border-radius:10px;text-align:center;padding:16px}
.director .d-ph-kind{font-size:2rem;font-weight:800;color:var(--text-strong)}
.director .d-ph-note{font-size:.9rem;color:var(--text-muted)}

/* sprint (focus timer) */
.sprint{display:flex;flex-direction:column;align-items:center;gap:8px;width:100%;padding:4px 0}
.sprint .s-phase{font-size:.95rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--link)}
.sprint[data-phase="work"] .s-phase{color:var(--accent)}
.sprint .s-clock{font-size:3.6rem;font-weight:800;line-height:1;color:var(--text-strong);font-variant-numeric:tabular-nums}
.sprint .s-notice{min-height:1.2em;font-size:.85rem;font-weight:600;color:var(--link);text-align:center}
.sprint .s-task{width:100%;max-width:320px;padding:8px 10px;border:1px solid var(--border);border-radius:10px;
  background:var(--surface-alt);color:var(--text);font:inherit;font-size:.95rem;text-align:center}
.sprint .s-row{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap}
.sprint .s-btn{padding:8px 16px;border:1px solid var(--border);border-radius:10px;background:var(--surface);
  color:var(--text);font:inherit;font-weight:600;cursor:pointer}
.sprint .s-btn:hover{background:var(--surface-alt)}
.sprint .s-primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent);min-width:104px}
.sprint .s-primary:hover{filter:brightness(.93);background:var(--accent)}
.sprint .s-mult{padding:6px 8px;border:1px solid var(--border);border-radius:8px;background:var(--surface);
  color:var(--text);font:inherit;font-size:.85rem}
.sprint .s-worth{font-size:.85rem;font-weight:700;color:var(--link)}
.sprint .s-foot{font-size:.8rem;color:var(--text-muted);text-align:center}

/* quests (the quest board / points economy) */
.quests{display:flex;flex-direction:column;gap:8px;width:100%}
.quests .q-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;flex-wrap:wrap}
.quests .q-balance{display:flex;align-items:baseline;gap:6px}
.quests .q-balance b{font-size:2.6rem;font-weight:800;line-height:1;color:var(--text-strong);font-variant-numeric:tabular-nums}
.quests .q-balance span{font-size:.85rem;color:var(--text-muted)}
.quests .q-sub{display:flex;gap:12px;font-size:.85rem;color:var(--text-muted)}
.quests .q-sub b{color:var(--text);font-variant-numeric:tabular-nums}
.quests .q-hours-row{display:flex;justify-content:space-between;font-size:.82rem;color:var(--text-soft)}
.quests .q-band{font-weight:700;color:var(--link)}
.quests .q-track{height:8px;border-radius:99px;background:var(--surface-alt);border:1px solid var(--border);overflow:hidden;margin-top:3px}
.quests .q-track i{display:block;height:100%;background:var(--accent);width:0}
.quests .q-topup{font-size:.76rem;color:var(--link);min-height:1em;margin-top:2px}
.quests .q-subjects{display:flex;flex-wrap:wrap;gap:10px;margin-top:4px;font-size:.74rem;color:var(--text-muted)}
.quests .q-subj b{color:var(--text-soft);font-weight:700}
.quests .q-flash{min-height:1.2em;font-size:.85rem;font-weight:600;color:var(--link)}
.quests .q-tabs{display:flex;gap:6px}
.quests .q-tab{flex:1;padding:6px 8px;border:1px solid var(--border);border-radius:9px;background:var(--surface);
  color:var(--text);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}
.quests .q-tab.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.quests .q-panel{max-height:360px;overflow-y:auto}
.quests .q-toolbar{display:flex;justify-content:flex-end;padding:2px 0 6px}
.quests .q-toggle{font-size:.8rem;color:var(--text-soft);display:flex;align-items:center;gap:5px;cursor:pointer}
.quests .q-list{display:flex;flex-direction:column;gap:5px}
.quests .q-item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;text-align:left;
  padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:var(--surface);color:var(--text);
  font:inherit;cursor:pointer}
.quests .q-item:hover:not(:disabled){background:var(--surface-alt)}
.quests .q-item:disabled{opacity:.45;cursor:not-allowed}
.quests .q-item[data-kind="Penalty"]{border-color:var(--accent-warm)}
.quests .q-item-main{display:flex;flex-direction:column;gap:1px;min-width:0}
.quests .q-item-name{font-weight:600;font-size:.9rem}
.quests .q-item-note{font-size:.74rem;color:var(--text-muted)}
.quests .q-reward-row{display:flex;flex-direction:column;gap:3px}
.quests .q-accepts{align-self:flex-start;font:inherit;font-size:.72rem;padding:2px 8px;border-radius:999px;
  border:1px solid var(--border);background:var(--surface);color:var(--text-muted);cursor:pointer}
.quests .q-ask{border:1px solid var(--border);border-radius:10px;padding:8px 10px;margin-bottom:8px;
  background:var(--surface-alt);font-size:.85rem}
.quests .q-ask p{margin:0 0 6px}
.quests .q-ask-btn{font:inherit;font-size:.8rem;padding:4px 10px;margin-right:6px;border-radius:8px;
  border:1px solid var(--border);background:var(--surface);color:var(--text);cursor:pointer}
.quests .q-item-pts{font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--link)}
.quests .q-item[data-kind="Penalty"] .q-item-pts{color:var(--accent-warm-deep)}
.quests .q-x2{font-style:normal;font-size:.68rem;margin-left:4px;color:var(--text-muted)}
.quests .q-log{width:100%;border-collapse:collapse;font-size:.78rem}
.quests .q-log th{text-align:left;color:var(--text-muted);font-weight:600;border-bottom:1px solid var(--border);padding:4px 6px}
.quests .q-log td{padding:4px 6px;border-bottom:1px solid var(--border);vertical-align:top}
.quests .q-log .q-src{display:block;font-size:.68rem;color:var(--text-muted)}
.quests .q-log .q-pos{color:var(--accent);font-weight:700;text-align:right}
.quests .q-log .q-neg{color:var(--accent-warm-deep);font-weight:700;text-align:right}
.quests .q-empty{font-size:.85rem;color:var(--text-muted);text-align:center;padding:14px}
/* currencies (2026-09-28): each balance beside the total, the why on a reward nobody can pay for, the trade block */
.quests .q-curs{display:flex;gap:12px;font-size:.85rem;color:var(--text-muted)}
.quests .q-cur{display:inline-flex;align-items:baseline;gap:4px}
.quests .q-cur b{color:var(--text-strong);font-weight:800;font-variant-numeric:tabular-nums}
.quests .q-cur-icon{width:1em;height:1em;align-self:center}
.quests .q-item-why{font-size:.74rem;color:var(--accent-warm-deep);font-weight:600}
.quests .q-exchange{margin-top:12px;padding-top:8px;border-top:1px solid var(--border)}
.quests .q-ex-head{font-size:.85rem;font-weight:700;color:var(--text-soft);margin-bottom:2px}
.quests .q-ex-head span{font-weight:400;color:var(--text-muted)}
.quests .q-ex-why{font-size:.76rem;color:var(--text-muted);min-height:1em;margin-bottom:4px}
.quests .q-inflation{font-size:.8rem;color:var(--text-soft);margin:0 0 6px}
.quests .q-price-was{color:var(--text-soft);font-weight:600}
.quests .q-log .q-cur-tag{display:inline-block;margin-left:6px;font-size:.68rem;color:var(--text-soft);font-weight:600}

/* progress (gameplay measurement — accuracy, concepts, sessions) */
.gprogress{display:flex;flex-direction:column;gap:8px;width:100%}
.gprogress .g-filters{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
.gprogress .g-filters:empty{display:none}
.gprogress .g-flabel{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-right:2px}
.gprogress .g-fbtn{padding:4px 9px;border:1px solid var(--border);border-radius:99px;background:var(--surface);
  color:var(--text);font:inherit;font-size:.78rem;cursor:pointer}
.gprogress .g-fbtn.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.gprogress .g-cards{display:flex;flex-wrap:wrap;gap:7px}
.gprogress .g-card{flex:1 1 84px;border:1px solid var(--border);border-radius:11px;background:var(--surface-alt);padding:8px 10px}
.gprogress .g-v{font-size:1.35rem;font-weight:800;line-height:1.1;color:var(--text-strong);font-variant-numeric:tabular-nums}
.gprogress .g-k{font-size:.66rem;text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted)}
.gprogress .g-h{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-top:2px}
.gprogress .g-chart svg{width:100%;height:150px;display:block;background:var(--surface-alt);
  border:1px solid var(--border);border-radius:11px}
.gprogress .g-grid{stroke:var(--border)}
.gprogress .g-axis{fill:var(--text-muted);font-size:10px}
.gprogress .g-bar{fill:var(--accent)}
.gprogress .g-tabs{display:flex;gap:6px}
.gprogress .g-tab{flex:1;padding:6px 8px;border:1px solid var(--border);border-radius:9px;background:var(--surface);
  color:var(--text);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer}
.gprogress .g-tab[hidden]{display:none}
.gprogress .g-tab.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.gprogress .g-ghost{flex:0 0 auto;font-weight:500;color:var(--link)}
.gprogress .g-ghost:disabled{opacity:.4;cursor:not-allowed}
.gprogress .g-panel{max-height:340px;overflow-y:auto}
.gprogress .g-note{font-size:.72rem;color:var(--text-muted);margin:2px 0 7px}
.gprogress .g-crow{display:grid;grid-template-columns:minmax(90px,1fr) 2fr auto;gap:8px;align-items:center;margin-bottom:6px}
.gprogress .g-cname{font-size:.82rem;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gprogress .g-cbar{height:9px;border-radius:99px;background:var(--surface-alt);border:1px solid var(--border);overflow:hidden}
.gprogress .g-cbar i{display:block;height:100%;background:var(--accent)}
.gprogress .g-cpct{font-size:.78rem;font-weight:700;font-variant-numeric:tabular-nums;text-align:right;min-width:56px}
.gprogress .g-cpct span{display:block;font-size:.64rem;font-weight:500;color:var(--text-muted)}
.gprogress .g-trend{font-style:normal;font-size:.7rem;margin-left:5px}
.gprogress .g-up{color:var(--accent)}
.gprogress .g-down{color:var(--accent-warm-deep)}
.gprogress .g-flat{color:var(--text-muted)}
.gprogress .g-table{width:100%;border-collapse:collapse;font-size:.75rem}
.gprogress .g-table th{text-align:left;color:var(--text-muted);font-weight:600;border-bottom:1px solid var(--border);padding:4px 5px}
.gprogress .g-table td{padding:4px 5px;border-bottom:1px solid var(--border)}
.gprogress .g-src{display:block;font-size:.64rem;color:var(--text-muted)}
.gprogress .g-empty{font-size:.85rem;color:var(--text-muted);text-align:center;padding:18px}

/* reading_log (a simple append-only log of what was read) */
.rlog{display:flex;flex-direction:column;gap:10px;width:100%}
.rlog .rl-form{display:flex;flex-direction:column;gap:6px}
.rlog .rl-input{width:100%;padding:8px 10px;border:1px solid var(--border);border-radius:10px;
  background:var(--surface-alt);color:var(--text);font:inherit;font-size:.9rem}
.rlog .rl-notefield{resize:vertical}
.rlog .rl-btn{align-self:flex-start;padding:8px 16px;border:1px solid var(--border);border-radius:10px;
  background:var(--surface);color:var(--text);font:inherit;font-weight:600;cursor:pointer}
.rlog .rl-btn:hover:not(:disabled){background:var(--surface-alt)}
.rlog .rl-btn:disabled{opacity:.45;cursor:not-allowed}
.rlog .rl-primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.rlog .rl-primary:hover:not(:disabled){filter:brightness(.93);background:var(--accent)}
.rlog .rl-list{display:flex;flex-direction:column;gap:6px;max-height:340px;overflow-y:auto}
.rlog .rl-row{border:1px solid var(--border);border-radius:10px;background:var(--surface);padding:8px 10px}
.rlog .rl-title{font-weight:700;font-size:.92rem;color:var(--text-strong)}
.rlog .rl-meta{font-size:.76rem;color:var(--text-muted);margin-top:1px}
.rlog .rl-note{font-size:.8rem;color:var(--text-soft);margin-top:4px;white-space:pre-wrap}
.rlog .rl-empty{font-size:.85rem;color:var(--text-muted);text-align:center;padding:18px}

/* wordforge (vocabulary game) */
.wordforge{display:flex;flex-direction:column;gap:10px;width:100%}
.wordforge .wf-top{display:flex;align-items:center;gap:10px;font-size:.8rem;color:var(--text-muted)}
.wordforge .wf-score{font-weight:800;color:var(--text-strong);font-variant-numeric:tabular-nums}
.wordforge .wf-streak{color:var(--link);font-weight:700}
.wordforge .wf-progress{margin-left:auto;font-variant-numeric:tabular-nums}
.wordforge .wf-kind{margin:0;font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}
.wordforge .wf-prompt{margin:4px 0 12px;font-size:1.15rem;font-weight:600;line-height:1.4}
.wordforge .wf-opts{display:flex;flex-direction:column;gap:7px}
.wordforge .wf-opt{text-align:left;padding:11px 13px;border:1px solid var(--border);border-radius:11px;
  background:var(--surface);color:var(--text);font:inherit;font-size:.95rem;cursor:pointer;line-height:1.4}
.wordforge .wf-opt:hover:not(:disabled){background:var(--surface-alt);border-color:var(--accent)}
.wordforge .wf-opt:disabled{cursor:default}
/* THE SWITCH CURSOR. Same treatment as `.tv-opt[data-on]` on purpose — one switch, two quizzes,
   one thing to learn. Without this the option a press would choose is invisible. */
.wordforge .wf-opt[data-on="1"]{border-color:var(--link,#105666);
  box-shadow:0 0 0 3px rgba(16,86,102,.18)}
.wordforge .wf-opt[data-on="1"]::before{content:"\25B8\00a0"}
.wordforge .wf-opt.is-right{border-color:var(--accent);background:var(--ok-surface);box-shadow:inset 3px 0 0 var(--accent)}
.wordforge .wf-opt.is-wrong{border-color:var(--accent-warm);background:var(--bad-surface);box-shadow:inset 3px 0 0 var(--accent-warm-deep)}
/* 'multi' guess mode's transient miss note — same shape as trivia.js's own `.tv-said`, no
   score in it, just "try again", since the question is still open. */
.wordforge .wf-said{margin:14px 0 8px;font-size:1.05rem}
.wordforge .wf-idk{display:block;width:100%;margin-top:10px;background:transparent;border-style:dashed;color:var(--text-muted);font-weight:600}
.wordforge .wf-idk:hover{background:var(--surface-alt);color:var(--text)}
.wordforge .wf-fb{margin-top:12px;padding:11px 13px;border-radius:11px;font-size:.9rem;line-height:1.5}
.wordforge .wf-fb.is-right{background:var(--ok-surface);border:1px solid var(--accent)}
.wordforge .wf-fb.is-wrong{background:var(--surface-alt);border:1px solid var(--border)}
.wordforge .wf-bonus,.wordforge .wf-try{display:block;margin-top:4px;font-size:.78rem;color:var(--link);font-weight:600}
/* WHAT EACH OPTION MEANS, on its own button once the question is over (row 2.59). Its own line under
   the option, a step smaller, in the button's own text colour: the button's wash is green, pink or
   plain, and the meaning has to read on each of them exactly as the option's own words do. In em, so
   it follows the option's size. (`.wf-picked`, the old "You picked:" line, went with it.) */
.wordforge .wf-mean{display:block;margin-top:3px;font-size:.88em;font-weight:400;line-height:1.35}
/* "I think this question is wrong" (contests.js) — a quieter second stop after Next / Got it. The
   lit stop gets the same border + arrow treatment as a lit option, never color alone. */
.wordforge .wf-after{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.wordforge .wf-contest{background:transparent;color:var(--text-soft)}
.wordforge .wf-after [data-on="1"]{outline:3px solid var(--scan-ring, var(--focus, var(--link,#105666)));outline-offset:2px}
.wordforge .wf-after [data-on="1"]::before{content:"\25B8\00a0"}
.wordforge .wf-contested{margin:8px 0 0;font-size:.9rem;color:var(--text-soft)}
.wordforge .wf-btn{margin-top:10px;padding:10px 18px;border:1px solid var(--border);border-radius:10px;
  background:var(--surface);color:var(--text);font:inherit;font-weight:600;cursor:pointer}
.wordforge .wf-primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.wordforge .wf-primary:hover{filter:brightness(.93)}
.wordforge .wf-done{text-align:center;padding:22px 10px}
.wordforge .wf-done-n{font-size:2.8rem;font-weight:800;color:var(--text-strong);line-height:1}
.wordforge .wf-done p{margin:2px 0 6px;color:var(--text-muted);font-size:.9rem}

/* lessons (watch to unlock) */
.lessons{display:flex;flex-direction:column;gap:10px;width:100%}
.lessons .l-top{font-size:.78rem;color:var(--text-muted);font-weight:600}
/* "Not finished yet", said once at the top. Cream on the card rather than red, because
   unfinished is not an error and a warning colour would say it was. */
.lessons .l-unfinished{margin:0 0 4px;padding:8px 11px;border-radius:10px;
  background:var(--surface-alt);border:1px solid var(--border);
  font-size:.84rem;color:var(--text-soft);max-width:62ch}
.lessons .l-list{display:flex;flex-direction:column;gap:9px}
.lessons .l-card{border:1px solid var(--border);border-radius:12px;background:var(--surface);padding:12px 14px}
.lessons .l-card.is-done{border-color:var(--accent);background:var(--surface-alt)}
.lessons .l-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
.lessons .l-head h3{margin:0;font-size:1rem}
.lessons .l-badge{margin-left:7px;font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;
  color:var(--on-accent);background:var(--accent);border-radius:99px;padding:2px 7px;vertical-align:middle}
.lessons .l-blurb{margin:3px 0 0;font-size:.82rem;color:var(--text-muted)}
.lessons .l-btn{padding:8px 13px;border:1px solid var(--border);border-radius:10px;background:var(--surface);
  color:var(--text);font:inherit;font-size:.85rem;font-weight:600;cursor:pointer;white-space:nowrap}
.lessons .l-primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.lessons .l-btn:disabled{opacity:.45;cursor:not-allowed}
.lessons .l-body{margin-top:11px}
.lessons .l-video{width:100%;aspect-ratio:16/9;border:0;border-radius:10px;background:var(--letterbox);display:block}
.lessons .l-none{font-size:.85rem;color:var(--text-muted);text-align:center;padding:20px;background:var(--surface-alt);
  border:1px dashed var(--border);border-radius:10px;margin:0}
.lessons .l-actions{display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap}
.lessons .l-wait{font-size:.78rem;color:var(--text-muted)}
/* transcript lessons (row 2.24) and the review list (§0h) */
.lessons .l-tq{margin-top:12px;padding:12px;border:1px solid var(--border);border-radius:10px;background:var(--surface-alt)}
.lessons .l-tq-title{margin:0 0 6px;font-size:.92rem}
.lessons .l-tq-help{margin:4px 0;font-size:.82rem;color:var(--text-muted);max-width:66ch}
.lessons .l-tq-text{width:100%;box-sizing:border-box;min-height:7em;padding:8px;border:1px solid var(--border);
  border-radius:8px;background:var(--surface);color:var(--text);font:inherit;font-size:.85rem;resize:vertical}
.lessons .l-tq-mins{display:flex;flex-wrap:wrap;gap:6px}
.lessons .l-tq-need,.lessons .l-tq-progress{margin:6px 0;font-size:.85rem;font-weight:600}
.lessons .l-tq-working{margin:6px 0;font-weight:600}
.lessons .l-tq-error{margin:6px 0;padding:8px 10px;border-radius:8px;background:var(--bad-surface);font-size:.85rem}
.lessons .l-tq-q{margin:10px 0 6px;font-size:1rem;font-weight:600}
.lessons .l-tq-opts{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
.lessons .l-tq-opt{width:100%;text-align:left;white-space:normal}
.lessons .l-tq-opt[data-right="1"]{background:var(--ok-surface);border-color:var(--accent)}
.lessons .l-tq-opt[data-wrong="1"]{background:var(--bad-surface)}
/* The answered options are disabled (so they cannot be pressed again), and `.l-btn:disabled` dims to
   45%. That dimmed the RIGHT ANSWER the moment it was shown. The two marked ones stay at full
   strength; the options nobody picked can stay dimmed. */
.lessons .l-tq-opt[data-right="1"]:disabled,.lessons .l-tq-opt[data-wrong="1"]:disabled{opacity:1;cursor:default}
/* ✓ / ✗ beside the colour (answer_mark.js). Drawn in the option's own text colour, which the theme
   suite holds to 4.5:1 on both washes, so the shape never depends on a colour of its own. A
   placeholder look: NOTES_FOR_DESIGN item 30. The word is for a screen reader only. Shared by
   lessons, trivia and Word Forge. */
.ans-mark{display:inline-block;margin-inline-start:.45em;font-weight:800;line-height:1}
.ans-mark-word{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}
.lessons .l-tq-said{margin:10px 0 4px;font-weight:600}
.lessons .l-tq-src,.lessons .l-rq-src{margin:4px 0;padding:6px 10px;border-left:3px solid var(--accent);
  background:var(--surface);font-size:.84rem;color:var(--text-soft)}
.lessons .l-tq-warn{margin:4px 0;font-size:.8rem;color:var(--text-soft);font-style:italic}
.lessons .l-ai{margin-top:10px;font-size:.84rem}
.lessons .l-ai summary{cursor:pointer;color:var(--text-muted)}
.lessons .l-ai-url{flex:1 1 16em;min-width:0;padding:7px 9px;border:1px solid var(--border);border-radius:8px;
  background:var(--surface);color:var(--text);font:inherit;font-size:.84rem}
.lessons .l-review{margin-top:6px;padding:12px 14px;border:1px solid var(--border);border-radius:12px;background:var(--surface)}
.lessons .l-rv-title{margin:0 0 4px;font-size:.95rem}
.lessons .l-rv-topic{margin:10px 0 4px;font-size:.8rem;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}
.lessons .l-rq{padding:8px 0;border-top:1px solid var(--border)}
.lessons .l-rq.is-flagged{background:var(--bad-surface)}
.lessons .l-rq-q{margin:0;font-weight:600;font-size:.9rem}
.lessons .l-rq-a{margin:2px 0;font-size:.82rem;color:var(--text-soft)}
.lessons .l-rq-flag{margin:4px 0;font-size:.8rem}
.lessons .l-rq-fact{margin:4px 0;font-size:.8rem;color:var(--text-soft)}
.lessons .l-rq-fact.is-contradicts{color:var(--text);font-weight:600}
.lessons .l-rq-wiki{color:var(--link)}
.lessons .l-fc:not([hidden]){display:flex;flex-wrap:wrap;align-items:center;gap:8px}
/* The fact-check checkbox (Mike, 2026-09-28: "obvious ... rather than buried in settings"): a real
   labelled input beside "Make questions"; the note in its label says what is sent. */
.lessons .l-fc-opt{display:inline-flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:.88rem;cursor:pointer;max-width:48ch}
.lessons .l-fc-opt input{width:1.15rem;height:1.15rem;margin:0}
.lessons .l-fc-note{flex-basis:100%;font-size:.78rem;color:var(--text-muted)}
.lessons .l-fc-off .l-fc-opt{margin-left:4px}
.lessons .l-tq-contested,.lessons .l-rq-contested{font-weight:600}
.lessons .l-rv-in summary{cursor:pointer;margin-top:8px;font-size:.84rem;color:var(--text-muted)}
.wordforge .wf-held{font-size:.75rem;color:var(--link);text-align:center;min-height:1em}

/* algebra (solve for x, with a calculator) */
.algebra{display:flex;flex-direction:column;gap:10px;width:100%}
.algebra .al-top{display:flex;align-items:center;gap:10px;font-size:.8rem;color:var(--text-muted)}
.algebra .al-earned{font-weight:800;color:var(--text-strong);font-variant-numeric:tabular-nums}
.algebra .al-streak{margin-left:auto;color:var(--link);font-weight:700}
.algebra .al-kind{margin:0;font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted)}
.algebra .al-prompt{margin:4px 0 12px;font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums}
.algebra .al-calc{max-width:280px}
.algebra .al-display{padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--surface-alt);
  font-size:1.5rem;font-weight:700;text-align:right;font-variant-numeric:tabular-nums;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;margin-bottom:7px}
.algebra .al-pad{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}
.algebra .al-key{padding:11px 0;border:1px solid var(--border);border-radius:9px;background:var(--surface);
  color:var(--text);font:inherit;font-size:1rem;font-weight:600;cursor:pointer}
.algebra .al-key:hover{background:var(--surface-alt)}
.algebra .al-key.is-op{background:var(--surface-alt);color:var(--link);font-weight:800}
.algebra .al-key.is-fn{color:var(--text-muted)}
.algebra .al-key.is-eq{grid-column:span 2;background:var(--link);border-color:var(--link);color:var(--on-link)}
.algebra .al-btn{padding:11px 16px;border:1px solid var(--border);border-radius:10px;background:var(--surface);
  color:var(--text);font:inherit;font-weight:600;cursor:pointer}
.algebra .al-primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.algebra .al-submit{max-width:280px;margin-top:8px;font-size:1rem}
.algebra .al-submit:disabled{opacity:.45;cursor:not-allowed}
.algebra .al-idk{max-width:280px;margin-top:7px;background:transparent;border-style:dashed;color:var(--text-muted);
  font-size:.85rem}
.algebra .al-fb{margin-top:12px;padding:11px 13px;border-radius:11px;font-size:.9rem;line-height:1.5;max-width:420px}
.algebra .al-fb.is-right{background:var(--ok-surface);border:1px solid var(--accent)}
.algebra .al-fb.is-wrong{background:var(--surface-alt);border:1px solid var(--border)}
.algebra .al-how{display:block;margin-top:5px}
.algebra .al-bonus,.algebra .al-try{display:block;margin-top:4px;font-size:.78rem;color:var(--link);font-weight:600}
.algebra .al-held{font-size:.75rem;color:var(--link);min-height:1em}
/* Fit the keypad to the box instead of overflowing it. Everything vertical shrinks
   together and in proportion — a keypad whose keys shrink while the prompt above it
   stays at 1.5rem just moves the overflow around. The upper bounds are the original
   values, so a roomy panel looks exactly as it did before. */
@container cell (min-height: 1px){
  .algebra{gap:clamp(1px, 1.4cqh, 10px)}
  .algebra .al-top{font-size:clamp(.62rem,2.4cqh,.8rem)}
  .algebra .al-kind{font-size:clamp(.55rem,2cqh,.7rem)}
  .algebra .al-prompt{margin:clamp(0px,.6cqh,4px) 0 clamp(2px,1.8cqh,12px);
    font-size:clamp(.85rem, 5cqh, 1.5rem);line-height:1.15}
  .algebra .al-display{padding:clamp(2px,1.4cqh,10px) 12px;font-size:clamp(.85rem,4.6cqh,1.5rem);
    margin-bottom:clamp(2px,1cqh,7px);line-height:1.2}
  .algebra .al-pad{gap:clamp(2px,.8cqh,5px)}
  /* line-height is part of a key's height, so leaving it at the inherited 1.5 puts a
     floor under the keypad that no amount of padding reduction can get past. */
  .algebra .al-key{padding:clamp(1px,1.9cqh,11px) 0;font-size:clamp(.7rem,3.2cqh,1rem);
    line-height:clamp(1.05,2.6cqh,1.5)}
  .algebra .al-submit,.algebra .al-idk{padding:clamp(2px,1.7cqh,11px) 16px;
    margin-top:clamp(2px,1.2cqh,8px);line-height:1.2}
  .algebra .al-submit{font-size:clamp(.7rem,3.4cqh,1rem)}
  .algebra .al-idk{font-size:clamp(.66rem,3cqh,.9rem)}
  .algebra .al-held{font-size:clamp(.6rem,2.4cqh,.75rem)}
  /* Feedback is the one thing allowed to scroll — it is prose, it appears after an
     answer, and clipping an explanation is worse than a short scroll. */
  .algebra .al-fb{margin-top:clamp(4px,1.6cqh,12px);font-size:clamp(.75rem,3cqh,.9rem);
    max-height:34cqh;overflow-y:auto}
}
/* Below this there is genuinely not room for a five-row keypad, a prompt, a display and
   two buttons at a size anyone can hit. Rather than shrink the KEYS past legibility —
   which would trade a scrollbar for a panel nobody can use — the decoration goes: the
   points-and-streak line and the reserved hold hint. Nothing functional is hidden; the
   problem, the keypad, Submit and "I don't know" all stay. */
@container cell (max-height: 300px){
  .algebra .al-top,.algebra .al-held{display:none}
}

/* WIDE PANEL: put the question BESIDE the keypad instead of above it.
   Stacked vertically, algebra is a column of eight things and the question — the only
   part anyone is actually thinking about — ends up small and scrolled off the top, which
   is exactly what it did in the kiosk. Two columns halves the height AND buys the prompt
   room to be the biggest thing on the panel, which is what it should have been.

   The grid goes on `.al-body`, not `.algebra`: `.al-body` is the element that actually
   holds the question and the keypad as siblings. Putting it on `.algebra` silently did
   nothing, because grid only positions DIRECT children and the keypad is a generation
   further down.

   Explicit grid areas rather than auto-placement: in source order the keypad follows the
   prompt, so auto-placement would start column two a row late and leave a hole. */
@container cell (min-width: 520px){
  .algebra .al-body{display:grid;grid-template-columns:1fr minmax(200px,290px);
    column-gap:20px;row-gap:clamp(2px,1cqh,8px);align-content:start}
  .algebra .al-kind{grid-area:1 / 1 / 2 / 2;align-self:end}
  .algebra .al-prompt{grid-area:2 / 1 / 3 / 2;align-self:start}
  .algebra .al-fb{grid-area:3 / 1 / 4 / 2;max-width:none;margin-top:0}
  .algebra .al-calc{grid-area:1 / 2 / 3 / 3;max-width:none}
  .algebra .al-submit{grid-area:3 / 2 / 4 / 3;max-width:none;margin-top:0}
  .algebra .al-idk{grid-area:4 / 2 / 5 / 3;max-width:none}
  /* The question is the point of the panel. Bounded by width as well as height, so a long
     equation cannot run past the column it lives in. */
  .algebra .al-prompt{font-size:clamp(1.4rem, min(11cqh, 5.5cqw), 3.2rem);line-height:1.2;
    margin:0 0 6px}
}


/* calculator (the standalone module: modules/calculator.js). Same tokens and the same key look as the
   keypad inside the math game, but it FILLS its box (the game's is a fixed-width widget), so the keys
   grow with the panel instead of sitting in a corner of it. No literal colours: everything is a token.
   The lit key (a switch walking the pad) is an OUTLINE plus an arrow, never a colour change alone. */
.calc{display:flex;flex-direction:column;gap:8px;width:100%;height:100%;min-height:0;box-sizing:border-box;padding:10px}
.calc .calc-display{flex:none;padding:8px 12px;border:1px solid var(--border);border-radius:10px;background:var(--surface-alt);
  color:var(--text-strong);font-size:1.7rem;font-weight:700;text-align:right;font-variant-numeric:tabular-nums;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2}
.calc .calc-pad{flex:1;min-height:0;display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:minmax(34px,1fr);gap:6px}
.calc .calc-key{position:relative;padding:0;border:1px solid var(--border);border-radius:10px;background:var(--surface);
  color:var(--text);font:inherit;font-size:1.15rem;font-weight:600;line-height:1.1;cursor:pointer}
.calc .calc-key:hover{background:var(--surface-alt)}
.calc .calc-key.is-op{background:var(--surface-alt);color:var(--link);font-weight:800}
.calc .calc-key.is-fn{color:var(--text-muted)}
.calc .calc-key.is-eq{grid-column:span 2;background:var(--link);border-color:var(--link);color:var(--on-link)}
.calc .calc-key[data-on="1"]{outline:3px solid var(--scan-ring, var(--focus, var(--link)));outline-offset:2px;z-index:1}
.calc .calc-key.is-eq[data-on="1"]{outline-color:var(--text-strong)}
.calc .calc-key[data-on="1"]::before{content:"\25B8";position:absolute;left:5px;top:50%;transform:translateY(-50%);
  font-size:.8em;line-height:1}
/* Fit the pad to the box rather than overflow it, the same way the game's keypad does: the type
   scales with the panel's height, and the upper bounds are the sizes above. */
@container cell (min-height: 1px){
  .calc{gap:clamp(2px,1.4cqh,8px);padding:clamp(3px,2cqh,10px)}
  .calc .calc-display{padding:clamp(2px,1.2cqh,8px) 12px;font-size:clamp(.9rem,7cqh,2.4rem)}
  .calc .calc-pad{gap:clamp(2px,1.2cqh,6px);grid-auto-rows:minmax(0,1fr)}
  .calc .calc-key{font-size:clamp(.72rem,4.6cqh,1.6rem)}
}

/* The pond fills whatever box it is given and clips its own canvas. `position:absolute`
 * inside a relative mount is what lets it size to a slot without measuring anything. */
/* RELATIVE, not absolute. `inset:0` would escape to the nearest POSITIONED ancestor, and
 * a module cannot know whether the host gave it one - the harness does, a grid slot may
 * not. Filling the mount and positioning the canvas inside works either way.
 * The min-height is the "mounted somewhere with no height yet" case: a zero-height canvas
 * renders nothing and looks exactly like a broken module. */
.pond { position: relative; width: 100%; height: 100%; min-height: 120px;
        overflow: hidden; border-radius: inherit; }
.pond canvas { position: absolute; inset: 0; display: block; }

/* ---- press overlay: what was just pressed -------------------------------
   Deliberately quiet. This sits on a screen somebody looks at all day, so it
   has to be readable at a glance and impossible to mistake for content.
   SHAPE PLUS COLOR, NEVER COLOR ALONE: a refused press is struck through AND
   tinted, because two rows differing only in hue are one row to somebody with
   reduced vision. */
.po-wrap {
  position: absolute; z-index: var(--z-floating); pointer-events: none;
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 14px; border-radius: 12px;
  background: rgba(10, 51, 35, .82); color: #F7F4D5;
  border: 1px solid rgba(247, 244, 213, .28);
  font: 600 15px/1.25 system-ui, sans-serif;
  max-width: 60%;
}
.po-wrap[data-corner="bl"] { left: 16px; bottom: 16px; }
.po-wrap[data-corner="br"] { right: 16px; bottom: 16px; }
.po-wrap[data-corner="tl"] { left: 16px; top: 16px; }
.po-wrap[data-corner="tr"] { right: 16px; top: 16px; }
.po-wrap .po-who,
.po-wrap .po-why { font-weight: 500; font-size: 12px; opacity: .85; }
.po-remote { border-color: rgba(211, 150, 140, .8); }
.po-refused .po-what { text-decoration: line-through; }
.po-refused { border-color: rgba(211, 150, 140, .8); background: rgba(70, 26, 26, .82); }
@media (prefers-reduced-motion: no-preference) {
  .po-wrap { transition: opacity .12s ease; }
}

/* ---- THE DEMO STRIP: the only thing on this page aimed at a STRANGER ----
   *
   * Signed out, this page is the front door. A visitor lands on a working screen showing
   * SAMPLE photos, and until now nothing said so, and nothing offered the one action worth
   * taking. They watched somebody else's stock images rotate and left.
   *
   * The whole conversion is: *these could be your mother's photos, in ten seconds, without
   * installing anything.* The browser folder picker already existed and was never surfaced.
   *
   * It is a STRIP, not a modal: a modal over a demo is a thing to close, and closing it is
   * the last interaction most people would have. This sits above the screen, says what the
   * screen is, offers one action, and gets out of the way when dismissed.
   *
   * IT NEVER APPEARS ON A REAL BEDSIDE SCREEN. It is inside the signed-out branch only —
   * a paired kiosk has a device key and never reaches this code. */
  #demo{position:fixed;top:0;left:0;right:0;z-index:var(--z-transport);
    display:flex;align-items:center;gap:14px;flex-wrap:wrap;
    padding:11px 16px;background:rgba(10,51,35,.94);color:#F7F4D5;
    font-size:.95rem;box-shadow:0 2px 14px rgba(0,0,0,.35)}
  #demo b{color:#fff}
  #demo .d-text{flex:1 1 22ch;min-width:0}
  #demo .d-sub{display:block;opacity:.8;font-size:.85rem;margin-top:2px}
  #demo button,#demo a{font:inherit;font-weight:700;border:0;border-radius:9px;
    padding:9px 15px;cursor:pointer;text-decoration:none;white-space:nowrap}
  #demo .d-go{background:#839958;color:#fff}
  #demo .d-in{background:transparent;color:#F7F4D5;border:1px solid rgba(247,244,213,.45)}
  #demo .d-x{background:transparent;color:#F7F4D5;opacity:.7;padding:9px 11px}
  #demo .d-note{flex:1 1 100%;opacity:.85;font-size:.85rem}
  @media (max-width:560px){ #demo{font-size:.9rem;padding:9px 11px;gap:9px} }

/* ---- THE CURSOR: where the aim is, drawn by us rather than by the operating system ----
 *
 * See cursor.js for why this is not the system pointer. The short version: an OS arrow is
 * 12px of thin gray, cannot be themed, and does not exist at all for a hand in front of a
 * camera. Position is set inline from a normalized aim; everything about how it LOOKS is
 * here, driven by data attributes and two custom properties the module writes.
 *
 * z-index sits ABOVE the press overlay (40): if both are on screen, the thing being pointed
 * with should not be covered by a caption about a button.
 *
 * pointer-events:none is load-bearing rather than tidy — without it the cursor would sit
 * under the real mouse and swallow every click on the screen it is meant to be pointing at.
 */
.cur {
  position: fixed; z-index: var(--z-cursor); pointer-events: none;
  left: 0; top: 0;
  --cur-color: #F7C948;
  --cur-ring: 4px;
}
/* A DARK OUTLINE UNDER EVERY STYLE, so the cursor reads against a white hospital sheet and a
   night sky without anybody having to change the color. */
.cur[data-style="ring"] {
  border-radius: 50%;
  border: var(--cur-ring) solid var(--cur-color);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), inset 0 0 0 1px rgba(0,0,0,.55);
}
.cur[data-style="dot"] {
  border-radius: 50%;
  background: var(--cur-color);
  box-shadow: 0 0 0 2px rgba(0,0,0,.55);
}
/* Two bars rather than a border, so the middle stays empty and whatever is being pointed at
   is still visible through it. */
.cur[data-style="crosshair"] { background: none; }
.cur[data-style="crosshair"]::before,
.cur[data-style="crosshair"]::after {
  content: ""; position: absolute; background: var(--cur-color);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55);
}
.cur[data-style="crosshair"]::before {
  left: 50%; top: 0; width: var(--cur-ring); height: 100%; margin-left: calc(var(--cur-ring) / -2);
}
.cur[data-style="crosshair"]::after {
  top: 50%; left: 0; height: var(--cur-ring); width: 100%; margin-top: calc(var(--cur-ring) / -2);
}
/* HALO — the bedside build's cursor, and the default. A ring that says roughly where, plus a
   solid centre dot that says exactly where. The dot is deliberately small (a fifth of the
   cursor) so it marks a point without covering the thing being pointed at: on this board that
   thing is a symbol somebody is reading. */
.cur[data-style="halo"] {
  border-radius: 50%;
  border: var(--cur-ring) solid var(--cur-color);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55), inset 0 0 0 1px rgba(0,0,0,.55);
}
.cur .cur-dot { display: none; }
.cur[data-style="halo"] .cur-dot {
  display: block; position: absolute; left: 50%; top: 50%;
  width: 20%; height: 20%; margin: -10% 0 0 -10%;
  border-radius: 50%; background: var(--cur-color);
  box-shadow: 0 0 0 1px rgba(0,0,0,.55);
}
/* THE DWELL FILL — how far through "holding still is the click". A conic sweep from twelve
   o'clock, inside the ring, so the growing thing IS the cursor rather than a second widget
   somewhere else on the screen. Hidden entirely at zero, so a screen with no dwell running
   looks exactly like one that never had it.

   `--cur-turn` is set from script every tick; there is no CSS transition on it on purpose —
   the number already arrives ten times a second, and easing it would make the ring lag the
   clock it is reporting, which is the one thing this must not do. */
.cur .cur-fill {
  position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(var(--cur-color) var(--cur-turn, 0turn), transparent 0);
  opacity: .38;
}
.cur .cur-fill[hidden] { display: none; }
/* NO TRANSITION ON POSITION, EVER, and this is not a stylistic choice. `comet.js` puts the
   comet's head exactly on the aim because the precision is what answers "did I do that?" for
   somebody re-learning that she can. A CSS transition is smoothing by another name, applied
   in the one place the person is most likely to notice it. */
@media (prefers-reduced-motion: no-preference) {
  .cur { transition: width .12s ease, height .12s ease; }
}
/* The system pointer, taken away only when the setting asks for it — kiosk yes, desktop no. */
body[data-hide-system-cursor="1"], body[data-hide-system-cursor="1"] * { cursor: none !important; }

/* ---- THE MARKER CALIBRATION PANEL (Devices tab) ----
 * Two columns: the live picture, and the readouts that tell somebody what to do about it.
 * Stacks on a narrow screen, because a caregiver may well be setting this up on a phone in
 * one hand while the other holds the camera.
 */
.mk h3 { margin: 18px 0 4px; font-size: 1rem; }
.mk .mk-lede { margin: 0 0 10px; color: var(--text-soft, #3c5346); max-width: 62ch; }
.mk-live { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-start; }
/* Pixelated: this is a 160x120 detection frame blown up, and smoothing it would show a
   cleaner picture than the thing actually being scanned. The caregiver should see what the
   DETECTOR sees, not a prettier version of it. */
.mk-view {
  width: 320px; height: 240px; image-rendering: pixelated;
  border-radius: 10px; background: var(--letterbox); cursor: crosshair;
  border: 1px solid var(--border, #e4e0c2);
}
/* The camera-off placeholder, in the same footprint as the preview it replaces, so pressing
   the button does not make the page jump. */
.mk-off {
  width: 320px; min-height: 240px; border-radius: 10px;
  border: 1px dashed var(--border, #e4e0c2); background: var(--surface-alt, #FBF9E9);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 16px; text-align: center;
}
.mk-off p { margin: 0; font-size: .9rem; color: var(--text-soft, #3c5346); max-width: 34ch; }
.mk-start {
  min-height: 44px; padding: 10px 18px; border-radius: 10px;
  border: 1px solid var(--link, #105666); background: var(--link, #105666);
  color: #fff; font: 650 .95rem var(--font, system-ui, sans-serif); cursor: pointer;
}
.mk-start:hover { filter: brightness(1.1); }
.mk-side { flex: 1 1 240px; min-width: 220px; }
.mk-advice { margin: 0 0 8px; font-weight: 650; }
/* Shape as well as color, per module-input-spec: never carry meaning by color alone. */
.mk-advice[data-state="seen"]::before { content: "\2713\00a0"; color: #2c6e49; }
.mk-advice[data-state="faint"]::before { content: "\26A0\00a0"; }
.mk-advice[data-state="lost"]::before,
.mk-advice[data-state="unsampled"]::before { content: "\2192\00a0"; }
.mk-advice[data-state="seen"] { color: #2c6e49; }
.mk-advice[data-state="lost"] { color: #a4243b; }
.mk-swatch-row { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; }
.mk-swatch {
  width: 26px; height: 26px; border-radius: 6px; display: inline-block;
  border: 1px solid rgba(0,0,0,.3); background-image:
    linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%),
    linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%);
  background-size: 10px 10px; background-position: 0 0, 5px 5px;   /* nothing sampled = checks */
}
.mk-chk { display: flex; align-items: center; gap: 8px; margin: 12px 0 4px; font-weight: 600; }
.mk-note { margin: 0 0 10px; font-size: .87rem; color: var(--text-muted, #5d7064); max-width: 62ch; }
.mk-rows { display: flex; flex-direction: column; gap: 8px; margin: 12px 0; max-width: 460px; }
.mk-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.mk-row select { flex: 0 1 220px; }
@media (max-width: 560px) {
  .mk-view { width: 100%; height: auto; aspect-ratio: 4 / 3; }
  .mk-off { width: 100%; }
}

/* ---- WHICH MICROPHONE (Devices tab) ----
 * An ordered list, best first, with arrows rather than drag-and-drop — see device_panel.js.
 * Dragging is unreachable for a large part of the people this exists for, and a caregiver may
 * be the person with the tremor.
 */
.dv h3 { margin: 18px 0 4px; font-size: 1rem; }
.dv .dv-lede { margin: 0 0 10px; color: var(--text-soft, #3c5346); max-width: 62ch; }
.dv-permission {
  margin: 0 0 12px; padding: 10px 12px; border-radius: 10px;
  background: var(--surface-alt, #FBF9E9); border: 1px solid var(--border, #e4e0c2);
  max-width: 62ch;
}
.dv-permission p { margin: 0 0 8px; }
.dv-list { list-style: none; margin: 0 0 10px; padding: 0; max-width: 560px; }
.dv-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border: 1px solid var(--border, #e4e0c2); border-radius: 10px;
  margin-bottom: 6px; background: var(--surface, #fff);
}
.dv-name { flex: 1 1 auto; font-weight: 600; }
.dv-state { font-size: .82rem; color: var(--text-muted, #5d7064); white-space: nowrap; }
/* NOT CONNECTED IS NOT AN ERROR. A phone that has left the room is the normal case this whole
   list exists for, so it is dimmed rather than marked wrong — and the word says "now", because
   it is expected back. */
.dv-missing { opacity: .62; border-style: dashed; }
/* Big enough to hit with an unsteady hand or a touchscreen: 44px is the usual floor and these
   are the controls somebody uses while holding a phone in the other hand. */
.dv-move button { min-width: 40px; min-height: 40px; font-size: 1rem; }
.dv-add { display: inline-block; margin: 0 6px 6px 0; }
.dv-note { margin: 10px 0 6px; font-size: .87rem; color: var(--text-muted, #5d7064); }
.dv-now { margin: 8px 0 0; font-weight: 650; max-width: 62ch; }
/* Shape as well as color — module-input-spec, "do not use color alone". */
.dv-now[data-state="preferred"]::before { content: "\2713\00a0"; color: #2c6e49; }
.dv-now[data-state="fallback"]::before,
.dv-now[data-state="unlisted"]::before { content: "\26A0\00a0"; }
.dv-now[data-state="none"]::before { content: "\2717\00a0"; color: #a4243b; }
.dv-now[data-state="none"] { color: #a4243b; }
.dv-empty { color: var(--text-muted, #5d7064); }

/* ---- RECORD A SESSION (Devices tab, under the microphone chooser) ---- */
.rp h3 { margin: 20px 0 4px; font-size: 1rem; }
.rp .rp-lede { margin: 0 0 10px; color: var(--text-soft, #3c5346); max-width: 62ch; }
.rp-folder, .rp-roles { margin: 6px 0; max-width: 62ch; }
.rp-roles { font-weight: 600; }
.rp-go { display: flex; align-items: center; gap: 14px; margin: 14px 0 8px; }
/* Big, and unmistakable when it is live. This is pressed by somebody holding a phone in the
   other hand, and "am I recording?" should never need a second look. */
.rp-rec { min-width: 130px; min-height: 52px; font-size: 1.05rem; font-weight: 700; }
.rp-rec[data-on="1"] { background: #a4243b; color: #fff; }
.rp-rec[data-on="1"]::before { content: "\25CF\00a0"; }
.rp-rec:disabled { opacity: .5; }
.rp-elapsed { font: 700 1.2rem ui-monospace, Menlo, Consolas, monospace; }
.rp-mark { display: flex; gap: 8px; margin: 8px 0; max-width: 560px; }
.rp-mark input { flex: 1 1 auto; min-height: 40px; }
.rp-mark button { min-height: 40px; }
.rp-marks, .rp-said { margin: 6px 0; max-width: 62ch; }
.rp-said { font-weight: 650; }
.rp-note { margin: 12px 0 6px; font-size: .87rem; color: var(--text-muted, #5d7064); }
.rp-row { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 7px 10px; border: 1px solid var(--border, #e4e0c2); border-radius: 8px;
  margin-bottom: 5px; background: var(--surface, #fff); max-width: 560px; }

/* ---- TRIVIA ----
 * Four choices, big, with a walking highlight — because the whole game has to be answerable
 * with one button. See modules/trivia.js.
 */
.tv { padding: 14px 16px; }
.tv-count { margin: 0 0 4px; font-size: .82rem; color: var(--text-muted, #5d7064); }
/* THE SCORE, when somebody has turned it on. Same weight as the question count beside it and
   deliberately not louder: it is a fact about the round, not a verdict about the person, and
   the moment it is the brightest thing on the panel it starts reading as one. */
.tv-score { font-weight: 700; color: var(--accent, #839958); }
.tv-q { margin: 0 0 14px; font-size: 1.35rem; line-height: 1.25; }
.tv-opts { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
/* *** `var(--surface)`, NEVER A LITERAL WHITE, ON ANYTHING CARRYING `var(--text)`. ***
 *
 * B6 fixed this pattern on the kiosk's panels and FOUR RULES WERE MISSED: `.dv-row`,
 * `.rp-row`, `.bk-text` and this one. Each paired a hardcoded `#fff` with `color: var(--text)`,
 * and `--text` is a NEAR-WHITE in dusk.
 *
 * Measured, per theme, ink on hardcoded white vs ink on `var(--surface)`:
 *   default 13.90 / 13.90 · contrast 21.00 / 21.00 · warm 14.54 / 14.00 · forge 14.25 / 14.25
 *   *** dusk 1.17 / 13.46 ***
 *
 * 1.17 against a 4.5 floor is text that is not there. And dusk is *"for a bedside screen at
 * night"* — the theme most likely to be on the screen this project exists for, while she is
 * looking at it. These are the trivia ANSWER BUTTONS. */
.tv-opt {
  width: 100%; min-height: 58px; font-size: 1.1rem; text-align: left;
  padding: 12px 16px; border-radius: 12px; border: 2px solid var(--border, #e4e0c2);
  background: var(--surface, #fff); color: var(--text, #0A3323); cursor: pointer;
}
/* THE HIGHLIGHT IS A BORDER AND AN ARROW, never color alone — module-input-spec, and it is
   also what makes it visible to somebody scanning from across a room. */
.tv-opt[data-on="1"] { border-color: var(--link, #105666); box-shadow: 0 0 0 3px rgba(16,86,102,.18); }
.tv-opt[data-on="1"]::before { content: "\25B8\00a0"; }
/* RIGHT AND WRONG, themed, and not by colour alone (MIKE_LIST_20260930). These were a hardcoded
   green border on a hardcoded pale wash (#eef6f0) with a CSS-drawn tick, and a hardcoded red border
   on the wrong one. In dusk the pale wash sat under near-white text. Now the theme's own washes,
   the same pair lessons and Word Forge use, and the ✓/✗ is markup (answer_mark.js, `.ans-mark`
   below) so it has a word for a screen reader too. */
.tv-opt[data-right="1"] { border-color: var(--accent); background: var(--ok-surface); }
.tv-opt[data-wrong="1"] { border-color: var(--accent-warm-deep); background: var(--bad-surface); }
.tv-opt:disabled { cursor: default; }
.tv-said { margin: 14px 0 8px; font-size: 1.05rem; }
/* a transcript-lesson question's source line, shown with the answer (row 2.24 / §0h) */
.tv-src { margin: 0 0 10px; padding: 6px 10px; border-left: 3px solid var(--accent); font-size: .88rem; color: var(--text-soft); }
/* "Say why after the answer" (answer_source.js): the item's explanation, between "Correct." and the source line.
   A touch smaller than the answer, larger than the source (it is the content; the source is the footnote). Theme colours only. */
.tv-explain { margin: 0 0 8px; max-width: 60ch; font-size: .92rem; line-height: 1.45; color: var(--text-soft); overflow-wrap: anywhere; }
/* "Show where the answer comes from" (answer_source.js): one small, quiet line under the answer. Theme colours only. */
.tv-answer-src { margin: 0 0 10px; font-size: .82rem; color: var(--text-muted); overflow-wrap: anywhere; }
.tv-answer-src a { color: var(--link); }
.tv-next { min-height: 48px; padding: 10px 20px; font-size: 1rem; }
/* "I think this question is wrong" (contests.js): a quieter second stop after Next. The post-answer
   highlight is the same border + arrow as the options', never color alone. */
.tv-after { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.tv-contest { min-height: 48px; padding: 10px 16px; font-size: .95rem; border-radius: 10px;
  border: 1px solid var(--border, #e4e0c2); background: transparent; color: var(--text-soft, #3d5347); cursor: pointer; }
.tv-next[data-on="1"], .tv-contest[data-on="1"] { outline: 3px solid var(--scan-ring, var(--focus, var(--link, #105666))); outline-offset: 2px; }
.tv-next[data-on="1"]::before, .tv-contest[data-on="1"]::before { content: "\25B8\00a0"; }
.tv-contested { margin: 10px 0 0; font-size: .92rem; color: var(--text-soft, #3d5347); }
/* Review by playing (pack_reviews.js): a small, quiet strip at the foot, for the reviewer, not the player.
   Muted, smaller than everything the player reads, never a highlight stop. Theme colours only. */
.tv-review { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; margin: 14px 0 0;
  padding-top: 8px; border-top: 1px dashed var(--border); font-size: .82rem; color: var(--text-muted); }
.tv-review-tag { font-style: italic; }
.tv-review-btn { min-height: 32px; padding: 3px 10px; border-radius: 8px; font-size: .82rem;
  border: 1px solid var(--border); background: transparent; color: var(--text-muted); cursor: pointer; }
.tv-review-btn:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 2px; }
.tv-review-said { margin: 0; }
/* The item's own source (packs.js PER-ITEM SOURCES): its own line in the strip, so a long title wraps. */
.tv-review-src { flex-basis: 100%; margin: 0; overflow-wrap: anywhere; }
.tv-review-src a { color: var(--link); }
/* ...and, once answered, its explanation above that (the reviewer checks it too). Same quiet size as the strip. */
.tv-review-explain { flex-basis: 100%; margin: 0; overflow-wrap: anywhere; }
.tv-review-note { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.tv-review-note input { min-height: 32px; min-width: 14ch; padding: 3px 8px; border-radius: 8px; font: inherit;
  border: 1px solid var(--border); background: var(--surface); color: var(--text); }
.tv-empty { color: var(--text-muted, #5d7064); max-width: 60ch; }

/* ---- THE BANK EDITOR (Questions tab) ----
 * A textarea, deliberately, not a row editor — see bank_panel.js. A syllabus arrives from
 * somewhere else and is edited in bulk, and paste is the whole workflow.
 */
.bk h3 { margin: 0 0 4px; font-size: 1.05rem; }
.bk .bk-lede { margin: 0 0 10px; color: var(--text-soft, #3c5346); max-width: 68ch; }
.bk-help { margin: 0 0 12px; max-width: 68ch; }
.bk-help summary { cursor: pointer; font-weight: 600; }
.bk-help pre { background: var(--surface-alt, #FBF9E9); border: 1px solid var(--border, #e4e0c2);
  border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: .85rem; }
.bk-text {
  width: 100%; max-width: 900px; min-height: 300px;
  font: 14px/1.55 ui-monospace, Menlo, Consolas, monospace;
  padding: 12px 14px; border-radius: 10px; border: 1px solid var(--border, #e4e0c2);
  /* NO SPELLCHECK SQUIGGLES over a vocabulary list — every unusual word is the point, and a
     screen full of red underlines reads as a screen full of mistakes. */
  background: var(--surface, #fff); color: var(--text, #0A3323); resize: vertical;
}
.bk-sum { margin: 8px 0 4px; font-weight: 650; max-width: 68ch; }
.bk-sum[data-bad="1"] { color: #a4243b; }
.bk-problems { max-width: 900px; }
/* An error names a line and shows it, so it can be found in a long document. */
.bk-prob { padding: 6px 10px; margin-bottom: 4px; border-radius: 8px;
  background: var(--bad-surface); border: 1px solid rgba(164,36,59,.25); font-size: .9rem; }
.bk-prob.bk-note { background: var(--surface-alt, #FBF9E9); border-color: var(--border, #e4e0c2); }
.bk-line { display: block; font: 13px ui-monospace, Menlo, Consolas, monospace;
  color: var(--text-muted, #5d7064); margin-top: 2px; }
.bk-derived { margin: 10px 0; color: var(--text-soft, #3c5346); max-width: 68ch; }

/* The bank editor rendered as a MODULE rather than a full tab — it may be a quarter of a
   screen beside the game it feeds, so it has to be usable small. */
.m-bank-host .bk-text, .tv-empty .tv-fmt { max-width: 100%; }
.tv-empty { max-width: 46ch; }
.tv-empty p { margin: 0 0 8px; }
.tv-empty .tv-fmt { font-size: .88rem; color: var(--text-muted, #5d7064); }

/* ==================================================================================
   Appended from Claude Design's live-themes handoff, 2026-09-22 (board_live.css).
   Adds the see-through/clear AAC board surfaces. Appended at the end of this file, not
   directly after either existing .aboard.ab-hc rule (there are two, ~275 lines apart) --
   its own final high-contrast reassertion below still needs to win the cascade over both.
   ================================================================================== */
/* board_live.css — the translucent and clear AAC board. APPEND to modules.css, directly after the
   `.aboard.ab-hc` rule (the re-assertion at the bottom of this file keeps high contrast winning
   either way, but keeping them adjacent is how the next reader finds both).

   From Claude Design, 2026-09-22. Pairs with the `surface` setting in board_patch.md.

   *** WHAT THIS ADDS, AND WHAT IT DOES NOT TOUCH ***
   The pinned dark board (2026-09-02) stays the DEFAULT, and every contrast number in the long
   comment above `.aboard` stays true for it. These are two NEW surfaces a caregiver opts into:

     solid  the board as it is today. Default. Nothing here applies.
     veil   cards are a translucent sheet (--board-veil) over whatever is behind the board: a scene
            in .ab-surface, or a live theme's scene behind the whole screen.
     clear  no card face at all. The word and the symbol sit straight on the scene with a halo,
            and the card edge is what separates "card" from "hole".

   THE CONTRAST ARGUMENT CHANGES IN THESE MODES and it is named here rather than hidden: over a
   moving scene the worst case is not "word on card", it is "word on the brightest thing that can
   cross behind the card". Each live theme's --board-veil was chosen by Design against exactly that
   case, and the estimates are in AacGround.jsx next to each value. Code measures them. */

.aboard.ab-veil,
.aboard.ab-clear{
  background:transparent;
  --ab-bg:transparent;
  --ab-card:var(--board-veil,rgba(27,36,41,.72));
  --ab-ink:var(--board-text,#e8eef0);
  --ab-line:var(--board-border,#7d939d);
  --ab-lit:var(--board-lit,#8fae63);
}
.aboard.ab-clear{--ab-card:transparent}

/* THE HALO. Only where there is no card face to read against. It runs whichever way the world
   needs: dark on the dark scenes, light on `cozy` (a dark halo under dark ink just thickens the
   letters into mud). The theme says which via --board-halo. */
.aboard.ab-clear .ab-word{text-shadow:var(--board-halo,0 1px 2px rgba(0,0,0,.85),0 0 6px rgba(0,0,0,.6))}
.aboard.ab-clear .ab-sym svg{filter:drop-shadow(0 1px 1.5px rgba(0,0,0,.55))}

/* THE SCAN HIGHLIGHT WITHOUT A FACE. On a clear card the border is the only surface left, so the
   lit ring is drawn outside it as well, rather than disappearing with the fill. Never animated. */
.aboard.ab-clear .ab-card.ab-lit{box-shadow:0 0 0 max(2px,calc(var(--u,1px) * 2.5)) var(--ab-lit),
  0 0 0 .8vmin rgba(143,174,99,.45)}

/* An empty slot on a see-through board is still DRAWN: dashed and faint, over the scene. */
.aboard.ab-veil .ab-card.ab-empty,
.aboard.ab-clear .ab-card.ab-empty{background:transparent}

/* *** THE SYMBOL AND WORD COLOURS, RE-TINTED PER WORLD, WITH NO JS. ***
   Every meaning-bearing hex in aac_symbols.js is a literal stroke/fill attribute on inline SVG,
   and the eight word colours above are the same hexes. Attribute selectors catch the literals,
   so a live theme re-tints both through --board-sym-* and every other mode keeps today's hex
   exactly (the fallback IS today's value). `i` because the source mixes #D3968C and #d3968c.
   Scoped to veil/clear: the pinned board's numbers were measured with the literal hexes. */
.aboard:is(.ab-veil,.ab-clear) [stroke="#6abf69" i]{stroke:var(--board-sym-yes,#6abf69)}
.aboard:is(.ab-veil,.ab-clear) [fill="#6abf69" i]{fill:var(--board-sym-yes,#6abf69)}
.aboard:is(.ab-veil,.ab-clear) [stroke="#d3968c" i]{stroke:var(--board-sym-no,#D3968C)}
.aboard:is(.ab-veil,.ab-clear) [fill="#d3968c" i]{fill:var(--board-sym-no,#D3968C)}
.aboard:is(.ab-veil,.ab-clear) [stroke="#e2b45a" i]{stroke:var(--board-sym-need,#e2b45a)}
.aboard:is(.ab-veil,.ab-clear) [fill="#e2b45a" i]{fill:var(--board-sym-need,#e2b45a)}
.aboard:is(.ab-veil,.ab-clear) [stroke="#d98a5f" i]{stroke:var(--board-sym-hot,#d98a5f)}
.aboard:is(.ab-veil,.ab-clear) [fill="#d98a5f" i]{fill:var(--board-sym-hot,#d98a5f)}
.aboard:is(.ab-veil,.ab-clear) [stroke="#7fc6d8" i]{stroke:var(--board-sym-cold,#7fc6d8)}
.aboard:is(.ab-veil,.ab-clear) [fill="#7fc6d8" i]{fill:var(--board-sym-cold,#7fc6d8)}
.aboard:is(.ab-veil,.ab-clear) [stroke="#cf6f86" i]{stroke:var(--board-sym-love,#cf6f86)}
.aboard:is(.ab-veil,.ab-clear) [fill="#cf6f86" i]{fill:var(--board-sym-love,#cf6f86)}
.aboard:is(.ab-veil,.ab-clear) .ab-yes .ab-word{color:var(--board-sym-yes,#6abf69)}
.aboard:is(.ab-veil,.ab-clear) .ab-no .ab-word{color:var(--board-sym-no,#d3968c)}
.aboard:is(.ab-veil,.ab-clear) .ab-need .ab-word{color:var(--board-sym-need,#e2b45a)}
.aboard:is(.ab-veil,.ab-clear) .ab-hot .ab-word{color:var(--board-sym-hot,#d98a5f)}
.aboard:is(.ab-veil,.ab-clear) .ab-cold .ab-word{color:var(--board-sym-cold,#7fc6d8)}
.aboard:is(.ab-veil,.ab-clear) .ab-love .ab-word{color:var(--board-sym-love,#cf6f86)}

/* *** HIGH CONTRAST STILL WINS. *** Re-asserted after the modes above because they set
   `background` and several --ab-* keys at the same specificity. Somebody who asked for maximum
   legibility gets the solid black-on-white board whatever surface was chosen, and the scene in
   .ab-surface is hidden rather than left showing through a white card. */
.aboard.ab-hc{background:var(--ab-bg);--ab-bg:#ffffff;--ab-card:#ffffff;--ab-ink:#000000;
  --ab-line:#000000;--ab-lit:#000000}
.aboard.ab-hc .ab-word{text-shadow:none}
.aboard.ab-hc .ab-surface{display:none}

/* *** THE WHOLE-PAGE SCENE CAN ONLY SHOW WHERE NOTHING PAINTS OVER IT. ***
   livescene.js sets [data-live-scene] on <html> while a live theme is on. The page background
   moves to <html> so the fixed scene layer (z-index:-1 inside <body>) sits between the two. Pages
   whose own containers paint var(--bg) opaquely will still cover it — see README "Known work". */
html[data-live-scene]{background:var(--bg)}
html[data-live-scene] body{background:transparent}


/* button (modules/button.js) — THE GAME'S NAME SIGN AND PICTURE, and any other button. The face is
   the AAC board's card (card_face.js: `.ab-img` then `.ab-word`), styled here under its own root so
   the board's stylesheet is untouched. NO LITERAL COLOURS: the two chosen colours arrive as
   --nb-ink / --nb-bg (set by the module from the settings menu's colour picker). The signs and
   frames are Claude Design's SVGs (web/client/design-assets/), handed in by the module as
   --nb-sign / --fr-art together with the geometry it measured from them; these rules only place
   them. The placeholder looks these replaced (plaque, glow; picture, tv) read as their designed
   successors — see LEGACY_STYLES / LEGACY_FRAMES in button.js. */
.nbtn-wrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  box-sizing:border-box;padding:6cqmin;container-type:size;container-name:nbtn;overflow:hidden}
.nbtn{--nb-ink:var(--text);--nb-bg:var(--surface);--nb-font:var(--font);
  position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:2cqmin;box-sizing:border-box;max-width:100%;max-height:100%;min-width:min(40cqw,100%);
  padding:4cqmin 7cqmin;margin:0;border:0;border-radius:1.6cqmin;cursor:pointer;
  color:var(--nb-ink);background:var(--nb-bg);font-family:var(--nb-font);
  /* A clear panel gives its text a halo to read against the scene; a button brings its own
     ground, so it does not want one (the neon sign adds its own below). */
  text-shadow:none;transition:transform .12s ease}
.nbtn .ab-word{display:block;max-width:100%;font-size:clamp(14px,12cqmin,170px);font-weight:700;
  line-height:1.08;text-align:center;overflow-wrap:break-word;hyphens:auto}
.nbtn .ab-img{display:block;flex:1 1 auto;min-height:0;width:100%;line-height:0}
.nbtn .ab-img img{display:block;width:100%;height:100%;object-fit:contain}
/* WITH A PICTURE the face takes a picture's shape and the words become its caption. */
.nbtn[data-has-img]{height:min(86cqh,120cqw);aspect-ratio:4/5;padding:3cqmin}
.nbtn[data-has-img] .ab-word{flex:none;font-size:clamp(11px,5.5cqmin,48px);font-weight:600}
.nbtn:focus-visible{outline:max(3px,.8cqmin) solid var(--scan-ring, var(--highlight));outline-offset:max(3px,.8cqmin)}
.nbtn.nbtn-press{transform:scale(.97)}

/* DESIGNED SIGNS. THE WORDS ELEMENT IS THE SIGN: Design's SVG is its background, at the sign's own
   proportions (--nb-sign-a), and the text box the module measured from the SVG is its padding
   (--nb-tb-*, each a % of the sign's width). A percentage padding is measured against the box the
   sign sits IN, so --nb-sw — the sign's width as a share of that box — scales the insets onto the
   sign. The module fits the font size to the box (button.js `fitWords`). */
.nbtn[data-sign] .ab-word{--nb-sw:1;display:flex;align-items:center;justify-content:center;
  box-sizing:border-box;flex:none;min-width:0;min-height:0;
  width:calc(var(--nb-sw) * 100%);aspect-ratio:var(--nb-sign-a);
  padding:calc(var(--nb-tb-t) * var(--nb-sw) * 1%) calc(var(--nb-tb-r) * var(--nb-sw) * 1%)
    calc(var(--nb-tb-b) * var(--nb-sw) * 1%) calc(var(--nb-tb-l) * var(--nb-sw) * 1%);
  background:var(--nb-sign) center/100% 100% no-repeat;font-weight:800}
/* A sign on its own: the face is exactly the sign, as big as the panel allows. The sign is its
   own ground, so the background colour does not paint here (see button.js's header). */
.nbtn[data-sign]:not([data-has-img]):not([data-frame-art]){
  width:min(100cqw,calc(100cqh * var(--nb-sign-a)));min-width:0;padding:0;border-radius:0;
  background:none}
/* Under a picture the sign is its caption: a plate on the wall below the painting. */
.nbtn[data-sign][data-has-img] .ab-word{--nb-sw:var(--nb-capw);align-self:center}
/* The neon sign's words glow in their own colour, as Design drew them. */
.nbtn[data-style="neon"] .ab-word{
  text-shadow:0 0 .12em currentColor,0 0 .4em color-mix(in srgb,currentColor 55%,transparent)}

/* DESIGNED FRAMES. The frame art is drawn OVER the window (::after), so the picture shows only
   through the transparent hole Design left and a rounded window rounds the picture; the window's
   ground (::before) is the background colour — the mat an empty frame shows, and what is seen
   while a picture loads. --fr-x/y/w/h are the window, measured from the SVG by the module, as %
   of the art. The picture FILLS the window (cover): a frame with bars inside it looks broken, and
   the module picks the frame shape closest to the picture's own so the crop stays small. */
.nbtn[data-frame-art]{padding:0;border-radius:0;background:none;min-width:0}
.nbtn[data-frame-art]:not([data-has-img]),.nbtn[data-frame-art] .ab-img{position:relative;isolation:isolate}
.nbtn[data-frame-art]:not([data-has-img])::before,.nbtn[data-frame-art] .ab-img::before{content:"";
  position:absolute;z-index:0;left:calc(var(--fr-x) * 1%);top:calc(var(--fr-y) * 1%);
  width:calc(var(--fr-w) * 1%);height:calc(var(--fr-h) * 1%);background:var(--nb-bg)}
.nbtn[data-frame-art]:not([data-has-img])::after,.nbtn[data-frame-art] .ab-img::after{content:"";
  position:absolute;inset:0;z-index:2;pointer-events:none;
  background:var(--fr-art) center/100% 100% no-repeat}
/* No picture (yet): the face is the frame, and the words sit in its window. */
.nbtn[data-frame-art]:not([data-has-img]){display:block;
  width:min(100cqw,calc(100cqh * var(--fr-a)));aspect-ratio:var(--fr-a)}
.nbtn[data-frame-art]:not([data-has-img]) .ab-word{position:absolute;z-index:1;box-sizing:border-box;
  display:flex;align-items:center;justify-content:center;
  left:calc(var(--fr-x) * 1%);top:calc(var(--fr-y) * 1%);
  width:calc(var(--fr-w) * 1%);height:calc(var(--fr-h) * 1%)}
.nbtn[data-frame-art]:not([data-has-img]):not([data-sign]) .ab-word{padding:4% 5%}
/* ...and with a sign as well, the sign hangs centred in the window. */
.nbtn[data-frame-art][data-sign]:not([data-has-img]) .ab-word{--nb-sw:calc(var(--fr-w) / 100);
  height:auto;top:calc((var(--fr-y) + var(--fr-h) / 2) * 1%);transform:translateY(-50%)}
/* A picture: the frame, then its caption. The face's width is solved so the frame (width / --fr-a
   tall) plus the caption (--nb-cap of the width tall) plus the gap fit the panel's height. */
.nbtn[data-frame-art][data-has-img]{
  --nb-fw:min(100cqw,calc((100cqh - 2cqmin) / (1 / var(--fr-a) + var(--nb-cap))));
  width:var(--nb-fw);height:auto;aspect-ratio:auto}
.nbtn[data-frame-art] .ab-img{flex:none;width:100%;min-height:0;aspect-ratio:var(--fr-a)}
.nbtn[data-frame-art] .ab-img img{position:absolute;z-index:1;
  left:calc(var(--fr-x) * 1%);top:calc(var(--fr-y) * 1%);
  width:calc(var(--fr-w) * 1%);height:calc(var(--fr-h) * 1%);object-fit:cover}
.nbtn[data-frame-art][data-has-img]:not([data-sign]) .ab-word{box-sizing:border-box;width:100%;
  height:calc(var(--nb-fw) * var(--nb-cap));display:flex;align-items:center;justify-content:center}

/* ---- WORD GAMES (Opposites, Rhyming, Yes/No) ----
 * Design's voice.html, sized by the game's own box (container units), so the same markup reads
 * in a grid cell and full screen. The yes/no switch path is on every asking screen. See
 * modules/word_games.js.
 *
 * *** THE CELEBRATION NEVER FLASHES. *** One fade-and-settle per star, played ONCE (iteration
 * count 1, never infinite), opacity only ever rising; under reduced motion there is no animation
 * at all and the stars simply appear. dev/word_games_test.html reads this block to hold that.
 */
/* TRANSPARENT, NOT `--bg` (Mike, 2026-10-02: "The backgrounds on brick breaker and brain games aren't
   transparent when they should be"). Every answer game draws `.wg` (word games, and quiz_view.js's
   spelling / math / name that / thinking / word builder / brain games), and `--bg` here covered the
   panel's own background - which IS the screen's "Panel backgrounds" setting (kiosk.css `panelSurface`)
   - so solid / veil / clear changed nothing on any of them. The photos fix (`.photos .stage`, above),
   for the same reason: on a solid panel the panel's surface shows; on veil / clear, the scene. */
.wg{position:absolute;inset:0;container-type:size;display:grid;grid-template-rows:auto 1fr auto;
  gap:2cqmin;padding:3cqmin 4cqmin;box-sizing:border-box;overflow:hidden;
  background:transparent;color:var(--text);font-family:var(--font)}
.wg-ask{margin:0;text-align:center;font-size:clamp(18px,7cqmin,84px);font-weight:800;line-height:1.1}
.wg-ask em{font-style:normal;color:var(--accent-warm-deep)}
.wg-mid{display:grid;grid-template-columns:auto 1fr;gap:4cqmin;align-items:center;min-height:0}
.wg-mid.wg-one{grid-template-columns:1fr}
@container (max-aspect-ratio: 1/1){.wg-mid{grid-template-columns:1fr;justify-items:center}}
.wg-pic{margin:0;display:grid;justify-items:center;gap:1cqmin;padding:2.5cqmin;
  border:max(2px,.5cqmin) solid var(--border);border-radius:3cqmin;background:var(--surface);color:var(--text)}
.wg-pic svg{width:clamp(48px,26cqmin,300px);height:auto;aspect-ratio:1}
.wg-pic b{font-size:clamp(18px,8cqmin,92px);line-height:1;letter-spacing:.02em}
.wg-st{display:grid;gap:2cqmin;align-content:center;min-width:0}
.wg-say{margin:0;font-size:clamp(15px,4.8cqmin,58px);font-weight:700;line-height:1.25}
.wg-say q{color:var(--accent-warm-deep)}
.wg-soft{color:var(--text-soft);font-weight:600}
.wg-hint{margin:0;font-size:clamp(14px,4cqmin,50px);padding:1.6cqmin 2.4cqmin;border-radius:2cqmin;
  background:var(--surface-alt);border-left:max(4px,1.1cqmin) solid var(--accent-warm)}
.wg-btns{display:flex;gap:2cqmin;flex-wrap:wrap;justify-content:center}
.wg-btn{min-height:max(48px,11cqmin);min-width:max(96px,22cqmin);padding:0 3.5cqmin;
  border-radius:2.4cqmin;border:max(2px,.5cqmin) solid var(--border);background:var(--surface);
  color:var(--text);font:800 clamp(16px,5cqmin,62px)/1 var(--font);cursor:pointer;white-space:nowrap}
.wg-btn[data-act="yes"],.wg-btn[data-act="confirm"],.wg-btn[data-act="more"]{border-color:var(--accent)}
/* THE HIGHLIGHT IS AN OUTLINE AND AN ARROW, never colour alone (module-input-spec). The outline is the theme's
   --scan-ring, 3:1 on every surface in every theme (theme.js), not the raw --link (2026-10-04). Every switch
   cursor in this file (word games, word forge, trivia, the calculator) rings the same way. */
.wg-btn[data-on="1"]{outline:max(3px,1cqmin) solid var(--scan-ring, var(--focus, var(--link)));outline-offset:max(2px,.6cqmin)}
.wg-btn[data-on="1"]::before{content:"\25B8\00a0"}
.wg-foot{display:flex;align-items:center;justify-content:space-between;gap:2cqmin;flex-wrap:wrap;
  font-size:clamp(13px,3.2cqmin,40px);color:var(--text-soft)}
.wg-right{justify-items:center;text-align:center}
.wg-count{margin:0;font-size:clamp(13px,3.4cqmin,44px);color:var(--text-soft)}
.wg-ring{width:clamp(60px,20cqmin,250px);aspect-ratio:1;box-sizing:border-box;border-radius:50%;
  border:max(4px,1.8cqmin) solid var(--accent);display:grid;place-items:center}
.wg-ring::after{content:"";width:28%;height:48%;border:solid var(--accent);
  border-width:0 max(4px,2cqmin) max(4px,2cqmin) 0;transform:rotate(45deg) translate(-10%,-12%)}
.wg-pair{display:flex;align-items:center;gap:2cqmin;font-size:clamp(18px,6cqmin,70px);font-weight:800}
.wg-pair svg{width:clamp(36px,12cqmin,150px);height:auto;aspect-ratio:1}
.wg-star{position:absolute;width:clamp(12px,3cqmin,34px);aspect-ratio:1;pointer-events:none;
  background:var(--highlight);
  clip-path:polygon(50% 0,61% 38%,100% 38%,68% 60%,80% 100%,50% 76%,20% 100%,32% 60%,0 38%,39% 38%)}
.wg-cat{position:absolute;right:3cqmin;bottom:2cqmin;width:clamp(60px,20cqmin,250px);pointer-events:none}
@keyframes wg-drift{from{opacity:0;transform:translateY(3cqmin) scale(.6)}to{opacity:1;transform:none}}
.wg[data-motion="full"] .wg-star{animation:wg-drift 1.6s ease-out 1 both}
.wg[data-motion="reduce"] .wg-star{animation:none}
/* ---- end WORD GAMES ---- */
