/* Your Room TV board, second build (/tv2).

   One DOM, three geometries. OBS shows this as a browser source on the 4K TV
   as a portrait strip (A, 1280x2160) or a wide strip (B, 3840x720), and
   the owner opens it on a laptop (C, 1920x1080). The aspect-ratio media queries
   at the bottom pick the grid; every figure sizes off its own cell with
   container query units (cqi) plus a px floor per geometry, never off vh,
   because the three canvases have nothing in common vertically.

   Design system is the portal's dark mode, verbatim tokens (portal/app/index.css
   and tv.css). The rules that keep it reading as an instrument, not a
   scoreboard:
   - cards are plain: --surface, 1px --line, 14px radius. No tint, no shadow,
     no gradient wash. Identity comes from the figure, not the panel.
   - two accents carry all the meaning. ORANGE --money is tokens, dollars,
     target, tip and spender amounts. BLUE --audience is viewers, followers and
     placement. Every other number is ink. Never add a third accent.
   - --gold is celebration only: jackpot rows and the target shimmer.
   - status colours (--good/--warn/--critical) appear only on chips, health
     dots, the private-show card, the month pace figure and stale labels.
     Never on a data series.
   - chips are the portal badge: soft status fill, 1px border, 8px radius.
   - figures are JetBrains Mono, tabular, weight 600. Labels are Instrument
     Sans, uppercase, 600, .07em, --ink-3. Weights stay between 500 and 700.
   - the chart carries two series and one axis: viewers #4a9bdf as the line,
     tokens #c67c18 as the bars, and the axis labels the viewers scale. Never
     add a series on a second hidden scale, an unlabelled axis is a lie. The
     line is smoothed and drawn monotone cubic (see tv2.js): the feed's raw
     samples are a staircase and the angle is what this chart is read for.
   - show mode (main[data-show]) is a different board, not a restyle: the
     clock band, the chart and the month zone drop out, a show card takes the
     chart's place, the two list zones carry the ticket audience or tonight's
     privates, and the room card stays so placement is readable while tickets
     sell. One grid template per geometry, at the bottom of the file.
   - the room card's hierarchy is placement first. The two cbhours ranks are
     the big figures, the viewer count is one line at --sec above them, and
     the follower total lives in the footer.

   Freshness is one pattern: an element carrying data-fresh="fresh|aged|stale|
   last" (set by stamp() in tv2.js) and a small.age inside it that prints the
   age, "feed stale · 9m" in warn, or "last stream". Stale dims the figures.

   Vertical budgets close to the pixel; the sums are in the comments by each
   media query. If a band is changed, re-add the column. */

:root {
  color-scheme: dark;
  font-family: "Instrument Sans", Inter, ui-sans-serif, system-ui, sans-serif;

  --plane: #0e0f12;
  --surface: #17181c;
  --surface-2: #1c1e23;
  --surface-3: #23252b;
  --line: #262931;
  --line-strong: #343841;

  --ink: #eceef2;
  --ink-2: #a3a7b0;
  --ink-3: #71757e;

  --money: #ff8a4c;
  --money-fill: #ef6c34;
  --money-soft: rgba(239, 108, 52, 0.16);
  --audience: #4a9bdf;
  --audience-soft: rgba(74, 155, 223, 0.14);

  --good: #3fcf3f;
  --good-soft: rgba(12, 163, 12, 0.16);
  --warn: #f0b429;
  --warn-soft: rgba(250, 178, 25, 0.16);
  --critical: #f06a6a;
  --critical-soft: rgba(208, 59, 59, 0.18);
  --gold: #f2c14e;

  --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --radius: 14px;
  --radius-sm: 8px;
  --gap: 16px;

  /* Type floors. C (laptop) values here; A and B override below. */
  --lbl: 20px;       /* labels, chips, notes */
  --meta: 20px;      /* row detail, footers, ages */
  --lead: 24px;      /* row primary text */
  --rowfig: 28px;    /* row amounts */
  --sec: 40px;       /* secondary figures */
  --hero-min: 96px;  /* hero figure floor */
  --ribbon: 20px;
  --pad: 18px;
  --head-h: 28px;
  --chip-h: 28px;
  --edge: 4px;

  background: var(--plane);
  color: var(--ink);
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; }
body { background: var(--plane); -webkit-font-smoothing: antialiased; }

main {
  width: 100vw; height: 100vh; padding: var(--gap); display: grid; gap: var(--gap);
}

/* ---- primitives ------------------------------------------------------- */
.fig, .total, .month-line b, .target-meta b, .foot-lead, .chip, .now-block b, .viewer-color, .chart-axis span {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  font-weight: 600;
}
.fig { display: block; line-height: .95; letter-spacing: -.03em; white-space: nowrap; color: var(--ink); }
.fig.money { color: var(--money); }
.fig.audience { color: var(--audience); }
.lbl {
  display: block; color: var(--ink-3); font-size: var(--lbl); font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2;
}
.lbl b { color: var(--ink-2); }
.age { color: var(--ink-3); font-size: var(--meta); font-weight: 500; letter-spacing: 0; text-transform: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.age:empty { display: none; }
.sub { color: var(--ink-3); font-size: var(--meta); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
.sub b { color: var(--ink-2); font-family: var(--mono); font-weight: 600; }

/* A figure and its label. container-type so the figure can size off the cell's
   own width with cqi; K = 94 / (0.62 x max chars) so the longest expected value
   fills the cell without clipping. */
.cell { container-type: inline-size; min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: center; gap: 6px; }

/* Freshness. stamp() writes data-fresh on a cell or a whole zone. Stale dims
   every figure inside; a cell stamped fresh inside a stale zone keeps its
   colour (the viewers cell when the backend number is the fresh one). */
[data-fresh="stale"] .fig { color: var(--ink-3); }
[data-fresh="stale"] .age, [data-fresh="stale"] > .zone-head .age { color: var(--warn); }
[data-fresh="stale"] .cell[data-fresh="fresh"] .fig.audience { color: var(--audience); }
[data-fresh="stale"] .cell[data-fresh="fresh"] .age { color: var(--ink-3); }

.zone {
  min-width: 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; gap: 10px;
  padding: var(--pad); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
}
.zone-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: var(--head-h); }
.zone-head .lbl { flex: 0 0 auto; }
.zone-head .age { flex: 0 1 auto; }
.zone-foot { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; min-height: calc(var(--meta) * 1.3); color: var(--ink-3); font-size: var(--meta); font-weight: 500; white-space: nowrap; overflow: hidden; }
.zone-foot b { color: var(--ink-2); font-family: var(--mono); font-weight: 600; }
.zone-foot .money { color: var(--money); }
.zone-foot .bad { color: var(--warn); }
.zone-foot > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.foot-lead { color: var(--ink-2); }
.foot-meta { flex: 1 1 auto; }
.empty { display: grid; place-items: center; height: 100%; color: var(--ink-3); font-size: var(--lead); font-weight: 500; }

/* Chips: the portal badge. */
.chip { display: inline-flex; align-items: center; height: var(--chip-h); padding: 0 12px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-3); color: var(--ink-2); font-size: var(--lbl); letter-spacing: -.01em; white-space: nowrap; }
.chip.boost, .chip.fresh { color: var(--good); border-color: rgba(63,207,63,.3); background: var(--good-soft); }
.chip.warm { color: var(--warn); border-color: rgba(240,180,41,.3); background: var(--warn-soft); }
.chip.cooling, .chip.cold { color: var(--critical); border-color: rgba(240,106,106,.3); background: var(--critical-soft); }
.chip.idle, .chip.complete { color: var(--ink-3); }

