/* ==========================================================================
   TimerPi app CSS — layout glue ONLY.
   All theme look (colors, buttons, panels, readouts, meters, lamps…) comes
   from ftl-themes v4 classes/tokens (link one dist/<theme>.css).
   This file is unlayered, so it wins over the theme's `ui` layer wherever it
   speaks — keep it to layout, states and app-specific widgets.
   Brand accent is PRIMARY PURPLE #7C3AED (orange banned). Green #22C55E is
   logo-only artwork, never UI chrome. Never re-point the theme's global
   --accent from here; this var is scoped to brand elements only.
   ========================================================================== */

/* Brand accent, scoped: wordmark/hero only (.btn-primary/.btn-go get their
   own scoped brand rule in the supervisor block at file end). */
.tp-brand { --accent: #7C3AED; }
/* Leave event / Sign out are POST forms (no cross-site logout): the form
   box takes no room in the header row. */
.tp-nav-form { display: contents; }

/* --------------------------------------------------------------------------
   Dashboard grid: left rail (now) | centre (cue list) | right (panels)
   -------------------------------------------------------------------------- */
.tp-dash {
  display: grid;
  /* U37: left = Current cue, Messages, Import; right = transport + running order. */
  grid-template-columns: minmax(16rem, 18rem) minmax(0, 1fr);
  gap: var(--space-m, 1rem);
  align-items: start;
  height: 100%;
  min-height: 0;
}
.tp-dash > * { min-width: 0; min-height: 0; }

@media (max-width: 1200px) {
  .tp-dash { grid-template-columns: minmax(17rem, 21rem) minmax(0, 1fr); }
}
/* Tablet and below (2026-10-07): one column — Current cue full width,
   transport under it, then the running order, Messages, Import. */
@media (max-width: 1100px) {
  .tp-dash { grid-template-columns: minmax(0, 1fr); height: auto; }
  .tp-dash > .tp-dash-left, .tp-dash > .tp-dash-main { display: contents; }
  .tp-dash .tp-now { order: 1; }
  .tp-dash .tp-transport-bar { order: 2; }
  .tp-dash .tp-runorder { order: 3; }
  .tp-dash #messages-panel { order: 4; }
  .tp-dash .tp-import { order: 5; }
}

.tp-dash-main { display: flex; flex-direction: column; min-height: 0; height: 100%; }
.tp-dash-main .tp-runorder { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.tp-dash-main .tp-cuelist-wrap { flex: 1 1 auto; min-height: 12rem; overflow: auto; }
/* Transport: ftl buttons with borders (touch screens get no hover, so the
   edge is what shows a button). GO is 25% taller than its neighbours. */
.tp-transport-bar { --tp-tbtn: 2.75rem; justify-content: flex-start; }
.tp-transport-bar .btn-icon { --btn-icon-size: var(--tp-tbtn); font-size: 1.05rem; }
.tp-transport-bar #tp-blank { block-size: var(--tp-tbtn); padding-block: 0; }
.tp-transport-bar .btn-go {
  block-size: calc(var(--tp-tbtn) * 1.25);
  padding-block: 0;
  font-size: 1.35rem;
  min-inline-size: 6.5rem;
}
/* Phones (E2E #24): FLASH and BLANK take a second row instead of being
   clipped off the right edge. */
@media (max-width: 600px) {
  .tp-transport-bar { flex-wrap: wrap; }
  .tp-transport-bar .btn-go { flex: 1 1 auto; }
  .tp-transport-bar > :is([data-cmd="flash"], #tp-blank) { flex: 1 1 8rem; justify-content: center; }
}

/* Now panel (left rail). A size container so the readout scales with the
   rail's width, not the window's (U33: "0:09.4" lost its tenths off the
   right edge of a narrow rail). */
.tp-now { display: flex; flex-direction: column; gap: var(--space-s, .75rem); container-type: inline-size; }

/* U34: the time of day sits at the top centre of the header bar. */
/* When the bar is crowded (a long nav: weyland-yutani's tracked caps,
   narrow windows) timerpi.js sets .tp-bar-crowded and the clock joins the
   flow, centred in the free space, instead of covering the nav. */
.app-bar { position: relative; }
.tp-bar-clock { position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; }
.tp-bar-crowded .tp-bar-clock { position: static; transform: none; margin-inline: auto; }
@media (max-width: 1200px) {
  .tp-bar-clock { position: static; transform: none; margin-inline: auto; }
}
/* Its own plate: bar art (hot-wheels' flame) sits behind it. */
.app-bar .tp-bar-clock { background: var(--readout-bg, var(--surface)); padding-inline: .45em; border-radius: var(--radius, 4px); }

/* --------------------------------------------------------------------------
   Clocks. Base look is .readout from the theme; this only sizes and states.
   States are driven by JS via [data-state]:
     idle | armed | running | paused | held | overtime | alert1 | alert2
   Colors: theme state tokens, overridable per-cue via --clock-alert1/-alert2
   (JS copies cue.alertColor1/2 onto them).
   -------------------------------------------------------------------------- */
.tp-clock {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  line-height: 1;
  letter-spacing: .02em;
}
.tp-clock[data-state="idle"],
.tp-clock[data-state="armed"] { color: var(--muted, inherit); }
.tp-clock[data-state="paused"] { color: var(--warning-text, var(--warning, inherit)); }
.tp-clock[data-state="held"] { color: var(--muted, inherit); }
.tp-clock[data-state="overtime"],
.tp-clock[data-state="alert2"] { color: var(--clock-alert2, var(--danger-text, var(--danger))); }
.tp-clock[data-state="alert1"] { color: var(--clock-alert1, var(--warning-text, var(--warning))); }

.tp-clock-xl { font-size: clamp(4rem, 13vw, 13rem); font-weight: 700; }

/* The current-cue rail (dashboard, frag-current) is a NARROW column: the
   XL stage scale shatters "00:00" into wrapped lines and pushes the state
   cluster + meter + cue label below the fold. Give the rail its own
   ceiling and forbid wrapping — the whole stack must fit one viewport. */
.tp-now .tp-clock {
  font-size: min(clamp(3.2rem, 11vh, 7rem), 17cqi);
  white-space: nowrap;
  max-width: 100%;
}
@media (max-width: 760px) {
  .tp-now .tp-clock { font-size: min(clamp(3.4rem, 17vw, 6rem), 17cqi); }
}

@keyframes tp-overtime-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .72; }
}
.tp-clock[data-state="overtime"],
.tp-clock[data-state="alert2"] { animation: tp-overtime-pulse 1s steps(2, jump-none) infinite; }
[data-motion="reduced"] .tp-clock[data-state="overtime"],
[data-motion="reduced"] .tp-clock[data-state="alert2"] { animation: none; }

/* Progress meter for the active cue: JS drives --meter-level */
.tp-progress { width: 100%; }
.tp-progress-meter { --meter-level: 0%; width: 100%; }
/* --- C2 drill findings: honest idle rail + offline render parity ---------- *
 * The idle clock's --:-- placeholder at rail scale rendered as four pale
 * blocks; idle now paints BLANK (LED-dark kiosk convention — JS sends ''
 * when nothing is armed) so "nothing on the clock" can't read as a glitch,
 * and the meter parks while no cue exists (the ADJUST row already disables).
 * And the day bar moved under the running order: two-line compact header
 * (title/badges + filter/undo, then the add form), bar block styled as a
 * quiet summary strip separated by a hairline. */
.tp-clock[data-state="idle"] { min-height: 1.1em; text-shadow: none; }
#tp-now.tp-idle-cue .tp-progress-meter { display: none; }

/* C2 layout: RUNNING ORDER header, now two compact rows. */
.tp-cuelist-head { display: flex; flex-direction: column; gap: .5rem; }
.tp-cuelist-title { font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }

/* C2 layout: the day-bar block rides inside the running-order panel under
   the cue list — a quiet summary strip with a hairline top rule, never a
   full-width orphan row below the grid. */
.tp-daybar-block { border-top: 1px solid var(--hairline); padding: .55rem .75rem .75rem; }
.tp-daybar-block .tp-daybar { box-sizing: border-box; }

/* Mobile tile editor (NOTES-board §5.3): the pointer grid is absolute-able
   on desks, not on phones — under 760 px, while editing, #b-editor lists
   tiles with X/Y/W/H steppers (revert-on-overlap, same autosave). */
.b-list-editor { display: none; }
@media (max-width: 760px) {
  body[data-editing] .b-list-editor { display: flex; flex-direction: column; gap: .5rem; }
}
.b-ed-row {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: nowrap;
}
.b-ed-name { flex: 1 1 auto; min-width: 4rem; text-transform: uppercase; letter-spacing: .04em; font-size: .8em; }
.b-ed-val { min-inline-size: 2ch; text-align: center; }

/* Core ships .input as display:block; width:100% (built for stacked .field
   rows) — inside the compact header clusters the operator form must stay on
   ONE line (label + duration + Add; day time + Save), or it re-stacks and
   the header eats three rows. */
.tp-daybar-block #tp-day-start-form { flex-wrap: nowrap; }
.tp-cuelist-head form[data-cmd="cueAdd"] { flex-wrap: wrap; }
.tp-cuelist-head form[data-cmd="cueAdd"],
.tp-daybar-block #tp-day-start-form {
  align-items: center;
}
.tp-cuelist-head form[data-cmd="cueAdd"] .input,
.tp-daybar-block #tp-day-start-form .input {
  width: auto;
  min-width: 0;
  flex: 0 1 auto;
}
.tp-cuelist-head form[data-cmd="cueAdd"] .input:not(.mono) {
  flex: 1 1 14rem;
  max-inline-size: 24rem;
}