/* ---- the one row treatment ---------------------------------------------
   Tips, spenders and the site rows share it. Single-line puts lead, meta and figure
   across; two-line (class on the list) stacks lead over meta with the figure
   centred against both, for cards too narrow for a username and its detail on
   one line. .mark is the newest tip or spender rank 1, .gold a jackpot, .quiet
   a spender who is only watching. */
.rows { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr); grid-auto-rows: minmax(0, 1fr); }
.row { min-width: 0; min-height: 0; display: grid; align-items: center; column-gap: 12px; padding: 0 12px; border-bottom: 1px solid var(--line); border-radius: var(--radius-sm);
  grid-template-columns: fit-content(58%) minmax(0, 1fr) auto; grid-template-areas: "lead meta fig"; }
.row:last-child { border-bottom: 0; }
.row.blank { border-bottom-color: transparent; }
.rows.two-line .row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "lead fig" "meta fig"; row-gap: 2px; align-content: center; }
.row .lead { grid-area: lead; min-width: 0; color: var(--ink); font-size: var(--lead); font-weight: 600; line-height: 1.15; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .meta { grid-area: meta; min-width: 0; color: var(--ink-3); font-size: var(--meta); font-weight: 500; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .fig { grid-area: fig; justify-self: end; align-self: center; color: var(--money); font-size: var(--rowfig); line-height: 1; letter-spacing: -.02em; }
.row.mark { box-shadow: inset var(--edge) 0 0 var(--money); }
.row.gold { box-shadow: inset var(--edge) 0 0 var(--gold); }
.row.gold .lead { color: var(--gold); }
.row.quiet .fig { color: var(--ink-3); font-size: var(--meta); font-weight: 500; }
/* .cold is a spender who used to pay and stopped, standing in the room right
   now. Amber, not money-orange and not red: nothing has gone wrong, there is
   just a name here worth saying out loud. */
.row.cold { box-shadow: inset var(--edge) 0 0 var(--warn); }
.row.cold .lead { color: var(--warn); }
.row.cold .fig { color: var(--warn); font-size: var(--meta); font-weight: 600; }
.row.enter { animation: row-in .5s cubic-bezier(.2,.9,.3,1.15) both; }

/* ---- ribbon ------------------------------------------------------------- */
.ribbon { container-type: inline-size; min-width: 0; display: grid; align-items: center; gap: 4px 24px; padding: 0 6px; grid-template-columns: auto 1fr auto; grid-template-areas: "brand health meta"; }
.ribbon-line { display: contents; }
.brand { grid-area: brand; display: flex; align-items: center; gap: 14px; font-size: var(--ribbon); font-weight: 700; white-space: nowrap; }
.brand strong { color: var(--money); font-weight: 700; text-transform: lowercase; letter-spacing: -.01em; }
.brand #status { color: var(--ink-3); font-size: var(--lbl); font-weight: 600; letter-spacing: .1em; text-transform: uppercase; }
.live-dot { width: .6em; height: .6em; border-radius: 50%; background: var(--ink-3); }
body.is-live .live-dot { background: var(--good); box-shadow: 0 0 0 .25em var(--good-soft); animation: dot-breathe 2.8s ease-in-out infinite; }
body.is-live #status { color: var(--good); }
.ribbon-meta { grid-area: meta; min-width: 0; display: flex; justify-content: flex-end; gap: 28px; color: var(--ink-3); font-size: var(--lbl); font-weight: 500; white-space: nowrap; overflow: hidden; }
.ribbon-meta .clock { color: var(--ink); font-family: var(--mono); font-size: var(--ribbon); font-weight: 600; letter-spacing: .02em; font-variant-numeric: tabular-nums; }
.ribbon-meta .clock .date { color: var(--ink-2); font-family: "Instrument Sans", Inter, ui-sans-serif, system-ui, sans-serif; font-weight: 500; letter-spacing: 0; margin-right: .3em; }
.health-bar { grid-area: health; justify-self: center; min-width: 0; display: flex; align-items: center; gap: 22px; }
.health { display: inline-flex; align-items: center; gap: 8px; color: var(--ink-3); font-size: var(--lbl); font-weight: 600; letter-spacing: .07em; text-transform: uppercase; white-space: nowrap; }
.health i { width: .55em; height: .55em; border-radius: 50%; background: var(--line-strong); }
/* Every pill says its name and one short fact (the owner, 2026-09-19: "the green
   dots are great but can there be some text telling me some info"). The dot
   still carries the state colour; the name stays dim and the value reads in
   ink so the eye lands on the number, not the word. A warn or err pill takes
   its colour on the whole thing. */
.health[hidden] { display: none; }
/* The toys pill never says "toys": "lush 82% · domi 64%" says what it is. At
   16:9 seven pills still overrun the 1250px between the brand and the meta
   (1350 worst case), so on a toys night the meta drops "stream #N". */
#health-toys em { display: none; }
@media (min-aspect-ratio: 1/1) and (max-aspect-ratio: 2/1) {
  .ribbon:has(#health-toys:not([hidden])) #context { display: none; }
}
.health em { font-style: normal; }
.health b { font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.health.warn b, .health.err b { color: inherit; }
.health.ok { color: var(--ink-2); } .health.ok i { background: var(--good); }
.health.warn { color: var(--warn); } .health.warn i { background: var(--warn); }
.health.err { color: var(--critical); } .health.err i { background: var(--critical); }
/* The mic and the toys are not system health, they are the two things read
   from the bed mid-show, and as dim words they blended into the row (the owner,
   2026-10-03: "its not very obvious kinda just blends in same with the lush
   info"). So they are solid blocks at the front of the bar: mic green, toys
   blue, amber when a toy drops, and a muted mic is red and pulses. */
.health.block { order: -1; background: var(--good); color: #0b0b0c; padding: .12em .6em; border-radius: .3em; font-weight: 700; }
.health.block i { display: none; }
.health.block b { color: inherit; font-weight: 700; }
#health-toys.block { background: var(--audience); }
/* Something switched off in the room app (wheel, menu, goals, quiet): amber, and only there while it is. */
#health-app.block { background: var(--warn); }
#health-app b:empty { display: none; }
/* A third block does not fit beside six named pills at any geometry (16:9 ran into the brand and
   the clock), so while the app pill is up the healthy pills are dots only. */
.health-bar:has(#health-app:not([hidden])) .health.ok:not(.block) em { display: none; }
#health-toys.block.warn { background: var(--warn); }
#health-toys.block:not(.ok):not(.warn) { background: var(--line-strong); color: var(--ink); }
.health.block.shout { background: var(--critical); animation: mic-shout 1.2s ease-in-out infinite; }
/* Two blocks plus six pills with values overrun every geometry (16:9 ran into
   the brand and the clock), so while a block is up the healthy pills are a dot
   and a name. A warn or err pill keeps its words. */
.health-bar:has(.block:not([hidden])) .health.ok:not(.block) b { display: none; }
@keyframes mic-shout { 50% { box-shadow: 0 0 0 .3em var(--critical-soft); filter: brightness(1.25); } }
/* The overlay bars (PIM-297, 13 Oct 2026): "keep it going" and the title goal as blocks at the front of the row, with
   the numbers only the owner and the model see. Pink for health (red under 30 s, grey once it has ended), purple for the goal.
   The word before the value is the heart, or the goal's title; the labels are not repeated by the pills' dots. */
.health.block.keep { background: #F20C7F; color: #fff; }
.health.block.keep.low { background: var(--critical); animation: mic-shout 1.2s ease-in-out infinite; }
.health.block.keep.done { background: #64748B; animation: none; }
.health.block.goalbar { background: #7C3AED; color: #fff; }
.health.block.goalbar.done { background: #0F766E; }
.health.block.keep em, .health.block.goalbar em { display: inline; margin-right: .1em; }
.health.block.goalbar b:empty, .health.block.keep b:empty { display: none; }
.health.block.keep b, .health.block.goalbar b { display: inline !important; }
@media (min-aspect-ratio: 1/1) and (max-aspect-ratio: 2/1) {
  .ribbon:has(#health-keep:not([hidden])) #context, .ribbon:has(#health-goal:not([hidden])) #context { display: none; }
}
/* Two more blocks overrun the 16:9 and portrait rows (the toys pill alone leaves about 110px at 16:9), so while a bars
   block is up the healthy system pills are dots, as they are while the app block is up. A warn or err pill keeps its words. */
.health-bar:has(#health-keep:not([hidden])) .health.ok:not(.block) em,
.health-bar:has(#health-goal:not([hidden])) .health.ok:not(.block) em { display: none; }


/* ---- money -------------------------------------------------------------- */
.money-grid { flex: 1 1 auto; min-height: 0; display: grid; gap: var(--gap); align-items: center; }
.cell.hero .fig { font-size: clamp(var(--hero-min), 25cqi, 240px); }
.money-side { min-width: 0; min-height: 0; display: grid; gap: 12px var(--gap); grid-template-columns: 1fr 1fr; align-content: center; }
.money-side .cell:first-child { grid-column: 1 / -1; }
.fig.dollars { font-size: clamp(var(--sec), 17cqi, 72px); }
.fig.rate { color: var(--ink-2); font-size: clamp(var(--sec), 25cqi, 56px); }
.target { flex: 0 0 auto; display: flex; flex-direction: column; gap: 8px; }
.target-line { color: var(--ink); font-size: var(--lead); font-weight: 600; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.target-track { height: 8px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.target-track i { display: block; width: 0; height: 100%; border-radius: inherit; background: var(--money); transition: width .35s ease; }
.target-track.done i { background: linear-gradient(100deg, var(--money-fill) 20%, var(--gold) 40%, var(--money) 60%, var(--money-fill) 80%); background-size: 200% 100%; animation: track-shine 2.4s linear infinite; }
.target-meta { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 4px 16px; min-width: 0; }
.target-meta .lbl { flex: 0 0 auto; }
/* Where the target bar's money came from, beside the target itself. It shares
   the month line's meta styling but stays on row one, so the bar, the split
   that fills it and the month it feeds read top to bottom. */
.target-split { flex: 0 1 auto; min-width: 0; text-align: right; color: var(--ink-3); font-size: var(--meta); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.month-line { flex: 1 1 100%; min-width: 0; text-align: left; color: var(--ink-3); font-size: var(--meta); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.month-line b { color: var(--ink-2); }
.month-line b.good { color: var(--good); }
.month-line b.bad { color: var(--critical); }

/* ---- time --------------------------------------------------------------- */
/* A thin band on A and C (label, clock and note on one line), a column on B.
   In show mode the band is gone and the show card carries its own clock. */
.zone.time { justify-content: center; }
.time-cell { flex-direction: row; align-items: baseline; gap: 24px; }
.time-cell .lbl { flex: 0 0 auto; }
.time-cell .fig { flex: 0 0 auto; font-size: clamp(var(--sec), 6.5cqi, 72px); }
/* The notes stack on the right: today against the goal, then the show plan.
   Alone, the first note sits on the figure's baseline as it always did. */
.time-notes { flex: 1 1 auto; min-width: 0; align-self: center; display: flex; flex-direction: column; gap: 3px; text-align: right; }
.time-notes:has(#show-plan[hidden]) { align-self: baseline; }
.time-cell .sub { flex: 0 0 auto; text-align: right; }
.time-cell .sub b.good { color: var(--good); }
.time-cell .sub b.warn { color: var(--warn); }
/* Words only the 16:9 layout drops: its band is the narrowest for its type
   size (the portrait strip is wider, the wide strip has .b-hide). */
@media (min-aspect-ratio: 1001/1000) and (max-aspect-ratio: 2999/1000) { .c-hide { display: none; } }
/* On air today against the five hour goal: a hairline meter along the foot of
   the band. Grey, because hours are neither money nor audience; green once
   the goal is reached. */
/* The line is lifted 6px so the meter clears the baseline; the portrait
   figure is capped at 72px (was 84) for the same reason. */
.zone.time { position: relative; padding-top: calc(var(--pad) - 6px); padding-bottom: calc(var(--pad) + 6px); }
.air-track { position: absolute; left: var(--pad); right: var(--pad); bottom: 9px; height: 6px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.air-track i { display: block; width: 0; height: 100%; border-radius: inherit; background: var(--ink-2); transition: width .35s ease; }
.air-track.done i { background: var(--good); }

/* ---- room --------------------------------------------------------------- */
/* The two cbhours ranks are the heroes. the owner reads this board from the sofa
   and the placement is what he acts on: the raw viewer count only says how big
   the room is, the rank says whether Chaturbate is pushing it. So viewers is a
   single line at --sec, the ranks are the big figures under it, and the
   follower total joined the footer.

   Rank figures are 2 to 4 characters ("#3", "#317"); anything longer gets
   .narrow from tv2.js, which is the only way to be char-aware here. */
/* Viewers, couples and site as one row of three figures. They were two hero
   ranks under a viewers line until the ticket call moved in beneath them
   (2026-09-16); the call is what the card is for now, and the placement pill
   repeats the ranks anyway. */
.room-figs { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gap); align-items: center; }
.room-viewers-line { min-width: 0; min-height: 0; }
.room-viewers-line .cell { flex-direction: column; align-items: flex-start; gap: 6px; }
.room-ranks { display: contents; }
.room-figs .fig { font-size: clamp(var(--sec), 22cqi, 96px); }
.room-figs .fig.narrow { font-size: clamp(var(--sec), 16cqi, 80px); }
.fig.sec { font-size: clamp(var(--sec), 17cqi, 72px); }
/* Followers and the cbhours line. Sized a step above every other footer on the
   board and at full ink on the figures: the owner reads these two from the sofa
   and at --meta in --ink-3 they were the only numbers up here he had to walk
   over to (2026-09-16). */
.room-foot { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 2px 18px; color: var(--ink-2); font-size: var(--lead); font-weight: 500; }
.room-foot > span { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.room-foot b { color: var(--ink); font-family: var(--mono); font-weight: 600; }
/* The freshness note the cbhours line carries when a ticket show has unlisted
   the room and the ranks are frozen. Quieter than the ranks beside it. */
.room-foot .src { color: var(--ink-3); font-weight: 500; font-size: var(--meta); }
/* The room app's goal: its own full-width line in the foot, the text over a
   bar (PIM-122, 11 Oct 2026). Orange, because goal progress is tokens. Green
   once the goal is met, grey while goals are switched off. */
.room-foot #goal-line { flex: 1 1 100%; display: flex; flex-direction: column; gap: 6px; }
.goal-text { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.goal-track { flex: none; height: 10px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.goal-track i { display: block; width: 0; height: 100%; border-radius: inherit; background: var(--money); transition: width .35s ease; }
.goal-track.done i { background: var(--good); }
.goal-track.off i { background: var(--ink-3); }

/* Ticket show call, under the ranks. One chip carries the verdict, one line
   the numbers (rooms like this sold N to M, sell 15 to 20 min, or the live
   sale's count against what past sales had by the same minute), then the
   signals as small pills coloured by state. The text is one ellipsized line
   beside the chip, never a wrapped paragraph: on the wide strip a long
   "waiting on" reason wrapped into four lines beside the chip and crushed
   the ranks and the viewers label into each other (2026-09-16). The reason
   repeats what the red pills already say, so tv2.js prints the sold range
   first and lets the reason be the part that gets clipped. flex-basis 0 is
   what keeps it on the chip's line; B sets it back to auto so a long line
   drops under the chip and gets the card's full width instead. */
.ticket-call { flex: 0 0 auto; display: flex; flex-direction: column; gap: 6px; padding-top: 8px; border-top: 1px solid var(--line); min-width: 0; }
.ticket-call[hidden] { display: none; }
.tc-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; min-width: 0; }
.tc-line .chip { flex: 0 0 auto; }
.tc-text { flex: 1 1 0; min-width: 0; color: var(--ink-2); font-size: var(--meta); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tc-text b { color: var(--ink); font-family: var(--mono); font-weight: 600; }
.tc-lights { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.tc-light { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 1px 8px; border-radius: var(--radius-sm); border: 1px solid var(--line); background: var(--surface-3); color: var(--ink-3); font-family: var(--mono); font-size: var(--meta); font-weight: 500; line-height: 1.35; }
.tc-light[data-state="good"] { color: var(--good); border-color: rgba(63,207,63,.3); background: var(--good-soft); }
.tc-light[data-state="ok"] { color: var(--warn); border-color: rgba(240,180,41,.3); background: var(--warn-soft); }
.tc-light[data-state="low"] { color: var(--critical); border-color: rgba(240,106,106,.3); background: var(--critical-soft); }
main[data-show="private"]:not([data-show-done]) .ticket-call { display: none; }
/* ---- show mode: ticket + private ----------------------------------------
   main[data-show] regrids the board (templates per geometry at the bottom).
   The show card is the one place the show is described: phase chip, a hero,
   three secondaries, a title line and a foot. Ticket figures are --money like
   every other token figure. A private hero is the CLOCK in --critical, and its
   token figures are ESTIMATES, so they are deliberately not --money: ink, with
   "est" in the label, so they never look like a measurement sitting next to
   the real tonight total in the money card. */
.zone.show { display: none; }
main[data-show] .zone.show { display: flex; }
main[data-show] .zone.time, main[data-show] .zone.chart, main[data-show] .zone.plan { display: none; }
main[data-show] .pair-time, main[data-show] .pair-lists { display: contents; }
main[data-show] .ribbon { grid-area: ribbon; }
main[data-show] .zone.money { grid-area: money; }
main[data-show] .zone.show { grid-area: show; }
main[data-show] .zone.room { grid-area: room; }
main[data-show] .zone.tips { grid-area: list1; }
main[data-show] .zone.spenders { grid-area: list2; }
main[data-show="private"] .zone.spenders { display: none; }
.show-grid { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(0, 1.5fr) repeat(3, minmax(0, 1fr)); gap: var(--gap); align-items: stretch; }
.show-grid .cell { min-width: 0; }
.show-hero .fig { font-size: clamp(var(--hero-min), 30cqi, 200px); }
.show-hero .fig.narrow { font-size: clamp(var(--sec), 20cqi, 160px); }
.show-grid .fig.sec { font-size: clamp(var(--sec), 20cqi, 88px); }
.show-grid .fig.est { color: var(--ink); }
.show-grid .fig.dim { color: var(--ink-2); }
.show-title { flex: 0 0 auto; min-width: 0; color: var(--ink); font-size: var(--lead); font-weight: 600; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.show-title b { color: var(--ink-2); font-family: var(--mono); font-weight: 600; }
.show-title .est, .show-foot .est { color: var(--ink-3); }
main[data-show="ticket"] .zone.show { border-color: var(--money); }
main[data-show="ticket"] .show-hero .fig, main[data-show="ticket"] .show-grid .fig.money { color: var(--money); }
main[data-show="private"] .zone.show { border-color: var(--critical); }
main[data-show="private"] .show-hero .fig, main[data-show="private"] .show-hero .lbl { color: var(--critical); }
/* The private hero is a clock, up to seven characters ("1:02:45"), so it runs
   narrower than the ticket count. */
main[data-show="private"] .show-hero .fig { font-size: clamp(var(--sec), 20cqi, 160px); }
/* A finished show holds its result for three minutes before the board reverts,
   so a glance after the fact still shows how it went. Grey, it is over. */
main[data-show][data-show-done] .zone.show { border-color: var(--line); }
main[data-show][data-show-done] .show-hero .fig, main[data-show][data-show-done] .show-hero .lbl, main[data-show][data-show-done] .show-grid .fig { color: var(--ink-2); }
/* The room card during a private: Chaturbate unlists the room for the whole
   thing, so both ranks are dead and go. Viewers becomes the figure, it is the
   people waiting in the chat room for you to come back. */
/* The room card during the hidden show itself: the room is unlisted, both ranks
   are dead, and the owner would rather see who is watching. The card leaves the
   board and the in-room list takes its column, running two names across. Sales
   keep the card, because the ranks are what sells tickets. */
main[data-show="ticket"][data-ticket-live] .zone.room { display: none; }
.rows.two-col { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); column-gap: var(--gap); }
main[data-show="private"]:not([data-show-done]) .room-ranks, main[data-show="private"]:not([data-show-done]) #cbhours-line { display: none; }
main[data-show="private"]:not([data-show-done]) .room-viewers-line { grid-column: 1 / -1; align-self: stretch; }
main[data-show="private"]:not([data-show-done]) .room-viewers-line .cell { flex-direction: column; align-items: flex-start; }
main[data-show="private"]:not([data-show-done]) .room-viewers-line .cell .lbl { flex: 0 0 auto; }
main[data-show="private"]:not([data-show-done]) .room-figs .fig.sec { font-size: clamp(var(--sec), 30cqi, 132px); }
/* Rows in the privates list: estimates, so ink rather than money. */
.row.est .fig { color: var(--ink-2); }
.row.est .lead b { color: var(--ink-3); font-weight: 500; }

.lbl .gain { color: var(--good); font-family: var(--mono); }
.lbl .gain.down { color: var(--critical); }
.lbl .src { color: var(--ink-3); text-transform: none; letter-spacing: 0; font-weight: 500; }

/* ---- chart -------------------------------------------------------------- */
.chart-head { flex-wrap: wrap; row-gap: 6px; }
.legend { min-width: 0; display: flex; flex-wrap: wrap; gap: 6px 18px; color: var(--ink-3); font-size: var(--lbl); font-weight: 600; letter-spacing: .07em; text-transform: uppercase; }
.legend span { white-space: nowrap; }
.legend span::before { content: ""; display: inline-block; width: 1.1em; height: .28em; margin-right: .45em; vertical-align: middle; border-radius: 2px; }
.viewer-key::before { background: #4a9bdf; }
.token-key::before { background: #c67c18; }
.pink-key::before { background: #c775ae; }
.purple-key::before { background: #733fd8; }
.chips { display: flex; align-items: center; gap: 10px; }
.chart-wrap { flex: 1 1 auto; min-height: 0; position: relative; border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; background: var(--plane); }
#performance-chart { display: block; width: 100%; height: 100%; }
/* Show bands sit under everything on the plot: a private unlists the room, so
   the viewer line falls to nothing for its whole length. The band says why. */
.show-band { fill: var(--critical); opacity: .12; }
.show-band.ticket { fill: var(--money); opacity: .10; }
#chart-grid line { stroke: var(--line); stroke-width: 1; }
/* Axis numbers are HTML over the plot: the SVG is preserveAspectRatio="none",
   which squashes any <text> inside it. Positioned off the same ratios as the
   grid lines, on the left, where the line spends the early part of a stream
   down at the floor. Each one carries a plate in the plot colour: the viewers
   line can cross any of them and a dim grey number on a bright blue stroke is
   unreadable. The top one starts below the now pill. */
.chart-axis { position: absolute; inset: 0; pointer-events: none; }
.chart-axis span { position: absolute; left: 8px; transform: translateY(-100%); padding: 0 6px 2px; border-radius: 4px; background: var(--plane); color: var(--ink-3); font-size: var(--meta); font-weight: 500; }
#token-bars rect { fill: rgba(198,124,24,.62); }
.line { fill: none; stroke: #4a9bdf; stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.area { fill: rgba(74,155,223,.1); }
/* Pink and purple names get their own plot under the viewers, same time axis,
   own scale on its own axis: 20 names against 20,000 viewers is a flat line on
   the floor of a shared scale, and a hidden second scale is what got these
   lines pulled on 2026-09-04. Small multiple, never dual axis. */
.tipper-wrap { flex: 0 0 30%; }
#tipper-chart { display: block; width: 100%; height: 100%; }
#tipper-grid line { stroke: var(--line); stroke-width: 1; }
.tier-line { fill: none; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.tier-line.pink { stroke: #c775ae; }
.tier-line.purple { stroke: #733fd8; }
.chart-empty { position: absolute; inset: 0; display: grid; place-items: center; color: var(--ink-3); font-size: var(--lead); font-weight: 500; }
.chart-empty.hidden { display: none; }
/* Top left. The line ends at the right edge and climbs, so a pill parked there
   covers the one point on the chart that is current. */
.now-pill { position: absolute; z-index: 1; top: 10px; left: 10px; display: none; align-items: center; gap: 14px; padding: 6px 14px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: rgba(23,24,28,.92); }
body.is-live .now-pill { display: flex; }
.now-block { display: inline-flex; align-items: baseline; gap: 8px; }
.now-block b { color: var(--audience); font-size: var(--rowfig); line-height: 1; letter-spacing: -.02em; }
.now-block small { color: var(--ink-3); font-size: var(--lbl); font-weight: 600; letter-spacing: .07em; text-transform: uppercase; }
.viewer-colors { display: inline-flex; align-items: center; gap: 8px; }
.viewer-colors:not(:empty) { padding-left: 14px; border-left: 1px solid var(--line); }
.viewer-color { display: inline-flex; align-items: baseline; gap: 8px; color: var(--ink-2); font-size: var(--meta); }
.viewer-color small { color: var(--ink-3); font-size: var(--lbl); font-weight: 600; letter-spacing: .07em; text-transform: uppercase; }
.now-pill .age { padding-left: 14px; border-left: 1px solid var(--line); }
.now-pill[data-fresh="stale"] .now-block b { color: var(--ink-3); }

/* ---- the month and the site ----------------------------------------------
   Took the socials' place on 4 Oct 2026. Two halves in one card: hours on air
   this month over a strip of one cell per cam day, and how the couples page
   as a whole is doing (rival ticket sales until 5 Oct 2026). Stacked on B and
   C, side by side on A.
   Hours are grey like the daily meter (neither money nor audience); a cell
   goes green only for a night that hit five hours. Site figures are ink, not
   orange: it is not our money. Our own room is green over the middle room and
   yellow under it. */
.zone.plan { gap: 14px; }
.plan-half { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 10px; }
#month-air { flex: 0 0 auto; }
#site-half { flex: 1 1 auto; border-top: 1px solid var(--line); padding-top: 12px; }
.plan-half .age b { color: var(--ink-2); font-family: var(--mono); font-weight: 600; }
.ma-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; min-width: 0; }
.ma-line .fig { font-size: var(--sec); }
.ma-line .sub { flex: 0 0 auto; }
.ma-line .ma-need { margin-left: auto; }
.ma-line .sub b.good { color: var(--good); }
.ma-line .sub b.warn { color: var(--warn); }
.ma-days { flex: 0 0 auto; height: 40px; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 3px; }
.ma-days .d { min-width: 0; display: flex; align-items: flex-end; border-radius: 3px; background: var(--surface-3); overflow: hidden; }
.ma-days .d i { display: block; width: 100%; background: var(--ink-3); }
.ma-days .d.hit i { background: var(--good); }
.ma-days .d.off { background: none; box-shadow: inset 0 -3px 0 var(--line-strong); }
.ma-days .d.next { background: none; box-shadow: inset 0 -3px 0 var(--line); }
.ma-days .d.today { box-shadow: inset 0 0 0 2px var(--ink-2); }
.row.site .fig { color: var(--ink); }
/* "18 of 29": the word is small so the two counts read as one figure. */
.row.site .fig i { font-style: normal; font-weight: 500; font-size: .6em; margin: 0 .3em; opacity: .7; }
.row.site.under { box-shadow: inset var(--edge) 0 0 var(--warn); }
.row.site.under .fig { color: var(--warn); }
.row.site.over .fig { color: var(--good); }
#site-half[data-state="quiet"] #site-word { color: var(--warn); }
#site-half[data-state="busy"] #site-word { color: var(--good); }
#site-half[data-fresh="stale"] .lead, #site-half[data-fresh="stale"] #site-word { color: var(--ink-3); }

/* ---- fun layer ---------------------------------------------------------- */
@keyframes dot-breathe {
  0%, 100% { box-shadow: 0 0 0 .25em var(--good-soft); }
  50% { box-shadow: 0 0 0 .45em rgba(63,207,63,.26); }
}
@keyframes row-in {
  from { opacity: 0; transform: translateX(-14px); }
  to { opacity: 1; transform: none; }
}
.pop { animation: num-pop .55s ease; transform-origin: left center; }
@keyframes num-pop {
  0% { transform: scale(1); }
  40% { transform: scale(1.06); }
  100% { transform: scale(1); }
}
@keyframes track-shine { to { background-position: -200% 0; } }
.confetti { position: fixed; inset: 0; z-index: 60; pointer-events: none; }
.milestone { position: fixed; left: 50%; z-index: 55; padding: 10px 22px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--money); font-size: var(--lead); font-weight: 600; letter-spacing: -.01em; white-space: nowrap; pointer-events: none; animation: milestone-ride 6s cubic-bezier(.2,.8,.3,1) both; }
@keyframes milestone-ride {
  0% { opacity: 0; transform: translate(-50%, -40px); }
  7%, 90% { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -30px); }
}

/* ---- couples ladder (portrait only) ------------------------------------
   Seven cards: the three rooms above us, us, the three below. Blue is
   audience; yellow only ever means another room is selling tickets. */
.zone.ladder { display: none; padding: 12px var(--pad); justify-content: center; }
.ladder-cards { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 10px; }
.lcard {
  min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: center; gap: 4px;
  padding: 4px 10px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-3);
}
.lcard .top { display: flex; align-items: baseline; gap: 6px; min-width: 0; white-space: nowrap; }
.lcard .pos { font-family: var(--mono); font-size: 20px; font-weight: 600; color: var(--ink-3); flex: none; }
.lcard .nm { font-size: 21px; font-weight: 600; color: var(--ink-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.lcard .bot { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
.lcard .vw { font-family: var(--mono); font-size: 32px; font-weight: 600; color: var(--audience); letter-spacing: -.03em; line-height: 1; }
.lcard .dv { font-family: var(--mono); font-size: 19px; font-weight: 600; color: var(--ink-3); }
.lcard .dv.up { color: var(--good); }
.lcard .dv.down { color: var(--critical); }
.lcard.us { background: var(--audience-soft, rgba(74,155,223,.14)); border-color: var(--audience); }
.lcard.us .nm, .lcard.us .pos { color: var(--ink); }
.lcard.sell { border-color: var(--warn); }
.lcard.sell .nm { color: var(--warn); }
.ladder-cards .empty { grid-column: 1 / -1; }
[data-fresh="stale"] .lcard .vw { color: var(--ink-3); }

/* ---- C: landscape 1920x1080 (default) -----------------------------------
   rows 56 + 480 + 480 = 1016, gaps 32, pad 32 = 1080.
   columns 480 + 776 + 600 = 1856, gaps 32, pad 32 = 1920.
   Row 1: money / time band over room / chart. Row 2: tips + spenders across
   the first two columns / the month and the site. */
main { grid-template-columns: 480fr 776fr 600fr; grid-template-rows: 56px minmax(0, 1fr) minmax(0, 1fr); }
.ribbon { grid-column: 1 / -1; }
/* Show mode: the show card takes the chart's place and the second row is the
   two audience lists and the room card. A private has one list. */
main[data-show] { grid-template-areas: "ribbon ribbon ribbon" "money show show" "list1 list2 room"; }
main[data-show="private"] { grid-template-areas: "ribbon ribbon ribbon" "money show show" "list1 list1 room"; }
main[data-show="ticket"][data-ticket-live] { grid-template-areas: "ribbon ribbon ribbon" "money show show" "list1 list1 list2"; }
.pair-time { min-width: 0; min-height: 0; display: grid; grid-template-rows: 110px minmax(0, 1fr); gap: var(--gap); }
.pair-lists { grid-column: 1 / 3; min-width: 0; min-height: 0; display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); }
.money-grid { grid-template-rows: auto auto; gap: 10px; }
.money-side { gap: 8px var(--gap); }
.cell.hero .fig { font-size: clamp(var(--hero-min), 25cqi, 100px); }
.room-figs .fig { font-size: clamp(var(--sec), 22cqi, 60px); }
.room-figs .fig.narrow { font-size: clamp(var(--sec), 16cqi, 48px); }

/* ---- A: portrait strip 1280x2160 ------------------------------------------
   rows 80 + 400 + 110 + 410 + 310 + 530 + 190 = 2030, gaps 96, pad 32 = 2158.
   Everything is one column: the clock is a thin band, the room card runs the
   full width with viewers and couples side by side, the bottom band is the
   month and the site (the socials until 4 Oct 2026). */
@media (max-aspect-ratio: 1/1) {
  :root { --lbl: 26px; --meta: 26px; --lead: 36px; --rowfig: 40px; --sec: 48px; --hero-min: 160px; --ribbon: 30px; --pad: 20px; --head-h: 34px; --chip-h: 40px; }
  /* Re-cut 2026-09-16. The socials band was 170 and clipped the note under
     every total; the room card needed ~390 once its footer grew. The socials
     tile puts its note beside the total here (below) instead of under it, so
     the band only needed 20 more, and the chart and the lists gave it up.
     The room took another 20 from the chart on 2026-09-16: with the ticket
     call's two rows of pills it measured 362 against 352 inside the padding
     and the ranks were overlapping the viewers label. */
  /* The couples ladder (96 + a gap) was paid for on 2026-10-01 mostly by
     the lists, which dropped from eight rows to seven here (rowCap in
     tv2.js): 530->480. Money 400->362 (hero capped 186->168 so it clears its
     label) and chart 310->290 gave the rest; the plot collapses under the
     now pill below about 290. The room card and socials kept theirs. */
  main { grid-template-columns: minmax(0, 1fr); grid-template-rows: 80px 362fr 110px 410fr 96fr 290fr 480fr 190fr; }
  .zone.ladder { display: flex; }
  main[data-show] .zone.ladder { display: none; }
  .ribbon { grid-template-columns: auto 1fr; grid-template-areas: "brand meta" "health health"; padding: 0 4px; }
  /* Six named pills with values are 1363px at the 26px label size on a
     1248px ribbon (the degraded obs case). Tighter tracking and 23px keeps
     the words and lands at about 1150. */
  .health-bar { justify-self: start; gap: 12px; }
  .health { font-size: 23px; letter-spacing: .03em; gap: 6px; }
  /* The toys pill (2026-10-02) only shows on a /toys on night, and seven pills
     do not fit 1248px at 23px (1442 worst case), so the row goes to 20px
     while it is up. */
  .health-bar:has(#health-toys:not([hidden])) .health, .health-bar:has(#health-keep:not([hidden])) .health, .health-bar:has(#health-goal:not([hidden])) .health { font-size: 20px; letter-spacing: .02em; }
  .health-bar:has(#health-toys:not([hidden])) { gap: 8px; }
  .pair-time { display: contents; }
  .pair-lists { grid-column: auto; }
  .money-grid { grid-template-columns: minmax(0, 1fr) 400px; grid-template-rows: none; }
  .cell.hero .fig { font-size: clamp(var(--hero-min), 25cqi, 168px); }
  .money-grid { gap: var(--gap); }
  .money-side { gap: 12px var(--gap); }
  .room-figs .fig { font-size: clamp(var(--sec), 20cqi, 84px); }
  .room-figs .fig.narrow { font-size: clamp(var(--sec), 14cqi, 66px); }
  /* Show mode: rows 80 + 380 + 520 + 360 + 690 = 2030. The lists sit side by
     side on the bottom row (one list, full width, for a private). */
  main[data-show] { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: 80px 380fr 520fr 360fr 690fr; grid-template-areas: "ribbon ribbon" "money money" "show show" "room room" "list1 list2"; }
  main[data-show="private"] { grid-template-areas: "ribbon ribbon" "money money" "show show" "room room" "list1 list1"; }
  main[data-show="ticket"][data-ticket-live] { grid-template-areas: "ribbon ribbon" "money money" "show show" "list1 list2" "list1 list2"; }
  .show-hero .fig { font-size: clamp(var(--hero-min), 30cqi, 220px); }
  .show-grid .fig.sec { font-size: clamp(var(--sec), 20cqi, 88px); }
  .month-line { flex: 1 1 auto; text-align: right; }
  /* The month and the site sit side by side in the bottom band. */
  .zone.plan { flex-direction: row; gap: var(--pad); }
  /* 9 to 11: the month half only needs its figure line, the site rows need
     every pixel for what is on the ticket. */
  #site-half { flex: 11 1 0; }
  #month-air { flex: 9 1 0; }
  .a-hide { display: none; }
  #site-half { border-top: 0; padding-top: 0; border-left: 1px solid var(--line); padding-left: var(--pad); }
  /* The plot is only ~260px tall here, so the top gridline sits behind the now
     pill. Two labelled lines is the honest count for the space. */
  #chart-axis span:first-child { display: none; }
}

/* ---- B: wide strip 3840x720 -----------------------------------------------
   one row of six cards: 780 + 700 + 840 + 484 + 484 + 440 = 3728, gaps 80,
   pad 32 = 3840. Card height 720 - 32 - 56 - 16 = 616. Lists show 5 two-line
   rows.

   The clock had its own full-height column here and spent it on one word. It
   is a 104px band over the room card now, which is the shape A and C already
   use, and it keeps its own card so a private show still turns it red. The
   width it gave up went to the chart, which needed it most. */
@media (min-aspect-ratio: 3/1) {
  :root { --lbl: 26px; --meta: 26px; --lead: 36px; --rowfig: 40px; --sec: 48px; --hero-min: 170px; --ribbon: 28px; --pad: 18px; --head-h: 34px; --chip-h: 40px; }
  /* The last column was 348 for the socials. The month and the site rows
     need names and a figure side by side, so it is 440 since 4 Oct 2026: 60
     from the chart, 16 from each list. */
  main { grid-template-columns: 780fr 700fr 840fr 484fr 484fr 440fr; grid-template-rows: 56px minmax(0, 1fr); }
  .pair-lists { display: contents; }
  .pair-time { grid-template-rows: 104px minmax(0, 1fr); }
  .money-grid { grid-template-rows: auto auto; }
  .money-side { grid-template-columns: 332fr 180fr 180fr; gap: 12px var(--gap); }
  .cell.hero .fig { font-size: clamp(var(--hero-min), 25cqi, 240px); }
  .money-grid { gap: var(--gap); }
  .money-side .cell:first-child { grid-column: auto; }
  .time-cell .fig { font-size: clamp(var(--sec), 6.5cqi, 84px); }
  /* The card is 700 wide and 496 tall here (the clock band takes 104 above
     it), 458 inside the padding. Viewers, couples and site run three across
     like the other geometries since 4 Oct 2026: viewers had its own row above
     the two ranks, and once the goal line joined the foot and a seventh pill
     made a third row the card was 74 over and the three figures were drawn
     on top of each other. Budget: head 34, figures 82, call 8 + 40 + 6 + 31
     + 6 + 67 = 158 with the text on its own line and two rows of pills, foot
     134 for three lines, gaps 30 = 438. */
  .room-figs { flex: 0 0 auto; gap: 8px var(--gap); grid-template-columns: minmax(0, 1.25fr) minmax(0, 1.05fr) minmax(0, 1fr); }
  /* Labels a step down so "viewers · 722 anon", "in 380 couples" and
     "of 8,900 live" sit side by side without running into each other. */
  .room-figs .lbl { font-size: 22px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .room-figs .cell { min-width: 0; }
  .room-figs .lbl .age { font-size: inherit; }
  .room-figs .fig, .room-viewers-line .fig { font-size: clamp(40px, 26cqi, 60px); }
  .room-figs .fig.narrow { font-size: clamp(36px, 20cqi, 48px); }
  .room-figs .cell { gap: 2px; }
  .tc-text { flex-basis: auto; font-size: 24px; }
  .tc-light { font-size: 22px; padding: 0 7px; }
  /* Two rows of pills, never three. The placement and viewers pills repeat
     the three figures directly above them, so they are the two to drop here. */
  .tc-lights { gap: 5px; max-height: 67px; overflow: hidden; }
  .tc-light[data-key="placement"], .tc-light[data-key="viewers"] { display: none; }
  /* Show mode: five across, 780 + 1160 + 600 + 602 + 602 = 3744. The show
     card is landscape here too, so the hero takes the left and the three
     secondaries stack on the right instead of running four across. */
  main[data-show] { grid-template-columns: 780fr 1160fr 600fr 602fr 602fr; grid-template-areas: "ribbon ribbon ribbon ribbon ribbon" "money show room list1 list2"; }
  main[data-show="private"] { grid-template-areas: "ribbon ribbon ribbon ribbon ribbon" "money show room list1 list1"; }
  main[data-show="ticket"][data-ticket-live] { grid-template-areas: "ribbon ribbon ribbon ribbon ribbon" "money show list1 list1 list2"; }
  .show-grid { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); grid-template-rows: repeat(3, minmax(0, 1fr)); }
  .show-grid .show-hero { grid-row: 1 / -1; }
  .show-hero .fig { font-size: clamp(var(--hero-min), 34cqi, 260px); }
  .show-grid .fig.sec { font-size: clamp(var(--sec), 18cqi, 80px); }
  .room-foot { line-height: 1.2; }
  .b-hide { display: none; }
  /* 312px inside the padding: the notes go under their labels and the nightly
     need takes its own line. */
  .plan-half .zone-head { flex-wrap: wrap; gap: 2px 12px; }
  .ma-line .ma-need { margin-left: 0; flex-basis: 100%; }
  /* The site's "usual" is short enough to stay on the headline's line. */
  .ma-line #site-usual { flex-basis: auto; }
}

/* Healthy services are already dots, so the only thing left to fit is the name
   of whatever has gone warn or err. Below ~1400px of ribbon there is no room
   beside the brand and the meta, so those names drop too and the dot carries
   it: red in the middle of the ribbon is the signal, the word is the detail. */
@media (min-aspect-ratio: 1/1) {
  @container (max-width: 1400px) { .health b { display: none; } .health-bar { gap: 14px; } }
}

/* ---- the last stream summary (PIM-300, 14 Oct 2026) ------------------------------------
   main[data-show="summary"] while the room is dark (tv2.js summaryDue()). It is its own
   board, not a restyle of show mode: it borrows only the grid names. The money and clock
   bands stay (tonight, the break countdown), the summary card takes everything else, and the
   room, chart, lists and month zones are off the board. Figures size off their own tile with
   cqi, from the number of characters in them (--n, set in renderSummary), up to --figmax.
   Colours as the rest of /tv: orange tokens and ticket money, blue viewers and followers,
   everything else ink. Last in the file so it wins the ties with the show-mode rules above. */
.zone.summary { display: none; }
main[data-show="summary"] .zone.summary { display: grid; grid-area: summary; border-color: var(--line-strong); gap: 12px var(--gap);
  grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr) auto; grid-template-areas: "head" "when" "grid" "tippers"; --figmax: 104px; }
main[data-show="summary"] .zone.show, main[data-show="summary"] .zone.room, main[data-show="summary"] .zone.tips,
main[data-show="summary"] .zone.spenders, main[data-show="summary"] .zone.chart, main[data-show="summary"] .zone.ladder, main[data-show="summary"] #site-half { display: none; }
/* The month's hours stay (the plan zone with only its left half): a night off the five hours is read here as much as in a stream. */
main[data-show="summary"] .zone.plan { display: flex; grid-area: plan; flex-direction: column; }
main[data-show="summary"] #month-air { flex: 1 1 auto; justify-content: center; }
main[data-show="summary"] .zone.time { display: flex; grid-area: time; }
main[data-show="summary"] .zone.money { grid-area: money; }
main[data-show="summary"] .ribbon { grid-area: ribbon; }
main[data-show="summary"] .pair-time, main[data-show="summary"] .pair-lists { display: contents; }
.zone.summary .zone-head { grid-area: head; }
.sum-when { grid-area: when; color: var(--ink-2); font-size: calc(var(--lead) * 1.5); font-weight: 600; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sum-when b { color: var(--ink); font-family: var(--mono); font-weight: 600; }
.sum-grid { grid-area: grid; min-height: 0; display: grid; gap: var(--gap); grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: minmax(0, 1fr); }
.sum-tile { padding: calc(var(--pad) * .8); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); gap: 4px; overflow: hidden; }
.sum-tile .fig { font-size: max(var(--sec), min(var(--figmax), calc(92cqi / (var(--n, 6) * .6)))); }
.sum-tile .lbl { font-size: calc(var(--lbl) * 1.15); }
.sum-tile .sub { font-size: calc(var(--meta) * 1.1); color: var(--ink-2); }
.sum-tile .sub b { color: var(--ink); }
.sum-tile .sub b.gold { color: var(--gold); }
.sum-tippers { grid-area: tippers; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.sum-tippers .rows { flex: 0 0 auto; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 84px; column-gap: var(--gap); }
.sum-tippers .row { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: "rank lead fig"; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); padding: 0 16px; column-gap: 14px; }
.sum-tippers .row:last-child { border-bottom: 1px solid var(--line); }
.sum-tippers .rank { grid-area: rank; color: var(--ink-3); font-family: var(--mono); font-weight: 600; font-size: calc(var(--rowfig) * 1.2); }
.sum-tippers .row .lead { font-size: calc(var(--lead) * 1.2); }
.sum-tippers .row .fig { font-size: calc(var(--rowfig) * 1.2); }
.sum-tippers .row:only-child { grid-column: 1 / -1; }

/* C: 1920x1080. The summary spans what the room card, chart and lists had. */
main[data-show="summary"] { grid-template-columns: 560fr 1296fr; grid-template-rows: 56px minmax(0, 1fr) 150px 250px; grid-template-areas: "ribbon ribbon" "money summary" "time summary" "plan summary"; }
main[data-show="summary"] .zone.time { flex-direction: column; justify-content: center; }
main[data-show="summary"] .time-cell { flex-wrap: wrap; row-gap: 4px; column-gap: 16px; }
main[data-show="summary"] .time-notes { flex: 1 1 100%; text-align: left; align-self: stretch; }
main[data-show="summary"] .time-cell .sub { text-align: left; white-space: normal; }
main[data-show="summary"] .time-notes .sub { white-space: normal; }

/* A: portrait 1280x2160, one column: money, clock band, then the card. */
@media (max-aspect-ratio: 1/1) {
  main[data-show="summary"] { grid-template-columns: minmax(0, 1fr); grid-template-rows: 80px 362px 110px 190px minmax(0, 1fr); grid-template-areas: "ribbon" "money" "time" "plan" "summary"; }
  main[data-show="summary"] .zone.plan { flex-direction: row; }
  main[data-show="summary"] .zone.summary { --figmax: 132px; }
  main[data-show="summary"] .zone.time { flex-direction: column; justify-content: center; }
  main[data-show="summary"] .time-notes { flex: 1 1 auto; text-align: right; align-self: center; }
  main[data-show="summary"] .time-cell { flex-wrap: nowrap; }
  .sum-tippers .rows { grid-auto-rows: 96px; grid-template-columns: minmax(0, 1fr); row-gap: 8px; }
}

/* B: wide strip 3840x720. Money, clock band, then the card with the tippers as its last column. */
@media (min-aspect-ratio: 3/1) {
  main[data-show="summary"] { grid-template-columns: 780fr 700fr 2260fr; grid-template-rows: 56px 104px minmax(0, 1fr); grid-template-areas: "ribbon ribbon ribbon" "money time summary" "money plan summary"; }
  main[data-show="summary"] .zone.summary { grid-template-columns: minmax(0, 1fr) 560px; grid-template-rows: auto auto minmax(0, 1fr); grid-template-areas: "head tippers" "when tippers" "grid tippers"; --figmax: 104px; }
  .sum-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); }
  .sum-tile .lbl { font-size: var(--lbl); }
  .sum-tippers .rows { grid-template-columns: minmax(0, 1fr); grid-auto-rows: minmax(0, 1fr); flex: 1 1 auto; row-gap: 8px; }
  main[data-show="summary"] .zone.time { flex-direction: row; }
  main[data-show="summary"] .time-cell { flex-wrap: nowrap; }
  main[data-show="summary"] .time-notes { flex: 1 1 auto; text-align: right; align-self: center; }
  main[data-show="summary"] .time-cell .sub { text-align: right; }
}