/* --------------------------------------------------------------------------
   Cue list rows
   -------------------------------------------------------------------------- */
.tp-cue-pos { opacity: .75; font-variant-numeric: tabular-nums; }
.tp-cue-dur, .tp-cue-start, .tp-cue-end { font-variant-numeric: tabular-nums; white-space: nowrap; }
.tp-cue-label { font-weight: 600; }
.tp-cue-notes { color: var(--muted); font-size: .85em; }
.tp-cue-rowbtns { white-space: nowrap; }

tr.tp-row-active {
  --row-selected-bg: color-mix(in srgb, var(--accent) 18%, var(--surface));
}
tr.tp-row-past { opacity: .55; }
tr.tp-row-past .tp-cue-label { text-decoration: line-through solid color-mix(in srgb, var(--muted) 55%, transparent) 1px; }
tr.tp-row-next td { border-block-start-style: solid; }

/* Row drag affordance (v1 reorder is button-driven; drag is a placeholder) */

/* --------------------------------------------------------------------------
   Day bar (bottom of dashboard). Segments are server-rendered; the needle is
   animated by the client clock.
   -------------------------------------------------------------------------- */
.tp-daybar {
  position: relative;
  height: 2.6rem;
  border-radius: var(--radius, .5rem);
  background: var(--surface-2, transparent);
  border: 1px solid var(--border, transparent);
  overflow: hidden;
}
.tp-daybar-seg {
  position: absolute; top: 0; bottom: 0;
  border-inline-start: 1px solid var(--hairline, transparent);
  overflow: hidden;
}
.tp-daybar-seg > span {
  position: absolute; inset-inline: .25rem; top: 50%;
  translate: 0 -50%;
  font-size: .68rem; line-height: 1.2;
  color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tp-daybar-seg.is-break { background: repeating-linear-gradient(45deg,
  color-mix(in srgb, var(--border) 55%, transparent) 0 .5rem,
  transparent .5rem 1rem); }
.tp-daybar-seg.is-done { filter: saturate(.4) brightness(.75); }
.tp-daybar-seg.is-active { outline: 2px solid var(--accent); outline-offset: -2px; z-index: 1; }
.tp-daybar-needle {
  position: absolute; top: -.25rem; bottom: -.25rem; width: 2px;
  background: var(--accent);
  box-shadow: 0 0 .5rem var(--accent);
  z-index: 2;
  translate: -1px 0;
}
.tp-daybar-scale {
  display: flex; justify-content: space-between;
  font-size: .7rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
  padding-block-start: .2rem;
}

/* --------------------------------------------------------------------------
   Messages panel & stage overlay
   -------------------------------------------------------------------------- */
/* Cue-list empty states + message overlay live in their fragments; row
   paint for the running cue comes from core tr.is-active (toggled alongside
   tp-row-active by timerpi.js — REVIEW-2 R2). */
.tp-stage-message {
  position: absolute; inset-inline: 0;
  bottom: max(12vh, env(safe-area-inset-bottom, 0px));
  display: flex; justify-content: center; padding-inline: 4vw;
  pointer-events: none; z-index: 5;
}
.tp-stage-message > div {
  max-width: 90vw;
  font-size: clamp(1.6rem, 4.5vw, 4rem);
  font-weight: 800; text-align: center;
  padding: .5em 1em;
  border-radius: var(--radius, .5rem);
  background: color-mix(in srgb, var(--surface) 78%, transparent);
  border: 2px solid var(--warning, currentColor);
  color: var(--text);
}

/* --------------------------------------------------------------------------
   Display page (fullscreen TV). L0 shell: tokens only, no .app shell.
   1080p50-safe: no scrollbars, fixed viewport blocks.
   -------------------------------------------------------------------------- */
body.tp-display {
  height: 100dvh;
  overflow: hidden;
  cursor: default;
}
body.tp-hide-cursor,
body.tp-hide-cursor * { cursor: none !important; }

.tp-display-stage {
  position: fixed; inset: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto;
  grid-template-columns: minmax(0, 1fr); /* content never widens the page (fit.js measures this) */
  padding: max(2.2vh, env(safe-area-inset-top, 0px)) max(3vw, env(safe-area-inset-right, 0px))
           max(2.2vh, env(safe-area-inset-bottom, 0px)) max(3vw, env(safe-area-inset-left, 0px));
  gap: 1.2vh;
  background: var(--bg);
  color: var(--text);
}
.tp-display-top { display: flex; align-items: center; gap: 1rem; min-height: 0; }
.tp-display-top .tp-display-label {
  font-size: clamp(1.6rem, 4.2vh, 4rem); font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tp-display-center {
  display: flex; align-items: center; justify-content: center;
  min-height: 0;
}
/* Height-bound here; fit.js shrinks it by width so a phone or a long
   H:MM:SS never overflows (E2E #21). */
.tp-display-center .tp-clock { font-size: calc(clamp(3rem, 34vh, 26rem) * var(--tp-fit, 1)); white-space: nowrap; }
.tp-display-top .tp-brand { flex: none; display: flex; }
.tp-display-top .tp-brand img { height: clamp(1.6rem, 4.2vh, 4rem); width: auto; }
.tp-display-next {
  display: flex; align-items: baseline; gap: 2vw;
  font-size: clamp(1.2rem, 3.4vh, 2.6rem);
  color: var(--muted);
}
.tp-display-next strong { color: var(--text); font-weight: 700; }
.tp-display-speaker { font-size: clamp(1rem, 2.6vh, 2rem); color: var(--muted); }
.tp-display-status {
  display: flex; align-items: center; gap: 1rem;
  font-size: clamp(.8rem, 1.6vh, 1.1rem);
  color: var(--muted);
  opacity: .8;
}

/* Alert state paints the whole stage edge so it reads at TV distance */
.tp-display-stage[data-state="alert1"] { box-shadow: inset 0 0 0 .45vmin var(--clock-alert1, var(--warning)); }
.tp-display-stage[data-state="alert2"] { box-shadow: inset 0 0 0 .7vmin var(--clock-alert2, var(--danger)); }
.tp-display-stage[data-state="overtime"] { box-shadow: inset 0 0 0 .7vmin var(--danger); }

/* Offline overlay shell (frag-offline): ftl has no full-screen wrapper for
   .empty-state, so the scrim + centring is ours; the content is
   .empty-state.is-offline. */
.tp-offline {
  position: fixed; inset: 0; z-index: 40;
  display: none; place-items: center;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
}
.tp-offline.is-visible { display: grid; }
.tp-offline .empty-state-title { font-size: clamp(2rem, 6vh, 4rem); }

/* F3: identical output at 720p/1080p/4K — on display pages the ROOT rem
   rides the viewport height (1.5vh ≈ 16px at 1080p), so every rem-based
   floor, cap, gap, track and tile scales with output size while the vh/vw
   middle terms keep their tuned ratios. Operator pages (.app) keep the OS
   root size (readability + 44px touch floors). Without :has support the
   rule simply doesn't apply (previous fixed-root behavior). Hairline
   1–3px strokes stay device pixels everywhere by design. */
html:has(body.tp-display) { font-size: max(1.5vh, 11px); } /* floor: a phone held landscape kept 5–7 px text (E2E) */

/* E3 blackout overlay: shown purely by the body flag (server-rendered AND
   JS-repainted on every snapshot adopt, so it survives oob swaps and works
   with zero JS past first paint). Pure black + dim logo + STANDBY. */
.tp-blanked {
  position: fixed; inset: 0; z-index: 60;
  display: none; place-items: center; align-content: center; gap: 2vh;
  background: #000; text-align: center;
}
body[data-blanked="true"] .tp-blanked { display: grid; }
.tp-blanked img { width: clamp(4rem, 12vh, 8rem); height: auto; opacity: .55; }
.tp-blanked p { color: #666; letter-spacing: .35em; font-size: clamp(.8rem, 2vh, 1.2rem); }

/* E3: blackout toggle pressed = displays are dark right now. */
#tp-blank[aria-pressed="true"] { outline: 3px solid var(--danger, #ff4444); outline-offset: 3px; }


/* --------------------------------------------------------------------------
   Misc glue
   -------------------------------------------------------------------------- */
/* htmx request feedback on buttons/panels is themed by core (.htmx-request);
   nothing to fork — this only keeps swap-in fragments from jumping. */
.tp-fragment { min-height: 2.5rem; }
/* htmx swap settling feedback on the cue-list wrap (core owns the
   .htmx-request state paint — not re-declared here). */
.tp-cuelist-wrap.htmx-settling { opacity: .6; }

/* Transport keyboard hint chips */

/* Rate control row */
.tp-rate { display: flex; align-items: center; gap: .6rem; }
.tp-rate .readout { min-width: 3.2em; text-align: end; }

/* Print: the dashboard is not paper material, but keep the cue list useful */
@media print {
  .app-rail, .tp-daybar, .tp-cue-rowbtns { display: none !important; }
  .tp-dash { display: block; }
}

/* ==========================================================================
   Fix-3 additions (REVIEW-2 follow-up, 2026-10-03) — APPENDED, ADDITIVE ONLY.
   Naming convention adopted for later agents:
     * every new app glue class/app widget lives in the existing `tp-` namespace;
     * each Fix-3 selector carries a `Fix-3:` comment and cites its review id;
     * no theme-owned (dist/) rule is re-declared — overrides only target
       timerpi.css's own app classes (`tp-*`) or app-owned elements;
     * reduced-motion is a single `@media (prefers-reduced-motion: reduce)`
       block at the end of this file; whoever adds an animation appends the
       selector INTO that block rather than making a new one.
   ========================================================================== */


/* --------------------------------------------------------------------------
   Fix-3[O4]: HELD vs IDLE distinction. Engine already emits all eight states
   (engine.js:96–111, data-state on .tp-clock/.tp-display-stage/#tp-now;
   chip text wired in timerpi.js: 'held' → HELD vs 'idle' → READY) — this is
   paint only, no new toggle class needed; [data-state="held"] IS the toggle.
   Same mouth for the chip/labels so a held exhausted cue cannot read as
   "idle --:--" at TV distance, but stays STILL (overtime/alert2 pulse is not
   inherited).
   Later declaration wins the earlier .tp-clock[data-state="held"] muted rule
   in the base block above (same specificity, source order decides).
   -------------------------------------------------------------------------- */
.tp-clock[data-state="held"] { color: var(--warning-text, var(--warning, inherit)); }
.tp-display-stage[data-state="held"],
.tp-display-stage[data-state="paused"] {
  box-shadow: inset 0 0 0 .3vmin var(--warning);
}

/* --------------------------------------------------------------------------
   Fix-3[O3]: display status line ≥3:1 against theme tokens. The base
   .tp-display-status rule is app-owned glue (timerpi.css:212), NOT a
   theme-owned rule — we restate our own glue here rather than editing it in
   place so the Fix-1 diff stays clean. Rationale: --muted is only certified
   4.5:1 on --surface; this line sits on --bg and carries the join credential
   (session code), so opacity multiplication and the .8 shrink are dropped and
   the mono credential bumps to --text.
   -------------------------------------------------------------------------- */
.tp-display-status {
  opacity: 1;
  font-size: clamp(1rem, 2vh, 1.4rem);
  font-weight: 600;
}
.tp-display-status .mono { color: var(--text); }

/* --------------------------------------------------------------------------
   Fix-3[O8]: contract-required prefers-reduced-motion gate. Core gates its own
   animations internally; app ones must too. Existing dead selector
   `.tp-clock[data-motion="reduced"] .tp-clock` lives in the base block —
   whoever deletes it per P3 must keep the two [data-motion] lines below until
   this block is judged sufficient.
   Fix-1 pulse note: the only app pulse is tp-overtime-pulse on
   .tp-clock[data-state="overtime"|"alert2"] (timerpi.css:75–83). If Fix-1 adds
   another (needle, lamp, day-bar), append its selectors here and keep it
   referenced from the .tp-pulse / element it lives on — do not create a new
   media block.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .tp-clock[data-state="overtime"],
  .tp-clock[data-state="alert2"] {
    animation: none !important;
  }
  [data-motion="reduced"] .tp-clock[data-state="overtime"],
  [data-motion="reduced"] .tp-clock[data-state="alert2"] { animation: none; }
}

/* ==========================================================================
   Agent N display board (customizable ?view=board) — APPENDED, ADDITIVE ONLY.
   12-column grid of b-widget tiles. Brand: purple #7C3AED accents only
   (toolbar trigger + active-tile edge); green stays logo-only, orange banned.
   ff-warning/ff-critical state colors appear ONLY inside countdown/message
   semantics (data-state alert/overtime paints), never as chrome.
   Naming: everything lives in the `b-` namespace (no tp-* collisions).
   Locked boards (no [data-editing]) show zero compose chrome.
   ========================================================================== */

.b-board { overflow: auto; }

.b-topbar {
  display: flex; align-items: center; gap: .8rem;
  padding: .5rem 1rem;
  font-weight: 700;
}
.b-topbar .b-topbar-show { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-topbar .b-topbar-board {
  color: var(--muted);
  font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* 12-column grid; rows are 4.5rem tracks (tiles span rows via inline style). */
.b-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-auto-rows: 4.5rem;
  gap: .6rem;
  padding: .6rem 1rem 6rem;
  min-height: 0;
}

.b-widget {
  position: relative;
  min-width: 0; min-height: 0;
  overflow: hidden;
  border: 1px solid var(--border, transparent);
  border-radius: var(--radius, .5rem);
  background: var(--surface, transparent);
  padding: .5rem .7rem;
}
.b-widget .b-w-body {
  height: 100%;
  display: flex; flex-direction: column; justify-content: center;
  gap: .2rem; min-height: 0;
}

/* Tile typography */
.b-clock { font-size: clamp(2rem, 6vh, 4.5rem); font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; }
.b-w-countdown .b-clock { font-size: clamp(2.6rem, 9vh, 7rem); }
.b-cd-label { color: var(--muted); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-cue-label { font-size: clamp(1.2rem, 3.4vh, 2.4rem); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-speaker { font-size: clamp(1rem, 2.6vh, 1.8rem); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-eyebrow { font-size: .8rem; letter-spacing: .22em; font-weight: 800; color: var(--accent); }
.b-next-label { font-size: clamp(1.2rem, 3.4vh, 2.4rem); font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-next-meta { color: var(--muted); font-variant-numeric: tabular-nums; }
.b-wall { font-size: clamp(1.6rem, 4.5vh, 3.2rem); font-weight: 700; font-variant-numeric: tabular-nums; }
.b-show-title { font-size: clamp(1.1rem, 3vh, 2rem); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* E2: static free-text tile — wraps (unlike the single-line title), so
   welcome/sponsor/Wi-Fi lines read on lobby boards at any tile size. */
.b-notice { font-size: clamp(1rem, 2.6vh, 1.8rem); font-weight: 600; overflow: hidden; white-space: pre-wrap; overflow-wrap: anywhere; }
.b-rate { font-size: clamp(1.2rem, 3.4vh, 2.4rem); font-weight: 800; font-variant-numeric: tabular-nums; }

/* Countdown state semantics (state colors only — alert1/alert2/overtime). */
.b-w-countdown[data-state="alert1"] .b-clock { color: var(--clock-alert1, var(--warning-text, var(--warning))); }
.b-w-countdown[data-state="alert2"] .b-clock,
.b-w-countdown[data-state="overtime"] .b-clock { color: var(--clock-alert2, var(--danger-text, var(--danger))); }
.b-w-countdown[data-state="paused"] .b-clock,
.b-w-countdown[data-state="held"] .b-clock { color: var(--warning-text, var(--warning, inherit)); }
.b-w-countdown[data-state="idle"] .b-clock,
.b-w-countdown[data-state="armed"] .b-clock { color: var(--muted, inherit); }

/* Progress bars: cue = ftl .progress, day = ftl .gauge-linear > .meter
   (one accent fill, not the level meter's traffic-light bands). */
.b-bar-label { color: var(--muted); font-size: .85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.b-w-dayprogress { --meter-low: var(--accent); --meter-mid: var(--accent); --meter-high: var(--accent); }

/* Message tiles (message semantics may carry warning/danger tones). */
.b-msgs { display: flex; flex-direction: column; gap: .4rem; overflow: auto; min-height: 0; }
.b-msg {
  border: 2px solid var(--warning, currentColor);
  border-radius: var(--radius, .5rem);
  padding: .3em .7em; font-weight: 800; text-align: center;
}
.b-msg-empty { color: var(--muted); font-style: italic; }

/* Schedules are ftl .schedule-list; rows on a TV are not tap targets. */
.b-board .schedule-list { --tap-min: 0px; min-height: 0; overflow: hidden; }
.b-board .schedule-item { flex-shrink: 0; } /* a row never squashes: it shows whole or hides */
.b-board .schedule-item[hidden] { display: none; } /* rows that don't fit whole (board.js fitRows) */
.b-board .schedule-item > .schedule { --schedule-font-size: 1em; }

/* Screen-side join card (d-chrome pattern, b- namespace). */
.b-join-card {
  position: fixed;
  inset-block-end: max(1rem, env(safe-area-inset-bottom, 0px));
  inset-inline-end: max(1rem, env(safe-area-inset-right, 0px));
  display: grid; justify-items: center; gap: .3rem;
  padding: .6rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius, .5rem);
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  color: var(--muted); font-size: .82rem; line-height: 1.25;
  text-decoration: none; text-align: center; z-index: 5;
}
.b-join-card img { width: 4.5rem; height: auto; image-rendering: pixelated; }
.b-join-card .mono { color: var(--text); }

/* ---- Compose chrome (visible ONLY while editing) ---- */
.b-toolbar {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .4rem 1rem;
  border-block-end: 1px solid var(--border, transparent);
}
.b-toolbar .b-board-name { font-weight: 800; }
.b-toolbar .b-boards-label { color: var(--muted); font-size: .85rem; }
.b-toolbar .b-save-state { color: var(--muted); font-size: .85rem; }
.b-palette {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .4rem 1rem;
  border-block-end: 1px solid var(--border, transparent);
}
.b-palette .b-palette-title { font-weight: 700; }
.b-palette-list { display: flex; gap: .35rem; flex-wrap: wrap; }
.b-settings {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .4rem 1rem;
  border-block-end: 1px solid var(--border, transparent);
}
.b-settings label { display: flex; align-items: center; gap: .4em; }

/* Per-tile edit affordances: hidden unless body[data-editing]. */
.b-w-chrome { display: none; }
.b-w-resize { display: none; }
body[data-editing] .b-w-chrome {
  display: flex; align-items: center; gap: .4rem;
  cursor: grab; touch-action: none; user-select: none;
  color: var(--muted); font-size: .78rem;
}
body[data-editing] .b-w-chrome:active { cursor: grabbing; }
body[data-editing] .b-w-chrome .b-w-title { font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
/* Buttons (ftl .btn-icon.is-clear / .btn-close) cluster at the right of
   the tile's bar; the title takes the rest. */
body[data-editing] .b-w-chrome .b-w-title { margin-inline-end: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Edit-mode visuals are ftl's .dashboard.is-editing (board.js toggles the
   classes while editing); the grid keeps our fixed 12-column template and
   each tile its inline grid-column/grid-row. */
body[data-editing] .b-grid { --dashboard-cols: 12; --dashboard-row-min: 0px; }
body[data-editing] .b-w-grip { font-size: 1.1em; color: var(--accent); letter-spacing: -.1em; }
body[data-editing] .b-w-chrome .icon { width: 1em; height: 1em; }
/* In the Screens page editor the modal owns Done and the layout choice. */
body[data-compose] #b-edit-toggle, body[data-compose] #b-boards,
body[data-compose] .b-boards-label, body[data-compose] #b-new { display: none !important; }
/* The resize hit area sits over ftl's drawn corner (.widget::after). */
body[data-editing] .b-w-resize {
  display: block;
  position: absolute; inset-block-end: 0; inset-inline-end: 0; z-index: 2;
  inline-size: 1.6rem; block-size: 1.6rem;
  cursor: nwse-resize; touch-action: none;
}
body[data-editing] .b-grid { padding-block-end: 10rem; }

/* Compose chrome never prints; boards print as pure display. */
@media print {
  .b-toolbar, .b-palette, .b-settings, .b-w-chrome, .b-w-resize,
  .b-join-card, .tp-offline { display: none !important; }
}

/* ==========================================================================
   SUPERVISOR coherence pass (2026-10-03) — APPENDED, ADDITIVE + OVERRIDES.
   Rules live in existing namespaces (tp-*, b-*); each cites its reason.
   Later agents: this block is the visual coherence authority — shell/chrome
   unification, TV minimums, edit-mode safety. Do not restyle beneath it;
   extend it.
   Coherence rules established here (obey them):
     C1. One loud thing: GO. Only .btn-primary/.btn-go carry brand purple.
     C2. One offline voice: every LINK DOWN strip reads "Server unreachable —
         working from the local copy." + "Reconnects automatically when the
         link returns." (base / display / d-chrome / b-chrome).
     C3. One link language on screens: LINK LIVE (lit) / LINK DOWN (danger)
         chips — .tp-dv-link on variant boards, #b-link on custom boards.
     C4. One join-card pattern: mini QR + <host>.local + self URL +
         "Control room: /c/<code>". QRs on compose (?edit=1) URLs are
         rewritten client-side to the locked board (board.js lockJoinCard).
     C5. TV minimums (conservative — bigger wins): stage clock ≥26vh,
         cue label ≥5vh, NEXT ≥2.6vh, status/join credential ≥2.2vh,
         4vh/4vw safe-area inset, no-scroll 100dvh, long labels ellipsis.
     C6. Operator voice, no dev jargon: titles/hints name the action
         ("Reorder with the ▲ ▼ buttons", never "(v1: …)").
     C7. Touch: compose controls ≥44px on coarse pointers; edit chrome never
         prints; ?edit=1 renders an EDIT MODE badge + live note and stays
         locked until "Edit layout" is pressed.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Supervisor[C1/REVIEW-2 O1]: the purple mandate, finally painted. Scoped
   component-token override (NOT a global --accent re-point — the theme keeps
   its own accent for links/focus/rail art): the two triggers that mean
   "TimerPi's action" render brand purple on every theme. White on
   #7C3AED clears 5.7:1 (spec --on-accent).
   -------------------------------------------------------------------------- */
.btn-primary, .btn-go { --accent: #7C3AED; --on-accent: #ffffff; }
/* Themes whose primary button is a dark tint with ACCENT-coloured text:
   brand purple text on near-black measured 1–3.5:1 ("Set up here", GO).
   There the label takes --on-accent; the purple stays on the edge (E2E). */
html:is([data-theme="alienware"], [data-theme="matrix"], [data-theme="pipboy"], [data-theme="tokie"], [data-theme="tron"], [data-theme="winamp-classic"]) :is(.btn-primary, .btn-go) { --btn-fg: var(--on-accent); }

/* --------------------------------------------------------------------------
   Supervisor[C5]: TV legibility minimums per docs/TIMERPI-THEME-SPEC.md.
   .tp-display-center clock already 34vh (≥26vh, covers the ≥30vh alarm
   minimum); NEXT already 3.4vh (≥2.6vh). Fixed here: safe-area inset to the
   4vh/4vw spec floor (was 2.2vh/3vw — content sat inside the overscan zone),
   cue label to the 5vh floor, status/join credential to the 2.2vh floor.
   -------------------------------------------------------------------------- */
.tp-display-stage {
  padding: max(4vh, env(safe-area-inset-top, 0px)) max(4vw, env(safe-area-inset-right, 0px))
           max(4vh, env(safe-area-inset-bottom, 0px)) max(4vw, env(safe-area-inset-left, 0px));
}
.tp-display-top .tp-display-label {
  font-size: clamp(1.6rem, 5vh, 4rem);
}
.tp-display-status {
  font-size: clamp(1rem, 2.2vh, 1.4rem);
}

/* --------------------------------------------------------------------------
   Supervisor[REVIEW-2 P4]: day-bar needle was clipped by its own glow —
   .tp-daybar is overflow:hidden while the needle overshot top/bottom by
   .25rem. Pin the needle inside the bar; the glow still reads at the ends
   without cropping the shaft.
   -------------------------------------------------------------------------- */
.tp-daybar-needle { top: 0; bottom: 0; }

/* --------------------------------------------------------------------------
   Supervisor[REVIEW-2 P6]: phone dashboard header — the Running-order
   panel-header crammed title + badges + add-cue form into one 390px line.
   Below 760px the form and badges take full rows; day-bar segment labels
   (unreadable at .68rem on a phone bar) hide; the app-bar theme label hides.
   -------------------------------------------------------------------------- */
@media (max-width: 760px) {
  .tp-dash-main .panel-header :is(form, .badge.push) { flex-basis: 100%; }
  .tp-dash-main .panel-header { flex-wrap: wrap; }
  .tp-daybar-seg > span { display: none; }
  .app-bar .label { display: none; }
  .tp-dash-main .tp-cuelist-wrap { min-height: 16rem; }
}

/* --------------------------------------------------------------------------
   Supervisor[C3]: one link language on screens — ftl .connection
   (data-state live|offline) on .tp-dv-link (variants) and #b-link (board
   topbar). Sized for TV distance; the lamp follows the text size.
   -------------------------------------------------------------------------- */
:is(.b-link, .tp-dv-link) { --connection-size: clamp(.9rem, 2.2vh, 1.6rem); --lamp-size: .8em; letter-spacing: .18em; font-weight: 700; }
.b-link { margin-inline-start: auto; }

/* --------------------------------------------------------------------------
   Supervisor[C4]: custom-board join card aligns to the .tp-join-card pattern
   (same content order, same TV-proportioned QR). Fixed positioning stays —
   the board grid scrolls, variant screens do not.
   -------------------------------------------------------------------------- */
.b-join-card img { width: 7.4vh; min-width: 3.2rem; }

/* --------------------------------------------------------------------------
   Supervisor[C7]: edit-mode safety + compose ergonomics.
   - The join card hides while actively composing (nothing to scan mid-edit;
     the QR is already re-pointed at the locked URL by lockJoinCard).
   - body[data-editable] topbar warns that THIS url composes — never put it
     on a TV.
   - Compose buttons meet the 44px coarse-pointer floor.
   -------------------------------------------------------------------------- */
body[data-editing] .b-join-card { display: none; }
body[data-editable="1"] .b-topbar-board::after {
  content: " · compose link — show the locked board on TVs";
  color: var(--muted);
  font-weight: 400;
}
.b-edit-note { color: var(--muted); font-size: .85rem; }
@media (pointer: coarse) {
  .b-palette-list button, .b-toolbar .btn, .b-settings .btn,
  .b-w-chrome button { min-block-size: 44px; min-inline-size: 44px; }
}
/* No narrow-screen reflow: a phone gets the layout's own portrait version
   (owner 2026-10-07), placed exactly as designed. */
@media (max-width: 760px) {
  .b-toolbar { font-size: .95rem; }
}

/* Supervisor[C7/C2]: reduced-motion partners for this block's only animated
   additions — none animate, so the single reduced-motion gate stays the one
   file-end block above; future pulses append INTO it, never a new one. */

/* ==========================================================================
   Agent U2 — TV/display + widget-board UX pass (2026-10-03) — APPENDED.
   Scope: stage (#d-stage) + variant boards (tp-dv) + widget board (b-*).
   Namespaces: tp-* / b-* glue only; no theme-owned rule is re-declared.
   Floors cite docs/TIMERPI-THEME-SPEC.md.

   U2-1. Stage urgency chip (#d-chip): engine-specified fills ONLY —
         overtime/alert2 danger fill + near-black text (never white — 3.4:1
         fails), alert1 warning fill; held/paused hollow warning; blank
         hollow muted; idle/armed/running leave it hidden. Quiet phases stay
         quiet (one loud thing: the clock).
   U2-2. Alarm clock scale: overtime/alert2 digits 36vh (spec floor ≥30vh,
         base rest clock already 34vh ≥26vh) + heavy weight so URGENT reads
         both bigger and bolder without leaving its grid band.
   U2-4. Message overlay band (stage + variants): message colour rides the
         4px LEFT edge only (default accent — amber was defaulting every
         card); ink body ≥6vh so a red/amber WRAP UP reads in one sweep;
         96% surface band per spec.
   U2-5. Widget 1080p floors: every fixed small size now scales with vh;
         message cards join the left-edge pattern; progress bars snap to
         crisp radii (no capsule smear) and rounded fractions (board.js).
   U2-6. Edit mode: overlap attempt = 1 s purple nudge on the tile (revert
         stays; never state-red — the orange/warning ban holds outside timer
         semantics), coarse-pointer resize handle meets the 44px floor,
         palette palette-rows quieted for a compositor-only toolbar.

   Reduced-motion partners for every animation added here live in the single
   reduce block at the end of THIS section (Fix-3 convention).
   Brand audit: purple #7C3AED accents only; #ffaa00/#ff4444 appear only in
   SPEC timer/message semantics (state chip, clock digits, countdown tile,
   authored message edges); no #22C55E anywhere in this block.
   ========================================================================== */

/* --- U2-1: stage urgency chip (top row, right-shouldered) ------------------ */
/* An ftl .badge; timerpi.js sets data-state, which picks the badge tokens
   (ftl has no filled badge variant, so the urgent fills are tokens here). */
.tp-display-top .tp-state-chip {
  margin-inline-start: auto;
  font-size: clamp(1rem, 2.6vh, 2rem);
  font-weight: 800;
  letter-spacing: .08em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.tp-state-chip:is([data-state="overtime"], [data-state="alert2"]) {
  --badge-bg: var(--clock-alert2, var(--danger)); --badge-fg: var(--on-danger, #140607);
}
.tp-state-chip[data-state="alert1"] { --badge-bg: var(--clock-alert1, var(--warning)); --badge-fg: var(--on-warning, #1A1200); }
.tp-state-chip:is([data-state="held"], [data-state="paused"]) {
  --badge-bg: transparent; --badge-border: var(--warning); --badge-fg: var(--warning-text, var(--warning));
}
.tp-state-chip[data-state="blank"] { --badge-bg: transparent; --badge-border: var(--border); }

/* --- U2-2: alarm clock scale — urgent counts read bigger, never squeezed --- */
.tp-display-center .tp-clock[data-state="overtime"],
.tp-display-center .tp-clock[data-state="alert2"] {
  font-size: clamp(9rem, 36vh, 27rem);
  font-weight: 800;
}

/* --- U2-4: message overlay band (overrides the always-amber default edge) -- */
.tp-stage-message > div {
  font-size: clamp(1.7rem, 6vh, 6rem);
  font-weight: 800;
  background: color-mix(in srgb, var(--surface-2, var(--surface)) 96%, transparent);
  border: 1px solid var(--border, currentColor);
  border-inline-start: 4px solid var(--accent, #7C3AED);
}
/* Message colour (JS borderInlineStartColor / template inline style) wins. */

/* --- U2-5: widget 1080p floors (was px-fixed at every panel size) ---------- */
.b-eyebrow { font-size: clamp(.8rem, 1.6vh, 1.2rem); }
.b-cd-label { font-size: clamp(.9rem, 2vh, 1.7rem); }
.b-next-meta { font-size: clamp(.8rem, 1.9vh, 1.5rem); }
.b-bar-label { font-size: clamp(.8rem, 1.7vh, 1.4rem); }
.b-msg { font-size: clamp(1rem, 2.4vh, 2rem); }
.b-msg-empty { font-size: clamp(.85rem, 1.8vh, 1.4rem); }

/* Countdown urgency on the wallboard widget: same pulse language as the
   stage readout (color/shape carry it; pulse is enhancement only). */
.b-clock[data-state="overtime"],
.b-clock[data-state="alert2"] { animation: tp-overtime-pulse 1s steps(2, jump-none) infinite; }

/* --- U2-6: compositor feel + overlap nudge (edit mode) --------------------- */
/* Compose chrome sits on a SURFACE: --muted is only certified there, and on
   windows95's teal/navy page --bg the labels and hints vanished (E2E #25). */
.b-chrome, body[data-editable] .b-topbar { background: var(--surface); color: var(--text); }
.b-palette .b-palette-title {
  color: var(--muted);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
body[data-editing] .b-w-chrome button { min-block-size: 1.6em; }
@media (pointer: coarse) {
  .b-w-resize { inline-size: 2.6rem; block-size: 2.6rem; }
}
@keyframes b-overlap-nudge {
  0%, 100% { translate: 0 0; }
  25% { translate: -.35% 0; }
  75% { translate: .35% 0; }
}
body[data-editing] .b-overlap {
  outline: 3px solid var(--tp-accent, #7C3AED);
  outline-offset: -1px;
  animation: b-overlap-nudge .16s ease-in-out 3;
}

/* Touch floors on compose chrome already applied (supervisor C7); the hint
   chip and the state chip never print. */
@media print {
  .tp-state-chip { display: none !important; }
}

/* Reduced motion: the operator's editor calms down; the display pulse does
   not (displays always animate — AGENTS.md). */
@media (prefers-reduced-motion: reduce) {
  body[data-editing] .b-overlap { animation: none; }
}

/* ==========================================================================
   Agent U1 operator-handling pass (2026-10-03) — APPENDED, ADDITIVE ONLY.
   Scope: operator handling on dashboard/home (keyboard map, quick adjust,
   two-tap delete, wrap-up button, import dropzone, day-bar honesty).
   Rules inherited from the supervisor block above: C1 (GO is the only
   loud thing — nothing here glows), C6 (operator voice, no dev jargon),
   C7 (44px coarse-pointer floors). Everything lives in the tp- namespace;
   no theme-owned rule is re-declared.
   ========================================================================== */

/* --- U1: quick-adjust row (frag-current) ----------------------------------
   One-tap length nudges for the cue on the clock. Buttons stay
   merely normal .btn paint (disabled = core's disabled look); no purple
   beyond the GO (the loud thing stays the transport). */
.tp-adjust-row .btn { font-variant-numeric: tabular-nums; min-inline-size: 3.2em; }
@media (pointer: coarse) {
  .tp-adjust-row .btn { min-block-size: 44px; }
}

/* --- U1: two-tap delete arming (cue table) --------------------------------
   First tap on the bin arms it; second tap inside the rollback window
   sends cueDel. Static double outline — no animation, so no
   reduced-motion partner needed. */
.tp-del-armed {
  outline: 2px solid var(--danger-text, var(--danger, #ff4444));
  outline-offset: 2px;
  background: color-mix(in srgb, var(--danger-text, var(--danger, #ff4444)) 18%, transparent) !important;
  color: var(--text) !important;
}

/* --- U1: wrap-up quick action (frag-messages) -----------------------------
   Danger fill is CONTRACT-UI-sanctioned for WRAP-UP semantics; the button
   is one confident band, not a member of the compose cluster. Link row
   with the pending marker? Nothing to link — it is instant. No pulse. */
.tp-wrapup { inline-size: 100%; justify-content: center; font-weight: 700; }

/* --- U1: import dropzone affordance --------------------------------------
   Self-child edges usually come from core; this only defines the drop
   state and it stays hairline-quiet while nothing is being dragged. */
.tp-import.is-dropping {
  outline: 2px dashed var(--accent-2, var(--accent));
  outline-offset: -3px;
}

/* --- U1: the running row reads from across the room ----------------------
   Core's --row-active-bg + first-cell inset marker stay; TWO cheap boosts:
   label goes bolder, and the position numeral takes light accent ink —
   peripheral-vision anchors without adding any new fill or glow. */
tr.tp-row-active .tp-cue-label { font-weight: 800; }
tr.tp-row-active .tp-cue-pos { color: var(--accent-text, var(--accent-2, var(--accent))); font-weight: 700; }

/* --- U1: sticky cue-table header survives phone scroll -------------------
   On phones the page (not the wrap) is the scrollport — the sticky th
   then hides with the content. Give the wrap its own viewport-height
   scrollport so `.table.is-sticky` (core) keeps the header nailed. */
@media (max-width: 760px) {
  .tp-dash-main .tp-cuelist-wrap { max-height: 62dvh; }
}

/* Day-bar segment labels already hide ≤760px (supervisor block); the
   needle keeps its supervisor-pinned geometry. Nothing else appends. */

/* --- A3: cue filter rides the running-order header ---------------------- */
.tp-dash-main .panel-header { flex-wrap: wrap; row-gap: .35rem; }
/* --- current-cue rail: rate row must not wrap in the narrow column ------- */
.tp-now .tp-rate { align-items: center; }
.tp-now .tp-rate > .label, .tp-now .tp-rate .readout { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* --- B2: drag-to-reorder ------------------------------------------------ */
.tp-cue-pos { touch-action: none; cursor: grab; }
.tp-cue-pos:active { cursor: grabbing; }
/* NOTE: the selectors below must match what initDragReorder toggles
   (body.tp-drag-body, tbody.tp-drag-on) — a descendant selector like
   `.tp-drag body` never matches and silently disables the lock. */
body.tp-drag-body { user-select: none; }
body.tp-drag-body, body.tp-drag-body * { cursor: grabbing !important; }
.tp-dragging { opacity: .45; outline: 2px dashed var(--accent); outline-offset: -2px; }
.tp-drag-line {
  position: absolute;
  height: 3px;
  background: var(--accent, #7C3AED);
  border-radius: 2px;
  pointer-events: none;
  z-index: 30;
  box-shadow: 0 0 0 1px rgba(0,0,0,.35);
}
.tp-cuelist-wrap { position: relative; }




/* Display splashes (waiting room, READY, box pairing) are ftl .splash on a
   black stage with our logo as the art. The stage is black on every theme,
   so the ink is fixed light too (a light theme's --text would vanish). */
.tp-display .splash {
  --splash-bg: #000; --splash-fg: #f2f2f2; --splash-message-fg: #a0a0ac; --splash-title-fg: #a0a0ac;
  --readout-fg: #f2f2f2;
  --splash-art: url("/img/timerpi.svg") center / contain no-repeat;
  --splash-art-mask: none; --splash-art-radius: 0; --splash-art-motion: none;
  --splash-art-size: clamp(6rem, 18vh, 12rem); --splash-gap: 2.5vh;
  --splash-title-size: clamp(1.2rem, 4.2vh, 2.6rem); --splash-title-tracking: .28em;
}
.tp-display .splash-message { max-width: 46ch; font-size: clamp(1rem, 2.6vh, 2rem); }
/* The waiting room shows purely by body[data-waiting] (waiting.js). */
.tp-waiting { display: none; }
body[data-waiting] .tp-waiting { display: flex; }

/* --------------------------------------------------------------------------
   THEME AUDIT FIXES (2026-10-05, 13-theme browser critic pass). No theme
   scored 8+; the blockers were OUR paint bugs, not bundle defects (the two
   JS crashes are fixed in board.js/timerpi.js). Append-only per the banner
   discipline above. */


/* Stage status line: --muted is contract-certified on SURFACES; the stage
   paints it directly on --bg (win95 teal: 2.09:1 — invisible). Primary text
   + opacity keeps the hierarchy; the trailing gutter stops the corner
   join-card/fullscreen-hint cluster from landing on top of the text. */
.tp-display-status { color: var(--text); opacity: .72; min-width: 0; flex-wrap: wrap;
  padding-inline-end: clamp(12rem, 30vw, 26rem); }

/* HELD on dark TV surfaces: --warning-text is tuned for light surfaces
   (ios's readable-on-white brown is ~1.5:1 on its black-glass readout).
   Display clocks paint the bright FILL token; dashboards keep the §9c
   -text variant on their own surfaces. */
.tp-display .tp-clock[data-state="held"] { color: var(--warning); }

/* Link toasts landed over the app-bar's Theme label on every operator
   page; park them below the bar (display surfaces have no bar). */
body.app .toast-region { inset-block-start: calc(4.2rem + env(safe-area-inset-top, 0px)); }

/* Inspector paired fields collapsed to 61px fragments ("Lesli") under the
   uppercase mono labels; give each field a real floor. */
.tp-inspector .row { flex-wrap: wrap; }
.tp-inspector .row > .field { flex: 1 1 7rem; min-width: 7rem; }
.tp-inspector .row > .tp-insp-wide { flex: 3 1 12rem; }
.tp-inspector .row > .tp-insp-narrow { flex: 0 1 7rem; }

/* Audit hardening RETIRED 2026-10-05: upstream 19c1e02 gave lcars/
   death-star a visible --border and windows95 added .badge to its
   panel-header reset (both issues confirmed fixed in the rebuilt bundles). */

/* ---------------------------------------------------------------------------
   PLAN §11.2 phase 2 — audience/venue board widgets + tile animations.
   ftl tokens only; displays ALWAYS animate (owner: prefers-reduced-motion
   is honored on audience phones only, never on display pages). */

/* Audience tiles are painted in place by audtiles.js: the title sits at
   the top, the body fills the rest. Positions move by transform, so every
   change animates (2026-10-07 audience notes). */
.b-js-poll, .b-js-qa, .b-js-cloud { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.b-aud-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
[data-aud-mode="bars"] > .b-aud-body { justify-content: center; }
.b-poll-q { font-size: clamp(1rem, 2.2vw, 1.7rem); font-weight: 600; overflow-wrap: anywhere; }
.b-poll-meta { color: var(--muted, #9a9aa8); font-size: .9em; margin: .2rem 0 .6rem; }
.b-poll-opt { margin-bottom: .55rem; }
.b-poll-opt-head { display: flex; justify-content: space-between; gap: .8rem; font-size: .95em; }
.b-poll-opt.is-correct .b-poll-opt-head { color: var(--success, #22C55E); font-weight: 600; }
.b-poll-bar { margin-top: .25rem; --progress-height: .55rem; }
.b-poll-opt.is-correct .b-poll-bar { --progress-fill: var(--success); }
.b-poll-num { font-variant-numeric: tabular-nums; }
.b-poll-reveal { animation: b-poll-reveal .6s ease-out both; }
@keyframes b-poll-reveal { from { opacity: .35; transform: translateX(-.6rem); } to { opacity: 1; transform: none; } }

/* Word cloud: absolute words around the centre, no badges. */
.b-cloud { position: relative; flex: 1; min-height: 0; overflow: hidden; }
.b-cloud-word { position: absolute; left: 50%; top: 50%; white-space: nowrap; line-height: 1; font-weight: 700;
  font-size: 100px; transform-origin: 0 0; opacity: 0; color: var(--text); will-change: transform;
  transition: opacity .9s ease-out, color .6s; }
.b-cloud-word[data-tone="1"] { color: var(--accent); }
.b-cloud-word[data-tone="2"] { color: var(--muted, var(--text)); }
.b-cloud-word.is-top { color: var(--accent); }

/* Q&A / ideas wall: cards stacked by transform; the spotlight is a
   thicker border; an answered card goes green, then fades. */
.b-qa-wall { position: relative; flex: 1; min-height: 0; margin: 0; padding: 0; list-style: none; overflow: hidden; row-gap: .6rem;
  --list-border-width: 0; --list-bg: transparent; } /* the cards carry the borders */
.b-qa-item { position: absolute; top: 0; left: 0; right: 0; display: flex; gap: .8rem; align-items: baseline;
  font-size: clamp(.95rem, 1.8vw, 1.4rem); padding: .5em .8em; border: 2px solid var(--border);
  border-radius: var(--radius, .5rem); background: var(--surface-2, var(--surface));
  transition: transform .8s cubic-bezier(.2, .8, .2, 1), opacity 1s ease, border-color .4s, background-color .4s; }
.b-qa-item.is-spot { border-width: 6px; border-color: var(--accent); }
.b-qa-item.is-answered { border-color: var(--success); background: color-mix(in srgb, var(--success) 18%, var(--surface-2, var(--surface))); }
.b-qa-item.is-leaving { opacity: 0 !important; }
.b-qa-votes { min-width: 3.2em; color: var(--muted, #9a9aa8); font-size: .85em; font-variant-numeric: tabular-nums; }
.b-qa-text { overflow-wrap: anywhere; }
.b-aud-empty { margin: auto; text-align: center; }

.b-w-map .b-w-body, .b-w-joinqr .b-w-body { display: flex; flex-direction: column;
  align-items: center; justify-content: center; height: 100%; }
.b-js-map { max-width: 100%; max-height: 100%; object-fit: contain; }
.b-map-empty { text-align: center; padding: 1rem; font-size: .9em; }
.b-js-joinqr { width: min(100%, 22vh); aspect-ratio: 1; background: #fff; border-radius: 8px; }
.b-join-label { margin-top: .4rem; color: var(--muted, #9a9aa8); font-size: .9em;
  text-align: center; overflow-wrap: anywhere; }

/* Tile entrance/exit — the operator picks the mode per tile (anim/animMS). */
.b-js-poll, .b-js-qa, .b-js-cloud { --b-anim-ms: 400ms; }
.b-anim-in-fade { animation: b-anim-fade var(--b-anim-ms) ease-out; }
.b-anim-out-fade { animation: b-anim-fade var(--b-anim-ms) ease-in reverse forwards; }
.b-anim-in-slide { animation: b-anim-slide-in var(--b-anim-ms) ease-out; }
.b-anim-out-slide { animation: b-anim-slide-out var(--b-anim-ms) ease-in forwards; }
.b-anim-in-pop { animation: b-anim-pop-in var(--b-anim-ms) cubic-bezier(.2, 1.4, .4, 1); }
.b-anim-out-pop { animation: b-anim-pop-out var(--b-anim-ms) ease-in forwards; }
@keyframes b-anim-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes b-anim-slide-in { from { opacity: 0; transform: translateY(1.2rem); } to { opacity: 1; transform: none; } }
@keyframes b-anim-slide-out { from { opacity: 1; } to { opacity: 0; transform: translateY(1.2rem); } }
@keyframes b-anim-pop-in { from { opacity: 0; transform: scale(.86); } to { opacity: 1; transform: none; } }
@keyframes b-anim-pop-out { from { opacity: 1; } to { opacity: 0; transform: scale(.86); } }

/* ---------------------------------------------------------------------------
   PLAN nav pass — brand link + appbar cluster. */
a.nav-brand { color: inherit; text-decoration: none; display: inline-flex; align-items: center; }
/* Logo sizing lives here (owner: no duplicated width/height attrs) —
   the appbar logo matches the bar buttons' height. */
a.nav-brand img { height: 30px; width: auto; vertical-align: middle; }
.app-bar .nav-item { display: inline-flex; align-items: center; gap: .35rem; }

/* ---------------------------------------------------------------------------
   PLAN §11.2 capture modal + nav polish (owner feedback round). */
.tp-ready-id { font-size: 1.1rem; margin: .6rem 0 .2rem; }
.tp-dlg-msg { margin: 0 0 .6rem; overflow-wrap: anywhere; }
.tp-ready-name { background: var(--accent, #7C3AED); color: #fff;
  padding: .15rem .6rem; border-radius: 6px; letter-spacing: .06em; }

/* ---------------------------------------------------------------------------
   Layout-editor feel round (owner): template row. The drag ghost is ftl's
   .widget-placeholder, placed on the target cells. */
.b-palette-list { max-width: 60rem; }
.widget-placeholder { pointer-events: none; }

/* --- Running order table (STATUS U39–U43) -------------------------------- */
.tp-cuetable { font-size: min(.82rem, 13.2px); } /* iOS themes root at 17px: keep every column in view at 1440 (E2E #3) */
.tp-cuetable :is(th, td) { padding-inline: .25rem; vertical-align: middle; }
.tp-cuetable th { font-size: .7rem; letter-spacing: .03em; }
.tp-cuetable .tp-cue-title { min-width: 8em; }
.tp-cuetable .tp-cue-notes { color: var(--muted); font-size: inherit; }
.tp-cuetable .text-truncate { display: block; max-width: 6.7em; }
.tp-cuetable .tp-cue-notes .text-truncate { max-width: 4.9em; }
.tp-cuetable .tp-cue-rowbtns .btn { padding-inline: .2rem; }
.tp-cuetable .tp-cue-rowbtns .btn-go { padding-inline: .45rem; }

.tp-cuetable :is(.tp-cue-timer, .tp-cue-atzero, .tp-cue-kind) { white-space: nowrap; }
.tp-cuetable :is(.tp-cue-alert1, .tp-cue-alert2) { white-space: nowrap; }
/* The drag handle (U40): the grip and number together. */
.tp-cue-pos .tp-grip { margin-inline-end: .3rem; opacity: .55; }
.tp-cue-pos:hover .tp-grip { opacity: 1; }
/* The add row (U39) is an ftl tr.is-editing; same height as the cue rows
   (owner: GO-button height). */
.tp-cuetable tfoot > tr { height: 3rem; }
/* Compact add-row controls (beat ftl's editing-cell padding): they set the
   column widths, and the table must fit 1440 px beside the Current cue. */
#cuelist tfoot td > :is(.input, .select) { padding-inline: .3rem; font-size: min(.78rem, 12.5px); }
#cuelist tfoot td > .select { padding-inline-end: .95rem; background-position: right .2rem center; font-size: min(.74rem, 11.8px); }
#cuelist tfoot .tp-cue-kind .select { width: 6.2em; }
#cuelist tfoot .tp-cue-timer .select { width: 7.4em; }
#cuelist tfoot .tp-cue-atzero .select { width: 5.6em; }
#cuelist tfoot .tp-cue-dur .input { width: 4.2em; }
.tp-alert-field { display: flex; align-items: center; gap: .25rem; }
.tp-alert-field .input { width: 3.6em; }
/* Alert colour dots (U30): a swatch the operator clicks to change. */
.tp-alert-dot {
  display: inline-block; inline-size: .85rem; block-size: .85rem; flex: none;
  margin-inline-end: .35rem; vertical-align: -.1em; border-radius: 50%;
  background: var(--swatch, var(--accent)); border: 1px solid rgb(128 128 128 / .5);
  padding: 0; cursor: pointer;
}
button.tp-alert-dot { inline-size: 1.1rem; block-size: 1.1rem; margin: 0; }
.tp-swatches { flex-wrap: wrap; max-width: 15rem; }
.tp-swatch-custom.is-selected { outline: 2px solid var(--text); outline-offset: 2px; }
/* Breaks wear their own accent (U43): sessions keep the theme accent. */
.tp-cuetable tr.tp-row-break { --tp-row-accent: var(--accent-2, var(--warning)); background: color-mix(in srgb, var(--tp-row-accent) 7%, transparent); }
.tp-cuetable tr.tp-row-break > td:first-child { box-shadow: inset 3px 0 0 var(--tp-row-accent); }
/* Row colour from the cue editor: the same edge + tint, in the cue's colour. */
.tp-cuetable tr.tp-row-color { background: color-mix(in srgb, var(--tp-row-accent) 10%, transparent); }
.tp-cuetable tr.tp-row-color > td:first-child { box-shadow: inset 4px 0 0 var(--tp-row-accent); }
.tp-badge-break { --badge-bg: color-mix(in srgb, var(--accent-2, var(--warning)) 22%, var(--surface)); --badge-fg: var(--accent-2, var(--warning-text, var(--warning))); }

/* Flash (2026-10-07): the operator's Flash blinks presenter timers; an
   alert set to flash blinks the timer while it is on. */
body.b-flash .b-js-clock, .b-js-clock.is-flash { animation: b-blink 1s steps(1) infinite; }
@keyframes b-blink { 50% { opacity: .12; } }

/* Edit modal: the row colour picker spans the dialog. */
.tp-insp-rowcolor .swatches { flex-wrap: wrap; } /* tablet/phone: wrap, never clip */
.tp-insp-rowcolor .swatches > * { flex-shrink: 0; }
.tp-insp-flash { align-self: end; }
