/*
 * IRONFORM app styles — the alpha-slice subset of the oracle's app.css,
 * rebuilt on the token sheet. Everything references tokens or roles; no raw
 * hex in structural rules (ADR-0032 discipline carried over).
 */

* { box-sizing: border-box; }
html { font-size: 16px; }
body.app {
  margin: 0;
  background: var(--b-bg);
  color: var(--b-fg);
  font-family: var(--b-font-body);
  font-size: var(--b-text-base);
  line-height: var(--b-leading-normal);
  min-height: 100vh;
}
a { color: var(--lb); text-decoration: none; }
b { font-weight: var(--b-weight-semibold); }
hr { border: 0; border-top: var(--b-border-w) solid var(--border-subtle); margin: var(--b-space-3) 0; }

/* ── App chrome ─────────────────────────────────────────────────────── */
.app-header {
  display: flex; align-items: center; gap: var(--b-space-4);
  padding: var(--b-space-3) var(--b-space-4);
  background: var(--bg);
  border-bottom: var(--b-border-w) solid var(--border-subtle);
}
.wordmark {
  font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-xl);
  letter-spacing: 0.02em; color: var(--text-primary); text-transform: uppercase;
}
.wordmark em { font-style: normal; color: var(--role-primary); }
.app-header .chrome { margin-left: auto; display: flex; align-items: center; gap: var(--b-space-3); }
.signin { display: inline-flex; align-items: center; gap: var(--b-space-1); color: var(--lb); font-weight: var(--b-weight-semibold); }
/* width:100% is load-bearing, not decoration (TEN-1066).
   body is a COLUMN flex container, and `margin: 0 auto` on a flex item
   disables cross-axis stretch — so without an explicit width this box sizes
   to MAX-CONTENT. On a 375px phone that measured 394px, and the whole
   document scrolled sideways: every page, not only the conversation.
   The auto margins still centre it inside max-width on a wide screen. */
main.container { max-width: 720px; width: 100%; margin: 0 auto; padding: var(--b-space-4); }
.app-foot { max-width: 720px; margin: 0 auto; padding: var(--b-space-4); color: var(--text-faint); }
.judgefoot { display: block; text-align: center; color: var(--text-muted); padding: var(--b-space-4) 0; }
body.bare main.container { max-width: 900px; }

.theme-toggle {
  position: fixed; right: var(--b-space-4); bottom: var(--b-space-4); z-index: 50;
  background: var(--surface-overlay); color: var(--text-muted);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-full);
  padding: var(--b-space-2) var(--b-space-3); font: inherit; font-size: var(--b-text-sm);
  cursor: pointer; box-shadow: var(--b-shadow-md);
}
.theme-toggle:hover { background: var(--hover); }

/* ── Shared voltage bits ────────────────────────────────────────────── */
.pip, .livedot {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: var(--b-font-mono); font-size: 9px; color: var(--role-attention);
}
.pip::before, .livedot::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--role-attention); }
.bdot { display: inline-block; width: 8px; height: 8px; border-radius: var(--b-radius-full);
  background: var(--role-attention); margin-right: var(--b-space-1); animation: ifpulse 1.6s infinite var(--b-ease); }
@keyframes ifpulse { 50% { opacity: 0.35; } }
.chip {
  font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em;
  font-size: 10px; font-weight: 700; color: var(--text-muted);
  border: var(--b-border-w) solid var(--border-strong); border-radius: 7px;
  padding: 6px 12px; display: inline-flex; align-items: center; gap: 4px;
}
.youpill {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold); color: var(--role-presence);
  background: var(--green-a); border: var(--b-border-w) solid var(--green-line);
  border-radius: var(--b-radius-full); padding: 0 var(--b-space-2); margin-left: var(--b-space-2);
}
.ycount {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold); color: var(--role-presence);
  background: var(--green-a); border-radius: var(--b-radius-full);
  padding: 0 var(--b-space-2); margin-left: var(--b-space-2);
}
.chev { color: var(--text-faint); margin-left: var(--b-space-2); }

/* ── Navigator (landing) ────────────────────────────────────────────── */
.nav-search {
  width: 100%; padding: var(--b-space-3); font: inherit;
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-md);
}
.nav-search::placeholder { color: var(--text-faint); }
.chips { display: flex; gap: var(--b-space-2); margin: var(--b-space-3) 0; }
.chips .chip { user-select: none; cursor: pointer; }
.chips .chip input { position: absolute; opacity: 0; pointer-events: none; }
.chips .chip.on, .chips .chip:has(input:checked) { color: var(--ink-on-primary); border-color: var(--lb); background: var(--lb); }
/* SEGMENTED TOGGLE — a .chip that IS the control (TEN-1448).
 *
 * Usage:
 *   <label class="chip chip-toggle{{if X}} on{{end}}">
 *     <input type="checkbox" name="x" value="1"{{if X}} checked{{end}}>Label</label>
 *   <a class="chip chip-toggle{{if X}} on{{end}}" href="…">Label</a>   <- link form
 *
 * Selected renders SOLID, unselected outlined. Both forms are legitimate:
 * the checkbox is a value the form posts, the link is view state that rides
 * the query — and a reader should not have to know which, so they look the
 * same. `.on` is the server's rendering of the current state; the :has()
 * clause keeps a checkbox right the instant it is clicked, with no
 * round-trip, so the two never disagree on screen.
 *
 * WHY THIS EXISTS RATHER THAN .chips .chip. The three rules above do the
 * same job but only inside a `.chips` ANCESTOR. Every form that reached for
 * a chip toggle without one — the registration settings block is the whole
 * of TEN-1448 item 5 — rendered a raw native checkbox beside an unfilled
 * outline and looked broken. An ancestor is a requirement nothing states at
 * the point of use, so this variant carries its own. Reach for `.chips` when
 * you want the ROW's layout too; reach for this for a toggle that sits in
 * somebody else's row. */
.chip-toggle { user-select: none; cursor: pointer; text-decoration: none; }
.chip-toggle input { position: absolute; opacity: 0; pointer-events: none; }
.chip-toggle.on, .chip-toggle:has(input:checked) { color: var(--ink-on-primary); border-color: var(--lb); background: var(--lb); }
.noresults { color: var(--text-muted); padding: var(--b-space-5) 0; text-align: center; }
.seclbl {
  font-family: var(--b-font-heading); font-weight: 700;
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); margin: var(--b-space-5) 0 9px;
  display: flex; justify-content: space-between; align-items: baseline;
}
.evcard {
  display: flex; align-items: center; gap: var(--b-space-3);
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  padding: var(--b-space-3); margin-bottom: var(--b-space-2);
  transition: background var(--b-duration-fast) var(--b-ease);
}
.evcard:hover { background: var(--hover); }
.evcard.hero { border-color: var(--green-line); }
.evcard .date {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  width: 42px; height: 42px; flex: none;
  background: var(--bg); border: var(--b-border-w) solid var(--border-strong);
  border-radius: 10px;
}
.evcard .date .mo { font-family: var(--b-font-heading); font-size: 9px; text-transform: uppercase; color: var(--text-faint); line-height: 1; }
.evcard .date .dy { font-family: var(--b-font-mono); font-weight: 700; font-size: 16px; color: var(--text-muted); line-height: 1; }
.evcard.now .date .dy { color: var(--role-attention); }
.evbody { display: flex; flex-direction: column; min-width: 0; }
.evnm { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 16px; font-weight: 700; line-height: 1.1; }
.evmeta { font-family: var(--b-font-mono); font-size: 10px; color: var(--text-faint); }
.evmeta b { color: var(--role-presence); }
.evcard .pip, .evcard .chip { margin-left: auto; }
.evcard .pip + .chev, .evcard .chip + .chev { margin-left: var(--b-space-2); }
.evcard .chev:only-child { margin-left: auto; }

/* ── Pinned context + the organizer's drafts (TEN-1309) ─────────────────
   The pin is the oracle's cyan band, and it uses the token literally named
   for the job: --role-context. It is a CONTEXT marker, so it reads quieter
   than a LIVE pip and louder than a section label, and it never renders at
   all when nothing is pinned — an empty band would make the ordinary state
   look like a broken one. */
.pinstrip {
  display: flex; align-items: center; gap: var(--b-space-3);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised); padding: var(--b-space-2) var(--b-space-3);
  margin-bottom: var(--b-space-3);
}
.pinstrip.on { border-color: var(--role-context); }
.pinstrip .pt {
  font-family: var(--b-font-heading); text-transform: uppercase;
  letter-spacing: 0.05em; font-size: 12px; font-weight: 700;
  color: var(--text-muted); text-decoration: none;
}
.pinstrip.on .pt { color: var(--role-context); }
.pinstrip .pinact { margin-left: auto; }
.pinstrip .act {
  margin-left: auto; font: inherit; font-size: 12px; cursor: pointer;
  color: var(--text-primary); background: var(--bg);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: 4px var(--b-space-3);
}
.pinstrip .act:hover { background: var(--hover); }
.pinstrip .pinact .act { margin-left: 0; }
/* A draft claims no date, so the date chip is dashes and stays quiet. */
.evcard.draft .date { border-style: dashed; }
.evcard.draft .date .mo, .evcard.draft .date .dy { color: var(--text-faint); }
.newevent {
  font-family: var(--b-font-heading); text-transform: uppercase;
  letter-spacing: 0.04em; font-size: 11px; font-weight: 700;
  color: var(--lb); text-decoration: none; white-space: nowrap;
}
.newevent:hover { text-decoration: underline; }
.draftsempty { color: var(--text-muted); margin: 0 0 var(--b-space-3); }

/* ── Event hub ──────────────────────────────────────────────────────── */
.hub .hubtop { display: flex; align-items: center; gap: var(--b-space-3); margin-bottom: var(--b-space-2); }
.hub .back {
  font-size: var(--b-text-xl); color: var(--text-muted); padding: 0 var(--b-space-2);
  border-radius: var(--b-radius-sm);
}
.hub .back:hover { background: var(--hover); }
.hub .hubtitle { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-2xl); }
.evwhen { color: var(--text-muted); margin-bottom: var(--b-space-4); }
.modebar { display: flex; gap: var(--b-space-2); margin-bottom: var(--b-space-5); }
.mode {
  flex: 1; display: flex; flex-direction: column; gap: 2px;
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-3);
}
.mode .ml { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.04em; font-size: 14px; font-weight: 800; }
.mode .ms { font-family: var(--b-font-mono); font-size: 9px; color: var(--text-faint); }
.mode.register { border-color: rgba(77,143,255,0.35); }
.mode.athlete { border-color: rgba(124,77,255,0.30); }
.mode.off { opacity: 0.45; }

/* ── Competition tree ───────────────────────────────────────────────── */
.tree { display: flex; flex-direction: column; }
.node {
  display: flex; align-items: center; gap: var(--b-space-2); width: 100%;
  color: var(--text-primary); background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  padding: var(--b-space-3); margin-bottom: var(--b-space-2); font: inherit; text-align: left;
}
a.node:hover { background: var(--hover); }
.node .nm { font-weight: var(--b-weight-semibold); }
.node .twig { color: var(--text-faint); font-family: var(--b-font-mono); }
.node .right { margin-left: auto; display: flex; align-items: center; gap: var(--b-space-2); }
.node .count { font-size: var(--b-text-sm); color: var(--text-faint); }
.node.leaf.off { opacity: 0.6; }
.branchbox { margin: 0; }
.branchbox > summary { list-style: none; cursor: pointer; }
.branchbox > summary::-webkit-details-marker { display: none; }
.branchbox[open] > summary .twig { transform: rotate(90deg); }
.node .twig { display: inline-block; transition: transform var(--b-duration-fast) var(--b-ease); }
.kids { margin-left: var(--b-space-5); }
.treelegend { font-size: var(--b-text-xs); color: var(--text-faint); margin-top: var(--b-space-2); }
.ss-missing { color: var(--text-muted); }

/* ── Leaderboard (spectator board) ──────────────────────────────────── */
.lb { background: var(--broadcast); border: 1px solid rgba(255,255,255,0.07); border-radius: 15px; padding: 15px; box-shadow: 0 16px 40px rgba(0,0,0,0.7); color: #F0F0F5; }
:root[data-theme="hc"] .lb, :root[data-theme="light"] .lb { background: var(--surface-raised); border: var(--b-border-w) solid var(--border-strong); }
.btop { display: flex; align-items: baseline; gap: var(--b-space-3); margin-bottom: var(--b-space-3); }
.bev { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.12em; font-size: 10px; font-weight: 700; color: var(--lv); }
.bwo { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.02em; font-size: 22px; font-weight: 900; color: #fff; line-height: 1; }
.bstate { margin-left: auto; color: var(--role-attention); font-weight: var(--b-weight-bold); font-size: var(--b-text-sm); }
.brows { display: flex; flex-direction: column; gap: var(--b-space-1); }
.brow {
  display: grid; grid-template-columns: 6px 2.2rem 1fr minmax(0,0.6fr) 3.5rem 4rem;
  align-items: center; gap: var(--b-space-2);
  background: var(--surface-raised); border-radius: var(--b-radius-sm);
  padding: var(--b-space-2) var(--b-space-3);
  font-size: var(--b-text-sm);
}
.brow .band { width: 4px; height: 1.6em; border-radius: 2px; background: var(--border-strong); }
.brow.rs-live .band { background: var(--sportscore-live-accent, var(--role-attention)); }
.brow.rs-disconnected .band { background: var(--role-danger); }
.brow.rs-final .band, .brow.rs-review .band { background: var(--text-faint); }
.brow .rk { font-family: var(--b-font-mono); font-weight: var(--b-weight-bold); text-align: right; }
.brow .nm { font-weight: var(--b-weight-semibold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brow .ns { color: var(--bad-fg); font-weight: var(--b-weight-normal); }
.brow .prov { color: var(--text-muted); font-weight: var(--b-weight-normal); }
.brow .fin { color: var(--text-faint); font-weight: var(--b-weight-normal); }
.brow .aff { color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.brow .gap { font-family: var(--b-font-mono); color: var(--text-faint); text-align: right; }
.brow .sc { font-family: var(--b-font-mono); font-weight: var(--b-weight-bold); font-size: var(--b-text-base); text-align: right; }
.brow.rs-notstarted { opacity: 0.5; }
.brow:nth-child(-n+3) .rk { color: var(--sportscore-podium-highlight, var(--role-attention)); }

/* ── Jumbotron (wall) ───────────────────────────────────────────────── */
.jb { background: var(--broadcast); color: #F0F0F5; border-radius: var(--b-radius-lg); padding: var(--b-space-5); }
.jbhead { display: flex; align-items: baseline; gap: var(--b-space-3); margin-bottom: var(--b-space-4); }
.jbev { font-size: var(--b-text-sm); letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.6; }
.jbwo { font-family: var(--b-font-heading); font-weight: 900; font-size: var(--b-text-3xl); }
.jblive { margin-left: auto; color: #FFB84D; font-weight: var(--b-weight-bold); }
.jbrows { display: flex; flex-direction: column; gap: var(--b-space-2); }
.jbrow {
  display: grid; grid-template-columns: 3rem 1fr 5rem 6rem; align-items: baseline; gap: var(--b-space-3);
  font-size: var(--b-text-lg); border-bottom: 1px solid rgba(255,255,255,0.12); padding-bottom: var(--b-space-2);
}
.jbrow .rk { font-family: var(--b-font-mono); font-weight: 700; font-size: var(--b-text-2xl); text-align: right; }
.jbrow .nm { font-weight: var(--b-weight-semibold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.jbrow .aff, .jbrow .ns, .jbrow .prov, .jbrow .fin { opacity: 0.55; font-size: var(--b-text-base); font-weight: var(--b-weight-normal); }
.jbrow .gap { font-family: var(--b-font-mono); opacity: 0.6; text-align: right; }
.jbrow .sc { font-family: var(--b-font-mono); font-weight: 700; font-size: var(--b-text-2xl); text-align: right; }
.jbrow.rs-notstarted { opacity: 0.35; }

/* ── THE NOTICE (TEN-1251) ───────────────────────────────────────────
   The kit has been drawing this shape for a while without declaring it: a
   raised panel, a hairline border, and a 3px LEFT RULE whose colour carries the
   intent. It is info-box.warn / info-box.unity in the handoff supplement, the
   left rule on every philosophy card, and the state edge on the venue cards in
   docs/ux/comps. Declared here so it can be REFERRED TO rather than redrawn.

   Named per design-system/CONVENTIONS.md: what the thing IS (a notice),
   variants as `--modifier`, and the intents are the kit's OWN shared set rather
   than a new vocabulary. A message about an operation is a status, so it
   collapses onto the same four intents everything else uses:

     --positive    role-presence   it happened
     --attention   role-attention  needs a look before or after acting
     --negative    role-danger     it did not happen
     --neutral     muted           context; the "why we ask" text

   ROLE TOKENS ONLY, never a pigment — that is the rule that keeps the kit
   theme-proof, and a notice that reached for --ok or --bad would break it.

   Two grammars exist and must not be mixed. The style guide's amber/gold/violet
   is about the ATHLETE — achievement, identity, elite thresholds. This is
   SYSTEM STATUS. "You hit a PR" is an achievement; "your phone number saved" is
   a system reporting an operation, and a confirmation is not an achievement.

   And it stays quiet either way: "EARNED, NOT CHEERFUL. No confetti. Settled.
   Permanent." A left rule and a line of text — not a fill, and not the
   full-screen checkmark the comps reserve for a scoring moment. A settings page
   has not earned that. */
.notice {
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
  padding: var(--b-space-3);
  margin: 0 0 var(--b-space-3);
  font-size: var(--b-text-sm);
  line-height: 1.6;
}
.notice--positive  { border-left: 3px solid var(--role-presence); }
.notice--attention { border-left: 3px solid var(--role-attention); }
.notice--negative  { border-left: 3px solid var(--role-danger); }
.notice--neutral   { border-left: 3px solid var(--border-strong); }

/* ── Console outcomes ────────────────────────────────────────────────
   .cmc-flash is on EVERY console page and was styled NOWHERE, so every outcome
   rendered as body text in the page's own colour — indistinguishable from the
   form around it (Mark, UAT 2026-08-25).

   It IS a notice; the name is legacy and belongs to CONVENTIONS.md's wiring
   pass (the old → new table), so it composes the same rules rather than growing
   a second set that can drift.

   Placement matters as much as treatment: on the security page the outcome
   renders INSIDE the card that produced it, because that page is a column of
   independent transactions, and an answer at the top of the document is
   orphaned from its question — on a phone, off-screen entirely. */
.cmc-flash {
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
  padding: var(--b-space-3);
  margin: 0 0 var(--b-space-3);
  font-size: var(--b-text-sm);
  line-height: 1.6;
}
.cmc-flash-ok    { border-left: 3px solid var(--role-presence); }
.cmc-flash-error { border-left: 3px solid var(--role-danger); }
/* .cmc-flash-error's leading icon is defined once, alongside .b-flash-error
   and .flash-error, near the bottom of this file (TEN-1419's comment). */
/* Inside a card it answers the control above it, so it sits under the title. */
.acct-card .cmc-flash { margin-top: var(--b-space-2); }

/* ── Registration funnel + paydoor (TEN-911) ───────────────────────── */
.regwrap, .paywrap { max-width: 560px; margin: 0 auto; }
.flash {
  border-radius: var(--b-radius-md); padding: var(--b-space-3);
  margin-bottom: var(--b-space-3); font-size: var(--b-text-sm);
}
.flash-error { background: var(--bad-a); color: var(--bad-fg); border: var(--b-border-w) solid var(--role-danger); }
/* .flash-error's leading icon is defined once, alongside .b-flash-error and
   .cmc-flash-error, near the bottom of this file (TEN-1419's comment). */
.regcard {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-lg); padding: var(--b-space-5);
  display: flex; flex-direction: column; gap: var(--b-space-3);
}
.regcard.regwon { border-color: var(--green-line); text-align: center; align-items: center; }
.regcheck {
  width: 56px; height: 56px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--b-radius-full); background: var(--green-a); color: var(--role-presence);
  font-size: var(--b-text-2xl); font-weight: var(--b-weight-bold);
}
.regbig { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-3xl); }
.regsub { color: var(--text-muted); }
.regonward {
  display: inline-flex; flex-direction: column; gap: 2px; margin-top: var(--b-space-3);
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-md); padding: var(--b-space-3) var(--b-space-5); color: var(--text-primary);
}
.regonward .ml { font-weight: var(--b-weight-bold); }
.regonward .ms { font-size: var(--b-text-xs); color: var(--text-faint); }
.regpasshint { color: var(--text-faint); font-size: var(--b-text-sm); }
.regevname { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-2xl); }
.regevmeta { color: var(--text-muted); margin-top: calc(-1 * var(--b-space-2)); }
.regstate {
  background: var(--surface-overlay); border-radius: var(--b-radius-md);
  padding: var(--b-space-3); color: var(--text-muted);
}
.regstate.ok { color: var(--role-presence); background: var(--green-a); }
.regprice { font-family: var(--b-font-mono); font-weight: var(--b-weight-bold); font-size: var(--b-text-lg); }
.regcta {
  display: block; width: 100%; text-align: center; font: inherit; cursor: pointer;
  background: var(--role-presence); color: var(--ink-on-primary);
  font-weight: var(--b-weight-bold); font-size: var(--b-text-lg);
  border: 0; border-radius: var(--b-radius-md); padding: var(--b-space-4);
}
.regcta:hover { filter: brightness(1.05); }
.regnote { color: var(--text-faint); font-size: var(--b-text-sm); }
.regjoin { display: flex; flex-direction: column; gap: var(--b-space-2); }
.regjoinrow {
  display: flex; align-items: center; gap: var(--b-space-3);
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-3); color: var(--text-primary);
}
.regjoinrow:hover { background: var(--hover); }
.regjoinrow .nm { font-weight: var(--b-weight-semibold); }
.regjoinrow .chev { margin-left: auto; }
.paycard {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-lg); padding: var(--b-space-5);
  display: flex; flex-direction: column; gap: var(--b-space-3);
}
.payamt { font-family: var(--b-font-mono); font-weight: 700; font-size: var(--b-text-3xl); margin: 0; }
.paylead { color: var(--text-muted); margin: 0; }
.paybtn {
  display: block; width: 100%; text-align: center; font: inherit; cursor: pointer;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-md); padding: var(--b-space-4);
  /* Button face per the button card (Mark 2026-09-07): condensed uppercase. */
  font-family: var(--b-font-heading); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
}
.paybtn.primary { background: var(--role-primary); color: var(--ink-on-accent); border: 0; }
.paybtn:hover { filter: brightness(1.05); }
.paynote { color: var(--text-faint); font-size: var(--b-text-sm); margin: 0; }

/* ── My Day + the boarding pass (TEN-912) ──────────────────────────── */
.mdcard {
  max-width: 560px; margin: 0 auto; position: relative;
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-lg); padding: var(--b-space-5);
}
.mdscan-toggle { position: absolute; opacity: 0; pointer-events: none; }
.mdchecked { margin-left: auto; color: var(--role-presence); font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.mdhero {
  background: var(--night); color: #F0F0F5; border-radius: var(--b-radius-md);
  padding: var(--b-space-4); margin: var(--b-space-3) 0;
}
:root[data-theme="light"] .mdhero, :root[data-theme="hc"] .mdhero { background: var(--surface-overlay); color: var(--text-primary); border: var(--b-border-w) solid var(--border-strong); }
.mdherotop { display: flex; align-items: center; }
.mdscanbtn {
  margin-left: auto; cursor: pointer; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
  background: var(--role-primary); color: var(--ink-on-accent);
  border-radius: var(--b-radius-md); padding: var(--b-space-2) var(--b-space-3);
}
.mdtitlerow { display: flex; align-items: center; gap: var(--b-space-2); margin-top: var(--b-space-2); }
.md-pill {
  font-family: var(--b-font-mono); font-weight: 700; font-size: var(--b-text-sm);
  background: rgba(255,255,255,0.14); border-radius: var(--b-radius-full); padding: 2px var(--b-space-3);
}
:root[data-theme="light"] .md-pill, :root[data-theme="hc"] .md-pill { background: var(--surface-raised); border: var(--b-border-w) solid var(--border-strong); }
.mdlist { display: flex; flex-direction: column; gap: var(--b-space-2); max-height: 320px; overflow-y: auto; }
.mdrow {
  display: flex; align-items: center; gap: var(--b-space-3);
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-3);
}
.mdrow.done { opacity: 0.75; }
.mdrow.done .mddot { background: var(--role-attention); }
.mdrow.upnext .mddot { background: var(--role-primary); }
.mdrow.upcoming { opacity: 0.85; }
.mdrow.selected { border-color: var(--role-primary); box-shadow: 0 0 0 var(--b-ring-w) var(--blue-a); }
.mddot { width: 8px; height: 8px; border-radius: var(--b-radius-full); background: var(--border-strong); flex: none; }
.mdtime { font-family: var(--b-font-mono); font-weight: var(--b-weight-bold); min-width: 3.6rem; }
.mdmain { display: flex; flex-direction: column; min-width: 0; }
.mdlname { font-weight: var(--b-weight-semibold); }
.mdlsub { font-size: var(--b-text-sm); color: var(--text-faint); }
.mdright { margin-left: auto; text-align: right; display: flex; flex-direction: column; }
.mdrv { font-family: var(--b-font-mono); font-weight: var(--b-weight-bold); }
.mdrs { font-size: var(--b-text-xs); color: var(--text-faint); }

/* The pass overlay — B&W all-business, CSS checkbox toggle. */
.pass {
  display: none; position: fixed; inset: 0; z-index: 100; overflow-y: auto;
  background: #fff; color: #000; padding: var(--b-space-5);
  text-align: center; font-family: var(--b-font-body);
}
.mdscan-toggle:checked ~ .pass { display: block; }
.passtop { display: flex; align-items: center; max-width: 420px; margin: 0 auto; }
.passeyebrow { font-size: var(--b-text-xs); letter-spacing: 0.12em; text-transform: uppercase; color: #555; }
.passclose { margin-left: auto; font-size: var(--b-text-2xl); cursor: pointer; padding: 0 var(--b-space-3); }
.passname { font-family: var(--b-font-heading); font-weight: 900; font-size: var(--b-text-3xl); margin-top: var(--b-space-3); }
.passsub { color: #333; }
.passrule { border-top: 2px solid #000; max-width: 420px; margin: var(--b-space-3) auto; }
.passdetail { font-weight: var(--b-weight-semibold); }
.qrwrap { width: min(66vw, 300px); margin: var(--b-space-4) auto; }
.passidlbl { font-size: var(--b-text-xs); letter-spacing: 0.12em; text-transform: uppercase; color: #555; }
.passid { font-family: var(--b-font-mono); font-weight: 700; font-size: var(--b-text-3xl); letter-spacing: 0.2em; }
.barcode { margin: var(--b-space-3) auto; }
.passidsm { font-family: var(--b-font-mono); color: #333; }
.passanum { display: flex; justify-content: space-between; max-width: 420px; margin: var(--b-space-3) auto; }
.passanum .muted { color: #888; }
.passfoot { font-size: var(--b-text-sm); color: #555; max-width: 420px; margin: 0 auto; }

/* ── Sessions (TEN-912) ─────────────────────────────────────────────── */
.sesswrap .empty { color: var(--text-muted); text-align: center; padding: var(--b-space-6) 0; }
.sesslist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.sessmain {
  display: flex; align-items: center; gap: var(--b-space-3);
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  padding: var(--b-space-3);
}
.sessmain:hover { background: var(--hover); }
.sesswhen { font-family: var(--b-font-mono); font-size: var(--b-text-sm); color: var(--text-muted); min-width: 9rem; }
.sesstext { display: flex; flex-direction: column; min-width: 0; }
.sessnode { font-weight: var(--b-weight-semibold); }
.sessev { font-size: var(--b-text-sm); color: var(--text-faint); }
.sbadge {
  margin-left: auto; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  border-radius: var(--b-radius-full); padding: 2px var(--b-space-3);
}
.sbadge.enrolled { color: var(--role-presence); background: var(--green-a); }
.sbadge.reserved { color: var(--lb); background: var(--blue-a); }
.sbadge.declined, .sbadge.invited { color: var(--text-muted); background: var(--surface-overlay); }
/* Participated is the funnel's LAST stage, not a live one (TEN-1312) —
   quiet ink on the raised surface, so a finished line never competes with
   the enrolled row above it for attention. */
.sbadge.participated { color: var(--text-faint); background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle); }
.sesswhen .tbd { color: var(--text-faint); font-style: italic; }
/* Withdraw on a standing line (TEN-1313). A sibling of .sessmain, not a
   child — a link inside a link is not a thing. Rendered only where the
   spot is still holdable, so this control never appears next to a row it
   cannot act on (TEN-858). */
.sessrow { display: flex; align-items: center; gap: var(--b-space-2); }
.sessrow .sessmain { flex: 1 1 auto; min-width: 0; }
.sesswdraw {
  flex: 0 0 auto; font-size: var(--b-text-sm); color: var(--text-muted);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-3); text-decoration: none;
}
.sesswdraw:hover { background: var(--hover); color: var(--text-primary); }

/* The reserve / decline acts on a recurring session (TEN-1312). Forms, not
   links: they mutate. They sit beside .sessmain for the same reason
   .sesswdraw does — a form inside a link is not a thing either. */
.sessact { margin: 0; flex: 0 0 auto; }
.sessbtn {
  font: inherit; font-size: var(--b-text-sm); cursor: pointer;
  color: var(--text-muted); background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-3);
}
.sessbtn:hover { background: var(--hover); color: var(--text-primary); }
.sessbtn.go { color: var(--ink-on-primary); background: var(--lb); border-color: var(--lb); }

/* The funnel filter (TEN-1312). The whole list is already in the page, so a
   chip tap must not cost a round-trip — and this book runs no framework, so
   the state lives in a radio the label owns and the rows answer through
   :has(). Same markup shape as the navigator's chips above, which is why
   .chips already hides the input and paints the checked label.

   Server-side filtering was not an option and is not a regression: a
   component on a DEFINITION page never sees the request's query string, so
   legacy's ?filter= deep link has no counterpart here (TEN-1312's report). */
.sessfilter { flex-wrap: wrap; }
.chips .count { font-variant-numeric: tabular-nums; opacity: 0.75; }
.sesswrap:has(.sessf-invited:checked) .sessrow:not([data-filter="invited"]),
.sesswrap:has(.sessf-response:checked) .sessrow:not([data-filter="reserved-declined"]),
.sesswrap:has(.sessf-enrolled:checked) .sessrow:not([data-filter="enrolled"]),
.sesswrap:has(.sessf-participated:checked) .sessrow:not([data-filter="participated"]) {
  display: none;
}

/* ── The bell (TEN-912) ─────────────────────────────────────────────── */
.notif { position: relative; }
.notif-toggle { position: absolute; opacity: 0; pointer-events: none; }
.notif-btn { position: relative; cursor: pointer; font-size: var(--b-text-lg); }
/* The badge must state its OWN line-height (TEN-1099). It sits inside
   .chrome-glyph-wrap, which sets line-height: 0 to kill the descender space
   under an inline SVG — and that inherits. With no line box the pill was
   17x0: the colour was set and nothing was painted, so the digit floated
   over the header with no background behind it. Measured, not guessed.
   A flex box with its own height cannot inherit the problem back. */
.notif-badge {
  position: absolute; top: -6px; right: -10px;
  display: inline-flex; align-items: center; justify-content: center;
  height: 16px; min-width: 16px; line-height: 1;
  background: var(--role-danger); color: var(--ink-on-color);
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  border-radius: var(--b-radius-full); padding: 0 5px; text-align: center;
}
.notif-panel {
  display: none; position: absolute; right: 0; top: 130%; z-index: 60; width: 320px;
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-md); box-shadow: var(--b-shadow-lg); padding: var(--b-space-3);
}
.notif-toggle:checked ~ .notif-panel { display: block; }
/* TEN-1433: the same visible-focus rule room-prefs-toggle already carries —
   the toggle is the element in the tab order (opacity: 0 above), so Escape's
   refocus (dismiss.js) needs a ring painted from ITS :focus-visible state,
   not the label's. */
.notif-toggle:focus-visible ~ .notif-btn {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px;
}
.notif-head { display: flex; align-items: baseline; margin-bottom: var(--b-space-2); }
.notif-titlebar { font-weight: var(--b-weight-bold); color: var(--text-primary); }
/* The panel head's two acts sit together on the right (TEN-1415): mark-all-read, then the gear
   through to the settings that govern which channels hear a category. */
.notif-headacts { margin-left: auto; display: flex; align-items: center; gap: var(--b-space-2); }
.notif-allread { font: inherit; font-size: var(--b-text-xs); color: var(--lb); background: none; border: 0; cursor: pointer; }
.notif-settings { display: inline-flex; align-items: center; color: var(--text-muted); }
.notif-settings:hover { color: var(--text-primary); }
.notif-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-1); max-height: 300px; overflow-y: auto; text-align: left; }
/* A row is now three things across one line (TEN-1415): the actor's face, the act that opens it,
   and the x. `align-items: start` keeps the face level with the title rather than centred against
   a two-line body. */
.notif-row { border-radius: var(--b-radius-sm); padding: var(--b-space-2); display: flex; align-items: flex-start; gap: var(--b-space-2); }
.notif-row.unread { background: var(--blue-a); }
.notif-row.unread .notif-dot { background: var(--role-primary); }
.notif-row.sev { border-left: 3px solid var(--role-critical); }
.notif-face { flex: none; display: flex; }
/* The platform's own disc: a mark, not somebody's initials, so it must not read as a person. */
.notif-av-system { background: var(--role-primary); color: var(--bg-canvas); font-size: var(--b-text-xs); }
/* The row's act fills the line, and the form wrapper must not introduce a box of its own. */
.notif-rowform { flex: 1; min-width: 0; margin: 0; }
.notif-rowlink { display: flex; gap: var(--b-space-2); color: var(--text-primary); font: inherit; text-align: left; width: 100%; background: none; border: 0; padding: 0; cursor: pointer; }
.notif-rowdel { flex: none; margin: 0; }
/* The club door on a notice about the reader's club conversation (TEN-1974): a secondary act with
   K55's mark, beside the row it belongs to. */
.notif-rowclub { flex: none; margin: 0; align-self: center; }
.notif-clubact { gap: 6px; white-space: nowrap; }
.notif-clubact .ktic { width: 16px; height: 16px; }
.notif-x { font: inherit; line-height: 1; color: var(--text-muted); background: none; border: 0; padding: 0 var(--b-space-1); cursor: pointer; }
.notif-x:hover { color: var(--role-critical); }
.notif-dot { width: 6px; height: 6px; border-radius: var(--b-radius-full); background: transparent; margin-top: 7px; flex: none; }
.notif-main { display: flex; flex-direction: column; }
.notif-rowtitle { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.notif-count { margin-left: var(--b-space-2); color: var(--role-attention); font-family: var(--b-font-mono); }
.notif-body { font-size: var(--b-text-xs); color: var(--text-muted); }
.notif-empty { color: var(--text-muted); text-align: center; padding: var(--b-space-4); list-style: none; }

/* ── The organizer plane (TEN-913) — stop-light surface, never the
      attention-gradient (the oracle's two-palette law) ───────────────── */
.mgwrap { max-width: 720px; margin: 0 auto; }
/* The archive stamp (TEN-1592) — the sentence every de-linked legacy console
   page wears above its chrome. Deliberately NOT a warning: nothing is broken
   and nothing is refused, so it takes the muted voice a note takes, not the
   stop-light one an error would. Quiet enough that a competition organizer
   working here is not nagged on every page; present enough that somebody who
   arrived from an old bookmark reads it before the tabs. */
.ss-archived {
  color: var(--text-muted); font-size: var(--b-text-sm);
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
  padding: var(--b-space-2) var(--b-space-3);
  margin-bottom: var(--b-space-2);
}
.mgev { margin-left: auto; color: var(--text-muted); }
.mgnav { text-align: right; margin-bottom: var(--b-space-2); }
.flash-ok { background: var(--good-a); color: var(--good); border: var(--b-border-w) solid var(--good); }
.recbox {
  background: var(--surface-raised); border-left: 3px solid var(--warn);
  border-radius: var(--b-radius-md); padding: var(--b-space-3); margin-bottom: var(--b-space-4);
}
.recrow { display: flex; align-items: center; gap: var(--b-space-2); padding: var(--b-space-2) 0; flex-wrap: wrap; }
.rbadge {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  border-radius: var(--b-radius-full); padding: 1px var(--b-space-2);
}
.rbadge.owes { color: var(--warn); background: var(--amber-a); }
.rbadge.unconfirmed { color: var(--lb); background: var(--blue-a); }
.rbadge.unlinked { color: var(--text-muted); background: var(--surface-overlay); }
.rbadge.overcap { color: var(--bad-fg); background: var(--bad-a); }
.recnote { font-size: var(--b-text-xs); color: var(--text-faint); }
.rbtn {
  font: inherit; font-size: var(--b-text-sm); cursor: pointer;
  /* Button face per the button card (Mark 2026-09-07): condensed uppercase. */
  font-family: var(--b-font-heading); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-3);
}
.rbtn.primary { background: var(--role-primary); color: var(--ink-on-accent); border: 0; }

/* ── The entries block + the Entry Ledger (TEN-1443) ─────────────────────── */
/* The state pill's date line. Tiny and muted on purpose: it is provenance for
   the pill above it, not a column of its own — it must never out-shout the
   state it dates. */
.statecell { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.state-at { font-size: var(--b-text-xs); color: var(--text-faint); font-family: var(--b-font-mono); }
/* A name the directory could not resolve is the raw subject id. It is printed
   (a staff surface is exactly where an unresolvable id is worth seeing) but it
   is NOT dressed as a name: monospace says "identifier", the muted colour says
   "not a person's name", and the title says which. */
.nm-unresolved { font-family: var(--b-font-mono); font-size: var(--b-text-xs); color: var(--text-faint); }
.ledger-who { font-family: var(--b-font-heading); font-size: var(--b-text-xl); font-weight: 800; }
.ledger-who.ledger-unresolved { font-family: var(--b-font-mono); font-size: var(--b-text-md); font-weight: 600; color: var(--text-muted); }
.ledger-id { font-family: var(--b-font-mono); font-size: var(--b-text-xs); color: var(--text-faint); }
.ledger-at { font-size: var(--b-text-xs); color: var(--text-faint); font-family: var(--b-font-mono); }
.ledger-amt { font-family: var(--b-font-mono); font-weight: 700; }
.rbtn.danger { border-color: var(--role-alert, var(--border-strong)); color: var(--role-alert, var(--text-primary)); }
.rbtn:hover { filter: brightness(1.08); }
a.rbtn { display: inline-block; text-decoration: none; }
/* Withdraw, on the register screen (TEN-1313). It asks before it acts —
   the fold takes no silent new round after a withdrawal, so this is one of
   the few genuinely irreversible things an athlete can do to themselves.
   The confirm is a second GET on the same screen: no script, and the
   consequence sits next to the button. */
.wdrawlink { display: inline-block; margin-top: var(--b-space-3); font-size: var(--b-text-sm); color: var(--text-muted); }
.wdrawbox {
  margin-top: var(--b-space-4); padding: var(--b-space-3);
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
}
.wdrawq { font-weight: var(--b-weight-semibold); }
.wdrawnote { margin-top: var(--b-space-1); font-size: var(--b-text-sm); color: var(--text-faint); }
.wdrawrow { display: flex; gap: var(--b-space-2); align-items: center; margin-top: var(--b-space-3); }
.wdrawrow form { margin: 0; }
.regdecline { margin-top: var(--b-space-2); }
.regpolicybox { background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); padding: var(--b-space-3); margin-bottom: var(--b-space-4); }
.polform { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; }
.polfee { display: inline-flex; align-items: center; gap: var(--b-space-1); font-size: var(--b-text-sm); color: var(--text-muted); }
.polfee input { width: 6rem; font: inherit; background: var(--surface-overlay); color: var(--text-primary); border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm); padding: var(--b-space-1) var(--b-space-2); }
.polhint { flex-basis: 100%; font-size: var(--b-text-xs); color: var(--text-faint); }
/* The registration settings block, completed (TEN-1442). The form went from
   seven controls to twenty-four, so it stops being one wrapped line of chips
   and becomes labelled ROWS — .polrow is one axis (Price, Member rates,
   Covered by, Early-bird, Payment, Door), .polsub indents the three that are
   adjustments to the entry fee above them.

   Every control here wears the kit (.chip, .polfee, .polsel, .rbtn) — Mark's
   standing ruling, the same one manage-offerings.html states. .polsel is
   .polfee's box on a <select>, so the two sit at one height in a row. */
.polrow { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; }
.polrow .seclbl.sub { margin: 0; min-width: 7.5rem; color: var(--text-faint); }
.polsub { padding-left: var(--b-space-3); border-left: var(--b-border-w) solid var(--border-subtle); }
.polmrow { display: inline-flex; gap: var(--b-space-2); align-items: center; }
.polsel, .polrow input[type=text], .polrow input[type=date] {
  font: inherit; font-size: var(--b-text-sm);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-2);
}
.polwide { width: 10rem; }
.polnarrow { width: 6rem; }
.polhint.inline { flex-basis: auto; }
/* ── The registration form, round 2 (TEN-1448) ───────────────────────────
   THE FEE BLOCK IS A FIELDSET, and both of its state attributes matter.
   `hidden` is what a reader sees; `disabled` is what the form POSTS — a
   disabled fieldset submits none of its controls, which is how the block can
   now be rendered in every state (so a script can reveal it without a
   round-trip) while keeping the property TEN-1442 pinned: a free event never
   posts the fee fields it is not showing. The !important is not decoration —
   .polrow sets `display: flex`, and a display rule beats [hidden] every time
   (the trap that ate three UAT rounds). */
.polform fieldset { border: 0; margin: 0; padding: 0; min-width: 0; display: contents; }
.polform [hidden] { display: none !important; }
/* A section title with an info icon beside it (item 6). The tips replaced two
   .polhint lines: the same sentences, out of the way until wanted. */
.polrow .seclbl.sub { display: inline-flex; align-items: center; gap: 0; }
/* The two pickers, sized for this form. The Covered-by picker owns its row
   and may grow; a member-rate picker is ONE of four controls on its row and
   must not. Ordered narrower-last on purpose: `.polmrow` sits inside a
   `.polrow`, so both selectors match a row picker at equal specificity and
   the later one wins. */
.polrow .tokpick { flex: 1 1 14rem; min-width: 12rem; max-width: 100%; }
.polrow .tokpick-list { max-height: 12rem; }
/* Member-rate rows (item 2): one line per rate, wrapping rather than
   overflowing — a row is four controls wide and the console is not. */
.polmrow { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; }
.polmrow .tokpick { flex: 0 1 16rem; min-width: 10rem; }
/* The row's raw <select> — the scriptless control, and NOTHING once the
   picker enhances it. Sized with `width`, never `min-width`: picker.js hides
   the select by adding `.tokpick-source`, whose rule is `width: 1px
   !important`, and a min-width here would beat that !important and leave a
   160px listbox sitting beside the chips it was replaced by. */
.polmrow .polorg { flex: 0 1 16rem; width: 16rem; }
/* Add / remove, the week-row builder's own two shapes (TEN-1429). */
.polrowops { flex-basis: 100%; display: flex; gap: var(--b-space-2); align-items: center; }
.polx {
  font: inherit; font-size: var(--b-text-sm); font-weight: 700; line-height: 1;
  cursor: pointer; background: none; border: 0; padding: var(--b-space-1);
  color: var(--text-muted);
}
.polx:hover { color: var(--role-danger); }
.poladd {
  font: inherit; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); cursor: pointer;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-3);
}
.poladd:hover { filter: brightness(1.08); }
/* The card-payments warning (item 7). The kit's attention notice, not a
   bespoke box — .notice--attention is the declared rung for "this will bite
   you later", and it renders only on a priced event because a free one
   collects nothing to be unable to collect. */
.polnotice { margin-top: var(--b-space-3); margin-bottom: 0; }
.mgsheets { display: flex; flex-direction: column; gap: var(--b-space-2); margin-bottom: var(--b-space-4); }
.sheetform { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; }
.sheetform input[type=text] { flex: 1; min-width: 12rem; font: inherit; background: var(--surface-raised); color: var(--text-primary); border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm); padding: var(--b-space-2); }
.sheetform select { font: inherit; background: var(--surface-raised); color: var(--text-primary); border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm); padding: var(--b-space-2); }
.sheethint { flex-basis: 100%; font-size: var(--b-text-xs); color: var(--text-faint); }
.mgtable { display: flex; flex-direction: column; gap: var(--b-space-1); }
.mghead { display: grid; grid-template-columns: 1.2fr 1fr 1.2fr 0.4fr; font-size: var(--b-text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-faint); padding: 0 var(--b-space-3); }
.mgrow { display: grid; grid-template-columns: 1.2fr 1fr 1.2fr 0.4fr; align-items: center; gap: var(--b-space-2); background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); padding: var(--b-space-2) var(--b-space-3); }
.status { font-size: var(--b-text-sm); color: var(--text-muted); }
.status.paid, .status.committed { color: var(--good); }
.status.owes, .status.owing { color: var(--warn); }
.status.invited { color: var(--lb); }
.paycell { display: flex; align-items: center; gap: var(--b-space-2); }
.capline { font-family: var(--b-font-mono); color: var(--text-muted); margin-bottom: var(--b-space-2); }
.doorcount { color: var(--good); font-weight: var(--b-weight-semibold); margin-bottom: var(--b-space-2); }
.doorlist { display: flex; flex-direction: column; gap: var(--b-space-2); }
/* The door's rows are PEOPLE (TEN-1766). The face is the console's own person
   disc — `.console-person-av` / `.console-person-initial`, reused rather than a
   second avatar idiom — and the name leads the text block with the handle and
   the row's meta under it. `.doorwho-unresolved` is the honest gap: the
   directory could not say, so the row says so quietly instead of printing the
   subject id at somebody standing in a doorway. */
.doorrow { display: flex; align-items: center; gap: var(--b-space-3); flex-wrap: wrap; }
.doorav { flex: none; }
.doortext { display: flex; flex-direction: column; min-width: 0; }
.doorwho { font-weight: var(--b-weight-semibold); }
.doorwho-unresolved { font-weight: var(--b-weight-normal); color: var(--text-faint); font-style: italic; }
.doorhandle { font-size: var(--b-text-xs); color: var(--text-muted); }
.doormeta { font-size: var(--b-text-xs); color: var(--text-faint); }
.doorrow .dooract { margin-left: auto; }
/* THE CHECK-IN METHOD MARK, `ktmethod` (TEN-1972, board D). Door and scan are
   a glyph in muted ink; APP adds the word and a DASHED outline — the "nobody
   proved this" stroke, deliberately not a colour (K29). */
.ktmethod { display: inline-flex; align-items: center; gap: 3px; font-size: var(--b-text-xs); color: var(--text-muted); padding: 1px 6px 1px 4px; border-radius: var(--b-radius-full); border: var(--b-border-w) solid transparent; }
.ktmethod .ktic { width: 15px; height: 15px; }
.ktmethod-app { border: var(--b-border-w) dashed var(--border-strong); color: var(--text-primary); font-weight: var(--b-weight-semibold); }
/* Mark as no-show: band 1's ICON row act at the quietest tier (K12), a square
   control the height of the row's other acts. */
.doornoshow-act { padding: 0; width: var(--control-h); color: var(--text-muted); }
.doornoshow-act:hover { color: var(--role-danger); }
.doornoshow { display: flex; flex-direction: column; gap: var(--b-space-3); }
.doornoshow-who { display: flex; align-items: center; gap: var(--b-space-2); margin: 0; }
.doornoshow-session { margin: 0; color: var(--text-muted); }
.drow { display: flex; align-items: center; gap: var(--b-space-3); background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); padding: var(--b-space-3); flex-wrap: wrap; }
.drow.in { opacity: 0.7; }
.ddot { width: 10px; height: 10px; border-radius: var(--b-radius-full); flex: none; }
.ddot.on { background: var(--ok); }
.ddot.warn { background: var(--warn); }
.ddot.new { background: var(--lb); }
.dlabel { font-family: var(--b-font-mono); font-size: var(--b-text-xs); color: var(--text-muted); }
.dacts { margin-left: auto; display: flex; gap: var(--b-space-2); flex-wrap: wrap; }
.dacts form { display: inline-flex; gap: var(--b-space-2); }

/* ── My Result ──────────────────────────────────────────────────────── */
.mrcard {
  max-width: 560px; margin: 0 auto;
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-lg); padding: var(--b-space-5);
}
.mrhdr { display: flex; align-items: baseline; gap: var(--b-space-3); }
.mreyebrow { font-size: var(--b-text-sm); color: var(--text-muted); }
.mrh1 { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-2xl); }
.mrlens { margin-left: auto; color: var(--lb); font-weight: var(--b-weight-semibold); }
.mrme { display: flex; align-items: center; gap: var(--b-space-3); margin: var(--b-space-4) 0; }
.mrava {
  width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--b-radius-full); background: var(--surface-overlay);
  font-weight: var(--b-weight-bold); color: var(--lv);
}
.mrnm { font-weight: var(--b-weight-semibold); }
.mraff { color: var(--text-muted); font-size: var(--b-text-sm); }
.mrfinal { margin-left: auto; color: var(--role-attention); font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.mrhero {
  background: var(--night); color: #F0F0F5; border-radius: var(--b-radius-md);
  padding: var(--b-space-4); margin-bottom: var(--b-space-3);
}
:root[data-theme="light"] .mrhero, :root[data-theme="hc"] .mrhero { background: var(--surface-overlay); color: var(--text-primary); border: var(--b-border-w) solid var(--border-strong); }
.mrvoltage { color: var(--la); font-size: var(--b-text-xs); letter-spacing: 0.12em; text-transform: uppercase; font-weight: var(--b-weight-bold); }
.mrfixture { color: var(--text-muted); margin-top: var(--b-space-2); }
.mrname { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-xl); }
.mrstats { display: flex; gap: var(--b-space-6); }
.mrstat { display: flex; flex-direction: column; }
.mrstat .v { font-family: var(--b-font-mono); font-weight: 700; font-size: var(--b-text-3xl); color: var(--gold); }
.mrstat .k { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.mrstat .sub { font-size: var(--b-text-xs); color: var(--text-faint); }
.mrprog { color: var(--text-muted); margin: var(--b-space-3) 0; }
.mrtodayhd { font-family: var(--b-font-heading); font-weight: var(--b-weight-bold); letter-spacing: 0.06em; text-transform: uppercase; font-size: var(--b-text-sm); color: var(--text-muted); }
.mrhint { font-size: var(--b-text-xs); color: var(--text-faint); margin-bottom: var(--b-space-3); }
.mrlist { display: flex; flex-direction: column; gap: var(--b-space-2); }
.mrrow {
  display: grid; grid-template-columns: 2.2rem 1fr auto auto auto; align-items: center; gap: var(--b-space-2);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  padding: var(--b-space-3);
}
.mrrow:hover { background: var(--hover); }
.mrrow.podium { border-color: var(--sportscore-podium-highlight, var(--role-attention)); }
.mrmedal {
  width: 1.8rem; height: 1.8rem; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--b-radius-full); background: var(--surface-raised);
  font-family: var(--b-font-mono); font-weight: 700; font-size: var(--b-text-sm);
}
.mrmedal.gold { color: var(--ink-on-primary); background: var(--gold); }
.mrlname { font-weight: var(--b-weight-semibold); }
.mrlsub { color: var(--text-faint); font-size: var(--b-text-sm); }
.mrplace { font-family: var(--b-font-mono); font-weight: var(--b-weight-bold); }
.mrplace .of { color: var(--text-faint); font-weight: var(--b-weight-normal); font-size: var(--b-text-sm); }
.mrnav { display: flex; gap: var(--b-space-3); justify-content: center; margin-top: var(--b-space-4); }
.mrnav .on { font-weight: var(--b-weight-bold); color: var(--text-primary); }
.mrnav a { color: var(--lb); }

/* The oracle's tactile signature: instant pressed ack (§09). */
.evcard:active, .chip:active, a.node:active, .mode:active, .rbtn:active,
.regcta:active, .paybtn:active, .mrrow:active, .sessmain:active {
  transform: scale(0.985); filter: brightness(1.06);
}

/* The board share popover (TEN-920): CSS-toggle + the faux-QR renderer. */
.bqr { margin-left: auto; }
.bqr-toggle { position: absolute; opacity: 0; pointer-events: none; }
.bqrbtn {
  cursor: pointer; font-family: var(--b-font-heading); text-transform: uppercase;
  letter-spacing: 0.04em; font-size: 11px; font-weight: 700; color: var(--t2, #A8A8B2);
  border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; padding: 6px 10px;
}
.bqrpop {
  display: none; position: absolute; right: 15px; top: 60px; z-index: 40;
  background: #fff; color: #000; border-radius: 12px; padding: 14px; text-align: center;
  box-shadow: 0 16px 40px rgba(0,0,0,0.6);
}
.bqr-toggle:checked ~ .bqrpop { display: block; }
.bqrpop .t { font-family: var(--b-font-heading); text-transform: uppercase; font-size: 11px; font-weight: 800; }
.bqrpop .qrwrap { width: 200px; margin: 10px auto; }
.bqrpop .s { font-size: 11px; color: #555; }

/* ── THE ATTACHED PANEL STAYS INSIDE THE SCREEN (TEN-1879) ──────────────────
   The cover half of the family pin dismiss.js writes. Every panel in the
   `[data-b-dismiss-toggle]` family — the bell, the avatar menu, the hamburger,
   the room gear, the conversation row's ⋯ — is anchored to a CONTROL, and a
   control near one edge of its bar cannot know how much room the panel has on
   the other side. Mark's M2 UAT screenshot is the bell's 320px panel starting
   at -9px on a 375px phone, its own heading reading "…tions".

   THE SCRIPT WRITES THE TWO NUMBERS NO STYLESHEET CAN SEE, and they are all it
   writes: `--b-panel-slide`, how far the panel must move to be on screen, and
   `--b-panel-room`, what the screen has if the panel is wider than all of it.
   THIS BLOCK DECIDES WHAT THEY MEAN — that the correction is a translate rather
   than a re-anchoring, so the seam with the control (K15: one outline, square at
   the joining edge) is not redrawn by a panel that merely moved. `translateX`
   and not `left` for the same reason it is not `margin-left`: this is an overlay
   sliding over a page, and nothing else on the row may be re-laid out by it.

   THE ATTRIBUTE IS THE WHOLE GATE. A panel that already fits is never stamped,
   so on a desktop these declarations do not exist — which is what makes
   "desktop unchanged" a measurement instead of an intention. */
[data-b-panel-pin] {
  transform: translateX(var(--b-panel-slide, 0px));
  max-width: var(--b-panel-room, none);
}

/* ── Hamburger nav (TEN-971): top-left CSS-toggle over the spine ─────── */
.hamnav { position: relative; display: flex; align-items: center; }
.hamnav-float { position: fixed; left: var(--b-space-4); top: var(--b-space-4); z-index: 50; }
.hamnav-toggle { position: absolute; opacity: 0; pointer-events: none; }
.hamnav-btn {
  cursor: pointer; font-size: var(--b-text-lg); line-height: 1; color: var(--text-muted);
  width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--b-radius-sm);
}
.hamnav-btn:hover { background: var(--hover); color: var(--text-primary); }
/* TEN-1433: see .notif-toggle:focus-visible above — same reason, same rule. */
.hamnav-toggle:focus-visible ~ .hamnav-btn {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px;
}
.hamnav-float .hamnav-btn {
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-full); box-shadow: var(--b-shadow-md);
}
.hamnav-panel {
  display: none; position: absolute; left: 0; top: calc(100% + var(--b-space-2)); z-index: 60;
  min-width: 200px; background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-md);
  box-shadow: var(--b-shadow-lg); padding: var(--b-space-2);
}
.hamnav-toggle:checked ~ .hamnav-panel { display: block; }
.hamnav-panel ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.hamnav-panel li { display: flex; flex-direction: column; }
.hamnav-panel a, .hamnav-panel .b-current {
  padding: var(--b-space-2) var(--b-space-3); border-radius: var(--b-radius-sm);
  font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
}
.hamnav-panel a { color: var(--text-primary); }
.hamnav-panel a:hover { background: var(--hover); }
.hamnav-panel .b-current { color: var(--role-primary); }
.hamnav-panel ul ul { padding-left: var(--b-space-3); }

/* ── Avatar menu (TEN-971): the oracle-style top-right identity chrome ── */
/* Flex so the header org tag (TEN-1417) sits beside the avatar button on
   one line — the toggle and the panel are both position:absolute, so this
   change is invisible to either. */
.avmenu { position: relative; display: inline-flex; align-items: center; gap: var(--b-space-2); }
.avmenu-toggle { position: absolute; opacity: 0; pointer-events: none; }
/* The header org tag (TEN-1417): the org id, by the same pill recipe as
   the theme switch below (.avmenu-theme) so the two small header chips
   read as one family. Handle only, never the display name — the header
   has no room for it and the console page it links to is one click away. */
.avmenu-orgtag {
  display: inline-flex; align-items: center; cursor: pointer;
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-full);
  background: var(--surface-overlay); color: var(--lv);
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  padding: 0.15rem var(--b-space-2); white-space: nowrap;
}
.avmenu-orgtag:hover { filter: brightness(1.1); }
.avmenu-btn {
  cursor: pointer; width: var(--b-avatar-md); height: var(--b-avatar-md); display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--b-radius-full); background: var(--surface-overlay); color: var(--lv);
  font-family: var(--b-font-heading); font-weight: var(--b-weight-bold); font-size: var(--b-text-sm);
  border: var(--b-border-w) solid var(--border-strong);
}
.avmenu-btn:hover { filter: brightness(1.1); }
/* TEN-1433: see .notif-toggle:focus-visible above — same reason, same rule. */
.avmenu-toggle:focus-visible ~ .avmenu-btn {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px;
}
/* The header wears your actual face (UAT 2026-08-13): the image covers the
   initials, which stay underneath as the fallback if it ever fails to load. */
.avmenu-btn { position: relative; overflow: hidden; }
.avmenu-av { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.avmenu-panel {
  display: none; position: absolute; right: 0; top: calc(100% + var(--b-space-2)); z-index: 60;
  min-width: 180px; background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-md);
  box-shadow: var(--b-shadow-lg); padding: var(--b-space-3); display: none; flex-direction: column; gap: var(--b-space-2);
}
.avmenu-toggle:checked ~ .avmenu-panel { display: flex; }
/* The name line wears the theme pill at its right edge, the @handle in tiny
   text beneath (TEN-1112, Mark's layout). One identity block (TEN-1121):
   the panel's column gap applies around the name+handle pair, never
   between them — the handle sits at line-height distance under the name. */
.avmenu-identity { display: flex; flex-direction: column; gap: 0.1rem; }
.avmenu-id { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2); }
.avmenu-name { font-weight: var(--b-weight-semibold); margin: 0; }
.avmenu-theme {
  display: inline-flex; align-items: center; gap: 0.3em; cursor: pointer;
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-full);
  background: transparent; color: var(--text-muted);
  font-size: var(--b-text-xs); padding: 0.15rem var(--b-space-2); white-space: nowrap;
}
.avmenu-theme:hover { background: var(--hover); color: var(--text-primary); }
.avmenu-handle { margin: 0; font-size: var(--b-text-xs); color: var(--text-muted); }
/* The session line (TEN-1417): "Session: @{org id} ✕", same row and font
   size as the handle above it (it lives inside the same .avmenu-handle
   element) — the org id keeps its link-blue, the ✕ is a bare inline
   button that inherits the row's font (buttons don't, by default) so it
   reads as the SAME size, not a smaller control bolted on. */
.avmenu-session-org { color: var(--lb); }
.avmenu-session-x {
  font: inherit; cursor: pointer; border: none; background: none; padding: 0;
  margin-left: 0.3em; color: var(--text-muted); line-height: 1;
}
.avmenu-session-x:hover { color: var(--text-primary); }
.avmenu-link { color: var(--lb); font-size: var(--b-text-sm); }
/* THE GROUPED BLOCKS (TEN-1855, K48 — PROPOSED; Mark's M1 UAT finding 5).
   Eleven flat links in one column read as a list of words: every row cost the
   same to scan, so finding Purchases meant reading Security. Two shaded blocks
   and a full-width row hover fix both halves of that — the ground says which
   rows belong together, and the hover says a row is a target.

   THE SHADE IS THE PANEL'S OWN NEXT STEP, not a new colour: --surface-overlay
   over the panel's --surface-raised, which is exactly the pair the icon row
   above already uses inside this same panel. A third ground here would be a
   second vocabulary for "one step in" on the one surface that has both.

   THE ROW IS FULL-WIDTH BY BEING THE BLOCK'S CHILD, so the hover runs to the
   block's edges without a negative margin: the block carries the corners and a
   sliver of vertical padding, the row carries the padding a pointer needs, and
   nothing has to know the panel's own padding to line up with it. */
.avmenu-group {
  display: flex; flex-direction: column;
  background: var(--surface-overlay);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) 0;
  overflow: hidden; /* the first and last row's hover stops at the rounded corner */
}
.avmenu-group .avmenu-link {
  display: block; padding: var(--b-space-1) var(--b-space-2);
  /* A coarse pointer gets a real row rather than a line of text — the same
     floor the header's three icons grow to (see @media (pointer: coarse)). */
  min-height: 1.9rem; line-height: 1.5;
}
.avmenu-group .avmenu-link:hover { background: var(--hover); color: var(--text-primary); }
@media (pointer: coarse) {
  .avmenu-group .avmenu-link { min-height: 2.6rem; }
}
.avmenu-signout { margin: 0; }
.avmenu-signout button {
  width: 100%; font: inherit; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); cursor: pointer;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-2);
}
.avmenu-signout button:hover { filter: brightness(1.08); }
.avmenu-signin { color: var(--lb); font-weight: var(--b-weight-semibold); }

/* ── Account console hub (TEN-971) ─────────────────────────────────────*/
.acct-card {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-4); margin-bottom: var(--b-space-3);
}
.acct-name { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-xl); margin: 0 0 var(--b-space-2); }
.acct-row { margin: 0 0 var(--b-space-1); font-size: var(--b-text-sm); }
.acct-label { color: var(--text-muted); margin-right: var(--b-space-2); }
.acct-value { font-family: var(--b-font-mono); }
.acct-profile .cmc-account-signout { margin-top: var(--b-space-3); }

/* ── A CLUB ASKING TO SHOW YOU (TEN-1692, board S5) ───────────────────────
   The card IS the question, so it is drawn as one block per club rather than
   as a row in a list: the sentence, then the club's own words as a QUOTATION
   (they are somebody else's writing about you, and the page should never let
   that read as yours), then the acts. */
.acct-listing {
  display: flex; flex-direction: column; gap: var(--b-space-2);
  padding: var(--b-space-3) 0;
}
.acct-listing + .acct-listing { border-top: var(--b-border-w) solid var(--border-subtle); }
.acct-listing-say { margin: 0; }
.acct-listing-words {
  margin: 0; display: flex; flex-direction: column; gap: 2px;
  padding: var(--b-space-2) var(--b-space-3); max-width: 62ch;
  border-left: 3px solid var(--border-strong);
  background: var(--surface-overlay); border-radius: var(--b-radius-sm);
}
.acct-listing-role { font-weight: var(--b-weight-semibold); font-size: var(--b-text-sm); }
.acct-listing-bio { font-size: var(--b-text-sm); line-height: 1.5; }
.acct-listing-acts { display: flex; flex-wrap: wrap; gap: var(--b-space-2); }
.acct-stubs { display: flex; flex-direction: column; gap: var(--b-space-3); }
.acct-stub-title { font-weight: var(--b-weight-semibold); margin: 0 0 var(--b-space-1); }
.acct-stub-body { color: var(--text-muted); font-size: var(--b-text-sm); margin: 0; }

/* ── CIRCLES (TEN-1843, board A5) — TEN-1856's fix, Mark's M1 UAT-C ───────
   The org card's stacked groups (identity, the "Next:" line, the held pills)
   sat flush against the Schedule/Rooms/Storefront row below them — Mark's
   screenshot: the Premium pill touching the button row with no air at all.
   `.acct-listing`'s own rhythm, two rules up, is this family's answer to
   exactly this question — stacked blocks in one card, separated by
   `gap: var(--b-space-2)` — so the org card takes it rather than inventing a
   second one. */
.circles-org { display: flex; flex-direction: column; gap: var(--b-space-2); }

/* ── PLACES (TEN-1883, board A4) — where the reader trains ────────────────
   THE CARDS ARE `.ktprev` AND THIS BLOCK STYLES NONE OF THEM. Board A4's card
   is R6's entity card at its PREVIEW rung — the rung board A1's next-up card
   already mounts — so what a place card looks like is that family's decision
   and this region owns only the page: the rhythm of the list, the note, the
   zero state's door, and the one thing the board draws that the family does
   not have.
   THAT ONE THING IS THE DASHED CARD. Board A4 draws a dashed card under the
   list carrying the sentence that says how a place gets onto it, and dashed is
   K12's shape for STAGED — a claim that does not exist yet — which is exactly
   what a slot for a place the reader has not earned is (the same reading
   `.ktpcard-pinbox`'s dashed pin already takes for an unclaimed place). It
   borrows the entity card's own box metrics rather than inventing a second
   card: same radius, same padding, same raised surface, with the border
   dashed and the text at the secondary tier. It is NOT `--wattage-inert`:
   nothing here is past, archived or switched off. */
.athplaces { display: flex; flex-direction: column; gap: var(--b-space-3); }
.athplaces-list { display: flex; flex-direction: column; gap: var(--b-space-3); }
.athplaces-door { align-self: flex-start; }
.athplaces-hint {
  margin: 0; padding: 16px; text-align: center;
  color: var(--text-muted); font-size: var(--b-text-sm);
  background: var(--surface-raised); border-radius: var(--control-r);
  border: 1px dashed var(--border-subtle);
}

/* ---- The data-grid kit skin (TEN-974, ADR-0017 §3) -------------------- */
/* Holds across all three variants: everything rides surface/border/text
   roles. Duotone stays subtle — the alternate row is a 45% mix toward the
   overlay surface, never the full alternate surface. */
.ss-grid { display: flex; flex-direction: column; gap: var(--b-space-3); }
.ss-grid-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); }
.ss-grid-search { position: relative; flex: 1 1 14rem; max-width: 22rem; }
.ss-grid-q {
  width: 100%; padding: var(--b-space-2) var(--b-space-5) var(--b-space-2) var(--b-space-2);
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}
.ss-grid-qclear {
  position: absolute; right: var(--b-space-1); top: 50%; transform: translateY(-50%);
  background: none; border: 0; color: var(--text-muted); font-size: var(--b-text-lg);
  cursor: pointer; padding: 0 var(--b-space-1);
}
.ss-grid-qclear:hover { color: var(--text-primary); }
.ss-grid-filterbtn, .ss-grid-chip, .ss-grid-export {
  display: inline-flex; align-items: center; gap: var(--b-space-1);
  padding: var(--b-space-1) var(--b-space-2);
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  font-size: var(--b-text-sm); cursor: pointer; text-decoration: none;
}
.ss-grid-filterbtn:hover, .ss-grid-export:hover { border-color: var(--border-strong); }
.ss-grid-filtericon::before { content: "\2630"; font-size: var(--b-text-sm); }
.ss-grid-chipform { display: inline; margin: 0; }
.ss-grid-chip { background: var(--b-accent); color: var(--b-accent-fg); border-color: transparent; }
.ss-grid-exports { display: inline-flex; gap: var(--b-space-1); margin-left: auto; }

/* Styled tooltips: any [data-tip] control, and clipped cells (marked by
   the kit's delegated clip check — never a native title).

   THE TIP IS THE TOPMOST LAYER THIS COVER DRAWS (TEN-1893, Mark's UAT-3: the
   header's Conversations and Notifications tips rendered BEHIND the athlete
   title row on every athlete page). It was z-index 40 — the same tier as the
   sticky title panels (`.athd`, the sticky picker drops) — and neither the
   header nor the control it hangs on makes a stacking context, so the tip and
   the title row met in the ROOT context at one number and DOM order broke the
   tie: the title row comes after the header, so it painted over the tip.
   Nothing about either page was wrong; the family had no tier for a tip.

   The cover's tiers, as the rules below spell them, lowest first: 1–3 in-flow
   lifts (a sticky table head, a row's own act over its link), 10–20 sticky
   bars, 30–40 attached drops and the sticky title panels, 50 the fixed floats
   (the hamburger float, the theme toggle), 60 panels and modals. A tip is
   transient, pointer-driven and never covers anything the reader is using, so
   it sits above all of them — 70, one tier clear of the modals, so a tip on a
   control inside a modal reads over the modal's own surface. One number for
   the whole family, rather than a per-page lift under whichever panel was
   caught covering it (browsergate: chromestacking_test.go). */
[data-tip], .ss-cell-clip[data-clipped] { position: relative; }
[data-tip]:hover::after, [data-tip]:focus-visible::after, .ss-cell-clip[data-clipped]:hover::after {
  content: attr(data-tip); position: absolute; left: 0; bottom: calc(100% + 4px);
  z-index: 70; white-space: nowrap; pointer-events: none;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-2); font-size: var(--b-text-xs);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.35);
}
.ss-cell-clip[data-clipped]:hover::after { content: attr(data-full); white-space: normal; min-width: 12rem; max-width: 24rem; }
/* A tip that is a SENTENCE rather than a label (TEN-1448). The rule above is
   `white-space: nowrap`, which is right for "Invitations" and wrong for the
   two explanations that moved off the registration form into info icons —
   nowrap made them one line wider than the viewport. Opt in per control:
   <span class="polinfo" data-tip="…" class="tip-wrap">. */
[data-tip].tip-wrap:hover::after, [data-tip].tip-wrap:focus-visible::after { white-space: normal; min-width: 14rem; max-width: 22rem; }
/* The info icon those tips hang on: a small ringed "i" that reads as
   explanation rather than as a control. Not focusable — the tip repeats what
   a title would say and adds no fact a keyboard reader needs to hunt for. */
.polinfo {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; margin-left: var(--b-space-1);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-full);
  font-family: var(--b-font-heading); font-size: 9px; font-weight: 700; font-style: normal;
  line-height: 1; color: var(--text-muted); cursor: help; flex: none;
}
.polinfo:hover { color: var(--text-primary); border-color: var(--lb); }

/* The table: fixed colgroup (zero reflow), frozen header inside the
   grid's OWN scroll container — the page never scrolls sideways. */
.ss-grid-scroll {
  overflow: auto; max-height: 32rem;
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.ss-grid-table { width: 100%; min-width: 56rem; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
.ss-col-date { width: 9.5rem; } .ss-col-payee { width: 11rem; } .ss-col-desc { width: auto; min-width: 16rem; }
.ss-col-method { width: 12rem; } .ss-col-amount { width: 7rem; } .ss-col-status { width: 7rem; }
.ss-grid-table thead th {
  position: sticky; top: 0; z-index: 10; text-align: left;
  background: var(--surface-overlay); color: var(--text-muted);
  font-size: var(--b-text-xs); text-transform: uppercase; letter-spacing: 0.06em;
  padding: var(--b-space-2); border-bottom: var(--b-border-w) solid var(--border-strong);
}
.ss-sort { background: none; border: 0; color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit; cursor: pointer; padding: 0; }
.ss-sort:hover { color: var(--text-primary); }
/* THE STATE OWNS ITS OWN HOVER (TEN-1702 sweep; the ruling is written out in
   full at `.ktlist-chip-on`). `.ss-sort:hover` is one specificity step above
   `.ss-sort-on`, so the hover rule decides what a SORTED column head looks like
   under the pointer. It happens to name the same ink today, which is exactly
   why this is worth declaring rather than leaving: the two are one edit from
   disagreeing and nothing would say so. */
.ss-sort-on, .ss-sort-on:hover { color: var(--text-primary); }
.ss-sort-on.ss-sort-asc::after { content: " \2191"; }
.ss-sort-on.ss-sort-desc::after { content: " \2193"; }
.ss-grid-table tbody td {
  padding: var(--b-space-2); font-size: var(--b-text-sm);
  border-bottom: var(--b-border-w) solid var(--border-subtle);
  overflow: hidden;
}
.ss-cell-clip { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The date cell's second line (TEN-1462): the clock that tells two same-day
   attempts apart, subordinate to the day above it. */
.ss-cell-time { display: block; font-size: var(--b-text-xs); color: var(--text-muted); white-space: nowrap; }
.ss-row:nth-child(even) td { background: color-mix(in srgb, var(--surface-overlay) 45%, var(--surface-raised)); }
.ss-row-link { cursor: pointer; }
.ss-row-link:hover td { background: color-mix(in srgb, var(--b-accent) 12%, var(--surface-raised)); }
.ss-row-link:hover td:last-child::after { content: "\203A"; color: var(--text-muted); float: right; }
.ss-row-a { color: inherit; text-decoration: none; }
.ss-cell-amount { text-align: right; font-family: var(--b-font-mono); }
.ss-th-amount { text-align: right !important; }
.ss-badge {
  display: inline-block; padding: 0 var(--b-space-2); border-radius: 999px;
  font-size: var(--b-text-xs); text-transform: uppercase; letter-spacing: 0.04em;
}
.ss-badge-paid { background: color-mix(in srgb, var(--b-success, #16a34a) 18%, transparent); color: var(--text-primary); }
.ss-badge-pending { background: color-mix(in srgb, var(--b-warning) 22%, transparent); color: var(--text-primary); }
.ss-badge-refunded { background: var(--surface-overlay); color: var(--text-muted); }
/* A retired attempt (TEN-1484). It KEEPS ITS ROW — the exports carry it and the
   pager counts it, so hiding it on screen would make the download disagree with
   the grid, which is the one divergence the single-pipeline design exists to
   prevent. What it loses is weight: the badge goes quiet and the money is struck
   through, because the amount is the one cell a reader scans for and a voided
   attempt never cost anything. The Status filter is the way to a history without
   them. */
.ss-badge-voided { background: var(--surface-overlay); color: var(--text-muted); }
.ss-row-voided td { color: var(--text-muted); }
.ss-row-voided .ss-cell-amount { text-decoration: line-through; }
/* The row's own act (TEN-1484's "Remove"). Quiet by default and named by its
   colour only on hover: it sits beside a status badge on every pending row, and
   a destructive-looking control repeated down a column reads as an alarm. */
.ss-rowact {
  font-size: var(--b-text-xs); color: var(--text-muted); text-decoration: none;
  border-bottom: var(--b-border-w) solid var(--border-subtle); margin-left: var(--b-space-2);
}
.ss-rowact:hover, .ss-rowact:focus-visible { color: var(--b-danger, #dc2626); border-bottom-color: currentColor; }
.ss-grid-empty { padding: var(--b-space-5); text-align: center; color: var(--text-muted); margin: 0; }

/* Pager */
.ss-grid-pager { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-3); font-size: var(--b-text-sm); color: var(--text-muted); }
.ss-pager-pages { display: inline-flex; gap: var(--b-space-1); }
.ss-pager-btn, .ss-pager-n {
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  padding: 0 var(--b-space-2); min-width: 1.8rem; cursor: pointer; font-size: var(--b-text-sm);
}
span.ss-pager-n.ss-pager-on { background: var(--b-accent); color: var(--b-accent-fg); border-color: transparent; cursor: default; display: inline-flex; align-items: center; justify-content: center; }
.ss-pager-size { margin-left: auto; display: inline-flex; align-items: center; gap: var(--b-space-1); }

/* The filter modal (v3: ALL filters live here) */
/* Modals ride HIGH, not centered (TEN-1126, Mark's phone UAT): a centered
   dialog puts a picker's live results under the on-screen keyboard, so a
   person typing can't tell realtime results exist. Top-anchored with a
   small offset, a row or two always shows above the keyboard — the
   top-third convention phones use for exactly this reason. */
.ss-grid-modal-wrap { position: fixed; inset: 0; z-index: 60; display: flex; align-items: flex-start; justify-content: center; padding-top: clamp(var(--b-space-4), 8vh, 6rem); }
.ss-grid-modal-scrim { position: absolute; inset: 0; background: rgb(0 0 0 / 0.5); }
/* THE popup-modal chrome (TEN-1230, Mark's ruling): every dialog on the
   console wears exactly this — raised surface, NEUTRAL stroke, one radius,
   one shadow. Never restyle a modal's border per surface; the sanctioned
   variation is .console-modal-danger below, opt-in, for destructive
   confirmations only. (The rule exists because per-modal borders kept
   appearing — a danger tint once leaked onto every modal on the console.) */
.ss-grid-modal {
  position: relative; z-index: 1; width: min(26rem, calc(100vw - 2 * var(--b-space-4)));
  max-height: 85vh; overflow: auto;
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-md); padding: var(--b-space-4);
  box-shadow: 0 12px 40px rgb(0 0 0 / 0.45);
}
.ss-grid-modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--b-space-3); }
.ss-grid-modal-title { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-lg); margin: 0; }
.ss-grid-modal-close { background: none; border: 0; color: var(--text-muted); font-size: var(--b-text-xl); cursor: pointer; }
.ss-grid-modal-form { display: flex; flex-direction: column; gap: var(--b-space-3); }
.ss-grid-field { display: flex; flex-direction: column; gap: var(--b-space-1); font-size: var(--b-text-sm); color: var(--text-muted); flex: 1; }
.ss-grid-field input, .ss-grid-field select {
  padding: var(--b-space-1) var(--b-space-2); background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}
.ss-grid-field-row { display: flex; gap: var(--b-space-2); }
.ss-grid-modal-actions { display: flex; gap: var(--b-space-2); margin-top: var(--b-space-1); }
.ss-grid-apply {
  background: var(--b-accent); color: var(--b-accent-fg); border: 0;
  border-radius: var(--b-radius-sm); padding: var(--b-space-2) var(--b-space-3); cursor: pointer; font-weight: var(--b-weight-semibold);
}
.ss-grid-cancel { background: none; border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm); padding: var(--b-space-2) var(--b-space-3); color: var(--text-primary); cursor: pointer; }

/* ── Finishing the field kit (TEN-1437, Mark's standing ruling the same
   day: "no raw HTML controls. Ever.") ────────────────────────────────────
   The three rules above dressed a control's BOX — background, border,
   radius, padding — and stopped there, which is enough for a text input and
   is not enough for the two controls that draw their own furniture:

     - a <select> keeps `appearance: auto`, so the disclosure arrow is drawn
       by the operating system in the operating system's colour. On a dark
       cover that is a light-grey OS triangle sitting inside our box.
     - a number input draws spin buttons the same way, and they are the
       giveaway that a "Sessions" field and a "Name" field are two different
       kinds of thing rather than one kit.

   Both are switched off and the select's caret is redrawn in CSS from two
   gradients, so it rides `currentColor` and follows the theme — which a
   data-URI arrow could not do. Kept inside `.ss-grid-field` on purpose: the
   token picker's own select (`select[data-tokpick]`, further down) is a
   different control with its own chrome, and this must not reach it. */
.ss-grid-field select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: calc(var(--b-space-2) + 1.1rem);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    right calc(var(--b-space-2) + 0.35rem) center,
    right var(--b-space-2) center;
  background-size: 0.35rem 0.35rem, 0.35rem 0.35rem;
  background-repeat: no-repeat;
}
.ss-grid-field input[type=number] { appearance: textfield; -moz-appearance: textfield; }
.ss-grid-field input[type=number]::-webkit-outer-spin-button,
.ss-grid-field input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* The label wraps its control (`<label class="ss-grid-field">Name <input>`),
   so the caption needs to stop looking like body copy. */
.ss-grid-field { cursor: pointer; }
.ss-grid-field > input, .ss-grid-field > select, .ss-grid-field > textarea { cursor: auto; font: inherit; width: 100%; box-sizing: border-box; }
.ss-grid-field > textarea {
  padding: var(--b-space-1) var(--b-space-2); background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}
.ss-grid-field > input:focus-visible, .ss-grid-field > select:focus-visible, .ss-grid-field > textarea:focus-visible {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 1px;
}
.ss-grid-field-row > .ss-grid-field { min-width: 0; }

/* ── THE LINKS CONTROL (TEN-1796 block 2, PM board L1) ───────────────────
   A list of branded link cards in place of a textarea, on BOTH profile
   editors — the club's Profile tab and a person's community card render one
   definition (modules/cmcauth/templates/links-control.html), so this is one
   block of rules rather than two.

   The textarea is still underneath and still the wire field; it keeps the
   `.ss-grid-field > textarea` treatment above, because with no scripting it
   IS the control and has to look like the fields beside it. */
.ktlinks { cursor: default; gap: var(--b-space-2); }
.ktlinks-head { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2); }
.ktlinks-label { cursor: pointer; }
/* Quieter and shorter than a method act: it adds a row to a list, it does not
   advance the screen (K12's lowest worded rung). */
.ktlinks-add { padding: 2px var(--b-space-2); font-size: var(--b-text-xs); }
.ktlinks-add:disabled { opacity: 0.45; cursor: default; }
.ktlinks-list { display: flex; flex-direction: column; gap: var(--b-space-1); }
/* The card sits a surface step above the block it is in, hairline and all —
   the same distinction the people cards on this tab draw (TEN-1796 block 3). */
.ktlinks-card {
  display: flex; align-items: center; gap: var(--b-space-2);
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-2);
}
/* EDITING IS A REAL STATE and it is drawn as one: the accent edge says which
   row the keyboard is in, and the row grows to hold the field rather than
   opening a second place to look. */
.ktlinks-card.ktlinks-editing { align-items: flex-start; border-color: var(--b-accent); }
/* The mark's well. One size for both families, so a brand mark and a generic
   glyph sit on the same baseline and a list of eight does not look ragged. */
.ktlinks-mark {
  width: 34px; height: 34px; flex-shrink: 0; border-radius: var(--b-radius-sm);
  background: var(--surface-sunken); display: flex; align-items: center; justify-content: center;
}
.ktlinks-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.ktlinks-name { font-weight: var(--b-weight-semibold); color: var(--text-primary); }
/* A URL is the one string on this card guaranteed to be longer than the card.
   Truncated on one line rather than wrapped: the name above already says what
   the destination is, so the address is confirmation and not reading matter. */
.ktlinks-url { font-size: var(--b-text-xs); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ktlinks-act {
  background: transparent; border: 0; padding: 4px; border-radius: var(--b-radius-sm);
  color: var(--text-muted); cursor: pointer; display: inline-flex; align-items: center;
}
.ktlinks-act:hover { color: var(--text-primary); background: var(--surface-overlay); }
.ktlinks-act:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 1px; }
.ktlinks-x { font-size: 18px; line-height: 1; }
.ktlinks-empty { margin: 0; color: var(--text-muted); font-size: var(--b-text-sm); }
.ktlinks-inlabel { display: flex; flex-direction: column; gap: 2px; font-size: var(--b-text-xs); color: var(--text-muted); }
.ktlinks-input {
  width: 100%; box-sizing: border-box; font: inherit;
  padding: var(--b-space-1) var(--b-space-2);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}
.ktlinks-input:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 1px; }
/* The live-resolved name holds its line whether or not anything resolved yet,
   so the row does not jump as somebody types the first character of a host. */
.ktlinks-resolved { min-height: 1.2em; }
.ktlinks-acts { display: flex; gap: var(--b-space-1); margin-top: var(--b-space-1); }
.ktlinks-note { margin: 0 0 var(--b-space-1) 0; }
/* The sprite is geometry, never a picture. Out of flow and zero-sized so it
   cannot add a gap to the block it is declared in. */
.ktlinks-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ── THE PASSES LIST: THE HIERARCHICAL CARD FAMILY (TEN-1797, board P1) ───
   K36's family, and it REPLACES the private row grammar TEN-1437 shipped here
   (`.off-row` on top-borders, `.off-tag` chips of its own invention, `.off-acts`
   claiming a whole line, `.off-children` as a bare indent). That grammar was the
   outlier Mark's UAT named: this page sits beside three listings built from the
   kit's entity card, and it was the only one wearing a shape nothing else wore.

   WHAT THIS BLOCK STILL OWNS, and it is deliberately only two things: the CARD
   BOX and the RAIL. The row anatomy inside every card — lead, middle, lines,
   pills, the worded overflow — is `.ktrow`'s and `.ktpill`'s, unrestyled, because
   the whole point of the re-skin is that these cards ARE the Catalog's rows at a
   card's density. A rule here that reached into `.ktrow-*` would be this surface
   restyling a shared family, which is what K32's "compose rungs, never restyle
   them" forbids one component along. */

/* THE PARENT SPECIES — one card, the raised surface, the family's own radius.
   `--card-r` and not a literal: the board drew 12px, this cover's cards are 10,
   and a card that matched the drawing instead of the token would have re-skinned
   the page into a SECOND outlier. */
.passcard {
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--card-r);
  background: var(--surface-raised); padding: var(--b-space-3) var(--b-space-4);
  margin-bottom: var(--b-space-2);
}
/* ARCHIVED RIDES AT 55%, which is what every other faded state in this cover
   costs (`.punchchip.off`, `.sfland-date--past`). It is on the CARD and so it
   fades the rail with it: an archived subscription's add-ons are as unbuyable as
   it is. */
.passcard-off { opacity: 0.55; }
/* The row rung, INSIDE a card that has already drawn the box. It keeps every
   column and every type size and gives up only the four properties the card is
   now responsible for.

   TWO CLASSES, AND THE SECOND ONE IS LOAD-BEARING (TEN-1831). `.passrow` alone
   is one class, exactly like `.ktrow` — so with the rung declared LATER in this
   file (the kit block, ~6400 lines down) the reset lost the cascade on source
   order and every face kept the rung's own border, ground, pad and radius. That
   is what Mark's UAT saw: a bordered box for the face, a bordered box for each
   child, and the rail hanging between them — a card that read as a container of
   separate regions instead of a subscription containing its add-ons. A reset
   that has to win over the rung it resets says so in its specificity rather
   than trusting where it happens to sit. */
.ktrow.passrow { border: 0; background: none; padding: 0; border-radius: 0; }
/* THE TITLE ROW: one growing text column, the acts beside it, aligned to its
   top. `align-items: flex-start` is what puts the acts at the TOP right of a
   card whose text runs to two lines, and `flex-wrap: nowrap` is what keeps them
   from becoming a bottom act row at 375px. */
.ktrow.passrow { align-items: flex-start; gap: var(--b-space-3); flex-wrap: nowrap; }
/* The name and the facts are ONE column, so the acts are a sibling of the pair
   rather than a third thing competing with them for the row's width. The lead
   gives up the listing's fixed 170px column here for the same reason: these are
   cards, and the title of a card runs the width it has. */
.passtext { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
.passtext > .ktrow-lead { width: auto; }
/* An untagged pass is honored NOWHERE, so its absence is a state worth naming
   rather than an empty pill row nobody notices — the retired `.off-tag-none`'s
   reason, kept, and now said in the pill family's own shape. */
.passpill-none { color: var(--warn); border-color: var(--warn); background: none; }
/* The catalog glyph's count, at the row-count rung (`.ktrow-count`) so it is the
   same glyph-plus-number the Catalog row wears for followers. It sits AFTER the
   pills it qualifies, with a gap that separates it from the last one. */
.passreach { margin-left: var(--b-space-2); }
/* THE ACTS SIT AT THE TITLE ROW'S TOP RIGHT, which is what the board draws and
   what the card family does. There are at most two of them now — an arrange
   arrow and Edit — because the danger pair moved into the editor (amendment 1)
   and the ledger code left the face entirely (TEN-1831, delta 4).
   `flex-wrap: nowrap` is deliberate at that size: wrapping was what let them
   fall under the facts and become the bottom act row `.off-acts` was retired
   for, and two small controls have no business needing a second line. */
.passacts { display: flex; flex-wrap: nowrap; align-items: center; gap: var(--b-space-2); }
.off-actform { margin: 0; }

/* THE ONE RAIL, THE ONE INDENT RUNG (K36). It is the parent's, drawn inside the
   parent's box, and there is never a second one inside a child — that is the
   "never a tree" clause, and it is enforced by the template having no recursive
   mount rather than by a selector here. */
.passrail {
  margin: var(--b-space-3) 0 0 18px; border-left: 2px solid var(--border-subtle);
  padding-left: var(--b-space-3); display: flex; flex-direction: column; gap: var(--b-space-2);
}
/* The rail's own line: what the children ARE, and the act that makes another.
   The label is the section rung's voice at the rail's scale, so it borrows
   `.seclbl`'s letter-spacing without its page-level margins. */
.passrail-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); }
.passrail-lbl {
  font-family: var(--b-font-heading); font-weight: 700; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted);
}
.passrail-add { margin-left: auto; }
/* THE CHILD: the board's COMPACT SPECIES (TEN-1831, delta 2) — the same card,
   quieter and visibly denser. The overlay surface is the board's s2 and the pad
   is the tighter one; what TEN-1797 shipped stopped there, and the result was a
   child the same height as its parent wearing a slightly different grey, which
   is not a density at all. The board's `.ccard` is unambiguous: name and facts
   run INLINE on one wrapping line, the pills ride that line, and Edit is the
   small rung at the right.
   K36's "one density change, and it is the box" is read here as TEN-1831
   reconciles it: the compact card may set its own SCALE — inline facts, smaller
   type, the act at its small rung — because that is the board's own drawing of
   this family. What it may not change is the VOCABULARY: the same `.ktrow`
   anatomy, the same `.ktpill` family, the same catalog glyph, the acts in the
   same place. Change those and it is a second species, and clause 1 is gone. */
.passchild {
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--control-r);
  background: var(--surface-overlay); padding: var(--b-space-2) var(--b-space-3);
}
.passchild > .ktrow.passrow { gap: var(--b-space-2); align-items: center; }
/* ONE WRAPPING LINE. The text column stops being a column: the lead, the facts
   and the pills are inline runs that wrap as one paragraph would, which is the
   board's `.ccard` exactly. Each selector carries the depth it needs to beat the
   kit's own rule for that part (`.ktrow-mid:has(> .ktrow-line)` is two classes,
   so inlining it takes three) — the same cascade lesson the row reset above is a
   scar of. */
.passchild .passtext { display: block; }
.passchild .passtext > .ktrow-lead { display: inline; width: auto; font-size: var(--b-text-sm); }
.passchild .passtext > .ktrow-mid:has(> .ktrow-line) { display: inline; }
.passchild .passtext .ktrow-line:first-child { display: inline; font-size: var(--b-text-xs); color: var(--text-muted); }
/* The separator the board prints between a compact card's name and its facts.
   It is punctuation inside one line and belongs to the LINE, not to the name —
   which is why it is drawn here and not stamped into the template: on a parent
   the facts are their own line and the bullet would be a stray character. */
.passchild .passtext .ktrow-line:first-child::before { content: "· "; }
.passchild .passtext > .ktrow-mid > .ktrow-pills {
  display: inline-flex; margin-top: 0; margin-left: var(--b-space-2); vertical-align: middle;
}
.passchild .ktrow-pills > .ktpill { height: 1.15rem; padding: 0 7px; font-size: var(--b-text-xs); }
/* Edit at the compact rung — the board's 24px act beside its 28px one. The
   family is the same `.rbtn`; only its scale moves, which is what a density is. */
.passchild .passacts .rbtn { padding: 1px var(--b-space-2); font-size: var(--b-text-xs); }
.off-orphan-lbl { margin-top: var(--b-space-3); color: var(--warn); }

/* ── THE PAGE FLOAT (K35) ─────────────────────────────────────────────────
   The census and the one primary act, above the list surface and aligned to its
   edges. It is `.orgc-netfloat`'s shape — K35's canonical instance — and not a
   second spelling of it, because two pages implementing one rung two ways is the
   drift the rung was minted to stop. */
.off-float {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--b-space-3);
  margin-bottom: var(--b-space-2);
}
.off-census { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.off-census strong { color: var(--text-primary); font-weight: var(--b-weight-semibold); }
.off-newpass { margin-left: auto; }

/* THE CHOOSER'S GRID INSIDE THIS PAGE'S MODAL SHELL (TEN-1797, amendment 3).
   `.ktchoices` brings the two columns and the phone behaviour with it; the only
   thing this page has to say is that the grid should not inherit the form gap of
   the `.ss-grid-modal` it now sits in. */
.ss-grid-modal > .ktchoices { margin-top: var(--b-space-3); }

/* THE SCRIPTLESS NEW-TAG FIELD (TEN-1475, ADR-0033 §"The fourth rung").
   ------------------------------------------------------------------------
   Tags became a creatable token picker, and its unenhanced form is a plain
   <select multiple> — which can only PICK. On a surface whose modals are
   deliberately reachable with no scripting (ADR-0036 branch 1, pinned by the
   console's own tests), that would quietly take creating a tag away from
   exactly the reader who has no other door.
   So the modal carries a companion text field, and this is the whole of what
   decides who sees it. With scripting, the create row IS this field and a
   second box would be two ways to do one thing.

   An ATTRIBUTE and not a class, deliberately: the field is a .ss-grid-field
   like every other control on the console — that is what keeps it inside the
   kit — and a second class in the same attribute would make the console's
   "no raw control is loose" count read it as neither. */
[data-scriptless-only] { display: none; }
@media not (scripting: enabled) {
  [data-scriptless-only] { display: flex; }
}

/* Deferred skeleton: identical colgroup, quiet shimmer */
.ss-grid-toolbar-ghost { height: 2.2rem; border-radius: var(--b-radius-sm); background: var(--surface-raised); opacity: 0.6; }
.ss-skel-row td { height: 2.4rem; color: var(--text-muted); font-size: var(--b-text-sm); padding: var(--b-space-2); }
.ss-skel-row + .ss-skel-row td { background: color-mix(in srgb, var(--surface-overlay) 30%, var(--surface-raised)); }
.b-spinner {
  display: inline-block; width: 1em; height: 1em; vertical-align: -0.15em;
  border: 2px solid var(--border-strong); border-top-color: transparent; border-radius: 50%;
  animation: ss-spin 0.8s linear infinite;
}
@keyframes ss-spin { to { transform: rotate(360deg); } }

/* ---- The user console (TEN-973) ---------------------------------------- */
/* Sidebar + main; the cover collapses this to a tab strip on mobile
   (TEN-972). Danger quarantine is a red accent on ITS items only. */
.console { display: flex; gap: var(--b-space-5); align-items: flex-start; }
.console-nav { flex: 0 0 13rem; position: sticky; top: var(--b-space-4); }
.console-nav-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.console-nav-item {
  display: block; padding: var(--b-space-2) var(--b-space-3);
  color: var(--text-muted); text-decoration: none;
  border-radius: var(--b-radius-sm); border-left: 3px solid transparent;
  font-family: var(--b-font-heading); text-transform: uppercase;
  letter-spacing: 0.04em; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
}
.console-nav-item:hover { color: var(--text-primary); background: var(--surface-raised); }
/* The state's own hover arm (TEN-1702 sweep; see `.ktlist-chip-on`). This
   family is the one the census nearly missed, because the element class and the
   state class are not spelled from one root — `.console-nav-item` wears
   `.console-nav-on` — so no naming rule relates them and only the browser can
   see the collision. Declared here so the rail's lit page keeps its ground and
   its left edge while the pointer is on it. */
.console-nav-on, .console-nav-on:hover {
  color: var(--text-primary); background: var(--surface-raised);
  border-left-color: var(--b-accent);
}
.console-nav-dangeritem { color: var(--b-danger, #dc2626); }
.console-nav-dangeritem.console-nav-on { border-left-color: var(--b-danger, #dc2626); }
.console-main { flex: 1 1 auto; min-width: 0; }
.console-title { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-xl); margin: 0 0 var(--b-space-3); text-transform: uppercase; letter-spacing: 0.03em; }
.console-danger-title { color: var(--b-danger, #dc2626); }
.console-danger-card { border-color: color-mix(in srgb, var(--b-danger, #dc2626) 45%, var(--border-subtle)); }
.console-overview-card { display: flex; gap: var(--b-space-4); align-items: center; }
.console-avatar {
  flex: 0 0 auto; width: var(--b-avatar-lg); height: var(--b-avatar-lg); border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--b-accent); color: var(--b-accent-fg);
  font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-xl);
}
.console-overview-id { min-width: 0; }
.console-chips { display: flex; flex-wrap: wrap; gap: var(--b-space-2); margin: 0 0 var(--b-space-4); }
.console-chip {
  display: inline-flex; align-items: center; gap: var(--b-space-1);
  padding: var(--b-space-1) var(--b-space-3);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: 999px; color: var(--text-muted); text-decoration: none; font-size: var(--b-text-sm);
}
.console-chip:hover { color: var(--text-primary); border-color: var(--border-strong); }
.console-chip-n { font-family: var(--b-font-mono); font-weight: 700; color: var(--text-primary); }
.console-btn {
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-3); cursor: pointer; font-size: var(--b-text-sm);
  /* Button face per the button card (Mark 2026-09-07): condensed uppercase. */
  font-family: var(--b-font-heading); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
}
/* A console button may be an ANCHOR (TEN-1266): a modal opener carries the
   scriptless fallback in its href, so the control is an <a> that dismiss.js
   swallows. An inline <a> would let this padding spill out of its line box, and
   `a { color: var(--lb) }` above would paint it link-blue — the class already
   wins the colour, and this wins the box. */
a.console-btn { display: inline-flex; align-items: center; justify-content: center; }
.console-btn:hover { border-color: var(--border-strong); background: var(--surface-raised); }
.console-btn:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
/* PRIMARY (TEN-1026): the one action on a screen should look like it. Send
   was a ghost button and read as the least important thing in the
   conversation. */
.console-btn-primary {
  background: var(--b-accent); color: var(--b-accent-fg); border-color: transparent;
  font-weight: var(--b-weight-semibold);
}
/* Primary needs its OWN focus ring (TEN-1056). The base rule outlines in
   --b-accent, which is this button's own background — so a keyboard user
   tabbing onto the most important control on the screen saw nothing at all.
   Ringed in the foreground colour with an offset gap instead. */
.console-btn-primary:focus-visible {
  outline: var(--b-ring-w) solid var(--b-accent-fg);
  outline-offset: 2px;
  box-shadow: 0 0 0 calc(var(--b-ring-w) + 2px) var(--b-accent);
}
/* A primary button has to RESTATE its own fill on every state (TEN-1091).
   `.console-btn:hover` sets `background: var(--surface-raised)`, and these
   rules only set `filter` — so hovering the most important control on the
   screen dropped the accent and left the label in --b-accent-fg on a
   surface colour. Measured in the dark theme: #4D8FFF became #323235 while
   the text stayed #09090D. Near-black on dark grey, about 1.4:1, which is
   not a contrast so much as an absence of one.
   The filter is the state change; the background is what it changes. */
.console-btn-primary:hover {
  background: var(--b-accent); border-color: transparent;
  filter: brightness(1.08);
}
.console-btn-primary:active {
  background: var(--b-accent); border-color: transparent;
  filter: brightness(0.94);
}
.console-btn-primary:disabled {
  background: var(--b-accent); border-color: transparent;
  opacity: 0.5; cursor: not-allowed; filter: none;
}

/* ══ THE BUTTON TAXONOMY (TEN-1443, Mark's ruling) ═══════════════════════════
   Three tiers, and the tier is chosen by WHAT THE CONTROL DOES, never by how
   much you want it noticed:

     PRIMARY   .console-btn-primary — the ONE action a screen exists to take.
               Solid accent fill. Save, Create, Send. At most one per form.
               Unchanged by this ticket and not to be re-tuned for balance:
               if a primary looks too loud beside its neighbours, the
               neighbours are the wrong tier, not the primary.

     SECONDARY .console-btn-secondary — a PROMINENT action that is not the
               screen's purpose. It opens something, adds a row, starts a
               flow: + Invite, + Register, + New, Connect card payments.
               Colored, so it reads as an action; outlined, so it never
               competes with the primary. Several per screen is normal.

     GHOST     .console-btn (the bare class) — inline, non-submitting
               controls that sit inside a row or a toolbar: dropdown
               openers, filters, pagers, per-row Edit. Neutral stroke on a
               surface fill. It is the DEFAULT: a button with no modifier
               is a ghost on purpose, not an unfinished secondary.

   Legacy called the middle tier by its markup (`.newevent`) and it is the
   style Mark asked to keep, so it enters the kit by behaviour instead. Do not
   confuse it with the SELECTION state on the same legacy form — a checked
   `.chip` is a solid blue fill with dark ink, which says "this option is on",
   not "press me". A selection is not a button tier and is not lifted here.
   ────────────────────────────────────────────────────────────────────────── */

/* SECONDARY — ported from legacy sportscore-go web/assets/css/app.css
   `.newevent`, the style on /manage/{event}/enrollments' "+ Member rate" and
   "Connect card payments". Faithful to the original: alpha-blue fill, blue
   hairline, light-blue uppercase Barlow Condensed label, the same 0.06em
   tracking and 700 weight. Two things are deliberately NOT verbatim — the
   border alpha is now the --blue-line token (legacy hardcoded it, which is
   why it could not follow a theme), and the radius/padding speak the kit's
   scale rather than legacy's raw 8px/6px 12px, so it lines up with the other
   two tiers on a shared row. */
.console-btn-secondary {
  background: var(--blue-a); color: var(--lb);
  border: var(--b-border-w) solid var(--blue-line);
  font-family: var(--b-font-heading); font-weight: var(--b-weight-bold);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-size: var(--b-text-xs);
}
/* The base .console-btn:hover swaps in a surface fill, which would drop the
   blue this tier IS — the TEN-1091 lesson, third time. Restate the fill and
   let the border do the state change, exactly as legacy did. */
.console-btn-secondary:hover {
  background: var(--blue-a); border-color: var(--lb); color: var(--lb);
}
.console-btn-secondary:active { filter: brightness(1.12); }
.console-btn-secondary:focus-visible { outline: var(--b-ring-w) solid var(--lb); outline-offset: 2px; }
.console-btn-secondary:disabled, .console-btn-secondary[disabled] {
  background: var(--blue-a); border-color: var(--blue-line);
  opacity: 0.5; cursor: not-allowed; filter: none;
}
/* Same hole, same fix: danger is an outline button, so the base hover's
   surface fill is fine — but its LABEL must keep saying danger. */
.console-btn-danger:hover, .console-btn-danger:active {
  border-color: var(--b-danger, #dc2626); color: var(--b-danger, #dc2626);
}
.console-btn-danger { border-color: var(--b-danger, #dc2626); color: var(--b-danger, #dc2626); }
.console-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.console-inline-form { margin: var(--b-space-2) 0; }

/* ── THE PRESS, HELD (TEN-1457; Mark's standing ruling 2026-09-01) ─────────
   The tactile signature further up this file (§09, `.rbtn:active` and friends)
   already says "pressed" — it just stops saying it the instant the mouse button
   comes up. A control that starts a round trip therefore flashes for a few dozen
   milliseconds and then looks untouched for as long as the network takes, which
   is how the payments desk's Re-check came to read as a dead click.

   So this is not a second vocabulary. It is the SAME two declarations, held from
   the press until the answer arrives — press.js (cmc-auth/press) adds the class
   on the click itself and `disabled` one tick later, and see that file for why
   those two cannot happen in the same moment.

   Keyed on the class alone, never on a tier or a component, because the controls
   that need it are not one family: the kit's `.console-btn` tiers, the older
   `.rbtn` row button on the payments desk and the entries screen, and whatever
   the next screen brings. Anything the reader can press inherits this by being
   pressed.

   Deliberately not a spinner: the state is usually over within a frame or two,
   and a spinner that flashes is noise. On an inline text link the transform does
   not apply at all (transforms skip non-replaced inline boxes) and only the
   brightness lands — which is the right weight for a link and needs no rule of
   its own. */
.is-pressed { transform: scale(0.985); filter: brightness(1.06); }
/* IN FLIGHT. `aria-disabled` and not `disabled`, because a disabled control is
   dropped from the form's entry list and would take its own name and value out
   of the POST it is acknowledging — measured, not assumed; press.js's header
   has the reading. This is the half that makes the affordance real: the button
   still submits its value, and simply cannot be pressed again.

   A busy button reads as HELD, not as unavailable. Nothing here touches
   opacity, so it keeps the weight it had — "this is happening", not "you
   cannot do this". */
.is-pressed[aria-disabled="true"] { cursor: progress; pointer-events: none; }

/* The token picker (TEN-1056). A flat <select multiple> is fine at five
   connections and unusable at two hundred, which is the point.

   The source <select> stays in the DOM as the form control and is taken out
   of the tab order; everything here is its interface. */
/* Visually hidden, still read aloud. The picker's source select was the
   only thing doing this and it did it inline; a badge whose whole meaning is
   a glyph needs the same treatment, so it is a utility now (TEN-1093).

   The geometry is !important, and that is the point of a utility rather than
   sloppiness: `.console-pref-form select { width: 100% }` is (0,1,1) and
   out-specifies a bare class however late it appears, so the picker's hidden
   source select rendered 375px wide and pushed the whole page sideways at a
   phone width — invisible, because clip-path still hid the pixels, and
   measurable, because the document scrolled (TEN-1099). A utility that can
   be out-specified is not a utility. */
.b-sr-only, .tokpick-source {
  position: absolute !important; width: 1px !important; height: 1px !important;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.tokpick { position: relative; }
.tokpick-field {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-1);
  padding: var(--b-space-1) var(--b-space-2);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
}
.tokpick-field:focus-within { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
/* THE FLASH BEFORE THE PICKER (TEN-1088).
   ------------------------------------------------------------------------
   picker.js ENHANCES a <select multiple> — the select is real markup, so a
   page with no scripting still has a working control — and script replaces
   it after the document parses. There is therefore always a window where
   the raw control is on screen, and it was loud: measured 92px of list box
   against the picker field's 39, in different colours, so the page visibly
   lurched when the picker arrived.

   The fix is NOT to hide the select. Hiding it would mean that scripting
   enabled + this gathering failing = a form with no control at all, which
   is the exact failure one account was already reporting. Instead the
   unenhanced control is dressed as its own destination: same height, same
   surface, same border. The swap stops being an event.

   `scripting: enabled` scopes it, so a browser with no JS keeps the taller
   list box — which is the better control when it is the only one you get. */
@media (scripting: enabled) {
  select[data-tokpick], select[data-peoplepick] {
    height: calc(var(--b-space-1) * 2 + var(--b-space-5) + var(--b-border-w) * 2);
    padding: var(--b-space-1) var(--b-space-2);
    background: var(--surface-overlay); color: var(--text-primary);
    border: var(--b-border-w) solid var(--border-strong);
    border-radius: var(--b-radius-sm);
  }
}
.tokpick-chips { display: contents; }
.tokpick-chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.1rem var(--b-space-2);
  background: color-mix(in srgb, var(--b-accent) 18%, var(--surface-raised));
  border-radius: 999px; font-size: var(--b-text-sm);
}
.tokpick-chip-x {
  border: 0; background: none; cursor: pointer; padding: 0; line-height: 1;
  color: var(--text-muted); font-size: 1em;
}
.tokpick-chip-x:hover { color: var(--text-primary); }
.tokpick-chip-x:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.tokpick-input {
  flex: 1 1 8ch; min-width: 8ch;
  border: 0; background: none; color: inherit; font: inherit; padding: 0.2rem 0;
}
.tokpick-input:focus { outline: none; }   /* the field wears the ring */
/* The panel's GROUND, BORDER, ELEVATION, PADDING, DENSITY and both state
   channels are K15's and are declared once, in the kit block below — this
   family's ALIGNMENT pass (TEN-1634 train 2) removed them from here rather than
   overriding them, so there is exactly one place a dropdown's colour lives.
   What stays is what is this control's own: where the panel sits relative to
   the field it belongs to, and the fact that an empty one is not drawn at all. */
.tokpick-list {
  position: absolute; z-index: 40; left: 0; right: 0; top: 100%;
  margin: 0; list-style: none;
}
.tokpick-list:empty { display: none; }
/* Inside a modal the dropdown joins the FLOW (TEN-1152): absolutely
   positioned, the modal's scroll box clipped it while it covered the
   buttons. In flow it pushes the actions down and the modal grows with it,
   bounded by the modal's own max-height. Still ATTACHED — the margin that used
   to separate it from the field is gone, because in flow or floating it is the
   same object joined the same way. */
.ss-grid-modal .tokpick-list {
  position: static; margin-top: 0; box-shadow: none;
  max-height: calc(var(--ktsel-row) * 3.5 + 6px);
}

/* THE CREATABLE RUNG (TEN-1475, ADR-0033 §"The fourth rung").
   --------------------------------------------------------------------------
   A create row and a pick row must not be able to be confused, because the
   thing they differ by is invisible afterwards: one reuses a tag the org
   already has, the other adds a word to its vocabulary, and a typo takes the
   second door silently. So the row is marked, and — the half that matters
   more — the CHIP it becomes stays marked, because by then the dropdown is
   gone and the mark is the whole signal left.

   Deliberately a dashed outline and a badge rather than a colour swap: the
   list is already tinted on hover/active, so a second colour would have to
   fight the highlight and would say nothing at all on the chip row. */
.tokpick-opt.tokpick-new {
  display: flex; align-items: center; gap: var(--b-space-2);
  border: var(--b-border-w) dashed var(--border-strong);
}
.tokpick-new-mark, .tokpick-chip-mark {
  flex: none;
  padding: 0 0.35rem; border-radius: 999px;
  background: color-mix(in srgb, var(--b-accent) 30%, var(--surface-raised));
  color: var(--text-primary);
  font-size: var(--b-text-xs); text-transform: uppercase; letter-spacing: 0.04em;
}
.tokpick-new-label { min-width: 0; overflow-wrap: anywhere; }
.tokpick-chip.tokpick-chip-new {
  background: var(--surface-raised);
  border: var(--b-border-w) dashed var(--border-strong);
}

/* THE ASYNC PEOPLE PICKER (TEN-1283, ADR-0033).
   --------------------------------------------------------------------------
   It wears the token field above — same wrap, same chips, same dropdown box —
   because it IS the same control from the reader's side; only where the
   options come from changed, and a picker that looked different for that
   reason would be teaching a difference that does not exist. What is new here
   is only what a SERVER-answered list needs: a face on each row, a line while
   the answer is in flight, and a way to say "already here" without hiding the
   row. */
.peoplepick-source[data-for] { display: none; }   /* a search source, not a surface */
/* A STANDALONE mount's list joins the FLOW, for the reason the modal's does
   (TEN-1152) and one more: this list arrives with the mount's own answer
   already in it, so it is not a transient dropdown over the page — it is part
   of the control, and floated it lands on whatever sits below. */
.peoplepick-standalone .peoplepick-list {
  position: static; margin-top: 0; box-shadow: none;
  max-height: calc(var(--ktsel-row) * 3.5 + 6px);
}
/* The row's SHAPE and both state channels are K15's, declared once in the kit
   block (TEN-1634 train 2). What is this family's own is what a row CONTAINS:
   a name, a handle in the mono tier, and a note for somebody already here.
   A row here is one line and carries no thumb, so it takes the SIMPLE density —
   see the density note in the kit block. */
.peoplepick-name { font-weight: 600; }
/* Already in the room: shown and marked, never hidden. Somebody typing a name
   they cannot add should find out why, not find nothing — so the row keeps a
   ground of its own and simply refuses to light up (it is the one row in any of
   these panels that the hover channel must not reach). */
.peoplepick-row[aria-disabled="true"] { cursor: default; opacity: 0.6; }
.peoplepick-row[aria-disabled="true"]:hover,
.peoplepick-row[aria-disabled="true"].is-active {
  background: var(--surface-raised); color: var(--text-primary); font-weight: 400;
}
.peoplepick-row[aria-disabled="true"]:hover .peoplepick-handle,
.peoplepick-row[aria-disabled="true"]:hover .peoplepick-here { color: var(--text-muted); }
.peoplepick-here { margin-left: auto; }
/* The scriptless pair. This control searches as you type, so with no scripting
   it cannot be a control at all — the line stands in for it rather than
   leaving a box that looks live and isn't (the TEN-1238 rule). Where the
   picker is ADOPTED, the surface's own scriptless story governs: the room's
   Add-people modal cannot be opened without scripting either way, and TEN-1266
   owns that decision. */
@media (scripting: enabled) { .peoplepick-noscript { display: none; } }
@media not (scripting: enabled) { .peoplepick-standalone .peoplepick-rows { display: none; } }

/* THE SUGGEST RUNG (TEN-1449, ADR-0033's third picker).
   --------------------------------------------------------------------------
   Same dropdown, same rows, same faces — it IS the same control from the
   reader's side, and the only thing that changed is the substrate underneath
   (a plain text field instead of a <select multiple>). So this section adds
   no appearance of its own; it only re-hangs the field styling on the wrapper
   the script inserts.

   THAT RE-HANGING IS THE WHOLE POINT. `.ss-grid-field > input` is a DIRECT
   child selector, and suggest.js wraps the input to position the list against
   it — so without these two rules the field silently loses its width, its box
   and its focus ring the moment scripting turns on. A styling rule that only
   holds in the unscripted render is the kind of break nobody sees in a diff.

   There is NO scriptless pair here, deliberately. Unlike peoplepick, this
   control enhances a field that is already the whole feature: with no
   scripting the input is exactly the box it was, so there is nothing to hide
   and nothing to stand in for it (ADR-0036 branch 1 is untouched). */
.suggest { position: relative; display: block; }
.ss-grid-field > .suggest > input {
  cursor: auto; font: inherit; width: 100%; box-sizing: border-box;
  padding: var(--b-space-1) var(--b-space-2); background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}
.ss-grid-field > .suggest > input:focus-visible {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 1px;
}
/* A row the server marked unpickable keeps the disabled treatment above; here
   the note wraps rather than pushing the handle off a narrow modal. */
.suggest-list .peoplepick-here { margin-left: auto; text-align: right; }

/* THE PICKED VALUE IS A TOKEN (TEN-1591, ADR-0033 amended).
   --------------------------------------------------------------------------
   Mark, on the invite modal: "it still shows raw text for selected users
   instead of tokens." It wears `.tokpick-chip` and `.tokpick-chip-x` — the
   kit's chip, not a lookalike — because it IS the same thing to a reader, and
   only what sits underneath it changed.

   Where it sits is the one difference from tokpick, and it follows from arity:
   that control holds MANY and lays its chips inside the field alongside a
   shrinking input; this one holds ONE and keeps the box full width for the
   next search, so the token goes above the box rather than into it. `:empty`
   collapses the row entirely, so a field with nothing picked is byte-for-byte
   the field it was. */
.suggest-chips { display: flex; flex-wrap: wrap; gap: var(--b-space-1); margin-bottom: var(--b-space-1); }
.suggest-chips:empty { display: none; }

/* Subscription status badges (TEN-973) ride the same badge grammar */
.ss-badge-active { background: color-mix(in srgb, var(--b-success, #16a34a) 18%, transparent); color: var(--text-primary); }
.ss-badge-paused { background: color-mix(in srgb, var(--b-warning) 22%, transparent); color: var(--text-primary); }
.ss-badge-canceled { background: var(--surface-overlay); color: var(--text-muted); }
/* Real-mode statuses (TEN-978): CMC's vocabulary mapped onto the grammar */
.ss-badge-past-due { background: var(--bad-a, color-mix(in srgb, var(--b-danger, #dc2626) 20%, transparent)); color: var(--text-primary); }
.ss-badge-ending { background: color-mix(in srgb, var(--b-warning) 22%, transparent); color: var(--text-primary); }

/* ---- Responsive/mobile pass (TEN-972) --------------------------------- */
/* The layout is single-column and fluid by construction (720px container,
   flex cards, fr-grid board rows); this block handles what genuinely
   changes on small screens: the console sidebar becomes a tab strip, the
   grid toolbar stacks, wide content scrolls in its own container, chrome
   compresses, tap targets grow. */
@media (max-width: 720px) {
  main.container, .app-foot { padding: var(--b-space-3); }
  /* The contract, enforced (Mark's phone walk): the PAGE never scrolls
     sideways — wide content scrolls inside its own container or not at
     all. clip (not hidden) so no stray scroll context appears. */
  main.container { overflow-x: clip; }
  .app-header { gap: var(--b-space-2); padding: var(--b-space-2) var(--b-space-3); }
  .app-header .chrome { gap: var(--b-space-2); }
  .wordmark { font-size: var(--b-text-lg); }
  /* THE BELL'S WIDTH RULE IS GONE (TEN-1879) and nothing replaces it here. It
     bounded the panel's WIDTH against the viewport and never moved its left
     edge, so on a 375px phone it left a 320px panel right-aligned to a bell at
     311px — starting at -9. That is the per-panel correction K39 clause 1 calls
     a defect even when its pixels are right; the family pin above owns this now,
     and it owns it for the four panels that never had a rule of their own. */

  /* Console: sidebar -> horizontal tab strip (the IA's mobile shape).
     align-items MUST flip to stretch in column direction — the desktop
     flex-start would size children to CONTENT width, letting the grid's
     min-width table drag the whole page sideways (the phone-walk bug). */
  .console { flex-direction: column; gap: var(--b-space-3); align-items: stretch; }
  .console-main { width: 100%; }
  .console-nav { position: static; flex: none; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .console-nav-list { flex-direction: row; gap: var(--b-space-1); padding-bottom: var(--b-space-1); }
  .console-nav-item {
    white-space: nowrap; border-left: 0;
    border-bottom: 3px solid transparent;
    border-radius: var(--b-radius-sm) var(--b-radius-sm) 0 0;
    padding: var(--b-space-2);
  }
  /* Same pair as the desktop rail above, for the same reason (TEN-1702): the
     mobile rung's lit state is a bottom edge, and the hover rule above would
     otherwise repaint its ground out from under it. */
  .console-nav-on, .console-nav-on:hover { background: var(--surface-raised); border-bottom-color: var(--b-accent); }
  .console-nav-dangeritem.console-nav-on { border-bottom-color: var(--b-danger, #dc2626); }
  .console-overview-card { flex-direction: column; align-items: flex-start; }

  /* Grids: search takes the row, exports wrap under, pager taps grow */
  .ss-grid-search { flex: 1 1 100%; max-width: none; }
  .ss-grid-exports { margin-left: 0; }
  .ss-grid-scroll { max-height: 70vh; }
  .ss-grid-pager { row-gap: var(--b-space-2); }
  .ss-pager-size { margin-left: 0; }
  .ss-pager-btn, .ss-pager-n { padding: var(--b-space-1) var(--b-space-2); min-width: 2.2rem; }

  /* Boards and other wide blocks scroll inside their own container —
     the page never scrolls sideways (same rule the grid kit set). */
  .lb { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .brow { grid-template-columns: 6px 2rem 1fr minmax(0,0.5fr) 3rem 3.5rem; gap: var(--b-space-1); }
  .bwo { font-size: 18px; }

  /* Forms and modal fields stack full-width */
  .ss-grid-field-row { flex-direction: column; gap: var(--b-space-3); }
  .regwrap, .paywrap { max-width: none; }
}

/* Preferences form (TEN-983) */
.console-pref-form { display: flex; flex-direction: column; gap: var(--b-space-2); margin-top: var(--b-space-2); }
.console-radio { display: flex; align-items: center; gap: var(--b-space-2); font-size: var(--b-text-sm); color: var(--text-primary); cursor: pointer; }
.console-pref-form .console-btn { align-self: flex-start; margin-top: var(--b-space-1); }
/* A setting's second line: what the switch actually does, under its name. */
.console-pref-hint { display: block; font-weight: var(--b-weight-normal); }

/* Detail receipts (TEN-982) */
.ss-detail { max-width: 30rem; }
.ss-detail-back { color: var(--text-muted); text-decoration: none; font-size: var(--b-text-sm); }
.ss-detail-back:hover { color: var(--text-primary); }
.ss-receipt {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-4); margin-top: var(--b-space-3);
}
.ss-receipt-head { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-3); }
.ss-receipt-payee { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-lg); text-transform: uppercase; letter-spacing: 0.02em; }
.ss-receipt-desc { color: var(--text-muted); margin: var(--b-space-2) 0 var(--b-space-3); }
.ss-receipt-rows { border-top: var(--b-border-w) solid var(--border-subtle); padding-top: var(--b-space-3); }
.ss-receipt-row { display: flex; justify-content: space-between; gap: var(--b-space-3); margin: 0 0 var(--b-space-2); font-size: var(--b-text-sm); }
.ss-receipt-row span:first-child { color: var(--text-muted); }
.ss-receipt-mono { font-family: var(--b-font-mono); }
.ss-receipt-total {
  display: flex; justify-content: space-between; margin: var(--b-space-3) 0 0;
  padding-top: var(--b-space-3); border-top: 2px solid var(--border-strong);
  font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-lg);
}

/* Console full build (TEN-973): wallet methods, MFA rows, saved tick */
.theme-toggle-inline { position: static; margin-top: var(--b-space-2); }
.console-pref-saved { color: var(--b-success, #16a34a); font-size: var(--b-text-sm); margin-left: var(--b-space-2); }
.console-method-head { display: flex; align-items: center; gap: var(--b-space-2); }
.console-method-name { font-family: var(--b-font-heading); font-weight: 800; text-transform: uppercase; letter-spacing: 0.02em; }
.console-method-badge {
  background: var(--b-accent); color: var(--b-accent-fg);
  border-radius: 999px; padding: 0 var(--b-space-2);
  font-size: var(--b-text-xs); text-transform: uppercase; letter-spacing: 0.04em;
}
.console-method-default { border-color: var(--b-accent); }
/* Blameless expiry flag (TEN-979 real wallet — MP008 §4 "needs updating") */
.console-method-badge-warn { background: color-mix(in srgb, var(--b-warning) 30%, transparent); color: var(--text-primary); }
/* One-click consent chip (TEN-990) */
.console-method-badge-oneclick { background: color-mix(in srgb, var(--b-success, #16a34a) 18%, transparent); color: var(--text-primary); }
/* The wallet's INVERSE VIEW (TEN-1714, board R2R-3): what rides each method.
   Rows sit under the card they are billed to, so "what breaks if I remove this"
   is answered on the card itself rather than guessed. */
.console-rides { list-style: none; margin: var(--b-space-2) 0 0; padding: 0; }
.console-ride {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--b-space-2);
  padding: var(--b-space-1) 0; border-top: var(--b-border-w) solid var(--border-subtle);
  font-size: var(--b-text-sm);
}
.console-ride-what { min-width: 0; }
/* A club that bills directly is a payment method in the only sense that matters
   to a member — something their money leaves by — so it borrows the method
   card's shape and the warning tint, never a bucket called "other". */
.console-method-direct { border-color: color-mix(in srgb, var(--b-warning) 45%, var(--border-subtle)); }
/* The act row a warning earns: an amber sentence with its one press. */
.console-warn-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2);
  flex-wrap: wrap; margin: var(--b-space-2) 0 0;
  font-size: var(--b-text-sm); color: var(--text-primary);
}
/* The card-add MOMENT: the offer at the moment of the member's own act. It
   states its scope as a fact and never grows a list — itemising is the sheet's. */
.console-moment { border-color: color-mix(in srgb, var(--b-success, #16a34a) 45%, var(--border-subtle)); }
.console-moment-title { font-family: var(--b-font-heading); font-weight: 800; margin: 0; }
.console-moment-body { margin: var(--b-space-1) 0 var(--b-space-2); }
.console-moment-choose { align-self: center; }
/* The switch sheet's rows: grouped by where each plan rides today, one line per
   plan, the money and the timing both visible before the press (K15). */
.console-sheet-group {
  margin: var(--b-space-3) 0 var(--b-space-1);
  font-size: var(--b-text-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary, inherit);
}
.console-sheet-row {
  display: flex; align-items: baseline; gap: var(--b-space-2);
  padding: var(--b-space-1) 0; font-size: var(--b-text-sm);
}
.console-sheet-what { flex: 1 1 auto; min-width: 0; }
.console-sheet-honesty {
  margin: var(--b-space-3) 0 0; padding-top: var(--b-space-2);
  border-top: var(--b-border-w) solid var(--border-subtle); font-size: var(--b-text-sm);
}
/* Calendar views (TEN-992): toolbar + the week/day hourly grids. The
   hourly column is a fixed-height positioning context: hour rules paint
   via a repeating gradient, chips and the current-time line position by
   top-percentage (verified via geometry, per the journal rule). */
.ss-cal-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2); margin-bottom: var(--b-space-2); flex-wrap: wrap; }
.ss-cal-toolbar .ss-cal-month { margin: 0; }
.ss-cal-nav, .ss-cal-views { display: flex; gap: var(--b-space-1); }
.ss-cal-viewbtn { text-transform: capitalize; }
.ss-cal-view-on { border-color: var(--b-accent); }
.ss-cal-hourly { display: grid; grid-template-columns: 3.5rem repeat(7, 1fr); gap: 0; border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm); overflow: hidden; }
.ss-cal-hourly-day { grid-template-columns: 3.5rem 1fr; }
.ss-cal-hourhead, .ss-cal-dayhead { padding: var(--b-space-1); border-bottom: var(--b-border-w) solid var(--border-subtle); background: var(--surface-overlay); font-size: var(--b-text-xs); text-align: center; }
.ss-cal-dayhead.ss-cal-today { color: var(--b-accent); font-weight: 600; }
.ss-cal-gutter { display: flex; flex-direction: column; }
.ss-cal-gutter .ss-cal-hour { flex: 1; font-size: var(--b-text-xs); color: var(--text-muted); text-align: right; padding-right: var(--b-space-1); border-top: 1px solid transparent; }
.ss-cal-gutter, .ss-cal-daycol { height: 40rem; }
.ss-cal-daycol { position: relative; border-left: var(--b-border-w) solid var(--border-subtle);
  background: repeating-linear-gradient(to bottom, var(--border-subtle) 0, var(--border-subtle) 1px, transparent 1px, transparent 2.5rem); }
.ss-cal-daycol.ss-cal-today { background-color: color-mix(in srgb, var(--b-accent) 5%, transparent); }
.ss-cal-allday { position: absolute; top: 0; left: 0; right: 0; z-index: 2; display: flex; flex-direction: column; gap: 2px; padding: 2px; }
.ss-cal-chip { display: block; font-size: var(--b-text-xs); padding: 1px var(--b-space-1); border-radius: var(--b-radius-sm); background: var(--surface-overlay); color: var(--text-primary); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ss-cal-chip.ss-cal-event { border-left: 3px solid var(--b-accent); }
.ss-cal-chip.ss-cal-renewal { border-left: 3px solid var(--b-warning); }
.ss-cal-chip.ss-cal-purchase { border-left: 3px solid var(--b-success, #16a34a); }
.ss-cal-timed { position: absolute; left: 2px; right: 2px; z-index: 2; }
.ss-cal-nowline { position: absolute; left: 0; right: 0; height: 2px; background: var(--b-accent); z-index: 3; }
.ss-cal-nowline::before { content: attr(data-time); position: absolute; right: 2px; top: -1.1em; font-size: var(--b-text-xs); color: var(--b-accent); }
@media (max-width: 720px) {
  .ss-cal-hourly { grid-template-columns: 2.5rem repeat(7, 1fr); }
  .ss-cal-hourly-day { grid-template-columns: 2.5rem 1fr; }
  .ss-cal-daydate { display: none; }
}

/* Subscription cards (TEN-991): one card per subscription, AddOns
   (Boosts) embedded as child lines — the grid retired here. */
.ss-subcard-list { display: flex; flex-direction: column; gap: var(--b-space-3); }
.ss-subcard-head { display: flex; align-items: center; gap: var(--b-space-2); justify-content: space-between; }
.ss-subcard-name { font-size: var(--b-text-lg); font-weight: 600; color: var(--text-primary); text-decoration: none; }
.ss-subcard-name:hover { text-decoration: underline; }
.ss-subcard-facts { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: var(--b-space-1) var(--b-space-2); margin-top: var(--b-space-2); }
.ss-subcard-addons { margin-top: var(--b-space-2); border-top: var(--b-border-w) solid var(--border-subtle); padding-top: var(--b-space-2); }
.ss-subcard-addon { display: flex; justify-content: space-between; padding: 2px 0 2px var(--b-space-3); }
.ss-subcard-addon-title { color: var(--text-primary); }
.ss-subcard-addon-amount { color: var(--text-muted); }
@media (max-width: 720px) {
  .ss-subcard-facts { grid-template-columns: auto 1fr; }
}

/* Subscription groups (TEN-1474): one seller org per disclosure, the org CARD
   as its heading. A native <details>, like .branchbox and .mvrow — this cover
   ships no JavaScript for any of it, so there is nothing to gate behind
   `(scripting: enabled)` and nothing that stops working without it.
   The UA marker is suppressed and replaced by a caret that turns, the same
   treatment .msg-people gets. */
.ss-subgroup { border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); background: var(--surface-page); }
.ss-subgroup > summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: var(--b-space-2); justify-content: space-between; padding: var(--b-space-2) var(--b-space-3); }
.ss-subgroup > summary::-webkit-details-marker { display: none; }
.ss-subgroup > summary::marker { content: ""; }
.ss-subgroup > summary:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.ss-subgroup-org { min-width: 0; text-decoration: none; color: inherit; }
.ss-subgroup-org:hover .acct-value { text-decoration: underline; }
.ss-subgroup-plain { color: var(--text-muted); font-weight: var(--b-weight-semibold); }
/* The count is what a SHUT group says, so it never wraps away from the caret. */
.ss-subgroup-count { color: var(--text-muted); font-size: var(--b-text-sm); white-space: nowrap; }
.ss-subgroup-count::after { content: " ›"; display: inline-block; transition: transform var(--b-duration-fast) var(--b-ease); }
.ss-subgroup[open] > summary .ss-subgroup-count::after { transform: rotate(90deg); }
.ss-subgroup-body { display: flex; flex-direction: column; gap: var(--b-space-3); padding: 0 var(--b-space-3) var(--b-space-3); }
/* Stated outright rather than left to the UA: the body is a flex container and
   a closed <details> must not lay it out (the .msg-people lesson). */
.ss-subgroup:not([open]) > .ss-subgroup-body { display: none; }

/* Notification-settings matrix (TEN-989; UAT round 6: duotone rows for
   the horizontal traversal + per-channel column headers so mobile fits
   two or three channels of On/Off toggles). */
.console-notify-row { display: flex; justify-content: space-between; align-items: center; gap: var(--b-space-2); padding: var(--b-space-1) var(--b-space-2); flex-wrap: wrap; }
.console-notify-row:nth-child(even of .console-notify-row):not(.console-notify-head) { background: var(--surface-overlay); border-radius: var(--b-radius-sm); }
.console-notify-head { border-bottom: var(--b-border-w) solid var(--border-subtle); }
.console-notify-colhead { display: inline-block; min-width: 4.5rem; text-align: center; font-size: var(--b-text-xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.console-notify-channels { display: flex; gap: var(--b-space-2); }
.console-notify-btn { min-width: 4.5rem; justify-content: center; }
.console-notify-on { border-color: var(--b-accent); }

/* TOTP setup key (TEN-980) — selectable monospace, wraps on mobile */
.console-mono { font-family: var(--b-font-mono, monospace); user-select: all; overflow-wrap: anywhere; }

/* ---- Click-to-edit blocks (TEN-986) ----------------------------------- */
/* The console's block grammar: read-only until Edit, back on submit or
   Cancel. The [hidden] rules are LOAD-BEARING (the journal's modal
   lesson): any author display on these containers would beat the UA
   default, so hidden is pinned explicitly. */
.console-cte-read, .console-cte-edit { margin-top: var(--b-space-2); }
.console-cte-read[hidden], .console-cte-edit[hidden] { display: none; }
.console-avatar-row { display: flex; gap: var(--b-space-3); align-items: center; }
/* No background override here (TEN-1108): this later rule was silently
   ghosting the accent pair the overview definition gives the disc. */
.console-avatar { border-radius: 50%; flex: 0 0 auto; }
.console-avatar-name { font-size: var(--b-text-lg); font-weight: 600; }
.console-method-nick { display: flex; gap: var(--b-space-2); align-items: center; margin: var(--b-space-2) 0; flex-wrap: wrap; }
/* UAT round 6: uniform button heights on the card edit row (no two-line
   buttons) + Cancel pinned to the far right for right-handed use; Remove
   sits far LEFT so it never gets accidentally nudged. TEN-1136 amends the
   ruling for phones: single-line nowrap overflowed the viewport, so under
   720px the row becomes a two-column grid — labels may wrap to two lines,
   every button the same height (1fr rows). */
.console-method-acts-even { align-items: center; }
.console-method-acts-even .console-btn { white-space: nowrap; }
.console-cte-cancel-right { margin-left: auto; }
.console-method-nick input[type="text"], .console-method-nick input[type="password"] {
  flex: 1 1 12rem; padding: var(--b-space-1) var(--b-space-2);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}
.console-method-acts { display: flex; gap: var(--b-space-2); }
@media (max-width: 720px) {
  .console-method-acts-even {
    display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: 1fr;
    align-items: stretch;
  }
  /* The buttons ride inside one-button forms; display:contents makes each
     BUTTON the grid item, so all four stretch to the same height. */
  .console-method-acts-even .console-inline-form { display: contents; }
  .console-method-acts-even .console-btn {
    white-space: normal; line-height: 1.25; padding-block: var(--b-space-2);
  }
  /* margin-left:auto would shove Cancel out of its grid cell. */
  .console-method-acts-even .console-cte-cancel-right { margin-left: 0; }
}
.console-factor { display: flex; align-items: center; gap: var(--b-space-3); margin-bottom: var(--b-space-2); }
/* TEN-1430: the row's action (MFA/passkey "Remove", platform-session "Sign
   out") is a bare flex child of .console-factor, so with no automatic
   minimum size to hold it (button/form overflow is visible) it inherits the
   default flex-shrink:1 and gets squeezed by whatever the neighbouring
   .acct-label text needs — the auth-methods string varies row to row
   ("Password" / "Password + authenticator code" / empty), so the button's
   own box shrank with it, at the extreme wrapping its label onto two lines.
   Anchored here, not on the bare .console-btn: that class is shared with
   longer-label buttons elsewhere (Enroll authenticator, Change password)
   that must stay free to size to their own text. */
.console-factor .console-inline-form { flex: 0 0 auto; }
.console-factor .console-btn { min-width: 6rem; white-space: nowrap; }
.console-pref-form input[type="password"] {
  padding: var(--b-space-1) var(--b-space-2);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}

/* The calendar (TEN-973) */
.ss-cal-month { font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-lg); text-transform: uppercase; letter-spacing: 0.03em; margin: 0 0 var(--b-space-3); }
.ss-cal-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px;
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-2); margin-bottom: var(--b-space-4);
}
.ss-cal-dow { text-align: center; font-size: var(--b-text-xs); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.05em; padding: var(--b-space-1) 0; }
.ss-cal-day { min-height: 3.2rem; padding: var(--b-space-1); border-radius: var(--b-radius-sm); position: relative; }
.ss-cal-day:not(.ss-cal-out) { background: var(--surface-overlay); }
.ss-cal-out { opacity: 0.25; }
.ss-cal-today { outline: 2px solid var(--b-accent); outline-offset: -2px; }
.ss-cal-n { font-family: var(--b-font-mono); font-size: var(--b-text-xs); color: var(--text-muted); }
.ss-cal-item { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin: 2px 2px 0 0; }
.ss-cal-event, .ss-cal-updot.ss-cal-event { background: var(--b-accent); }
.ss-cal-renewal, .ss-cal-updot.ss-cal-renewal { background: var(--b-warning); }
.ss-cal-purchase, .ss-cal-updot.ss-cal-purchase { background: var(--b-success, #16a34a); }
.ss-cal-upcoming { list-style: none; margin: var(--b-space-2) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.ss-cal-upcoming li { display: flex; align-items: center; gap: var(--b-space-2); font-size: var(--b-text-sm); }
.ss-cal-updot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.ss-cal-update { font-family: var(--b-font-mono); color: var(--text-muted); min-width: 3.5rem; }
.ss-cal-upcoming a { color: var(--text-primary); text-decoration: none; }
.ss-cal-upcoming a:hover { text-decoration: underline; }

/* Walk fixes (TEN-973 round 2) */
/* The fuzzy search's clear: OURS only (neutral) — kill the native
   search-cancel so the box never grows a second x. */
.ss-grid-q::-webkit-search-cancel-button, .ss-grid-q::-webkit-search-decoration { -webkit-appearance: none; appearance: none; display: none; }
.ss-grid-qclear { color: var(--text-faint); }
.ss-grid-qclear:hover { color: var(--text-primary); }
/* The handle input's @ prefix adornment: the @ lives OUTSIDE the editable
   value — impossible to delete, unambiguous that you don't type it. */
.console-handle-wrap {
  display: inline-flex; align-items: center; max-width: 18rem;
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-sm);
}
.console-handle-wrap:focus-within { border-color: var(--border-strong); }
.console-handle-at { padding: var(--b-space-1) 0 var(--b-space-1) var(--b-space-2); color: var(--text-muted); font-family: var(--b-font-mono); }
.console-handle-wrap input {
  flex: 1 1 auto; min-width: 0; border: 0; background: transparent; color: var(--text-primary);
  padding: var(--b-space-1) var(--b-space-2) var(--b-space-1) 2px; outline: none;
}
/* Destructive confirmations opt IN to the danger tint (TEN-1230). This used
   to match `.console-modal .ss-grid-modal` — every module modal on the
   console, picture editor included — which is how a red stroke reached
   dialogs that threaten nothing. */
.console-modal-danger .ss-grid-modal { border-color: color-mix(in srgb, var(--b-danger, #dc2626) 45%, var(--border-strong)); }

/* [hidden] must actually hide (Mark's live catch): the modal wrap's
   display:flex OVERRIDES the hidden attribute's UA rule — any author
   display does. Author rule wins it back. */
.console-modal[hidden] { display: none; }

/* ── The scriptless modal (TEN-1266, ADR-0036) ─────────────────────────────
   A modal opener is a LINK now: its href carries `?modal=<wrap-id>#<wrap-id>`,
   the server withholds `hidden` from the named wrap, and dismiss.js swallows
   the navigation whenever it takes the press. So with no scripting a person
   really does arrive on a page with one of these open — and an OVERLAY is the
   wrong render for that. There is no script to close it: the scrim, the × and
   the Cancel are all [data-modal-close], which is dismiss.js's attribute and
   nobody else's, so as a fixed overlay this would be a trap wearing three dead
   controls — the very defect this ticket exists to remove, mirrored.

   So scriptless it is a SECTION, in flow, where the fragment lands the reader.
   Nothing is covering the page, so there is nothing to dismiss, and the close
   chrome is not rendered rather than rendered dead. The way out is the way out
   of any page: the link you came from, or Back.

   `.console-modal[hidden]` above still wins — an attribute selector out-weighs
   this one — so every modal the server did not name stays shut. */
@media not (scripting: enabled) {
  .console-modal {
    position: static; inset: auto; z-index: auto;
    display: block; padding-top: var(--b-space-4);
  }
  .console-modal .ss-grid-modal {
    width: auto; max-height: none; overflow: visible;
    box-shadow: none;
  }
  /* The scrim is in here too — it carries the attribute, and a half-opaque
     sheet over a page nothing is floating above would just be a dark box. */
  .console-modal [data-modal-close] { display: none; }
}

/* Round 4 (Mark's cosmetics pass) */
/* Neutral chrome glyphs: bell + messages ride currentColor */
/* The bell and the envelope get the AVATAR'S BOX as their hit area
   (TEN-1069). The target grows; the glyph does not — an oversized icon is
   not what was asked for and reads worse.

   The badge anchors to .chrome-glyph-wrap rather than to the link, so it
   stays pinned to the glyph instead of drifting to the corner of the new
   (invisible) square. */
.notif-btn, .chrome-iconlink {
  color: var(--text-muted); cursor: pointer;
  width: var(--b-avatar-md); height: var(--b-avatar-md); flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--b-radius-full);
}
.chrome-glyph-wrap { position: relative; display: block; line-height: 0; }
/* The ring outlines the real hit area, not the glyph. */
.notif-btn:focus-visible, .chrome-iconlink:focus-visible {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px;
}
/* A 32px avatar is under the 44px touch-target floor, so on a coarse
   pointer all THREE grow together — matching each other is only a good rule
   if what they match is big enough to hit. Padding inward, so the spacing
   between them does not grow with the boxes. */
@media (pointer: coarse) {
  .notif-btn, .chrome-iconlink, .avmenu-btn { width: 44px; height: 44px; }
}
.notif-btn:hover, .chrome-iconlink:hover { color: var(--text-primary); }
.chrome-glyph { display: block; }
/* Avatar menu: icon row above the console links.

   THE RULE UNDER THE ICON ROW IS GONE, AND SO IS TWO THIRDS OF THE GAP
   (TEN-1865, Mark's M1 UAT-2 finding 4: "the vertical spacing anomaly between
   the icon-button row and the first block of links").

   WHAT THE "STRAY HR" ACTUALLY WAS. There is no `<hr>` in avatar-menu.plate.html
   and there never was — the line a reader sees is this rule's own
   `border-bottom`. Worth recording, because the plate is where anybody would
   look for it and the plate is innocent.

   WHY THE GAP WAS SO BIG. Three separations were stacked between the glyphs and
   the first block, each added by a different ticket and none of them aware of the
   others: this rule's asymmetric `padding-bottom: --b-space-2`, its
   `margin-bottom: --b-space-2`, and `.avmenu-panel`'s own `gap: --b-space-2`,
   which applies between every pair of children in the panel. Three times the
   panel's one spacing unit, with a hairline floating in the middle of it — which
   is why it read as an anomaly rather than as roomy: every OTHER pair in this
   panel (identity→icons, block→block, block→Sign out) is separated by exactly
   one gap, so this pair was the only one that was not.

   THE FIX IS TO LET THE PANEL'S GAP BE THE WHOLE SEPARATION, which also settles
   the rule on K48's own terms: the entry rules that the SHADING is what says a
   block is a block ("the shading IS the label"), and each block already carries
   its own border. A second border above the first block is that boundary drawn
   twice, in two different vocabularies, one of them a line. */
.avmenu-icons { display: flex; gap: var(--b-space-2); justify-content: space-between; padding: var(--b-space-1) 0; }
.avmenu-icon {
  position: relative; flex: 1; display: inline-flex; align-items: center; justify-content: center;
  padding: var(--b-space-2); border-radius: var(--b-radius-sm);
  color: var(--text-muted); background: var(--surface-overlay);
}
.avmenu-icon:hover { color: var(--text-primary); }
.avmenu-panel { min-width: 15rem; max-height: 80vh; overflow-y: auto; }
.avmenu-link.avmenu-danger { color: var(--b-danger, #dc2626); }
/* Mobile: the console tab bar goes away entirely — the avatar menu is
   the navigation (Mark's ruling); wide view keeps the sidebar. */
@media (max-width: 720px) {
  .console-nav { display: none; }
}
/* Invitations */
.ss-invite-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-3);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-3); margin-bottom: var(--b-space-2);
  color: var(--text-primary); text-decoration: none;
}
.ss-invite-row:hover { background: var(--hover); }
.ss-invite-name { font-family: var(--b-font-heading); font-weight: 800; text-transform: uppercase; letter-spacing: 0.02em; }
.ss-invite-cta { color: var(--text-muted); font-size: var(--b-text-sm); text-decoration: none; }
/* An invitation is two doors, not one (TEN-1313): the row carries Decline
   beside Register, so answering "no" is as cheap as answering "yes". The
   row stopped being a single <a> when the form arrived — the name is the
   link now. */
.ss-invite-main { flex: 1 1 auto; min-width: 0; color: inherit; text-decoration: none; }
.ss-invite-decline { margin: 0; }
/* Community-profile form fields (UAT round 7b).
   These used --surface-2 / --line / --ink, which this cover NEVER DEFINED —
   so every one of them fell through to a hardcoded dark literal and ignored
   the theme completely. Measured in the light theme: the page was
   rgb(242,243,245) and these controls were rgb(34,34,34), a near-black box
   on a white page. Ten rules across four phantom tokens (TEN-1100); they
   now use the real ones the rest of the cover uses. */
.console-pref-form textarea, .console-pref-form select {
  width: 100%; padding: 0.45rem 0.6rem; border-radius: 6px;
  border: 1px solid var(--border-subtle); background: var(--surface-raised);
  color: var(--text-primary); font: inherit;
}
.console-check { display: flex; gap: 0.5rem; align-items: flex-start; font-size: 0.9rem; margin: 0.5rem 0; }
.console-check input { width: auto; margin-top: 0.2rem; }

/* Person rows (UAT round 7 — My Network identity display). */
.console-person { display: inline-flex; align-items: center; gap: 0.55rem; min-width: 0; }
.console-person-av {
  /* Row avatars match the header's (TEN-1023: "resize to same size as
     header") — one size for a person wherever they appear in a list. */
  width: var(--b-avatar-md); height: var(--b-avatar-md); border-radius: 50%; object-fit: cover; flex: none;
  background: var(--surface-raised);
}
.console-person-initial {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.8rem; font-weight: 600;
  /* The accent treatment (TEN-1108), the same pair .console-avatar wears:
     role tokens, never ad-hoc grays. The old ghost disc (muted text on a
     raised surface behind a 1px border) read SMALLER than the photo beside
     it — the border ate the diameter and the fill vanished into the page.
     No border now: a photo has none, and the circles must match. */
  background: var(--b-accent); color: var(--b-accent-fg);
}
.console-person-id { display: inline-flex; flex-direction: column; line-height: 1.25; min-width: 0; }

/* THE ORG FACE — ONE MODIFIER, DECLARED ONCE (TEN-1869, Mark's ruling
   2026-09-24; the K47 amendment note in the rulings ledger).

   An organization's avatar is a ROUNDED SQUARE wherever it appears, because the
   SHAPE IS THE KIND SIGNAL: a circle is a person, a rounded square is a club. A
   reader should never have to read the label under a face to know which kind of
   thing it is.

   `.pp-org .pp-avatar` said this first — "clubs are squared, as in the people
   rows" — and the rows it cited had drifted back: `console-person-av`'s 50% on
   every org row above, a `▦` glyph standing in for a face on My Network, and a
   `people-org-glyph` squaring the INITIALS at a different radius on search while
   an org that had a PICTURE stayed round. Three improvisations for one rule, so
   the rule lives here now and every family wears the same class.

   THE LINK CARRIES IT TOO: the face-link's hover/focus ring follows the anchor's
   own radius, so a pill ring around a squared face would be K15's silhouette
   rule broken at the one place it is visible. Same shape, face and ring.

   THE RADIUS IS A PROPORTION AND NOT A NUMBER (TEN-1879, Mark's M2 UAT). This
   rule said `--b-radius-md` — 10px — and 10px is a rounded square on the 32px
   row face it was written against and a CIRCLE on the 24px face the search
   preview draws: 10 of 24 leaves four pixels of straight edge a side, and in
   Mark's screenshot the club and the person beside it are the same shape. The
   modifier was on the markup, on both arms, on both boxes — the class was never
   the miss — so a ticket looking for the arm that forgot it would have found
   every arm carrying it and the signal still gone.

   A KIND SIGNAL HAS TO HOLD ITS SHAPE AT EVERY SIZE IT IS DRAWN AT, and a
   constant radius cannot: it says "rounded square" at one diameter and drifts
   toward "circle" at every smaller one, silently, with nothing to fail. A
   percentage is the same corner at 24px, at 32px and at whatever the next
   surface picks from the avatar scale, which is what "declared once" has to mean
   for a shape. 30% is the proportion the blessed 32px row face already had
   (10/32 = 31%), so the surfaces TEN-1869 shipped are unmoved to the pixel and
   the small face is the one that changes — which is the one that was wrong. */
.av-org,
.console-person-av.av-org,
.hdr-people-av.av-org,
.msg-head-av-link.av-org,
.msg-people-av-link.av-org { border-radius: 30%; }

/* The add-card Element mount (UAT round 7). */
#wallet-form { margin-top: 0.5rem; }
#wallet-element { margin-bottom: 0.75rem; }
.ss-subcard-portal { margin-top: 0.5rem; }

/* Slide toggles (UAT round 7 — legacy's notification switches). The
   switch IS the submit button: no-JS friendly, one POST per flip. */
.console-switch {
  position: relative; display: inline-block; width: 2.6rem; height: 1.4rem;
  border-radius: 0.7rem; border: 1px solid var(--border-subtle);
  background: var(--surface-raised); cursor: pointer; padding: 0;
  transition: background 0.15s ease;
}
.console-switch-thumb {
  position: absolute; top: 0.12rem; left: 0.14rem; width: 1.05rem; height: 1.05rem;
  border-radius: 50%; background: var(--text-muted);
  transition: left 0.15s ease, background 0.15s ease;
}
.console-switch-on { background: var(--accent, #e05a2b); border-color: var(--accent, #e05a2b); }
.console-switch-on .console-switch-thumb { left: 1.32rem; background: #fff; }
.console-switch:focus-visible { outline: 2px solid var(--accent, #e05a2b); outline-offset: 2px; }

/* Entitlements (punch cards + coverage) */
.ss-ents { margin-top: var(--b-space-4); }
.ss-ents .acct-stub-title { margin-top: var(--b-space-3); }
.ss-ent-card {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-3); margin-bottom: var(--b-space-2);
}
.ss-ent-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--b-space-3); }
.ss-ent-name { font-weight: var(--b-weight-semibold); }
.ss-ent-left { font-family: var(--b-font-mono); font-weight: 700; color: var(--b-accent); }
.ss-ent-through { font-family: var(--b-font-mono); font-size: var(--b-text-sm); color: var(--text-muted); }
.ss-ent-punches { color: var(--text-muted); font-size: var(--b-text-sm); margin-top: var(--b-space-1); }
/* Notifications page reuses the bell rows full-width */
.notif-page { position: static; width: auto; }
.notif-pagehead { display: flex; align-items: baseline; gap: var(--b-space-3); }
.notif-settings-link { margin-left: auto; font-size: var(--b-text-sm); color: var(--lb); }

/* Round 5 */
/* Header tooltips drop DOWN and hug the right edge — above the header is
   off-screen (Mark's catch: desktop clipped, mobile a 1px sliver). */
.app-header [data-tip]:hover::after { bottom: auto; top: calc(100% + 4px); left: auto; right: 0; }
/* Microtext for security notes */
/* --text-muted, NOT --text-faint. This is the TINY tier of the grammar
   documented at tokens.css's role-token block (TEN-1454, formalizing the
   TEN-1411 fix below into a kit-wide rule): tiny/meta text is something a
   reader is meant to read, so it gets the tier proven legible, never the
   one reserved for inert controls.
   Origin (TEN-1411 #4, Mark's UAT: "small text is rendered looking inactive
   - it needs wattage so that its readable"): measured against the surfaces
   this class actually renders on, --text-faint was 2.25:1 in dark and
   3.19:1 in light — both under the 4.5:1 floor, and dark was close to the
   3:1 where text stops being text and starts reading as a disabled
   control, which is exactly what he saw. --text-muted takes the same
   strings to 4.80:1 and 6.46:1, and changes nothing in the high-contrast
   theme, which was already fine at 12.63:1.
   ONE TOKEN, not one string: .console-micro carries the sentence he quoted and
   roughly every other explanatory line in the console, so fixing the quoted
   one would have left its neighbours dim and the next reviewer re-reporting
   the same bug about a different sentence. It is still the quietest text on
   the page — --text-muted is a step below --text-primary — so the hierarchy
   this class exists for survives; it just clears legibility now.
   Siblings on the same tier: .console-notify-colhead, .console-handle-at —
   already verified on --text-muted, no drift to sweep here. */
.console-micro { color: var(--text-muted); font-size: var(--b-text-xs); margin: var(--b-space-1) 0 0; }
/* The danger section now lives at the BOTTOM of Security — same red
   quarantine, no standalone tab shouting from the nav. */
.console-danger-rule { border: 0; border-top: 2px solid color-mix(in srgb, var(--b-danger, #dc2626) 45%, var(--border-subtle)); margin: var(--b-space-5) 0 var(--b-space-3); }

/* Messages (TEN-999). Two views, never both: the inbox lists conversations,
   and opening one gives the whole column to that conversation (UAT — a chat
   pane under its own index reads as a sidebar of itself). This cover's
   console column measures ~560px, which is a readable line and nothing to
   spare. */
/* THE CHAT HEADER (TEN-1024): one row — back, face, who, then the wire.
   It replaces a stacked back-link / title / meta-line stack that spent three
   rows saying what fits in one. */
.msg-head {
  display: flex; align-items: center; gap: var(--b-space-3);
  margin: 0 0 var(--b-space-3); min-width: 0;
  /* The people sheet hangs off this row (TEN-1067). */
  position: relative;
}
.msg-head-id { min-width: 0; }
.msg-head-id .acct-value,
.msg-head-id .acct-label {
  display: block; line-height: var(--b-leading-tight);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.msg-head-id .acct-value { font-weight: var(--b-weight-semibold); }
/* Everything after the person block is pushed right: the presence chip
   travels with the name, the wire's state sits at the far end. */
.msg-head .msg-live { margin: 0 0 0 auto; }
/* Ghost square, per the design-kit gallery, with its states. The label is a
   tooltip because the arrow is the affordance and the row is tight. */
.msg-back-btn {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: var(--b-avatar-md); height: var(--b-avatar-md);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  background: transparent; color: var(--text-muted);
  font-size: var(--b-text-lg); line-height: 1; text-decoration: none;
  transition: background var(--b-duration-fast) var(--b-ease), color var(--b-duration-fast) var(--b-ease);
}
.msg-back-btn:hover { background: var(--surface-overlay); color: var(--text-primary); border-color: var(--border-strong); }
.msg-back-btn:active { background: var(--surface-raised); }
.msg-back-btn:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.msg-gate {
  margin: 0 0 var(--b-space-2); padding: var(--b-space-2) var(--b-space-3);
  font-size: var(--b-text-sm); color: var(--text-muted);
  border-left: 3px solid var(--b-warning, #d97706); background: var(--surface-overlay);
}
.msg-inbox, .msg-pane {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-4); margin-bottom: var(--b-space-3);
}
.msg-row {
  display: flex; align-items: flex-start; gap: var(--b-space-2); justify-content: space-between;
  padding: var(--b-space-2); border-radius: var(--b-radius-sm);
  text-decoration: none; color: inherit;
}
/* HOVER OWNS THE WRAP, NOT THE ANCHOR (TEN-1819, the schedule rows' pattern). The
   whole row is one press, so the whole row is what lights up — a highlight that
   stopped at the anchor's edge would say the press stopped there too. */
.msg-row-wrap:hover { background: var(--surface-overlay); }
/* The row's way into its people (TEN-1085). Quiet until you want it, and a
   real link so it works without scripting and keeps its own hit area rather
   than borrowing the row's. */
.msg-row-wrap {
  display: flex; align-items: stretch; gap: var(--b-space-1);
  position: relative; border-radius: var(--b-radius-sm);
}
.msg-row-wrap .msg-row { flex: 1 1 auto; min-width: 0; }

/* ── THE WHOLE ROW IS ONE PRESS (TEN-1819) ─────────────────────────────────
   `.ktrow-linked`'s move, spelled the way that family spells it, because a reader
   who knows one list in this book knows them all: the row's own anchor grows an
   `::after` over the wrap, and the only things lifted above it are the row's OTHER
   addresses. The timestamp, the pip, the sub-line and the severed mark are FACTS
   and stay under it — a fact that intercepted the press would leave the row with
   dead patches, which is the exact defect this pattern exists to remove and the
   defect a reader reported as "the row doesn't open unless I hit the name".

   THE FOCUS RING BELONGS TO THE WRAP once the wrap is the target: a name-width
   anchor with a row-wide hit area would otherwise outline a fraction of what a
   keyboard press activates. The kit row's own reasoning, unchanged. */
.msg-row::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.msg-row-wrap > .msg-row-people,
.msg-row-wrap > .conv-row-acts { position: relative; z-index: 1; }
.msg-row-wrap:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.msg-row-wrap > .msg-row:focus-visible { outline: none; }

/* ── THE ROW'S TWO LINES (TEN-1819) ────────────────────────────────────────
   A name over a sub-line, each on ONE line, each truncating with its own ellipsis.
   Neither wraps, so every row is the same height and the list scans as a column
   rather than as a stack of differently-sized cards — which is what let a long
   group name climb into the timestamp ("the Brosb4hoes overlap").

   `min-width: 0` is what makes `text-overflow` work at all inside a flex row: a
   flex item's default min-width is min-content, so without it the name pushes the
   box wider instead of clipping, and the ellipsis never appears. */
.conv-row-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.conv-row-name {
  display: block; min-width: 0; font-size: 13px; line-height: 1.35;
  color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* THE UNREAD SIGNAL IS WEIGHT PLUS A COUNT (TEN-1819). Weight and not colour: K29
   allows one live treatment and the pip beside it is already spending it, and a
   list where a third of the names are accent-coloured has no accent left for the
   row you are actually in. */
.conv-row-name-unread { font-weight: var(--b-weight-bold); color: var(--text-primary); }
.conv-row-sub {
  display: block; min-width: 0; font-size: 11.5px; line-height: 1.35;
  color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A class chat's event chip and its "ended" pill (TEN-1903, board E3), riding the name line so the
   row stays two lines (TEN-1819). An ended class chat keeps its row at the archived treatment's weight. */
.conv-row-chip {
  display: inline-block; margin-left: 6px; padding: 0 7px; border-radius: 999px;
  font-size: 10.5px; font-weight: var(--b-weight-normal); line-height: 1.6;
  background: var(--surface-overlay); color: var(--role-primary-ink); vertical-align: 1px;
}
.conv-row-chip-ended { color: var(--text-muted); }
.msg-row-ended { opacity: 0.55; }
.msg-row-people {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; flex: none; border-radius: var(--b-radius-sm);
  color: var(--text-faint); text-decoration: none; font-size: var(--b-text-lg);
}
.msg-row-people:hover { color: var(--text-primary); background: var(--surface-overlay); }
.msg-row-people:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
/* The cluster IS the trigger now (TEN-1249, retiring the ⋯): it leads the
   wrap, wearing the footprint the row's own face slot had — plus the row's
   own left padding so group and dm rows keep one grid — and the whole face
   is the hit area. The row anchor starts at the title beside it. */
.msg-row-people-face {
  width: auto; height: auto; padding: var(--b-space-2) 0 var(--b-space-2) var(--b-space-2);
  border-radius: var(--b-radius-sm); align-self: stretch;
}
.msg-row-people-face:hover { background: var(--surface-overlay); }
.msg-row-wrap .msg-row-people-face + .msg-row { padding-left: var(--b-space-1); }
/* [hidden] must actually hide, and .msg-row is display:flex — which beats
   the UA stylesheet's [hidden] rule on specificity. This is not decoration:
   the server marks a search miss hidden (markThreadHits), so without this
   line a filtered list shows everything, and it shows everything on a page
   with no scripting at all. Same trap as the modal wrap. */
.msg-row[hidden], .msg-row-wrap[hidden] { display: none; }
/* The conversation on a phone (TEN-1066).
   ------------------------------------------------------------------------
   The real fault was one line up, on main.container: an auto margin on a
   flex item kills cross-axis stretch, so the page sized to max-content and
   the whole document scrolled sideways. That is fixed there, not here.

   What is left here is deliberate: the cover's .container pads 12px each
   side, which is right for a console page and wrong for a chat. A
   conversation wants the width — every messaging app a person has used is
   edge to edge — so cancel the gutter at phone widths and put it back
   inside, where it keeps text off the glass.

   (A min-width:0 chain was tried here first and removed: with the container
   fixed it changed nothing at 375px or 320px. Measured both times.) */
@media (max-width: 640px) {
  /* The header and the composer travel with the history — a full-bleed list
     under an inset composer would look like a mistake in the other
     direction. */
  .msg-screen {
    margin-left: calc(-1 * var(--b-space-3));
    margin-right: calc(-1 * var(--b-space-3));
    width: auto;
  }
  /* The gutter comes back INSIDE, but ONLY on the direct children of
     .msg-screen. .msg-composer lives inside .msg-pane, which already pads
     16px, and padding it again put the composer 28px in while the header sat
     at 12 (measured). */
  .msg-head, .msg-typing-line { padding-left: var(--b-space-3); padding-right: var(--b-space-3); }
}

/* The group avatar cluster (TEN-1065, fixed in TEN-1074) — the SMS pattern.
   Member faces packed into EXACTLY the footprint one avatar occupies, so a
   group row and a 1:1 row still scan on the same grid.

   THE RULE THAT WAS MISSING: a face is sized from the CLUSTER, never from
   its grid cell. Cells are not square — two faces share one row, and a
   spanning face is twice as wide — so `width/height: 100%` made a face take
   the shape of whatever cell it landed in. Measured before this fix: two
   faces rendered 16x32, a 0.48 ratio, visibly egg-shaped on a real account;
   three faces put a full-size 32x32 face across the top and pushed the
   other two OUTSIDE the cluster bounds. Only the four-face case was right.

   So --face is a definite square derived from the avatar size, and
   place-items centres it in whatever cell it gets. Every arrangement is
   then circles by construction rather than by the cell happening to be
   square. */
.msg-cluster {
  --face: calc((var(--b-avatar-md) - 1px) / 2);
  width: var(--b-avatar-md); height: var(--b-avatar-md); flex: none;
  display: grid; gap: 1px; place-items: center;
  grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr;
}
.msg-cluster-av, .msg-cluster-more-chip {
  width: var(--face); height: var(--face);
  border-radius: 50%; object-fit: cover;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.5rem; line-height: 1;
}
.msg-cluster-av {
  background: var(--surface-raised); color: var(--text-muted);
}
/* Initials faces in the cluster take the accent pair (TEN-1108) — the
   surface-raised stays on imgs only, as the ground behind transparency. */
.msg-cluster-initial { background: var(--b-accent); color: var(--b-accent-fg); }
/* Two faces read better on one row than stacked in a corner. */
.msg-cluster-2 { grid-template-rows: 1fr; }
/* Three faces and NOTHING else: one across the top, two below, so the shape
   stays balanced rather than leaving a hole in a corner. The span only sets
   where the face is CENTRED now — it no longer sets how big it is.
   Scoped away from the overflow case on purpose — three faces PLUS a chip is
   four cells, and letting the first span two columns pushes the fourth out of
   the box entirely (measured: faces rendering outside the cluster bounds). */
.msg-cluster-3:not(.msg-cluster-more) > :first-child { grid-column: 1 / -1; }
/* The +n chip takes the last quadrant, and only above four others. */
.msg-cluster-more-chip {
  background: var(--surface-overlay); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The admin's star (TEN-1093), bottom-left of the avatar — the quadrant
   presence left free. Sized from the AVATAR, not from whatever box it lands
   in, which is TEN-1074's lesson wearing a different hat. */
/* The moderator star is ALWAYS in the roster markup and shown only on a row carrying
   `.room-row-mod` (TEN-1423), rather than rendered conditionally. A live `member.role`
   event can grant or clear the flag while a member panel is open, and the live client's
   standing rule is that it never composes what the plate owns -- building this badge would
   be a second copy of a glyph and its screen-reader word. One class toggle instead, the
   same shape `.room-floor-strike` already uses for the microphone. */
.msg-people-admin { display: none; }
.room-row-mod .msg-people-admin {
  /* `display` is stated here because the base rule above turns it off; the original rule relied on
     a <span>'s default and so had none to override. Caught in the browser, not by reading. */
  display: inline;
  position: absolute; left: -2px; bottom: -2px;
  font-size: 0.6rem; line-height: 1; color: var(--b-warning, #d4a017);
  text-shadow: 0 0 2px var(--surface-raised), 0 0 2px var(--surface-raised);
  pointer-events: none;
}
/* Leave and Rename left, Invite right (TEN-1092). Destructive at the far
   edge from a right thumb, additive under it — see the plate for why. */
.msg-people-acts {
  display: flex; align-items: center; gap: var(--b-space-2);
  margin-top: var(--b-space-3); flex-wrap: wrap;
}
/* Three classes, not two (TEN-1114, Mark's UAT: Leave sat below its row).
   The panel-wide rule below gives every inline form a top margin and it is
   LATER in the file — equal specificity let it override this zero, so the
   Leave form (the row's only form) dropped while the bare Rename/Invite
   buttons stayed put. The extra class wins on specificity, not on order. */
.msg-people-panel .msg-people-acts .console-inline-form { margin: 0; }
/* The admin pair rides the right edge TOGETHER (TEN-1132, Mark's ruling):
   the auto-margin lives on the gear when it renders, and falls back to
   Invite for plain members (no gear in their row). */
.msg-people-acts .msg-people-invite { margin-left: auto; }
.msg-people-acts .msg-people-edit { margin-left: auto; }
.msg-people-acts .msg-people-edit ~ .msg-people-invite { margin-left: 0; }

/* Who spoke last, ahead of the timestamp (TEN-1057). Dimmer than the time
   because it is context, not the fact the row is sorted by. */
/* "No longer connected" (TEN-1094) — dimmed and italic so it recedes next
   to the timestamp rather than competing with it. A statement about the
   RELATIONSHIP, which is the thing that changed; the conversation itself is
   intact and resumes the moment the connection does. */
/* "No longer connected" — A MARK ON THE ROW AND A SENTENCE IN THE SHEET (TEN-1817).
   Its third home, and the move is the same one it has made twice: it began in the
   meta column beside the timestamp, where a phone had nowhere to put it and it
   overlapped the name; TEN-1099 gave it its own line under the handle. There is no
   handle line on this row any more (the rail is a third of the screen now), and a
   third line under a slimmed name is the crowding this ticket exists to undo.

   SO THE ROW KEEPS THE SIGNAL AND THE SHEET KEEPS THE SENTENCE. One warning glyph
   says "there is something about this one", its title says what, and Details… carries
   the full wording as a danger chip — from ONE server-side string, so the mark and the
   chip can never say different things. A mark with no explanation anywhere would be
   the mistake; a mark whose explanation is one press away is a disclosure ladder. */
.msg-row-severed-mark {
  margin-left: var(--b-space-1); color: var(--b-warning, #d97706);
  font-size: var(--b-text-xs); line-height: 1; cursor: help;
}
.msg-row-by { color: var(--text-faint); }
/* THE CORNER IS A COLUMN NOW (TEN-1819): the stamp on top, the unread pip under it.
   Two facts stacked at the row's right boundary rather than side by side, which is
   what gives the ellipsis a corner to fade into on hover without covering the pip —
   the one signal on this list a reader must never lose sight of. Tabular figures,
   because a column of stamps that shifts by a digit is not a column. */
.msg-row-meta {
  display: inline-flex; flex-direction: column; align-items: flex-end;
  gap: 2px; flex: none;
}
.conv-row-when { font-variant-numeric: tabular-nums; white-space: nowrap; }
.msg-pip {
  min-width: 1.25rem; text-align: center; border-radius: 0.75rem;
  background: var(--b-accent); color: #fff; font-size: var(--b-text-xs);
  font-weight: 700; padding: 0 0.35rem;
}
/* The chat grammar (TEN-1010/TEN-1011). History is days of runs of
   bubbles: a quiet centred divider at each day boundary, consecutive
   messages from one author packed tight with ONE timestamp on the last of
   them, and the other person's face only where a face is needed — a group.
   The pane scrolls, not the page, so the composer never leaves the screen
   and the newest message is where the eye already is. */
.msg-thread {
  display: flex; flex-direction: column; gap: var(--b-space-3);
  margin: var(--b-space-3) 0; max-height: min(60vh, 32rem);
  overflow-y: auto; overscroll-behavior: contain; padding-right: var(--b-space-2);
}

/* THE CONVERSATION OWNS THE VIEWPORT (Mark, 2026-08-13). A chat screen
   where the PAGE scrolls is a chat screen where the composer walks off the
   bottom while you read. So on this one screen the page is pinned to the
   viewport and the history pane is the only thing that scrolls — the
   header, the title, the composer and the live line stay put, and the pane
   takes whatever is left.

   Every link in the chain has to agree, because a flex child only gets a
   bounded height if its parent has one — hence `min-height: 0` at every
   rung (the default `auto` lets a child grow past its parent and hands the
   overflow back to the page, which is the bug this replaces).

   Guarded by @supports so a browser without :has() keeps the plain bounded
   pane above rather than an unbounded one. `dvh`, not `vh`: on a phone the
   URL bar is part of the answer. */
@supports selector(:has(*)) {
  body.app:has(.msg-screen) {
    height: 100dvh; min-height: 0; overflow: hidden;
    display: flex; flex-direction: column;
  }
  body.app:has(.msg-screen) main.container {
    flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
  }
  body.app:has(.msg-screen) .console { flex: 1 1 auto; min-height: 0; align-items: stretch; }
  body.app:has(.msg-screen) .console-main { display: flex; flex-direction: column; min-height: 0; }
  /* The engine's own instance wrapper sits between the console column and
     the plate, so it is part of the chain whether the author sees it or not. */
  body.app:has(.msg-screen) .console-main > .b-component {
    display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
  }
  body.app:has(.msg-screen) .app-foot,
  body.app:has(.msg-screen) .judgefoot { display: none; }
  .msg-screen { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
  .msg-screen .msg-pane { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
  .msg-screen .msg-thread { flex: 1 1 auto; min-height: 0; max-height: none; }
  /* TEN-1026: the history takes the slack, so the composer sits on the FLOOR
     of the pane instead of floating directly under a short conversation.
     Every rung of this chain still needs min-height: 0, or the pane hands
     its overflow back to the page and the whole pinned column unwinds. */
  .msg-screen .msg-composer { margin-top: auto; }
  /* The console nav is sticky on desktop; inside a pinned column it has
     nothing to stick to and must not stretch to the full height. */
  body.app:has(.msg-screen) .console-nav { position: static; align-self: flex-start; }
}
.msg-day {
  display: flex; align-items: center; gap: var(--b-space-2);
  margin: 0; font-size: var(--b-text-xs); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.msg-day::before, .msg-day::after {
  content: ""; flex: 1 1 auto; height: var(--b-border-w); background: var(--border-subtle);
}
.msg-run { display: flex; align-items: flex-start; gap: var(--b-space-2); }
.msg-run + .msg-run { margin-top: calc(-1 * var(--b-space-2)); }
.msg-run-mine { justify-content: flex-end; }
.msg-face { flex: none; margin-top: 0.15rem; }
/* The bubble face stays the SMALL rung: it sits inside a message run and
   is there to tell speakers apart, not to introduce them. */
.msg-face .console-person-av { width: var(--b-avatar-sm); height: var(--b-avatar-sm); font-size: var(--b-text-xs); }
.msg-run-bubbles {
  display: flex; flex-direction: column; gap: 0.2rem;
  min-width: 0; max-width: 80%;
}
.msg-run-mine .msg-run-bubbles { align-items: flex-end; }
/* One message is a SLOT (TEN-1213): the bubble, the chips under it, and the
   actions menu anchored to it. The slot is the positioning context — the menu
   is absolute against it — and it shrinks to its bubble rather than filling
   the column, or the "…" would float in space at the end of a short message.

   `position: relative` is doing the anchoring; `max-width: 100%` keeps a long
   message inside the run's 80% the way the bubble used to on its own.

   (That paragraph described the INTENT and, on the peer side, only the mine
   column ever delivered it — see the entry immediately below, which is the rule
   that makes the sentence above true on both sides.) */
/* THE SLOT SHRINKS TO ITS BUBBLE (TEN-1820, Mark's UAT round 2).
   `align-items: flex-start` is the whole of the reaction fix. Without it the
   slot's children STRETCH to the run's 80% column, so a peer's chip row was as
   wide as the widest message in the run — a 👍1 floating in the middle of the
   thread, belonging to no message in particular. The blessed mine column
   already had the answer (`.msg-slot-mine` below has aligned to flex-end since
   TEN-1213), and this is that answer given to the other side.
   It also lets a short message from somebody else be a short bubble instead of
   a full-width band, which is the same fact seen from the other end. */
.msg-slot { position: relative; display: flex; flex-direction: column; align-items: flex-start; max-width: 100%; }
/* MUST STAY BELOW the rule above — it is the one that keeps your own messages
   on their side, and a cascade tie goes to the last one written. */
.msg-slot-mine { align-items: flex-end; }
/* Who is speaking, over the first bubble of their run (TEN-1820). Quiet on
   purpose: it is a label on a run, not a heading — the bubbles under it are
   what the eye is going to. It sits in the bubbles column, so it is already
   indented past the avatar and lines up with the bubble's left edge. */
.msg-run-name {
  margin: 0 0 0.1rem; padding: 0 0.15rem;
  font-size: var(--b-text-xs); font-weight: 600; color: var(--text-muted);
  max-width: 100%; overflow-wrap: anywhere;
}
.msg-bubble {
  padding: var(--b-space-2) var(--b-space-3);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-overlay); max-width: 100%;
}
/* Your own messages are FILLED, not outlined (UAT 2026-08-13): an outline
   at this contrast reads as the same bubble as everyone else's. */
.msg-bubble-mine {
  margin-left: auto;
  background: color-mix(in srgb, var(--b-accent) 22%, var(--surface-overlay));
  border-color: color-mix(in srgb, var(--b-accent) 55%, var(--border-subtle));
}
.msg-body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
/* THE GHOST RUNG: A REMOVED MESSAGE, DRAINED (TEN-1885, K41 proposed — PM design under Mark's own
   ask, which lifted his blessed-tombstone fence for exactly this).
   A tombstone KEEPS this bubble's geometry and its side. The run's rhythm and the mine/theirs read
   are facts about the thread, and removing one message does not change either — which is why this is
   a rung of the bubble family rather than a system-event line. It is the WhatsApp-family answer, and
   a room's own settings already make the promise out loud: "Leave a mark where it was".
   WHAT IS TAKEN AWAY: the fill. Your own bubble is accent-filled and everyone else's carries the
   overlay ground; a husk carries neither, because a fill is what makes a bubble read as CONTENT and
   there is no content left. What is left is a hairline — the subtle border at the inert wattage, so
   the shape is still legibly a bubble in the column it belongs to.
   MUST STAY BELOW .msg-bubble-mine: both are single-class selectors, so the cascade tie goes to the
   last one written, and the whole point of this rule is to beat the accent fill (the TEN-1224
   cascade lesson, the same one .msg-chip-pending's entry cites).
   AND THE INERT WATTAGE IS SPENT ON THE CHROME, NOT AS AN OPACITY OVER THE WHOLE BUBBLE — which is
   tokens.css's own rule doing real work rather than pedantry: a piece of text is either something to
   read (muted) or something that is off (opacity-dimmed), never both, and stacking 0.55 over
   --text-muted is how a husk becomes unreadable on a phone in dark mode. So the token mixes the
   border down and the HUSK'S OWN TIER carries "off" for the words. K12's pairing rule is satisfied
   the strongest way available: what this bubble is, it says, in words, inside itself. */
.msg-bubble-ghost {
  background: none;
  border-color: color-mix(in srgb, var(--border-subtle) calc(var(--wattage-inert) * 100%), transparent);
}
/* Muted, and italic from the plate's own <em> — the husk is a description of an absence rather than
   somebody's words, and italic is how every messenger in this family says so. */
.msg-bubble-ghost .msg-body { color: var(--text-muted); }
/* The @-mention callout (TEN-1250, ADR-CM009 §4). Subtle on purpose: it marks
   a name, it does not shout — a tint and a weight, no border, no pill outline
   competing with the reaction chips underneath. Written against --b-accent the
   way .msg-bubble-mine and .msg-chip-mine are, so it re-themes for free and
   there is no raw hex in a structural rule (ADR-0032).

   The tint is weak because this has to survive landing INSIDE your own bubble,
   which is already accent-filled: a strong fill would disappear there and a
   dark one would fight it. Weight and colour carry it in both places. */
.msg-mention {
  color: color-mix(in srgb, var(--b-accent) 80%, var(--text-primary));
  background: color-mix(in srgb, var(--b-accent) 14%, transparent);
  border-radius: var(--b-radius-sm);
  padding: 0 0.15em;
  font-weight: 600;
}
.msg-bubble-mine .msg-mention {
  color: var(--text-primary);
  background: color-mix(in srgb, var(--b-accent) 30%, transparent);
}
/* Jumbo emoji-only messages (TEN-1210, ADR-CM009 §4): 1-3 emoji and nothing
   else lose the bubble entirely and step down in size as the count goes up, so
   one emoji reads as a gesture and three still read as one message. The count
   is decided server-side in chatHistory() — this is only the paint. */
.msg-bubble-jumbo {
  padding: 0; background: none; border-color: transparent; line-height: 1.15;
}
.msg-bubble-jumbo .msg-body { font-size: var(--jumbo-size); }
.msg-bubble-jumbo-1 { --jumbo-size: 2.9rem; }
.msg-bubble-jumbo-2 { --jumbo-size: 2.4rem; }
.msg-bubble-jumbo-3 { --jumbo-size: 2rem; }
/* The receipt still has to read on a bubble with no background behind it. */
.msg-bubble-jumbo .msg-foot { text-align: right; }
/* Reaction chips (TEN-1213). Under the bubble and OUTSIDE it, so they sit on
   the page rather than inside your own message's fill — and so a jumbo
   message, which has no bubble at all, still has something to hang them on.

   They follow the message's side: left under a peer, right under your own,
   which the slot's alignment does for free. */
.msg-chips { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem; max-width: 100%; }
.msg-chip-form { margin: 0; display: contents; }
.msg-chip {
  display: inline-flex; align-items: center; gap: 0.2rem;
  padding: 0.1rem 0.4rem; cursor: pointer;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: 999px;
  font-size: var(--b-text-xs); line-height: 1.5;
}
.msg-chip:hover { border-color: var(--border-strong); }
/* A chip you hold is FILLED, the same distinction your own bubble makes —
   an outline at this size is not a state anybody notices. */
.msg-chip-mine {
  background: color-mix(in srgb, var(--b-accent) 22%, var(--surface-overlay));
  border-color: color-mix(in srgb, var(--b-accent) 55%, var(--border-subtle));
}
.msg-chip-glyph { font-size: var(--b-text-sm); line-height: 1; }
.msg-chip-count { font-variant-numeric: tabular-nums; color: var(--text-muted); }
.msg-chip-mine .msg-chip-count { color: var(--text-primary); }
/* Pending acknowledgment (TEN-1240, ADR-0030 §3 annotation): the instant a
   reaction is tapped the chip dims and goes inert — deliberately tentative,
   never presented as fact — until the action's own re-render replaces it with
   the record. Sits BELOW the base chip rules; must stay there (the TEN-1224
   cascade lesson). pointer-events backs up the disabled attribute for the
   provisional span, which is not a button. */
.msg-chip-pending { opacity: 0.5; pointer-events: none; }
/* The same rule on Send (TEN-1249): dimmed and inert while the send is in
   flight; the morphed composer arrives with the server's own live button. */
.msg-send-pending { opacity: 0.5; pointer-events: none; }
/* AND THE BUBBLE THE CLIENT DREW (TEN-1885). A message the reader has pressed Send on is in the pane
   immediately, in their own column, with the words they typed and a local timestamp — not yet a fact.
   THE ABSENT ✓ IS THE PENDING SIGNAL, and the fade is the second voice rather than the first. That is
   the messenger convention Mark named ("slightly faded or with no checkmarks"), and it is also the
   only honest option: a tick claims the platform has the message, and the client cannot know that.
   In the mine column it is unmissable — every landed bubble around it carries ✓ or ✓✓, so a bubble
   with a timestamp and no tick is different at a glance rather than by degree.
   THIS IS NOT `opacity`, AND THAT IS THE ONE INTERESTING LINE IN THE RULE. It was, in this ticket's
   first draft — 0.5, joining .msg-chip-pending and .msg-send-pending above by spelling. Measured, that
   takes the reader's OWN MESSAGE from 7.50:1 to 3.30:1 against this pane's ground in the dark theme,
   under the 4.5:1 that text this size needs, on precisely the phone-in-the-dark surface the UAT is run
   on. The two siblings are CONTROLS going inert, and dimming a button nobody needs to re-read costs
   nothing; a bubble is CONTENT, and the content is the words somebody just wrote.
   So the pending rung follows the same rule the ghost rung below does, which is the rule tokens.css
   states and K41 makes binding: THE CHROME CARRIES THE STATE, THE INK CARRIES READABILITY. The fill
   drops to less than half a landed message's accent and the border softens with it; the body keeps its
   tier, and the measured contrast goes UP rather than down (8.6:1 — a weaker fill is a darker one
   here). The FOOT may fade, at the provisional 0.5 its siblings wear, because a timestamp the client
   guessed is exactly the part that is not yet a fact.
   The reconciling morph brings the record's own bubble — tick, id and acts — INTO this node, so the
   whole treatment leaves with the same morph that confirms it: no class to remove, nothing to time out.
   Deliberately NOT --wattage-inert anywhere: inert means "still a thing somebody reads or opens" and
   pending means "in flight". The ghost below spends that token, and reaching for it here would make a
   message on its way look like one that has been taken away (K29 — a value never moonlights). */
.msg-bubble-pending {
  background: color-mix(in srgb, var(--b-accent) 10%, var(--surface-overlay));
  border-color: color-mix(in srgb, var(--b-accent) 28%, var(--border-subtle));
}
.msg-bubble-pending .msg-foot { opacity: 0.5; }
/* A BUBBLE BEING REMOVED (TEN-1872). The husk's words are already in the bubble — msgacts.js puts
   them there in the press's own frame, before the round trip — so what this rule adds is the only
   thing left to say: that it is PROVISIONAL. Same vocabulary as the two rules above and the same
   reason for it; `aria-busy` rather than a class of our own, because that is the attribute the kit
   pager's waiting region already uses for "this is being replaced" and it announces itself. The
   morph brings the record's own husk, which carries no aria-busy at all, and the treatment goes
   with it. */
.msg-slot[aria-busy="true"] { opacity: 0.55; }
.msg-slot[aria-busy="true"] .msg-acts { pointer-events: none; }
/* The note that stands in for the husk when the wire never answered: the bubble is put back and
   this says so, in the plate's own stamped words. It is an error, so it wears the error voice the
   rest of this console wears. */
.msg-del-note { margin: var(--b-space-1) 0 0; }

/* The gear glyph fills its button (TEN-1259, Mark's UAT — the TEN-1249 smiley
   lesson at every gear): at text size the ⚙ reads as a bullet point. Covers
   the lists page's rename gear and the conversation/inbox Edit Group gears,
   so the next walk does not find the same defect one surface over. */
.console-btn-gear [aria-hidden], .msg-people-edit [aria-hidden] { font-size: 1.35rem; line-height: 1; }

/* The face as a link (TEN-1249): the dm header's avatar and the roster's
   avatars go to their person's profile. The affordance is an accent ring on
   hover/focus rather than any resting chrome — a border at rest would make
   every face read as a button, including the ones that are not. */
.msg-head-av-link, .msg-people-av-link { display: inline-flex; border-radius: 999px; text-decoration: none; }
.msg-head-av-link:hover, .msg-people-av-link:hover,
.msg-head-av-link:focus-visible, .msg-people-av-link:focus-visible {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 1px;
}

/* THE MESSAGE-ACT BAR (K33, TEN-1751; the menu itself is TEN-1213/ADR-0030 §7).
   The bar rides the bubble's top OUTER corner — on the far side from the
   speaker, so it never covers the first or last word — and is invisible until
   it is wanted. `⋯` was alone here until K33; it now sits at the end of a row
   of the acts a reader reaches for most, and opens the menu behind them. */
.msg-acts { position: absolute; top: -0.4rem; right: -0.4rem; z-index: 20; }
.msg-slot-mine .msg-acts { right: auto; left: -0.4rem; }
/* ONE ROW, ONE REVEAL. The bar is laid out here and revealed as a unit below:
   buttons that faded independently would read as a row assembling itself. */
.msgact-bar { display: flex; align-items: center; gap: 2px; }
.msgact {
  width: 1.5rem; height: 1.5rem; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-raised); color: var(--text-muted);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: 999px;
  font-size: var(--b-text-sm); line-height: 1;
  opacity: 0; transition: opacity 120ms ease;
}
/* Revealed by hover, by keyboard focus, and while the menu is open. The focus
   rung is not decoration: without it the bar is mouse-only, and a keyboard
   reader would have controls they can tab to and never see.

   THE WHOLE BAR IS REVEALED, not the button under the pointer — `:focus-within`
   and the `:has()` arm are what keep the row whole while the reader moves along
   it or opens the menu from its end. Hover owns this surface and nothing else
   rides it: every fact about the message (a receipt, a chip, a tombstone) is
   drawn without a pointer, because a fact that waits for hover is a fact most
   readers never learn.

   Gated on scripting (TEN-1238): these acts are JS-only by design (ADR-0030),
   so without a script the bar must not LOOK available - Mark's JS-off walk
   found a visible ellipsis that did nothing, which breaks the
   never-a-control-that-looks-live-and-isn't rule. display:none on the bar
   (below) is the real gate; these reveal rules stay as-is.

   ONE RULE PER ARM, deliberately. A comma-joined selector list is ALL-OR-NOTHING
   in CSS: one selector the browser cannot parse drops every other selector
   beside it, silently, with the rule still sitting in the file looking correct.
   These arms are not equally old — `:has()` is the newest — so joining them
   would stake the plain hover reveal on the newest thing in the list. */
.msg-slot:hover .msgact { opacity: 1; }
.msgact:focus-visible { opacity: 1; }
.msgact-bar:focus-within .msgact { opacity: 1; }
.msgact[aria-expanded="true"] { opacity: 1; }
.msg-acts:has(.msgact[aria-expanded="true"]) .msgact { opacity: 1; }
@media not (scripting: enabled) {
  .msgact-bar { display: none; }
}
.msgact:hover { color: var(--text-primary); border-color: var(--border-strong); }
/* A coarse pointer has no hover, and a permanently visible row of controls on
   every bubble is clutter on the screen with the least room for it. A single
   tap on the bubble opens the sheet there (TEN-1224), so the bar stays out of
   the way — the touch twin of the bar is the sheet, not a smaller bar. */
@media (pointer: coarse) {
  .msg-slot:hover .msgact { opacity: 0; }
  /* Descendant-qualified deliberately: it has to OUT-WEIGH the hover rule
     above, not merely follow it. A touch that opens the menu also leaves the
     slot in :hover, and at equal specificity the taller selector wins — which
     hid the trigger of the menu the reader had just opened. */
  .msg-slot .msgact[aria-expanded="true"] { opacity: 1; }
  .msg-slot .msg-acts:has(.msgact[aria-expanded="true"]) .msgact { opacity: 1; }
}
/* The conversation's scriptless remove (TEN-1751), the room's `room-msg-del`
   twin. With scripting the menu's confirmed Remove submits this form, so the
   button must not be a second one-tap destructive control on the bubble face;
   with no scripting it is the whole affordance. Same shape as the room's, for
   the room's reason. */
.msg-del { position: absolute; top: -0.4rem; right: -2.1rem; margin: 0; }
.msg-slot-mine .msg-del { right: auto; left: -2.1rem; }
.msg-del-btn {
  width: 1.5rem; height: 1.5rem; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-raised); color: var(--text-muted);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: 999px;
  font-size: var(--b-text-sm); line-height: 1;
}
.msg-del-btn:hover { color: var(--text-primary); border-color: var(--border-strong); }
@media (scripting: enabled) {
  .msg-del { display: none; }
}
.msg-acts-menu {
  position: absolute; top: calc(100% + 0.25rem); right: 0; z-index: 40;
  min-width: 12rem; max-width: min(16rem, calc(100vw - 2rem));
  padding: var(--b-space-1); display: flex; flex-direction: column; gap: 2px;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-md);
  box-shadow: var(--b-shadow-lg, 0 12px 32px rgb(0 0 0 / 45%));
}
.msg-acts-menu[hidden] { display: none; }
/* The two flips the script applies after measuring: above when there is no
   room below, and over to the other side rather than off the screen. */
.msg-acts-menu-flip { top: auto; bottom: calc(100% + 0.25rem); }
.msg-acts-menu-left { right: auto; left: 0; }
.msg-acts-detail {
  margin: 0; padding: var(--b-space-1) var(--b-space-2);
  font-size: var(--b-text-xs); color: var(--text-muted);
  border-bottom: var(--b-border-w) solid var(--border-subtle);
}
.msg-acts-quick { display: flex; flex-wrap: wrap; gap: 2px; padding: 2px 0; }
.msg-acts-emoji {
  width: 2.4rem; height: 2.4rem; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: var(--b-radius-sm);
  font-size: 1.5rem; line-height: 1; color: inherit;
}
.msg-acts-emoji:hover, .msg-acts-emoji:focus-visible { background: var(--surface-raised); }
/* The quick-react glyphs are a CHOOSING surface like the picker grid
   (TEN-1227): big enough to tell 😢 from 😮 at arm's length, and a real
   touch target on a phone. */
@media (pointer: coarse) {
  .msg-acts-emoji { width: 2.75rem; height: 2.75rem; font-size: 1.7rem; }
}
.msg-acts-more { color: var(--text-muted); font-size: var(--b-text-lg); }
.msg-acts-item {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: var(--b-space-1) var(--b-space-2);
  background: none; border: none; border-radius: var(--b-radius-sm);
  color: inherit; font: inherit; font-size: var(--b-text-sm); text-decoration: none;
}
.msg-acts-item:hover, .msg-acts-item:focus-visible { background: var(--surface-raised); }

/* THE MENU IS A BOTTOM SHEET on touch (TEN-1224, Mark's ruling): an anchored
   popover has nowhere to flip on a phone, and a sheet is what native chat
   apps render, so this reads as the status quo. Opened by a SINGLE TAP on
   the bubble (msgacts.js); the flip classes are inert because place() yields
   to the sheet.

   This block must sit BELOW the base .msg-acts-menu rules: it overrides them
   at EQUAL specificity, so the cascade order is the whole mechanism — the
   first cut sat above them inside the trigger's media query and every line
   of it silently lost (Mark's re-walk: a ghost popover, off-screen again). */
@media (pointer: coarse) {
  .msg-acts-menu {
    position: fixed; left: 0; right: 0; top: auto; bottom: 0; z-index: 60;
    min-width: 0; max-width: none;
    border-radius: var(--b-radius-md) var(--b-radius-md) 0 0;
    border-left: none; border-right: none; border-bottom: none;
    padding: var(--b-space-2);
    padding-bottom: max(var(--b-space-2), env(safe-area-inset-bottom));
    box-shadow: 0 -12px 32px rgb(0 0 0 / 45%);
  }
  .msg-acts-menu-flip, .msg-acts-menu-left { top: auto; bottom: 0; }
  /* Sheet items breathe like touch controls, not desktop menu rows. */
  .msg-acts-menu .msg-acts-item { padding: var(--b-space-2); }

  /* Long-press is RESERVED on bubbles (a future multi-select mode), so the
     platform's own answer to a held finger — selection handles, the copy
     callout, sometimes selecting the menu's own text — degrades to quiet
     instead of to noise. Coarse pointers only: a mouse still selects and
     copies message text like any other text. */
  .msg-slot, .msg-acts-menu {
    -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  }
}

/* The timestamp sits bottom-right INSIDE the bubble, after the body — the
   name that used to head it is gone (in a DM one of the two people is you)
   and lives in the tooltip and the actions menu's detail line instead. */
.msg-foot { margin: var(--b-space-1) 0 0; text-align: right; font-size: var(--b-text-xs); }

/* Emoji entry in the composer (TEN-1211, ADR-0030). The wrap is what the
   panels anchor to; both are absolutely positioned so neither one displaces
   the composer when it opens. */
.msg-composer-wrap { position: relative; }
.emoji-open {
  flex: 0 0 auto; align-self: flex-end; width: 2.25rem; height: 2.25rem;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); color: var(--b-text-muted);
  /* The smiley at text size read as a bullet point (TEN-1249, Mark's UAT) —
     the glyph fills the button now; the button's own footprint is unchanged. */
  font-size: 1.5rem; line-height: 1; cursor: pointer;
}
.emoji-open:hover { color: var(--b-text); border-color: var(--border-strong); }
/* The picker is a pure enhancement (ADR-0030): scriptless pages must not
   offer its button either (TEN-1238). */
@media not (scripting: enabled) {
  .emoji-open { display: none; }
}

.emoji-picker {
  position: absolute; right: 0; bottom: calc(100% + var(--b-space-2)); z-index: 30;
  width: min(21rem, calc(100vw - 2rem)); display: flex; flex-direction: column;
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-md); box-shadow: var(--b-shadow-lg, 0 12px 32px rgb(0 0 0 / 45%));
}
.emoji-picker[hidden] { display: none; }
/* ANCHORED AT THE INVOKING ACT (TEN-1827, Mark's UAT on v0.1.243). The rule
   above is the COMPOSER's placement — above the composer, flush to its right
   edge — and it is correct for the composer's own button and for nobody else:
   pressing react on a bubble at the top of the pane opened the chooser in the
   bottom-right corner, nowhere near the thing being decorated.

   emoji.js stamps this attribute when it places the panel against an act
   (position/left/top-or-bottom/max-height, inline, because a viewport rect is a
   runtime fact CSS cannot see — the same division of labour as the TEN-1228
   keyboard pin). What CSS owns is the two things that are NOT arithmetic:

     - THE GRID GIVES UP ITS OWN CEILING. The inline max-height caps the PANEL,
       and a cap the grid ignores is not a cap — the content would simply
       overflow the box and run off the screen, which is the clipping this
       placement exists to prevent. So the grid flexes inside whatever height the
       panel was given, exactly as it does under the coarse branch below.
     - IT PAINTS OVER THE THREAD. Anchored, the panel sits over bubbles rather
       than over the composer, so it takes the coarse branch's z-index rather
       than the composer popover's. Higher specificity than either, so cascade
       order is not the mechanism here (the TEN-1224 lesson read the other way). */
.emoji-picker[data-emoji-anchored] { z-index: 60; }
.emoji-picker[data-emoji-anchored] .emoji-picker-grid { max-height: none; flex: 1 1 auto; min-height: 0; }
.emoji-picker-head { display: flex; gap: var(--b-space-1); padding: var(--b-space-2); border-bottom: var(--b-border-w) solid var(--border-subtle); }
.emoji-picker-search { flex: 1; min-width: 0; }
.emoji-picker-close { background: none; border: none; color: var(--b-text-muted); cursor: pointer; padding: 0 var(--b-space-2); }
.emoji-picker-grid { overflow-y: auto; max-height: 17rem; padding: var(--b-space-1) var(--b-space-2) var(--b-space-2); }
.emoji-section-title {
  position: sticky; top: 0; margin: var(--b-space-2) 0 var(--b-space-1);
  background: var(--surface-overlay); font-size: var(--b-text-xs);
  text-transform: uppercase; letter-spacing: .06em; color: var(--b-text-muted);
}
/* The grid packs as many columns as fit a 2.5rem-minimum cell — the glyph is
   what has to be LEGIBLE (TEN-1227, Mark's UAT: at list-text size the faces
   were indistinguishable), so a narrow panel drops columns, never point size.
   The keyboard walk no longer assumes eight: emoji.js reads the computed
   column count, so arrow-up/down always move one visual row. */
.emoji-section-cells { display: grid; grid-template-columns: repeat(auto-fill, minmax(2.5rem, 1fr)); gap: 2px; }
.emoji-cell {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  background: none; border: none; border-radius: var(--b-radius-sm);
  font-size: 1.6rem; line-height: 1; cursor: pointer; padding: 0;
}
.emoji-cell:hover, .emoji-cell.is-active { background: var(--surface-raised); }
/* A finger needs more than a cursor does — the crop-tool rule, same reasons:
   44px is the touch-target floor, and a phone is read in worse light. */
@media (pointer: coarse) {
  .emoji-section-cells { grid-template-columns: repeat(auto-fill, minmax(2.75rem, 1fr)); }
  .emoji-cell { font-size: 1.75rem; }
}
.emoji-picker-note { margin: 0; padding: var(--b-space-3); text-align: center; color: var(--b-text-muted); font-size: var(--b-text-sm); }
.emoji-picker-note[hidden] { display: none; }

/* KEYBOARD-AWARE PICKER on touch (TEN-1228). Opening the picker focuses its
   search field, which raises the on-screen keyboard — and the browser resizes
   only the VISUAL viewport, so a composer-anchored panel keeps its
   layout-viewport position and sinks behind the keys. On coarse pointers the
   panel goes fixed near the top of the screen; emoji.js pins and sizes it
   against visualViewport (a runtime fact CSS cannot see) on open and on every
   keyboard resize, and the grid flexes inside the capped panel.

   Below the base .emoji-picker rules on purpose: this overrides them at equal
   specificity, so cascade order is the mechanism (the TEN-1224 lesson). */
@media (pointer: coarse) {
  .emoji-picker { position: fixed; left: 0.5rem; right: 0.5rem; top: 0.5rem; bottom: auto; width: auto; z-index: 60; }
  .emoji-picker-grid { max-height: none; flex: 1 1 auto; min-height: 0; }
}

/* The `:shortcode` suggestions sit above the composer, spanning it, so the
   list never covers what is being typed and cannot run off a narrow screen. */
.emoji-ta {
  position: absolute; left: 0; right: 0; bottom: calc(100% + var(--b-space-1)); z-index: 30;
  max-height: 14rem; overflow-y: auto; padding: 2px;
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-md); box-shadow: var(--b-shadow-lg, 0 12px 32px rgb(0 0 0 / 45%));
}
.emoji-ta[hidden] { display: none; }
.emoji-ta-row {
  display: flex; align-items: center; gap: var(--b-space-2); width: 100%;
  padding: var(--b-space-1) var(--b-space-2); background: none; border: none;
  border-radius: var(--b-radius-sm); color: var(--b-text); text-align: left; cursor: pointer;
}
.emoji-ta-row:hover, .emoji-ta-row.is-active { background: var(--surface-raised); }
.emoji-ta-glyph { font-size: var(--b-text-md); line-height: 1; }
.emoji-ta-code { color: var(--b-text-muted); font-size: var(--b-text-xs); }
/* The double check (ADR-CM008): one mark sent, two seen, on your own
   messages only. Colour carries the difference as well as the count, because
   one tick and two ticks are a hard thing to tell apart at this size. */
.msg-tick { margin-left: 0.35rem; letter-spacing: -0.12em; }
.msg-tick-sent { color: var(--text-faint); }
.msg-tick-seen { color: var(--b-accent); }
/* Presence (ADR-CM007): four visually distinct treatments, ordered by
   specificity. Offline is the ABSENCE of the whole element — a rendered
   absence, never a grey dot claiming knowledge we do not have. */
/* ABSENCE RENDERS AS NOTHING (TEN-1054). Hidden by default and revealed only
   by a state class, so "we have no claim" and "they are offline" stop looking
   identical — the first is silence, and only the second would be a statement.
   Before this the bare element showed a faint dot, which asserted knowledge
   nobody had. */
.msg-presence { display: none; align-items: center; gap: 0.3rem; }
.msg-presence.msg-presence-signed-in,
.msg-presence.msg-presence-open,
.msg-presence.msg-presence-watching { display: inline-flex; }

/* The dot rides the avatar's NW corner, LEVEL WITH THE CIRCLE (TEN-1077).
   It used to be offset by its own size in both axes — tangent to the corner
   of the BOX, which for a circular avatar is a corner of empty space, so the
   dot floated away from the thing it belongs to. Its top now sits on the
   circle's topmost edge and it overlaps the corner, which Mark called for
   explicitly ("some overlap is ok").
   Absolute either way, so appearing and disappearing never moves the avatar
   or the name beside it. */
.msg-head-face { position: relative; display: inline-flex; flex: none; overflow: visible; }
.msg-head-face .msg-presence,
.msg-people-face .msg-presence { position: absolute; top: 0; left: 0; }
/* A roster row's face is the same arrangement one size down (TEN-1077). */
.msg-people-face { position: relative; display: inline-flex; flex: none; overflow: visible; }
.msg-people-face .msg-presence { --msg-dot-size: 0.45rem; }
.msg-presence { --msg-dot-size: 0.5rem; }
.msg-presence-dot {
  width: var(--msg-dot-size); height: var(--msg-dot-size); border-radius: 50%;
  background: var(--text-faint); flex: none;
  /* Reads against both the avatar and the page behind it. */
  box-shadow: 0 0 0 2px var(--surface-1, var(--surface-raised));
}
/* GREEN, not the brand accent (TEN-1077, Mark's call). Blue read as a piece
   of branding; green is what every client on earth uses for "available", and
   the three states are its shades — muted for signed-in, half for open, full
   for actively watching. */
.msg-presence-signed-in .msg-presence-dot { background: var(--text-muted); }
.msg-presence-open .msg-presence-dot { background: color-mix(in srgb, var(--b-success, #16a34a) 60%, var(--surface-overlay)); }
.msg-presence-watching .msg-presence-dot {
  background: var(--b-success, #16a34a);
  /* The separating ring stays; the halo sits outside it. */
  box-shadow:
    0 0 0 2px var(--surface-1, var(--surface-raised)),
    0 0 0 5px color-mix(in srgb, var(--b-success, #16a34a) 25%, transparent);
}
/* .msg-detail is GONE (TEN-1213). The long-press card it painted has become
   the first line of the message actions menu — .msg-acts-detail above —
   because two behaviours were bound to one gesture. Removed rather than left
   as a dangling rule: an unused class is a class the next fork wires up. */
.msg-composer { display: flex; gap: var(--b-space-2); align-items: flex-end; }
.msg-composer textarea {
  flex: 1 1 auto; resize: vertical; font: inherit;
  padding: var(--b-space-2); background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}

/* ---- Kit surfaces this cover never skinned (UAT 2026-08-13) -----------
   Purchases, Subscriptions and Calendar are DEFINITION pages: their heading
   comes from core/page-meta and their bodies from the sportscore kit, and
   none of those class names had a rule here — so they rendered in browser
   defaults next to console screens wearing the design kit. Same tokens as
   the module screens; the two page kinds now read as one console. */
.b-page-meta { margin: 0 0 var(--b-space-3); }
.b-page-meta-title {
  font-family: var(--b-font-heading); font-weight: 800; font-size: var(--b-text-xl);
  margin: 0 0 var(--b-space-1); text-transform: uppercase; letter-spacing: 0.03em;
}
.b-page-meta-description { color: var(--text-muted); font-size: var(--b-text-sm); margin: 0; }

/* core/flash (ADR-0037): the engine's page-level outcome, on definition pages
   where no module route was there to write one. Same treatment as .cmc-flash
   — the console's own outcomes — because they ARE the same thing said in two
   render paths, and a confirmation that changes shape depending on which path
   produced it reads as two different systems. A cover claiming bindery-core@1
   styles these three the way it styles .b-page-meta: unstyled, they render as
   body text in the page's own colour, which is the UAT scar .cmc-flash already
   carries above. */
.b-flash {
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
  padding: var(--b-space-3);
  margin: 0 0 var(--b-space-3);
  font-size: var(--b-text-sm);
  line-height: 1.6;
}
.b-flash-ok    { border-left: 3px solid var(--role-presence); }
.b-flash-error { border-left: 3px solid var(--role-danger); }

/* TEN-1419: a leading error icon, applied ONCE for every error-flash family
   this cover knows — .b-flash-error above, plus .cmc-flash-error and
   .flash-error (their own rules are elsewhere in this file; see the pointer
   comments there). Those three are NOT one shared component: they are three
   independently hand-rolled markup shapes (core/flash's engine default, and
   ~30 copy-pasted {{if .flash}}<p class="cmc-flash …"> / <div class="flash
   …"> template lines across modules/cmcauth and modules/sportscore — this
   module can't share a {{define}} across plates, ADR-0032's "own template
   namespace" note). Editing ~30 templates one at a time is exactly the
   per-surface hack ADR-0037 named and this ticket rules out ("not
   per-page"), so the icon lives here instead: one CSS rule, keyed off the
   one thing all three families already share (their "-error" class name),
   with zero Go/template changes — the most cover-driven form this can
   take. Any future surface that reuses one of these three names inherits
   it for free. Decorative only (mask-image content is not exposed to
   assistive tech): the text already carries the meaning, via role="alert"
   (core/flash) or plain visible text (the other two). .*-ok is untouched —
   this ticket scopes to error only. */
.b-flash-error, .cmc-flash-error, .flash-error {
  position: relative;
  padding-left: calc(var(--b-space-3) + 1.4em);
}
.b-flash-error::before, .cmc-flash-error::before, .flash-error::before {
  content: "";
  position: absolute;
  left: var(--b-space-3);
  top: 50%;
  transform: translateY(-50%);
  width: 1em;
  height: 1em;
  background-color: var(--role-danger);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* Deferred bodies: the frame paints first, so the wait needs to look
   deliberate rather than like an empty page. */
.b-defer-loading, .ss-grid-loading {
  display: flex; align-items: center; gap: var(--b-space-2);
  color: var(--text-muted); font-size: var(--b-text-sm); padding: var(--b-space-4) 0;
}

/* Subscription cards (TEN-991): the card grammar the console already uses. */
.ss-subcards { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ss-subcard {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-4);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--b-space-2) var(--b-space-3);
}

/* Row counts sit quiet next to the search box. */
.ss-pager-count { color: var(--text-muted); font-size: var(--b-text-sm); margin-left: auto; }

/* The calendar body (TEN-992) rides the same card surface as everything
   else on these pages. */
.ss-cal {
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-3);
}

/* The header avatar's fallback layer (initials show through if the image
   never loads). */
.avmenu-initials { position: relative; z-index: 0; }

/* My Network tabs (TEN-1003): Connections / Followers / Following, with
   the count each holds. A tab is a link — the list it names is a different
   page of the same surface, not a widget state. */
.net-tabs { display: flex; flex-wrap: wrap; gap: var(--b-space-1); margin: 0 0 var(--b-space-3); border-bottom: var(--b-border-w) solid var(--border-subtle); }
.net-tab {
  display: inline-flex; align-items: center; gap: var(--b-space-2);
  padding: var(--b-space-2) var(--b-space-3); text-decoration: none;
  color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.net-tab:hover { color: var(--text-primary); }
/* The state's own hover arm (TEN-1702 sweep; see `.ktlist-chip-on`). */
.net-tab-on, .net-tab-on:hover { color: var(--text-primary); border-bottom-color: var(--b-accent); font-weight: var(--b-weight-semibold); }
.net-tab-count {
  font-size: var(--b-text-xs); color: var(--text-muted);
  background: var(--surface-overlay); border-radius: var(--b-radius-full); padding: 0 0.45rem;
}
/* The presence label follows the STATE CLASS, never a separate render: all
   three are in the markup and CSS shows the one that matches, so a pushed
   state change (ADR-CM015 §6) cannot leave the words disagreeing with the
   dot. Offline is absence — no state class, no label. */


/* The group's people (TEN-1017) — collapsed by default: the conversation is
   the point, and the membership is a thing you go looking for. */
/* The people sheet (TEN-1067). The <summary> IS the header's name block, so
   it has to keep looking like a name and not like a disclosure widget: the
   marker goes, and the only tell is a caret next to the member count that
   turns when it is open. */
.msg-people { min-width: 0; font-size: var(--b-text-sm); }
.msg-people > summary { cursor: pointer; list-style: none; }
.msg-people > summary::-webkit-details-marker { display: none; }
.msg-people > summary::marker { content: ""; }
.msg-people > summary .acct-value { color: var(--text); }
.msg-people-caret { display: inline-block; margin-left: var(--b-space-1); transition: transform 120ms ease; }
.msg-people[open] .msg-people-caret { transform: rotate(180deg); }
/* A SHEET, not a row: opening the list must not shove the conversation down
   the screen. It hangs under the header, over the history, and scrolls on
   its own when a group is big.

   The closed state is stated OUTRIGHT rather than left to the UA. A closed
   <details> hides its contents, but this panel's containing block is
   .msg-head — an ancestor outside the <details> — and it escapes: measured
   296px tall and on screen with the details shut. Third time in this cover
   that "the browser will hide it" has been wrong (the modal wrap, the
   filtered row, now this), so it is spelled out. */
.msg-people:not([open]) .msg-people-panel { display: none; }
.msg-people-panel {
  position: absolute; z-index: 5; top: calc(100% + var(--b-space-2)); left: 0; right: 0;
  /* The popover chrome (TEN-1249, Mark's UAT: the sheet read as part of the
     pane). Overlay surface + strong stroke — the emoji picker's exact
     language, so every floating panel in the conversation speaks one — and a
     two-layer shadow: the wide soft one says "above the pane", the tight one
     draws the edge where the panel's tone and the pane's meet. */
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-md); padding: var(--b-space-3);
  max-height: 60vh; overflow-y: auto;
  box-shadow: 0 16px 40px rgb(0 0 0 / 0.35), 0 2px 8px rgb(0 0 0 / 0.22);
}
.msg-people-panel .console-inline-form { margin-top: var(--b-space-2); }
.msg-people-list { list-style: none; margin: var(--b-space-2) 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.msg-people-list .console-person { display: flex; align-items: center; gap: var(--b-space-2); }
.msg-people-act { margin-left: auto; }

/* Typing (TEN-1006). Every indicator is rendered and hidden; the socket adds
   msg-typing-on to one of them and removes it when the platform's expiry
   passes. Reserving the line's height stops the history jumping each time
   someone starts and stops — the indicator appearing must not move the
   message you are reading. */
.msg-typing-line {
  margin: 0 0 var(--b-space-1); min-height: 1.2em;
  font-size: var(--b-text-sm); color: var(--text-muted);
}
.msg-typing { display: none; }
.msg-typing-on { display: inline-flex; align-items: center; gap: 0.4rem; }
/* A revealed row that the tally has collapsed into "{n} users". It keeps
   msg-typing-on — and therefore its own expiry claim — and loses only
   its visibility, so each typist still times out on their own clock. */
.msg-typing-on.msg-typing-muted { display: none; }
/* Three dots that flutter (TEN-1055).
   The previous version animated `content` between "" and "…", which does
   nothing — `content` is not an animatable property, so the indicator was
   static and the keyframes were decoration. Three real dots, staggered. */
.msg-typing-dots { display: inline-flex; align-items: flex-end; gap: 0.2em; height: 1em; }
.msg-typing-dots > i {
  width: 0.32em; height: 0.32em; border-radius: 50%; background: currentColor;
  opacity: 0.28;
}
.msg-typing-on .msg-typing-dots > i { animation: msg-typing-flutter 1.1s ease-in-out infinite; }
.msg-typing-on .msg-typing-dots > i:nth-child(2) { animation-delay: 0.18s; }
.msg-typing-on .msg-typing-dots > i:nth-child(3) { animation-delay: 0.36s; }
@keyframes msg-typing-flutter {
  0%, 60%, 100% { opacity: 0.28; transform: translateY(0); }
  30%           { opacity: 1;    transform: translateY(-0.22em); }
}
@media (prefers-reduced-motion: reduce) {
  .msg-typing-on .msg-typing-dots > i { animation: none; opacity: 0.6; }
}
}

/* The conversation list's search (TEN-1023) — the same input the network
   filters use, so the two lists behave and read alike. */
/* The branded 404 (TEN-1125): quiet, centered, and pointing somewhere.
   Shared with the branded 403/5xx (TEN-1387, .errorpage*) — same shape for
   every "you landed somewhere unexpected" surface the book owns. */
.notfound, .errorpage { max-width: 32rem; margin: var(--b-space-8) auto; text-align: center; }
.notfound-exits, .errorpage-exits { display: flex; justify-content: center; gap: var(--b-space-2); flex-wrap: wrap; margin-top: var(--b-space-4); }
.notfound-exits .console-btn, .errorpage-exits .console-btn { text-decoration: none; }

/* The inbox title row wears "+ New" at its right edge (TEN-1120). */
.msg-inbox-head { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2); }
.msg-inbox-head .acct-stub-title { margin: 0; }
.msg-search { display: flex; gap: var(--b-space-2); align-items: center; margin: 0 0 var(--b-space-3); }
.msg-search .ss-grid-q { flex: 1 1 auto; }
.msg-search-clear { color: var(--text-muted); font-size: var(--b-text-sm); white-space: nowrap; }
/* THE CONVERSATIONS BOX CLEARS FROM INSIDE ITSELF (TEN-1893, Mark's UAT-3): its
   clear is the house's in-box × — `.people-qwrap` / `.people-clear`, declared once
   further down for Find people and My Network. The rule above stays for the one
   box that still says "Clear" in words beside it (list-compose), and the in-box ×
   restates the pair's own size and ink so which of two equal-specificity rules
   comes last in this file cannot decide what it looks like. The wrap has to be
   allowed to shrink: the rail is a proportion of the window, and a flex item's
   default floor is its content. */
.msg-search .people-qwrap, .msg-search .people-q { min-width: 0; }
.msg-search .people-clear { font-size: var(--b-text-lg); color: var(--text-muted); }
.msg-search .people-clear:hover { color: var(--text-primary); }

/* ONE ROW, ONE HEIGHT (TEN-1861, Mark's UAT round 2: "ellipsis button — resize
   to match the heights of the dropdown and search box").

   The three controls were three heights, and every one of them was arrived at
   by a different route rather than chosen: the search box from `.ss-grid-q`'s
   space-2 padding (~36px), the dropdown from `.kit-select`'s space-1 padding at
   text-sm (~26px), and the ellipsis from `.btn`'s own `--control-h` (44px).
   Nothing was wrong with any of them on its own, which is exactly how a row
   ends up disagreeing with itself.

   THE TOKEN IS THE FIX, NOT THREE CORRECTIONS. `--net-control-h` is declared on
   the bar and every control in it reads that one number, so the next control
   somebody adds to this row inherits the height instead of introducing a fourth.
   It is 36px — the search box's own resting height — because the box is the
   widest thing here and the one a reader aims at, so the two small controls come
   up to meet it rather than it coming down. The cover's own `--control-h: 44px`
   comment already states the law this keeps ("one height for every field and
   button on a surface: a form whose boxes disagree by 2px reads as broken long
   before anybody can say why"); this row is a filter bar rather than a form and
   takes the quieter rung, once. */
.net-filters { --net-control-h: 36px; display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; margin: 0 0 var(--b-space-3); }
.net-filters .ss-grid-q { flex: 1 1 12rem; max-width: 20rem; }
.net-filters .people-q,
.net-filters .kit-select,
.net-filters .net-filter-btn,
.net-filters .net-lists-more { height: var(--net-control-h); }
/* The two that were shorter had vertical padding doing the sizing; with a height
   declared, that padding would fight it (and `.btn`'s 20px would make an
   ellipsis as wide as a word). Everything is border-box already (the reset at the
   top of this file), so the borders are inside the 36px on all four. */
.net-filters .kit-select { padding-top: 0; padding-bottom: 0; }
.net-filters .net-filter-btn { padding-top: 0; padding-bottom: 0; }
/* The search box wears the people-qwrap/people-clear pair now (TEN-1107) —
   the input's flex sizing moves to the wrap so the x can sit inside it. */
.net-filters .people-qwrap { flex: 1 1 12rem; max-width: 20rem; }
/* Filter-as-you-type (TEN-1107). [hidden] must actually hide, and .net-row
   is a display:flex .console-factor — the same specificity trap as .msg-row
   above; without this the no-script marked page filters nothing. */
.net-row[hidden] { display: none; }
/* listfilter.js bound (.net-live): the marks answer live, so the submit
   button is noise — it exists for the no-script GET round trip only. */
.net-live .net-filter-btn { display: none; }
/* THE WAY TO MY LISTS (TEN-1855, Mark's M1 UAT finding 4) — a ghost, sized to
   the controls beside it and sitting in the row's own rhythm. The ellipsis needs
   no width of its own, so the padding comes off the .btn default.

   THE NEGATIVE MARGIN IS GONE (TEN-1861, Mark's UAT round 2: "equalize the space
   between ellipsis and dropdown to match dropdown↔searchbox spacing").

   It read `margin-left: calc(-1 * var(--b-space-2) + 2px)`, and the argument
   written here for it was that the ellipsis is an adjunct of the dropdown, so a
   gap "would read as a third filter". Mark's eye on the real bar says the
   opposite, and the reason is worth keeping rather than just the correction: the
   pull was tuned against a 44px button beside a 26px select, where closing the
   gap was the only thing making an obviously mismatched pair look related. With
   all three controls at one height (above) the row is already legible as a set,
   and the cancelled gap is just one seam narrower than the other — which is the
   uneven rhythm a reader sees before they can name it. One `gap` on the row, the
   same between every pair. */
.net-lists-more {
  padding: 0 var(--b-space-2); letter-spacing: 0.1em;
}
/* THE MY LISTS DIALOG (TEN-1861) — the editor the ellipsis above opens, in the
   kit's modal family so it rides K39's visual-viewport pin with every other
   dialog in that family rather than needing a rule of its own.

   THE ONLY THING THE COVER SAYS HERE IS THE WIDTH, and it is a cover decision in
   the place kit-modal already puts one (`panel-class`, TEN-1661 item 1): each row
   is a name, a size and an Open control on one line, which is a wider body than
   the family's default 460px two-field form. Everything else — where the box
   sits, how much gutter it keeps, what the keyboard does to it — is the family's
   own block further down this file, untouched. */
.net-lists-panel { width: 520px; }
/* THE GROUP SEAM (TEN-1855). K15's panel already draws an optgroup label as a
   `.ktsel-grp` header, and the label is most of the break Mark asked for; this
   adds the hairline, so "My Lists" reads as a section of the menu rather than a
   shouty option.

   SCOPED TO THIS ONE MOUNT BY THE SELECT'S NAME, which is what `data-ktsel-for`
   carries (ktsel.js draws the panel as a child of <body>, so there is no
   ancestor to scope by). A bare `.ktsel-grp:not(:first-child)` rule would have
   put a rule above every group header in every picker in the book — a kit-wide
   pixel change smuggled in on a lane about a menu. */
.ktsel-panel[data-ktsel-for="kind"] .ktsel-grp {
  border-top: var(--b-border-w) solid var(--border-subtle);
  margin-top: 6px;
}
/* "Waiting to connect" on the Following tab (TEN-1096). Quiet — it is a
   state, not an action, and the row already has the action beside it. */
.net-pending { font-style: italic; color: var(--text-faint); white-space: nowrap; }
.net-row { flex-wrap: wrap; }
.net-since { margin-right: auto; }

/* The room watcher count (TEN-1255, ADR-CM013 §6) — "23 here now".
   Exactly the .msg-presence discipline one surface over: ABSENCE RENDERS AS
   NOTHING. Hidden by default and revealed only by the state class a live push
   adds, so "we have no count" and "the room is empty" never look alike — the
   first is silence and only the second would be a claim. The count also
   decays on its own TTL, so a wire that stops feeding it takes the number
   away rather than leaving yesterday's on the wall. */
.room-watchers { display: none; align-items: baseline; gap: 0.25rem; color: var(--text-faint); font-size: 0.85em; white-space: nowrap; }
.room-watchers.room-watchers-known { display: inline-flex; }
.room-watchers-n { font-variant-numeric: tabular-nums; color: var(--text-muted); }

/* The conversation's live state (TEN-1004). Chat UX rule: say nothing when
   everything works. A quiet dot reassures without decorating; "Paused" only
   appears when the server genuinely isn't watching, and brings its own way
   out rather than asking the user to guess (reload? wait? is it broken?). */
.msg-live { display: flex; align-items: center; gap: var(--b-space-2); margin: var(--b-space-2) 0 0; }
/* The live badge is CLIENT truth (ADR-0021): the plate renders both states
   and the engine's stream observation on <html data-b-stream> picks one, so
   the badge changes the instant the wire does, with no round trip to ask
   the server a question the server cannot answer. Default to STALE — before
   any byte has arrived, "live" would be a claim nothing supports. */
.msg-live-when-live { display: none; }
.msg-live-when-stale { display: inline-flex; align-items: center; gap: var(--b-space-2); }
.msg-live-when-expired { display: none; }
[data-b-stream="live"] .msg-live-when-live { display: inline-flex; align-items: center; gap: var(--b-space-2); }
[data-b-stream="live"] .msg-live-when-stale { display: none; }
/* THE THIRD STATE (TEN-1281). "Not live" used to be one word, Reconnecting,
   and it was two facts: a wire that will come back, and a session whose
   credential is dead and never will. The second wore the first's badge
   indefinitely — a promise the page could not keep — so it gets its own
   branch, and the stale one is hidden under it rather than left to win by
   being the default. */
[data-b-stream="expired"] .msg-live-when-stale { display: none; }
[data-b-stream="expired"] .msg-live-when-live { display: none; }
[data-b-stream="expired"] .msg-live-when-expired { display: inline-flex; align-items: center; gap: var(--b-space-2); }
.msg-live-dot {
  width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--b-success, #16a34a); flex: none;
}
.msg-live-off { background: var(--b-warning, #d97706); }
/* Red, not amber: the dot beside Reconnecting says "wait", and this one says
   "act". Same shape, different promise. */
[data-b-stream="expired"] .msg-live-off { background: var(--b-danger, #dc2626); }
/* A link rather than the ghost button its neighbour uses (TEN-1024 governs
   ACTION controls on this header; signing in is a navigation, and a link is
   what survives middle-click, a screen reader's link list, and a page whose
   scripting is otherwise wedged). */
.msg-live-signin {
  color: var(--text-primary); font-size: var(--b-text-xs); line-height: 1;
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-2); text-decoration: none;
}
.msg-live-signin:hover { background: var(--surface-overlay); }
.msg-live-signin:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.msg-live-act { margin: 0; }
/* A BUTTON, not a text link (TEN-1024: no text-link action controls on this
   header). Ghost, so it reads as secondary to the status it sits beside. */
.msg-live-resume {
  background: transparent; cursor: pointer;
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  color: var(--text-muted); font-size: var(--b-text-xs);
  padding: var(--b-space-1) var(--b-space-2); line-height: 1;
}
.msg-live-resume:hover { color: var(--text-primary); border-color: var(--border-strong); background: var(--surface-overlay); }
.msg-live-resume:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }

/* ── Find people & orgs (TEN-1101) ─────────────────────────────────────── */
/* The header box rides the headmid slot: centered between wordmark and
   chrome, desktop only — under 720px the slot hides and the hamburger's
   "Find people & orgs" entry carries mobile, so the TEN-1069 hit targets in
   .chrome never compete with it for room. */
.app-header .headmid { flex: 1 1 auto; display: flex; justify-content: center; min-width: 0; }
.hdr-people { position: relative; width: min(340px, 100%); }
.hdr-people-q {
  width: 100%; background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-2); font-size: var(--b-text-sm);
}
.hdr-people-q:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
/* ALIGNED ONTO K15 (TEN-1634 train 2). This panel was the widest gap in the
   set: it solved the identical problem — a floating list of faces with an
   `.is-active` keyboard cursor — in a parallel vocabulary and DIFFERENT tokens,
   an overlay ground with a subtle border and no elevation at all, and a row
   hover that was a flat surface swap. It is the same object as the pickers'
   and the kit's, so it is drawn by the same declarations now; what is left here
   is where it sits and the fact that an empty one is not drawn. */
.hdr-people-drop {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 40;
}
.hdr-people-drop:empty { display: none; } /* quiet, never an error flash */
.hdr-people-av {
  /* From the avatar SCALE (TEN-1108) — a surface picks a named size, never
     invents a number (the tokens.css rule); 24px was the last stray. */
  width: var(--b-avatar-sm); height: var(--b-avatar-sm); border-radius: 50%; object-fit: cover; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-raised); color: var(--text-muted); font-size: var(--b-text-xs);
}
/* The dropdown's initials take the accent pair too (TEN-1108). */
.hdr-people-initial { background: var(--b-accent); color: var(--b-accent-fg); }
.hdr-people-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hdr-people-meta { margin-left: auto; flex: none; }
@media (max-width: 720px) { .app-header .headmid { display: none; } }

/* The /people page: one ranked list, rows on the console-person grid. */
.people-form { display: flex; gap: var(--b-space-2); margin-bottom: var(--b-space-3); }
.people-form .people-q { flex: 1 1 auto; }
.people-rows { list-style: none; margin: 0; padding: 0; }
.people-row-wrap + .people-row-wrap { border-top: var(--b-border-w) solid var(--border-subtle); }
.people-row {
  display: flex; align-items: center; gap: var(--b-space-2);
  padding: var(--b-space-2) 0; color: var(--text-primary); text-decoration: none;
}
.people-row:hover { background: var(--surface-raised); } /* tint, never underline (UAT) */
.people-followers { margin-left: auto; color: var(--text-muted); font-size: var(--b-text-sm); flex: none; }
/* `.people-org-glyph` WAS HERE, at --b-radius-sm and on the initials span only.
   It is the org face's one treatment now (`.av-org`, declared with the
   console-person family) — one radius, and the club's PICTURE is squared too. */
.people-more { display: flex; justify-content: center; padding: var(--b-space-3) 0; }
.people-more[hidden] { display: none; } /* the class display beats [hidden]'s UA rule without this (UAT: More was ever-present) */
/* people-qwrap/people-clear serve TWO consumers now: the Find-people forms
   (TEN-1101) and My Network's filter bar (TEN-1107) — the internal clear x
   is one idiom wherever a search box has one. */
.people-qwrap { position: relative; flex: 1 1 auto; display: flex; }
.people-qwrap .people-q { flex: 1 1 auto; padding-right: calc(var(--b-space-4) + var(--b-space-2)); }
.people-clear {
  position: absolute; right: var(--b-space-2); top: 50%; transform: translateY(-50%);
  color: var(--text-muted); text-decoration: none; font-size: var(--b-text-lg); line-height: 1;
}
.people-clear:hover { color: var(--text-primary); }

/* The kit dropdown (UAT: no naked selects). Token-styled, custom chevron;
   first consumer is My Network's filter, the class is for every select. */
.kit-select {
  appearance: none; -webkit-appearance: none;
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) calc(var(--b-space-4) + var(--b-space-1)) var(--b-space-1) var(--b-space-2);
  font: inherit; font-size: var(--b-text-sm);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right var(--b-space-2) center;
}
.kit-select:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.net-find-cta { color: var(--b-accent); }

/* ── The public profile & club pages (TEN-1140/1143/1144) ─────────────────
   The cmc-auth plates ship structure and no colour — the cover decides how
   they look, and IRONFORM is the first to answer. The shape is the one every
   reader already knows: banner, an avatar lifted over its lower edge, the
   actions out on that same line, then identity, stats, bio, links. There is
   no feed under it and there may never be one; the page is a place to point
   at a person, and it should look like one.                                */
.pp { display: flex; flex-direction: column; gap: var(--b-space-3); }

/* BANNER. A wide strip that keeps its proportion as the column narrows,
   bounded so it never eats a phone screen. */
.pp-banner {
  aspect-ratio: 3 / 1; min-height: 110px; max-height: 210px;
  border-radius: var(--b-radius-md); overflow: hidden;
  /* background-COLOR, never the shorthand: the shorthand resets
     background-image, and the pp-banner-d* defaults are background-images. */
  background-color: var(--surface-raised);
}
.pp-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The three built-in defaults, for everyone who has not uploaded one. Drawn
   from the cover's own accent roles rather than fixed colours, so a theme
   swap carries them; deliberately quiet — a default banner is a backdrop,
   not a statement. */
.pp-banner-default { background-color: var(--surface-raised); }
.pp-banner-d0 { background-image: linear-gradient(135deg, var(--blue-a), var(--surface-raised) 62%); }
.pp-banner-d1 { background-image: linear-gradient(135deg, var(--green-a), var(--surface-raised) 62%); }
.pp-banner-d2 { background-image: linear-gradient(135deg, var(--amber-a), var(--surface-raised) 62%); }

/* THE OVERLAP ROW. The avatar rises over the banner; the actions ride the
   same line at the far end. The negative top margin is the whole trick, and
   it is paired with the avatar's own height so the two stay in step. */
.pp-top {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--b-space-3); flex-wrap: wrap;
  margin-top: calc(var(--pp-avatar-size) / -1.7);
  padding: 0 var(--b-space-1);
}
.pp { --pp-avatar-size: 104px; }
.pp-avatar {
  width: var(--pp-avatar-size); height: var(--pp-avatar-size);
  border-radius: var(--b-radius-full); object-fit: cover; flex: none;
  /* The ring is the page behind it, which is what lifts the circle off the
     banner instead of letting it sink into the seam. */
  border: 4px solid var(--page-bg); background: var(--surface-overlay);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--b-text-2xl); font-weight: var(--b-weight-semibold);
  color: var(--text-muted); line-height: 1;
}
/* Clubs are squared, as in the people rows — the same radius `.av-org` carries
   for every row-sized org face (TEN-1869). This one stays keyed to `.pp-org`
   because the profile page's big avatar is drawn by the page's own kind class,
   not by a per-face modifier the composer has to remember to pass. */
.pp-org .pp-avatar { border-radius: var(--b-radius-md); }
.pp-actions { display: flex; gap: var(--b-space-2); flex-wrap: wrap; padding-bottom: var(--b-space-2); }
.pp-actions form { margin: 0; }

/* IDENTITY. Name and handle stack tight — they are one block, not two rows. */
.pp-id { display: flex; flex-direction: column; gap: 2px; padding: 0 var(--b-space-1); }
.pp-name { margin: 0; font-size: var(--b-text-2xl); line-height: var(--b-leading-tight); }
.pp-handle { margin: 0; color: var(--text-muted); font-size: var(--b-text-sm); }
/* The relationship pill — a fact, not a control. Quiet enough to read past,
   present enough to change what the button means. Connected earns the accent
   because it is the state the whole graph is FOR; the others stay muted. */
.pp-pill-connected { color: var(--b-accent); border-color: var(--b-accent); font-style: normal; }
.pp-pill {
  margin-left: var(--b-space-2); padding: 1px var(--b-space-2);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-full); font-size: var(--b-text-xs); font-style: italic;
}

/* STATS. The count leads, the label follows in muted type — the shape every
   reader parses without reading. */
.pp-stats {
  list-style: none; margin: 0; padding: 0 var(--b-space-1);
  display: flex; flex-wrap: wrap; gap: var(--b-space-4);
}
.pp-stat { color: var(--text-muted); font-size: var(--b-text-sm); }
.pp-stat strong { color: var(--text-primary); font-weight: var(--b-weight-semibold); }

.pp-bio {
  margin: 0; padding: 0 var(--b-space-1);
  max-width: 60ch; line-height: var(--b-leading-normal); white-space: pre-wrap;
}

/* LINKS as chips: a profile's links are a short list of destinations, and a
   bulleted stack of raw URLs reads like a footnote. */
.pp-links {
  list-style: none; margin: 0; padding: 0 var(--b-space-1);
  display: flex; flex-wrap: wrap; gap: var(--b-space-2);
}
.pp-links .pp-link {
  display: inline-block; max-width: 30ch; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
  padding: var(--b-space-1) var(--b-space-2);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-full); font-size: var(--b-text-sm);
}
.pp-links .pp-link:hover { border-color: var(--border-strong); }
.pp-link { color: var(--b-accent); }

/* ON THE TEAM AT (TEN-1889): the clubs that list this person. The rows are the
   org family's row-rung identity block (`.console-person` + `.av-org`, as on
   My Clubs and Coaches) — this only stacks them and quiets the role line. */
.pp-teams { display: flex; flex-direction: column; gap: var(--b-space-2); padding: 0 var(--b-space-1); }
.pp-teams .seclbl { margin: 0; }
.pp-teams-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--b-space-2);
}
/* The club's name in the page's own voice, not the console's mono value face —
   the `.lists-name` precedent for a name that is a link, not a datum. */
.pp-teams .acct-value { font-family: var(--b-font-sans, inherit); font-weight: var(--b-weight-semibold); }
.pp-team-role { color: var(--text-muted); font-size: var(--b-text-sm); }

/* The states that are not a profile: a miss, or a person at a club address.
   Quiet and centred, so a wrong address does not look like a broken page. */
.pp-blank { padding: var(--b-space-6) var(--b-space-2); text-align: center; }
.pp-empty { color: var(--text-muted); margin: 0; }

@media (max-width: 560px) {
  .pp { --pp-avatar-size: 76px; }
  .pp-name { font-size: var(--b-text-xl); }
  /* The actions drop below the avatar rather than squeezing beside it. */
  .pp-actions { width: 100%; padding-bottom: 0; }
}

/* The banner preview on the console profile page (TEN-1141): the same
   proportion the public page draws, at console scale, so what you see when
   you upload is what a visitor gets. Shares the pp-banner-d* defaults. */
.console-banner {
  aspect-ratio: 3 / 1; max-height: 140px; border-radius: var(--b-radius-md);
  overflow: hidden; margin-bottom: var(--b-space-3);
  background-color: var(--surface-raised); /* not the shorthand — see .pp-banner */
}
.console-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* THE PICTURE IS THE CONTROL (TEN-1160). The picture itself opens the editor
   and wears a pencil; there is no Edit button beside it any more. A <button>
   rather than a click handler on the image, so it is reachable by keyboard and
   announces itself. */
.pic-pane {
  position: relative; display: block; padding: 0; border: 0; background: none;
  cursor: pointer; border-radius: var(--b-radius-md); line-height: 0;
}
.pic-pane:focus-visible { outline: 2px solid var(--b-accent); outline-offset: 3px; }
.pic-pane-round { border-radius: var(--b-radius-full); }
.pic-pane-round img { border-radius: var(--b-radius-full); display: block; }
.pic-pane-wide, .pic-frame-wide {
  width: 100%; aspect-ratio: 3 / 1; max-height: 140px; overflow: hidden;
  background-color: var(--surface-raised); margin-bottom: var(--b-space-3);
}
.pic-pane-wide img, .pic-frame-wide img, .pic-pane-fill {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* The pencil sits ON the picture, bottom right, and lifts on hover with the
   pane so the whole tile reads as one control rather than a badge stuck to a
   photo. */
.pic-pencil {
  position: absolute; right: 0; bottom: 0;
  width: 26px; height: 26px; border-radius: var(--b-radius-full);
  display: grid; place-items: center; font-size: 13px; line-height: 1;
  background-color: var(--b-accent); color: var(--b-accent-fg);
  /* `--surface-page`, for the masthead disc's reason (TEN-1700's sweep):
     `--surface-base` is not a token this book defines anywhere. */
  box-shadow: 0 0 0 2px var(--surface-page);
  transition: filter 120ms ease;
}
.pic-pane:hover .pic-pencil, .pic-pane:focus-visible .pic-pencil { filter: brightness(1.08); }

/* ── The picture editor is the OTHER branch of the opener rule
   (TEN-1331, ADR-0036) ─────────────────────────────────────────────────────
   Ask the one question of the modal and it answers no twice over: the server
   renders a single `<input type="file" hidden>` into it and nothing else. The
   frame, the camera, the zoom, Cancel and SAVE are all built by crop.js at
   runtime, so without scripting this door opens onto an empty room with no way
   to submit — crop.js IS the modal, which is what ADR-0036 predicted.

   So the opener and its wrap are simply not there without scripting, and a
   plain form takes their place. The act SURVIVES even though the editor does
   not: normalizeAvatar and normalizeBanner do the authoritative crop and
   downscale on the server, and the banner's comment says in as many words that
   its crop is "here for the plain one". The picture itself stays visible either
   way — the opener is what has to go, not somebody's face.

   Default-off for the enhanced control and default-ON for the plain form, the
   same way round as .room-add-open: an engine that does not understand the
   query gets the form that works rather than a button that cannot. */
.pic-open { display: none; }
.pic-plain { display: block; }
.pic-frame-round { display: block; line-height: 0; }
.pic-frame-round img { border-radius: var(--b-radius-full); display: block; }
.pic-plain-form {
  display: flex; gap: var(--b-space-3); align-items: flex-end; flex-wrap: wrap;
  margin-top: var(--b-space-3);
}
.pic-plain-form input[type="file"] { max-width: 100%; }
@media (scripting: enabled) {
  .pic-open { display: block; }
  .pic-plain { display: none; }
}
/* Must out-weigh .ss-grid-modal-wrap's display:flex — it does by coming later
   at equal specificity, the same trick .room-add-modal turns. Without it the
   scriptless in-flow render (ADR-0036's corollary) would paint the empty
   dialog as a section, which is the dead modal in a different position. */
@media not (scripting: enabled) {
  .pic-modal-wrap { display: none; }
}
/* ── The SECOND axis: scripting on, but the editor cannot run (TEN-1360) ─────
   `scripting: enabled` is a PROXY for "crop.js will work", and it is wrong
   whenever a browser runs scripts but lacks canvas.toBlob or DataTransfer.
   There build() was skipped while the opener above stayed rendered, so the
   door opened onto the server's lone hidden file input — the same empty room
   the scriptless branch was written to prevent, reached the other way.

   No media query can ask this: the capability is a runtime JS fact, so there
   is nothing for CSS to test. crop.js is the only thing that knows, and
   `crop-unsupported` on <html> is it saying so. The rules are the scriptless
   pair again, and deliberately identical — one degraded render, two ways in,
   rather than a third state to keep in step.

   Specificity, not order, is what makes these win: a class ancestor (0,2,0)
   out-weighs the bare `.pic-open` inside the media query (0,1,0), because a
   media query adds none of its own. */
.crop-unsupported .pic-open { display: none; }
.crop-unsupported .pic-plain { display: block; }
/* Belt and braces, mirroring the scriptless rule above. The wrap is `hidden`
   in the template and nothing can now clear it — the opener is gone. This is
   here so that stays true if something later un-hides it (a `?modal=`, a stray
   handler): an empty dialog must not be paintable on this path either. */
.crop-unsupported .pic-modal-wrap { display: none; }

/* The editor's own controls, bottom right INSIDE the frame — the same corner
   the pencil used, so the eye goes to one place for "do something to this
   picture". */
.crop-tools {
  position: absolute; right: var(--b-space-2); bottom: var(--b-space-2);
  display: flex; gap: var(--b-space-2); z-index: 2;
}
/* Big enough to READ, not just to hit (UAT 2026-08-22: at 34px with a 15px
   glyph they "look like rectangles"). The mark is what has to be legible, so
   the icon is sized directly rather than left to inherit a font size. */
.crop-tool {
  width: 40px; height: 40px; padding: 0; border: 0; cursor: pointer;
  border-radius: var(--b-radius-md); display: grid; place-items: center;
  background-color: rgba(9, 9, 13, 0.72); color: #fff;
  backdrop-filter: blur(2px);
}
.crop-ico { width: 22px; height: 22px; display: block; }
/* A finger needs more than a cursor does, and a phone screen is held closer
   but read in worse light — bigger on both counts. */
@media (pointer: coarse) {
  .crop-tool { width: 46px; height: 46px; }
  .crop-ico { width: 26px; height: 26px; }
}
.crop-tool:hover { background-color: rgba(9, 9, 13, 0.88); }
.crop-tool:focus-visible { outline: 2px solid var(--b-accent); outline-offset: 2px; }
/* Pressed-in, because removal is a PENDING state you can see and undo, not an
   act that already happened.
   AND IT KEEPS ITS GROUND UNDER THE POINTER (TEN-1702 sweep; the ruling is at
   `.ktlist-chip-on`). This one was a REAL break rather than a latent one:
   `.crop-tool:hover` repaints the ground to the dark plate wash while
   `.crop-tool-on` still supplies `--b-accent-fg` as the ink, so pointing at the
   pressed tool left accent-foreground ink on a near-black plate — the pressed
   state simply vanished while the pointer was on the only control that could
   un-press it. */
.crop-tool-on, .crop-tool-on:hover { background-color: var(--b-accent); color: var(--b-accent-fg); }

/* What you would be left with, previewed in place of the picture. */
.crop-empty { position: absolute; inset: 0; }
.crop-empty-disc {
  display: grid; place-items: center;
  background-color: #4D8FFF; color: #09090D;
  font-family: var(--b-font-display, sans-serif); font-weight: 600;
  font-size: clamp(28px, 22cqmin, 72px);
}

/* THE hidden ATTRIBUTE HAS TO WIN (UAT 2026-08-22: "it ignores my existing
   avatar and shows the initials disc").
   [hidden] hides things by way of `display: none` in the USER-AGENT stylesheet,
   which any author `display` rule outranks. Both of these carry one — the
   preview was `display: block` and the picture still is — so the disc sat on
   top of a perfectly good avatar forever, and removal would not have hidden the
   picture either.
   The selectors are shaped to WIN, which takes more than adding [hidden]:
   `.crop-img[hidden]` ties `.crop-view .crop-img` on specificity and loses on
   order, because that rule lives further down the file. Matching the descendant
   too breaks the tie without depending on where either block sits. */
.crop-empty[hidden], .crop-view .crop-img[hidden] { display: none; }

/* Cancel and Save on one line, Save last and primary — the rightmost thing is
   the thing that commits. */
.pic-acts {
  display: flex; justify-content: flex-end; gap: var(--b-space-2);
  margin-top: var(--b-space-3);
}
.pic-modal .crop-stage { max-width: none; }
.pic-modal .crop-view { container-type: size; }

/* THE CROP STAGE (TEN-1158). Appears under a file input once a picture is
   chosen; the frame is the crop, and what it shows is exactly what gets
   stored. Aspect ratio is set inline by the script (1/1 for an avatar, 3/1
   for a banner) because one stage serves both. */
.crop-stage { margin: var(--b-space-3) 0; max-width: 420px; }
/* The confirm-it's-you dialog (TEN-1235). It borrows the picture editor's modal
   chrome on purpose: this console should have ONE dialog vocabulary, not a
   second one invented for security. */
.stepup-modal .ss-grid-modal { max-width: 26rem; }
.stepup-modal input[type="password"],
.stepup-modal input[type="text"] { width: 100%; margin: var(--b-space-1) 0 var(--b-space-3); }
/* The named action carries the weight — it is the sentence that tells somebody
   they are about to do the thing they meant to do. */
.stepup-what { margin: 0 0 var(--b-space-2); font-weight: 600; }
.stepup-passkey { margin-bottom: var(--b-space-3); }
.stepup-or { margin: var(--b-space-2) 0 0; opacity: .8; }
/* The keyboard problem (TEN-1251). This dialog raises a soft keyboard the
   moment it opens, which shrinks the VISUAL viewport — but a position:fixed
   wrap is laid out against the LAYOUT viewport, which does not move. The top of
   the panel, where the passkey offer is, ended up above the visible area, so
   the first thing somebody saw was the password box and nothing above it.

   dvh tracks the visual viewport, so the panel stays inside what is actually on
   screen; the vh value is the fallback for browsers without it. The top inset
   also gets out of the way on short screens — 8vh of air is generous on a
   desktop and most of a phone. */
.stepup-modal { padding-top: var(--b-space-3); align-items: center; }
.stepup-modal .ss-grid-modal { max-height: 85vh; max-height: 85dvh; }
@media (max-height: 700px) {
  .stepup-modal { padding-top: var(--b-space-2); }
}
.stepup-modal.stepup-busy .ss-grid-modal { cursor: progress; }
/* A refusal belongs in the dialog the person is looking at, not at the top of
   a document they have scrolled away from. */
.stepup-error { margin: 0 0 var(--b-space-2); }
.stepup-modal.stepup-busy input,
.stepup-modal.stepup-busy [data-passkey-use] { pointer-events: none; opacity: .6; }
/* The Save click is acknowledged on the frame it happens, before any encoding
   or upload begins (sop/ux/responsiveness.md). The veil is what makes the wait
   legible: the picture dims, the controls stop responding to the pointer, and
   a spinner says work is under way. */
.crop-stage.crop-busy .crop-view,
.crop-stage.crop-busy .crop-zoom { opacity: .55; pointer-events: none; }
.crop-stage.crop-busy .crop-view { position: relative; }
.crop-stage.crop-busy .crop-view::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--b-fg) 25%, transparent);
  border-top-color: var(--b-accent);
  animation: crop-spin .7s linear infinite;
}
@keyframes crop-spin { to { transform: rotate(360deg); } }
/* Honour a reduced-motion preference: the veil still says "busy", it just does
   not spin. */
@media (prefers-reduced-motion: reduce) {
  .crop-stage.crop-busy .crop-view::after { animation: none; }
}
.crop-view {
  position: relative; overflow: hidden; width: 100%;
  border-radius: var(--b-radius-md);
  background-color: var(--surface-raised);
  cursor: grab; touch-action: none; /* the script owns the gesture */
  outline-offset: 2px;
}
.crop-view:active { cursor: grabbing; }
.crop-view:focus-visible { outline: 2px solid var(--b-accent); }
/* Positioned by transform, sized in px — both set by the script. max-width
   must NOT apply here: the image is deliberately larger than its frame. */
.crop-view .crop-img {
  position: absolute; top: 0; left: 0; max-width: none;
  transform-origin: 0 0; user-select: none; -webkit-user-drag: none;
  display: block;
}
.crop-zoom { display: flex; align-items: center; gap: var(--b-space-2); margin-top: var(--b-space-2); }
.crop-zoom input[type="range"] { flex: 1; accent-color: var(--b-accent); }
.crop-hint { margin-top: var(--b-space-1); }

/* The bindery-core button vocabulary: .b-btn, which module built-ins
   use because they cannot know a cover's own class names. Same look as the
   console's buttons; this is the cover answering the core idiom rather than
   a second idiom appearing on one page. */
.b-btn {
  display: inline-block; background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-1) var(--b-space-3); cursor: pointer;
  font: inherit; font-size: var(--b-text-sm); text-decoration: none; line-height: 1.7;
  /* Button face per the button card (Mark 2026-09-07): condensed uppercase. */
  font-family: var(--b-font-heading); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
}
.b-btn:hover { background: var(--surface-raised); border-color: var(--border-strong); }
.b-btn:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.b-btn-primary {
  background: var(--b-accent); color: var(--b-accent-fg);
  border-color: transparent; font-weight: var(--b-weight-semibold);
}
/* Primary rings in its FOREGROUND — the base rule outlines in the accent,
   which is this button's own background (the TEN-1056 lesson). */
.b-btn-primary:focus-visible { outline: var(--b-ring-w) solid var(--b-accent-fg); outline-offset: 2px; }
/* And primary needs its OWN hover, for the reason .console-btn-primary does:
   .b-btn:hover swaps the background to a surface colour while the label keeps
   --b-accent-fg (white), so the text vanished on hover. The console solved
   this already; this is the same answer, not a second one. */
.b-btn-primary:hover {
  background: var(--b-accent); border-color: transparent;
  filter: brightness(1.08);
}
.b-btn-primary:active {
  background: var(--b-accent); border-color: transparent;
  filter: brightness(0.94);
}

/* ── My Lists and "Send to a list" (TEN-1253, ADR-CM014) ───────────────────

   The surface reuses the console's existing grammar wherever it can — rows are
   .console-factor, modals are .ss-grid-modal, the composer is .msg-composer and
   its Send is .msg-send, so the pending acknowledgment above applies with no new
   rule. What is left here is what is genuinely new: the entry point, the size
   warning, and the outcome report. */

/* The way in, on My Network. Reads as a destination rather than an action —
   it is a place, and the sentence beside it says what the place is for. */
.net-lists-cta { margin: 0 0 var(--b-space-3); }
.net-lists-cta a { color: var(--b-accent); font-weight: var(--b-weight-semibold); text-decoration: none; }
.net-lists-cta a:hover { text-decoration: underline; }

.lists-head { display: flex; justify-content: flex-end; margin-bottom: var(--b-space-2); }
.lists-row { gap: var(--b-space-2); }
.lists-name { flex: 1; min-width: 0; }
.lists-name .acct-value { font-family: var(--b-font-sans, inherit); font-weight: var(--b-weight-semibold); }
.lists-name a { color: inherit; text-decoration: none; }
.lists-name a:hover { text-decoration: underline; }

/* "Too big for one send". A WARNING tone, not an error one: the list is
   perfectly legal to own — it is only larger than a single send may carry, and
   the copy points at the tool that fits (ADR-CM014 §4). Colouring it as a
   failure would say the person did something wrong. */
.lists-over-cap {
  color: var(--b-warning, #d97706); white-space: nowrap;
  border: var(--b-border-w) solid color-mix(in srgb, var(--b-warning, #d97706) 45%, transparent);
  border-radius: var(--b-radius-sm); padding: 0 var(--b-space-2); font-size: var(--b-text-xs);
}

.lists-compose-head { display: flex; align-items: center; gap: var(--b-space-2); margin-bottom: var(--b-space-3); }
.lists-compose-head .msg-head-id { flex: 1; min-width: 0; }
.lists-compose-acts { display: flex; gap: var(--b-space-2); }
.lists-roster { margin-bottom: var(--b-space-3); }

/* The cap warning above the composer inherits .msg-gate — the same left-rule
   treatment the conversation's "you aren't connected any more" line wears,
   because it is the same kind of statement: a thing the platform will refuse,
   said before the press rather than after it. */
.lists-cap-warning { margin-top: var(--b-space-2); }

/* The outcome report. Deliberately QUIET and never success-toned: "sent to 43
   of 45" is a report, not a celebration, and two of those people did not get
   the message. A green banner over a partial result reads as a claim that
   everything worked. */
.lists-report {
  margin: 0 0 var(--b-space-3); padding: var(--b-space-3);
  border: var(--b-border-w) solid var(--border-strong); border-left-width: 3px;
  border-radius: var(--b-radius-sm); background: var(--surface-overlay);
}
.lists-report-line { margin: 0; font-weight: var(--b-weight-semibold); }
.lists-report .console-micro { margin-top: var(--b-space-1); }

/* The cherry-pick disclosure. JS-ONLY: without scripting the token picker never
   binds, and a bare <select multiple> pre-selected with everybody is a worse
   control than none — one stray click silently drops people from a send. So the
   whole affordance is hidden unless scripting is on, and the form then sends to
   the list the page's own heading names. */
.lists-pick { display: none; }
@media (scripting: enabled) {
  .lists-pick { display: block; margin-bottom: var(--b-space-2); }
}
.lists-pick > summary { cursor: pointer; }
.lists-pick > summary:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.lists-pick .tokpick { margin-top: var(--b-space-2); }

/* The composer here is a block, not the conversation's floor-anchored row: this
   page scrolls as an ordinary document, and the textarea wants the width. */
.lists-screen .msg-composer { flex-wrap: wrap; }
.lists-screen .msg-composer textarea { flex: 1 1 100%; }

/* ── The list page at scale (TEN-1261, Mark's design) ──────────────────────

   The page is one ever-scrolling roster with a filter over it and a single Send
   stuck to the floor; composing happens in a modal. Everything below is either
   the roster's own furniture or the scripting gate that keeps the scriptless
   page honest.

   THE SCRIPTING GATE, stated once because five rules depend on it: the filter,
   select mode, the send bar and the compose modal are all script. A control that
   looks live and isn't is the TEN-1238 defect, so each of them is display:none
   by default and turned on inside `@media (scripting: enabled)` — and the plain
   form that stands in for them is the mirror image. Default-off, not
   default-on: an engine that does not understand the query gets the fallback,
   which works. */
.lists-roster-screen { padding-bottom: var(--b-space-8); }
.lists-roster-head { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2); }
.lists-roster-head .acct-stub-title { margin: 0; }

.lists-filter, .lists-select-btn { display: none; }
.lists-sendbar { display: none; }
@media (scripting: enabled) {
  .lists-filter { display: flex; margin: var(--b-space-2) 0 var(--b-space-3); }
  .lists-select-btn { display: inline-flex; }
  /* Always in reach — at row four and at row three hundred alike. Sticky rather
     than fixed so it belongs to this screen and cannot outlive it, and it sits
     inside .lists-screen so it stays put past the roster, past the danger card,
     to the foot of the page. The tinted band is not decoration: 300 rows
     scrolling under a transparent button is unreadable. */
  .lists-sendbar {
    position: sticky; bottom: 0; z-index: 20;
    display: flex; align-items: center; justify-content: flex-end; gap: var(--b-space-3);
    padding: var(--b-space-3) 0; margin-bottom: var(--b-space-3);
    background: var(--surface-page); border-top: var(--b-border-w) solid var(--border-subtle);
  }
  .lists-noscript { display: none; }
}
.lists-sendbar-note { margin: 0; margin-right: auto; }
.lists-send-open { min-width: 10rem; justify-content: center; }

/* The rows the filter hides. [hidden] must actually hide: .msg-people-list
   .console-person is display:flex, and an author display beats the UA rule for
   [hidden] — the same specificity trap .msg-row and .net-row each hit once. */
.lists-member[hidden] { display: none; }
.lists-none { margin-top: var(--b-space-2); }

/* Select mode is ONE class on the screen, not an attribute written into every
   row: growing a checkbox into three hundred rows on entry is three hundred
   writes and a full reflow for what a class change already does. */
.lists-tick, .lists-all { display: none; }
.lists-selecting .lists-tick { display: inline-flex; align-items: center; flex: none; }
.lists-selecting .lists-all {
  display: flex; align-items: center; gap: var(--b-space-2);
  margin-bottom: var(--b-space-2); cursor: pointer;
}
.lists-tick input, .lists-all input { width: 1.1rem; height: 1.1rem; }
/* Removing somebody is not what select mode is for, and a Remove button beside
   every checkbox is a mis-tap waiting to happen on a phone. */
.lists-selecting .msg-people-act { display: none; }

/* The compose modal (TEN-1261). Wider than the console's confirm dialogs
   because it holds a composer and a picklist rather than one field, and it may
   be showing an outcome report at the same time. */
.lists-send-modal .ss-grid-modal { width: min(34rem, calc(100vw - 2 * var(--b-space-4))); }
/* Scriptless it is not offered at all — the plain form is. Must out-weigh
   .ss-grid-modal-wrap's display:flex, which it does by coming later at equal
   specificity. */
@media not (scripting: enabled) {
  .lists-send-modal { display: none; }
}
/* "Sending to N of M on this list" — the sentence that answers "did I get
   everyone?" before a word is written. Quiet: it is a fact about the send, not
   a warning about it. */
.lists-send-count { margin: 0 0 var(--b-space-3); color: var(--text-muted); font-size: var(--b-text-sm); }
.lists-send-count strong { color: var(--text-primary); font-variant-numeric: tabular-nums; }
.lists-send-modal .lists-report, .lists-send-modal .lists-cap-warning { margin-bottom: var(--b-space-3); }
/* ── Rooms (TEN-1038, ADR-CM011) ─────────────────────────────────────────────

   A room borrows the conversation's chrome wholesale — .msg-screen, .msg-head,
   .msg-pane, .msg-people, the composer — because it IS that surface with
   different governance, and giving it a second visual grammar would say the two
   are unrelated. What follows is only the difference.

   The one rule worth stating up front, because two blocks below serve it: the
   ATTENTION control and the LEAVE control must never read as neighbours. Access
   is not attention, and the most common mistake available on this screen is
   somebody leaving a room when what they wanted was to turn it down. */

/* The list row. Wider than a conversation row because it carries chips, and a
   retired one recedes rather than being struck through — it is read-only, not
   dead, and the history in it is the reason somebody opens it. */
/* Rows need to be tellable APART. .console-factor is a bare flex row, which is
   right for a settings list of two or three items and reads as one undivided
   block once there are several rooms with chips on them. */
.room-row {
  display: grid; grid-template-columns: 1fr auto;
  align-items: center; gap: var(--b-space-1) var(--b-space-2);
  padding: var(--b-space-2) 0;
  border-bottom: var(--b-border-w) solid var(--border-subtle);
  margin-bottom: 0;
}
.room-row:last-child { border-bottom: 0; }
.room-row-id { min-width: 0; }
/* The way in holds the right edge of the first line, so the primary action is in
   the same place on every row whatever chips the room is wearing. */
.room-row-open { grid-column: 2; justify-self: end; }
/* The chips take their own line, spanning. The console column is narrow by
   design, so a title, two chips and a button never fit on one line — loose on
   the row they wrapped a chip at a time and pushed the button around, which
   turned three rooms into one undivided block. */
.room-row-chips {
  grid-column: 1 / -1; display: flex; flex-wrap: wrap;
  align-items: center; gap: var(--b-space-1);
}
.room-row-chips:empty { display: none; }
.room-row-locked .acct-value > a { color: var(--text-muted); }

/* Unread: a chip, never a shout. A room of 400 people would otherwise turn the
   list into a scoreboard, and the number is a nudge rather than a debt. */
.room-unread {
  display: inline-block; min-width: 1.25rem; padding: 0 0.35rem; margin-left: var(--b-space-1);
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold); line-height: 1.4;
  text-align: center; border-radius: 999px;
  background: var(--b-accent); color: var(--b-accent-fg, var(--ink-on-accent));
}

/* The row's state chips — reading-only, retired, the attention rung, hidden.
   All one quiet register on purpose: each is a FACT about the row rather than a
   warning, and colouring them differently would rank states nobody ranked. */
.room-chip {
  padding: 0.1rem 0.45rem; border-radius: var(--b-radius-sm);
  border: var(--b-border-w) solid var(--border-subtle); background: var(--surface-overlay);
  white-space: nowrap;
}
/* Retired takes the warning rule the composer's gate wears, and for the same
   reason: it is the platform's "you cannot write here", said early. */
.room-chip-locked { border-left: 3px solid var(--b-warning, #d97706); }
/* Hidden is dimmer still. It is not a warning — plenty of people want it — and
   an alarming treatment would read as something having gone wrong. */
.room-chip-hidden { color: var(--text-muted); font-style: italic; }

/* The formula picker. Radio buttons with their sentences, stacked, because the
   blurbs have to be readable WHILE choosing — a <select> shows exactly one at a
   time, which turns a choice between five postures into a memory test. */
.room-formulas { border: 0; padding: 0; margin: var(--b-space-3) 0 0; }
.room-formulas > legend { padding: 0; margin-bottom: var(--b-space-2); }
.room-formula {
  display: flex; gap: var(--b-space-2); align-items: flex-start;
  padding: var(--b-space-2); border-radius: var(--b-radius-sm); cursor: pointer;
}
.room-formula:hover { background: var(--surface-overlay); }
.room-formula input { width: auto; margin-top: 0.25rem; flex: none; }
.room-formula-body { display: flex; flex-direction: column; gap: 0.15rem; }
.room-formula:focus-within { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }

/* The one-time hidden-member reminder (TEN-1036 obligation 1). It sits above
   the history rather than at the composer because it is not a fact about
   writing — it is a fact about BEING HERE, and somebody needs it before they
   read a single post. Informational tone: being hidden is a state, often a
   chosen one, and an error-red banner would tell somebody something is broken. */
.room-notice {
  margin: 0 0 var(--b-space-3); padding: var(--b-space-2) var(--b-space-3);
  border-left: 3px solid var(--b-accent);
  background: var(--surface-overlay); color: var(--text-primary);
}

/* A retired room, said above its own history (TEN-1037 obligation 4). Same
   warning rule as the composer's gate — one treatment for "the platform will
   not accept writing here", wherever that is said. */
.room-lifecycle {
  margin: 0 0 var(--b-space-3); padding: var(--b-space-2) var(--b-space-3);
  font-size: var(--b-text-sm); color: var(--text-muted);
  border-left: 3px solid var(--b-warning, #d97706); background: var(--surface-overlay);
}

/* The roster. Scrolls at a fixed height because a room's roster is not a group
   thread's — it can be two thousand rows, and a sheet that grew with it would
   put the acts row and the attention control an unreachable distance away. */
/* NO max-height of its own. The sheet around it (.msg-people-panel) already
   scrolls at 60vh, and a second scroller nested inside the first is two
   scrollbars for one list — which on a phone means the outer one is the only
   one a thumb can find. The acts row leads the panel, so nothing an owner needs
   can scroll out of reach however long the roster is. */
/* A roster row carries a face, a name, a rank, and — for a governor — four
   controls. That does not fit the console's column on one line, and squeezing
   it turns every name into two or three wrapped words. So the row is a grid:
   face and identity on the top line, the governance controls beneath them,
   spanning. The name gets to be a name. */
/* ── Add people: the OTHER branch of the opener rule (TEN-1266, ADR-0036) ───
   Every other opener in this panel is a link whose href re-renders the page
   with its modal open, because an ordinary form is what sits behind it. This
   one is not: the modal holds peoplepick, whose <select> is EMPTY until typing
   fetches rows from the server, so opening it without scripting would offer a
   required control with nothing in it — a door with nothing behind it.

   So the opener and its modal are simply not there without scripting, and the
   line takes their place. Default-off for the control and default-ON for the
   line, deliberately: an engine that does not understand the query gets the
   honest sentence rather than a button that cannot work. */
.room-add-open { display: none; }
.room-add-noscript { display: block; margin-top: var(--b-space-2); }
@media (scripting: enabled) {
  .room-add-open { display: inline-flex; }
  .room-add-noscript { display: none; }
}
/* Must out-weigh .ss-grid-modal-wrap's display:flex — it does by coming later
   at equal specificity, the same trick .lists-send-modal turns. */
@media not (scripting: enabled) {
  .room-add-modal { display: none; }
}

/* FOUR columns since TEN-1411, not three: face, identity, the hidden chip, the
   acts. The chip and the acts both used to want the third — the acts only
   because they moved onto the row this ticket — and a hidden member's row put
   the two in the same cell, which bumped the icons onto a second line on that
   row alone. An empty `auto` column costs nothing on the rows with no chip. */
.room-roster > .console-person {
  display: grid; grid-template-columns: auto 1fr auto auto;
  align-items: center; gap: var(--b-space-1) var(--b-space-2);
  padding: var(--b-space-2) 0;
  border-bottom: var(--b-border-w) solid var(--border-subtle);
}
.room-roster > .console-person > .room-chip-hidden { grid-column: 3; }
.room-roster > .console-person:last-child { border-bottom: 0; }
.room-roster > .console-person > .console-person-id { min-width: 0; }
.room-member-hidden .console-person-id .acct-value { color: var(--text-muted); }
/* THE ACTS SIT ON THE ROW NOW, hard right (TEN-1411, Mark's UAT ruling).
   They used to span `1 / -1` — a second full-width line under the identity —
   because four text buttons could not fit beside a name. Four ICONS can, so
   they take the row's third column, which is what that column was for.

   The DM moved in with them. It had its own `1 / -1` line below the governance
   three, so an owner's row rendered as three stacked bands; it is an act on
   this person like the others and belongs in the same group. */
.room-member-acts {
  grid-column: 4; justify-self: end;
  display: flex; align-items: center; gap: var(--b-space-1); flex-wrap: wrap; justify-content: flex-end;
}
.room-member-acts .console-inline-form { margin: 0; }

/* The icon buttons. Square, quiet until touched, and sized by ONE rule so the
   glyphs cannot drift apart — the reason crop.js gives for sizing its pair in
   CSS rather than on each <svg>. */
.room-act-icon {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; width: 2rem; height: 2rem; line-height: 1;
}
.room-act-ico { width: 1.15rem; height: 1.15rem; display: block; }
/* Line art takes currentColor, so the danger control would wear the danger
   colour for free if it were an icon. It is not one, deliberately — see the
   plate — but the rule stays true for whatever is added next. */
.room-member-acts .console-btn-danger { font-size: var(--b-text-sm); }
@media (max-width: 560px) {
  /* Below this the name and four controls stop fitting side by side, so the
     acts drop back to their own line rather than crushing the identity — still
     right-aligned, which is the half of the ruling that matters on a phone. */
  .room-member-acts { grid-column: 1 / -1; }
}

/* Your own standing in the room, and then — with a rule between them — how much
   it tells you. The border is doing real work: these are two different KINDS of
   decision (who can see me here; what reaches my pocket) and running them
   together as one settings blob is how somebody changes the wrong one. */
.room-self, .room-attention, .room-leave {
  margin-top: var(--b-space-3); padding-top: var(--b-space-3);
  border-top: var(--b-border-w) solid var(--border-subtle);
}
/* Inside the dropdown the first block leads the panel under its heading, so it
   must not draw the divider it drew when it followed a roster (TEN-1412). */
.room-prefs-panel > .room-prefs-head + .room-self,
.room-prefs-panel > .room-prefs-head + .room-attention {
  margin-top: var(--b-space-2); padding-top: 0; border-top: 0;
}

/* ── Your settings for this room (TEN-1412) ────────────────────────────────
   The header gear and its dropdown. A CSS checkbox, like every other menu on
   this cover (avatar, bell, hamburger): no script, so it opens with scripting
   off — which the forms inside have always required of themselves.
   It hangs off .msg-head, which is already `position: relative` for the people
   sheet, so the panel is pinned to the header's right edge rather than to the
   gear — at this width a menu anchored to a control this close to the edge
   would hang off the page. */
.room-prefs { position: relative; margin: var(--b-space-2) 0 0; }
.room-prefs-toggle { position: absolute; opacity: 0; pointer-events: none; }
.room-prefs-btn {
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; border-radius: var(--b-radius-sm);
  color: var(--text-muted); background: transparent;
  border: var(--b-border-w) solid transparent;
}
.room-prefs-btn:hover { background: var(--hover); color: var(--text-primary); }
/* The open state is marked on the control, or a reader who pressed it has no
   way to tell which of the header's several affordances the panel came from. */
.room-prefs-toggle:checked ~ .room-prefs-btn {
  background: var(--surface-overlay); color: var(--text-primary);
  border-color: var(--border-strong);
}
/* Keyboard reachability is on the INPUT, which is what actually takes focus —
   the label is not focusable, so a ring on the label alone would be a control
   no keyboard could see. */
.room-prefs-toggle:focus-visible ~ .room-prefs-btn {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px;
}
.room-prefs-ico { width: 1.15rem; height: 1.15rem; display: block; }
.room-prefs-panel {
  display: none; position: absolute; right: 0; top: calc(100% + var(--b-space-2)); z-index: 60;
  width: min(24rem, calc(100vw - 2rem));
  max-height: min(28rem, calc(100vh - 8rem)); overflow-y: auto;
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-md);
  box-shadow: var(--b-shadow-lg); padding: var(--b-space-3);
}
.room-prefs-toggle:checked ~ .room-prefs-panel { display: block; }
/* Two gears now render on this page — the room's, for owners, inside the
   roster sheet, and this one, for everybody, in the header. They govern
   different things, so this panel names itself in its first line rather than
   leaving the icon to carry a distinction an icon cannot carry. */
.room-prefs-head {
  margin: 0 0 var(--b-space-2); font-weight: var(--b-weight-semibold);
  color: var(--text-primary); font-size: var(--b-text-base);
}

/* What being on the roster MEANS, under the reader's own handle (TEN-1411).
   Italic per the ruling, and a block so it takes its own line under the handle
   rather than trailing the role label. */
.room-self-note {
  display: block; font-style: italic;
  font-size: var(--b-text-sm); color: var(--text-muted);
}

/* LEAVING — the panel's foot, in the danger register (TEN-1411, Mark's ruling:
   "give it a red block treatment"). A tinted block with a danger rule down its
   leading edge, which is the treatment the composer's gate notices already use
   on this page for "here is a consequence"; this is the only one of them that
   is also a door. The title carries the weight and the button is small and
   last, because the description is the part somebody has not read. */
.room-leave {
  border-top-color: var(--role-danger);
  background: var(--bad-a, color-mix(in srgb, var(--role-danger) 10%, transparent));
  border-radius: var(--b-radius-sm);
  padding: var(--b-space-3); margin-bottom: var(--b-space-1);
}
/* --bad-fg, not --role-danger. The tokens file keeps a separate danger colour
   for TEXT precisely because the surface colour is not readable as type: on
   this block's composited background --role-danger measures 2.63:1 in dark and
   3.93:1 in light, both under 4.5:1, and at 0.95rem this is not large text.
   Shipping a fresh sub-AA string in the same slice as the contrast fix would
   have been a joke at the ticket's expense. */
.room-leave-title {
  margin: 0; font-weight: var(--b-weight-semibold);
  color: var(--bad-fg, var(--role-danger)); font-size: var(--b-text-base);
}
.room-leave-why {
  margin: var(--b-space-1) 0 var(--b-space-2);
  font-size: var(--b-text-sm); color: var(--text-primary);
}
.room-leave-go { align-self: flex-start; }
.room-self .console-inline-form { margin: 0; }
.room-self .acct-stub-body { margin: var(--b-space-1) 0 var(--b-space-2); }

/* The four attention rungs. A STACK OF ROWS with the current one marked, not a
   dropdown: this is a choice among four and all four have to be readable at
   once, or "Summaries only" and "Only when I am mentioned" — the pair people
   confuse — are never seen side by side.

   Rows rather than the wrapping row of pills this was until TEN-1410 (Mark's
   UAT ruling). Four equal pills side by side read as four independent buttons
   — the shape of a toolbar, not of a choice; one full-width row each, in a
   single bordered card, is the shape every settings surface uses for pick-one.
   The mechanics did not move: still four forms, still one POST per press,
   still nothing here needs scripting.

   The current rung renders as a <span> rather than a disabled button,
   deliberately: it is a STATE, and a disabled control invites somebody to
   wonder what would happen if it worked. Which means the rows are two
   different elements wearing one class, and every rule below has to hold for
   both — hence the paired :last-child selectors (the span is a direct child,
   the buttons are wrapped in their forms). */
.room-attention-lead {
  color: var(--text-primary); font-size: var(--b-text-base);
  margin: var(--b-space-1) 0 var(--b-space-2);
}
/* --border-strong, not --border-subtle, and this is not a taste call: the
   roster sheet's own surface is #3A3A3E and --border-subtle is #3C3C40, so a
   subtle border drawn on this panel is INVISIBLE — the card had no edges and
   the four rows had no dividers, which is the "raw html" reading Mark's
   ruling is against. Anything drawing a line inside this panel needs the
   strong token. */
.room-rungs {
  display: flex; flex-direction: column; margin: var(--b-space-2) 0;
  border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-sm); overflow: hidden;
}
.room-rungs .console-inline-form { margin: 0; display: block; }
.room-rung {
  display: flex; width: 100%; box-sizing: border-box;
  align-items: center; justify-content: space-between; gap: var(--b-space-3);
  padding: var(--b-space-2) var(--b-space-3);
  background: none; color: var(--text-primary);
  border: 0; border-bottom: var(--b-border-w) solid var(--border-strong); border-radius: 0;
  font: inherit; font-size: var(--b-text-sm); text-align: left; cursor: pointer;
  transition: background var(--b-duration-fast) var(--b-ease);
}
/* The last row owns the card's bottom edge, so it must not draw its own. */
.room-rungs > .room-rung:last-child,
.room-rungs > :last-child > .room-rung { border-bottom: 0; }
button.room-rung:hover { background: var(--hover); }
.room-rung:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: -3px; }
.room-rung-label { min-width: 0; }
/* The mark is a ring on every row and a filled ring on the chosen one: four
   marks in a column, so which rung is on is answerable without reading a word.
   An empty ring on the unchosen rows (rather than nothing) is what makes the
   column legible as pick-one instead of as one decorated row. */
.room-rung-mark {
  flex: 0 0 auto; position: relative; width: 1.05rem; height: 1.05rem;
  border-radius: 50%; background: var(--surface-page);
  border: var(--b-border-w) solid var(--border-strong);
}
/* THE CHOSEN RUNG KEEPS ITS TINT UNDER THE POINTER (TEN-1702 sweep; the ruling
   is at `.ktlist-chip-on`). A real break, and note the SHAPE of the second
   selector: the hover rule it has to outrank is `button.room-rung:hover`, which
   carries an element name — so a bare `.room-rung-on:hover` would lose the
   cascade and read as a fix while changing nothing. The arm is written at the
   same weight as the rule it answers. */
.room-rung-on, button.room-rung-on:hover {
  background: color-mix(in srgb, var(--b-accent) 14%, transparent);
  color: var(--text-primary); font-weight: var(--b-weight-semibold); cursor: default;
}
.room-rung-on .room-rung-mark { background: var(--b-accent); border-color: var(--b-accent); }
.room-rung-on .room-rung-mark::after {
  content: ""; position: absolute; inset: 0.26rem; border-radius: 50%;
  background: var(--b-accent-fg, var(--ink-on-accent));
}

/* ── The floor, as one toggle in the composer (TEN-1413) ───────────────────
   Four states, all of them visible: ON/OFF is whether the reader holds the
   floor, enabled/disabled is whether they are the one who may move it. The
   disabled cells are STATUS — an owner grants the floor in a room whose
   self-promotion is off, and their member has to be able to see that it
   landed — so they are dimmed, never hidden, and never so dim that the
   microphone stops reading as on or off. That is the whole reason this is
   `opacity: 0.75` and a muted colour rather than the 0.45 a dead control
   would take: TEN-1411 #4's complaint was small text that read as disabled,
   and a status indicator that reads as broken is the same mistake wearing an
   icon.
   Sized and shaped like .emoji-open, which it sits beside. */
.room-floor-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0; line-height: 1;
  border: var(--b-border-w) solid transparent; border-radius: var(--b-radius-sm);
  background: transparent; color: var(--text-primary); cursor: pointer;
  transition: background var(--b-duration-fast) var(--b-ease), color var(--b-duration-fast) var(--b-ease);
}
.room-floor-ico { width: 1.15rem; height: 1.15rem; display: block; }
/* Holding the floor is the LOUD state, because it is the one with a
   consequence: what you type will be attributed to you in front of everybody. */
.room-floor-on { color: var(--b-accent); border-color: var(--border-strong); background: var(--surface-overlay); }
/* The microphone's strike is ALWAYS in the markup and hidden by the ON class (TEN-1422),
   rather than rendered conditionally. The floor toggle has to flip LIVE when an owner grants
   or withdraws the floor, and the live client's standing rule is that it never composes what
   the plate owns. A conditional path would have forced it to build an SVG node with a literal
   `d` attribute copied out of the template — a second copy of a shape, drifting the moment the
   icon is redrawn. One class toggle instead, and the two renders cannot disagree. */
.room-floor-strike { display: none; }
.room-floor-btn:not(.room-floor-on) .room-floor-strike { display: inline; }
.room-floor-btn:hover:not([aria-disabled="true"]) { background: var(--hover); }
.room-floor-btn:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }

/* THE ENABLED/DISABLED SPLIT IS A COLOUR, not just an opacity, and it has to
   be: three of the four cells are OFF-or-muted-looking, and at one shared ink
   "you may press this to take the floor" and "you cannot take the floor here"
   were the same picture with a 25% opacity between them — which is no
   difference at all on a phone in daylight.
   So ACTIONABLE cells take full-strength ink (--text-primary above) and
   STATUS-ONLY cells drop to --text-muted. Muted, not faint, and 0.9 rather
   than the 0.5 a dead control would take: TEN-1411 #4's whole complaint was UI
   that reads as switched off, and these two cells are the ones a reader most
   needs to believe — an owner granted the floor and their member has to see
   that it landed. `aria-disabled`, not `[disabled]`, so both keep their focus
   order and their tooltip, which is the only text this control has. */
.room-floor-btn[aria-disabled="true"] { cursor: default; color: var(--text-muted); opacity: 0.9; }
.room-floor-on[aria-disabled="true"] { color: var(--b-accent); opacity: 0.8; }

/* THE FLOOR AS WORDS (TEN-1820, the read-only branch).
   Where there is no composer, the floor is not an icon beside a text box — it
   is the ACT under the sentence that explains why you cannot post, and an act
   under a paragraph wears words. It keeps `.room-floor-btn` so the live client
   still re-derives it by class and attribute (roomlive.js applyFloorPlan), and
   these three lines undo the icon GEOMETRY that class carries: a 2rem square
   is the right cell beside a textarea and the wrong one here.
   Sits BELOW the icon rules on purpose — same specificity, last one wins. */
.room-floor-act { margin: var(--b-space-2) 0 0; }
.room-floor-take {
  width: auto; height: auto;
  padding: var(--b-space-1) var(--b-space-3);
  font-size: var(--b-text-sm);
  /* The primary FACE, restated. `.room-floor-btn` sits later in this file than
     `.console-btn-primary` and paints the icon's transparent background over
     it, so without these three the act rendered as bare text on the panel —
     measured in the browser, not assumed. */
  background: var(--b-accent); color: var(--b-accent-fg); border-color: transparent;
}
/* `.room-floor-on` paints the ICON's loud state — an accent glyph on a surface
   fill — and it is applied by class, so a live role event can land it on this
   button too. On a primary button that reads as the button going pale, so the
   primary face is restated here rather than left to the last rule written. */
.room-floor-take.room-floor-on {
  background: var(--b-accent); color: var(--b-accent-fg); border-color: transparent;
}

/* Post removal (the author's own, or a moderator's). Revealed on hover, and
   always reachable by keyboard focus — an affordance that exists only under a
   mouse is one a phone reader cannot get to, which is why the coarse-pointer
   query below keeps it visible. It is a real form, so it works with scripting
   switched off. */
.room-msg-del { margin: 0; }
.room-msg-del-btn {
  border: 0; background: none; cursor: pointer; padding: 0 0.25rem;
  color: var(--text-muted); opacity: 0; font-size: var(--b-text-sm); line-height: 1;
  transition: opacity var(--b-duration-fast) var(--b-ease);
}
.msg-slot:hover .room-msg-del-btn,
.room-msg-del-btn:focus-visible { opacity: 1; }
.room-msg-del-btn:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
@media (hover: none) {
  .room-msg-del-btn { opacity: 0.6; }
}
/* With scripting the form leaves the pane face (TEN-1271, Mark's ruling): the
   actions menu's confirmed Remove submits it instead, so a one-tap destructive
   control no longer sits under every message. Scriptless keeps the form — the
   menu is JS-only, and delete must survive without it. */
@media (scripting: enabled) {
  .room-msg-del { display: none; }
}

/* The in-menu Remove confirmation (TEN-1271). The question reads as a
   statement of consequence in the danger register; the confirm carries the
   danger color, the cancel stays ordinary — the safe path is never the one
   dressed loudest. */
.msg-acts-del { border-top: 1px solid var(--b-border); margin-top: 0.25rem; padding-top: 0.25rem; }
.msg-acts-del-ask { color: var(--role-danger); }
.msg-acts-del-q { margin: 0; padding: 0.35rem 0.65rem 0.15rem; font-size: var(--b-text-sm); color: var(--text-muted); max-width: 16rem; }
.msg-acts-del-yes { color: var(--role-danger); font-weight: 600; }

/* The attachments this surface cannot show (TEN-1037 obligation 5). Dim and
   plain — it is a statement about the CLIENT'S limits, not about the post, and
   dressing it up as a file affordance would imply something to click. */
.room-media-absent { font-style: italic; opacity: 0.85; }

/* THE GATED COMPOSER'S DIMMING RULE IS GONE (TEN-1820, Mark's UAT round 2).
   It dimmed a disabled textarea and a disabled Post button inside
   `.room-composer-gated`, and that branch no longer renders either: a read-only
   room shows ONE thing — the sentence that says why, and the act where there is
   one. The old comment argued that keeping the control present-and-unavailable
   beat making it vanish; the ruling is that a third option beats both, because
   the sentence was already there underneath and was the only honest part of the
   arrangement. Deleted rather than left behind: this selector could not match
   anything, and dead CSS is how the next reader learns the wrong shape. */

/* Settings: two fieldsets, and the second one looks different on purpose.
   Defaults are starting points a member's own choice overrides; CEILINGS are
   the room's law and cannot be undone from below (ADR-CM011 §4). Rendering them
   as one undifferentiated stack of switches is exactly how somebody flips a
   ceiling believing they changed a default. */
.room-set-group {
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
  padding: var(--b-space-3); margin: var(--b-space-3) 0;
}
.room-set-group > legend { padding: 0 var(--b-space-1); }
.room-set-ceilings { border-left: 3px solid var(--b-warning, #d97706); }

/* Retire, in the danger register at the FOOT of the settings panel, past
   everything the room is (the TEN-1262 placement). It is the only control on
   this surface whose consequence cannot be walked back through any op, so it is
   the last thing an owner meets rather than the first. */
.room-danger {
  margin-top: var(--b-space-4); padding-top: var(--b-space-3);
  border-top: var(--b-border-w) solid var(--bad);
}
.room-danger .acct-stub-body { font-size: var(--b-text-sm); color: var(--text-muted); }
.room-danger .console-pref-form { margin-top: var(--b-space-2); }

/* The club storefront (TEN-1302): the commerce half of the public club page,
   rendered by sportscore's club-storefront plate beneath the org profile.
   Offer rows follow the recrow register (surface-raised card per sellable
   thing); the Upcoming list mirrors the navigator's tappable-row grammar. */
.ss-store { margin-top: var(--b-space-5); }
.ss-store-h { font-size: var(--b-text-lg); margin: var(--b-space-4) 0 var(--b-space-2); }
.ss-store-list { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ss-store-offer {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-2) var(--b-space-3);
}
.ss-store-offer strong { flex-shrink: 0; }
.ss-store-offer .rbtn { margin-left: auto; }
.ss-store-line { color: var(--text-muted); font-size: var(--b-text-sm); }
/* OWNED (TEN-1463): the row a signed-in holder already pays for keeps its
   place in the catalog and loses its button — a .chip in the presence green
   the rest of the cover uses for "this is yours" (.youpill, .regstate.ok),
   sitting where the Join it replaces used to be. It is a state, not a
   control: nothing to press, nothing for press.js to acknowledge. Renders
   for the holder alone; every other viewer still gets the form. */
.ss-store-have {
  margin-left: auto; color: var(--role-presence);
  background: var(--green-a); border-color: var(--green-line);
}
/* Add-ons are CHILD blocks of the subscription they attach to (TEN-1473) —
   the storefront's half of the nesting the offerings console got in TEN-1437,
   and deliberately the SAME grammar: an indent and a rail, nothing else. An
   add-on is never sold on its own and only a holder of the parent ever sees
   one, so the relation has to be visible on the page rather than implied by a
   heading three sections up. The group is the parent block plus its children
   as one unit; the child rows sit inside it wearing the ordinary offer card so
   the row register stays one register. */
.ss-store-group { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ss-store-children {
  display: flex; flex-direction: column; gap: var(--b-space-2);
  margin-left: var(--b-space-4); border-left: 2px solid var(--border-subtle);
  padding-left: var(--b-space-3);
}
.ss-store-child strong { font-size: var(--b-text-sm); }
.ss-store-events { display: flex; flex-direction: column; gap: var(--b-space-1); }
.ss-store-event {
  display: flex; align-items: center; gap: var(--b-space-2);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-2) var(--b-space-3);
  text-decoration: none; color: var(--text-primary);
}
.ss-store-event span { color: var(--text-muted); font-size: var(--b-text-sm); }
.ss-store-event .chev { margin-left: auto; color: var(--text-faint); }
.ss-store-note { color: var(--text-faint); font-size: var(--b-text-sm); margin-top: var(--b-space-2); }

/* The register page's punch strip (TEN-1304): held-card chips (compatible
   active, incompatible dimmed with a title tooltip) and the in-context
   offers — the offer rows share the storefront's card register. */
.punchchips { display: flex; flex-wrap: wrap; gap: var(--b-space-2); margin: var(--b-space-2) 0; }
.punchchip {
  display: inline-flex; align-items: center; gap: var(--b-space-1);
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-lg); padding: var(--b-space-1) var(--b-space-2);
  font-size: var(--b-text-sm);
}
.punchchip.off { opacity: .55; }
.punchoffers { display: flex; flex-direction: column; gap: var(--b-space-2); margin: var(--b-space-2) 0; }
.punchoffer {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-2) var(--b-space-3);
}
.punchoffer .rbtn { margin-left: auto; }

/* The org console shell (TEN-1296): header + tab bar + event rows for
   /account/org/{org}. Cards inside the tabs reuse the console's acct-card
   register; what lives here is only the shell's own chrome. */
.orgc { display: flex; flex-direction: column; gap: var(--b-space-3); }
/* K19'S HEAD (TEN-1671) — the org card, then the wayfinding row. ONE block for
   every console page in the book: cmc-auth's shell renders it inline and the
   off-mount surfaces take the same bytes over the console's chrome seam, so
   these rules are the only place the header's geometry is decided.

   The card is the person/org card family's row rung (`console-person-av` is the
   same face this book draws in every listing), with one thing added at this
   rung: the FACE IS A LINK to the org's public page. */
.ohead { display: flex; flex-direction: column; gap: var(--b-space-2); margin-bottom: var(--b-space-3); }
.ohead-org { display: flex; align-items: center; gap: var(--b-space-2); min-width: 0; }
.ohead-face { display: inline-flex; flex: none; border-radius: 50%; text-decoration: none; }
.ohead-face:focus-visible { outline: 2px solid var(--accent, var(--text-primary)); outline-offset: 2px; }
.ohead-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
/* Mark's spec, exactly: the name in BOLD, the handle under it. */
.ohead-name { font-weight: 700; color: var(--text-primary); overflow-wrap: anywhere; }
.ohead-handle { color: var(--text-muted); font-size: var(--b-text-sm); overflow-wrap: anywhere; }
/* The session's own row (roles chip, active-context marker, enter-this-context
   act, switch-organization link) is RETIRED (Mark's ruling, 2026-09-13;
   TEN-1674) — the console header prints none of it. .orgc-session/-roles/
   -switch/-active/-enter are deleted along with the markup that wore them. */
.orgc-note { color: var(--text-muted); font-size: var(--b-text-sm); margin: 0; }
.orgc-tabs {
  display: flex; flex-wrap: wrap; gap: var(--b-space-1);
  border-bottom: var(--b-border-w) solid var(--border-subtle);
  padding-bottom: var(--b-space-1);
  align-items: center;
}
/* THE GROUP PICKER (TEN-1790) — [ Operations | Admin ] at the LEFT of the strip,
   inside the same nav and the same row.

   ONE ROW, WHICH IS WHAT KEEPS K19 RUNG 2 INTACT. The console divides in two, and
   the cheap way to show that is a second nav above the strip — which would be two
   wayfinding rows answering one question, the thing K19 exists to forbid. So the
   family name sits at the strip's left margin, hairline-separated, and the active
   group's tabs run to its right. The strip is still one row; it just says which
   family it is a strip OF.

   IT IS THE KIT'S SEGMENT IN ITS ANCHOR SHAPE, not a control of this console's
   own — `.ktseg` + `.ktseg-opt` from the kit block, exactly as `.plhap-filter`
   mounts them once the server has already narrowed the answer. What it adds is
   the QUIET WEIGHT Mark specified: this is wayfinding beside eleven muted tabs,
   not a form's primary choice, so the lit value takes the raised surface and the
   primary ink that `.orgc-tab-on` takes one element over, rather than the kit's
   filled --role-primary ground. Two controls in one row drawn at two weights
   would read as two kinds of thing, and they are one kind of thing.

   THE LIT CLASS IS SERVER-STAMPED, AND THAT IS THE SUPPORTED ARM. The kit block
   above refuses `-on` classes for segments wrapping a CHECKBOX, because a class
   cannot move without another render; it keeps them for the anchor shape, whose
   "selected state is a fact about the address rather than about a checkbox". This
   is that shape. Nothing here can go stale, because the only thing that changes
   it is arriving on another tab. */
/* THE HAIRLINE IS THE WRAPPER'S, not the segment's, and the nesting is the whole
   reason there are two spans. `.ktseg` brings its own box — that is the `[ | ]` of
   Mark's spec — and a `border-right` added to the same element would have drawn a
   second line a pixel outside the first. One element cannot carry both the kit's
   border and the strip's separator, so the separator gets its own. */
.orgc-tabgroups {
  flex: none; display: inline-flex; align-items: center;
  margin-right: var(--b-space-1);
  padding-right: var(--b-space-2);
  border-right: var(--b-border-w) solid var(--border-subtle);
}
/* Quieter than the kit's default: this is wayfinding beside eleven muted tabs,
   not a form's primary choice. The kit's own 13px/700 and --border-strong would
   make the picker the loudest thing in a row of muted words. */
.orgc-tabgroups .orgc-tabseg {
  font-size: var(--b-text-sm); font-weight: 600;
  border-color: var(--border-subtle);
  border-radius: var(--b-radius-sm);
}
/* TWO-CLASS SELECTORS, DELIBERATELY. The kit block defines `.ktseg-opt` some two
   thousand lines BELOW this one, and with equal specificity the later rule wins —
   so a single `.orgc-tabgroup` would lose its padding and its ground to the kit's
   12px/raised default and this whole quiet-weight paragraph would be a comment
   describing a render nobody gets. Descending from `.orgc-tabseg` is what makes
   these rules actually apply, not tidiness. */
.orgc-tabseg .orgc-tabgroup {
  padding: var(--b-space-1) var(--b-space-2);
  color: var(--text-muted); text-decoration: none;
  display: inline-block; white-space: nowrap;
  background: none;
}
.orgc-tabseg .orgc-tabgroup:hover { color: var(--text-primary); background: var(--surface-raised); }
.orgc-tabseg .orgc-tabgroup-on,
.orgc-tabseg .orgc-tabgroup-on:hover {
  color: var(--text-primary); background: var(--surface-raised);
}
.orgc-tab {
  display: inline-block; padding: var(--b-space-1) var(--b-space-2);
  border: none; background: none; cursor: pointer;
  color: var(--text-muted); text-decoration: none;
  font: inherit; font-size: var(--b-text-sm);
  border-radius: var(--b-radius-sm);
}
.orgc-tab:hover { color: var(--text-primary); background: var(--surface-raised); }
/* The state's own hover arm (TEN-1702 sweep; see `.ktlist-chip-on`). */
.orgc-tab-on, .orgc-tab-on:hover { color: var(--text-primary); font-weight: 600; background: var(--surface-raised); }
.orgc-tabform { display: inline; }
/* THE ATTENTION MARK ON A TAB LABEL (TEN-1713, board R2R-2) — one small amber
   disc carrying "!", never a count. It rides INSIDE the tab's own anchor or
   button so it moves with the label and inherits the tab's hit area: a mark
   beside a link is a second thing to aim at, and there is nothing separate to
   press here. --role-attention is the palette's own amber, the same token the
   notice bar and the warn chip take, so the three attention surfaces on this
   console cannot drift to three ambers.

   IT IS AN OUTLINED DISC RATHER THAN A FILLED ONE, and that is TEN-1700's scar
   rather than a taste: the obvious spelling is an amber ground with the page's
   ground as the ink, and `--surface-base` IS NOT A TOKEN THIS BOOK DEFINES.
   An undefined custom property is invalid at computed value time, so `color`
   would fall back to INHERIT and the mark would draw the tab's own muted ink on
   amber — legible enough in a diff to ship, and wrong on screen. Amber on the
   page's own raised surface needs no second colour to be invented, so there is
   nothing here that can fall back to inherit. */
.orgc-bang {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--amber-a); color: var(--role-attention);
  border: 1px solid var(--role-attention);
  font-size: 10.5px; font-weight: 800; line-height: 1;
  vertical-align: middle;
}
/* ── THE CALL-TO-ACTION BLOCK (`ktcta`) — K51, PROPOSED (TEN-1895) ──────────
   "Demands attention, disappears once addressed." Named here after it shipped
   twice without a name: TEN-1713's landing info bar (`orgc-r2rbar`, then the
   Payments page's notice, TEN-1786) and, by TEN-1895, the Organizations page's
   "Clubs asking to show you". One treatment — the amber pair the attention mark
   above already speaks — and two RUNGS:

     .ktcta          THE BLOCK. A title, one or more pending asks, their acts.
     .ktcta--bar     THE BAR. One sentence (and at most one act) on one line.

   NO CLOSE CONTROL ANYWHERE IN THIS FAMILY, and that is the definition rather
   than an omission: the block derives away when the thing it asks for is done,
   so there is no dismissal state to store and nothing a reader could dismiss
   their way out of. A block whose content is gone is ABSENT (K30 arm 1), never
   an amber box saying there is nothing here — the server decides presence, the
   stylesheet only draws it.

   AMBER IS SPENT ON "NEEDS A HUMAN" AND NOTHING ELSE — `--role-attention`, the
   same token the tab mark and the pending-request rail below take, so the
   console's attention surfaces cannot drift to three ambers. The block's TEXT
   stays at the resting ink tiers: the ground carries the attention, the words
   carry readability (the pending bubble's "chrome carries the state, ink
   carries readability" clause, TEN-1885 — the same argument). */
.ktcta {
  padding: var(--b-space-3) var(--b-space-4);
  margin-bottom: var(--b-space-3);
  border: var(--b-border-w) solid var(--role-attention);
  border-radius: var(--b-radius-md);
  background: var(--amber-a);
}
.ktcta-title { font-weight: var(--b-weight-semibold); margin: 0 0 var(--b-space-1); color: var(--text-primary); }
.ktcta-text { margin: 0; font-size: var(--b-text-sm); color: var(--text-primary); }
/* The rows inside the block rule off each other in the attention edge's own
   colour at low weight, rather than the grey hairline `.acct-listing` draws on a
   plain card — a grey rule on an amber ground reads as a second surface. */
.ktcta .acct-listing + .acct-listing { border-top-color: var(--role-attention); }
/* THE BAR RUNG — TEN-1713's metrics exactly, so the Payments notice is unmoved
   to the pixel by being renamed. */
.ktcta--bar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--b-space-2);
  padding: var(--b-space-2) var(--b-space-3);
  margin-bottom: 0;
  border-radius: var(--b-radius-sm);
}
/* THE PENDING-REQUEST ROW (TEN-1713, board R2R-2). It is an `.orgc-person` with
   one modifier and nothing else: the row IS a person-with-acts on a tab full of
   people-with-acts, so it inherits the roll's layout rather than restating it.
   The modifier carries only the amber left rule that says this row is WAITING on
   somebody, which is the one way it differs from a settled member below it. */
.orgc-r2rreq { border-left: 3px solid var(--role-attention); padding-left: var(--b-space-2); }
/* THE WIND-DOWN COLUMN'S ROWS (TEN-1715, board R2R-4). Same shape as the row
   above and for the same reason — it is an `.orgc-person` with one modifier, so
   it inherits the roll's layout rather than restating it.

   THE DEFAULT RULE IS MUTED, NOT AMBER, and the difference from `.orgc-r2rreq`
   directly above is the whole point: that row is WAITING ON SOMEBODY IN THIS ORG,
   this one is waiting on nobody here. The system is doing the asking, and a
   console that lit these rows the same colour as the ones needing a decision
   would be inventing fourteen chores an org was promised it did not have.

   Only the two rows that are genuinely NOT "in progress" recolour: done, and the
   ones the platform cannot move. `--role-presence` is the green this book already
   uses for a fact about a standing (`.orgc-chip-held`); `--role-attention` is the
   amber the row above uses for something needing a human. There is no
   `--role-positive` in this book's tokens — the green is `--role-presence`. */
.orgc-winddown { border-left: 3px solid var(--border-subtle); padding-left: var(--b-space-2); }
.orgc-winddown-done { border-left-color: var(--role-presence); }
.orgc-winddown-blocked { border-left-color: var(--role-attention); }
/* THE CONFIRMED ARRANGEMENT'S TWO CHIPS (TEN-1713, board R2R-2). They ride the
   roll's existing `.orgc-chip` base and only set their own pair, so a cover that
   restyles the status chip restyles these at the same moment.

   GREEN IS K29's AND IT IS SPENT ON THE RIGHT HALF: what the member HOLDS is a
   fact about their standing, so it is green; who COLLECTS is a fact about the
   arrangement, so it is neutral. Reversing them would paint the org's admin
   chore in the colour that means "you're covered".

   THE PAIR IS `--role-presence` ON `--green-a`, which is what `.regstate.ok` and
   the entry chip already use. `--role-positive` is NOT a token this book defines
   — the second time this lane reached for a colour that does not exist, and
   TEN-1700's note says what it costs: invalid at computed value time, `color`
   falls back to inherit, and the chip draws the row's own ink on green. Every
   token in these two rules was grepped in tokens.css before it was written. */
.orgc-chip-held { color: var(--role-presence); background: var(--green-a); }
.orgc-chip-billed { color: var(--text-muted); background: var(--surface-raised); }
/* The Events tab's control bar (TEN-1592). It carried one button and could get
   away with being an undeclared block; it now carries two — "+ Event" and
   "Your classes" — and in the unfocused state each is its own <form>, which
   would otherwise stack them. A flex row lines the two tiers up whichever shape
   they take. */
.orgc-eventsbar {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--b-space-2); margin-bottom: var(--b-space-3);
}
.orgc-events { display: flex; flex-direction: column; gap: var(--b-space-1); }
.orgc-event {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2);
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); padding: var(--b-space-2) var(--b-space-3);
}
.orgc-date { display: inline-flex; flex-direction: column; align-items: center; min-width: 2.4em; }
.orgc-date .mo { font-size: var(--b-text-xs, 0.7rem); color: var(--text-muted); text-transform: uppercase; }
.orgc-date .dy { font-weight: 700; }
.orgc-event-name { color: var(--text-primary); text-decoration: none; font-weight: 600; }
.orgc-event-name:hover { text-decoration: underline; }
.orgc-chip {
  font-size: var(--b-text-sm); color: var(--text-muted);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-sm); padding: 0 var(--b-space-1);
}
.orgc-chip-live { color: var(--accent, var(--text-primary)); border-color: currentColor; }
/* .orgc-event-links / .orgc-muted are gone with their only markup (TEN-1592):
   the Events tab's per-row Enrollments · Door drill-ins into the archived event
   console, and the greyed variant they wore while the org was unfocused. */
.orgc-danger { border-color: var(--danger, #b00020); }
.orgc-delete { display: flex; flex-wrap: wrap; gap: var(--b-space-2); margin-top: var(--b-space-2); }

/* Members tab rows (TEN-1383): duotone member cards. TEN-1297 shipped the
   plate but no cover styling landed with it, so the row rode in as loose
   inline text with a full-resolution photo. Fixed by REUSE, not new rules:
   the avatar is .console-person-av/.console-person-initial, the console's
   one fixed-size avatar + initials-disc fallback (My Network, messages,
   rooms all wear it); the alternating tint is the same
   ":nth-child(even of …)" technique the notification-settings matrix uses
   (UAT round 6, above); the status colors borrow the .sbadge vocabulary
   rather than inventing a second one. */
.orgc-people {
  display: flex; flex-direction: column;
  background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md); overflow: hidden;
}
.orgc-person {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-3);
  padding: var(--b-space-2) var(--b-space-3);
}

/* ── THE PERSON CARD: A SURFACE STEP OVER THE BLOCK (TEN-1796, Mark's UAT r3) ──
   The rows above are a LIST — one panel, zebra bands, a table's posture. Mark's
   finding is that these particular rows are not list entries: each one is a thing
   you press, and it opens an editor about a person. So they take the step every
   other pressable thing in this book takes to say so.

   ONE STEP, MEASURED IN TOKENS AND NOT IN PIXELS: the block stays `--surface-raised`
   (s1) and the card sits on `--surface-overlay` (s2), which is the same pair the
   modal shell and the notification matrix already use. A hairline of
   `--border-subtle` and ONE shadow step. Nothing here invents a colour.

   `.orgc-people-cards` REPLACES the panel rather than decorating it: a card that
   steps up out of a bordered, clipped container reads as a card inside a box, so
   the container gives up its own background, border and radius and becomes spacing.
   The zebra goes with it — alternating tint on separated cards is two visual
   systems answering one question, and the card's own edge is the better answer. */
.orgc-people-cards {
  background: none; border: 0; border-radius: 0; overflow: visible;
  gap: var(--b-space-2);
}
.orgc-people-cards .orgc-person:nth-child(even of .orgc-person:not([hidden])) {
  background: none;
}
.orgc-person-card {
  background: var(--surface-overlay);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
  padding: var(--b-space-3);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--b-shadow-ink, #000) 10%, transparent);
}
/* THE WHOLE CARD IS THE TARGET, so it says so on hover and when the anchor inside
   it takes focus — `:focus-within` and not `:focus`, because the thing that
   receives focus is the anchor over the identity area rather than the card. A
   pressable surface that never acknowledges the pointer is the wave-2 defect in
   its quietest form. */
.orgc-person-card:hover, .orgc-person-card:focus-within {
  border-color: var(--border-strong);
}
/* The anchor is a BLOCK spanning the identity half, and it carries no link
   decoration: the card is the control, and an underlined name inside it would read
   as a second, smaller destination. `min-width: 0` so a long handle ellipsises
   inside the flex row instead of pushing the toggle off the card. */
.orgc-person-open {
  display: flex; align-items: center; gap: var(--b-space-3);
  flex: 1 1 auto; min-width: 0;
  color: inherit; text-decoration: none;
}
.orgc-person-open:focus-visible { outline: 2px solid var(--b-accent); outline-offset: 3px; border-radius: var(--b-radius-sm); }
/* The Staff roll's live filter (TEN-1785). Same [hidden] specificity trap as
   .msg-row and .net-row: .orgc-person is display:flex, so `hidden` alone hides
   nothing and the no-script marked page would filter nothing either. */
.staff-row[hidden] { display: none; }
/* listfilter.js bound AND able to answer the whole question (.staff-live, set
   by the server on a single-page roll): the marks answer live, so the submit is
   noise. On a roll that pages the button stays — it is the way to the matches
   this page does not hold. */
.staff-live .staff-filter-go { display: none; }
/* The zebra counts VISIBLE rows. `:nth-child(even of ...)` re-evaluates its
   selector list, so narrowing the filter would otherwise leave the stripes
   where the hidden rows used to be — bands of two and gaps of one. */
.orgc-person:nth-child(even of .orgc-person:not([hidden])) {
  background: color-mix(in srgb, var(--surface-overlay) 45%, transparent);
}
/* ── THE ROW ANSWERS THE POINTER (TEN-1814, Mark's UAT r3) ────────────────────
   A pressable surface that never acknowledges the pointer is the wave-2 defect, and
   this roll had it: `.orgc-person` carried no hover at all, so the only sign the card
   was pressable was the underline that just left. This is the replacement, on K15's
   surface channel.

   QUIET WEIGHT, ON PURPOSE. These are list rows, not the calendar's chips, so the
   full accent ground K15 gives a cell would read as a selection. One surface step is
   the whole treatment: the zebra band is `--surface-overlay` at 45%, so hover is the
   same token at full strength — a step the eye reads as "this one" without the row
   changing colour, and not a single new value invented.

   IT MUST OUT-SPECIFY THE ZEBRA, which is `.orgc-person:nth-child(...)` at (0,2,0).
   `.orgc-people .staff-row:hover` is (0,3,0), so the ground is the same under an odd
   row and an even one — a hover that only worked on half the rows would be worse than
   none. Declared AFTER the zebra as well, so the intent survives a future reorder.

   ONE TREATMENT FOR HOVER AND FOCUS (K15). `:focus-within` and not `:focus`, because
   the thing that takes focus is the anchor spanning the identity half, never the row. */
.orgc-people .staff-row:hover,
.orgc-people .staff-row:focus-within {
  background: var(--surface-overlay);
}
/* THE ROW'S PRESSABLE HALF (TEN-1791). Pressing a card opens the role-edit
   dialog, and the press target is a LINK because `?member=` is a real address.

   IT IS A GRID, AND THAT IS WHAT ALIGNS THE ROLE COLUMN — Mark's item. The row
   was flex all the way down, so each card sized its own identity block and the
   roles landed wherever that card's name happened to end: a column of roles that
   was not a column. `1fr` for the identity and a FIXED track for the roles means
   every card breaks in the same place, whatever the names are.

   THE AVATAR IS OUTSIDE IT, because two destinations cannot nest: the picture
   goes to the person's public page, the rest of the card opens the dialog. */
/* FIXED TRACKS, NOT `1fr` — and the browser gate is what caught the difference.
   The first attempt was `1fr 10rem auto`, which looks right and aligns nothing: a
   grid sizes per CONTAINER, and every card is its own container, so `1fr` is
   "whatever is left after this row's status chip" — and the chips say active,
   invited, declined, expired, which are four different widths. The roles column
   came out at five different x positions (484, 329, 393, 342, 348) while the
   markup was perfect. Declaring the first two tracks is what makes the third
   column start in the same place on every card. */
.orgc-person-press {
  flex: 1 1 auto; min-width: 0;
  display: grid; grid-template-columns: 16rem 9rem auto;
  align-items: center; gap: var(--b-space-3);
  color: inherit; text-decoration: none;
}
/* ── THE NAME IS NOT A TEXT LINK (TEN-1814, Mark's UAT r3; the no-text-links rule) ──
   `a.orgc-person-press:hover .orgc-person-name { text-decoration: underline }` stood
   here, and it was the 1996 tell: the whole card is the press target, so underlining
   one word inside it advertises a second, smaller destination that does not exist —
   and the card already has a real second destination (the avatar, to the public
   page), which is the one thing the underline was not marking.

   THE ACKNOWLEDGEMENT MOVES TO THE SURFACE, which is where it belongs for a
   card-wide target and is the same family treatment the Catalog cards' sweep used:
   the ground answers the pointer, the ink does not move. K15's channel — hover owns
   the surface — and its corollary here is that the row needs its own hover rule,
   below, because without one this would be a removal and not a replacement. */
a.orgc-person-press:focus-visible { outline: 2px solid var(--accent, var(--text-primary)); outline-offset: 2px; }
.orgc-person-face { display: inline-flex; flex: none; border-radius: 50%; text-decoration: none; }
.orgc-person-face:focus-visible { outline: 2px solid var(--accent, var(--text-primary)); outline-offset: 2px; }
.orgc-who { display: flex; flex-direction: column; gap: 1px; line-height: 1.3; min-width: 0; }
.orgc-person-name { font-weight: var(--b-weight-semibold); color: var(--text-primary); }
.orgc-person-handle, .orgc-person-email, .orgc-person-when {
  font-size: var(--b-text-sm); color: var(--text-muted);
}
.orgc-person-roles { font-size: var(--b-text-sm); color: var(--text-muted); white-space: nowrap; }
/* ── THE STATUS PILL IS CONTENT-SIZED (TEN-1814, Mark's UAT r3) ───────────────
   Measured before the fix: 606px wide, `display: block`, on a word four characters
   long. Two things did that, and both are grid, so neither is visible in the markup:

   1. A `<span>` placed in a grid IS BLOCKIFIED — `display: inline` becomes `block` —
      so it stops sizing to its text and starts filling its box.
   2. The third track is `auto`, and an auto track ABSORBS the row's leftover space
      when `justify-content` is `normal`. So the box it fills is "everything after the
      identity and the roles", which on a wide card is most of the card.

   `justify-self: start` is the fix for both at once: the item stops stretching into
   its track and takes its own width, and the track is still free to be as wide as it
   likes. `inline-flex` so the pill can centre its own label, and `nowrap` so a
   two-word status never becomes two lines inside a pill.

   AND `display` IS NOT THE FIX, which the browser gate had to say twice. Blockification
   rewrites `inline-flex` to `flex` on a grid item just as it rewrites `inline` to
   `block`, so declaring an inline display here does not make the item inline and
   cannot stop it filling its track. It is declared for the label centring (and because
   it is `.ktpill`'s own spelling); `justify-self` is what does the work.

   THE TINY-TEXT TREATMENT IS `.ktpill`'s OWN, cited and not re-invented: `--b-text-xs`
   at semibold, which is what every other pill in this book wears. The chip was at
   `--b-text-sm` and normal weight — body-copy size, so it read as a word that had
   been given a border rather than as a pill.

   SCOPED TO THE GRID CHILD, deliberately. `.orgc-chip` is shared with the Events
   tab's rows and with the R2R held/billed chips, and those are flex children that
   never stretched. Widening this to `.orgc-chip` would be re-styling three families
   to fix one. */
.orgc-person-press > .orgc-chip {
  justify-self: start;
  display: inline-flex; align-items: center; white-space: nowrap;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
}
/* At phone width the fixed role track would squeeze the names to nothing, so the
   card stacks and the roles become a line of their own — the alignment it exists
   to give is only meaningful while there are columns to align. */
/* The three tracks need ~25rem before the acts column; below that the card stacks
   and the roles become a line of their own — the alignment exists to give columns,
   and there is no column to align when there is no room for three. */
@media (max-width: 900px) {
  .orgc-person-press { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .orgc-person-roles { white-space: normal; }
}
.orgc-chip-active { color: var(--role-presence); border-color: currentColor; }
.orgc-chip-invited { color: var(--lb); border-color: currentColor; }
.orgc-chip-declined { color: var(--text-muted); }
.orgc-chip-expired { color: var(--text-faint); }
/* THE ACTS COLUMN, and the "invited 1d ago" line now sits ABOVE it (TEN-1791).
   It used to be in the identity column under the handle, where it read as a fact
   about who somebody is; it is a fact about the INVITATION, and the two controls
   that answer it are directly beneath it here. A column rather than a row, so the
   line stacks over the buttons instead of pushing them sideways. */
.orgc-person-acts {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--b-space-1); margin-left: auto;
}
.orgc-person-acts > form, .orgc-person-acts > a { flex: none; }
.orgc-person-acts .orgc-person-when { white-space: nowrap; }
/* ── THE ACTS SIT ON ONE LINE (TEN-1814, Mark's UAT r3) ───────────────────────
   Measured before the fix: three children of `.orgc-person-acts` at three different
   `top` values — the "invited 3h ago" line, then Resend, then Withdraw, each on its
   own row.

   The cause is the column above, and the column is RIGHT: it exists so the "invited
   1d ago" line can stack over the acts it is about (TEN-1791), which is the earlier
   ruling and stays. What was wrong is that the two acts were siblings of that line
   rather than a cluster beside each other, so they inherited a stacking they had no
   reason to want. Resend and Withdraw do the same job to the same invitation; they
   belong on one line.

   `nowrap` AND NOT MERELY `row`: a row that wraps is the same defect at a narrower
   viewport, which is exactly where Mark would meet it again. The acts cluster is
   short — two ghost buttons, or one link — and `.orgc-person-acts` is `margin-left:
   auto` inside a wrapping flex row, so a card too narrow for the cluster wraps the
   whole acts column onto its own line rather than breaking the pair apart. */
.orgc-person-actrow {
  display: flex; flex-wrap: nowrap; align-items: center;
  gap: var(--b-space-1);
}
.orgc-person-actrow > form, .orgc-person-actrow > a { flex: none; }

/* ── THE STAFF ROW'S EDIT DIALOG (TEN-1791) ───────────────────────────────
   The terse user card is the panel's LEFT MARGIN, exactly as Mark specified: the
   avatar at the half-dollar size the console's person rows already use, with the
   name and handle beside it, so the dialog is unmistakably about somebody rather
   than a form carrying a name in its title.

   THE ROLE IS A CONTROL ON A MEMBER ROW (TEN-1957) and a fact everywhere else —
   see the fenced TEN-1957 block below. There is still deliberately no rule for a
   disabled select: a greyed dropdown would be K12's dead control and K15's
   unmounted popup at the same time. */
.orgc-staffedit { display: flex; flex-direction: column; gap: var(--b-space-3); }
.orgc-sehead { display: flex; align-items: flex-start; gap: var(--b-space-3); }
.orgc-seface { display: inline-flex; flex: none; border-radius: 50%; text-decoration: none; }
.orgc-seface:focus-visible { outline: 2px solid var(--accent, var(--text-primary)); outline-offset: 2px; }
.orgc-seav { flex: none; }
.orgc-sewho { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.orgc-sename { font-weight: var(--b-weight-bold); color: var(--text-primary); text-transform: none; letter-spacing: 0; }
.orgc-sehandle { font-size: var(--b-text-sm); color: var(--text-muted); }
.orgc-sefacts { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; }
.orgc-sex { margin-left: auto; }
.orgc-sesec { display: flex; flex-direction: column; gap: var(--b-space-1); }
.orgc-selbl {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); margin: 0;
}
.orgc-seroles { font-weight: var(--b-weight-semibold); color: var(--text-primary); margin: 0; }
.orgc-seacts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); }
/* The way out sits at the end, after the acts — the dialog's own Cancel rung. */
.orgc-seacts > .console-btn-secondary { margin-left: auto; }
/* "+ Invite" in the list header (TEN-1791): the list grammar's `[ + Add ]` rung,
   pushed to the far end of the bar past the count and the filters. */
.staff-invite-go { flex: none; margin-left: auto; }

/* TEN-1957 — the Staff dialog's role control. The K15 select and its primary
   save share one row (the select takes the room, the save keeps its own width),
   with the label above and the notes under — the same column the fact arm uses,
   so the dialog keeps its shape whichever arm it draws. */
.orgc-serole { display: flex; flex-direction: column; gap: var(--b-space-1); margin: 0; }
.orgc-serole-row { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; }
.orgc-serole-row > select { flex: 1 1 12rem; min-width: 0; }
.orgc-serole-row > button { flex: none; }
.orgc-serole .orgc-note { margin: 0; }
/* /TEN-1957 */

/* ── THE CLUB-PAGE COLUMN (TEN-1692, board S5) ────────────────────────────
   Two decisions drawn as two things: the org's toggle, and the person's pill.
   The pill has no control anywhere near it, which is the whole feature. */
/* `margin-left:auto` IS ROUND 3's "far right" (TEN-1796). The toggle used to sit
   in the flow right after the name block with its caption beside it; the caption is
   gone (it is the switch's `aria-label` now) and the control is pushed to the card's
   trailing edge, which is where a switch belongs on a card whose subject it is. */
.orgc-listing {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2);
  margin-left: auto;
}
.orgc-listing-form { display: flex; align-items: center; gap: var(--b-space-2); }
.orgc-toggle { display: inline-flex; align-items: center; gap: var(--b-space-2); cursor: pointer; }
/* R9's mechanical clause: THE CONTROL REFLECTS ITS OWN CHECKED STATE. The
   track and the label are drawn from the input's OWN `:checked` and never from
   a class the server rendered — a control whose appearance is a server-drawn
   class can only be redrawn by asking the server again, which is precisely the
   wave-2 defect the ruling was written about.

   AND IT CARRIES NO TRANSITION, WHICH IS A MEASURED DECISION RATHER THAN A
   MISSING FLOURISH (TEN-1692, found in the browser gate). The first cut eased
   the track's colour and the knob's transform over 120ms. On Chrome 152 that
   FROZE BOTH: with the box unchecked, `matches(':checked')` false, only the
   base rule matching and a full second elapsed, `getComputedStyle` still
   reported the checked green and the checked translate. Removing the
   transitions — and nothing else — made both correct, and it reproduced on the
   `background` shorthand and on `background-color` alike. The drawing only
   settled for a real click.

   A toggle whose drawn state can lag its real one is R9's own defect arriving
   by another route, and a 120ms ease is decoration. So the decoration goes and
   the correctness stays, with the measurement written down so nobody adds it
   back as a polish pass.

   The native box is visually hidden rather than removed, so it keeps its focus
   ring and its keyboard; the LABEL is the press target. */
.orgc-toggle input {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  clip-path: inset(50%); overflow: hidden;
}
.orgc-toggle-track {
  position: relative; flex: none; width: 34px; height: 20px;
  border-radius: var(--b-radius-full); background-color: var(--border-strong);
}
.orgc-toggle-knob {
  position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: var(--surface-raised);
}
.orgc-toggle input:checked ~ .orgc-toggle-track { background-color: var(--role-presence); }
.orgc-toggle input:checked ~ .orgc-toggle-track .orgc-toggle-knob { transform: translateX(14px); }
.orgc-toggle input:focus-visible ~ .orgc-toggle-track { outline: 2px solid var(--accent, currentColor); outline-offset: 2px; }
/* THE LABEL'S TWO RULES ARE GONE (TEN-1796). `.orgc-toggle-label` was the caption
   beside the switch — "On the public page" / "Not on the public page" — and round 3
   removed it from the markup: nine copies of one sentence down the middle of a list
   about that sentence. The string is the switch's `aria-label` now, so nothing was
   lost to a screen reader and there is nothing left for these rules to paint.
   Deleted rather than kept: a rule with no element is a rule the next reader
   assumes is doing something. R9's clause is unaffected and still measured — the
   TRACK and the KNOB draw the control's own `:checked`, which is the half that was
   ever load-bearing. */

/* The consent pill. K29: GREEN IS A FACT ABOUT SOMEBODY — here, that they said
   yes — and the waiting and refused states are drawn quietly rather than as
   warnings. A person's no is not a problem the console is helping to fix. */
.orgc-consent {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 2px 10px; white-space: nowrap;
}
.orgc-consent-accepted { color: var(--green-ink, var(--text-primary)); background: var(--green-a); }
.orgc-consent-waiting { color: var(--text-primary); background: var(--surface-overlay); }
.orgc-consent-declined { color: var(--text-muted); background: transparent; border: var(--b-border-w) solid var(--border-subtle); }
/* `.orgc-bio-open` IS GONE (TEN-1796) — the "Edit page bio ✎" link it styled was
   the smallest target on the widest element, and the CARD opens that editor now.
   Deleted rather than left as an unused class. */

/* ── BLOCK 1: THE CLUB PAGE ON THE PROFILE TAB (TEN-1796) ──────────────────────
   The masthead inside this card is the PUBLIC one, rendered by the public
   composer — so it needs no styling here at all, which is the whole claim of the
   block. Every rule below is the CONTROL layer the console adds around it.

   `.orgc-face` gives the rendered masthead room to sit flush: the card's own
   padding would otherwise inset a banner that is meant to run edge to edge, which
   is exactly the "looks nearly like the page" failure the block replaces. */
.orgc-face { position: relative; }
/* THE MOUNT'S WRAPPER (TEN-1813) — the console's own element, around the club
   page's bytes. It carries the gap the masthead used to carry itself
   (`.orgc-face .sfmast { margin-top }`), and that move is load-bearing rather
   than tidying: a margin on the FIRST CHILD collapses out through a wrapper with
   no padding or border, so the wrapper's top edge would have sat a space above
   the banner's and every zone measured from it would have been 12px low. On the
   wrapper, the two edges are the same edge.

   `position: relative` makes it the containing block for the control layer. It is
   the whole mechanism: the seam's markup is untouched and byte-identical to the
   public render, and the controls float OVER it from outside. */
.orgc-facemount { position: relative; margin-top: var(--b-space-3); }
/* THE BANNER'S GEOMETRIC TWIN, and it is a twin rather than a copy: the wrapper's
   full width, then `.sfmast-banner`'s OWN three rules — restated here because a
   sibling cannot inherit a box, but restated as RULES, so a banner that changes
   proportion changes this with it. (The numbers live once, in `.sfmast-banner`
   above; if they ever diverge the browser gate's chip clauses go red, because they
   measure the chip against the banner's live box rather than against these.)

   It draws nothing and eats no press. It exists to be the box the two zones are
   measured from — which is what makes the avatar zone expressible at all: the
   disc's position depends on the banner's HEIGHT, and inside this element that
   height is `100%`.

   `width: 100%` RATHER THAN `right: 0`, and that is not a style choice: with both
   insets set and a width of auto, a ratio box whose height hits `max-height` has
   the ratio drive its WIDTH back down instead — the guide came out 630px wide
   against the banner's 654 and both chips sat short of the banner's right edge.
   A definite width leaves the ratio with only the height to decide, which is the
   in-flow banner's own situation. Measured, in the browser gate. */
.orgc-faceguide {
  position: absolute; left: 0; top: 0; width: 100%;
  aspect-ratio: 3 / 1; min-height: 110px; max-height: 210px;
  pointer-events: none;
}
/* THE ZONES ARE THE PICTURES (TEN-1160, back on this surface with TEN-1813).
   Transparent buttons over the two image boxes, so the image IS the control and
   the chip in its corner is the same element rather than a badge beside it.

   NO `display` HERE, DELIBERATELY. These carry `.pic-open`, and its two gates —
   scriptless, and crop.js's stand-down class — are `display` rules. A `display` of
   our own at this specificity would out-weigh both and leave a dead chip over the
   picture in exactly the two cases they exist to cover. The chip is placed by
   position inside the button for the same reason. */
.orgc-facezone {
  position: absolute; pointer-events: auto;
  padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: var(--b-radius-md);
}
.orgc-facezone:focus-visible { outline: 2px solid var(--b-accent); outline-offset: 3px; }
.orgc-facezone-banner { inset: 0; }
/* THE DISC'S BOX, composed from the masthead's own arithmetic rather than measured
   off a screenshot: the banner's bottom (`100%` of the guide), plus `.sfmast`'s
   own row gap, less the half-disc `.sfmast-top` pulls itself up by. `left` is
   `.sfmast-top`'s own horizontal padding. The disc's size is `--sfmast-disc`,
   which this element INHERITS from the same declaration `.sfmast-full` uses — the
   wrapper is named in that rule, so there is one number and no copy of it.

   Second in the source order on purpose: it overlaps the banner zone by half a
   disc and must be the one that takes the press (ADR-0033). */
.orgc-facezone-avatar {
  left: var(--b-space-1);
  top: calc(100% + var(--b-space-2) - var(--sfmast-disc) / 2);
  width: var(--sfmast-disc); height: var(--sfmast-disc);
}
/* The chip itself — the account console's `.pic-pencil` treatment (K13 geometry,
   K14 "a pencil edits the thing it sits on"), in the same corner, at the same
   size, on the accent it uses. Not `.pic-pencil` itself: that rule is anchored to
   `.pic-pane`'s hover and belongs to the pane family. */
.orgc-facechip {
  position: absolute; right: 4px; bottom: 4px;
  width: 26px; height: 26px; border-radius: var(--b-radius-full);
  display: grid; place-items: center;
  background-color: var(--b-accent); color: var(--b-accent-fg);
  /* `--surface-page`, for the masthead disc's reason (TEN-1700's sweep):
     `--surface-base` is not a token this book defines anywhere. */
  box-shadow: 0 0 0 2px var(--surface-page);
  transition: filter 120ms ease;
}
.orgc-facezone:hover .orgc-facechip, .orgc-facezone:focus-visible .orgc-facechip { filter: brightness(1.08); }
/* Sized down from `.ktic`'s 18px to sit inside a 26px disc — the icon law's own
   allowance for a chip, the same one `.ktpcard-pinbox .ktic` takes. */
.orgc-facechip-mark { width: 15px; height: 15px; }
/* THE STRIP IS THE WORDS' PENCIL AND NOTHING ELSE NOW (TEN-1813). It sits
   top-right of the block, above the masthead, because it edits the CARD's words
   rather than anything under it — and the two controls that used to sit beside it,
   and did not belong on a strip at all, are chips on the images above.
   orgconsolepublicface.go states the mechanism. */
.orgc-facebar {
  display: flex; flex-wrap: wrap; gap: var(--b-space-2);
  justify-content: flex-end; margin-bottom: var(--b-space-1);
}
.orgc-pencil {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
  color: var(--text-primary); text-decoration: none;
  background: var(--surface-overlay);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-full);
  padding: 4px 12px; cursor: pointer;
}
.orgc-pencil:hover { border-color: var(--border-strong); }
.orgc-pencil:focus-visible { outline: 2px solid var(--b-accent); outline-offset: 2px; }
/* THE MARK IS A DRAWING (TEN-1796; K13 as amended 2026-09-19). It was a
   `&#9998;` character and this rule was its type size — the one thing a stroke
   glyph does not take. `.ktic` carries the weight, the ink and the 18px; at the
   14px these strips read at that is a shade large beside the word next to it, so
   the only rule left here is the size-down. */
.orgc-pencil-mark { width: 15px; height: 15px; }
/* `.orgc-facebar .pic-open { display: inline-flex }` IS GONE (TEN-1813), with the
   two picture pencils it was written for. Worth naming rather than deleting
   quietly: at (0,2,0) it out-weighed BOTH of `.pic-open`'s gates, so with scripting
   off — and when crop.js stood down — #601's two pencils were drawn anyway, onto
   dialogs that cannot work. The chips above set no `display` at all, which is why
   they cannot repeat it. */
/* The one fact the public render cannot carry: who may read the page. */
.orgc-face-audience {
  display: flex; flex-wrap: wrap; gap: var(--b-space-2);
  margin-top: var(--b-space-3); font-size: var(--b-text-sm);
}
/* The terse person card at the top of the page-bio dialog. Not pressable — it is
   there to say WHO this dialog is about, and a control inside it would be a way to
   lose what you have typed. */
.orgc-bioedit-who {
  display: flex; align-items: center; gap: var(--b-space-3);
  padding: var(--b-space-2) 0;
}

/* The editor takes the row's whole width when it opens — it replaces the
   listing cell, and the person's identity above it stays put. */
.orgc-bioedit {
  flex: 1 0 100%; display: flex; flex-direction: column; gap: var(--b-space-2);
  padding: var(--b-space-3); max-width: 42rem;
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.orgc-bioedit-title { margin: 0; font-weight: var(--b-weight-semibold); }
.orgc-membersbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--b-space-2); margin-bottom: var(--b-space-2);
}
.orgc-counts { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.orgc-filter { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; }
/* ── `.field` WAS A CLASS WITH NO RULE (TEN-1670) ─────────────────────────
   Mark, v0.1.218, second half: the org console's controls "are not styled in
   keeping with our dropdown/selector styling rules". They were not styled at
   all. `class="field"` is on six controls across the console's two templates
   and this stylesheet had no `.field` selector anywhere in it, so every one of
   them rendered as the operating system's own box — which is K11a's named
   violation ("a form control never shows the browser's own chrome") sitting
   inside the surface K15's own paragraph excuses from a re-skin, because that
   paragraph excused the console's "own field chrome" and its own field chrome
   turned out to be nothing.

   THE BOX IS `.tokpick-field`'s, TO THE TOKEN. The invite control on this very
   form is a token picker now, so the field beside it and the field it replaced
   have to be the same object drawn twice — same ground, same border, same
   radius, same padding — or the swap that made one of them a picker would read
   as two kits on one line.

   THE CARET IS `.ss-grid-field select`'s, verbatim and for its reason
   (TEN-1437, Mark's "no raw HTML controls. Ever."): two gradients rather than a
   data-URI arrow, so it rides `currentColor` and follows the theme. K15 owns
   the POPUP and K11a owns the TRIGGER; the console's dropdowns have mounted
   K15's panel since TEN-1634, and this is the other half of the same control.

   NOT ON A `multiple` SELECT. `<select multiple>` renders as a list box with no
   disclosure arrow of its own, so a painted chevron there would be a picture of
   an affordance the control does not have — and the one multiple on this form is
   the picker's own source, which `select[data-tokpick]` already dresses and
   `.tokpick-source` then takes out of sight. */
.field {
  font: inherit; box-sizing: border-box;
  padding: var(--b-space-1) var(--b-space-2);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong);
  border-radius: var(--b-radius-sm);
}
select.field:not([multiple]) {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: calc(var(--b-space-2) + 1.1rem);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    right calc(var(--b-space-2) + 0.35rem) center,
    right var(--b-space-2) center;
  background-size: 0.35rem 0.35rem, 0.35rem 0.35rem;
  background-repeat: no-repeat;
}
.field:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 1px; }
.orgc-induct { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; margin-top: var(--b-space-2); }
/* THE INVITEE FIELD IS A TOKEN PICKER NOW (TEN-1670). Two rules and one of them
   is load-bearing.

   THE WIDTH: the enhanced field and the select it stands in for get the same
   flexible basis, so the swap is not a jump — the TEN-1088 flash rule at this
   mount, and the same pair `.clsorg` already declares. `.tokpick` grows with what
   is in it, which on a single-token mount is one chip.

   THE FALLBACK APPEARS WHENEVER THE PICKER DID NOT ARM, which is wider than
   `[data-scriptless-only]`'s own `@media not (scripting: enabled)` and is the
   reason this rule exists at all. app.css already refuses to hide a tokpick's
   source select for exactly this case ("scripting enabled + this gathering
   failing = a form with no control at all"), and that refusal works because
   every other mount's select carries OPTIONS. This one does not: its candidates
   are all the directory's, so an unenhanced select here is an empty box and the
   text field beside it is the whole control. `:has(.tokpick)` asks the honest
   question — did picker.js actually build? — so a failed gathering shows both
   halves and a working page shows one. It out-specifies the attribute selector,
   which is what lets it reveal rather than fight it. */
.orgc-induct .tokpick, .orgc-invitee { flex: 1 1 16rem; min-width: 12rem; }
.orgc-invitee-new { flex: 1 1 16rem; flex-direction: column; gap: 4px; }
.orgc-induct:not(:has(.tokpick)) .orgc-invitee-new { display: flex; }

/* "+ INVITE TO FOLLOW" INSIDE ITS DIALOG (TEN-1784). `.orgc-induct` is a
   horizontal row because the Staff tab's invite sits in a bar beside a button;
   this mount is a K18 rung-(b) dialog whose fields STACK, so the one rule it
   needs is the axis. Kept as an override rather than as a second form class
   because the two really are the same control family — picker, scriptless
   fallback, submit — and forking the base rule would leave two sets of widths
   free to drift.

   THE NOTE IS THE ONLY NEW PIECE. It is a labelled textarea with its hint under
   it, which is the label grammar every other stacked field on these screens
   already wears. */
.ktmodal .orgc-netinvite { flex-direction: column; align-items: stretch; }
.ktmodal .orgc-netinvite .tokpick,
.ktmodal .orgc-netinvite .orgc-invitee,
.ktmodal .orgc-netinvite .orgc-invitee-new { flex: 0 0 auto; min-width: 0; }
.orgc-invite-note { display: flex; flex-direction: column; gap: 4px; }
.orgc-invite-note textarea { resize: vertical; min-height: 3.5rem; }
.orgc-invite-note .orgc-note { margin: 0; }

.orgc-paging { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; margin-top: var(--b-space-2); }

/* ══ THE NETWORK SURFACES (TEN-1787, boards N2/N3) ═══════════════════════════
   TEN-1760 shipped this tab's markup and NO COVER RULES WITH IT. Every class it
   invented — `.orgc-netbar`, `.orgc-netrow`, `.orgc-netpress`, `.orgc-pills`,
   `.orgc-qvacts`, `.orgc-money`, `.orgc-amber` — resolved to nothing, so the
   whole surface rendered as the browser's own stacked inline boxes: a bare
   count on the page background, chips floating under it, four text lines per
   person, and an amber "honestly absent" block that was neither amber nor a
   block. That is Mark's "anemic", and it is a stylesheet gap rather than a
   markup one, which is why most of this pass lives here.

   EVERY VALUE BELOW IS A TOKEN. Boards N2/N3 were drawn against the dark
   palette's own primitives (#3A3A3E is --s2, #00E878 is --green, and so on), so
   the anatomy and the proportions are the board's and the pigment resolves
   through the role layer — which is what lets the light and high-contrast
   themes re-tint all of it for free.

   TWO PLACES WHERE THE COVER OVERRULES THE BOARD, both deliberate:
     · the "since" and the billing gloss are drawn on --text-muted, not the
       --t3 the board picks. tokens.css's three-tier note is explicit that the
       faint tier means DISABLED and that small-but-readable meta text may not
       borrow it, and "since Jun 12" is something a reader reads.
     · there is no ⌕ glyph in the search field. This book has no search-icon
       idiom anywhere, and minting one for a single field would be inventing
       iconography on a fidelity pass. The field's own box carries the affordance.
   ────────────────────────────────────────────────────────────────────────── */

/* THE CARD OWNS THE SURFACE (board N2 item 2). Header band, list, footer — one
   bordered object, with nothing left standing on the page's own ground. */
/* `overflow: clip`, NOT `hidden`, and the difference is load-bearing (TEN-1791).
   Both clip the rounded corners; only `hidden` creates a SCROLL CONTAINER, and a
   scroll container between a sticky element and the viewport is what stops the
   frozen header below from ever freezing. The card needs the clipping and must not
   become a scroll port, which is exactly what `clip` means. */
.orgc-netcard {
  display: flex; flex-direction: column;
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-lg); overflow: clip;
}
/* THE FROZEN LIST HEADER (TEN-1791, Mark's item). The chips and the search stay
   put while the rows move under them — they are controls ON the rows, so losing
   them off the top of the viewport is losing the way to narrow the thing you are
   looking at.

   IT IS THE ONLY STICKY THING ON THIS TAB, which is K34's corollary: the census
   and the invite act float above the surface and do NOT stick, because two things
   pinned at two heights is two headers. It needs its own background — a
   transparent sticky header shows the rows sliding through it. */
.orgc-netcard-head {
  display: flex; flex-direction: column; gap: var(--b-space-3);
  padding: var(--b-space-3) var(--b-space-4);
  border-bottom: var(--b-border-w) solid var(--border-strong);
  position: sticky; top: 0; z-index: 2;
  background: var(--surface-raised);
}
/* K34 — THE PAGE-LEVEL CENSUS AND PRIMARY ACT, ABOVE THE LIST SURFACE AND
   ALIGNED TO ITS EDGES (TEN-1791; the rung is minted in
   docs/sop/sop_design_grammar_rulings.md in the same PR).

   `.orgc-netcard-top` and `.orgc-netcard-title` are GONE. The list card carried a
   bordered header row with a second "Network" title and the count beside it,
   under a page the shell had already titled "Network" — the same word twice with
   a box drawn between them. The title is deleted (K34 clause 3) and the count and
   the invite float above the surface (clause 1).

   THE ALIGNMENT IS THE WHOLE RELATIONSHIP (clause 2). With the border gone, the
   only thing left saying "this row belongs to that list" is that their edges
   line up — so the float takes no side padding of its own and sits flush with
   the card below it.

   IT IS NOT STICKY, deliberately: the header that survives a scroll is the
   in-surface one, and two things pinned at two heights is two headers. */
.orgc-netfloat {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--b-space-3);
  margin-bottom: var(--b-space-2);
}
.orgc-netcount { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.orgc-netcount strong { color: var(--text-primary); font-weight: var(--b-weight-semibold); }
.orgc-netinvite { margin-left: auto; }
/* The chips and the search live INSIDE the header band — board N2's second rung
   and the reason nothing on this tab floats any more. */
.orgc-netcard-band { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); }
.orgc-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.orgc-netsearch { display: flex; align-items: center; gap: var(--b-space-2); flex: 1 1 14rem; min-width: 11rem; }
.orgc-netq { flex: 1 1 auto; max-width: 360px; height: 2.25rem; }
/* THE SUBMIT RETIRES ONLY WHERE THE SCRIPT ACTUALLY BOUND (`.orgnet-live` is
   added by listfilter.js, never by the server) AND ONLY WHERE THE SERVER SAID IT
   MAY (`data-orgnet-live`, a single page). Both halves are R9 clause 3: a roll
   that pages holds matches this box cannot see, and a page whose script never
   ran must keep the one arm it has. */
.orgnet-live .orgnet-filter-go { display: none; }

/* THE ROW: two text lines and a right-hand column (board N2 item 1). */
.orgc-netlist { display: flex; flex-direction: column; }
.orgc-netrow {
  display: flex; align-items: center; gap: var(--b-space-2);
  padding-right: var(--b-space-3);
  transition: background var(--b-duration-fast) var(--b-ease);
}
/* Hairline separators, and none under the last row — the card's footer border
   is already there. */
.orgc-netrow + .orgc-netrow { border-top: var(--b-border-w) solid var(--border-subtle); }
/* HOVER OWNS THE SURFACE, not the anchor inside it: the whole row is the press
   target, so the whole row lights. */
.orgc-netrow:hover { background: var(--surface-overlay); }
/* The same specificity trap `.staff-row`, `.msg-row` and `.net-row` each hit
   before it: `.orgc-netrow` is display:flex, which beats the UA's `[hidden]`
   rule outright. Without this BOTH filter arms are broken — the live one hides
   nothing, and so does the scriptless GET whose marks are the same attribute. */
.orgc-netrow[hidden] { display: none; }
.orgc-netpress {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: var(--b-space-3);
  padding: 11px var(--b-space-2) 11px var(--b-space-4);
  color: var(--text-primary); text-decoration: none;
}
.orgc-netav { width: 38px; height: 38px; font-size: 0.85rem; }
/* COLUMN ONE IS A FIXED WIDTH ACROSS EVERY CARD (TEN-1791, Mark's item). It used
   to be `flex: 1 1 auto` on the identity, so each row sized its own first column
   off whatever the name happened to be and the two columns to its right started
   at a different x on every card — three columns that were not columns. A basis
   with no grow is what makes them line up; `min-width: 0` keeps a long name
   ellipsising inside its track instead of pushing the track wider. */
.orgc-netid {
  flex: 0 0 16rem; min-width: 0;
  display: flex; align-items: center; gap: var(--b-space-2);
}
/* THE MIDDLE COLUMN — the following list, folded to two facts with the rest
   counted in a tooltip (orgNetFollowLine). */
.orgc-netmid { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.orgc-netwho { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; line-height: 1.3; }
/* Name and handle INLINE on line one — one identity read as one line.
 *
 * BOTH LINES TRUNCATE RATHER THAN WRAP, and that is what makes board N2's
 * two-line row true AT EVERY WIDTH rather than only at the board's 1160. The
 * console renders inside a 720px column, where "Follows you · Sunrise Fran ·
 * Follows a class" wraps to a third line — and a list whose rows are two lines
 * tall except when they are three has exactly the ragged rhythm this pass was
 * opened about. Nothing is lost by truncating: the whole row presses into the
 * quickview, which prints the same facts with room for them. */
.orgc-netname, .orgc-netwhy { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.orgc-netname { font-weight: var(--b-weight-semibold); color: var(--text-primary); }
.orgc-nethandle { font-weight: var(--b-weight-normal); font-size: var(--b-text-sm); color: var(--text-muted); }
.orgc-netwhy { font-size: var(--b-text-sm); color: var(--text-muted); }
/* THE RIGHT-HAND COLUMN IS ONE LINE. It does not wrap for the reason above: a
   "since" that drops under the pills is a third line by another route. The
   identity column carries `min-width: 0` and truncates, so it is the side that
   gives way when the row is narrow — which is right, because the pills and the
   since are short and fixed and the name is not. */
.orgc-netmeta {
  flex: 0 0 auto; display: flex; flex-wrap: nowrap; align-items: center;
  justify-content: flex-end; gap: var(--b-space-2);
}
.orgc-pills { margin: 0; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; }
.orgc-netmeta .orgc-pills { flex-wrap: nowrap; }
.orgc-netsince { font-size: var(--b-text-xs); color: var(--text-muted); white-space: nowrap; }
/* ON A PHONE THE COLUMN GOES UNDER, and this is the one place the two-line rule
   gives way on purpose. A fixed right-hand column and a truncating identity
   column are the right trade at console width; at 380px the column is wider than
   the card, so the identity collapsed to nothing and the row scrolled sideways —
   measured, not guessed. A row that hides whose row it is has stopped being a
   row, so the pills and the since take a line of their own, indented to the
   text's own left edge. The desktop census is unaffected: it is a claim about
   the console's width, which is where board N2 was drawn. */
@media (max-width: 560px) {
  .orgc-netpress { flex-wrap: wrap; }
  /* The fixed first column becomes the whole width when there is no room for
     three of them — the alignment it exists to give is only meaningful while
     there are columns to align. */
  .orgc-netid { flex: 1 1 100%; }
  .orgc-netmid { flex: 1 1 100%; }
  .orgc-netmeta { flex: 1 1 100%; flex-wrap: wrap; justify-content: flex-start; }
  .orgc-netmeta .orgc-pills { flex-wrap: wrap; justify-content: flex-start; }
}

/* THE ENTITLEMENT PILL (board N2 item 5) — the kit's pill weights, in this
   console's three readings. It is its own family rather than a restyle of
   `.orgc-chip-held` / `-billed` because those two are ALSO the Staff tab's R2R
   chips, whose colours are a documented decision: what a member HOLDS is green
   and who COLLECTS is neutral. The amber here is the roster's org-billed
   holding — billing provenance, a different fact — so it gets its own class
   rather than repainting theirs. */
.orgc-pill {
  display: inline-flex; align-items: center; gap: 4px;
  border-radius: var(--b-radius-full); padding: 2px 9px;
  border: var(--b-border-w) solid transparent;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  white-space: nowrap;
}
.orgc-pill-live { background: var(--green-a); border-color: var(--green-line); color: var(--role-presence); }
.orgc-pill-direct { background: var(--amber-a); border-color: var(--amber-wash); color: var(--role-attention); }
/* THE HANDSHAKE (TEN-1791, Mark's item) — the org-billed mark, replacing the
   "Paid directly · " words that used to eat the front of the pill. Stroke line
   art in `currentColor` per K13, so it takes the amber the pill already sets and
   follows the theme with it; sized down from `.ktic`'s 18px because it rides
   inside a chip rather than beside a label.

   AN AUTOMATED-PAYMENT PILL HAS NO RULE HERE AT ALL, which is the item's other
   half: being on a card is the norm, and a mark on the norm distinguishes
   nothing. */
.orgc-pill .orgc-hand { width: 13px; height: 13px; display: inline-block; vertical-align: -2px; }
.orgc-pill-direct { display: inline-flex; align-items: center; gap: 4px; }
.orgc-pill-word { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.orgc-pill-more { background: var(--surface-overlay); color: var(--text-muted); font-weight: var(--b-weight-normal); }
/* The remainder rides the pill it is about (TEN-1775), one weight quieter than
   the name it decorates. */
.orgc-pill-bal { font-weight: var(--b-weight-normal); }

/* THE OPERATOR'S OWN ✉ — a square icon target, not a console button wearing a
   glyph. `.console-btn`'s uppercase tracking and text padding are undone here
   because there is no word in it. Dead rather than absent is board N2's call and
   orgNetRowMsgWhy argues it: the tooltip supplies the "why" a disabled control
   otherwise leaves unanswered, and a fixed slot keeps the column aligned. */
.orgc-msgform { margin: 0; display: inline-flex; }
.orgc-msg {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  background: transparent; color: var(--text-muted);
  border-radius: var(--b-radius-sm);
  font-size: 14px; letter-spacing: 0;
}
.orgc-msg:hover { background: var(--surface-overlay); color: var(--text-primary); border-color: var(--text-muted); }
.orgc-msg-dead { opacity: 0.35; cursor: default; }
.orgc-msg-dead:hover { background: transparent; color: var(--text-muted); border-color: var(--border-strong); }

/* The card's own footers: the count line, the live no-match line, and the
   honest-absence notices that ride inside it. */
.orgc-netfoot {
  margin: 0; padding: var(--b-space-2) var(--b-space-4);
  border-top: var(--b-border-w) solid var(--border-subtle);
  font-size: var(--b-text-sm); color: var(--text-muted);
}
.orgc-netnote { margin: var(--b-space-3) var(--b-space-4) 0; }
.orgc-netempty { display: flex; flex-direction: column; align-items: flex-start; gap: var(--b-space-2); padding: var(--b-space-4); }

/* ── HONESTLY ABSENT, DRAWN AS A BLOCK (board N3 item 5) ──────────────────
   Dashed amber is this book's shape for staged-or-not-yet (K12's shape rule,
   the same dashed ring the place card's unclaimed pin wears). It was bare text
   on every surface that uses it — the quickview's attendance block, the
   navigator's two amber tabs, the roster's unavailable sentence and its cap
   notice — so one rule fixes four screens. Declared AFTER `.orgc-note` on
   purpose: the money-down line composes both, and this is the half that should
   win. */
.orgc-amber {
  margin: 0; padding: var(--b-space-3) var(--b-space-3);
  border: var(--b-border-w) dashed var(--role-attention);
  border-radius: var(--b-radius-md);
  background: var(--amber-a); color: var(--role-attention);
  font-size: var(--b-text-sm); line-height: 1.5;
}

/* ── THE CUSTOMER QUICKVIEW (board N3) ────────────────────────────────────
   The kit's dialog, sized and padded to the board: 560 wide because the body is
   three read blocks rather than two stacked fields (`.ktpcard`'s own argument),
   r16, and the deeper shadow a layer over a list wants. `max-width: 100%` still
   comes from `.ktmodal-panel`, so a phone gets the screen exactly as every other
   dialog does.

   THE COMPOUND SELECTORS ARE LOAD-BEARING, not tidiness. `.ktmodal-panel` and
   `.ktmodal-title` are declared ~2000 lines BELOW this block, and a single class
   ties with a single class on specificity — so `.orgc-qv` alone lost the width,
   the radius, the padding and the shadow to the base rule, and `.orgc-qvname`
   lost its size and its case to the dialog title's uppercase. Measured in the
   browser before it was written down. */
.ktmodal-panel.orgc-qv {
  width: 560px; border-radius: var(--b-radius-lg);
  padding: 20px 22px; box-shadow: var(--b-shadow-lg);
}
.ktmodal-head.orgc-qvhead { align-items: flex-start; gap: var(--b-space-3); }
/* TWICE THE SIZE (TEN-1791, Mark's item): a modal that leads with a face has
   room for one, and 52px was still the list's own thumbnail. Linked to the public
   page, so the picture is a way to the person rather than a decoration. */
.orgc-qvav { width: 104px; height: 104px; font-size: 2.1rem; }
.orgc-qvface { display: inline-flex; flex: none; border-radius: 50%; text-decoration: none; }
.orgc-qvface:focus-visible { outline: 2px solid var(--accent, var(--text-primary)); outline-offset: 2px; }
.orgc-qvwho { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* The name in the heading face at reading size — NOT `.ktmodal-title`'s small
   uppercase, which is a form header's voice and made a person's modal read as a
   settings panel. The class stays on the element because it is still what
   `aria-labelledby` points at. */
.ktmodal-title.orgc-qvname {
  font-family: var(--b-font-heading); text-transform: none; letter-spacing: 0;
  font-size: var(--b-text-xl); font-weight: 700; color: var(--text-primary);
  overflow-wrap: anywhere;
}
.orgc-qvfacts { font-size: var(--b-text-sm); color: var(--text-muted); line-height: 1.4; overflow-wrap: anywhere; }
.link--dismiss.orgc-qvx { flex: none; font-size: 16px; }
/* The label rung over a bordered sub-block (board N3 item 2). */
.orgc-qvsec { display: flex; flex-direction: column; gap: 6px; }
.orgc-qvlbl {
  margin: 0; font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.05em;
  font-size: 11px; font-weight: 700; color: var(--text-muted);
}
.orgc-qvlbl-sub {
  font-family: var(--b-font-body); text-transform: none; letter-spacing: 0;
  font-weight: var(--b-weight-normal); color: var(--text-muted);
}
.orgc-qvblk {
  display: flex; flex-direction: column; gap: var(--b-space-2);
  background: var(--surface-overlay);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
  padding: var(--b-space-3);
}
/* PER-PLAN ROWS AND TRANSACTION ROWS (board N3 items 3 and 4): what it is on the
   left, what it costs or what is left of it on the right. */
.orgc-planrow, .orgc-moneyrow {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--b-space-3); font-size: var(--b-text-sm);
}
.orgc-qvblk .orgc-moneyrow {
  padding-bottom: 5px;
  border-bottom: var(--b-border-w) solid var(--border-subtle);
}
.orgc-qvblk .orgc-moneyrow:last-of-type { padding-bottom: 0; border-bottom: none; }
.orgc-money { display: flex; flex-direction: column; gap: 6px; }
.orgc-planwhat, .orgc-money-what { min-width: 0; overflow-wrap: anywhere; color: var(--text-primary); }
.orgc-planname { font-weight: var(--b-weight-semibold); }
/* K29: green is a fact about a STANDING, so it is spent on the holding that is
   actually live and on nothing else. */
.orgc-planstate { font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold); color: var(--role-presence); }
.orgc-planstate-off { color: var(--text-muted); font-weight: var(--b-weight-normal); }
.orgc-planmark { font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold); color: var(--role-attention); }
.orgc-planfig, .orgc-money-fig { color: var(--text-muted); white-space: nowrap; }
.orgc-qvmore { margin: 4px 0 0; font-size: var(--b-text-sm); }
/* The three exits: outline, ghost, and the ✉ pushed right. */
.orgc-qvacts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); }
.orgc-qvghost { background: transparent; color: var(--text-muted); }
.orgc-qvghost:hover { background: var(--surface-overlay); color: var(--text-primary); }
.orgc-qvmsg { margin-left: auto; }

/* ── THE NAVIGATOR'S HEAD (board N4) ──────────────────────────────────────
   NOT part of the N2/N3 gap list, and here because it is the address the
   quickview's first exit opens: `.orgc-navhead`, `.orgc-navacts` and
   `.console-person-av-lg` were the same never-styled classes as everything
   above, so a reader pressing "Open full view ›" left a mended surface for an
   unmended one. Four rules, the same tokens, no new ideas. */
.orgc-navhead { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-3); }
.console-person-av-lg { width: 52px; height: 52px; font-size: 1.05rem; }
.orgc-navacts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); margin-left: auto; }
.orgc-navtabs { margin-bottom: var(--b-space-3); }
.orgc-soon { font-size: var(--b-text-xs); color: var(--role-attention); }

/* "Your Organization Identities" card head (TEN-1416): the title on the left,
   the "+ New" opener on the right — same shape as .lists-roster-head and
   .msg-inbox-head, kept as its own rule because each surface owns its card. */
.orgs-head { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2); }
.orgs-head .acct-stub-title { margin: 0; }

/* ── THE ORG IDENTITY CARDS (TEN-1895) ─────────────────────────────────────
   Each of a person's organizations as R6's org card at its card rung: the
   squared face (`av-org`, K47 + TEN-1869) and the `.console-person` identity
   block the Clubs page draws, with the meta on the right.

   ONE CARD, TWO PRESS TARGETS, LAYERED RATHER THAN NESTED. The org's name is the
   card's press — a <button> in a POST form, or a link to the console when the
   org is already the focus — and its ::after is stretched over the whole card,
   so a press anywhere on the card is a press on the name. The FACE is a link to
   the club's public profile and sits one layer above that overlay, so pressing
   it goes to the profile and never activates anything. An anchor inside a
   button is invalid HTML; siblings layered by z-index are the honest shape. */
.orgid-list { display: flex; flex-direction: column; gap: var(--b-space-2); margin: var(--b-space-3) 0; }
.orgid-card {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--b-space-2) var(--b-space-3);
  padding: var(--b-space-3);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
}
.orgid-card .console-person { min-width: 0; }
.orgid-face { position: relative; z-index: 1; }
.orgid-pressform { display: inline; margin: 0; }
/* A card with no press (not yet claimed, or an active member's) names its org in
   the SAME face as a pressable one — the list reads as one list, and only the
   pointer and the underline say which cards act. */
.orgid-press, .orgid-name {
  display: inline; padding: 0; border: none; background: none;
  color: var(--text-primary); font: inherit; font-weight: var(--b-weight-semibold);
  text-align: left; text-decoration: none;
}
.orgid-press { cursor: pointer; }
.orgid-press::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
}
/* HOVER AND FOCUS ARE THE CARD'S EDGE, NEVER ITS GROUND. The active card's
   state lives in its GROUND (the identity tint), and a hover that repainted the
   background would erase the one thing that says which identity you are acting
   as — K32 §3's lesson: a state mark must survive the pointer. So the hover
   takes the border and the name's underline, and the focus ring is drawn on the
   stretched overlay, i.e. round the whole card the press actually covers. */
.orgid-card:has(.orgid-press:hover) { border-color: var(--border-strong); }
.orgid-press:hover { text-decoration: underline; }
.orgid-press:focus-visible { outline: none; }
.orgid-press:focus-visible::after { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.orgid-meta { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); }
/* THE ACTIVE CARD'S FANFARE — K52, PROPOSED (TEN-1895, the PM's design pick).
   The selected-entity row in the IDENTITY family's voice: the calendar's booked
   PROMINENCE (a filled 3px rail plus a tinted ground, K32's rung) spoken in the
   identity role rather than in K29's green — because what it says is "you are
   acting AS this organization", which is who you are, not what you hold. The
   "Acting as" word rides beside it so the state is never carried by colour alone
   (K12's pairing rule). The rail is restated under hover so the pointer cannot
   take it. */
.orgid-card-active,
.orgid-card-active:has(.orgid-press:hover) {
  border-left: 3px solid var(--role-identity);
  background: var(--role-identity-soft);
}
.orgid-acting {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--role-identity);
}

/* ── The authoring world (TEN-1305, ADR-0035) ────────────────────────────
   The build editor reuses the organizer plane's frame (.mgwrap, .seclbl,
   .flash) on purpose — it IS the organizer plane, one door further in, and a
   second visual language for the same person doing the same job would just be
   two things to keep in sync. What it adds is the tree: a breadcrumb trail, a
   sub-event list, and the prescription's indent ladder. */
.bldcrumbs { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--b-space-1); margin-bottom: var(--b-space-3); }
.bldcrumbs .crumb { color: var(--text-muted); font-size: 13px; text-decoration: none; }
.bldcrumbs a.crumb:hover { color: var(--text-primary); text-decoration: underline; }
.bldcrumbs .crumb.here { color: var(--text-primary); font-weight: 600; }
.crumbsep { color: var(--border-subtle); }
/* The header is a two-column row (TEN-1428): the event's identity on the left,
   its one PRIMARY action top-right. It wraps to two rows on a narrow screen —
   at which point Publish sits under the title rather than beside it, which is
   the right answer when there is no room to be beside it. */
.bldhead {
  display: flex; flex-wrap: wrap; align-items: flex-start;
  justify-content: space-between; gap: var(--b-space-3);
  margin-bottom: var(--b-space-4);
}
.bldtitle { flex: 1 1 20rem; min-width: 0; }
.bldname { font-family: var(--b-font-heading); font-size: 24px; margin: 0; }
/* Who made this and who changed it last, directly under the name it describes.
   It used to be the tail of the footer, as far from the title as the page
   allowed. */
.bldwho { color: var(--text-muted); font-size: 12px; margin: var(--b-space-1) 0 0; }
.bldmeta { color: var(--text-muted); margin: var(--b-space-1) 0 0; }
.blddate, .bldseries { color: var(--text-muted); font-size: 13px; margin: var(--b-space-1) 0 0; }
.bldnone { color: var(--text-muted); font-style: italic; }
.bldfacts {
  display: grid; grid-template-columns: max-content 1fr;
  gap: var(--b-space-1) var(--b-space-4); margin: 0 0 var(--b-space-4);
}
.bldfacts dt { color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
.bldfacts dd { margin: 0; }
.bldkids { display: flex; flex-direction: column; gap: var(--b-space-2); margin-bottom: var(--b-space-4); }
.bldkid {
  display: grid; grid-template-columns: 1fr max-content;
  gap: 0 var(--b-space-3); padding: var(--b-space-3);
  background: var(--surface-raised); border-radius: var(--b-radius-md);
  color: inherit; text-decoration: none;
}
.bldkid:hover { outline: var(--b-border-w) solid var(--border-subtle); }
.bldkid .kidname { font-weight: 600; }
.bldkid .kidwhen { color: var(--text-muted); font-size: 13px; text-align: right; }
.bldkid .kidsum, .bldkid .kidseries { color: var(--text-muted); font-size: 13px; }
/* The prescription ladder. Indent is a CLASS, not an inline style: the module
   says how deep a row sits, the cover says what a step is worth. */
.bldrx { list-style: none; margin: 0 0 var(--b-space-3); padding: 0; }
.bldrow { padding: var(--b-space-1) 0; }
.bldrow.rxgroup {
  font-family: var(--b-font-heading); font-weight: 700;
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted);
}
.rxd0 { padding-left: 0; }
.rxd1 { padding-left: var(--b-space-4); }
.rxd2 { padding-left: calc(var(--b-space-4) * 2); }
.rxd3 { padding-left: calc(var(--b-space-4) * 3); }
.bldraw { color: var(--text-muted); font-size: 13px; margin: 0 0 var(--b-space-4); }
.rawlbl {
  font-family: var(--b-font-heading); font-weight: 700;
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  margin-right: var(--b-space-2);
}
.bldfoot { color: var(--text-muted); font-size: 12px; border-top: var(--b-border-w) solid var(--border-subtle); padding-top: var(--b-space-2); }
.blddel { color: var(--b-danger, #dc2626); }
/* The structure verbs (TEN-1305 PR 2). Every control here is a plain form
   POST — no disclosure toggles, no client confirm(), nothing that needs
   scripting — so the editor is complete with JavaScript off. Legacy put the
   division and movement forms behind Alpine `x-cloak`, which meant they were
   not merely un-animated without scripting, they were UNREACHABLE. */
/* One row, one Save (TEN-1428). This IS the form now — it used to be a wrapper
   around three of them, one per property, each with its own button. */
.bldedit, .bldadd, .bldnew { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; }
.bldedit { margin-bottom: var(--b-space-4); }
.bldedit .gbtn { margin-left: auto; }
.bldrename { flex: 1 1 18rem; }
.bldadd, .bldnew { margin-bottom: var(--b-space-4); }
.bldnew .bldin { flex: 1 1 16rem; }
.bldin {
  font: inherit; background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-2);
}
.bldin-name { flex: 1 1 14rem; min-width: 12rem; }
.bldin-num { width: 6rem; }
.gbtn {
  font: inherit; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); cursor: pointer;
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-sm);
  padding: var(--b-space-2) var(--b-space-3);
}
.gbtn:hover { filter: brightness(1.08); }
.bldacts { margin-bottom: var(--b-space-4); }
/* A division chip and its removal. The × is a LINK, not a submit: it opens the
   confirmation screen that states what the removal takes with it. */
.dchip {
  display: inline-flex; align-items: center; gap: var(--b-space-1);
  background: var(--surface-overlay); border-radius: var(--b-radius-full);
  padding: 1px var(--b-space-2); margin-right: var(--b-space-1);
  font-size: var(--b-text-sm);
}
.chipx { color: var(--text-muted); text-decoration: none; font-weight: 700; }
.chipx:hover { color: var(--b-danger, #dc2626); }
.bldnote { color: var(--text-muted); font-size: 12px; margin: 0 0 var(--b-space-4); }
.bldimpact { margin: 0 0 var(--b-space-2); }
/* ── Rx programming (TEN-1305 PR 3) ──────────────────────────────────────
   Every disclosure here is a native <details>: the editor still ships ZERO
   JavaScript, so there is nothing to gate behind `(scripting: enabled)` —
   TEN-1238's rule is satisfied by not having the problem. Legacy put all of
   this behind Alpine `x-cloak`, which made the row editors and the override
   forms unreachable with scripting off rather than merely un-animated. */
.blddivs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-1); margin-bottom: var(--b-space-3); }
.divlbl {
  font-family: var(--b-font-heading); font-weight: 700;
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-muted); margin-right: var(--b-space-1);
}
.divchip {
  background: var(--surface-overlay); border-radius: var(--b-radius-full);
  padding: 1px var(--b-space-3); font-size: var(--b-text-sm);
  color: var(--text-muted); text-decoration: none;
}
.divchip:hover { color: var(--text-primary); }
.divchip.on { background: var(--surface-raised); color: var(--text-primary); font-weight: 600; }
/* The summary IS the row: it reads as the prescription line until it is
   opened, so the marker is dropped and the pointer does the telling. */
.mvrow > summary.mvline { cursor: pointer; list-style: none; padding: var(--b-space-1) 0; }
.mvrow > summary.mvline::-webkit-details-marker { display: none; }
.mvrow > summary.mvline:hover { text-decoration: underline; text-decoration-color: var(--border-subtle); }
.mvrow[open] > summary.mvline { font-weight: 600; }
/* A movement a division drops still renders — struck through rather than
   removed, because a row that is not there offers no way to un-drop it. */
.mvline.mvdropped { text-decoration: line-through; color: var(--text-muted); }
.mvvaries { color: var(--text-muted); font-size: 12px; font-weight: 400; }
.mvform {
  display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center;
  margin: var(--b-space-2) 0 var(--b-space-3);
}
.mvform .bldin { flex: 1 1 14rem; }
.mvform .ovin { flex: 0 0 auto; width: 6rem; }
.mvdrop { display: inline-flex; align-items: center; gap: var(--b-space-1); font-size: var(--b-text-sm); color: var(--text-muted); }
.mvdel { color: var(--b-danger, #dc2626); text-decoration: none; }
.shbar { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; margin-bottom: var(--b-space-2); }
.shbar .shin { flex: 1 1 24rem; }
.shhint { color: var(--text-muted); font-size: 12px; margin: 0 0 var(--b-space-4); }
/* ── Recurrence + publish (TEN-1305 PR 4) ────────────────────────────────
   Legacy gated the every-N box, the weekday chips and the until date behind
   Alpine `x-show`/`x-cloak`. With scripting off `[x-cloak]{display:none}`
   never lifts, so the chips were not un-animated, they were UNREACHABLE — a
   weekly rule could only ever repeat on the start's own weekday. Every
   control here renders always; the legend says which frequency it applies
   to, which costs a line of text and buys a form that works everywhere. */
.recur { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: center; margin-bottom: var(--b-space-2); }
.recur-iv, .recur-until { display: inline-flex; align-items: center; gap: var(--b-space-1); font-size: var(--b-text-sm); color: var(--text-muted); }
.recur-wds { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-1); border: 0; margin: 0; padding: 0; }
.recur-wds legend { float: left; margin-right: var(--b-space-2); padding: 0; }
.wdchip {
  display: inline-flex; align-items: center; gap: 2px;
  background: var(--surface-overlay); border-radius: var(--b-radius-full);
  padding: 1px var(--b-space-2); font-size: var(--b-text-sm); color: var(--text-muted);
}
.wdchip:has(input:checked) { background: var(--surface-raised); color: var(--text-primary); font-weight: 600; }
/* Publishing is consequential rather than destructive, so the affordance is
   a plain link to a review screen — no danger colour, because nothing is
   lost, and no bare submit, because plenty is at stake. */
/* Publish, top-right and right-aligned, with its state said in tiny text under
   it (TEN-1428). It was a full-width row at the FOOT of the editor wearing the
   same neutral .rbtn as Cancel — the most consequential control on the screen,
   styled as the least. */
.bldpub {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--b-space-1); margin: 0; text-align: right;
}
.bldpubnote { color: var(--text-muted); font-size: 12px; margin: 0; max-width: 22rem; }
/* A Publish that would do nothing is rendered as a NON-control: published, and
   nothing changed since, so there is nothing to publish. This surface ships
   zero JavaScript and the affordance is a link (publishing goes through a
   server-rendered review, ADR-0035 §7), so there is no <button disabled> to
   lean on — the honest scriptless answer is an element that simply is not a
   link, and says why underneath. */
.rbtn.off { opacity: 0.5; cursor: default; }
.rbtn.off:hover { filter: none; }

/* ── Manage · run & finalize (TEN-1306) ──────────────────────────────────
   The manage surface is a READINESS DASHBOARD, so its colour is the
   STOP-LIGHT convention: green = ready/resolved, yellow = incomplete or
   at-risk, red = a problem. Green is the calm target here and red shouts —
   the inverse of the live-action gradient the judge app uses, and the two
   palettes never cross. Forcing the judge palette onto an ops dashboard was
   the repeated mistake (legacy s5 §6a). */
.mgehead { display: flex; flex-wrap: wrap; gap: var(--b-space-3); justify-content: space-between; align-items: flex-start; margin-bottom: var(--b-space-3); }
.mgetitle { font-family: var(--b-font-heading); font-size: var(--b-text-lg); font-weight: 700; }
.mgesum { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); font-size: var(--b-text-sm); color: var(--text-muted); }
.mgelife { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-2); }
.bell { border-radius: var(--b-radius-full); padding: 1px var(--b-space-2); font-size: var(--b-text-xs); font-weight: 600; }
.bell.ok { color: var(--good); background: var(--good-a); }
.bell.warn { color: var(--warn); background: var(--amber-a); }
.bell.blocked { color: var(--bad-fg); background: var(--bad-a); }
.pill { border-radius: var(--b-radius-full); padding: 1px var(--b-space-2); font-size: var(--b-text-xs); font-weight: 700; letter-spacing: 0.06em; }
.pill.live { color: var(--ink-on-accent); background: var(--warn); }
.pill.recent { color: var(--good); background: var(--good-a); }
.pill.upcoming { color: var(--text-muted); background: var(--surface-overlay); }
.mgbtn { font: inherit; font-size: var(--b-text-sm); padding: var(--b-space-1) var(--b-space-3); border-radius: var(--b-radius-md); border: var(--b-border-w) solid var(--border-strong); background: var(--surface-raised); color: var(--text-primary); cursor: pointer; }
.mgbtn:hover { background: var(--hover); }
/* The un-start safety valve is present but must not compete with the
   primary lifecycle action — muted, never hidden (s5 §6c). */
.mgbtn.quiet { border-color: var(--border-subtle); color: var(--text-muted); }
/* Schedule ⇄ Tree are CO-EQUAL: the toggle is a peer pair, not a primary
   with a secondary (s5 §2 Axis A). */
.mglens { display: flex; flex-wrap: wrap; gap: var(--b-space-2); margin-bottom: var(--b-space-3); }
.mglens .lens { font-size: var(--b-text-sm); padding: var(--b-space-1) var(--b-space-3); border-radius: var(--b-radius-full); border: var(--b-border-w) solid var(--border-subtle); color: var(--text-muted); text-decoration: none; }
.mglens .lens.on { color: var(--text-primary); background: var(--surface-raised); border-color: var(--border-strong); font-weight: 600; }
.mglens .lens.jump { border-style: dashed; }
.conflicts { border: var(--b-border-w) solid var(--role-danger); background: var(--bad-a); border-radius: var(--b-radius-md); padding: var(--b-space-3); margin: var(--b-space-3) 0; display: flex; flex-direction: column; gap: 6px; }
.conflicthead { font-weight: 700; color: var(--bad-fg); font-size: var(--b-text-sm); }
.conflictline { font-size: var(--b-text-sm); color: var(--text-muted); }
/* "Can't be checked yet" is a DIFFERENT answer from "clear", so it renders
   whether or not anything clashed — a quiet line, never a silent omission. */
.conflictnote { font-size: var(--b-text-xs); color: var(--text-faint); margin: var(--b-space-2) 0; }
/* Venue is a FIXED column so the eye scans it straight down; the aligned
   table is accepted here because scannability is the whole job, and the
   time-slot header keeps it from reading as a cold spreadsheet (s5 §4a). */
.schedhead, .actrow { display: grid; grid-template-columns: 14px 1.6fr 1fr 0.6fr; align-items: center; gap: var(--b-space-2); }
.schedhead { font-size: var(--b-text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-faint); padding: 0 var(--b-space-3); }
.slot { display: flex; align-items: center; gap: var(--b-space-2); margin: var(--b-space-3) 0 var(--b-space-2); }
.slot .t { font-family: var(--b-font-mono); font-size: var(--b-text-sm); color: var(--text-muted); }
.slot .rule { flex: 1; height: 1px; background: var(--border-subtle); }
.actrow { background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); padding: var(--b-space-2) var(--b-space-3); margin-bottom: var(--b-space-2); text-decoration: none; color: var(--text-primary); }
a.actrow:hover { background: var(--hover); }
.adot { width: 8px; height: 8px; border-radius: 50%; background: var(--mut); }
.adot.ok { background: var(--good); }
.ainfo { display: flex; flex-direction: column; }
.anm { font-weight: var(--b-weight-semibold); }
.apath { font-size: var(--b-text-xs); color: var(--text-faint); }
.aven { font-size: var(--b-text-sm); color: var(--text-muted); font-family: var(--b-font-mono); }
.ast { font-size: var(--b-text-xs); color: var(--text-faint); }
.ast.ok { color: var(--good); }
.mgempty { color: var(--text-muted); }
.mgtree .node.d1 { margin-left: var(--b-space-5); }
.mgtree .node.d2 { margin-left: calc(var(--b-space-5) * 2); }
.mgtree .node.d3 { margin-left: calc(var(--b-space-5) * 3); }
.mgtree .node .rt { margin-left: auto; display: flex; align-items: center; gap: var(--b-space-2); }
.mgtree .node .bullet { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--text-faint); }
.mgtreenote { font-size: var(--b-text-xs); color: var(--text-faint); margin: 0 0 var(--b-space-3); }
/* The hub is the delegation line made into a page. Sections STACK rather
   than collapse-on-complete, and the look-ahead ones stay VISIBLE with a
   quiet tag — never greyed out of existence — so the organizer can validate
   the whole arc forward (s5 §4c). */
.hubhead { display: flex; flex-wrap: wrap; gap: var(--b-space-3); justify-content: space-between; align-items: flex-start; margin-bottom: var(--b-space-3); }
.hubactivity { font-family: var(--b-font-heading); font-size: var(--b-text-lg); font-weight: 700; }
.hubmeta { display: flex; gap: 6px; font-size: var(--b-text-sm); color: var(--text-muted); }
.hublinks { display: flex; flex-wrap: wrap; gap: var(--b-space-2); }
.hublink { font-size: var(--b-text-sm); padding: var(--b-space-1) var(--b-space-3); border-radius: var(--b-radius-md); border: var(--b-border-w) solid var(--border-subtle); background: var(--surface-raised); color: var(--text-primary); text-decoration: none; cursor: pointer; font-family: inherit; }
.hublink:hover { background: var(--hover); }
.hubsec { border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); padding: var(--b-space-3); margin-bottom: var(--b-space-3); }
.hubsec.dim { opacity: 0.72; }
.hubsech { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--b-space-2); margin-bottom: var(--b-space-2); }
.secn { font-family: var(--b-font-mono); font-weight: 700; color: var(--text-muted); }
.sect { font-family: var(--b-font-heading); font-weight: 700; }
.sechint, .ahead { font-size: var(--b-text-xs); color: var(--text-faint); }
/* One L-shaped scan: status → item → what's wrong → what to do. */
.ckrow { display: grid; grid-template-columns: 10px 1fr 1fr 1.4fr; align-items: center; gap: var(--b-space-2); padding: var(--b-space-2) 0; border-bottom: var(--b-border-w) solid var(--border-subtle); }
.ckrow:last-of-type { border-bottom: 0; }
.ckdot { width: 8px; height: 8px; border-radius: 50%; background: var(--mut); }
.ckdot.ok { background: var(--good); }
.ckdot.warn { background: var(--warn); }
.ckdot.block { background: var(--bad); }
.cklabel { font-weight: var(--b-weight-semibold); font-size: var(--b-text-sm); }
.ckval { font-size: var(--b-text-sm); color: var(--text-muted); }
.cknote { font-size: var(--b-text-xs); color: var(--text-faint); }
.schedform { display: flex; flex-wrap: wrap; gap: var(--b-space-2); align-items: flex-end; margin: var(--b-space-3) 0 var(--b-space-2); padding: var(--b-space-3); border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); }
.schedlbl { display: flex; flex-direction: column; gap: 3px; font-size: var(--b-text-xs); color: var(--text-faint); }
.schedlbl input { padding: 6px; border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm); background: transparent; color: var(--text-primary); font: inherit; }
.hubstart { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-3); margin-top: var(--b-space-2); }
.hubstarted { font-size: var(--b-text-sm); color: var(--good); font-weight: 600; }
/* Don't advertise the glass: the exceptional path is a small, quiet note off
   to the side, found by looking rather than met by accident (s5 §6c). */
.hubglass { font-size: var(--b-text-xs); color: var(--text-faint); }
.hubbody { font-size: var(--b-text-sm); color: var(--text-muted); }
.hubclose { margin-top: var(--b-space-2); }
.deleg { display: flex; align-items: center; gap: var(--b-space-2); font-size: var(--b-text-xs); color: var(--text-faint); margin: var(--b-space-3) 0; }
.deleg .ln { flex: 1; height: 1px; background: var(--border-subtle); }
@media (max-width: 640px) {
  .schedhead { display: none; }
  .actrow { grid-template-columns: 10px 1fr; row-gap: 4px; }
  .actrow .aven, .actrow .ast { grid-column: 2; }
  .ckrow { grid-template-columns: 10px 1fr; row-gap: 2px; }
  .ckrow .ckval, .ckrow .cknote { grid-column: 2; }
}

/* ── Manage · the seal (TEN-1306) ────────────────────────────────────────
   The trigger stays QUIET and the weight sits on the confirmation behind
   it (s5 §6c: "the accountability copy lives on the confirmation, not the
   trigger"), so seal and reopen borrow the ordinary lifecycle control
   rather than shouting in red from a lens head somebody is only reading. */
a.mgbtn { display: inline-block; text-decoration: none; }
.hubclose { display: flex; flex-wrap: wrap; align-items: center; gap: var(--b-space-3); }
.hubby { margin-left: 6px; color: var(--text-muted); font-weight: 400; }

/* ── Manage · the payment record (TEN-1311) ──────────────────────────────
   The entry list's pay pill is now a LINK into that order's record, and it
   has to keep reading as the pill it already was — `.status.paid` still wins
   the colour over the global `a` rule, so all this adds is the affordance,
   on hover, in the pill's own colour. Legacy opened an htmx sheet from the
   same pill; there is no sheet here, so the pill is simply a link. */
a.status { cursor: pointer; border-bottom: var(--b-border-w) dotted transparent; }
a.status:hover { border-bottom-color: currentColor; }
/* ══ THE ACTION AND TEXT ROLE GRAMMAR (K12, TEN-1565 kit v2) ═════════════
   The button family CONVENTIONS.md has always specified and this cover never
   had. Before this block the book carried .rbtn.primary, .paybtn.primary,
   .b-btn-primary and a .ghost — four spellings of two ideas — and the wave-2
   kit was about to mint a fifth (.ktbtn-primary). It does not: it wears these.

   THE ROLE IS THE NAME. A surface has ONE advancing act, and that act is
   .btn--primary whatever colour a theme paints it. .btn--generative (make-new)
   and .btn--critical (irreversible extreme-confirm) are declared in the kit and
   left undeclared here until a surface on this cover needs one — a rule
   nothing wears is dead CSS, not a grammar. Disabled is a STATE and never a
   role: [disabled] / [aria-disabled] dims whichever role the control already
   wears, so a disabled primary is still recognisably the primary act.

   FOUR TIERS, NOT THREE (Mark's ruling 2026-09-07; K12 amended). This block
   first shipped collapsing every non-primary act into .btn--ghost, on the
   argument that a .btn--secondary would be "a second spelling of a rung the
   system already has". It is not: this book had already been drawing the
   middle rung for months as .console-btn-secondary — the "+ Invite" and
   "+ Register" buttons on the Entries screen (TEN-1443's taxonomy). VISUAL
   WEIGHT ENCODES COMMITMENT, so a glance ranks a page before reading does:

     PRIMARY    filled accent. The ONE significant act per surface region.
     SECONDARY  ACCENT OUTLINE + SOFT ACCENT FILL — accent border, accent ink,
                --role-primary-soft ground (punch r4; r2's "no fill" is
                reversed, see below). A supportive call-to-action: a real act,
                deliberately quieter than centre stage. The entry to a flow;
                the extra acts a page offers beside its primary. Plural
                allowed, rationed.
     GHOST      NEUTRAL outline. The null/granular tier: Cancel, Never mind,
                Back, control-level adjustments. It commits nothing, so it
                carries NO accent — that is the whole distinction from
                secondary, and the reason it is not "the quiet blue one".
     (below ghost, and not a .btn at all: icon acts on a row — the .link
     family — message someone, open a profile, dismiss. See .link below.)

   REVERSAL, 2026-09-07 (punch r4): the secondary's idle state gets its SOFT
   ACCENT FILL BACK. Punch r2 took the fill off on the argument that an accent
   spent on the edge and the ink is what separates this rung from the primary;
   live, an unfilled accent outline on a dark card read as an outlined ghost
   and lost the rung. The separation is carried by the WEIGHT of the accent —
   solid fill vs 10% tint — not by its absence, and hover now steps up to
   --role-primary-soft-strong from the tint rather than up from nothing.

   The test is not prominence, it is commitment: DOES PRESSING THIS BEGIN
   SOMETHING? A view-first card's Edit gate does, so an Edit gate is SECONDARY
   (the amendment's largest re-map, because R2 puts one on nearly every
   settings block). Cancel does not, so Cancel stays ghost.

   DANGER MIRRORS THE LADDER. .btn--danger is FILLED and is the destructive
   COMMIT, inside a confirmation; .btn--danger-quiet is the danger outline and
   is the ENTRY to a destructive flow — the wireframes' "Archive…" button.
   Filled commits, outline opens, in either hue. (.btn--danger was an outline
   before this amendment; it is now the fill, and the outline it used to be is
   .btn--danger-quiet. Every current wearer in this book is an entry, so every
   one of them moved.)

   THE BUTTON FACE IS THE CARD'S: uppercase Barlow Condensed 700, 14px,
   letter-spacing .1em (design-system/components/button — "Uppercase Barlow
   Condensed"). The book had drifted to sentence-case Barlow and Mark ruled
   the drift closed (2026-09-07): every button family moves back in the same
   breath — .btn here, and the legacy .rbtn / .b-btn / .paybtn / .console-btn
   get the same face where they are defined, geometry untouched. A theme may
   repaint a button; only the button card re-typesets one.

   INTERACTION STATE LIVES ONCE, on .btn, per ADR-0032: a control inherits
   normal/hover/pressed/focus/disabled by wearing the class, and :active fires
   the pressed ack before any network work.

   Legacy note: .rbtn / .paybtn / .b-btn are NOT folded in here — they carry
   their own geometry on shipped screens and re-pointing them is the
   design-system wiring pass, not this ticket. New surfaces use .btn.
   ────────────────────────────────────────────────────────────────────────── */
:root {
  /* Shape tokens — role-and-shape names, no pigment, no feature. One control
     height for every field and button on a surface: a form whose boxes
     disagree by 2px reads as broken long before anybody can say why. */
  --control-h: 44px;
  --control-r: 8px;
  --card-r: 10px;
}

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--control-h); padding: 0 20px; border-radius: var(--control-r);
  font-family: var(--b-font-heading); font-size: 14px; font-weight: 700; line-height: 1;
  text-transform: uppercase; letter-spacing: 0.1em;
  border: var(--b-border-w) solid transparent; background: transparent; color: inherit;
  text-decoration: none; cursor: pointer; box-sizing: border-box;
}
.btn:focus-visible { outline: var(--b-ring-w) solid var(--b-ring); outline-offset: 2px; }
.btn:active { filter: brightness(0.92); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.5; cursor: default; pointer-events: none; }

.btn--primary { background: var(--role-primary); border-color: var(--role-primary); color: var(--ink-on-primary); }
.btn--primary:hover { filter: brightness(1.08); color: var(--ink-on-primary); }
/* SECONDARY — the accent outline WITH THE SOFT FILL BACK (Mark's punch r4,
   reversing punch r2's no-fill call after seeing both live). The tier still
   spends the accent on the edge (--role-primary, the FILL weight, so it holds
   up beside the primary's block of colour) and on the label
   (--role-primary-ink, the TEXT weight — --role-primary as ink sits near the
   4.5:1 floor on a dark card), and it now also carries --role-primary-soft as
   its resting GROUND. The tint is what makes the rung read as a real act at a
   glance rather than as an outlined ghost: on a dark card an unfilled accent
   outline all but disappears next to the filled primary, which is a rung too
   far down for "a supportive call-to-action".
   HOVER STEPS UP FROM IT rather than arriving from nothing:
   --role-primary-soft-strong is the same tint at roughly double weight, so the
   resting state and the hover are one ladder in every theme instead of two
   different ideas. Hover restates border and ink for the TEN-1091 reason the
   primary does: a state rule that only changes the ground inherits whatever
   the next rule happens to set. */
.btn--secondary { background: var(--role-primary-soft); border-color: var(--role-primary); color: var(--role-primary-ink); }
.btn--secondary:hover { background: var(--role-primary-soft-strong); border-color: var(--role-primary); color: var(--role-primary-ink); }
.btn--ghost { background: var(--surface-raised); border-color: var(--border-strong); color: var(--text-muted); }
.btn--ghost:hover { background: var(--hover); border-color: var(--text-faint); color: var(--text-primary); }
/* DANGER, both rungs. Filled = the destructive COMMIT (in a confirmation);
   outline = the ENTRY to a destructive flow. --ink-on-color is the book's
   existing answer for ink on a filled danger ground (.notif-badge). */
.btn--danger { background: var(--role-danger); border-color: var(--role-danger); color: var(--ink-on-color); }
.btn--danger:hover { background: var(--role-danger); border-color: var(--role-danger); color: var(--ink-on-color); filter: brightness(1.08); }
.btn--danger-quiet { background: transparent; border-color: var(--role-danger); color: var(--role-danger); }
.btn--danger-quiet:hover { background: var(--bad-a); border-color: var(--role-danger); color: var(--role-danger); }

/* The inline acts. A link is an act too, and it gets the same treatment: the
   role names the intent, the theme names the pigment. */
.link { color: var(--role-primary-ink); font-weight: 600; text-decoration: none; }
.link:hover { text-decoration: underline; }
.link--danger { color: var(--role-danger); font-size: 13px; font-weight: 600; text-decoration: none; }
.link--danger:hover { text-decoration: underline; }
/* --text-muted, not --text-faint (K12's amended text ladder, 2026-09-07). A
   dismiss is a LIVE ACT — the quietest rung of the ladder, but a rung. Drawn
   in the tier that now means "switched off", it was a control claiming to be
   disabled; quiet is a size and a weight, not the disabled colour. */
.link--dismiss { color: var(--text-muted); text-decoration: none; font-size: 14px; line-height: 1; }
.link--dismiss:hover { color: var(--text-primary); }

/* ══ THE WAVE-2 EVENT DESIGN KIT (TEN-1565, epic TEN-1563) ═══════════════
   The `kt` block below is the wave-2 event kit's skin: the plate family in
   modules/sportscore/plates/kit-*.plate.html, plus the organizers-only kit
   page (/kit) that renders every one of them in every state.

   KIT V2 — IT NO LONGER CARRIES ITS OWN PALETTE. Wave 1 of this ticket pinned
   the wireframes' zinc/#2563eb language as a --kt-* block so Mark could review
   the pixels; his ruling on seeing it was that the wireframes are STRUCTURE,
   FLOW AND GRAMMAR and were never comps. So every colour and face below now
   resolves through the role layer in tokens.css, which means /kit re-colours
   with the cover's own theme selector (dark / light / high-contrast) exactly
   like every other screen in this book. What survived from the wireframes is
   what they were actually deciding: geometry, spacing, states, copy, and the
   eleven K-laws.

   Four role tokens and one token family were ADDED to carry things the role
   layer could not yet say — --role-primary-ink / --role-primary-soft /
   --role-staged and the --zone-* pair. Their definitions and the reason each
   one exists are in tokens.css; they are mirrored into sportscore-go's
   tokens.css and the Foundations "Role tokens" card per the design-system
   lockstep table.
   ────────────────────────────────────────────────────────────────────────── */

/* ── Kit primitives ─────────────────────────────────────────────────────── */
.ktcard { border: 1px solid var(--border-subtle); border-radius: var(--card-r); background: var(--surface-raised); padding: 20px; display: flex; flex-direction: column; gap: 14px; color: var(--text-primary); }
.ktsub { color: var(--text-muted); font-size: 13px; font-weight: 400; }
.ktstrong { font-weight: 700; color: var(--text-primary); }
.ktlabel { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 15px; font-weight: 700; color: var(--text-primary); }
.ktnote { font-size: 13px; line-height: 1.5; color: var(--text-muted); border-top: 1px solid var(--border-subtle); padding-top: 12px; margin: 0; }
.kteyebrow { font-family: var(--b-font-heading); text-transform: uppercase; font-size: 12px; font-weight: 700; letter-spacing: 0.08em; color: var(--text-muted); }
/* The visually-hidden label. Every kit control is a REAL form element, so
   every one of them needs a name a screen reader can read even where the
   design shows only a glyph. */
.ktsr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ── THE ICON (K13, Mark's standing ruling 2026-09-07) ─────────────────────
   ICONS ARE THICK LINE ART: stroke-based, flat, unfilled, monochrome, drawn
   on a 24-unit grid at a 1.8 stroke with round caps and joins. NO CARTOON
   ICONS ANYWHERE — the who's-affected footer shipped an emoji bell (🔔) and
   the chooser shipped four text glyphs standing in for drawings, one of
   which (☰) read as a hamburger menu on a card that is not a menu.

   THE STROKE LAW LIVES HERE, ONCE, so every kit icon is the same weight and
   an icon added later cannot arrive at a different one. `currentColor` is
   the whole theming story: an icon inherits the ink of whatever it sits in,
   so it re-colours with the theme, with a hover, and with a disabled state
   without a rule of its own.

   EMOJI ARE ALLOWED ONLY WHERE THE CONTENT GENUINELY IS AN EMOJI — a
   reaction, a message someone typed. An emoji standing in for a UI icon is
   the thing this rule forbids. Typographic marks (✓ ✕ − ›) are not icons and
   are unaffected: they are characters set in the page's own face. */
.ktic { width: 18px; height: 18px; flex-shrink: 0; display: block; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.ktic-lg { width: 28px; height: 28px; }

/* THE SECOND ICON FAMILY, AND IT IS BOUNDED (TEN-1796; K13 as amended by Mark's
   2026-09-19 correction). A BRAND MARK is not a picture of one of our controls —
   it is the mark of the site a link a club typed itself points at, and it is
   somebody else's drawing. Simple Icons ships each as one FILLED path, so the
   stroke law above cannot draw it: restroking a logo produces our own wrong
   rendering of a trademark, which is a worse outcome than a second class.
   `currentColor` is kept, so a mark still re-colours with the theme, with a hover
   and with a disabled state exactly as `.ktic` does — the theming story is the
   same story; only the fill/stroke half differs. Never used for one of OUR acts:
   the moment a brand mark labels a button of ours it has become a UI icon and
   `.ktic` is the only law for those. Provenance: modules/cmcauth/assets/
   brandmarks.LICENSE. */
.ktbrand { width: 18px; height: 18px; flex-shrink: 0; display: block; fill: currentColor; stroke: none; }

/* The kit's buttons wear the .btn role grammar above; the only kit-local rule
   is the sentence card's smaller control. */
.ktsent .btn { height: 38px; padding: 0 16px; font-size: 14px; flex-shrink: 0; }

/* The field. One height everywhere — --control-h, shared with .btn. */
.ktinput { height: var(--control-h); border: 1px solid var(--border-strong); border-radius: var(--control-r); background: var(--surface-raised); color: var(--text-primary); display: inline-flex; align-items: center; gap: 6px; padding: 0 14px; font-size: 15px; box-sizing: border-box; }
.ktinput input, .ktinput select { border: 0; background: transparent; font: inherit; color: var(--text-primary); padding: 0; min-width: 0; width: 100%; }
.ktinput input:focus, .ktinput select:focus { outline: none; }
/* NO NATIVE CHROME on a select (Mark's law, K11 amendment): the OS arrow goes
   (appearance:none) and a K13 stroke chevron takes its place, drawn via mask so
   it takes the themed ink. A fully custom listbox is a later JS enhancement, not
   a law.
   THIS COMMENT USED TO CLAIM color-scheme HANDLED THE POPUP. It does not, and
   that claim is what shipped the round-11 finding — see the option rule below. */
.ktinput select { appearance: none; -webkit-appearance: none; }
/* THE POPUP'S ROWS NEED A GROUND OF THEIR OWN (TEN-1633, Mark's UAT round 11:
   the options render "unstyled raw html / not properly legible").
   MEASURED BEFORE IT WAS CODED, in a dark browsergate drive on this book's own
   booking golden: the root computes `color-scheme: dark`, the select computes
   `appearance: none`, and both the select AND its options compute
   `background-color: rgba(0, 0, 0, 0)` — transparent — with `color` at
   --text-muted, rgb(168,168,178). So the diagnosis is not "the options inherit no
   colours": they inherit the INK fine and have no GROUND at all.
   WHY color-scheme DOES NOT SAVE IT. `color-scheme: dark` governs what the
   platform draws ITSELF. `appearance: none` is the author taking the select out of
   that path, and Blink then paints the popup from the select's own computed
   background — which here is transparent on purpose, because the visible box is
   the `.ktinput` wrapper behind it. Transparent resolves against the platform's
   default canvas, so the popup arrives white on Windows with muted-grey rows on
   it. The select BODY looks right throughout, which is exactly the shape of the
   report.
   WRITTEN FOR EVERY SELECT ON THE COVER, not just this one. The other selects
   here escape only because each happens to carry an opaque box of its own; "a
   popup row needs a ground" is a fact about the control, not about this card, and
   one rule is cheaper than the audit that would have to be repeated every time
   somebody adds a select. The ink is --text-primary rather than the muted ink the
   closed body wears: a row you are choosing between is being read, and the muted
   tier is for a value already settled (K12's ladder).
   NOT MEASURED, and named as the fiction it is: the OS-composited popup's own
   pixels. A browser gate can read what the option computes and cannot screenshot
   a native popup, so what is pinned is the ground, the ink and the contrast
   between them. */
option { background-color: var(--surface-raised); color: var(--text-primary); }
/* THE CHEVRON IS AN OVERLAY, NOT A NEIGHBOUR (TEN-1631, Mark's UAT round 10:
   "the chevron is dead — the menu opens only from the current value").
   It used to be a flex ITEM beside the select, which took 12px of the control's
   width away from the select's own box and left an 18px strip inside the border
   that was not the select at all. `pointer-events: none` on the glyph does not
   help there: with the chevron transparent to the pointer the hit lands on the
   LABEL, and clicking a label whose control is a <select> focuses it in Chrome
   without opening the popup. So the strip looked like the part of the control
   you press and was the one part that did nothing.
   Measured before it was coded, in this book's own stylesheet: label content box
   ended at x=413, the select's box at x=395. Now the select fills the content
   box and reserves room with padding, and the glyph is drawn on top of it and
   stays transparent to the pointer — ONE hit target, chevron included.
   The inset is a custom property because the two skins below carry different
   horizontal padding, and a chevron that ignored it would sit off its own edge. */
.ktinput:has(> select) { position: relative; --ktchev-inset: 14px; }
.ktinput:has(> select) > select { padding-right: 18px; }
.ktinput:has(> select)::after {
  content: ""; width: 12px; height: 12px; flex-shrink: 0; pointer-events: none;
  position: absolute; right: var(--ktchev-inset); top: 50%; margin-top: -6px;
  background-color: var(--text-muted);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.ktinput:focus-within { border-color: var(--role-primary); box-shadow: 0 0 0 3px var(--role-primary-soft); }
.ktinput-sm { width: 92px; }
.ktinput-md { width: 158px; }
.ktinput-time { width: 116px; }
/* `.ktinput-dur` is GONE (TEN-1610): the weekly-time row's bare minutes box was
   its only wearer, and that box is the kit's duration control now. A width class
   with nothing to size is the shim this book does not keep. */
.ktinput-money .ktmoney-sym { color: var(--text-muted); font-weight: 600; }
.ktinput-unit { color: var(--text-muted); font-size: 13px; white-space: nowrap; }

/* THE TIME PICKER (TEN-1582). A real text box with a dropdown of the common
   slots under it — never <input type="time">, which is OS chrome all the way
   down and exactly what K11a forbids. The panel is a CUSTOM LISTBOX rather
   than a <select>, which K11a explicitly permits ("a fully custom listbox is a
   permissible later enhancement"), and here it earns itself: 73 quarter-hours
   in an OS popup is the chrome the law is about, drawn 73 times.
   The wrapper positions the panel; the field itself keeps .ktinput's skin. */
.kttime { position: relative; display: inline-flex; }
.kttime-field { width: 116px; }
.kttime-menu {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 20;
  width: 100%; min-width: 116px; max-height: 264px; overflow-y: auto;
  border: 1px solid var(--border-strong); border-radius: var(--control-r);
  background: var(--surface-raised); box-shadow: var(--b-shadow-md);
  padding: 6px 0; display: flex; flex-direction: column;
}
.kttime-opt { padding: 7px 14px; font-size: 14px; color: var(--text-muted); cursor: pointer; white-space: nowrap; }
.kttime-opt:hover { background: var(--hover); color: var(--text-primary); }
/* The keyboard's highlight, and it CARRIES `aria-selected` — kitcontrols.js
   moves this class and that attribute together, so this is the row the platform
   is announcing.
   IT SURVIVES THE POINTER (TEN-1702 sweep; the ruling is at `.ktlist-chip-on`).
   A real break: `.kttime-opt:hover` outranks it and paints the neutral `--hover`
   wash, so arrowing to a slot and then moving the pointer over that same slot
   erased the highlight from the row the screen reader was still calling
   selected. Nothing else moved it back — the pointer does not move `mark()`.
   NOTE FOR THE NEXT READER: the comment that stood here claimed this drawing
   and `:hover`'s were "the same drawing on purpose". They are not and were not
   — `--role-primary-soft` with weight 600 against `--hover` at inherited weight
   — so the claim is removed rather than left to be trusted. Whether the two
   channels should be unified (K15's "hover and keyboard focus are ONE
   treatment") is a design call above this sweep's pay grade and is left as it
   is found; what is fixed here is only that the announced row keeps its
   drawing. */
.kttime-opt-on, .kttime-opt-on:hover { background: var(--role-primary-soft); color: var(--text-primary); font-weight: 600; }

.ktchip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; border-radius: 999px; padding: 3px 10px; background: var(--surface-overlay); color: var(--text-muted); }
.ktchip-tag { border-radius: 6px; background: var(--role-primary-soft); color: var(--role-primary-ink); font-size: 13.5px; padding: 5px 10px; }
.ktchip-x { color: var(--role-primary-ink); text-decoration: none; font-size: 11px; }
.ktchip-warn { background: var(--amber-a); border: 1px solid var(--role-attention); color: var(--role-attention); }
.ktchips { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: var(--control-h); border: 1px solid var(--border-strong); border-radius: var(--control-r); background: var(--surface-raised); padding: 8px 10px; box-sizing: border-box; }
.ktchips-inline { border: 0; padding: 0; min-height: 0; background: transparent; gap: 6px; }
.ktchips-in { border: 0; background: transparent; font: inherit; font-size: 13.5px; color: var(--text-primary); flex: 1; min-width: 90px; }
.ktchips-in::placeholder { color: var(--text-muted); }
.ktchips-in:focus { outline: none; }

.ktbadge { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; font-weight: 700; border-radius: 999px; padding: 2px 8px; }
.ktbadge-good { color: var(--role-presence); background: var(--green-a); }
.ktbadge-quiet { color: var(--text-muted); background: var(--surface-page); border: 1px solid var(--border-strong); font-weight: 600; padding: 2px 9px; }
.ktbadge-warn { color: var(--role-attention); background: var(--amber-a); border: 1px solid var(--role-attention); }
.ktavatar { width: 44px; height: 44px; border-radius: 50%; background: var(--role-context); color: var(--ink-on-accent); display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; flex-shrink: 0; }

/* ── 1. The sentence-settings card ──────────────────────────────────────── */
/* View-first: nothing here is a field, and the Edit control is a link. The
   whole point of the card is that a settings page has no inputs on it. */
.ktsent { gap: 6px; }
.ktsent-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.ktsent-title { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 16px; font-weight: 700; }
.ktsent-title-danger { color: var(--role-danger); }
.ktsent-danger { border-color: var(--role-danger); }
.ktsent-lines { display: flex; flex-direction: column; gap: 2px; }
.ktsent-line { font-size: 14.5px; color: var(--text-muted); line-height: 1.6; }
.ktsent-hint { font-size: 13px; color: var(--text-muted); line-height: 1.5; margin: 0; }
/* The lateral way out (TEN-1617): one thing to DO in the header's control, one
   place to GO underneath. A link rather than a second button, because two
   buttons would put them at the same rung when only one of them is the act. */
.ktsent-away { margin: 2px 0 0; font-size: 13.5px; }
/* The link rung as a form (TEN-1903): a submit that reads exactly as the anchor beside it. */
.ktsent-postbtn { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
.ktsent-rows { display: flex; flex-direction: column; gap: 16px; margin-top: 6px; }
.ktsent-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding-top: 16px; border-top: 1px solid var(--border-subtle); }
.ktsent-rows > .ktsent-row:first-child { padding-top: 0; border-top: 0; }
.ktsent-rowtext { display: flex; flex-direction: column; gap: 4px; }
/* THE RESOLVED-VALUE ROWS (TEN-1970, check-in boards A1/B1/C1): label, the value
   as the wire resolved it, and the chip-prov naming its source — the registration
   policy card's read row on the sentence card. The label column wraps above the
   value on a phone. */
.ktsent-facts { display: flex; flex-direction: column; margin-top: 2px; }
.ktsent-fact { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin: 0; padding: 6px 0; border-top: 1px solid var(--border-subtle); }
.ktsent-facts > .ktsent-fact:first-child { border-top: 0; padding-top: 0; }
.ktsent-factlbl { font-size: 13.5px; color: var(--text-muted); min-width: 10rem; }
.ktsent-factval { font-size: 14.5px; font-weight: 600; flex: 1 1 auto; min-width: 0; }
@media (max-width: 640px) { .ktsent-factlbl { min-width: 100%; } }
/* The door-code dialog's NFC note (TEN-1970 E1): told, not pending, so the sub tier. */
.clscode-nfc { margin: 0; padding-top: 10px; border-top: 1px solid var(--border-subtle); font-size: 13.5px; color: var(--text-muted); line-height: 1.5; }
.ktsent-rowname { font-size: 14.5px; font-weight: 600; }

/* ── 2. The tag-coverage card ───────────────────────────────────────────── */
.ktcov-hint { font-size: 13px; color: var(--text-muted); margin: 0; }
.ktcov-derived { display: flex; flex-direction: column; gap: 2px; border-top: 1px solid var(--border-subtle); padding-top: 12px; }
.ktcov-derived .kteyebrow { padding-bottom: 6px; }
.ktcov-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
/* ONE ROW, ONE HAIRLINE (TEN-1645, Mark's UAT round 14: as the offerings list
   grows "it gets hard to eyeball which row a tag pill belongs to"). The pills sit
   flush right and the name sits flush left, so a wide row puts 400px of nothing
   between a word and the thing it belongs to — and the eye has no line to carry
   across it. A rule between rows is that line.

   IT IS THE CARD'S OWN HAIRLINE, NOT A NEW TOKEN: `--border-subtle`, which is
   exactly what `.ktcov-derived` draws above the list and what `.ktsent-row`
   (K1's sentence card) already separates ITS rows with — same construction,
   `border-top` on every row but the first of a run, so the first row sits flush
   under the eyebrow and the run reads as one block rather than as a fenced table.

   WHY NOT THE DUOTONE (the other rung TEN-1645 offered, and it is a design call
   worth seeing the reasoning for): an alternating surface step is keyed on a
   row's POSITION, and these rows MOVE — the re-rank carries a row between the
   applicable section and the fold on every tag change (TEN-1603), so the stripes
   would reshuffle under the pointer as somebody harvests tags. A stripe that
   moves is the "second thing to read" the order clause forbids, wearing a tint.
   A hairline belongs to the gap between two rows and is the same line whichever
   rows those are. It is also the quieter of the two, which is what the ticket
   asks for: association support, not zebra wattage. */
.ktcov-row + .ktcov-row { border-top: 1px solid var(--border-subtle); }
.ktcov-mark { width: 15px; text-align: center; color: var(--role-presence); font-weight: 700; }
.ktcov-name { flex: 1; font-size: 14.5px; font-weight: 600; }
.ktcov-note { font-size: 12.5px; color: var(--text-muted); }
/* Unmatched: inert, and it keeps its row. A plan that does NOT cover this is
   the fact an organizer most needs to see, so it is never filtered out — it is
   FOLDED since TEN-1603, which is a different thing: still on the page, still
   carrying its reason, one press away. The row is genuinely switched off, which
   is the one thing --text-faint is for. */
.ktcov-row-off .ktcov-mark, .ktcov-row-off .ktcov-name, .ktcov-row-off .ktcov-note { color: var(--text-faint); font-weight: 400; }
/* THE TAG PILLS (TEN-1608). The row's right-hand slot shows the offering's own
   tags instead of a sentence about them, and each is a press that adds the tag
   to the class's selector — coverage built by harvesting the words the plans
   already use.

   THE PILLS ARE NOT DIMMED WITH THE ROW. Everything else on an unmatched row is
   --text-faint because the row is genuinely switched off; the pills are the one
   part of it that is still LIVE — pressing one is how the row stops being
   unmatched — so they keep readable ink (K12's floor) and the row's greying
   deliberately does not name them. A press drawn in the disabled colour is the
   .link--dismiss mistake K12's punch r3 already corrected once.

   LIT = THE CLASS CARRIES THIS TAG. The soft accent is the same tint the
   secondary button rests on, so "this word is one of mine" reads as the
   accent's quiet weight rather than as a fourth colour nobody has met. */
.ktcov-pills { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; max-width: 60%; }
.ktcov-pill {
  font: inherit; font-size: 11.5px; line-height: 1.4; font-weight: 600;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--border-subtle); background: var(--surface-page);
  color: var(--text-muted); white-space: nowrap;
}
/* Only the promoted BUTTON answers a pointer — the server-rendered span is a
   label and says so by never changing under one (R9 clause 3). */
button.ktcov-pill { cursor: pointer; }
button.ktcov-pill:hover { border-color: var(--role-primary); color: var(--role-primary-ink); }
button.ktcov-pill:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
/* The state's own hover arm (TEN-1702 sweep; see `.ktlist-chip-on`). The hover
   rule to outrank is `button.ktcov-pill:hover` — element name included — so the
   arm carries the element name too; `.ktcov-pill-on:hover` alone would be a
   specificity step short and would look like a fix without being one. The
   plain class still paints the server-rendered span, which answers no pointer. */
.ktcov-pill-on, button.ktcov-pill-on:hover { border-color: var(--role-primary); background: var(--role-primary-soft); color: var(--role-primary-ink); }
.ktcov-empty { font-size: 14px; color: var(--role-attention); background: var(--amber-a); border: 1px solid var(--role-attention); border-radius: var(--control-r); padding: 10px 14px; margin: 4px 0 0; }
/* THE FOLD (TEN-1603). A quiet disclosure, not a control that draws the eye:
   the applicable rows are the answer and this is the footnote under them. It is
   a real <details>, so it opens with no scripting at all — and the marker is
   left alone rather than restyled, because the platform's own triangle is the
   thing every reader already knows means "there is more here". */
.ktcov-apply, .ktcov-restrows { display: flex; flex-direction: column; gap: 2px; }
/* AND THE OPENED FOLD SCROLLS (TEN-1608, Mark's UAT round 7: the list "doesn't
   swallow the page"). TEN-1603 shut the catalog behind a disclosure; opening it
   still handed a long org an unbounded list, so the answer above it left the
   viewport the moment somebody went looking for a reason.

   THE HEIGHT IS ~5.5 ROWS, NOT 5 OR 6, AND THAT HALF IS THE WHOLE POINT. A fold
   cut on a row boundary looks like the end of the list; a row cut in half is
   the one affordance every reader already knows means "keep going". The inset
   shadow at the bottom edge says the same thing a second way for a list that
   happens to land flush.

   A row is 14.5px/1.4 of name plus 8px of padding either side ≈ 36px, so 5.5
   rows is 198px; `overscroll-behavior` keeps the wheel inside the fold rather
   than handing the page a scroll the reader did not ask for, and the rule under
   the box draws the edge the list is folded at rather than letting the rows
   simply stop. No token is minted for this: it is the card's own hairline. */
.ktcov-restrows {
  max-height: 198px; overflow-y: auto; overscroll-behavior: contain;
  border-bottom: 1px solid var(--border-subtle);
}
.ktcov-rest { margin-top: 6px; }
.ktcov-more { font-size: 12.5px; color: var(--text-muted); cursor: pointer; padding: 4px 0; list-style-position: outside; }
.ktcov-more:hover { color: var(--text-primary); }
.ktcov-rest > summary:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; border-radius: var(--b-radius-sm); }

/* ── 3. The venue picker and its place card ─────────────────────────────── */
.ktplace { display: flex; flex-direction: column; gap: 10px; max-width: 620px; }
.ktplace-field { display: flex; flex-direction: column; width: 360px; max-width: 100%; position: relative; }
/* THE FIELD IS K15's OWN DROPDOWN NOW (TEN-1639), so everything this block used
   to declare about a panel is gone: `.ktplace-in`, `.ktplace-field-open`,
   `.ktplace-menu`, `.ktplace-grp`, `.ktplace-opt` and `.ktplace-tick` described
   a SERVER-RENDERED menu at `?open=where` that nothing linked to, beside a
   closed select that popped the operating system's window. The panel, the seam,
   the corners, the elevation and both state channels are the kit block below.

   WHAT IS LEFT IS A SIZE. `.ktselect` is the STANDALONE dropdown's skin — sized
   to a phrase mid-sentence, bold at 13px so it reads as a value inside a line of
   prose — and this mount is a FIELD at the head of a form column. So the two
   overrides that skin carries are put back to `.ktinput`'s own numbers, which is
   what `.ktplace-in` was doing by never having them. */
.ktplace-field > .ktselect {
  width: 100%; min-width: 0;
  padding: 0 14px; font-size: 15px; font-weight: 400; --ktchev-inset: 14px;
}
/* The minting affordance — the panel's footer act, and this is the SCRIPTLESS
   half of it (R4's position, kept; see the plate). */
.ktplace-add { padding: 10px 14px 2px; font-size: 13.5px; font-weight: 600; color: var(--role-primary-ink); text-decoration: none; border-top: 1px solid var(--border-subtle); margin-top: 6px; }
/* AND DASHED, once it can actually mint (TEN-1581). Dashed = STAGED: an
   invitation to a thing that does not exist yet (K12's shape rule), which is
   the same drawing the pricing kit's add row wears — one reading of dashed
   across the kit, resolved through --role-staged so a theme cannot drift them
   apart. It sits UNDER the field in every state now, open menu or closed, so
   the border-top the menu version wore is replaced by a box of its own. */
.ktplace-add-new {
  border: 1.5px dashed var(--role-staged); border-radius: var(--control-r);
  padding: 10px 14px; margin-top: 8px; border-top-width: 1.5px;
  width: 360px; max-width: 100%; box-sizing: border-box;
}
.ktplace-add-new:hover { border-color: var(--role-primary); border-style: solid; }
/* THE CARD STACK (TEN-1656). One card per option, all but the picked one carried
   `hidden`, so the card follows a pick with no round trip. `display: contents`
   because this element is an ADDRESS rather than a box: it exists so
   kitcontrols.js can find the cards and the control they follow without counting,
   and the cards stay `.ktplace`'s own flex children for every purpose a reader can
   see. A wrapper that laid out would have nested a second gap inside the first. */
.ktplace-cards { display: contents; }
.ktplace-card { padding: 18px 20px; gap: 12px; }
.ktplace-card-min { padding: 14px 20px; gap: 2px; }
.ktplace-top { display: flex; align-items: flex-start; gap: 14px; }
.ktplace-ident { display: flex; flex-direction: column; gap: 3px; flex: 1; }
.ktplace-nameline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ktplace-name { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 16.5px; font-weight: 700; }
.ktplace-handle { font-size: 13px; font-weight: 600; color: var(--role-primary-ink); text-decoration: none; }
.ktplace-addr { font-size: 13.5px; }
/* `.ktplace-view` was the "View place ›" tail, retired by TEN-1612: it pointed
   nowhere on every real surface, and the place card is what it always meant. */
/* THE MENTION (TEN-1612). Every place name in the book that opens the place
   card wears this — the picker's chosen name, the settings Place row, the class
   hub's Where line — so one rule decides what a pressable place looks like and
   a surface cannot invent a second. It is a `.link` first; this adds only the
   things a NAME inside somebody else's sentence needs. */
.ktplace-mention { text-decoration: none; font-weight: 600; }
.ktplace-mention:hover { text-decoration: underline; }
/* On the picker's card the mention IS the place name, so it keeps the name's
   type ramp and takes the accent only for its ink. */
a.ktplace-name { color: var(--role-primary-ink); }
.ktplace-room { display: flex; align-items: center; gap: 8px; border-top: 1px solid var(--border-subtle); padding-top: 12px; font-size: 13.5px; color: var(--text-muted); }
/* --text-muted, not --text-faint (K12's amended text ladder): the room icon
   labels a line somebody reads, and nothing about it is switched off. */
.ktplace-roomicon { color: var(--text-muted); }
/* THE ROOM BLOCK (TEN-1625) — the sentence and the rung as one region, because
   they are one story about one place. The border-top that used to sit on
   `.ktplace-room` moves here so a block carrying chips draws ONE rule above
   itself rather than one above the sentence and none above the chips. */
.ktplace-roomblock { display: flex; flex-direction: column; gap: 10px; border-top: 1px solid var(--border-subtle); padding-top: 12px; }
.ktplace-roomblock .ktplace-room { border-top: 0; padding-top: 0; }
/* `.ktplace-roomblock[hidden]` is NOT declared here — it rides the kit's one
   [hidden] list further down, where every other `display`-bearing element the kit
   reveals or stands down is spelled. It is load-bearing rather than tidy: the kit
   stands this block down the moment the field names another place, and a block
   that stayed visible would go on describing the building somebody just left. */
/* The rung: a WRAPPING row of pill radios, never a joined strip. A room list is
   org-authored — any number of them, any length of name — so `.ktseg`'s
   up-to-four short words would overflow at "Pool deck"; chips wrap. The
   micro-label takes its own line (K12's micro-label tier). */
.ktrooms { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.ktrooms-label { width: 100%; }
/* `.ktroom` is `.ktday`'s mechanism with radio semantics: the real control is a
   hidden input inside the label, the box is the target, and the CHOSEN state is
   drawn from `:has(input:checked)` in the R9 block below — never by a class the
   server moves. The height matches the day chip's 40px, so a thumb finds both. */
.ktroom { height: 40px; padding: 0 14px; border-radius: var(--control-r); border: 1px solid var(--border-strong); background: var(--surface-raised); display: inline-flex; align-items: center; font-size: 13px; font-weight: 600; color: var(--text-muted); cursor: pointer; }
.ktroom input { position: absolute; opacity: 0; width: 0; height: 0; }
.ktroom:focus-within { box-shadow: 0 0 0 3px var(--role-primary-soft); }

/* ── 4. The pricing-option block ────────────────────────────────────────── */
.ktprice { gap: 12px; }
.ktprice-head { display: flex; align-items: center; justify-content: space-between; }
.ktprice-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.ktprice-unit { font-size: 14px; }
/* A sub-option is drawn INSIDE the price it discounts — the containment is the
   statement that it is not a second price. */
.ktprice-opt { border: 1px solid var(--border-subtle); border-radius: var(--control-r); padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; background: var(--surface-page); }
/* THE GATE'S WRAPPER IS INVISIBLE (TEN-1608). `<fieldset disabled>` is what
   switches a whole option block off — the platform's own region switch, so no
   internal markup is touched and nothing inside posts — and `display: contents`
   keeps its box out of the layout, so the block's own flex column and gaps are
   exactly what they were before it was wrapped. The resets are the belt: a
   browser that will not honour `display: contents` on a fieldset must not
   suddenly draw the legend well every fieldset ships with. */
.ktprice-optfields { display: contents; border: 0; margin: 0; padding: 0; min-width: 0; }
/* SWITCHED OFF, AND SAYING SO. --text-faint is the state colour, not a tier,
   and this is exactly what it is for (K12, punch r3): the block is disabled and
   wearing a disabled affordance, so the low contrast confirms what the reader
   can already see. The anchors inside have had their href removed by the plate
   or by the gate family — this only stops a pointer from suggesting otherwise. */
.ktprice-optfields:disabled { color: var(--text-faint); }
.ktprice-optfields:disabled a { pointer-events: none; color: var(--text-faint); }
.ktoff .ktprice-opt { opacity: 0.6; }
.ktoff .ktadd { border-color: var(--border-subtle); pointer-events: none; }
.ktoff .ktadd .ktadd-label { color: var(--text-faint); }
.ktoff .ktadd .ktsub { color: var(--text-faint); }
.ktprice-opthead { display: flex; align-items: center; justify-content: space-between; }
.ktprice-optname { font-size: 13.5px; font-weight: 700; }
.ktprice-optrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 14px; }
.ktprice-optrow .ktinput { background: var(--surface-raised); }
/* THE TRAILING SYMBOL (TEN-1603) — "$25" leads, "40%" trails, which is how the
   two are written. The markup does not move: the symbol slot stays where it is
   and this reorders the flex line off the attribute the segment writes, so the
   glyph and its side are one decision made in one place (the server's mode
   table) and drawn in two. `order` and not a second span, because a second span
   would be a second thing kitcontrols.js has to keep empty. */
.ktinput-money[data-ktsym-side="trail"] .ktmoney-sym { order: 2; }
/* WHO QUALIFIES FOR THE MEMBER RATE (TEN-1580) — the org picker, on its own
   line. A tokpick grows with what is in it, and a control that reflowed the
   price row every time somebody named an org would make the sentence beside it
   move while it is being read. Same select skin as the tag picker
   (.ktcov-pick), because it is the same control.
   IT SITS ABOVE THE RATE SINCE TEN-1603 (Mark's Dropins01 annotation): the row
   is a sentence — "Members of [Apex] pay [$15] per session" — and the amount
   was arriving before anybody had said whose amount it was. The plate moved it;
   this rule only stopped saying "under the row". */
.ktprice-optorgs { display: flex; flex-direction: column; gap: 6px; margin-bottom: 2px; }
.ktprice-optorgs .ktcov-pick { min-height: 70px; max-width: 420px; }
.ktprice-optorgs .tokpick-field { min-height: var(--control-h); max-width: 420px; }
/* The async rung's dropdown is positioned against the field, exactly as the
   tag picker's is — same control, one card up. */
.ktprice-optorgs .peoplepick-list { max-height: 14rem; }
/* The caller's note (TEN-1594) — where these options are really authored, said
   under them rather than beside the add row, so it reads as a fact about the
   options above and not as a hint about the row below. */
.ktprice-note { margin: -2px 0 0; }
/* THE DURATION CONTROL (TEN-1604 as the early bird's deadline; a kit family of
   its own, `.ktdur`, since TEN-1606). A count and the unit it is counted in,
   welded into one object mid-sentence: the two are ONE setting and a gap between
   them would read as two. The parts keep their own borders and sit flush, which
   is the same seam the segmented control uses between its own rungs.
   THE HEIGHT DOES NOT MOVE — every control on the pricing row is --control-h,
   which is the one height .btn, .ktinput and the segment already share. What
   narrows is the WIDTH: the stepper's box is 64px of monospace sized for a
   card's own line, and a two-digit count in the middle of a sentence does not
   need it. Nothing about the family changes — same markup, same attributes,
   same behaviour — only the measurements.
   THE SELECT RUNG STANDS 2PX TALLER THAN A BARE .ktinput, and that is the weld
   rather than a fudge: a bordered wrapper whose children are --control-h is 2px
   over a border-box .ktinput at the same token, which is why every .ktseg in
   this book has sat 2px above the money box since the segment shipped (the
   browser gate's geometry note says so out loud). Welding the select to the
   stepper means matching the stepper, not the token. */
.ktdur { display: inline-flex; align-items: center; }
.ktdur .ktstepper { border-top-right-radius: 0; border-bottom-right-radius: 0; border-right: 0; }
.ktdur .ktseg, .ktdur .ktinput { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.ktdur .ktinput { height: calc(var(--control-h) + 2px); }
.ktdur-unitsel { width: 104px; padding: 0 10px 0 12px; font-size: 13px; font-weight: 700; --ktchev-inset: 10px; }
.ktdur-unitsel select { color: var(--text-muted); }
/* THE STANDALONE DROPDOWN (TEN-1629, kit-control's `select` rung). It is the
   unit selector's skin without the welded edge: the same .ktinput chrome, the
   same K11a chevron, sized to the phrase it holds rather than to a two-character
   unit. Mid-sentence, so it does not stretch. */
.ktselect { min-width: 132px; padding: 0 10px 0 12px; font-size: 13px; font-weight: 700; --ktchev-inset: 10px; }
.ktselect select { color: var(--text-primary); }
/* THE PRICING MODE (TEN-1645) — the standalone dropdown, sized to "$ total" and
   STANDING WHERE THE SEGMENT STOOD. The height is the weld's, not the token's:
   a bordered wrapper whose children are --control-h is 2px over a border-box
   .ktinput at the same token, which is why the segment this replaces — and the
   stepper and unit selector beside it on the same row — have all stood at
   --control-h + 2px since the row shipped (the browser gate's geometry note says
   so out loud, and measures it). Matching the row rather than the token is what
   keeps this a change of CONTROL and not a change of layout. */
.ktprice-modesel { min-width: 104px; height: calc(var(--control-h) + 2px); }

/* ══ K15 — THE DROPDOWN PANEL (TEN-1634; board 21 v3 is the normative image) ══
   Every dropdown in this book is one object, and this is it. K15 governs the
   PANEL, not the machinery behind it: a plain select, an async directory search
   and the venue picker mount the same panel — outline, seam, corners, elevation,
   state channels, width and collision laws — at one of two row densities,
   `simple` (the control's own height, one line) or `card` (56px: avatar, title,
   mono meta). Nothing else varies.

   WHY THERE IS A PANEL AT ALL, when TEN-1633 just shipped an `option` rule for
   this. That rule is the best a stylesheet can do and it is not enough: the
   native popup is drawn by the operating system, so its rows, its scrollbar, its
   corners, its elevation and its highlight are all outside every token in this
   file. K15's line is that a native popup, however themed, is a violation by
   definition — the `option` rule above stays because it is what the SCRIPTLESS
   path still gets, and that path is the fallback rather than the product (R9
   clause 3). ktsel.js suppresses the native popup and this draws what replaces it.

   THE OPEN OBJECT IS ATTACHED, AND EVERY RULE BELOW IS ABOUT NOT LOOKING LIKE
   TWO THINGS: no gap, one continuous outline, one elevation, and a join that is
   a hairline rather than a second border. The hairline is not drawn — it IS the
   control's own bottom border, recoloured, which is the only construction where
   the two cannot disagree by a pixel. The panel therefore butts against the
   control's border box exactly and carries no border on the joining edge.

   PORTABILITY IS A STANDING CONSTRAINT. Nothing here is a web-only primitive:
   it is tokens, geometry and interaction rules, so desktop, web and Unity can
   render the same object. ──────────────────────────────────────────────────── */

/* The lift. A cover-local elevation rather than --b-shadow-*, for two measured
   reasons: the object's shadow is cast by TWO elements (the control and the
   panel) and has to be the same one on both, and the high-contrast theme —
   which zeroes every --b-shadow-* — must still resolve to a single valid
   `none` rather than to a shadow list that is invalid and silently dropped. */
:root { --ktsel-lift: 0 10px 28px rgba(0, 0, 0, 0.45); }
:root[data-theme="light"] { --ktsel-lift: 0 10px 28px rgba(20, 24, 32, 0.16); }
:root[data-theme="hc"] { --ktsel-lift: none; }

/* ══ THE PANEL VOCABULARY, AND IT IS SHARED (K15, TEN-1634 train 2) ═══════════
   "K15 governs the PANEL, not the machinery behind it — a plain select, an async
   directory search, and the venue picker mount the same panel object."

   Before this pass there were FIVE panels in this book and THREE token dialects
   between them: the token picker and its two descendants on the --b-* layer with
   a hard-coded shadow; the header's people search on the same layer with
   DIFFERENT picks and no shadow at all; the venue picker on the kit's --role-*
   layer. Three answers to "what colour is a dropdown", none of them wrong on its
   own, all of them visible on one screen.

   THIS IS AN ALIGNMENT, NOT A REBUILD. Not one line of picker JavaScript, not
   one attribute of picker markup and not one behaviour changes here. What
   changes is which declarations exist and where: every property K15 owns is
   declared ONCE, below, for all five — so the next theme change moves five
   panels, and the next divergence has nowhere to hide. Each family's own block
   keeps exactly what is ITS OWN (its arity, its rows' contents, its scriptless
   pair) and has had the overlapping declarations removed rather than overridden.

   THE TWO DEVIATIONS ARE ALREADY IN THE RULING and are behaviours, not visuals:
   a token picker's panel STAYS OPEN on pick, and a pick becomes a token in the
   control rather than the control's value. Nothing below touches either.

   AND THE FIFTH PANEL IS GONE ALTOGETHER (TEN-1639). The venue picker's was the
   one that was never an asset's at all — a menu the SERVER drew at `?open=where`,
   aligned into this list and then left behind the native popup its closed select
   still popped. Its field mounts `data-ktsel` now, so there are FOUR panels here
   and the venue picker is reading the first one rather than resembling it: the
   alignment's own argument, taken one step further than an alignment could.
   ────────────────────────────────────────────────────────────────────────── */
.ktsel-panel,
.tokpick-list,
.hdr-people-drop {
  box-sizing: border-box; padding: 6px;
  background-color: var(--surface-raised); color: var(--text-primary);
  border: 1px solid var(--border-strong);
  box-shadow: var(--ktsel-lift);
  --ktsel-row: var(--control-h);
  overflow-y: auto; overflow-x: hidden;
  max-height: calc(var(--ktsel-row) * 6.5 + 6px);
}
/* THE CARD DENSITY, AND IT IS DECIDED BY WHETHER THE ROWS CARRY A FACE rather
   than by which family drew them. The header's people search puts an avatar on
   every hit, so it is a card panel. The people picker's rows are a name, a
   handle and a note on one line — no thumb — so they take the simple density,
   and giving them the card height would be 56px of air around one line of text.
   Two densities, every panel, and no third measurement anywhere — the venue
   picker's rows are the card density for exactly this reason (TEN-1639): a venue
   row carries its owner's face, a title and a mono meta line. */
.ktsel-panel-card,
.hdr-people-drop { --ktsel-row: 56px; }
/* ATTACHED, ALL OF THEM. The panels that floated with a 2-4px gap now butt
   against their control: no gap, the joining edge borderless so the outline is
   ONE line, and the corners square where the two meet. The seam itself is the
   CONTROL's own bottom border, recoloured — the construction K15 uses precisely
   so the hairline cannot disagree with the control's width. */
.tokpick-list, .hdr-people-drop {
  border-top: 0; border-radius: 0 0 var(--control-r) var(--control-r);
}
.tokpick:has(> .tokpick-list:not(:empty)) > .tokpick-field,
.suggest:has(> .tokpick-list:not(:empty)) > input,
.hdr-people:has(> .hdr-people-drop:not(:empty)) > .hdr-people-q,
/* The find page's own box grew the same panel (TEN-1874), so it takes the same
   seam: the panel hangs off .people-qwrap, which already establishes the
   positioning context for the clear x. One object, one construction — a panel
   that floated here while the chrome bar's butted against its control would
   read as two controls. */
.people-qwrap:has(> .hdr-people-drop:not(:empty)) > .people-q {
  border-bottom-color: var(--border-subtle);
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
}

/* ── THE PICKER PANEL MAY GROW (TEN-1703) ───────────────────────────────────
   K15's width clause is one sentence and it has two halves: "panel min-width =
   control; excess grows RIGHT and never shrinks; rows never wrap or truncate."
   These two panels only ever had the first half. `left: 0; right: 0` is not
   "min-width = the control", it is "width = the control, exactly" — a clamp in
   both directions — and the note beside `.ktsel-opt` said so out loud: rows
   never truncate "which a picker panel cannot do because its width is its
   field's". That was a description of a limitation being read as a rule.

   WHAT MARK SAW (storefront UAT r2): two organisation rows in the header's
   "Find people & orgs" dropdown, both rendered as "T…". The header box lives in
   `.headmid`, a `flex: 1 1 auto; min-width: 0` slot between the wordmark and the
   chrome — so at the widths where the chrome is busy the CONTROL is squeezed to
   a fraction of its 340px, and a panel clamped to the control is squeezed with
   it. The rows had a whole page of room beside them and were ellipsized to one
   character because the box above them was narrow. A panel that shrinks with
   its control is the failure; min-width is a FLOOR.

   SO: the floor stays the control (`min-width: 100%` against the relatively
   positioned mount), the growth is rightward (`right: auto`, the clamp's other
   half removed), the width is the content's own appetite (`max-content`), and
   it stops at a sane ceiling rather than at whatever a 200-character
   organisation name would ask for. `--pickpanel-max` is a token so a cramped
   mount can tighten it without a second construction; the viewport term is the
   same `calc(100vw - 16px)` K15's own panel uses.

   AT THE CEILING, AND ONLY THERE, A ROW MAY STILL ELLIPSIZE — `.hdr-people-name`
   keeps its `text-overflow`. That is not a retreat from "rows never truncate":
   a ceiling has to degrade somehow, and an ellipsis at 30rem is the honest
   degradation where a panel that grew to the width of the longest string in a
   directory would be the dishonest one.

   NOT DELIVERED HERE, AND SAID SO RATHER THAN LEFT TO BE DISCOVERED: the
   leftward FLIP at the container's edge. K15's collision clause is implemented
   in `ktsel.js`'s `place()`, which can do it because that panel is
   `position: fixed` and its script measures on every open. These two are in-flow
   `position: absolute` panels whose three scripts (picker.js, suggest.js,
   peoplesearch.js) contain no geometry pass at all, and writing a third, fourth
   and fifth copy of `place()` is precisely the drift the kit's one-family rule
   exists to stop. The ceiling above keeps the mounts that exist inside the
   window — measured in the browser gate rather than asserted here
   (TestPeoplesearchPanelGrowsToItsRows) — and a shared geometry pass for the
   picker family is the follow-up this note is the marker for. */
.tokpick-list, .hdr-people-drop {
  right: auto;
  min-width: 100%;
  width: max-content;
  max-width: min(var(--pickpanel-max, 30rem), calc(100vw - 16px));
}
/* THE IN-FLOW MOUNTS ARE NOT IN THIS. Inside a modal the list joins the FLOW
   (TEN-1152 / the `.ktmodal-panel` note) — `position: static`, so `min-width:
   100%` resolves against the MODAL rather than against the field and
   `max-content` would make it narrower than the control it belongs to, which is
   the very thing above is about. Both static overrides put the width back. */
.ss-grid-modal .tokpick-list,
.ktmodal-panel .tokpick-list { width: auto; max-width: none; }

/* THE ROWS, at whichever density their panel declared. Full-row hit target, the
   kit's radius, and a ground of its own on every one of them (TEN-1633's rule,
   generalised: a row that resolves its background against something else is a
   row somebody else gets to colour). */
.ktsel-opt, .tokpick-opt, .peoplepick-row, .hdr-people-hit {
  display: flex; align-items: center; gap: 12px; box-sizing: border-box;
  min-height: var(--ktsel-row); padding: 0 12px;
  border-radius: var(--b-radius-sm);
  background: var(--surface-raised); color: var(--text-primary);
  cursor: pointer; text-decoration: none;
}
/* The rows that are not values — a status line, a group header — sit on the
   same rhythm without pretending to be pickable. */
.tokpick-none, .peoplepick-none, .peoplepick-waiting {
  padding: 10px 12px; color: var(--text-muted); font-size: var(--b-text-sm);
}
/* A handle, an org, a "already here" note: the MONO tier, which is what makes
   an identifier read as an identifier rather than as a second name (K12). */
.peoplepick-handle, .hdr-people-meta, .peoplepick-here {
  font-family: var(--b-font-mono); font-size: 11px; color: var(--text-muted);
}

/* THE CONTROL, OPEN. The box is whatever the mount declared with
   `data-ktsel-box` — the kit's `.ktinput` label on a kit field, the select
   itself where the select IS the visible box. Its corners go square on the
   JOINING edge only; every corner still exposed to the page keeps its radius.
   These rules sit after `.ktinput:focus-within` on purpose: at equal
   specificity the later rule wins, and an open dropdown's indication is the
   panel hanging off it rather than a focus halo around it. */
.ktsel-box[data-ktsel-open] { border-color: var(--role-primary); box-shadow: var(--ktsel-lift); }
.ktsel-box[data-ktsel-open]:not([data-ktsel-up]) {
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
  border-bottom-color: var(--border-subtle);
}
.ktsel-box[data-ktsel-open][data-ktsel-up] {
  border-top-left-radius: 0; border-top-right-radius: 0;
  border-top-color: var(--border-subtle);
}
/* The chevron flips while open, where there is a kit chevron to flip. A mount
   on a surface with its own caret keeps that surface's caret — K15 is about the
   popup; the closed control's chrome is K11a's, and one ruling does not reach
   into the other's half. */
.ktinput.ktsel-box[data-ktsel-open]:has(> select)::after { transform: scaleY(-1); }

/* THE PANEL. `position: fixed` and a child of <body>, which is the one
   construction that cannot be clipped by an ancestor — a dropdown inside a
   scrolling grid or a modal is the case the chats/rooms off-screen scar was
   about. ktsel.js places it; everything here is the drawing. */
.ktsel-panel {
  /* Everything a panel and a picker panel share is declared once, above. What
     is left here is what only K15's own panel does: it is placed against the
     WINDOW rather than laid out in the page (see the family's note on why), and
     its silhouette edge is painted rather than bordered. */
  position: fixed; z-index: 60; margin: 0;
  /* min-width = control, set by the asset; excess grows into the content's own
     width and never shrinks, so rows never wrap or truncate. */
  width: max-content; max-width: calc(100vw - 16px);
  border-top: 0;
  font-family: var(--b-font-body); font-size: 15px;
  background-repeat: no-repeat; background-origin: border-box; background-clip: border-box;
}
.ktsel-panel[data-ktsel-up] { border-top: 1px solid var(--border-strong); border-bottom: 0; }
/* CORNERS: SQUARE AT THE SEAM, ROUNDED AT THE SILHOUETTE. A panel the control's
   own width has a seam across its whole joining edge and both of those corners
   are square. A WIDER one has a corner out in the page — and that corner rounds,
   on whichever side the overhang landed. */
.ktsel-panel { border-radius: 0 0 var(--control-r) var(--control-r); }
.ktsel-panel[data-ktsel-up] { border-radius: var(--control-r) var(--control-r) 0 0; }
.ktsel-panel[data-ktsel-wide]:not([data-ktsel-left]):not([data-ktsel-up]) { border-top-right-radius: var(--control-r); }
.ktsel-panel[data-ktsel-wide][data-ktsel-left]:not([data-ktsel-up]) { border-top-left-radius: var(--control-r); }
.ktsel-panel[data-ktsel-wide][data-ktsel-up]:not([data-ktsel-left]) { border-bottom-right-radius: var(--control-r); }
.ktsel-panel[data-ktsel-wide][data-ktsel-up][data-ktsel-left] { border-bottom-left-radius: var(--control-r); }
/* THE OVERHANG'S OWN EDGE. Across the control the joining edge is the control's
   recoloured border (above); beyond it there is no border to borrow, so the
   silhouette's line is painted as a 1px background strip exactly as long as the
   overhang. `--ktsel-seam` is the control's width, stamped by the asset. It is a
   background rather than a pseudo-element because the panel SCROLLS and a
   pseudo-element inside it would scroll with the rows. */
.ktsel-panel[data-ktsel-wide] {
  background-image: linear-gradient(var(--border-strong), var(--border-strong));
  background-size: calc(100% - var(--ktsel-seam, 100%)) 1px;
  background-position: right top;
}
.ktsel-panel[data-ktsel-wide][data-ktsel-left] { background-position: left top; }
.ktsel-panel[data-ktsel-wide][data-ktsel-up] { background-position: right bottom; }
.ktsel-panel[data-ktsel-wide][data-ktsel-up][data-ktsel-left] { background-position: left bottom; }

/* The row's shape is the shared one above. What is K15-only is that its rows
   NEVER WRAP OR TRUNCATE — the panel grows to the widest of them instead, which
   a picker panel cannot do because its width is its field's. */
.ktsel-opt { white-space: nowrap; }
.ktsel-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.ktsel-meta { font-family: var(--b-font-mono); font-size: 11px; font-weight: 400; color: var(--text-muted); }
.ktsel-panel-card .ktsel-ttl { font-weight: 600; }
.ktsel-av {
  width: var(--b-avatar-md); height: var(--b-avatar-md); border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--role-context); color: var(--ink-on-color);
  font-size: 12px; font-weight: 700; letter-spacing: 0.02em;
}
.ktsel-av[data-ktsel-hue="1"] { background: var(--role-generative); }
.ktsel-av[data-ktsel-hue="2"] { background: var(--role-attention); color: var(--ink-on-accent); }
.ktsel-av[data-ktsel-hue="3"] { background: var(--role-primary); color: var(--ink-on-primary); }
/* The check is drawn on every row and its VISIBILITY is the state: adding and
   removing it would move layout under the pointer on every arrow key. */
.ktsel-tick { visibility: hidden; flex-shrink: 0; font-size: 13px; color: var(--role-primary-ink); }

/* THE TWO STATE CHANNELS, AND THEY NEVER COMPETE ON ONE AXIS.
   SELECTED owns the CHECK — the tick on the right, the accent ink, a quiet
   tint. It is the state at REST, and it is drawn quietly because a value
   already chosen is being confirmed rather than considered (K12's ladder). */
.ktsel-opt[data-ktsel-on] { background: var(--role-primary-soft); color: var(--role-primary-ink); font-weight: 600; }
.ktsel-opt[data-ktsel-on] .ktsel-tick { visibility: visible; }
/* HOVER AND KEYBOARD FOCUS ARE ONE TREATMENT and own the SURFACE — the full
   accent and the ink that reads on it. One drawing for one position: a pointer
   and an arrow key move the same cursor, and two drawings is how a list starts
   looking like it has two selections (the time picker's own note).
   THIS RULE COMES AFTER THE SELECTED ONE ON PURPOSE. A row that is both wears
   the hover surface and KEEPS its check — which is what the comp draws, and
   what falls out of neither mark removing the other. */
.ktsel-opt[data-ktsel-active],
.tokpick-opt:hover, .tokpick-opt.is-active,
.peoplepick-row:hover, .peoplepick-row.is-active,
.hdr-people-hit:hover, .hdr-people-hit.is-active {
  background: var(--role-primary); color: var(--ink-on-primary); font-weight: 600;
}
/* A PRESS ON A PREVIEW ROW ANSWERS IN ITS OWN FRAME (TEN-1879; Mark's standing
   ruling, "every act should acknowledge a press for as long as it takes").

   THE MECHANISM WAS ALREADY ON THE PAGE and this is a treatment, not a second
   one. `cmc-auth/press` is gathered by the `main` layout, so press.js puts
   `.is-pressed` on this anchor synchronously on the click and holds it until the
   profile paints — MEASURED on the running book, which is how it was established
   that the gathering was NOT the problem this time. What the reader got for it
   was the class's house treatment: `scale(.985)` and `brightness(1.06)`. On a
   56px row already sitting on `--surface-raised` in a dark theme, a 6% lift is
   invisible and a 1.5% scale moves the row's edge by two pixels. So the press
   landed, said nothing, and the row sat there looking untouched for the whole
   round trip — Mark's "pressing a dropdown row hangs until the next page load".

   THE PRESSED STATE IS THE ROW'S OWN SELECTED SURFACE, which is the strongest
   thing this row can say and the one it already says for hover and for the
   keyboard cursor (K15's channel law: hover owns the SURFACE). A touch reader
   never hovers, so before this a preview row had no surface state at all. Taking
   the same pair rather than minting a third means a pressed row and a
   cursor-on row read identically, which is right: both are "this is the one". */
.hdr-people-hit.is-pressed {
  background: var(--role-primary); color: var(--ink-on-primary); font-weight: 600;
  /* The house transform is deliberately cancelled here. A row is full-bleed
     inside its panel, and scaling it down leaves a hairline of panel ground
     showing along an edge that is supposed to be flush. */
  transform: none; filter: none;
}
.hdr-people-hit.is-pressed .hdr-people-meta { color: var(--ink-on-primary); }
.ktsel-opt[data-ktsel-active] .ktsel-meta,
.ktsel-opt[data-ktsel-active] .ktsel-tick,
.peoplepick-row:hover .peoplepick-handle, .peoplepick-row.is-active .peoplepick-handle,
.peoplepick-row:hover .peoplepick-here, .peoplepick-row.is-active .peoplepick-here,
.hdr-people-hit:hover .hdr-people-meta,
.hdr-people-hit.is-active .hdr-people-meta { color: var(--ink-on-primary); }
.ktsel-opt[data-ktsel-off] { color: var(--text-faint); cursor: default; }
/* A disabled row can never BE the active one (the asset will not move the
   cursor onto it), so this rule draws nothing today. It is here as the
   belt-and-braces half of that: if a later change let the cursor land there,
   the row would go on reading as inert rather than lighting up as pickable. */
.ktsel-opt[data-ktsel-off][data-ktsel-active] { background: var(--surface-raised); color: var(--text-faint); }

/* Group headers and the footer act — family members, not decorations. */
.ktsel-grp {
  display: block; font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.07em;
  font-size: 13px; font-weight: 700; color: var(--text-muted);
  padding: 12px 12px 4px;
}
.ktsel-grp:first-child { padding-top: 4px; }
/* An ACT, so it is an anchor to a real address and it still works with the
   asset removed. Separated by a rule because it is not one of the values. */
.ktsel-foot {
  display: block; border-top: 1px solid var(--border-subtle);
  margin: 6px -6px -6px; padding: 12px;
  font-size: 14px; font-weight: 600; color: var(--role-primary-ink); text-decoration: none;
}
.ktsel-foot:hover { background: var(--hover); }

/* ══ THE FILTER READOUT (TEN-1656) ═══════════════════════════════════════════
   A filterable mount (`data-ktsel-filter`) narrows its rows as somebody types,
   and this is the row that says WHAT they typed. It is a READOUT and not an
   input: the keystrokes go to the real `<select>`, which is the one control in
   the accessibility tree and the only thing on this object that takes focus
   (K15's accessibility decision, unchanged — see ktsel.js's header for why a
   text box here would have been a second control for one control on screen).

   It wears the group-header slot's geometry rather than a row's, because that is
   what it is: a heading over the rows beneath it, not one of them. The query
   itself is MONO — it is the characters somebody typed, which is the identifier
   tier (K12), and a proportional face would let "Ill" and "III" read alike. */
.ktsel-find {
  display: flex; align-items: baseline; gap: 8px;
  border-bottom: 1px solid var(--border-subtle);
  margin: -6px -6px 6px; padding: 10px 12px;
  font-size: 13px; color: var(--text-muted);
}
.ktsel-find-q { font-family: var(--b-font-mono); font-size: 12px; color: var(--text-primary); font-weight: 600; }
/* NO MATCHES IS A STATE, SAID OUT LOUD — never an empty panel. The everpresent
   rows (None, and the footer act) are still there under it, which is the whole
   point of pinning them: a filter that matches nothing still leaves a way to
   answer and a way to mint. */
.ktsel-find-none { padding: 10px 12px; color: var(--text-muted); font-size: var(--b-text-sm); }

/* ══ THE CLOSED CONTROL'S FACE (TEN-1639, Mark's round-14 requirement) ════════
   "The closed control wears the chosen venue as a slightly more detailed card —
   never a bare text line." An ENTITY-subject picker is the one sanctioned
   variation of K15's closed control, and the amendment under the ruling states
   the ladder it is held to: ROW < FACE < FULL CARD. The 56px row says whose
   venue it is; the face says that and the address the record already carried;
   the `kit-place-card` under the picker keeps the map, the links and the rooms.
   A face that repeated the card would be two cards about one place.

   THE HEIGHT IS THE CARD ROW'S OWN 56PX, deliberately not a third number. This
   file already says it one block up — "two densities, every panel, and no third
   measurement anywhere" — so the face is a card row's height carrying a card
   row's content, and what makes it a FACE rather than a row is the meta line's
   extra fact. It is still a RAISE of the closed control: every other dropdown in
   this book stands at --control-h (44px), and the variation is exactly the 12px
   the second line needs.

   EVERY RULE HERE IS KEYED OFF THE FACE EXISTING, never off the attribute that
   asks for one. ktsel.js draws a face only where the chosen row has one, so all
   three states fall out of one selector: a venue gets the card, "None" is not a
   venue and gets the simple density with the word in the select's own ink, and a
   browser with no scripting gets the native select it always had (R9 clause 3).

   IT IS A PICTURE OF THE CONTROL, SO IT IS TRANSPARENT TO THE POINTER. The press
   has to land on the select itself — that is what opens the panel, and it is the
   law TEN-1631 measured: a click on the LABEL focuses a select in Chrome without
   opening anything, so a strip inside the border that is not the select is a
   strip that looks pressable and does nothing. The select therefore STRETCHES to
   the taller box rather than sitting 20px tall in the middle of it. One hit
   target, chevron and both lines included.

   AND THE FACE MAY ELLIPSIZE WHERE A ROW MAY NOT. K15's no-truncation clause is
   about the PANEL, which grows to its widest row; the face is bound to the
   control's own width and an address has no bound, so the meta line is cut with
   an ellipsis rather than wrapping the control onto a third line. ────────────── */
.ktsel-box:has(> .ktsel-face) { height: auto; min-height: 56px; }
.ktsel-box:has(> .ktsel-face) > select { align-self: stretch; color: transparent; }
.ktsel-face {
  position: absolute; inset: 0; pointer-events: none;
  display: flex; align-items: center; gap: 12px;
  /* The box's own padding, plus the chevron's room on the trailing side — the
     inset is a custom property for the reason the chevron's own rule says, and a
     face that ignored it would run its meta line under the glyph. */
  padding: 0 calc(var(--ktchev-inset, 14px) + 18px) 0 14px;
  font-size: 15px; font-weight: 400; color: var(--text-primary);
}
/* The two lines are the panel row's own, at the row's weights — one drawing for
   one object, which is why the face reuses `.ktsel-txt`, `.ktsel-av` and
   `.ktsel-meta` rather than minting a second vocabulary. The ownership disc is
   therefore the row's disc, unchanged, both channels included. */
.ktsel-face .ktsel-ttl,
.ktsel-face .ktsel-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ktsel-face .ktsel-ttl { font-weight: 600; }

.ktstepper-sm .ktstepper-btn { width: 32px; font-size: 16px; }
.ktstepper-sm .ktstepper-box { width: 44px; }
.ktstepper-sm .ktstepper-box input { font-size: 15px; }
.ktseg-sm .ktseg-opt { padding: 12px 12px; font-size: 12.5px; }
/* THE READ-ONLY BOX LOOKS READ-ONLY (TEN-1604). The legacy absolute deadline is
   rendered and carried but not authored here, and a box that is styled like
   every editable box and simply refuses the keys is the "unclickable control"
   failure inverted — the state was right and the drawing was a lie. Muted ink
   on the page ground rather than the raised one, and the cursor says so before
   anybody tries. `readonly` and never `disabled`: a disabled input does not
   post, and this value has to survive the round trip to be carried. */
.ktprice-optrow .ktinput:has(input[readonly]) { background: var(--surface-page); border-style: dashed; }
.ktprice-optrow input[readonly] { color: var(--text-muted); cursor: default; }
/* The row's own footnote (TEN-1604) — why a box in the row above cannot be
   typed in. Under the row rather than in it, for `.ktprice-note`'s reason one
   rung down: it is a fact about what is above it, not a hint about a control. */
.ktprice-optnote { margin: -2px 0 0; }
/* THE QUIETED REVEAL (TEN-1595) — a control that belongs to a choice nobody has
   made yet. It is drawn rather than hidden because it is the POINT of the
   choice above it: "a booking commits" means nothing until you can see there is
   a window to set. Dimmed and inert, never removed, and never blanked — the
   value it holds is the one the toggle turns back on. */
[data-ktquiet] { opacity: 0.5; }
[data-ktquiet] input, [data-ktquiet] select, [data-ktquiet] textarea, [data-ktquiet] a { pointer-events: none; }
.ktseg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--control-r); overflow: hidden; font-size: 13px; font-weight: 700; }
.ktseg-opt { padding: 12px 14px; background: var(--surface-raised); color: var(--text-muted); cursor: pointer; }
.ktseg-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The SERVER'S render of the chosen segment. It paints only where :has() is
   missing — see the R9 block below, which is what paints it live. */
.ktseg-opt:focus-within { box-shadow: inset 0 0 0 2px var(--role-primary); }
/* Dashed = STAGED: an invitation to a thing that does not exist yet, never a
   control that already did something (K12's shape rule). */
.ktadd { border: 1.5px dashed var(--role-staged); border-radius: var(--control-r); padding: 12px 16px; display: flex; align-items: center; gap: 10px; text-decoration: none; }
.ktadd:hover { border-color: var(--role-primary); border-style: solid; }
.ktadd-label { font-size: 14px; font-weight: 600; color: var(--role-primary-ink); }

/* ── 5. The day-chips schedule row ──────────────────────────────────────── */
.ktdayrow { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
/* THE SEVEN CHIPS SQUISH; THEY DO NOT OVERFLOW (TEN-1610, Mark's UAT round 7).
   Seven 40px chips and six 6px gaps ask for 316px, and the schedule editor's
   card gives them 275 inside a 375px phone — so the week ran off the right edge
   of a card the page cannot scroll sideways past. Same answer the hub's week
   grid got one ticket earlier (TEN-1597): squish, do not scroll.

   It is a GRID whose tracks are `minmax(0, 40px)`, which is the whole fix and
   is deliberately not a breakpoint. A track that maxes at 40px renders exactly
   what the flex row rendered wherever there is room — the desktop and tablet
   drawings are byte-for-byte the ones that shipped — and, because its floor is
   0, the row shrinks the chips proportionally instead of the group overflowing
   the moment there is not. `min-width: 0` is what lets the flex parent shrink
   it at all; without it the group's own content width is a floor and a grid
   inside it would never be asked to give anything up.

   THE TAP TARGET IS THE HEIGHT, AND THE HEIGHT DOES NOT MOVE. 40px stays 40px
   tall at every width; at 375 the chips come out ~34px wide, which is one
   letter's worth of box and still a target a thumb finds. */
.ktdays { display: grid; grid-template-columns: repeat(7, minmax(0, 40px)); gap: 6px; min-width: 0; }
/* A preposition and the control it governs are ONE wrappable unit (TEN-1610) —
   the plate's own note says why. The gap is the row's, restated inside the pair,
   so a wrapped row and an unwrapped one are spaced identically. */
.ktdayrow-pair { display: inline-flex; align-items: center; gap: 16px; min-width: 0; }
.ktday { width: auto; height: 40px; border-radius: var(--control-r); border: 1px solid var(--border-strong); background: var(--surface-raised); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: var(--text-muted); cursor: pointer; }
.ktday input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The SERVER'S render of a chosen chip; live drawing is the R9 block below. */
.ktday:focus-within { box-shadow: 0 0 0 3px var(--role-primary-soft); }
.ktdayrow-x { margin-left: 4px; }

/* TEN-1940 — THE RECURRENCE RULE SET (prototype) and the CONTROL GALLERY's
   harness around it. A schedule is a list of rules; each rule is a card whose
   chips, segments and radios are the kit's own skins (K5's .ktday, .ktseg-opt,
   .ktradio), so every :checked state below is drawn by the rules already in this
   file and nothing here paints a selected state of its own.

   THE DATE GRID SQUISHES FOR K5's REASON: 1…31 plus Last is 32 chips, laid seven
   to a row on the same `minmax(0, 40px)` tracks as the week, so at 375px the
   cells narrow and the 40px tap height does not move. Last takes the tail of the
   fifth row, where the eye looks for "the end of the month". */
.ktrecur { border: 0; margin: 0; padding: 0; min-width: 0; display: flex; flex-direction: column; gap: 16px; }
.ktrecur-legend { padding: 0; margin: 0 0 12px; }
.ktrecur [hidden] { display: none !important; }
.ktrecur-noscript { margin: 0; }
.ktrecur-body { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.ktrecur-when { display: flex; flex-wrap: wrap; gap: 16px 32px; align-items: flex-start; }
.ktrecur-ends { flex: 1 1 280px; min-width: 0; }
.ktrecur-rules { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.ktrecur-rule { position: relative; padding: 16px 20px; min-width: 0; }
.ktrecur-rulebox { border: 0; margin: 0; padding: 0; min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.ktrecur-num { padding: 0; }
.ktrecur-rulebox > .ktseg { align-self: flex-start; }
.ktrecur-x { position: absolute; top: 14px; right: 16px; background: none; border: 0; padding: 4px; cursor: pointer; font-family: inherit; }
.ktrecur-say { margin: 0; padding-right: 28px; font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--text-primary); }
.ktrecur-say-empty { color: var(--role-attention); font-weight: 500; }
.ktrecur-weekly { display: flex; flex-wrap: wrap; gap: 12px 16px; align-items: center; min-width: 0; }
.ktrecur-monthly { display: flex; flex-direction: column; min-width: 0; }
.ktrecur-basis { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.ktrecur-basis .ktradio-nest { gap: 10px; min-width: 0; }
.ktrecur-dates { display: grid; grid-template-columns: repeat(7, minmax(0, 40px)); gap: 6px; min-width: 0; }
.ktrecur-lastday { grid-column: span 4; }
.ktrecur-ords { display: grid; grid-template-columns: repeat(5, minmax(0, 52px)); gap: 6px; min-width: 0; }
.ktrecur-skip { margin: 0; max-width: 42ch; }
.ktrecur-time { gap: 12px 16px; }
.ktrecur-add { align-self: flex-start; background: transparent; cursor: pointer; font-family: inherit; }
.ktrecur-next { display: flex; flex-direction: column; gap: 8px; }
.ktrecur-nexthead { margin: 0; }
.ktrecur-nextlist { list-style: none; margin: 0; padding: 0; border: 1px solid var(--border-subtle); border-radius: var(--card-r); background: var(--surface-raised); }
.ktrecur-nextitem { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; padding: 10px 14px; border-top: 1px solid var(--border-subtle); }
.ktrecur-nextitem:first-child { border-top: 0; }
.ktrecur-nextwhen { font-size: 14px; font-variant-numeric: tabular-nums; color: var(--text-primary); }
.ktgallery-back { align-self: flex-start; font-size: 13px; }
.ktgallery-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 32px; align-items: start; }
.ktgallery-stage, .ktgallery-io, .ktgallery-block { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.ktgallery-io { gap: 24px; }
.ktgallery-presets { display: flex; flex-wrap: wrap; gap: 8px; }
.ktgallery-out { margin: 0; padding: 12px 14px; max-height: 420px; overflow: auto; white-space: pre; font-family: var(--b-font-mono); font-size: 12.5px; line-height: 1.5; color: var(--text-primary); background: var(--surface-raised); border: 1px solid var(--border-subtle); border-radius: var(--card-r); }
.ktgallery-inbox { width: 100%; height: auto; padding: 10px 14px; align-items: stretch; }
.ktgallery-inbox textarea { width: 100%; min-height: 160px; resize: vertical; border: 0; background: transparent; color: var(--text-primary); font-family: var(--b-font-mono); font-size: 12.5px; line-height: 1.5; }
.ktgallery-inbox textarea:focus { outline: none; }
.ktgallery-acts { display: flex; gap: 8px; }
.ktgallery-err { margin: 0; color: var(--role-attention); }
/* The gallery is one section per implementation the control library declares,
   each headed by its registry reference and — on the library default — a badge. */
.ktgallery-impl, .ktgallery-type { display: flex; flex-direction: column; gap: 10px; padding-top: 24px; border-top: 1px solid var(--border-subtle); min-width: 0; }
.ktgallery-implhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 12px; }
.ktgallery-implhead .ktpage-secname { margin: 0; }
.ktgallery-ref { font-family: var(--b-font-mono); font-size: 13px; color: var(--text-secondary); }
.ktgallery-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: var(--role-primary-ink); background: var(--role-primary-soft); }
.ktgallery-impls { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; }
.ktgallery-noscript { margin: 0; }
@media (max-width: 900px) {
  .ktgallery-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 480px) {
  .ktrecur-rule { padding: 14px; }
  .ktrecur-basis .ktradio-nest { margin-left: 8px; padding-left: 12px; }
}
/* /TEN-1940 */

/* ── 6. The tri-color session bar ───────────────────────────────────────── */
/* here = --role-presence (people actually in the room), expected =
   --role-staged (booked and not arrived — the role's founding case), open =
   --role-vacant (unclaimed capacity).
   ALL THREE SEGMENTS ARE DRAWN, AND ALL THREE ARE LEGIBLE (K6's legibility
   clause, Mark's ruling 2026-09-07). Before this the open count was not a
   segment at all: two fills were painted and the remainder was left as the
   track's own --surface-overlay ground, with the legend dot on
   --border-subtle. On a phone in daylight that reads as nothing — and "6
   open" is the fact an organizer promotes a session off, so it is
   information, never chrome. It now takes --role-vacant, a neutral minted
   with actual wattage against the track ground in all three themes, and the
   segment carries a hairline against its neighbour so a boundary is a
   boundary even where two fills happen to sit close. */
.ktbar { display: flex; flex-direction: column; gap: 6px; }
.ktbar-track { width: 280px; max-width: 100%; height: 8px; border-radius: 999px; background: var(--surface-overlay); overflow: hidden; display: flex; }
.ktbar-seg { height: 100%; }
.ktbar-here { background: var(--role-presence); }
.ktbar-expected { background: var(--role-staged); }
.ktbar-open { background: var(--role-vacant); }
/* EVERY BOUNDARY IS DRAWN. Contrast is a luminance measure and hue is not part
   of it, so two adjacent fills can clear their ground handsomely and still sit
   at the same brightness as each other — in the light theme the presence green
   and the vacancy grey are within 1.05:1. A hairline in the track's own ground
   at the start of every segment after the first makes three regions three
   regions in every theme, without asking any pigment to do a job pigment
   cannot do. A zero-width segment draws nothing, which is correct. */
.ktbar-seg + .ktbar-seg { box-shadow: inset 1px 0 0 var(--surface-overlay); }
.ktbar-legend { display: flex; gap: 14px; font-size: 12.5px; color: var(--text-muted); flex-wrap: wrap; }
.ktbar-key { display: flex; align-items: center; gap: 5px; }
.ktbar-dot { width: 8px; height: 8px; border-radius: 2px; }
.ktbar-key-live { color: var(--role-presence); }
.ktbar-pip { width: 7px; height: 7px; border-radius: 50%; background: var(--role-presence); }

/* ── 7. The who's-affected footer ───────────────────────────────────────── */
.ktaffect { display: flex; flex-direction: column; gap: 10px; max-width: 620px; }
.ktaffect-body { background: var(--surface-page); border: 1px solid var(--border-subtle); border-radius: var(--control-r); padding: 14px 16px; display: flex; gap: 12px; align-items: flex-start; }
/* The bell rides the icon law (K13) — it only needs to be nudged onto the
   first line of the sentence beside it and to take the sentence's own ink. */
.ktaffect-bell { margin-top: 2px; color: var(--text-muted); }
.ktaffect-line { font-size: 13.5px; color: var(--text-muted); line-height: 1.55; margin: 0; }
/* The deal-impact line is the money verdict, and it resolves through
   --role-attention — the role that already means "money-pending, needs a
   look". K7 says "amber whenever present"; amber is what --role-attention is
   painted in the themes we ship, and the day a theme repaints it the line
   follows without this rule changing. */
.ktaffect-deal { background: var(--amber-a); border: 1px solid var(--role-attention); border-radius: var(--control-r); padding: 12px 14px; font-size: 13.5px; color: var(--role-attention); line-height: 1.5; }

/* ── 8. THE DEADLINE CLOCK ──────────────────────────────────────────────── */
.ktclock { border: 1px solid var(--border-subtle); border-radius: var(--card-r); background: var(--surface-raised); padding: 22px 24px 18px; display: flex; flex-direction: column; gap: 6px; color: var(--text-primary); }
.ktclock-hint { font-size: 13px; color: var(--text-muted); margin: 0 0 10px; line-height: 1.5; }
.ktclock-stage { position: relative; height: 240px; margin: 0 8px; }
/* THE CALENDAR RULER — THE SECOND OF TWO (Mark's punch r4). Calendar-day
   boundaries at true midnights, flush against the track's TOP edge, each with
   the weekday that starts there. The lead ruler (below the track) counts back
   from the session's own start time, so the two rows sit OFFSET by exactly
   that time of day — and the offset is the information: it answers "beginning
   or end of the day?" by construction. Never align them.

   The band is bottom-anchored so the marks touch the track no matter what the
   labels above them do, and it takes the K12 micro-label tier the lead axis
   already wears — same weight, same ink, because it is the same kind of
   reading. Ticks arrive as data (`calendar-ticks`), like everything else here. */
.ktclock-cal { position: absolute; top: 52px; left: 0; right: 0; height: 24px; }
.ktcal { position: absolute; bottom: 0; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 4px; }
/* The weekday carries the card's own ground as a HALO. On a wide track a stop's
   stem passes a midnight label with 25px to spare; on a phone the whole window
   is 270px and the gap between a lead marker and the midnight beside it is
   under 10px, so the 2px stem would cross the glyphs. The calendar ruler is
   drawn after the stops, so an opaque label simply interrupts the stem it
   passes behind — the cartographer's answer, and it costs nothing at the width
   where there was no collision to begin with. */
.ktcal-day { font-family: var(--b-font-mono); font-size: 9.5px; font-weight: 500; letter-spacing: 0.02em; line-height: 1; color: var(--text-muted); white-space: nowrap; background: var(--surface-raised); padding: 1px 3px; border-radius: 3px; }
.ktcal-mark { display: block; width: 1px; height: 8px; background: var(--text-muted); }
.ktclock-track { position: absolute; top: 76px; left: 0; right: 0; height: 40px; border-radius: var(--control-r); border: 1px solid var(--border-strong); overflow: hidden; display: flex; }
.ktzone { display: flex; align-items: center; justify-content: center; }
.ktzone-label { font-family: var(--b-font-heading); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; }
/* The gradient is the point: "free" fades toward the boundary rather than
   ending at a line, because the cost does not arrive all at once either. */
.ktzone-free { background: linear-gradient(90deg, transparent, var(--zone-releasable)); }
.ktzone-free .ktzone-label { color: var(--zone-releasable-ink); }
.ktzone-paid { background: var(--zone-committed); border-left: 1px solid var(--border-strong); }
.ktzone-paid .ktzone-label { color: var(--zone-committed-ink); }

/* THE AXIS (TEN-1565 kit v2). A bar with two markers on it is a diagram; a bar
   with a TIME SCALE under it is a graph, and the clock's whole claim is that
   where a marker sits and what it costs are the same picture. Majors carry a
   micro-label, minors are unlabelled subdivisions, and the rail sits between
   the track and the below-bar stems so it crosses nothing. The interval rule
   is data, not CSS — see K8 and kitClockTicks. */
.ktclock-axis { position: absolute; top: 116px; left: 0; right: 0; height: 22px; }
.kttick { position: absolute; top: 0; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 3px; }
.kttick-mark { display: block; width: 1px; height: 4px; background: var(--border-strong); }
/* The major mark takes the SAME ink as the label under it (--text-muted, K12's
   amended ladder) rather than the faint tier: the mark and its label are one
   reading, and half of it drawn in the disabled colour is half a scale. */
.kttick-major > .kttick-mark { height: 8px; background: var(--text-muted); }
/* The micro-label tier (K12): mono, because it is DATA, and --text-muted
   rather than --text-faint, because this cover's three-tier law reserves
   faint for genuinely inert things and a scale you cannot read is not a
   scale (tokens.css, TEN-1411/1454). */
.kttick-label { font-family: var(--b-font-mono); font-size: 9.5px; font-weight: 500; letter-spacing: 0.02em; line-height: 1; color: var(--text-muted); white-space: nowrap; }

.ktstop { position: absolute; display: flex; flex-direction: column; align-items: center; transform: translateX(-50%); text-decoration: none; cursor: grab; touch-action: none; }
.ktstop:active { cursor: grabbing; }
/* The above-bar stops start at the TOP of the stage now (punch r4): the
   calendar ruler took the band immediately over the track, so an attendee chip
   rides higher and its stem runs the extra distance down to the bar, PAST the
   weekday labels rather than into them. The stem is a 2px line and the marks
   it passes are 1px, so the two rulers and the markers share the band without
   anybody's label being clipped — and a stop and a midnight can never coincide
   anyway while the session starts at any time other than 00:00. */
.ktstop-above { top: 0; }
.ktstop-below { top: 140px; }
.ktstop-chip { font-size: 11.5px; font-weight: 700; border-radius: 6px; padding: 4px 9px; white-space: nowrap; }
.ktstop-stem { width: 2px; }
.ktstop-head { width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; }
/* The attendee marker is STAGED — a window that is still open — so it is the
   dashed one, in the staged role. */
.ktstop-attendee .ktstop-chip { border: 1.5px dashed var(--role-staged); color: var(--role-primary-ink); background: var(--role-primary-soft); }
.ktstop-attendee .ktstop-stem { background: var(--role-staged); }
.ktstop-attendee .ktstop-head { border-top: 7px solid var(--role-staged); }
/* The attendee-booking CLOSE, the other attendee-side stop — and the one that
   is absent far more often than it is present, because plenty of orgs welcome
   a late booking after a session has started. SOLID, not dashed: unlike the
   lock-in window, a close is a hard stop that either exists or does not, and
   dashed means staged (K12's shape rule). */
.ktstop-close .ktstop-chip { background: var(--role-primary); color: var(--ink-on-primary); }
.ktstop-close .ktstop-stem { background: var(--role-primary); }
.ktstop-close .ktstop-head { border-top: 7px solid var(--role-primary); }
.kteyebrow-close { color: var(--role-primary-ink); }
.ktstop-cancel .ktstop-chip { background: var(--role-attention); color: var(--ink-on-accent); box-shadow: 0 0 0 3px var(--amber-a); }
.ktstop-cancel .ktstop-stem { background: var(--role-attention); }
.ktstop-cancel .ktstop-head { border-bottom: 7px solid var(--role-attention); }
/* The venue's own deadlines are somebody else's fact: neutral, readable, and
   deliberately not wearing any of the acting roles. */
.ktstop-venue .ktstop-chip { background: var(--surface-overlay); color: var(--text-primary); border: 1px solid var(--border-strong); }
.ktstop-venue .ktstop-stem { background: var(--text-muted); }
.ktstop-venue .ktstop-head { border-bottom: 7px solid var(--text-muted); }
.ktstop-on .ktstop-chip { outline: 2px solid var(--role-primary); outline-offset: 2px; }
.ktstop:focus-visible .ktstop-chip { outline: var(--b-ring-w) solid var(--b-ring); outline-offset: 2px; }

/* THE EDGE CAPTIONS (punch r4) — both below the bar, two lines each. The
   CAPTION leads, in the reading tier: what happens at this edge is the thing
   somebody is here to read, and it used to be set in the muted tier under a
   date that was drawn heavier than it. The date and time follow in the muted
   tier, mono because it is a VALUE — the same face the two rulers' labels
   wear. Neither edge takes an accent: an edge is a fact about the window, not
   an act, and the accent on the left one made "attendee registration opens"
   look like something a pointer could press. */
.ktclock-edge { position: absolute; top: 140px; display: flex; flex-direction: column; gap: 3px; }
.ktclock-edge-l { left: 0; align-items: flex-start; }
.ktclock-edge-r { right: 0; align-items: flex-end; text-align: right; }
.ktclock-edgename { font-size: 12px; font-weight: 700; color: var(--text-primary); }
.ktclock-edgedate { font-family: var(--b-font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.02em; color: var(--text-muted); white-space: nowrap; }

.ktclock-panel { border: 1px solid var(--border-subtle); border-radius: var(--control-r); background: var(--surface-page); padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.ktclock-panelhead { display: flex; align-items: center; justify-content: space-between; }
.kteyebrow-cancel { color: var(--role-attention); }
.kteyebrow-venue { color: var(--text-muted); }
.kteyebrow-attendee { color: var(--role-primary-ink); }
.ktclock-sentence { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 14.5px; }
.ktclock-sentence .ktinput { background: var(--surface-raised); height: 38px; }
.ktclock-panelhint { font-size: 12.5px; color: var(--text-muted); margin: 0; line-height: 1.5; }
.ktclock-foot { line-height: 1.55; }

/* ── 8b. THE BOOKING TIMELINE (TEN-1609) ────────────────────────────────────
   The deadline clock's grammar with the editing taken out: the same two rulers
   (.ktcal above the track, .kttick below it — reused rather than restyled, so
   two graphs of time in this book cannot end up wearing two type ramps), the
   same edge-caption block, no drag affordance and no panels.

   WHAT IS NEW HERE IS THE SEGMENTED RAIL. A booking window can span a week at
   one end and two hours at the other, so above a declared ratio the bar is
   drawn as two true-to-scale linear pieces with a BREAK GLYPH between them. The
   rail is a flex row and each piece carries its own percentage width from the
   server; every position inside a piece is still an exact instant, which is why
   the pieces are laid out by width and the marks inside them by percent.

   The chips are NOT the clock's `.ktstop`: those are anchors that drag, with a
   grab cursor and a selected outline, and wearing that skin on something you
   cannot press is the "live-looking control that goes nowhere" K11 forbids one
   rung up. Same shape, same roles, no affordance. */
.kttl { border: 1px solid var(--border-subtle); border-radius: var(--card-r); background: var(--surface-raised); padding: 20px 22px 16px; display: flex; flex-direction: column; gap: 6px; color: var(--text-primary); }
.kttl-hint { font-size: 13px; color: var(--text-muted); margin: 0 0 10px; line-height: 1.5; }
/* THE EDIT GATE (TEN-1631's round-10 amendment). The bar is a live MIRROR at rest
   and an INPUT only while the gate is open, and these are the three controls that
   say so: the pencil in the caption row, and the Done/Cancel pair at the foot.
   Both blocks are rendered `hidden` by the plate and revealed by the asset, so a
   scriptless page — which has no drag to gate — shows neither.
   `[hidden]` IS RESTATED ON BOTH, and it is not belt and braces: these rules set
   `display`, which has the same specificity as the reset's `[hidden] { display:
   none }` and comes LATER in this sheet, so without the restatement a hidden gate
   draws (the trap .kttl-mark[hidden] already carries, three surfaces in). */
.kttl-cap { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.kttl-pencil { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; flex-shrink: 0; padding: 0; border: 0; border-radius: var(--control-r); background: transparent; color: var(--text-muted); cursor: pointer; }
.kttl-pencil[hidden] { display: none; }
.kttl-pencil:hover { color: var(--role-primary-ink); background: var(--hover); }
.kttl-pencil:focus-visible { outline: var(--b-ring-w) solid var(--b-ring); outline-offset: 2px; }
.kttl-gate { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; padding-top: 12px; border-top: 1px solid var(--border-subtle); }
.kttl-gate[hidden] { display: none; }
/* AND THE CARD SAYS IT IS BEING EDITED. One accent edge, which is the quietest
   thing that distinguishes a bar you can move from a bar you are reading — the
   chrome at the foot is the statement; this is the frame around it. */
.kttl-editing { border-color: var(--role-primary); }
/* THE BANDS (re-cut by TEN-1631 for the five dedicated elevations — three above
   the track, two below it). Every number here has a twin in kittimeline.go's
   `kitTLPlace` block, which is where the per-mark half of the same geometry
   lives: a stem's LENGTH depends on which elevation its mark is on, so it is
   rendered as data, while a band's own position is not and is here. That file
   names this one; keep them in step. */
.kttl-stage { position: relative; height: 276px; margin: 0 8px; }
.kttl-rail { position: relative; display: flex; align-items: stretch; height: 236px; }
.kttl-seg { position: relative; height: 236px; }
.kttl-cal { position: absolute; top: 72px; left: 0; right: 0; height: 24px; }
.kttl-track { position: absolute; top: 96px; left: 0; right: 0; height: 34px; border-radius: var(--control-r); border: 1px solid var(--border-strong); background: var(--surface-overlay); overflow: hidden; }
/* THE SESSION IS THE ONE PAINTED REGION, and it is a FACT rather than a
   consequence: the session runs from here to there. It takes the primary role's
   soft weight because it is the thing every deadline on the bar is measured
   against — not because it is an act.
   ITS THREE NUMBERS ARE THE SERVER'S (TEN-1631) — right offset, width, and the
   legibility floor — so there is no geometry for this block in this sheet at
   all. It is right-anchored so the floor grows it LEFT, which is the only
   direction that survives the track's own `overflow: hidden`; kittimeline.go
   states what that costs. */
.kttl-session { position: absolute; top: 0; bottom: 0; background: var(--role-primary-soft); border-left: 1px solid var(--role-primary); display: flex; align-items: center; justify-content: center; }
.kttl-session-label { font-family: var(--b-font-heading); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; color: var(--role-primary-ink); }
.kttl-axis { position: absolute; top: 130px; left: 0; right: 0; height: 22px; }
/* THE BREAK GLYPH: a cut in the track, drawn as the axis-break convention draws
   one — two parallel strokes with the card's own ground between them, at the
   track's own vertical band so it reads as the bar being interrupted rather
   than as a marker sitting on it. */
.kttl-break { position: relative; flex: 0 0 auto; }
.kttl-break::before, .kttl-break::after { content: ""; position: absolute; top: 92px; height: 42px; width: 2px; background: var(--border-strong); transform: skewX(-18deg); }
.kttl-break::before { left: 30%; }
.kttl-break::after { left: 62%; }

.kttl-mark { position: absolute; display: flex; flex-direction: column; align-items: center; transform: translateX(-50%); }
/* HIDDEN MEANS HIDDEN, and it has to be said HERE. `.kttl-mark`'s own `display:
   flex` and the reset's `[hidden] { display: none }` have equal specificity, so
   whichever comes later in the sheet wins — and a positioned sprite that ignored
   `hidden` would draw every armed-but-off boundary on the bar at once (TEN-1629;
   the same trap three surfaces have now met). */
.kttl-mark[hidden] { display: none; }
/* A DRAGGABLE SPRITE ANSWERS THE POINTER (K11's pickable-card clause, applied to
   a marker): the cursor says it can be taken, the chip lifts under the hand, and
   the focus ring is the keyboard rung's — arrows move it one unit at a time. The
   affordance is on the mark the server declared draggable and on no other, so a
   boundary this page cannot author never invites a press. */
/* AND THE AFFORDANCE IS GATED WITH THE ABILITY (TEN-1631's round-10 amendment).
   `.kttl-mark-drag` says the page CAN write this value; `.kttl-editing` on the card
   says it may right now — so the grab cursor and `touch-action: none` live under
   the second. At rest a sprite is a picture: the cursor does not invite a press,
   and a touch that starts on one scrolls the page instead of being swallowed by a
   drag that will not happen. A scriptless page never carries the class, which is
   the same answer for the same reason. */
.kttl-mark-drag:focus-visible { outline: none; }
.kttl-editing .kttl-mark-drag { cursor: grab; touch-action: none; }
.kttl-editing .kttl-mark-drag:active, .kttl-mark-dragging { cursor: grabbing; }
.kttl-mark-drag:focus-visible .kttl-chip, .kttl-mark-dragging .kttl-chip { box-shadow: 0 0 0 3px var(--role-primary-soft); }
.kttl-editing .kttl-mark-drag .kttl-chip { cursor: grab; }
/* A MARK'S `top` IS STAMPED BY THE SERVER (TEN-1631) — it is the elevation the
   CARD gave that boundary, and the two rules that used to set it here could only
   say "above" and "below". What stays here is the side's own DIRECTION, which is
   about markup order rather than about a number. */
.kttl-chip { font-size: 11.5px; font-weight: 700; border-radius: 6px; padding: 4px 9px; white-space: nowrap; }
/* THE PRESSED READOUT (TEN-1631). The chip carries the boundary's NAME; this
   carries its VALUE, under the pin, inside the track, and only while the sprite
   is held. Its `top` is the server's too, for the same reason the stem's height
   is: how far the track is from a mark's own box is a consequence of the
   elevation. Centred on the pin and never wider than it needs — it is a reading
   of one number, not a label. */
.kttl-read { position: absolute; left: 50%; transform: translateX(-50%); z-index: 2; font-family: var(--b-font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.02em; white-space: nowrap; padding: 3px 8px; border-radius: 5px; background: var(--surface-raised); border: 1px solid var(--border-strong); color: var(--text-primary); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); }
/* AND IT HANGS INWARD NEAR AN EDGE, exactly as the chip does and for the same
   reason: it is centred on its own pin, which puts half of it off the card when
   the pin is at either end — and a deadline near the start is always at one.
   Same `kitTLAlign` classes, so one decision drives both labels. */
.kttl-mark-l .kttl-read { transform: translateX(-7px); }
.kttl-mark-r .kttl-read { transform: translateX(calc(-100% + 7px)); }
/* A CHIP NEAR AN EDGE HANGS INWARD, and the stem does not move (kitTLAlign).
   A chip is centred on its own instant, which puts half of it off the bar at
   either end — and "closes 30 minutes before start" is always at that end. The
   label slides; the position it points at is exactly where it was. */
.kttl-mark-l .kttl-chip { transform: translateX(calc(50% - 7px)); }
.kttl-mark-r .kttl-chip { transform: translateX(calc(-50% + 7px)); }
.kttl-stem { width: 2px; }
.kttl-head { width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; }
.kttl-mark-above .kttl-head { border-top: 7px solid; }
.kttl-mark-below .kttl-head { border-bottom: 7px solid; }
/* The early bird's window is still OPEN, so it is the dashed one — dashed means
   staged (K12's shape rule) and the two other deadlines are hard stops. */
.kttl-mark-early .kttl-chip { border: 1.5px dashed var(--role-staged); color: var(--role-primary-ink); background: var(--role-primary-soft); }
.kttl-mark-early .kttl-stem { background: var(--role-staged); }
.kttl-mark-early .kttl-head { border-top-color: var(--role-staged); border-bottom-color: var(--role-staged); }
/* The commitment cutoff is the one that costs somebody money, and
   --role-attention is the role that already means "money-pending, needs a
   look" — the same reading the who's-affected footer's deal line takes. */
.kttl-mark-commit .kttl-chip { background: var(--role-attention); color: var(--ink-on-accent); }
.kttl-mark-commit .kttl-stem { background: var(--role-attention); }
.kttl-mark-commit .kttl-head { border-top-color: var(--role-attention); border-bottom-color: var(--role-attention); }
.kttl-mark-close .kttl-chip { background: var(--role-primary); color: var(--ink-on-primary); }
.kttl-mark-close .kttl-stem { background: var(--role-primary); }
.kttl-mark-close .kttl-head { border-top-color: var(--role-primary); border-bottom-color: var(--role-primary); }
.kttl-mark-opens .kttl-chip { background: var(--surface-overlay); color: var(--text-primary); border: 1px solid var(--border-strong); }
.kttl-mark-opens .kttl-stem { background: var(--text-muted); }
.kttl-mark-opens .kttl-head { border-top-color: var(--text-muted); border-bottom-color: var(--text-muted); }
/* SESSION VISIBLE (TEN-1629) — the attendee horizon, the outermost boundary and
   the one this card does not author. It is drawn QUIETEST of the five for that
   reason: a dashed outline in the muted ink, so it reads as the edge of the
   picture rather than as another deadline competing with the three settings. */
.kttl-mark-visible .kttl-chip { background: transparent; color: var(--text-muted); border: 1px dashed var(--border-strong); }
.kttl-mark-visible .kttl-stem { background: var(--border-strong); }
.kttl-mark-visible .kttl-head { border-top-color: var(--border-strong); border-bottom-color: var(--border-strong); }

.kttl-edge { position: absolute; top: 240px; display: flex; flex-direction: column; gap: 3px; max-width: 46%; }
.kttl-edge-l { left: 0; align-items: flex-start; }
.kttl-edge-r { right: 0; align-items: flex-end; text-align: right; }
.kttl-edgename { font-size: 12px; font-weight: 700; color: var(--text-primary); }
.kttl-edgedate { font-family: var(--b-font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.02em; color: var(--text-muted); line-height: 1.4; }
.kttl-foot { line-height: 1.55; }
/* THE LIVE REFUSAL (TEN-1631) — the two orders that cannot be broken, said the
   moment a typed value breaks one. It wears the attention role rather than the
   note's muted ink because it is a thing to fix rather than a thing to know, and
   it is a `role="alert"` so a screen reader hears it without hunting. */
.kttl-refuse { color: var(--text-primary); border-top-color: var(--role-attention); }

/* The booking card's duration rows: a ktdur mount and the words the count is
   measured back from, on one line (K11 — a control speaks its unit, never a
   bare number). It wraps rather than squeezing, because "before a session
   starts" is a phrase and half of it on a phone is worse than a second line. */
.clsdur { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 2px 0 4px; }

/* THE TIMELINE'S CHROME IS NOT SELECTABLE (TEN-1636, Mark's UAT round 13).
   Dragging a sprite across a bar full of tick labels, chip text and the word
   SESSION sweeps a text selection along with the pointer — the browser's default
   for a gesture that starts on text — which leaves half the card highlighted and,
   on a long drag, hands the selection the pointer events instead of the sprite.
   The repro is the drag itself.

   IT IS ON THE STAGE, which is the whole instrument: the rulers, the calendar
   band, the session block's label, every chip and every pressed readout. Nothing
   inside it is prose somebody would want to copy — the words that ARE prose (the
   hint above the bar, the footnote and the refusal under it) sit outside the stage
   and are untouched, which is the line this rule is drawn on rather than "the
   whole component".

   `-webkit-` first, unprefixed second: Safari still wants the prefix on this
   property and the unprefixed one must win where both are understood. */
.kttl-stage {
  -webkit-user-select: none; user-select: none;
}

/* ── 9. The step-wizard shell ───────────────────────────────────────────── */
.ktwiz { display: flex; flex-direction: column; gap: 28px; background: var(--surface-page); padding: 28px; border-radius: var(--card-r); color: var(--text-primary); }
/* THE HEAD SLOT (TEN-1636) — the block above the title on a shell that is not the
   top of its own page. It is spaced tighter than the shell's own 28px rhythm
   because it belongs WITH the title under it: a trail and the thing it leads to,
   then the name of the section. */
.ktwiz-above { display: flex; flex-direction: column; gap: 12px; margin-bottom: -10px; }
.ktwiz-head { display: flex; flex-direction: column; gap: 18px; }
/* THE EYEBROW (TEN-1658) — the title block's top line, naming what this screen's
   subject belongs to. The head's own gap is 18px, which is the distance between the
   title and the STEPPER under it; an eyebrow that far above its own title would read
   as a separate block, so this one pulls back to the 6px the entity card's eyebrow
   sits at. The two lines are one statement. */
.ktwiz-eyebrow { margin-bottom: -12px; }
.ktwiz-title { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 24px; font-weight: 800; }
.ktsteps { display: flex; align-items: center; gap: 20px; list-style: none; margin: 0; padding: 0; }
.ktstep { display: flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 600; }
/* The connector is drawn on the step, not between them — one element fewer to
   get out of step with the list. */
.ktstep + .ktstep::before { content: ""; width: 48px; height: 1px; background: var(--border-strong); margin-right: 12px; }
.ktstep-num { width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; }
.ktstep-done { color: var(--role-presence); }
.ktstep-done .ktstep-num { background: var(--green-a); color: var(--role-presence); }
.ktstep-current { color: var(--role-primary-ink); }
.ktstep-current .ktstep-num { background: var(--role-primary); color: var(--ink-on-primary); }
/* --text-muted, not --text-faint (K12's amended text ladder). A todo step is
   not switched off — it is the reader's own map of what is coming, and the
   outlined numeral already carries "not yet". Faint would have said "this
   step is unavailable", which is not what a stepper means. */
.ktstep-todo { color: var(--text-muted); }
.ktstep-todo .ktstep-num { border: 1.5px solid var(--border-strong); color: var(--text-muted); }
/* The shell's incoming flash (TEN-1578) wears the app's own .flash family — the
   same treatment the build and manage surfaces give the same value — and only
   gives up its trailing margin, because .ktwiz is a flex column that already
   owns the rhythm and the two would stack. */
.ktwiz-flash { margin-bottom: 0; }
.ktwiz-cols { display: flex; gap: 32px; align-items: flex-start; flex-wrap: wrap; }
.ktwiz-main { flex: 1 1 460px; display: flex; flex-direction: column; gap: 22px; min-width: 0; }
.ktrail { width: 360px; flex: 0 0 auto; max-width: 100%; }
/* K9's footer: Back and Continue APART, so the backwards direction is never the
   one under your thumb. `space-between` says that at a width where there is any
   between to speak of — and at a phone's, where there is not, it said nothing:
   two buttons whose labels fill the row end up shoulder to shoulder with no seam
   at all, which is where Mark met them (TEN-1610, UAT round 7). The gap is the
   floor under the rule rather than a replacement for it: at every width the
   layout already handled, `space-between` still wins and this changes nothing;
   at the width it did not, the two acts stop touching. Wrapping is the same
   thought one step further on — a pair that cannot fit on one line belongs on
   two, not squeezed until the words break. */
.ktwiz-foot { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }

/* ── 10. The entity cards ───────────────────────────────────────────────── */
.ktprev { border: 1px solid var(--border-subtle); border-radius: var(--control-r); background: var(--surface-raised); padding: 16px; display: flex; flex-direction: column; gap: 6px; }
.ktprev-name { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 17px; font-weight: 700; }
.ktprev-meta { font-size: 13.5px; }
.kthub { flex-direction: row; align-items: center; gap: 28px; flex-wrap: wrap; padding: 18px 22px; }
.kthub-ident { display: flex; flex-direction: column; gap: 6px; }
/* THE EYEBROW (TEN-1658) — the card's top line, above the name. It carries no
   spacing of its own: `.kthub-ident` already sets the 6px rhythm every line in the
   block sits on, and a margin here would put the class further from its session
   than the session is from its meta. As a link it drops the underline at rest and
   takes it back on hover, which is the treatment a quiet mention wears everywhere
   in this book — an underlined eyebrow reads as a control rather than as context. */
.kthub-eyebrow { align-self: flex-start; }
a.kthub-eyebrow { text-decoration: none; color: var(--text-muted); }
a.kthub-eyebrow:hover { color: var(--role-primary-ink); text-decoration: underline; }
.kthub-nameline { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.kthub-name { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 24px; font-weight: 800; }
.kthub-meta { font-size: 14px; }
/* The Where line (TEN-1612): its own line under the meta, because a place is a
   mention somebody presses rather than one more fact in a joined string. */
.kthub-where { font-size: 14px; }
/* THE AUDIENCE LINE (TEN-1644) — a count under the Where line, with the sentence
   that says who those people are under it again. The label and the number sit on
   one row because they are one fact ("Audience · 42 people" read aloud), and the
   explainer is a line of its own because it is a sentence rather than a tail. */
.kthub-aud { font-size: 14px; }
.kthub-audlabel { margin-right: 8px; }
.kthub-audnote { display: block; margin-top: 2px; font-size: 13px; line-height: 1.45; max-width: 62ch; }
.kthub-side { margin-left: auto; display: flex; flex-direction: column; gap: 4px; align-items: flex-end; text-align: right; font-size: 14.5px; }
/* THE CARD'S ACTS, AT ITS TOP RIGHT (TEN-1644). `align-self: flex-start` is the
   "top" half and the auto margin is the "right" half — and the auto margin is
   dropped when the side block already took one, because two auto margins split the
   leftover space and would leave the NEXT SESSION block floating in the middle of
   the card instead of beside the acts. At a width where the card wraps, the acts
   take a row of their own, which is the same thing the side block does. */
.kthub-acts { margin-left: auto; align-self: flex-start; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.kthub-side ~ .kthub-acts { margin-left: 0; }
/* The icon act: K12's quietest tier — no box at rest, the ink of the page, and the
   hover/focus treatment the kit's own pencil wears one component over, because an
   icon act on a card is one rung and must not have two looks. */
.kthub-iconact {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; flex-shrink: 0; border-radius: var(--control-r);
  color: var(--text-muted); text-decoration: none;
}
.kthub-iconact:hover { color: var(--role-primary-ink); background: var(--hover); }
.kthub-iconact:focus-visible { outline: var(--b-ring-w) solid var(--b-ring); outline-offset: 2px; }
.ktprog { display: flex; flex-direction: column; gap: 6px; width: 300px; max-width: 100%; }
.ktprog-line { font-size: 15px; }
.ktprog-track { height: 8px; border-radius: 999px; background: var(--surface-overlay); overflow: hidden; }
.ktprog-fill { display: block; height: 8px; background: var(--role-primary); border-radius: 999px; }
.ktrow { display: flex; align-items: center; gap: 14px; padding: 11px 14px; border-radius: var(--control-r); border: 1px solid var(--border-subtle); background: var(--surface-raised); flex-wrap: wrap; }
.ktrow-now { border: 1.5px solid var(--role-primary); background: var(--role-primary-soft); }
/* DASHED MEANS STAGED (K12's shape rule, --role-staged) — the same edge the
   hub's grid cell wears, so one session reads the same in both of its views. */
.ktrow-staged { border-style: dashed; border-color: var(--role-staged); background: var(--surface-page); }
/* THE INERT ROW (TEN-1864) — an archived class's card: still a thing somebody
   reads and opens, and not running. It resolves --wattage-inert rather than
   naming a number, which is the whole of K12-amended's rule: one inert wattage,
   declared in tokens.css and nowhere else, so the next inert thing in this book
   has somewhere to ask rather than a literal to copy.

   IT IS ON THE ROW, so the fade carries the title, the lines, the tails and the
   type chip together — an archived card that dimmed only its text would leave a
   full-strength border reading as live. The row's ACT is exempt below: a Delete
   this card exists to offer must not be the faintest thing on it, which is K12's
   pairing rule (a dimmed thing still wears a visible affordance saying why — here
   the "Archived" tail says it and the act stays legible).

   Not pointer-events:none, deliberately. The row still opens, because the class
   page is exactly where an organizer goes to restore or destroy it. */
.ktrow-off { opacity: var(--wattage-inert); }
.ktrow-off:hover, .ktrow-off:focus-within { opacity: 1; }
.ktrow-off > .ktrow-link { opacity: 1; }
.ktrow-lead { width: 170px; font-size: 14px; font-weight: 600; }
.ktrow-mid { flex: 1; font-size: 13.5px; color: var(--text-muted); min-width: 120px; }
.ktrow-tail { font-size: 13px; }
.ktrow-link { font-size: 13px; }
/* THE ROW'S ACT MAY BE DESTRUCTIVE (TEN-1864). `danger-quiet` is the OUTLINE rung
   of K12's two-arm pair — it OPENS the confirmation and commits nothing, so it is
   deliberately not the filled danger button; that one lives inside the dialog and
   is the only thing on the page that destroys anything. Same vocabulary as the
   settings screen's own Archive control, at the row rung. */
.ktrow-link-danger-quiet { color: var(--role-danger); font-weight: 600; }
.ktrow-link-danger-quiet:hover { text-decoration-thickness: 2px; }
/* THE ROW'S EYEBROW (TEN-1661 item 7) — the hub rung's own top line, at the row
   rung: what this row's subject is a part of, above it rather than beside it.
   FULL-WIDTH BY FLEX BASIS rather than by a second layout: the row is already a
   wrapping flex line, so a 100% basis puts the eyebrow on its own line and leaves
   every other child exactly where it was. The row's vertical rhythm loosens with
   it (`align-items: flex-start`, a tighter gap) because a two-line row centred on
   its own middle puts the lead level with nothing. */
.ktrow-eyebrow { flex-basis: 100%; margin-bottom: -8px; }
.ktrow-browed { align-items: flex-start; row-gap: 10px; }

/* THE WHOLE ROW IS THE LINK (TEN-1602). Mark, UAT round 6: "each entire class
   card should link to the hub, with a hover state — not just the title."

   STRETCHED LINK, not an anchor around the row. The row can carry a second act
   in `.ktrow-link`, and an anchor wrapping the row would contain it — nested
   anchors are invalid and the browser unnests them, which is a layout that
   changes shape depending on the data. So the LEAD stays the one anchor, and
   its ::after covers the card: one destination, one accessible name (the class
   is still what a screen reader announces), and the hit area a pointer expects.

   `.ktrow-linked` is the plate's marker for "this row has a way in" — the row
   whose lead is a span gets no overlay and no hover, because a row that lights
   up under the pointer and does nothing when pressed is the same broken promise
   one rung smaller.

   ONLY A SECOND ADDRESS IS LIFTED ABOVE THE OVERLAY. `.ktrow-link` names a
   different destination, so it has to stay reachable; the meta, the tail and
   the chips are FACTS, and a fact that intercepted the press would leave a card
   with dead patches — which is the defect this rule exists to remove, wearing
   different clothes. */
.ktrow-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.ktrow-linked:hover { background: var(--hover); border-color: var(--border-strong); }
/* THE IDENT WRAPPER IS A SECOND PARENT, AND THE STRETCH FOLLOWS IT (TEN-1793).
   class05 put a follower chip beside the title, which means the lead is a
   grandchild of the row on any card that carries one — so a one-selector rule
   silently dropped the card-wide hit area on exactly the rows Mark was looking at.
   Two selectors rather than a descendant one (`.ktrow-linked .ktrow-lead`), because
   the row may hold a SECOND anchor in `.ktrow-link` and a loose selector would
   stretch whichever came first. */
.ktrow-linked > .ktrow-lead[href]::after,
.ktrow-linked > .ktrow-ident > .ktrow-lead[href]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.ktrow-linked > .ktrow-link { position: relative; z-index: 1; }
/* The focus ring belongs to the ROW once the row is the target: a 170px lead
   with a card-wide hit area would otherwise outline a sixth of what a keyboard
   press activates. */
.ktrow-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.ktrow-linked > .ktrow-lead[href]:focus-visible,
.ktrow-linked > .ktrow-ident > .ktrow-lead[href]:focus-visible { outline: none; }
/* A hovered staged row keeps its dashed edge, exactly as a staged grid cell
   does: hover is a pointer's position, never a change of state. */
.ktrow-staged.ktrow-linked:hover { border-color: var(--role-staged); }

/* ── 11. The small controls ─────────────────────────────────────────────── */
.ktstepper-row { display: flex; align-items: center; gap: 12px; }
.ktstepper { display: inline-flex; align-items: center; border: 1px solid var(--border-strong); border-radius: var(--control-r); overflow: hidden; background: var(--surface-raised); }
.ktstepper-btn { width: 40px; height: var(--control-h); display: flex; align-items: center; justify-content: center; font-size: 18px; color: var(--text-muted); text-decoration: none; }
.ktstepper-btn:first-child { border-right: 1px solid var(--border-subtle); }
.ktstepper-btn:last-child { border-left: 1px solid var(--border-subtle); }
.ktstepper-btn:hover { background: var(--hover); color: var(--text-primary); }
.ktstepper-box { width: 64px; height: var(--control-h); display: flex; align-items: center; justify-content: center; }
.ktstepper-box input { width: 100%; text-align: center; border: 0; background: transparent; font: inherit; font-family: var(--b-font-mono); font-size: 16px; font-weight: 700; color: var(--text-primary); }
.ktstepper-box input:focus { outline: none; }
.ktstepper:focus-within { border-color: var(--role-primary); box-shadow: 0 0 0 3px var(--role-primary-soft); }
.ktstepper-row .ktsub { font-size: 14px; }

.kttog-row { gap: 10px; padding: 20px 24px; }
.kttog-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.kttog-hint { font-size: 13.5px; line-height: 1.5; margin: 0; }
.kttoggle { position: relative; display: inline-flex; flex-shrink: 0; cursor: pointer; }
.kttoggle input { position: absolute; opacity: 0; width: 0; height: 0; }
.kttoggle-track { width: 44px; height: 26px; border-radius: 999px; background: var(--border-strong); display: flex; align-items: center; justify-content: flex-start; padding: 2px; box-sizing: border-box; }
/* The knob rides --ink-on-primary, which is the one token guaranteed to read
   against a --role-primary fill in every theme — and it happens to read
   against the neutral off-track too, so the switch needs no second rule. */
.kttoggle-knob { width: 22px; height: 22px; border-radius: 50%; background: var(--ink-on-primary); }
/* The SERVER'S render of an on toggle; live drawing is the R9 block below. */
.kttoggle:focus-within .kttoggle-track { box-shadow: 0 0 0 3px var(--role-primary-soft); }

.ktradios { gap: 12px; padding: 20px 24px; }
.ktradio { display: flex; gap: 12px; align-items: flex-start; cursor: pointer; }
.ktradio input { position: absolute; opacity: 0; width: 0; height: 0; }
.ktradio-dot { width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--border-strong); background: var(--surface-raised); flex-shrink: 0; margin-top: 2px; box-sizing: border-box; }
/* The SERVER'S render of a chosen radio; live drawing is the R9 block below. */
.ktradio:focus-within .ktradio-dot { box-shadow: 0 0 0 3px var(--role-primary-soft); }
.ktradio-text { display: flex; flex-direction: column; gap: 2px; }
.ktradio-title { font-size: 14.5px; color: var(--text-muted); }
/* THE NESTED FORM (TEN-1629): an option's own entry form, childed under it. It
   is INDENTED TO THE RADIO'S TEXT — 30px is the dot plus the row's gap — so the
   form reads as belonging to the sentence above it rather than to the card, and
   the rule down its left edge is the same statement drawn: this hangs off that.
   The rule goes to the accent while the option is chosen, which is the only
   thing on the block CSS draws from state (R9: the drawing is the stylesheet's,
   the quieting is the server's and the asset's). */
.ktradio-nest { margin: 2px 0 6px 30px; padding: 12px 0 4px 16px; border-left: 2px solid var(--border-subtle); display: flex; flex-direction: column; gap: 6px; }
.ktradio-nest:not([data-ktquiet]) { border-left-color: var(--role-primary); }
/* The nest's own trailing words sit on the row, not under it. */
.ktradio-nest .clsdur { margin: 0; }
.ktradio-nest .ktlabel { margin-top: 6px; }
/* Fixed platform guarantees, kept visibly apart from the settings (R8). */
.ktalways { background: var(--surface-page); border-radius: var(--control-r); padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.ktalways-line { font-size: 13.5px; color: var(--text-muted); line-height: 1.5; margin: 0; }

/* THE CHOOSER'S GRID IS TWO COLUMNS AT EVERY WIDTH (TEN-1793) — see the
   ktchoices block at the foot of this file for the argument. The auto-fit rule
   that stood here collapsed to ONE column below 620px, which is the vertical list
   Mark reported: four large cards stacked is a screenful of scrolling to compare
   four things, which is the one job a chooser has. */
.ktchoice { position: relative; border: 1px solid var(--border-subtle); border-radius: var(--card-r); background: var(--surface-raised); padding: 22px; display: flex; flex-direction: column; gap: 10px; cursor: pointer; }
.ktchoice input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The SERVER'S render of a picked card. It STAYS unconditional for the anchor
   rung, which has no input to read: a chooser card's selected state is a fact
   about the address, not about a checkbox. The label rung is redrawn live by
   the R9 block below, which wins on specificity while a box is checked. */
.ktchoice-on { border: 2px solid var(--role-primary); background: var(--role-primary-soft); padding: 21px; }
.ktchoice:focus-within { box-shadow: 0 0 0 3px var(--role-primary-soft); }
.ktchoice-tick { position: absolute; top: 16px; right: 16px; width: 20px; height: 20px; border-radius: 50%; background: var(--role-primary); color: var(--ink-on-primary); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; }
.ktchoice-badge { position: absolute; top: 16px; right: 16px; font-size: 11px; font-weight: 600; color: var(--text-muted); border: 1px solid var(--border-strong); border-radius: 999px; padding: 3px 10px; background: var(--surface-raised); }
.ktchoice-icon { line-height: 0; color: var(--text-muted); }
.ktchoice-on .ktchoice-icon { color: var(--role-primary-ink); }
.ktchoice-title { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 18px; font-weight: 700; }
.ktchoice-body { font-size: 14px; line-height: 1.45; color: var(--text-muted); }
/* --text-muted, not --text-faint (K12's amended text ladder, Mark's ruling
   2026-09-07). The "e.g." line is the fastest way to tell a class from a
   course — it is the most READ line on the card — and it was drawn in the
   colour that now means "switched off", which is why the whole tier kept
   reading as disabled. Faint is a STATE now; only the coming-soon card below
   still wears it, where the low contrast IS the message. */
.ktchoice-eg { font-size: 12.5px; color: var(--text-muted); }
/* HOVER (Mark's punch r3). A pickable card answers the pointer, in the book's
   own hover grammar: the neutral ground wash the ghost button and the picker
   rows already use, plus the border going to the accent so the card previews
   what picking it looks like. Selected keeps its own tinted ground — a hover
   must not read as a de-selection. */
.ktchoice:not(.ktchoice-off):hover { border-color: var(--role-primary); background: var(--hover); }
.ktchoice-on:not(.ktchoice-off):hover { background: var(--role-primary-soft); }
.ktchoice:not(.ktchoice-off):hover .ktchoice-icon { color: var(--role-primary-ink); }
/* Coming soon: switched off, badged, and genuinely unpickable — the radio is
   disabled, so the card cannot lie about being available. Disabled is a STATE
   (K12): the card keeps its role and loses its contrast. NO HOVER — a card
   that lit up under the pointer would be promising a press it will not take,
   which is the same lie in motion. */
.ktchoice-off { background: var(--surface-page); cursor: default; }
.ktchoice-off .ktchoice-icon, .ktchoice-off .ktchoice-title, .ktchoice-off .ktchoice-body { color: var(--text-faint); }
/* The card AS the control (R9): an anchor card wearing the same skin, with the
   link's own colours refused — a card is not a sentence with a link in it. */
.ktchoice-link { text-decoration: none; color: inherit; }

/* ── R9: A CONTROL DRAWS ITS OWN CHECKED STATE ──────────────────────────────
   THE BUG THIS BLOCK IS THE FIX FOR, because the shape of it generalises to
   every control on every surface. Mark reported the day chips, the $/% segments
   and the choice cards as UNCLICKABLE. Every one of them was working perfectly:
   the label wrapped the input, the click ticked the box, the form posted the
   right thing. What did not happen was any PICTURE of it — because `.ktday-on`,
   `.ktseg-opt-on`, `.kttoggle-on`, `.ktradio-on` and `.ktchoice-on` were classes
   the SERVER stamped, describing what was checked at render time, and a class
   cannot move without another render. The control's state was right and its
   drawing was a snapshot; the only way to refresh the snapshot was the postback
   R9 forbids.

   THE FIX IS THE PLATFORM'S OWN, NOT A SCRIPT. `:has(input:checked)` is the
   browser drawing its own control, so the chip lights the instant the pointer
   lands and there is no JavaScript in the loop at all. A script toggling these
   classes would be a second implementation of `:checked` — the exact drift the
   K-laws exist to stop — which is why kitcontrols.js never touches a class that
   means "selected".

   THE SERVER'S CLASSES DO NOT PAINT ALONGSIDE THIS, and getting that wrong was
   the second half of the same bug. A `-on` class left painting would go STALE
   the other way — the toggle Mark switched off would stay lit, because the
   class describing "on at render time" was still on the element. So the
   `-on` rules live in the @supports fallback at the bottom of this block: they
   are the render a browser without :has() gets, and they cannot argue with the
   live one because only one of the two is ever in effect.

   The chooser's ANCHOR card is the exception and keeps its class
   unconditionally: it has no input to read, and its selected state is a fact
   about the address rather than about a checkbox. */
.ktday, .ktroom, .ktseg-opt, .kttoggle, .ktradio, .ktchoice { position: relative; }
.ktday:has(input:checked) { background: var(--role-primary); border-color: var(--role-primary); color: var(--ink-on-primary); }
/* The room chip lights exactly as the day chip does (TEN-1625) — same rule, same
   reason, and written beside it rather than near the component so the family
   cannot acquire a second answer to "what does chosen look like". */
.ktroom:has(input:checked) { background: var(--role-primary); border-color: var(--role-primary); color: var(--ink-on-primary); }
.ktseg-opt:has(input:checked) { background: var(--role-primary); color: var(--ink-on-primary); }
.kttoggle:has(input:checked) .kttoggle-track { background: var(--role-primary); justify-content: flex-end; }
.ktradio:has(input:checked) .ktradio-dot { border: 5px solid var(--role-primary); }
.ktradio:has(input:checked) .ktradio-title { font-weight: 600; color: var(--text-primary); }
.ktchoice:has(input) { border: 1px solid var(--border-subtle); background: var(--surface-raised); padding: 22px; }
.ktchoice:has(input:checked) { border: 2px solid var(--role-primary); background: var(--role-primary-soft); padding: 21px; }
.ktchoice:has(input:checked):not(.ktchoice-off):hover { background: var(--role-primary-soft); }

@supports not selector(:has(*)) {
  .ktday-on { background: var(--role-primary); border-color: var(--role-primary); color: var(--ink-on-primary); }
  .ktroom-on { background: var(--role-primary); border-color: var(--role-primary); color: var(--ink-on-primary); }
  .ktseg-opt-on { background: var(--role-primary); color: var(--ink-on-primary); }
  .kttoggle-on .kttoggle-track { background: var(--role-primary); justify-content: flex-end; }
  .ktradio-on .ktradio-dot { border: 5px solid var(--role-primary); }
  .ktradio-on .ktradio-title { font-weight: 600; color: var(--text-primary); }
}

/* [hidden] MUST ACTUALLY HIDE, and on this kit it is load-bearing rather than
   tidy: every reveal R9 introduced — the Ends date field, an unadded pricing
   option, the add menu's panel, a remove that only appears on the second row —
   is expressed as the `hidden` attribute, and each of those elements has a
   `display` rule of its own that beats the UA stylesheet on specificity. Same
   catch, same fix, as `.console-modal[hidden]` and `.msg-row[hidden]` above. */
.ktinput[hidden], .ktprice-opt[hidden], .ktaddmenu[hidden], .ktaddmenu-panel[hidden],
.ktaddmenu-opt[hidden], .ktdayrow-x[hidden], .ktcov-new[hidden], .ktcov-rest[hidden],
.kttime-menu[hidden], .kttime-opt[hidden], .ktmodal[hidden],
/* TEN-1656: the place card (one per option, all but the picked one down) and the
   dropdown's filtered-out rows and their now-empty group headers. Every one of
   these sets its own `display`, so each is on this list for the reason the whole
   list exists rather than for tidiness. */
.ktplace-card[hidden], .ktsel-opt[hidden], .ktsel-grp[hidden],
.ktsel-find[hidden], .ktsel-find-none[hidden],
.ktplace-roomblock[hidden] { display: none !important; }

/* ── The modal (TEN-1581) ───────────────────────────────────────────────── */
/* A LAYER, not a block — but only where there is room for one. On a phone the
   panel is the screen, which is the honest rendering of "focused, interruptive,
   single-step" (R3) rather than a small box somebody has to aim at. */
.ktmodal { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 24px; }
/* ── A DIALOG LIVES INSIDE THE VISUAL VIEWPORT (K39, TEN-1834) ─────────────
   Mark's phone UAT on v0.1.245: with the keyboard up, the invite dialog's
   lower half — the personal line and the submit — was behind the keys. The
   rule his ruling minted is a FAMILY rule, so this is the family's own block
   and no dialog restates it.

   `inset: 0` above is the LAYOUT viewport, and the layout viewport is not what
   a keyboard shrinks. kitcontrols.js pins the root to the VISUAL viewport's
   rectangle — inline `top/left/width/height`, because the keyboard's height is
   a runtime fact no media query can test — and stamps this attribute while
   that pin is on. The numbers are the script's; what is DRAWN in the box it
   hands back is this stylesheet's, which is the same division emoji.js and
   `[data-emoji-anchored]` already make for the picker (TEN-1228).

   TWO CHANGES, and each is one clause of the ruling:
   - UPPER REGION, not centred. Centring inside a box the keyboard has already
     halved still spends half the remaining room above the dialog. Top-anchored
     is the convention phones use, and it is the same call TEN-1126 made for the
     console's own dialogs — this is that decision reaching the kit's family.
   - A SMALLER GUTTER. 24px on each side of a 250px-tall viewport is a fifth of
     it spent on air. 8px keeps the dialog reading as a layer without charging
     a phone a quarter of its screen for the border.

   The panel needs nothing: `.ktmodal-panel` is already `max-height: 100%;
   overflow-y: auto`, so a dialog taller than what is left scrolls ITSELF
   inside the re-sized root — the ruling's third clause, already true. */
.ktmodal[data-ktmodal-vv] { align-items: flex-start; padding: 8px; }
/* The veil is an ANCHOR (it closes with no scripting), so it needs the display
   and the reset an <a> does not bring: no underline, no ink, no focus ring on a
   thing that is not a reading target. */
.ktmodal-veil { position: absolute; inset: 0; background: var(--b-scrim, rgba(0, 0, 0, 0.55)); display: block; text-decoration: none; }
.ktmodal-panel {
  position: relative; z-index: 1; width: 460px; max-width: 100%;
  max-height: 100%; overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
  background: var(--surface-raised); color: var(--text-primary);
  border: 1px solid var(--border-strong); border-radius: var(--control-r);
  box-shadow: var(--b-shadow-md); padding: 22px 24px;
}
.ktmodal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.ktmodal-title { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 17px; font-weight: 700; }
.ktmodal-hint { margin: -6px 0 0; font-size: 13px; line-height: 1.5; }
.ktmodal-field { display: flex; flex-direction: column; gap: 5px; }
.ktmodal-field .ktinput { width: 100%; }
/* THE COMPOSED BODY (TEN-1656's slot, first skinned TEN-1658). The panel is already
   a 14px-gapped column, so this is a block in that column and brings only the
   internal rhythm its own contents need. It sets no width, no padding and no border:
   what arrives here is a composition the surface built, and a slot that restyled it
   would be the dialog having an opinion about somebody else's component. */
.ktmodal-body { display: flex; flex-direction: column; gap: 14px; }
/* Apart, and the primary on the right — the wizard's own footer grammar, so a
   dialog does not teach a different one (K12: one advancing act per region). */
.ktmodal-foot { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 4px; }

/* ── THE PLACE CARD (TEN-1612, blessed board 18) ─────────────────────────────
   The kit's modal wearing a place instead of a form. It is WIDER than the
   dialog's default — board 18 draws 560px — because the panel's 460 was sized
   for two stacked fields and this body is a map area with a street line under
   it. `max-width: 100%` still comes from `.ktmodal-panel`, so a phone gets the
   screen exactly as every other dialog does. */
.ktpcard { width: 560px; gap: 16px; }
.ktpcard-ident { display: flex; align-items: center; gap: 12px; min-width: 0; }
.ktpcard-avatar { width: 44px; height: 44px; }
/* The UNCLAIMED placeholder: a DASHED ring around a pin, because dashed means
   staged everywhere in this book (K12's shape rule) and an unclaimed place is a
   claim nobody has made yet. --role-staged, so a theme cannot drift it apart
   from the pricing kit's add row or the clock's attendee marker. */
.ktpcard-pinbox {
  width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px dashed var(--role-staged); color: var(--text-muted);
  display: flex; align-items: center; justify-content: center; box-sizing: border-box;
}
.ktpcard-pinbox .ktic { width: 22px; height: 22px; }
.ktpcard-idtext { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ktpcard-nameline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ktpcard-name { font-size: 17px; }
.ktpcard-runby { font-size: 13px; }

/* The tile: a STATIC image with a pin on it and a handoff chip in the corner.
   Nothing here pans or zooms — board 18's lens note — and the whole thing is
   one anchor, so the chip is a label rather than a second control. */
.ktpcard-tile {
  position: relative; display: block; height: 210px; border-radius: var(--control-r);
  overflow: hidden; background: var(--surface-overlay); text-decoration: none;
}
/* THE PICTURE'S MIDDLE, AT THE CARD'S OLD SCALE (TEN-1894). The tile is the
   1024×512 window the place page expands into; drawn at 160% of the frame and
   centred, its middle lands exactly where the old 640×360 picture's did (1024 is
   640 × 1.6, and both are centred on the pin), so the card shows the same
   neighbourhood at the same scale. The 420px floor is twice the frame's height —
   the least width at which a 2:1 picture still covers it top to bottom. */
.ktpcard-tileimg {
  position: absolute; left: 50%; top: 50%; display: block;
  width: max(160%, 420px); max-width: none; height: auto; transform: translate(-50%, -50%);
}
.ktpcard-pin {
  position: absolute; left: 50%; top: 50%; width: 26px; height: 26px;
  transform: translate(-50%, -85%); color: var(--role-attention);
  fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
.ktpcard-tilechip {
  position: absolute; right: 10px; bottom: 10px; background: var(--surface-raised);
  border-radius: var(--control-r); padding: 6px 12px; font-size: 12.5px; font-weight: 600;
  color: var(--text-primary); box-shadow: var(--b-shadow-md);
}
/* THE CREDIT, and it is small and quiet but never absent: the provider's terms
   require it wherever the imagery appears (TEN-1617). It sits ON the tile, in
   the corner opposite the handoff chip, over a scrim so it stays legible on a
   dark map and on a pale one — a credit that disappears into the imagery is the
   same breach as a credit that was never printed. */
.ktpcard-credit {
  position: absolute; left: 8px; bottom: 8px; font-size: 10.5px; line-height: 1.2;
  color: var(--text-primary); background: color-mix(in srgb, var(--surface-raised) 78%, transparent);
  border-radius: 4px; padding: 2px 6px; max-width: calc(100% - 120px);
}
.ktpcard-addr { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.ktpcard-addrtext { font-size: 14px; color: var(--text-primary); min-width: 0; }
.ktpcard-go { font-size: 13.5px; font-weight: 600; white-space: nowrap; text-decoration: none; }
.ktpcard-go:hover { text-decoration: underline; }
.ktpcard-golinks { display: flex; align-items: center; gap: 14px; margin-left: auto; }
/* NO TILE, AND THIS IS A REAL STATE rather than a placeholder for a picture:
   the pin and the street line ARE the location, and both handoffs sit on them.
   A dashed or shimmering box here would be promising an image the seam has not
   promised (placecard.go's placeMapTile). */
.ktpcard-nomap {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border: 1px solid var(--border-subtle); border-radius: var(--control-r);
  background: var(--surface-page); padding: 14px 16px;
}
.ktpcard-nomapicon { color: var(--text-muted); }
/* The third header state, and it is a SENTENCE — never an empty grey map. */
.ktpcard-noaddr { margin: 0; font-size: 13.5px; color: var(--text-muted); }
.ktpcard-rooms {
  display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--text-muted);
  border-top: 1px solid var(--border-subtle); padding-top: 12px;
}
/* --text-muted, not --text-faint: the room line is read (K12's amended ladder). */
.ktpcard-roomicon { color: var(--text-muted); }
.ktpcard-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; border-top: 1px solid var(--border-subtle); padding-top: 14px;
}
.ktpcard-page { white-space: nowrap; }

/* ── The add-a-pricing-option MENU (R9) ─────────────────────────────────────
   K4's dashed add row, made to name its options rather than pick one for you.
   The trigger is the row that was always there; the panel is a small menu
   under it, and it is `hidden` rather than absent so the browser can reveal it
   without asking the server what the options are. */
.ktaddmenu { position: relative; display: flex; flex-direction: column; }
.ktaddmenu-panel {
  display: flex; flex-direction: column; margin-top: 6px;
  border: 1px solid var(--border-strong); border-radius: var(--card-r);
  background: var(--surface-raised); overflow: hidden;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
}
.ktaddmenu-opt { display: flex; flex-direction: column; gap: 2px; padding: 10px 14px; text-decoration: none; color: var(--text-primary); }
.ktaddmenu-opt + .ktaddmenu-opt { border-top: 1px solid var(--border-subtle); }
.ktaddmenu-opt:hover { background: var(--hover); }
.ktaddmenu-name { font-size: 14px; font-weight: 600; color: var(--role-primary-ink); }

/* ── The tag field, now the picker kit's tokpick (R9 clause 4) ──────────────
   With the picker running, `.tokpick-source` takes the select out of sight and
   the kit's own chip UI stands in its place; these rules are what the field
   looks like on the way there and with scripting off. The raw multi-select is
   skinned on the kit's own terms rather than left in the browser's, which is
   K11a applied to the one control shape that has no popup to theme. */
.ktcov-tags { display: flex; flex-direction: column; gap: 10px; margin: 10px 0 4px; }
.ktcov-pick {
  font: inherit; font-size: 14px; color: var(--text-primary);
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--control-r); padding: 6px 8px; min-height: 84px;
}
.ktcov-pick:focus { outline: none; border-color: var(--role-primary); box-shadow: 0 0 0 3px var(--role-primary-soft); }
/* NO `display` HERE, and that is load-bearing. `[data-scriptless-only]` hides
   this box wherever scripting is on, and that rule is an ATTRIBUTE selector —
   the same specificity as a class — so a `display: flex` on `.ktcov-new` later
   in this file wins on source order and shows the scriptless box to everybody.
   It did, and the wizard grew a second way to author a tag beside the picker.
   The direction and the gap are safe; the display is the attribute's to give. */
.ktcov-new { flex-direction: column; gap: 4px; }
.ktcov-tags .tokpick-field { min-height: var(--control-h); }

/* ── The kit page (/kit) ────────────────────────────────────────────────── */
/* The one surface in this book that captions its own parts: it exists to be
   clicked through by the person who owns the design language. */
.ktpage { background: var(--surface-page); color: var(--text-primary); padding: 32px 40px; border-radius: var(--card-r); display: flex; flex-direction: column; gap: 36px; font-family: var(--b-font-body); }
.ktpage-head { display: flex; flex-direction: column; gap: 10px; }
.ktpage-title { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.04em; font-size: 28px; font-weight: 800; margin: 0; color: var(--text-primary); }
.ktpage-lede { font-size: 14.5px; color: var(--text-muted); line-height: 1.55; margin: 0; max-width: 720px; }
.ktpage-toc { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.ktpage-tocitem { font-size: 12.5px; font-weight: 600; color: var(--role-primary-ink); background: var(--surface-raised); border: 1px solid var(--border-subtle); border-radius: 999px; padding: 4px 12px; text-decoration: none; }
.ktpage-tocitem:hover { border-color: var(--role-primary); }
.ktpage-sec { display: flex; flex-direction: column; gap: 8px; scroll-margin-top: 24px; }
.ktpage-secname { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 20px; font-weight: 800; margin: 0; padding-top: 12px; border-top: 2px solid var(--border-strong); color: var(--text-primary); }
.ktpage-law { font-size: 13.5px; color: var(--text-muted); line-height: 1.55; margin: 0 0 12px; max-width: 780px; }
.ktpage-lawlbl { font-family: var(--b-font-heading); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; color: var(--role-primary-ink); text-transform: uppercase; margin-right: 6px; }
.ktpage-spec { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }
.ktpage-speclbl { font-family: var(--b-font-mono); font-size: 11.5px; font-weight: 500; letter-spacing: 0.02em; color: var(--text-muted); }
.ktpage-stage { background: var(--surface-page); }
/* THE MODAL, STAGED (TEN-1581). A fixed layer would cover the gallery it is
   being reviewed in, so on this page — and only on this page — it is flattened
   to a block and the veil is dropped. The PANEL is untouched: what a reviewer
   is looking at is the real one, drawn by the real plate. */
.ktpage-stage .ktmodal { position: static; padding: 0; display: block; }
.ktpage-stage .ktmodal-veil { display: none; }
.ktpage-stage .ktmodal-panel { box-shadow: none; max-height: none; }

/* The action-tier row (K12 amended, TEN-1565). Side by side and in ladder
   order, because the whole claim of the grammar is that the ranking is legible
   at a glance — a tier shown alone proves nothing. The caption is page copy on
   the internal kit page (this is the one surface in the module allowed to label
   its own parts), and it names the CLASS, since the class is what a builder
   types. */
.kttiers { display: flex; flex-wrap: wrap; gap: 18px; align-items: flex-start; }

/* THE KIT PAGE'S OWN STAGING FOR ktcal (K32, TEN-1750), and it is the REVIEW
   CHROME rather than the family — the same thing `.kttiers` is. A calendar
   owns its grid; the kit page is not a calendar, so it declares the smallest
   one that shows a cell at its real width and nothing else. Putting a grid in
   the ktcal block instead would have been the family growing a layout that its
   consumers are supposed to bring. */
.ktpage-calweek { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--b-space-1); }
.ktpage-calrow { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start; }
/* The dot rung's cell is small by definition — this is the width a mini
   calendar's day actually is, which is the point of showing it at all. */
.ktpage-calrow .ktcal-day-dots { width: 56px; height: 56px; box-sizing: border-box; }
.ktpage-calrow .ktcal-block { width: 170px; }
.kttier { display: flex; flex-direction: column; gap: 8px; width: 250px; }
.kttier-cap { font-size: 12.5px; line-height: 1.5; color: var(--text-muted); }
.kttier-cls { display: block; font-family: var(--b-font-mono); font-size: 11.5px; color: var(--text-primary); }

/* The kit page needs the room the wizard's two-column layout is ABOUT: at the
   cover's ordinary 720px reading column the content and its preview rail stack,
   and a stacked rail is not the thing being reviewed. Widened by the same
   :has() move the messages screen already uses — and guarded by @supports for
   the same reason, so a browser without :has() keeps the bounded column rather
   than an unbounded one. Scoped to the kit page alone; no other surface on this
   cover changes width. */
@supports selector(:has(*)) {
  body.app:has(.ktpage) main.container { max-width: 1180px; }
}

@media (max-width: 720px) {
  .ktpage { padding: 20px 16px; }
  .ktwiz { padding: 18px; }
  .ktrail { width: 100%; }
  .ktsteps { flex-wrap: wrap; gap: 10px; }
  .ktstep + .ktstep::before { display: none; }
  .ktplace-field { width: 100%; }
  .ktclock-stage { margin: 0; }
}

/* ══ THE WAVE-2 CREATION SURFACES (TEN-1573, epic TEN-1563) ═══════════════
   The `cls` block is the chooser, the 3-step class wizard and the class hub.

   IT ADDS NO CONTROLS. Every control on those three screens is a kit plate,
   and every wrapper below is either a kit primitive (.ktcard, .ktlabel,
   .ktinput, .ktsub, .ktnote, .ktseg, .ktavatar) or LAYOUT — a field stack, a
   two-column split, a seven-column week. The only rules here that paint
   anything paint with roles the kit already established: --role-primary for
   today, --role-staged for a dashed staged cell, because dashed means staged
   everywhere in this book (K12) and the two dashed things must not drift apart
   in a theme.
   ────────────────────────────────────────────────────────────────────────── */

/* ── The wizard steps ───────────────────────────────────────────────────── */
/* ── The class settings train's head (TEN-1636) ──────────────────────────
   Breadcrumbs, and under them the class every screen on the train is about.

   THE SEPARATOR IS HERE AND NOT IN THE MARKUP, which is the whole reason this
   block exists: a chevron between two rungs is decoration between two links, and
   rendered as text it is read out between every pair on the way to the page name.
   `::before` on every crumb but the first is the same arrangement the book's other
   inline dividers use.

   The crumb you are ON is the muted tier and carries no underline: it is a
   statement about where you are, not a place to go. */
.clscrumbs-list { display: flex; flex-wrap: wrap; align-items: center; gap: 0; list-style: none; margin: 0; padding: 0; font-size: 13px; }
.clscrumb { display: flex; align-items: center; }
.clscrumb + .clscrumb::before { content: "\203A"; color: var(--text-faint); margin: 0 8px; }
.clscrumb-link { color: var(--text-muted); text-decoration: none; }
.clscrumb-link:hover, .clscrumb-link:focus-visible { color: var(--text-primary); text-decoration: underline; }
.clscrumb-here { color: var(--text-primary); font-weight: 600; }
/* The event card is the entity card's ROW rung, so it brings its own box; this
   only keeps it from stretching to a width nobody reads a name at. */
.clshead .ktrow { max-width: 720px; }

.clsstep { display: flex; flex-direction: column; gap: 22px; }
/* The early bird's window row (TEN-1636): the nests' own spacing, on a block that
   hangs under no radio — so it states its own label rather than borrowing one. */
.clsearly { max-width: 720px; }
/* The review step: a reading column, because it has no preview rail beside it
   to fill the wizard's second column. */
.clsstep-narrow { max-width: 720px; margin: 0 auto; width: 100%; }
.clsfield { display: flex; flex-direction: column; gap: 8px; }
.clsfield-wide { width: 420px; max-width: 100%; }
.clsfield-date { width: 200px; }
.clsfield-sel { width: 190px; }
.clsfield-ends { gap: 10px; }
.clslabel-sm { font-size: 13px; }
.clsrow { display: flex; gap: 32px; flex-wrap: wrap; align-items: flex-start; }
/* Starts and Ends sit SIDE BY SIDE (board 2): one is a date and the other is
   two sentences about that date, and separating them puts a card between a
   question and its qualifier. The ends block is bounded so it cannot take the
   whole row and push the date onto a line of its own. */
.clsfield-ends { flex: 0 1 420px; min-width: 300px; }
.clsfield-ends .ktradios { padding: 16px 18px; }
.clstimes { display: flex; flex-direction: column; gap: 12px; }
.clsadd { align-self: flex-start; }
.clshoriz { display: flex; gap: 40px; flex-wrap: wrap; }
.clshoriz-hint { width: 200px; max-width: 100%; line-height: 1.45; margin: 0; }
/* The wizard's own form wrapper carries no box of its own — kit-wizard draws
   the surface, and this exists only because a submit needs a form around it. */
.clswrap { display: block; }

/* The verdict block. It is NOT a control and NOT chrome: it is the server's own
   sentences, so it takes the attention role the coverage card's loud-empty
   state already uses rather than a colour minted for it. */
.clsverdicts { border-color: var(--role-attention); gap: 8px; }
.clsverdicts-title { color: var(--role-attention); font-size: 14px; }
.clsverdict { font-size: 14px; line-height: 1.5; color: var(--text-primary); margin: 0; }

/* ── The preview rail's body ────────────────────────────────────────────── */
.clsrail-sec { display: flex; flex-direction: column; gap: 8px; }
.clsrail-lines { display: flex; flex-direction: column; gap: 5px; font-size: 13.5px; color: var(--text-primary); }
.clsrail-note { margin-top: 4px; }

/* ── The review ─────────────────────────────────────────────────────────── */
.clsreview { gap: 14px; }
.clsreview-edits { display: flex; gap: 16px; flex-wrap: wrap; border-top: 1px solid var(--border-subtle); padding-top: 12px; font-size: 13.5px; }
.clschecks { display: flex; flex-direction: column; gap: 10px; }
.clscheck { display: flex; gap: 10px; align-items: flex-start; font-size: 14.5px; line-height: 1.5; color: var(--text-primary); margin: 0; }
.clscheck-mark { color: var(--role-presence); font-weight: 700; flex-shrink: 0; }
.clsreview-foot { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.clsreview-draft { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-left: auto; }
.clsreview-back { margin-right: auto; font-size: 14px; }
.clsreview-drafthint { max-width: 280px; line-height: 1.45; }

/* ── Your classes (TEN-1584) ────────────────────────────────────────────────
   The organizer's listing. It draws NO row of its own: the rows are the kit's
   entity card at its list-row rung, so everything below is the page around
   them — the heading block, the spacing between rows, and the one card the
   empty state and the read's refusals share.

   The quiet link that reaches this screen from the chooser (.clsfind) lives
   here too, beside the surface it points at rather than with the wizard's
   steps, because it is this listing's front door. */
.clslist { display: flex; flex-direction: column; gap: 20px; color: var(--text-primary); }
.clslist-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.clslist-ident { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }
.clslist-back { font-size: 13px; }
/* K19 RUNG 3 (TEN-1671) — the page's own identity, and ONE definition of it.
   Mark named the "Your classes" shape as the model for every console page, so
   the console shell's title (`.orgc-title`) and the /manage desks' (`.pgtitle`)
   are the same rule rather than three that look alike until one is edited. */
.clslist-title, .orgc-title, .pgtitle { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 24px; font-weight: 800; margin: 0; }
.clslist-lede, .orgc-lede, .pglede { margin: 0; max-width: 70ch; line-height: 1.45; }
.orgc-lede, .pglede { color: var(--text-muted); font-size: var(--b-text-sm); }
.orgc-ident, .pgident { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--b-space-3); }
.pgback { margin: 0 0 var(--b-space-2); font-size: 13px; }
.clslist-rows { display: flex; flex-direction: column; gap: 10px; }
/* THE ARCHIVED TAIL (TEN-1864) — a native <details>, so the chevron and the
   open/closed state are the element's, not a script's. The marker is replaced
   with our own triangle for one reason: the platform's default glyph differs per
   browser, and a listing's one expansion should read the same everywhere in this
   cover. The summary is a HEADING-weight row rather than a button, because it
   names a section of the list it sits in. */
.clslist-arch { margin-top: 22px; border-top: 1px solid var(--border-subtle); padding-top: 14px; }
.clslist-archsum { display: flex; align-items: baseline; gap: 8px; cursor: pointer; list-style: none; padding: 4px 0; }
.clslist-archsum::-webkit-details-marker { display: none; }
.clslist-archsum::before { content: ""; width: 0; height: 0; border-left: 5px solid var(--text-muted); border-top: 4px solid transparent; border-bottom: 4px solid transparent; transition: transform 0.12s ease; flex: none; }
.clslist-arch[open] > .clslist-archsum::before { transform: rotate(90deg); }
.clslist-archsum:focus-visible { outline: 2px solid var(--role-primary); outline-offset: 2px; border-radius: var(--control-r); }
.clslist-archlabel { font-size: 14px; font-weight: 650; }
.clslist-archcount { font-size: 13px; }
.clslist-archhint { margin: 2px 0 10px; font-size: 13px; }
.clslist-archrows { margin-top: 10px; }
/* The loud empty state, and the refusal that wears the same block. Left-aligned
   and bounded rather than centred: it is a sentence to read, not a poster. */
.clslist-empty { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
.clslist-emptyline { margin: 0; font-size: 15.5px; font-weight: 650; }
.clslist-hint { margin: 0; max-width: 66ch; line-height: 1.45; }
.clslist-act { margin-top: 4px; }
.clsfind { margin: 4px 0 0; font-size: 13.5px; }

/* ── The place record editor (TEN-1613, board 19) ───────────────────────────

   The screen behind "Your places → a place": five sections that read as
   sentences with exactly ONE of them open. The shut face of every one of them
   is a kit sentence card and is already styled; what is here is the page around
   them, the open face's field frame, and the two `hidden` rules the disclosure
   depends on.

   THE `hidden` RULES ARE LOAD-BEARING AND NOT TIDINESS. kitcontrols.js's own
   header states the measured behaviour: `hidden` on an element that a
   stylesheet gives a `display` to does NOT hide it — an attribute selector
   loses to a class on specificity, and the UA stylesheet loses to both. Both
   faces below are flex containers, so both need the explicit
   `display: none !important` or the reveal would draw the sentence and the
   editor at once. */
.plrec { display: flex; flex-direction: column; gap: 20px; color: var(--text-primary); }
.plrec-top { display: flex; flex-direction: column; gap: 6px; }
.plrec-back { font-size: 13px; align-self: flex-start; }
.plrec-ident { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.plrec-name { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 24px; font-weight: 800; margin: 0; }
.plrec-lede { margin: 0; max-width: 70ch; line-height: 1.45; }
.plrec-flash { margin: 0; }
.plrec-note { padding: 14px 20px; }
.plrec-noteline { margin: 0; font-size: 14px; line-height: 1.5; color: var(--text-muted); }

/* THE DID-YOU-MEAN (board 14b, TEN-1617). It sits above the sections, in the
   attention hue rather than the danger one: nothing here went wrong and nothing
   needs undoing — a place somebody just pinned is beside one already on the map,
   and the question is whether they are the same building. Red would say a save
   failed; this asks. */
.plnudge {
  padding: 16px 20px; display: flex; flex-direction: column; gap: 10px;
  border-color: var(--role-attention);
}
.plnudge-line { margin: 0; font-size: 15px; font-weight: 650; }
.plnudge-hint { margin: 0; line-height: 1.5; max-width: 70ch; }
.plnudge-rows { display: flex; flex-direction: column; gap: 10px; }
.plnudge-row {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  border-top: 1px solid var(--border-subtle); padding-top: 10px;
}
.plnudge-what { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.plnudge-name { font-size: 14.5px; font-weight: 650; }
.plnudge-acts { display: flex; gap: 10px; flex-wrap: wrap; margin-left: auto; }

.plsecs { display: flex; flex-direction: column; gap: 12px; }
.plsec { display: flex; flex-direction: column; }
.plsec-shut, .plsec-body { display: flex; flex-direction: column; }
.plsec-shut[hidden], .plsec-body[hidden] { display: none !important; }
/* The open face wears the accent edge the wireframe draws on it: the one
   section being changed is the one thing on the page with the accent, which is
   how a glance finds it after a save lands somewhere else. */
.plsec-form { border-color: var(--role-primary); gap: 16px; }
.plsec-title { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.03em; font-size: 16px; font-weight: 700; }
.plsec-foot { display: flex; align-items: center; gap: 10px; }
.plform { display: block; }

.plfield { display: flex; flex-direction: column; gap: 6px; max-width: 520px; }
.plfield .ktinput { width: 100%; }
.plfield-hint { margin: 0; line-height: 1.5; max-width: 66ch; }

.plrooms { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.plrooms-rows { display: flex; flex-direction: column; gap: 8px; align-self: stretch; }
.plroom { display: flex; align-items: center; gap: 10px; }
.plroom-box { flex: 1; }
.plroom-x { flex-shrink: 0; }
.plroom-x[hidden] { display: none !important; }
.plrooms-add { align-self: flex-start; }

/* The map block in its HONEST no-map state — a bordered note, deliberately not
   a grey rectangle at map proportions. A box the size of a map that contains no
   map reads as a map that failed to load; a note reads as what it is. */
.plmap { display: flex; flex-direction: column; gap: 4px; padding: 14px 16px; border: 1px dashed var(--border-strong); border-radius: var(--card-r); background: var(--surface-page); max-width: 520px; }
.plmap-line { font-size: 14px; font-weight: 650; }
.plmap-hint { line-height: 1.5; }
/* THE LIT STATE (TEN-1617). A solid border rather than the dashed one, because
   dashed means STAGED (K12's shape rule) and a pin that is really saved is not
   staged — the same distinction the place card draws between its dashed
   unclaimed pin and a real avatar. The picture leads, the sentence and the
   credit sit under it. */
.plmap-lit { border-style: solid; border-color: var(--border-subtle); padding: 0; overflow: hidden; gap: 0; }
.plmap-lit .plmap-line { padding: 10px 16px 0; }
/* The editor frames the picture's middle the way the card does (TEN-1894): the
   tile is the 1024×512 window, drawn at 160% of the frame and centred, which is
   the scale the old 640×360 picture was framed at. 360px is twice the frame's
   height — the least width at which a 2:1 picture still covers it. */
.plmap-frame { position: relative; display: block; height: 180px; overflow: hidden; }
.plmap-tile {
  position: absolute; left: 50%; top: 50%; display: block;
  width: max(160%, 360px); max-width: none; height: auto; transform: translate(-50%, -50%);
}
.plmap-credit { padding: 2px 16px 12px; font-size: 11px; }

/* The danger dialog's body lines (kit-modal's `lines`): plain sentences about
   who is affected, read once before a press that cannot be taken back lightly. */
.ktmodal-lines { display: flex; flex-direction: column; gap: 8px; }
.ktmodal-line { margin: 0; font-size: 14px; line-height: 1.55; color: var(--text-muted); }

/* ── The class hub ──────────────────────────────────────────────────────── */
.clshub { display: flex; flex-direction: column; gap: 24px; color: var(--text-primary); }
/* THE TOP IS A COLUMN NOW (TEN-1644): the trail (or the attendee's one link back),
   then the class card. It used to be a space-between row with two buttons in its
   right half — those moved inside the card, so there is no second column here to
   align against, and at every width this is the same stack. `.clshub-ident` and
   `.clshub-acts` went with the row: the wrapper had nothing left to wrap and the
   buttons are `.kthub-acts` now, drawn by the card that owns them. */
.clshub-top { display: flex; flex-direction: column; gap: 10px; }
.clshub-back { font-size: 13px; }
.clshub-notice { margin: 0; }
/* `.clshub-cols`, `.clshub-main` and `.clshub-side` are GONE (TEN-1644 item 6). The
   Audience card was the side column's only tenant; with the block removed a
   two-column wrapper around one child is just a place for an orphaned gap to open
   up, so the hub's own column holds the blocks directly and they share its width. */

/* The next-session strip. Its left border is the accent — it marks the row
   somebody is about to act on, which is the same job the picker's current row
   already gives --role-primary. */
.clsnext { flex-direction: row; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; border-left: 3px solid var(--role-primary); }
.clsnext-body { display: flex; flex-direction: column; gap: 10px; flex: 1; min-width: 0; }
.clsnext-line { font-size: 15px; }

/* THE WHOLE BLOCK IS THE LINK (TEN-1657 item 2). Mark, UAT round 15: the next
   session block should hover and click through to the session, as the class cards
   already do.

   IT IS THE ROW RUNG'S CONSTRUCTION, and deliberately not a new one: the LEAD
   stays the one anchor and its ::after covers the card, because this block also
   carries the Check-in BUTTON and an anchor wrapping the block would contain it —
   nested anchors are invalid, the browser unnests them, and the layout would then
   change shape depending on whether the next session is staged. `.ktrow-linked`
   one file over is the same three rules for the same reason.

   ONLY THE BUTTON IS LIFTED ABOVE THE OVERLAY. It names a different address, so it
   has to stay reachable; the sub-line and the fill bar are FACTS, and a fact that
   intercepted the press would leave the card with dead patches — the defect this
   rule exists to remove, wearing different clothes.

   THE LEAD KEEPS THE PAGE'S INK AND TAKES NO UNDERLINE, which is the CELL's rule
   (TEN-1644 item 9) rather than the row's. The row's lead is a class's NAME, so
   accent ink says "this name is the way in"; this lead is the words "Next
   session", a label for the block under it — drawn in link ink it would claim the
   phrase is the target when the whole card is, and an underline on hover would say
   it twice. The block answers the pointer with its ground and its edge.

   THE LEFT BORDER SURVIVES THE HOVER. It is the accent marking the row somebody is
   about to act on; a hover that repainted the edge would take it, so only the
   ground and the three neutral sides move. */
.clsnext-lead { color: inherit; text-decoration: none; }
.clsnext-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.clsnext-linked:hover { background: var(--hover); border-color: var(--border-strong); border-left-color: var(--role-primary); }
.clsnext-linked > .clsnext-body .clsnext-lead[href]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.clsnext-linked > .clsnext-act { position: relative; z-index: 1; }
/* The focus ring belongs to the CARD once the card is the target: outlining the
   two words of the lead would ring a tenth of what a keyboard press activates. */
.clsnext-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.clsnext-linked .clsnext-lead[href]:focus-visible { outline: none; }

/* A REGION WAITING FOR ITS REPLACEMENT (TEN-1657 item 4). `aria-busy` is the
   pager's only mark on the page while a swap is in flight, and this is the whole
   of what it draws: the block dims a little so the press is acknowledged, and
   nothing about it moves — a spinner or a collapse would make the swap a layout
   change, which is the thing the swap exists to avoid. Presses inside a block
   that is being replaced go nowhere, which is honest rather than merely tidy.
   Reduced motion takes the fade and keeps the state. */
[data-ktpage-region][aria-busy="true"] { opacity: 0.55; pointer-events: none; transition: opacity 0.12s ease; }
@media (prefers-reduced-motion: reduce) {
  [data-ktpage-region][aria-busy="true"] { transition: none; }
}
/* The add-a-session dialogs are the schedule's second paged region and exist only
   to be NAMED, so the wrapper must not be a box: they are fixed overlays and
   `.clshub` is a flex column with a 24px gap, which an in-flow element of zero
   height would open at the foot of every hub. */
.clsadhocs { display: contents; }

.clssched { display: flex; flex-direction: column; gap: 12px; }
.clssched-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.clssched-nav { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.clssched-step { height: 30px; width: 30px; padding: 0; }
.clssched-range { font-size: 13.5px; }
/* THE SEGMENTS HUG THE RIGHT, by an auto margin on the first of them rather
   than by `space-between` (TEN-1644). The head holds a title, one pager per
   scope and now two segments, and `space-between` would spread four or five
   items across the row — so the positioning rule says the one thing that is
   actually meant: everything left, the two controls right, however many
   pagers the reveal leaves standing. The head's own `gap` spaces the pair. */
.clssched-scope { margin-left: auto; font-size: 13px; }
.clssched-scope .ktseg-opt, .clssched-arrange .ktseg-opt { padding: 6px 14px; }
.clssched-arrange { font-size: 13px; }

/* THE TOGGLES, WIRED (TEN-1602, split into two axes by TEN-1868). Mark, UAT
   round 6: "list view toggle doesn't appear to do anything." It didn't: the
   segment was two spans and the List half carried nothing at all.

   All four blocks are in the page now and the two radios together decide
   which one is drawn. No JavaScript, no round-trip: the grids and the lists
   are arrangements of the sessions the hub already read, at either of two
   scopes, so choosing between them is not a semantic boundary (R9), and a
   radio plus :has() is the platform doing it — the same move .sesswrap's
   funnel filter and every kit segment already make. Each segment's own lit
   state comes from `.ktseg-opt:has(input:checked)` up in the kit block; only
   the reveal is here.

   A browser without :has() drops these rules and shows ALL four blocks, which
   is the honest fallback: everything is readable, nothing is hidden behind a
   control that cannot work there.

   TWO INDEPENDENT AXES, SO FOUR RULES — NOT A RULE PER COMBINATION (TEN-1868,
   replacing the old three-way week|month|list toggle's six). A block is
   hidden the moment it disagrees with EITHER radio, so hiding "wrong scope" and
   "wrong arrangement" separately is enough to leave exactly the one block that
   agrees with both standing — the same "hide what was not chosen" shape as
   before, now applied to each axis rather than to the cross product. THE NAV
   ROW FOLLOWS SCOPE ALONE (its two rules, unchanged in count): a pager belongs
   to a date window, not to a shape, so the arrangement radio never touches it —
   this is the rule that used to pin the list to the week's own nav, gone now
   that the list can stand under either scope. */
.clssched:has(.clssched-scope input[value="week"]:checked) [data-clsscope="month"],
.clssched:has(.clssched-scope input[value="month"]:checked) [data-clsscope="week"],
.clssched:has(.clssched-arrange input[value="grid"]:checked) [data-clsarrange="list"],
.clssched:has(.clssched-arrange input[value="list"]:checked) [data-clsarrange="grid"] { display: none; }
.clssched:has(.clssched-scope input[value="week"]:checked) [data-clsnav="month"],
.clssched:has(.clssched-scope input[value="month"]:checked) [data-clsnav="week"] { display: none; }

/* The list view. Rows are the kit's entity card at its list-row rung, spaced
   like the listing's own rows so a week read down the page and a listing of
   classes are one instrument at two altitudes.

   THE DAY IS A DIVIDER (TEN-1610). Grouping is what lets a row say the time
   alone, so the heading has to read as a heading rather than as another row:
   it takes the WEEK GRID'S OWN head style — the heading face, 12px, the muted
   ink, --role-primary-ink for today — because it is the same fact in the other
   arrangement, and a second style for it would be the two views disagreeing
   about what a date looks like. The rule after it is a hairline that fills what
   the words leave; it is drawn with ::after rather than a border so it starts
   where the date ends instead of running underneath it. */
.clslistview { display: flex; flex-direction: column; gap: 20px; }
.clslistday { display: flex; flex-direction: column; gap: 10px; }
.clslistday-head { display: flex; align-items: center; gap: 10px; font-family: var(--b-font-heading); font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
.clslistday-head::after { content: ""; flex: 1; height: 1px; background: var(--border-subtle); }
.clslistday-today { color: var(--role-primary-ink); }
.clslistview-empty { margin: 0; padding: 14px 0; }

/* ── A session's own page (TEN-1649, board 6) ─────────────────────────────
   The trail, the session's header card, the roster, and the dialogs the card's
   acts open — one column with the train's own rhythm, which is `.clshub`'s gap
   because this page and the class hub are the same object at two altitudes and a
   second spacing for it would read as a different screen.

   THE EDIT DIALOG'S BODY IS THE AD-HOC PANEL'S RHYTHM for the same reason one rung
   down: two dialogs on one train that both author a session's time must not sit
   their fields apart by different amounts. Date, time and length ride ONE row that
   wraps — board 8 draws them side by side, and at a phone's width three stacked
   fields is the same form, not a different one. */
.clssess { display: flex; flex-direction: column; gap: 24px; color: var(--text-primary); }
.clssess-edit { display: flex; flex-direction: column; gap: 14px; }
.clssess-when { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-start; }
.clssess-when .ktinput-time { width: 100%; }

/* ── THE DOOR CODE'S DIALOG (TEN-1658, against TEN-1653's wire) ──────────────
   The QR, the code beside it, and the address under both — three renderings of one
   string, for a camera, a pair of thumbs and a screen reader.

   THE QR'S INK IS NOT THEMED, AND THAT IS THE RULE RATHER THAN AN OVERSIGHT. Every
   other drawing in this book is `currentColor` so it follows the theme (K13). A QR
   is read by a CAMERA: decoders on real phones expect dark modules on a light
   ground, and inverting it is a coin-flip on hardware nobody here can test. So the
   two values are fixed, identical in both themes, and deliberately not tokens —
   a token is a thing somebody is invited to re-tone, and this one must not be.
   `#fff` and `#111` rather than pure black for the same reason every ink in this
   cover stops short of it: pure black on a bright screen is glare, and the contrast
   ratio at #111 is 18.9:1, which is past every decoder's margin.

   THE QUIET ZONE IS IN THE viewBox, not in padding here — a code whose margin came
   from a stylesheet would lose it the moment somebody printed the page with author
   styles off, which is exactly when a poster gets printed. */
.ktqr { display: block; width: 168px; height: 168px; flex-shrink: 0; border-radius: 4px; }
.ktqr-ground { fill: #fff; }
.ktqr-ink { fill: #111; }

.clscode { display: flex; flex-direction: column; gap: 14px; }
/* The scope sentence is BODY ink, not the muted sub tier: it is the one thing in
   this dialog a reader can get wrong in a way that costs them a reprinted poster. */
.clscode-scope { margin: 0; font-size: 13.5px; line-height: 1.5; }
.clscode-pair { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.clscode-chars { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
/* LARGE PRINT, and `word-break` rather than a scroll: this value is read out loud
   and typed at a keypad, so every character has to be on the screen at once. The
   monospace face is the one thing standing between an organizer and reading the
   separator as part of the secret. */
.clscode-value {
  font-family: var(--b-font-mono, ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace);
  font-size: 22px; font-weight: 700; letter-spacing: 0.06em; line-height: 1.3;
  word-break: break-all; color: var(--text-primary);
}
.clscode-addr { display: flex; flex-direction: column; gap: 4px; }
.clscode-url { font-size: 13px; line-height: 1.45; word-break: break-all; color: var(--text-muted); }
.clscode-note { margin: 0; line-height: 1.5; }
/* The way to the printable sheet (TEN-1653) — a link, on its own line under the
   address, so the dialog's three renderings of the code stay the block a reader
   scans first. */
.clscode-poster { margin: 0; font-size: 13px; }
/* On a phone the pair stacks and the code is what stays big: a 168px square beside
   22px type is two things competing for a 320px line. */
@media (max-width: 420px) {
  .clscode-pair { gap: 12px; }
  .ktqr { width: 140px; height: 140px; }
}

/* ── CALLING ONE SESSION OFF (TEN-1658, against TEN-1652's contract) ──────────
   The dialog's body: a required reason, an optional note, and K7's footer. The
   rhythm is `.clssess-edit`'s, because this and the reschedule editor are two forms
   about the same session and a second spacing would read as a different screen. */
.clscancel { display: flex; flex-direction: column; gap: 14px; }
.clscancel .ktinput { width: 100%; }
.clscancel .msg-composer textarea { width: 100%; }

/* ── ROUND 16: THE SESSION PAGE'S THREE MOVES (TEN-1661) ─────────────────────

   1. THE ATTENDANCE BAR'S LINE CARRIES AN ACT (item 3). Mark: "the door-code
      button moves below the session header block — same line as the attendance
      bar, RIGHT-aligned, TOP-aligned with the bar." `.doorhead` is a plain block
      on every other surface that draws it and stays one: the row treatment is
      earned by the `doorhead-row` class the plate stamps only where the composing
      surface handed it a bar, so the console's own door renders exactly as it
      did. The bar takes the remaining width, the act is pushed right and pinned
      to the TOP of the row rather than centred against a two-line component, and
      the capacity line takes a full row under both.
   2. THE TITLE BLOCK'S OWN ACTS (item 4). The pencil edits the values the TITLE
      names, so it rides at the right end of the name line — `margin-left: auto`
      inside a flex row that already holds the name and the badge, which keeps it
      level with the title rather than with the eyebrow above it.
   3. NOBODY IS REGISTERED (item 5). One line where two empty bands were. It is
      the band's own `.empty` tier — this is the same kind of silence, said once
      instead of twice — with the bands' own left rhythm. */
.doorhead-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 12px; }
.doorhead-row .ktbar { flex: 1 1 260px; min-width: 0; }
.doorhead-row .doorhead-act { margin-left: auto; align-self: flex-start; }
.doorhead-row .capline { flex-basis: 100%; }
/* THE EYEBROW'S OWN LINE, WITH AN ACT AT ITS FAR RIGHT (TEN-1664 item 5). The
   `.kteyebrow` keeps `align-self: flex-start` for the no-acts case above; inside
   this row it is the left half and the auto margin is the right one.
   `--control-h` is NOT taken here: the row's height is the eyebrow's, so the icon
   act hangs level with it rather than pushing the card's first line taller than a
   line of text. */
.kthub-browline { display: flex; align-items: center; gap: 12px; }
.kthub-browacts { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.doorempty { margin: 0; }

/* THE DOOR-CODE BUTTON (TEN-1664 item 4). Mark's round-17 ask: it is a K12
   SECONDARY button wearing the icon in lieu of text — not a loose glyph.

   It shipped as `.kthub-iconact`, which is K12's quietest tier: no box at all,
   for row-level granular acts. Showing a class's door code stands on its own
   beside the attendance bar and is a supportive call-to-action, which is the
   SECONDARY rung in the ladder's own words. So it wears the book's `.btn
   .btn--secondary` — the same border, ground, ink and hover every other
   secondary wears — and the only thing declared here is what a button with no
   LABEL in it needs: a square-ish box instead of the text button's horizontal
   padding, and the glyph at the size a bare icon act drew it.

   `.ktic` is 18px by the icon law and is left alone; what changes is the box
   around it. `--control-h` keeps it the same height as every other control on
   the row, so a button with a picture in it does not sit a pixel off a button
   with a word in it. */
.clssess-codeact { padding: 0; width: var(--control-h); }

/* THE PER-TOKEN RESULTS (TEN-1664 item 2). One press may seat some people and
   invite others, so the answer is a LIST — the flash carries the count and this
   carries the rows, in the order they were named.

   THE TONE IS PER ROW, because a batch is genuinely mixed, and it is carried on
   the LEFT EDGE rather than in the ink: three different text colours down a
   column read as three different kinds of thing, where a rule at the start of a
   row reads as a status on one thing. `--role-*` throughout, so it follows the
   theme like everything else.

   The word is fixed-width-ish by its own min-width rather than by a grid: the
   three outcomes are short and of similar length, and a grid here would collapse
   badly at a phone width where the sentence wants the whole line. */
.clsorg-results {
  margin: 0 0 var(--b-space-3); padding: 12px 14px;
  border: 1px solid var(--border); border-radius: var(--control-r);
  background: var(--surface-raised);
}
.clsorg-rows { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.clsorg-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  padding-left: 10px; border-left: 3px solid var(--border-strong);
}
.clsorg-row-ok { border-left-color: var(--role-good); }
.clsorg-row-warn { border-left-color: var(--role-warn); }
.clsorg-row-error { border-left-color: var(--role-danger); }
.clsorg-word { min-width: 6.5em; font-weight: 600; font-size: 13px; }
.clsorg-row-ok .clsorg-word { color: var(--role-good); }
.clsorg-row-warn .clsorg-word { color: var(--role-warn); }
.clsorg-row-error .clsorg-word { color: var(--role-danger); }
.clsorg-who { font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
.clsorg-say { flex: 1 1 16ch; min-width: 0; }

/* THE PROVENANCE CHIP (TEN-1660), and it is the registration policy card's chip
   grown a treatment rather than a new component: that card has answered "where
   does this value come from" since the policy ladder shipped, and the venue
   ladder asks the identical question of a place. One idiom, two surfaces.
   OWN IS INK AND INHERITED IS QUIET, which is the only distinction a reader
   needs from a chip: a value authored HERE is a thing they can change here, and
   an inherited one is a thing they would change somewhere else. Neither is a
   warning, so neither borrows the attention hue. */
.chip-prov { font-size: 11.5px; padding: 2px 8px; }
.chip-prov.chip-own { border-color: var(--role-primary); color: var(--role-primary-ink); background: var(--role-primary-soft); }
.chip-prov.chip-inherited { color: var(--text-muted); }
/* PLATFORM IS DASHED (TEN-1970): a value nobody on the ladder stated — the platform's
   floor — wears the outline of a fact nobody decided. Neither ink nor attention. */
.chip-prov.chip-platform { color: var(--text-muted); border-style: dashed; }

/* THE VENUE CARD ON THE EDITOR (TEN-1661 item 10, and the CONTROL since TEN-1664
   item 9). The prompt is "Date"'s own label tier; under it the resolved place sits
   in a box that is itself the way in to the picker.

   IT IS THE ROW RUNG'S STRETCHED-LEAD CONSTRUCTION AND DELIBERATELY NOT A NEW ONE
   (`.ktrow-linked`, three rules, same reasons): the LEAD stays the one anchor and
   its ::after covers the card, because this block sits inside the editor's form and
   an anchor wrapping it is a nesting the parser undoes. One destination, one
   accessible name (the place is what a screen reader announces), and the hit area a
   pointer expects.

   THE HOVER IS HONEST, which is the half that makes the rest legal: a box that
   lights up under the pointer and does nothing when pressed is the broken promise
   `.ktrow-linked`'s own note is about, and here the box IS the press.

   THE FOCUS RING BELONGS TO THE CARD once the card is the target — a 170px name
   with a card-wide hit area would otherwise outline a fraction of what a keyboard
   press activates. */
.clssess-venue { display: flex; flex-direction: column; gap: 5px; }
.clssess-venuecard {
  position: relative; min-height: var(--control-h);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; font-size: 14px;
  border: 1px solid var(--border-strong); border-radius: var(--control-r);
  background: var(--surface-overlay);
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.clssess-venuecard:hover { background: var(--hover); border-color: var(--role-primary); }
.clssess-venuelead { color: var(--text-primary); text-decoration: none; min-width: 0; overflow-wrap: anywhere; }
.clssess-venuecard > .clssess-venuelead[href]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.clssess-venuecard:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.clssess-venuecard > .clssess-venuelead[href]:focus-visible { outline: none; }

/* A SPACE ABOVE THE DANGER BLOCK (TEN-1661 item 9). The editor's form, the venue
   dialog and the danger card are siblings in the page's normal flow rather than
   a gapped column — the two dialogs between them render as nothing until they are
   opened — so the separation is a margin on the card that must stand apart, and
   it is deliberately larger than the form's own rhythm: this is the boundary
   between "change this session" and "end it". */
.ktsent-apart { margin-top: var(--b-space-6); }

/* THE ORGANIZER'S TWO DIALOGS (TEN-1651). The body's rhythm is the cancel
   dialog's, because all three are forms about one session and a second spacing
   would read as a different screen. */
.clsorg { display: flex; flex-direction: column; gap: 14px; }
.clsorg .ktinput, .clsorg .ktselect { width: 100%; }
.clsorg .msg-composer textarea { width: 100%; }

/* THE RECIPIENT TOKPICK INSIDE A KIT MODAL (TEN-1664 item 1).
   ---------------------------------------------------------------------------
   The picker's panel is absolutely positioned over the field, and
   `.ktmodal-panel` is `overflow-y: auto` — which is a CLIPPING context, so the
   dropdown would be cut off at the panel's edge and the rows nearest the
   button would simply not be there. `.ss-grid-modal` solved this once already
   (TEN-1152) and this is that same rule for the kit's own dialog: in flow, the
   list pushes the acts down and the panel grows with it, bounded by the panel's
   own max-height. Still ATTACHED — floating or in flow it is the same object
   joined the same way. */
.ktmodal-panel .tokpick-list {
  position: static; margin-top: 0; box-shadow: none;
  max-height: calc(var(--ktsel-row) * 3.5 + 6px);
}
/* The source select is already visually hidden by `.tokpick-source` once the
   picker has armed; this only stops the unenhanced control stretching the dialog
   in the window before it does (the TEN-1088 flash rule, at this mount's width),
   and gives the enhanced field the same one. */
.clsorg .tokpick, .clsorg .clsorg-pick { width: 100%; }
/* THE SCRIPTLESS HALF (R9 clause 3) — the coverage card's `.ktcov-new` rule, at
   this mount and for its reason: NO `display` here, because
   `[data-scriptless-only]` hides it with a bare attribute selector at the same
   specificity as a class, and a `display: flex` declared later would out-order it
   and show the box on a page that has a picker. */
.clsorg-new { flex-direction: column; gap: 4px; }

/* THE GIANT QR (TEN-1661 item 1). Mark: "the QR overlay maximizes the code
   graphic — as much screen real estate as the layout provides without disruption
   (trim padding/bleeds); people scan from as far away as possible."

   THE SQUARE TAKES THE PANEL'S WHOLE USABLE WIDTH. `.clscode-pair` stops being a
   row and becomes a column, the QR's fixed 168px is replaced by `width: 100%`
   with `height: auto` so the viewBox drives the aspect, and the panel is the
   widest the cover draws with its side padding trimmed — the bleed Mark named.
   The quiet zone is untouched because it rides in the viewBox rather than here,
   which is the one margin scanning actually depends on.
   `max-width` is what stops it growing past the panel on a wide screen: a code
   larger than the dialog is a code with its finder patterns off the edge.

   THE SQUARE IS THE WRAPPER'S, NOT THE SVG'S `height: auto` (TEN-1706). The rule
   above used to be `width: 100%; height: auto` on the `<svg>` alone, and it drew
   the code ON TOP OF the three lines under it on a desktop-width dialog. The
   mechanism, measured rather than guessed: `qrsvg.go` emits a `<svg>` with a
   `viewBox` and NO width/height attributes, so the element has an intrinsic
   RATIO but no intrinsic SIZE. Chrome resolves the svg's own box from
   `width: 100%` (530px in the 560px panel) and lays it out square — but the
   contribution it makes to its parent's content height is the CSS default object
   size, 300px. So `.clscode-qr` was 300px tall around a 530px square, and the
   230px of overflow painted straight over "SCAN OR TYPE THE CODE", the code and
   the address. At phone width the square is ~312px and the overflow is ~12px, so
   the same bug showed as a correct-looking page — which is why a golden and a
   one-width drive both stayed green (the TEN-1664 lesson, again).

   So the DEFINITE square is declared on the WRAPPER, whose width is a real
   number, and the svg simply fills it. Nothing is positioned over anything: the
   pair is a flex column, the QR owns a box the layout can measure, and the text
   is in normal flow below it. `scripts/browsergate/sessionround4_test.go` drives
   this at 1280 and 390 and asserts the boxes do not intersect at either. */
.ktcodepanel { width: 560px; padding-left: 14px; padding-right: 14px; }
.ktcodepanel .clscode-pair { flex-direction: column; align-items: stretch; gap: 12px; }
.ktcodepanel .clscode-qr { width: 100%; max-width: 100%; aspect-ratio: 1 / 1; }
.ktcodepanel .ktqr { width: 100%; height: 100%; max-width: 100%; }
.ktcodepanel .clscode-chars { align-items: center; text-align: center; }

/* The note a STAGED session's own page wears above its door (TEN-1602). Dashed
   in --role-staged, like the grid cell and the list row, so the same session
   carries the same marking on all three surfaces — and quiet, because staged is
   a state somebody chose rather than a problem. */
.clssess-staged {
  margin: 0 0 var(--b-space-3); padding: 10px 12px; line-height: 1.45;
  border: 1.5px dashed var(--role-staged); border-radius: var(--control-r);
  background: var(--surface-page);
}

/* THE WEEK. Seven columns, and the grid SCROLLS inside its own container on a
   narrow screen rather than reflowing — a week that reflowed into a list is a
   different instrument, and the List view it would be pretending to be is a
   later train (R7's mechanical constraint, read the right way round). */
.clsweek { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 10px; }
.clsday { display: flex; flex-direction: column; gap: 8px; position: relative; min-width: 0; }
.clsdaylab { display: flex; align-items: center; justify-content: space-between; gap: 6px; font-family: var(--b-font-heading); font-size: 12px; font-weight: 700; letter-spacing: 0.04em; color: var(--text-muted); }
.clsdaylab-today { color: var(--role-primary-ink); }
/* THE + GREW ITS GLYPH, NOT ITS TARGET (TEN-1644 item 8). Mark: the day-header +
   is hard to read. It is the BOX that was already the right size — 18px is the
   pointer's and the thumb's share of a 45px column, and growing it would start
   eating the weekday beside it — so what changes is the character inside: 13px to
   17px, with `line-height: 1` so the taller glyph centres in the same box rather
   than pushing it open. Nothing about the hit area moves, and a browser gate case
   measures both halves of that sentence. */
.clsday-add { width: 18px; height: 18px; border-radius: 5px; background: var(--role-primary-soft); color: var(--role-primary-ink); display: flex; align-items: center; justify-content: center; font-size: 17px; line-height: 1; font-weight: 700; text-decoration: none; flex-shrink: 0; box-sizing: border-box; }
.clsday-add:hover { background: var(--role-primary); color: var(--ink-on-primary); }
/* THE PRESSED AND FOCUSED STATES ARE THE HOVER'S, AND THEY CHANGE NO GEOMETRY
   (TEN-1657 item 5). Mark's UAT round 15 saw the + "very strange looking on
   click", which was two faults reading as one: the box was the wrong SIZE in the
   month grid (the rung below fixes that), and the only state it drew was `:hover`
   — so a touch left the control in a hovered ground with no press acknowledgement
   of its own, and a keyboard focus drew the browser's default outline, which is
   the one ring on this page not measured against its cell. `:active` is the
   acknowledgement (ADR-0032: it fires before any network work), and the focus ring
   is the page's own — the same inset-free 2px `.clscell-linked:focus-within` uses,
   as a box-shadow rather than an outline so it sits inside the day cell's padding
   instead of spilling past its border at a phone's width. */
.clsday-add:active { background: var(--role-primary); color: var(--ink-on-primary); }
.clsday-add:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--role-primary); }

/* The ad-hoc panel — a DIALOG now (TEN-1605), not a popover hanging off its
   day. It used to be positioned against `.clsday`, which is why the phone rung
   below had to re-anchor it to the week: a 47px column cannot hold a 244px
   panel on either edge. A modal has no such argument to lose — the kit's own
   `.ktmodal` centres it and `.ktmodal-panel` sizes it, at every width — so all
   of that goes and what is left here is the BODY, which is the only part this
   surface ever had an opinion about.

   The form is the panel's one child, so it carries the column itself rather
   than nesting a second one inside `.ktmodal-panel`'s. */
.clsadhoc { display: flex; flex-direction: column; gap: 14px; }
/* The usual slots: one tap each, stacked, and ruled apart from the composed
   time below them because they are different KINDS of act. */
.clsadhoc-usual { display: flex; flex-direction: column; gap: 6px; }
.clsadhoc-opt { display: block; width: 100%; text-align: left; padding: 10px 14px; font: inherit; font-size: 13.5px; font-weight: 600; color: var(--text-primary); background: var(--surface-overlay); border: 1px solid var(--border-subtle); border-radius: var(--control-r); cursor: pointer; }
.clsadhoc-opt:hover { background: var(--hover); border-color: var(--border-strong); }
/* THE FREE-TIME ARM (TEN-1590) — the same panel's second half, ruled off from
   the usual slots above it because they are different KINDS of act: a row is one
   tap on a time the class already keeps, this is a time being composed. The rule
   is the whole separation; nothing here is a second menu.

   It is the panel that widens, not the controls that shrink: the time box and
   the duration stepper are the kit's own at the kit's own size, and a panel that
   squeezed them would be this page holding a private opinion about how a stepper
   looks. */
.clsadhoc-free { display: flex; flex-direction: column; gap: 8px; padding-top: 12px; border-top: 1px solid var(--border-subtle); }
.clsadhoc-freehead { margin-bottom: 2px; }
.clsadhoc-free .ktinput-time { width: 100%; }
/* THE DIALOG'S PRIMARY IS IN ITS FOOT, with Cancel beside it — the kit's own
   `.ktmodal-foot` grammar (K12: one advancing act per region). It used to be a
   full-width button at the bottom of the free arm, which read as that arm's
   button rather than as the panel's; in a dialog the foot is where the
   committing act lives and there is nothing left for this rule to say. */

.clscell { border: 1px solid var(--border-subtle); border-radius: var(--control-r); background: var(--surface-raised); padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.clscell-time { font-size: 13.5px; font-weight: 700; }
.clscell-meta { font-size: 12px; }
/* The week's bars share the cell's width rather than the kit's fixed track —
   the component's own rule sizes a SPECIMEN, and a grid cell is narrower than
   one. The legend is dropped at this size and nowhere else: a legend naming
   three counts is the whole point of the bar, and the cell that hides it is the
   cell whose session opens a door page that shows it in full. */
.clscell .ktbar-track { width: 100%; }
.clscell .ktbar-legend { display: none; }
.clscell-now { border-color: var(--role-primary); }
/* THE WHOLE CELL IS THE LINK (TEN-1602). Same stretched-link move as the kit's
   list row, for the same reason and with the same rule about what may sit above
   it: the time keeps the anchor, its ::after covers the cell, and hover marks
   the block a pointer is over rather than four bold characters inside it.
   `.clscell-linked` is only on the cells that HAVE a way in — an attendee's
   grid has none, and a cell that lit under the pointer and did nothing would be
   the dead control this ticket is about, one size down. */
.clscell-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.clscell-linked:hover { background: var(--hover); border-color: var(--border-strong); }
/* NO UNDERLINE ON THE TIME (TEN-1644 item 9). Mark: the hover underline reads as
   link-like. It is `.link:hover`'s own rule arriving here as a side effect of the
   stretched hit area — the anchor's `::after` covers the whole cell, so a pointer
   anywhere on the block is hovering the anchor and the time underlines itself.
   Inside a cell that is wrong twice: the thing being pointed at is the BLOCK, which
   already answers with its ground and its edge, and a 13.5px bold time with a line
   under it reads as one word in a sentence rather than as a session. The hover
   STATE stays exactly as it was; only the decoration goes, on every state of the
   anchor, because a focus ring that underlined would say the same wrong thing. */
.clscell-time a.link, .clscell-time a.link:hover, .clscell-time a.link:focus-visible { text-decoration: none; }
.clscell-linked .clscell-time a[href]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.clscell-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.clscell-linked .clscell-time a[href]:focus-visible { outline: none; }
/* A hovered staged cell keeps its dashed edge — dashed means staged everywhere
   in this book and a hover is not a state change. */
.clscell-staged.clscell-linked:hover { border-color: var(--role-staged); }
/* DASHED MEANS STAGED (K12's shape rule), through --role-staged, so a staged
   cell and the pricing kit's dashed add row cannot drift apart in a theme. */
.clscell-staged { border-style: dashed; border-color: var(--role-staged); background: var(--surface-page); }

/* ── THE MONTH (TEN-1644, Mark's annotated board 5) ────────────────────────
   A calendar month of the same sessions, at compact density: one row of column
   heads and five or six rows of days, in the week grid's own seven columns and its
   own order, so a Tuesday is in the same place in both arrangements.

   THE CELLS ARE THE WEEK'S CELLS (`.clscell`), so the dashed staged edge, the
   stretched link, the hover and the today accent are the rules that already exist
   rather than a second vocabulary — what is scoped here is DENSITY and nothing
   else. The meta line and the fill bar are not rendered at all in this view (the
   plate says why), which is the same call the phone's week makes.

   A DAY OUTSIDE THE MONTH IS QUIET, NOT DISABLED. Its number drops to
   `--text-muted` — the readable floor — and never to `--text-faint`, because faint
   is a disabled state (K12's punch-r3 amendment) and these days are perfectly
   real: they carry their sessions and their +, and a reader can press both. */
.clsmonth { display: flex; flex-direction: column; gap: 6px; }
.clsmonth-heads, .clsmonth-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
.clsmonth-head { font-family: var(--b-font-heading); font-size: 11px; font-weight: 700; letter-spacing: 0.04em; color: var(--text-muted); text-align: center; }
.clsmday { display: flex; flex-direction: column; gap: 4px; min-height: 76px; min-width: 0; padding: 6px; border: 1px solid var(--border-subtle); border-radius: var(--control-r); background: var(--surface-page); }
.clsmdaylab { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.clsmdaylab-num { font-family: var(--b-font-heading); font-size: 12px; font-weight: 700; color: var(--text-primary); }
.clsmday-out { background: transparent; }
.clsmday-out .clsmdaylab-num { color: var(--text-muted); }
.clsmday-today { border-color: var(--role-primary); }
.clsmday-today .clsmdaylab-num { color: var(--role-primary-ink); }
.clsmonth .clscell { padding: 3px 6px; gap: 0; border-radius: 6px; background: var(--surface-raised); }
.clsmonth .clscell-time { font-size: 11.5px; }

.clsweek-foot { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.clsweek-hint { margin: 0; }
.clsweek-key { display: flex; align-items: center; gap: 8px; }
.clskey-swatch { width: 46px; height: 22px; padding: 0; align-items: center; justify-content: center; }

/* The audience card's own vocabulary. The HUB no longer mounts it (TEN-1644 item 6
   removed that block outright), and these rules stay because the ANNOUNCE COMPOSER
   wears the same card — `class-announce.plate.html` draws the card, its head, the
   initials and the note, which is why they are still here and why the four rules
   only the hub's side column ever used (`clsside-lines`, `-line`, `-when`, `-act`)
   are gone with it. */
.clsside-card { gap: 12px; }
.clsside-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.clsfaces { display: flex; }
.clsface { width: 28px; height: 28px; font-size: 11px; border: 2px solid var(--surface-raised); }
.clsface + .clsface { margin-left: -8px; }
.clsside-note { line-height: 1.5; margin: 0; }

/* The creation surfaces need the room the wizard's two-column layout is ABOUT,
   for the same reason the kit page does — and the hub needs it for a seven-day
   grid. Same :has() move, same @supports guard, scoped to these screens alone. */
/* The WIZARD needs it — its two-column content-plus-rail layout is the thing
   K9 is about, and at the cover's ordinary reading column the rail stacks. The
   CHOOSER deliberately does not: board 1 is a 2x2 grid of choice cards, and a
   wider column turns it into 3+1, which reads as three options and an
   afterthought. So the selector asks for the STEPPER, which only the wizard
   has. Same @supports guard as the kit page's, for the same reason. */
@supports selector(:has(*)) {
  body.app:has(.clswrap .ktsteps) main.container { max-width: 1180px; }
  body.app:has(.clshub) main.container { max-width: var(--b-lens-wide); }
}

@media (max-width: 900px) {
  .clsweek { grid-template-columns: repeat(7, minmax(120px, 1fr)); }
  .clssched { overflow-x: auto; }
}

@media (max-width: 720px) {
  .clsfield-wide, .clsfield-date, .clsfield-sel { width: 100%; }
  .clsrow, .clshoriz { gap: 20px; }
}

/* ── The week on a phone: it SQUISHES, it does not scroll (TEN-1597) ───────
   Mark's UAT round 5: "the weekly view on the class page needs to squish to
   screenwidth for mobile, with chicklets for class times."

   The 900px rung above keeps the scroll and that stays right where it is: at
   tablet width 120px columns still read as a week, and a week that scrolls
   sideways is a better week than a squeezed one. Below the cover's narrow
   breakpoint there is nothing left to scroll INTO — a seven-day grid asking for
   840px inside a 375px phone is a page whose content is mostly off-screen, and
   the ask is squish, not scroll. So the grid takes the screen's width and the
   cells become what fits.

   NOTHING IS REMOVED THAT IS NOT READABLE SOMEWHERE ELSE, which is the
   difference between squishing and hiding. The meta line and the fill bar are
   the two things a 45px column genuinely cannot draw; the bar's own legend is
   already dropped at grid size one block up for the same reason, and the
   session's door page — the href the chicklet still carries — has all of it.

   The chicklet keeps its href, so a tap on a phone lands exactly where a click
   on a desktop does. It is the same element at a different size, not a second
   control that would have to be kept in step. */
@media (max-width: 560px) {
  /* THE HUB'S HEAD NEEDS NOTHING HERE ANY MORE (TEN-1644). It used to be a
     space-between row that did not wrap at this width — the two action buttons held
     their size and squeezed the class's name into a column one word wide — so this
     block stacked it. The buttons are inside the card now and `.clshub-top` is a
     column at every width, so the stacking is the structure rather than a
     breakpoint's correction. What the CARD does at this width is the kit's own
     business: `.kthub` already wraps, and the acts take a row when it does. */
  .clssched { overflow-x: visible; }
  .clsweek { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
  .clsday { gap: 4px; }
  /* The head stacks and the staff + moves under it: a label and a control side
     by side is the one thing that does not fit at this width. */
  .clsdaylab { flex-direction: column; align-items: stretch; gap: 2px; font-size: 9px; letter-spacing: 0.01em; }
  .clsdaylab-name { display: flex; flex-direction: column; align-items: center; line-height: 1.2; }
  .clsdaylab-num { font-size: 12px; color: var(--text-primary); }
  .clsdaylab-today .clsdaylab-num { color: var(--role-primary-ink); }
  .clsdaylab-mark { display: none; }
  /* The glyph grows here too (TEN-1644 item 8) and the control does not: 15px of
     height is what the stacked head can spare, and 13px of + inside it is the same
     trade the desktop rung makes at 18/17.

     IT IS SCOPED TO THE WEEK NOW, AND THAT IS THE WHOLE OF TEN-1657 ITEM 5. Mark:
     at a phone's width the day-header + extends beyond the day block's border,
     worst on click. `width: 100%` is the right answer for the WEEK's head, which
     this rung turns into a column with `align-items: stretch` — the + sits UNDER
     the date and takes the column. The MONTH's head (`.clsmdaylab`) is untouched by
     that stacking and stays a ROW with the date beside the +, so the same
     unscoped rule asked for 100% of the head's width NEXT TO the date — and
     `flex-shrink: 0` on the base meant it would not give any of it back. The + then
     ran the date's width past `.clsmday`'s padding and out through its border,
     which is exactly what Mark saw; at rest the overflow wears
     `--role-primary-soft` and is nearly invisible, and on press it fills with the
     solid accent and is the loudest thing on the grid. A rule written for one grid
     reached the other because nothing said which grid it was for. */
  .clsweek .clsday-add { width: 100%; height: 15px; border-radius: 4px; font-size: 13px; }
  /* The month's + is a SQUARE that fits its cell: 14px inside a day 3px/2px padded
     and ~50px wide at this width, so the control, its focus ring and the date all
     sit inside the border. The glyph takes the same 18/17 → 14/12 trade the week's
     does. */
  .clsmonth .clsday-add { width: 14px; height: 14px; border-radius: 4px; font-size: 12px; }
  .clscell { padding: 4px 2px; gap: 0; align-items: center; text-align: center; border-radius: 6px; }
  .clscell-time { font-size: 9.5px; line-height: 1.25; overflow-wrap: anywhere; }
  .clscell-meta, .clscell .ktbar { display: none; }
  /* THE MONTH SQUISHES ON THE SAME TERMS AS THE WEEK (TEN-1644). Thirty-one days
     of a seven-column grid inside 375px is the week's own problem one order of
     magnitude on, and the answer is the one Mark already ruled for the week: take
     the screen's width and let the cells become what fits. Nothing is removed that
     is not readable somewhere else — the month never drew the meta line or the bar
     at any width, and the chicklet keeps the href a tap needs. */
  .clsmonth-heads, .clsmonth-week { gap: 3px; }
  .clsmonth-head { font-size: 8.5px; letter-spacing: 0.01em; }
  .clsmday { min-height: 52px; padding: 3px 2px; gap: 2px; border-radius: 6px; }
  .clsmdaylab { gap: 2px; }
  .clsmdaylab-num { font-size: 10px; }
  .clsmonth .clscell { padding: 2px 1px; align-items: center; text-align: center; border-radius: 4px; }
  .clsmonth .clscell-time { font-size: 9px; line-height: 1.2; overflow-wrap: anywhere; }
  /* The legend's swatch is a chicklet too, so the key under the grid keeps
     looking like the thing it is a key to. */
  .clskey-swatch { width: 30px; height: 16px; }
  /* THE AD-HOC PANEL NEEDS NOTHING HERE ANY MORE (TEN-1605). It used to hang
     off its DAY, and at this width the day is 47px and the panel is five
     columns wide — so anchored to any single day it ran off one edge or the
     other, and this block had to re-anchor it to the whole week to keep the
     page from scrolling sideways to show a popover. It is a dialog now: the
     kit's `.ktmodal` centres it in the viewport and `.ktmodal-panel` is already
     `max-width: 100%`, so a phone gets the panel at the screen's width by the
     component's own rule and this surface holds no opinion about it. Which day
     it belongs to is still in its own title — "ADD A SESSION · THU 6". */
}

/* ── The public place page (TEN-1614, boards 20/20b) ─────────────────────────
 *
 * /places/<slug>. A PUBLIC surface (R10), so there is nothing here that draws a
 * control which writes — the whole vocabulary below is a banner, an identity
 * row, a filter and a list.
 *
 * IT BORROWS RATHER THAN INVENTS. The rows are the kit's entity card at its
 * list-row rung and are styled by the kit's own block; the chips are `.chips
 * .chip-toggle`, the same control the navigator and the funnel filter already
 * use; the day divider is the class hub's `.clslistday-head` grammar, restated
 * here rather than shared only because the two surfaces are in different
 * families and a shared class would tie a public page's typography to an
 * administrative one's. */
.plpage { display: flex; flex-direction: column; gap: 20px; color: var(--text-primary); }

/* THE BANNER, in its two real states. `.plbanner-flat` is the one a deployment
 * with no map provider renders — and a place with no pin, on any deployment: a
 * BAND WITH A SENTENCE, deliberately not a tall grey rectangle, because an empty
 * map-shaped box reads as a map that failed and a reader cannot tell that from a
 * place with no address at all. With a tile (TEN-1617) the image fills the band,
 * the note goes, and nothing else about this block moves — which is exactly what
 * "when the tile lands" was written to mean. */
.plbanner {
  position: relative; overflow: hidden; display: flex; align-items: flex-end;
  justify-content: space-between; gap: 16px; flex-wrap: wrap;
  padding: var(--b-space-4); border-radius: var(--b-radius-md);
  border: var(--b-border-w) solid var(--border-subtle); background: var(--surface-raised);
}
.plbanner-tile { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The credit rides over the tile, quiet but legible on any imagery — the
   provider's terms, held in the same corner the place card holds them. */
.plbanner-credit {
  position: absolute; left: 8px; bottom: 8px; font-size: 10.5px; line-height: 1.2;
  color: var(--text-primary); background: color-mix(in srgb, var(--surface-raised) 78%, transparent);
  border-radius: 4px; padding: 2px 6px;
}
.plbanner-note { margin: 0; }
.plbanner-acts { position: relative; display: flex; gap: 10px; flex-wrap: wrap; margin-left: auto; }
.plbanner-act-go { color: var(--role-primary-ink); }

/* THE LIT BAND, TWICE AS TALL AND EXPANDABLE IN PLACE (TEN-1894).
 *
 * Collapsed, the band is 156px — twice the 78px the lit band measured when its
 * height was only the handoff row's — and the picture is drawn at ONE IMAGE PIXEL
 * PER CSS PIXEL, centred on the pin: the middle of the 1024×512 window the tile
 * stitcher composes (placemaps.go). A band wider than the picture scales it up to
 * cover rather than leaving an edge.
 *
 * Expanded, the band grows to 344px — the height at which the whole 2:1 picture
 * fits a 688px column exactly — and the picture covers it, so the reader sees
 * the rest of the same image: half again as wide and three times as tall as the
 * collapsed crop on a desktop, and more of both on a phone. NOTHING IS FETCHED.
 * The expand is a checkbox read with `:checked +`, the same move the Only-yours
 * filter makes one block down, so it costs no request and works with scripting
 * off (R9 clause 2).
 *
 * The flat band is untouched: with no picture there is nothing to grow, and a
 * tall empty box would read as a map that failed. */
.plbanner-lit { min-height: 156px; }
.plbanner-lit .plbanner-tile {
  inset: auto; left: 50%; top: 50%; width: max(100%, 1024px); max-width: none; height: auto;
  object-fit: fill; transform: translate(-50%, -50%);
}
.plbanner-grow:checked + .plbanner-lit { min-height: 344px; }
.plbanner-grow:checked + .plbanner-lit .plbanner-tile {
  inset: 0; left: 0; top: 0; width: 100%; height: 100%; object-fit: cover; transform: none;
}
/* The input is visually hidden and still in the tab order; its ring is drawn on
   the label a reader actually presses. */
.plbanner-grow { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.plbanner-grow:focus-visible + .plbanner-lit .plbanner-growbtn { outline: 2px solid var(--role-primary); outline-offset: 2px; }
.plbanner-growbtn { cursor: pointer; }
.plbanner-grow-less { display: none; }
.plbanner-grow:checked + .plbanner-lit .plbanner-grow-more { display: none; }
.plbanner-grow:checked + .plbanner-lit .plbanner-grow-less { display: inline; }

/* THE IDENTITY ROW. Name, claim badge, where it is, who runs it — and the two
 * counts, which are the place's whole published tree and how many organizers
 * host here. Two numbers because they answer two questions. */
.plident { display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.plident-who { display: flex; align-items: center; gap: 14px; min-width: 0; }
.plident-avatar { width: 44px; height: 44px; font-size: 15px; flex-shrink: 0; }
.plident-lines { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.plident-nameline { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.plident-name { font-family: var(--b-font-heading); font-size: 21px; font-weight: 800; letter-spacing: 0.01em; }
.plident-meta { line-height: 1.4; }
.plident-counts { display: flex; gap: 18px; flex-wrap: wrap; font-size: 13.5px; }
.plcount { white-space: nowrap; }
/* THE ORGANIZER COUNT IS A DOOR (TEN-1894): the kit's quiet link, number and
   words together, opening the dialog of org cards. */
.plcount-link {
  color: inherit; text-decoration: underline; text-decoration-style: dotted;
  text-decoration-color: var(--text-muted); text-underline-offset: 3px;
}
.plcount-link:hover { text-decoration-style: solid; }

/* THE ORGANIZERS DIALOG (TEN-1894). The kit's modal, holding two native
 * disclosures of org cards — the reader's own clubs first and open, everybody
 * else folded under them. Each row is the org card family's identity block
 * (`.console-person` + the squared `.av-org` face on its face-link), dressed by
 * the rules that already dress it on Clubs and the people surfaces; this block
 * only lays the groups out. */
.plorgs { gap: 12px; }
.plorgs-group { border-top: var(--b-border-w) solid var(--border-subtle); padding-top: 10px; }
.plorgs-summary {
  display: flex; align-items: baseline; gap: 8px; cursor: pointer; list-style: none;
  font-family: var(--b-font-heading); font-size: 12px; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted);
}
.plorgs-summary::-webkit-details-marker { display: none; }
/* The fold's own mark, drawn rather than native (K11a): a chevron that turns. */
.plorgs-summary::after {
  content: ""; width: 7px; height: 7px; margin-left: auto; align-self: center;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(-45deg); transition: transform 0.15s ease;
}
.plorgs-group[open] > .plorgs-summary::after { transform: rotate(45deg); }
.plorgs-list { display: flex; flex-direction: column; gap: 10px; padding-top: 10px; max-height: 50vh; overflow-y: auto; }
/* A club's name is a name, not an identifier: sans and semibold, as the public
   profile's Teams rows draw theirs (`.pp-teams`), rather than `.acct-value`'s
   mono, which is for handles and ids. */
.plorg .acct-value { font-family: var(--b-font-sans, inherit); font-weight: var(--b-weight-semibold); }
.plorg-name { color: var(--text-primary); text-decoration: none; }
.plorg-name:hover { text-decoration: underline; }

/* THE UNCLAIMED STRIP. Amber, because it qualifies everything above it: this
 * page was assembled from organizers' records and may be incomplete. It carries
 * NO control — claiming is Epic B, and a button to nowhere is a promise (R9). */
.plunclaimed {
  display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap;
  padding: 12px 16px; border-radius: var(--b-radius-md);
  border: var(--b-border-w) solid var(--role-attention); background: var(--amber-a);
}
.plunclaimed-pill { flex-shrink: 0; }
.plunclaimed-lines { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.plunclaimed-line { font-size: 14px; font-weight: 600; }
.plunclaimed-soon { line-height: 1.45; max-width: 72ch; }

/* THE ORG LENS BAND — the first of the lens's three quiet devices. It is the
 * accent colour ONCE, at the top, saying whose things are marked below; the
 * rails on the rows are the same colour and mean the same thing. That is the
 * whole reason this is a band rather than paint: one explanation, one legend,
 * and a public list that still reads as a public list. */
.plband {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 12px 16px; border-radius: var(--b-radius-md);
  border: var(--b-border-w) solid var(--role-primary); background: var(--role-primary-soft);
}
.plband-avatar { width: 30px; height: 30px; font-size: 12px; flex-shrink: 0; }
.plband-lines { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.plband-line { font-size: 14px; line-height: 1.45; }
.plband-next { line-height: 1.4; }
.plband-only { font-size: 13px; font-weight: 600; white-space: nowrap; }

/* HAPPENING HERE. The filter is the kit's segmented control — two labels each
 * WRAPPING a radio, which is what every segment in this kit already is — so the
 * lit state comes from `.ktseg-opt:has(input:checked)` up in the kit block and
 * nothing here has to draw it. The controls are real radios, so the keyboard and
 * the screen reader work and the page works with scripting off (R9 clause 3). */
.plhap { display: flex; flex-direction: column; gap: 14px; }
.plhap-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.plhap-title { display: flex; flex-direction: column; gap: 2px; }
.plhap-summary { line-height: 1.4; }
.plhap-filter { font-size: 13px; }
.plhap-filter .ktseg-opt { padding: 6px 14px; }
/* THE SAME SEGMENT IN ITS OTHER SHAPE. Once the server has already narrowed the
   answer the two halves are LINKS, because a reveal cannot bring back rows the
   response does not contain — and they wear the same skin, because a reader
   should not have to know which mechanism they are looking at. */
.plhap-filter a.ktseg-opt { text-decoration: none; display: inline-block; }
.plrooms { margin: 0; flex-wrap: wrap; }

/* "ONLY YOURS" — a reveal over rows already on the page (R9 clause 2), the same
 * :has() move the class hub's view toggle and the funnel filter already make.
 *
 * The day dividers carry `plday-mine` from the SERVER rather than being found
 * with a second :has(), so a divider is never left standing over rows the filter
 * just hid — and the disclosure goes with them, because everything it could
 * reveal has just been filtered out of view.
 *
 * A browser without :has() drops this block and shows every row unfiltered. That
 * is the honest fallback: nothing is hidden behind a control that cannot work
 * there, and the `?host=` link in the band still asks the server the same
 * question. */
.plhap:has(.plhap-filter input[value="mine"]:checked) .plrow:not(.plrow-mine),
.plhap:has(.plhap-filter input[value="mine"]:checked) .plday:not(.plday-mine),
.plhap:has(.plhap-filter input[value="mine"]:checked) .plmore { display: none; }

/* "SHOW ALL N PROGRAMS" — a NATIVE disclosure. The rows are in the markup, so
 * opening it costs no request and closing it loses nothing (R9); the browser
 * owns the open state, so there is no input to skin and no script in the loop.
 * The marker goes because the summary is drawn as a button and two affordances
 * on one control is one too many (K11a: no native chrome). */
.plmore { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.plmore-btn { cursor: pointer; list-style: none; }
.plmore-btn::-webkit-details-marker { display: none; }
.plmore-rows { width: 100%; }

.pllist { display: flex; flex-direction: column; gap: 10px; }
.pllist-empty { margin: 0; padding: 14px 0; }

/* THE DAY DIVIDER (board 20). The date is said once, on a heading, and the rows
 * under it carry the clock alone — the class hub's own grammar (TEN-1610), in
 * the same heading face and the same hairline, because a date on a public list
 * and a date on an organizer's list are the same fact. */
.plday { display: flex; flex-direction: column; gap: 10px; }
.plday-head { display: flex; align-items: center; gap: 10px; font-family: var(--b-font-heading); font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
.plday-head::after { content: ""; flex: 1; height: 1px; background: var(--border-subtle); }
.plday-today .plday-head { color: var(--role-primary-ink); }

/* THE ROW, AND THE LENS'S OTHER TWO DEVICES. The card inside is the kit's and is
 * untouched; this wrapper adds the accent rail and the small mark, which is what
 * keeps the marking a property of THIS page rather than a variant of a component
 * every other surface shares (R6: one family, conditional on data presence). */
.plrow { display: flex; align-items: center; gap: 10px; min-width: 0; }
.plrow > .ktrow { flex: 1; min-width: 0; }
.plrow-mine { padding-left: 9px; border-left: 3px solid var(--role-primary); }
.plrow-mark { width: 20px; height: 20px; font-size: 9px; flex-shrink: 0; }

@media (max-width: 640px) {
  .plident { align-items: flex-start; }
  .plident-counts { gap: 14px; font-size: 12.5px; }
  .plident-name { font-size: 18px; }
  .plbanner { padding: 12px; }
  .plbanner-acts { margin-left: 0; }
  /* ON A PHONE THE LIT BAND'S THREE ACTS SIT ON ONE COMPACT ROW (TEN-1894), so
     they do not stack two deep over the very map they are about, and the credit
     moves to the top corner, where no act can cover it — the provider's terms
     want it legible wherever the imagery appears. */
  .plbanner-lit .plbanner-acts { flex-wrap: nowrap; gap: 6px; }
  .plbanner-lit .plbanner-act { height: 32px; padding: 0 9px; font-size: 11px; letter-spacing: 0.05em; white-space: nowrap; }
  .plbanner-lit .plbanner-credit { top: 8px; bottom: auto; }
  /* The band stacks: the sentence is the point and the link under it still
     reaches the same address the chip reveals. */
  .plband { align-items: flex-start; }
  .plband-only { margin-left: 44px; }
}

/* ══ BOOKING TERMS (TEN-1640, blessed comp board 22) ═══════════════════════
 *
 * The class's resolved policy read back as sentences, COLLAPSED BY DEFAULT
 * (Mark's binding modification to the blessing). One 48px disclosure row where
 * the panel sits: the terms never crowd the booking act, and they are not
 * buried behind a link either.
 *
 * THE DISCLOSURE IS A NATIVE `details` AND NEEDS NO SCRIPT (R9). The scriptless
 * path and the product are the same path, which is the strongest form of R9
 * clause 3 — there is no enhanced behaviour left over for a kit asset to own,
 * and K11's "pure state reflection is not a family and gets no file" says the
 * same thing from the other side.
 */
.bterms { border: 1px solid var(--border-subtle); border-radius: var(--card-r); background: var(--surface-raised); color: var(--text-primary); }

/* NO NATIVE CHROME, EVER (K11a). Three spellings and none is redundant: the
 * standards marker is a `::marker` on the summary, WebKit draws its triangle
 * through a pseudo-element no list rule reaches, and `list-style: none` is what
 * older engines honour. One missing spelling is one browser showing the UA's
 * triangle beside ours. */
.bterms > summary { list-style: none; }
.bterms > summary::marker { content: ""; }
.bterms > summary::-webkit-details-marker { display: none; }

/* THE WHOLE ROW IS THE HIT TARGET (K11a's amendment, read onto a disclosure).
 * The summary IS the control — there is no inner button to take the press off
 * it — and the chevron is a flow item INSIDE its box rather than a glyph beside
 * it, so no strip within the row's border belongs to something that is not the
 * row. That is the ornament-takes-clicks failure stated as a construction. */
.bterms-row { display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 0 20px; cursor: pointer; border-radius: var(--card-r); }
.bterms-row:hover { background: var(--surface-overlay); }
.bterms-row:focus-visible { outline: 2px solid var(--role-primary); outline-offset: -2px; }
.bterms-chev { width: 13px; height: 13px; color: var(--text-muted); transition: transform 120ms ease; }
.bterms[open] .bterms-chev { transform: rotate(90deg); }
.bterms[open] .bterms-row { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.bterms-name { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.05em; font-size: 16px; font-weight: 700; }
/* The meta earns the tap: how many rules, and which terms they are. */
.bterms-meta { margin-left: auto; font-family: var(--b-font-mono); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
.bterms-body { padding: 2px 20px 18px; display: flex; flex-direction: column; gap: 12px; }
.bterms-act { margin: 0; text-align: right; }
/* A `.link` that is a real button, because its act is the browser's and not an
 * address. The box is stripped so it reads as the link tier K12 puts it in. */
.bterms-print { font-size: 13px; font-family: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.bterms-print[hidden] { display: none; }
.bterms-lines { display: flex; flex-direction: column; gap: 9px; }
.bterms-line { font-size: 14.5px; line-height: 1.55; }
.bterms-foot { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-top: 1px solid var(--border-subtle); padding-top: 10px; }
.bterms-stamp { font-family: var(--b-font-mono); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); }
.bterms-enforced { font-size: 12px; color: var(--text-muted); }
/* The printed sheet's own heading. Off-screen entirely: the summary names the
 * panel on screen, and on paper there is no summary to read. */
.bterms-sheet { display: none; }
/* THE HUB'S MOUNT SHARES THE PAGE'S WIDTH (TEN-1644 item 6). It used to be a
 * centred 720px column, which was right while the hub had a 320px side column
 * beside the schedule: the terms sat under both and a full-width panel would have
 * been wider than anything above it. With the Audience block gone the schedule is
 * the page's width, and a narrower centred panel under it is the orphaned column
 * the ticket asks to close — two different left edges on one page. */
.clshub-terms { margin: 0; }

@media (max-width: 640px) {
  .bterms-row { flex-wrap: wrap; padding: 10px 14px; }
  .bterms-meta { margin-left: 25px; width: 100%; }
  .bterms-body { padding: 2px 14px 16px; }
  .bterms-foot { flex-direction: column; align-items: flex-start; gap: 6px; }
}

/* ══ THE PRINTED SHEET (board 22 section 2) ════════════════════════════════
 *
 * THE PRINT STATE RENDERS THE FULL PANEL WHATEVER THE COLLAPSE STATE, and that
 * is the one thing `details` does not give for free: a closed one prints
 * closed. `[open]` is deliberately not consulted anywhere below — a sheet
 * taped to the front desk must not depend on what the person at the printer
 * happened to have unfolded.
 *
 * BOTH FORCING RULES ARE REQUIRED because the engines hide a closed details in
 * two different ways. Older ones hide the non-summary content itself, which the
 * `display` rule reaches; current Chrome and Firefox skip it through
 * `content-visibility` on `::details-content`, which no `display` can reach and
 * which older engines drop as an unknown selector. Measured in the browser gate
 * rather than asserted, under emulated print media.
 *
 * PAPER IDIOM, NOT THE DARK THEME PHOTOCOPIED: black on white, the chevron and
 * the Print control gone (neither means anything on paper), and the sheet's own
 * heading revealed in the summary's place. The stamp survives the print, which
 * is what makes a sheet on the wall checkably current or checkably stale.
 */
@media print {
  .bterms > .bterms-body { display: block !important; }
  .bterms::details-content { content-visibility: visible !important; block-size: auto !important; }
  .bterms { border: 0; background: #fff; color: #000; border-radius: 0; }
  .bterms-row { display: none; }
  .bterms-act { display: none; }
  .bterms-sheet { display: block; font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.02em; font-size: 19px; font-weight: 800; color: #000; border-bottom: 2px solid #000; padding-bottom: 6px; margin-bottom: 10px; }
  .bterms-body { padding: 0; }
  .bterms-lines { display: block; }
  .bterms-line { font-size: 12.5px; line-height: 1.5; color: #000; margin-bottom: 7px; break-inside: avoid; }
  .bterms-line .ktstrong { color: #000; }
  .bterms-foot { border-top: 1px solid #000; padding-top: 6px; margin-top: 4px; }
  .bterms-stamp, .bterms-enforced { color: #000; font-size: 9.5px; }
}

/* == THE TERM SHEET FOR COUNSEL (TEN-1641, blessed comp board 23) ===========
 *
 * An org-level export the owner hands their attorney. Board 23 draws the app's
 * dark chrome around a WHITE PAPER SHEET, and in print there is no chrome left
 * at all -- the page IS the sheet, because the "PDF" is the browser's print of
 * it.
 *
 * THE SHEET IS PAPER IN EVERY THEME, AND THAT IS THE ONE PLACE THIS FILE PAINTS
 * OUTSIDE THE TOKENS. Every colour below is a literal, because the object being
 * drawn is a picture of the document that comes out of the printer: a sheet that
 * went dark with the theme would preview one artifact and print another, which is
 * exactly the disagreement between the drawing and the thing that this whole
 * ticket exists to close. The ink is #17181C on #FFFFFF -- about 17:1 -- so the
 * fixed palette clears AAA and the high-contrast theme loses nothing by it.
 *
 * THE CHROME AROUND IT IS TOKENED as usual: the back link and the Print control
 * belong to the app, not to the document, and they are gone on paper.
 */
.csheet-page { max-width: 720px; margin: 0 auto; display: flex; flex-direction: column; gap: 14px; }
.csheet-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
/* A `.link` that is a real button, because its act is the browser's and not an
   address -- the booking-terms sheet's own construction (TEN-1640). */
.csheet-print { font-size: 13px; font-family: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.csheet-print[hidden] { display: none; }

.csheet { background: #FFFFFF; color: #17181C; border-radius: 4px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); box-sizing: border-box; padding: 30px 36px; display: flex; flex-direction: column; gap: 18px; }
.csheet-head { display: flex; flex-direction: column; gap: 2px; border-bottom: 2px solid #17181C; padding-bottom: 10px; }
.csheet-org { font-family: var(--b-font-heading); font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; font-size: 13px; color: #565A62; }
.csheet-title { font-family: var(--b-font-heading); font-weight: 800; text-transform: uppercase; font-size: 22px; line-height: 1.15; margin: 0; color: #17181C; }
.csheet-prov { font-family: var(--b-font-mono); font-size: 9.5px; color: #878B94; }

/* THE DISCLAIMER BAND. Amber because it is the one thing on the sheet that is
   not a fact about the organization -- it is what the document IS -- and a
   reader's eye has to land on it before the rules. It is structural in the
   plate: no condition, no data key, no way to render this sheet without it. */
.csheet-band { background: rgba(176, 119, 20, 0.10); border: 1px solid rgba(176, 119, 20, 0.45); border-radius: 6px; padding: 12px 16px; font-size: 12px; line-height: 1.55; color: #7A5000; }
.csheet-band strong { color: #6A4500; }

/* THE SHEET'S OWN GAP. A second band, drawn plainer than the disclaimer so the
   two do not compete: the disclaimer says what the document IS and this says
   what it does not cover. A dashed edge, because what is behind it is a reading
   that did not complete rather than a rule that did. */
.csheet-gap { border: 1px dashed #9A9EA6; border-radius: 6px; padding: 10px 14px; font-size: 11.5px; line-height: 1.55; color: #3C4046; }

.csheet-sec { display: flex; flex-direction: column; gap: 8px; }
.csheet-head2 { font-family: var(--b-font-heading); font-size: 13px; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; color: #565A62; border-bottom: 1px solid #D3D7DD; padding-bottom: 4px; margin: 0; }

/* SECTION 1 -- a sentence on the left, the verbatim wire settings on the right.
   The provenance column never wraps: it is a citation, and a setting name broken
   over two lines is a setting name somebody has to reassemble. */
.csheet-rule { display: flex; justify-content: space-between; gap: 18px; font-size: 12.5px; line-height: 1.5; break-inside: avoid; }
.csheet-said { display: flex; flex-direction: column; gap: 2px; }
.csheet-text { color: #17181C; }
/* The attendee panel's own bold lead ("Commitment:", "Grace:"), on paper's ink
   rather than the theme's -- `.ktstrong` resolves through a token, and a token on
   a fixed-white sheet is the one combination that can go invisible. */
.csheet-text .ktstrong { color: #17181C; }
/* HOW THIS RULE EARNED ITS STANDING -- "Organization default", or a COUNT of the
   classes that share it. Small and quiet, and never absent: a standing rule that
   did not say how standing it is would be a claim rather than a derivation. */
.csheet-basis { font-size: 10.5px; color: #6E7280; }
/* Said ONCE where every row earned its standing the same way — under the section
   heading rather than repeated four times in four different greys. */
.csheet-basis-all { margin: -2px 0 2px; }
.csheet-wire { font-family: var(--b-font-mono); font-size: 9.5px; color: #878B94; white-space: nowrap; padding-top: 2px; }

/* SECTION 2 -- the computed variances. A grid, because these are FACTS being
   compared down a column (R7's column test), not entities being chosen between. */
.csheet-diffs { display: grid; grid-template-columns: 1fr 1fr 1fr; font-size: 12px; }
.csheet-col { font-family: var(--b-font-mono); padding: 4px 0; color: #878B94; font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.04em; }
.csheet-cell { padding: 5px 0; border-top: 1px solid #E4E6EA; line-height: 1.5; color: #17181C; }
.csheet-cell b { font-weight: 700; }
.csheet-none { font-size: 12.5px; line-height: 1.5; color: #565A62; margin: 0; }

/* SECTION 3 -- the vocabulary. Mirrored from the docs container's canon; the Go
   file carries the lockstep note. */
.csheet-terms { display: grid; grid-template-columns: 130px 1fr; font-size: 12px; line-height: 1.5; }
.csheet-term { padding: 4px 0; font-weight: 600; color: #17181C; }
.csheet-means { padding: 4px 0; color: #17181C; }

/* SECTION 4 -- the enforcement seam, two columns exactly as board 23 draws it. */
.csheet-seam { display: grid; grid-template-columns: 1fr 1fr; gap: 0 24px; font-size: 12px; line-height: 1.55; }
.csheet-seam .csheet-col { padding: 0 0 4px; }
.csheet-seam .csheet-cell { padding-top: 6px; }

.csheet-foot { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; border-top: 1px solid #D3D7DD; padding-top: 8px; font-family: var(--b-font-mono); font-size: 9.5px; color: #878B94; }

/* The entry card under the classes listing. The sub-line is mono because it
   names the ARTIFACT rather than describing it -- "PDF, regenerated, stamped"
   are properties of the file, on the same tier as the sheet's own stamp. */
.clslist-foot { margin-top: 18px; }
.clslist-foot .ktsent-hint { font-family: var(--b-font-mono); font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; }

@media (max-width: 640px) {
  .csheet { padding: 20px 18px; }
  .csheet-rule { flex-direction: column; gap: 4px; }
  .csheet-wire { white-space: normal; padding-top: 0; }
  .csheet-diffs { grid-template-columns: 1fr 1fr; }
  .csheet-diffs .csheet-col:nth-child(3) { display: none; }
  .csheet-terms { grid-template-columns: 1fr; }
  .csheet-term { padding-bottom: 0; }
  .csheet-seam { grid-template-columns: 1fr; }
}

/* == THE PRINTED TERM SHEET ================================================
 *
 * THE PAGE BECOMES THE SHEET. The booking-terms rules one block up had to force
 * a collapsed `details` open; this document has nothing to unfold, and what it
 * has instead is a whole application around it. So the app's own header and
 * footer go, the on-screen chrome row goes, and the sheet loses the shadow and
 * the rounded corner that were drawing "a page on a desk" -- on paper it IS the
 * page.
 *
 * THE CHROME IS HIDDEN ONLY WHERE THIS SHEET IS ON THE PAGE (`body:has(.csheet)`),
 * because this stylesheet is the whole book's and every other screen still prints
 * the way it always did. A book printing a page that is not this one is not this
 * ticket's business, and a blanket rule would have made it so.
 */
@media print {
  body:has(.csheet) > .app-header,
  body:has(.csheet) > .app-foot { display: none !important; }
  .csheet-top { display: none; }
  .csheet-page { max-width: none; }
  .csheet { box-shadow: none; border-radius: 0; padding: 0; gap: 16px; background: #fff; color: #000; }
  .csheet-org, .csheet-title, .csheet-text, .csheet-term, .csheet-means, .csheet-cell { color: #000; }
  .csheet-text .ktstrong { color: #000; }
  .csheet-head { border-bottom-color: #000; }
  .csheet-head2 { color: #333; border-bottom-color: #999; }
  .csheet-band { background: transparent; border: 1.5px solid #000; color: #000; break-inside: avoid; }
  .csheet-band strong { color: #000; }
  .csheet-gap { border-color: #000; color: #000; break-inside: avoid; }
  .csheet-sec { break-inside: auto; }
  .csheet-basis, .csheet-wire, .csheet-col, .csheet-prov, .csheet-foot { color: #555; }
  .csheet-cell { border-top-color: #BBB; }
  .csheet-foot { border-top-color: #000; }
}

/* == THE NEUTRAL FLASH (TEN-1653) ==========================================
 *
 * A THIRD TIER, and it was needed the first time a DECIDED verdict was neither
 * a success nor a failure. A scan forty minutes early lands on the booking page
 * carrying "check-in opens at 6:40 AM" — true, useful, and not an error: painting
 * it in danger red would teach a reader that arriving early is a mistake, and
 * painting it green would claim something happened.
 *
 * So it is the calm tier: the panel's own ground and a quiet left edge, on the
 * same box `.flash` already draws. Two tiers were enough while every verdict was
 * an act's outcome; a landing page is the surface where that stopped being true.
 */
.flash-note { background: var(--surface-raised); color: var(--text-primary); border: var(--b-border-w) solid var(--border-subtle); border-left-width: 3px; }

/* == THE SCAN LANDING (TEN-1653, blessed comp board 7d) =====================
 *
 * WHAT A PHONE SHOWS IN A DOORWAY. Board 7d draws it at 390px inside a phone
 * outline; the outline is the board's device frame and never ships, so what is
 * here is the CARD — a single narrow column that is the whole page on a phone and
 * a centred card on a laptop.
 *
 * IT IS APP IDIOM, NOT PAPER. Every colour below is a token: this page is read on
 * a screen in whatever theme its reader keeps, and it is the poster one wall over
 * that is a picture of printed paper. The two surfaces of this ticket are
 * deliberately opposite in exactly that way.
 *
 * THE TAP IS 56px TALL and full width, which is the one dimension on this page
 * that is a requirement rather than a preference: it is pressed with a thumb, by
 * somebody holding a bag, in a doorway, possibly in the cold.
 */
.drland { max-width: 420px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }
.drland-card { background: var(--surface-raised); border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-lg); padding: 24px 20px; display: flex; flex-direction: column; gap: 18px; }
.drland-head { display: flex; flex-direction: column; gap: 4px; }
.drland-class { font-family: var(--b-font-heading); font-size: 26px; font-weight: 800; line-height: 1.1; margin: 0; color: var(--text-primary); }
.drland-when { font-size: 14.5px; color: var(--text-muted); }

/* THE STATE BLOCK — one heading, one sentence, and on the walk-up arm one more
   about what the check-in spends. Bordered rather than tinted so that the ONE
   tinted thing on this page is the confirmed state. */
.drland-state { display: flex; align-items: flex-start; gap: 10px; border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); padding: 14px 16px; }
.drland-state-done { border-color: var(--good); background: var(--good-a); }
.drland-tick { width: 20px; height: 20px; flex-shrink: 0; margin-top: 1px; color: var(--good); }
.drland-said { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.drland-title { font-size: 15.5px; font-weight: 700; line-height: 1.35; color: var(--text-primary); }
.drland-note { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text-muted); }
/* The covering entitlement reads in BODY ink: it is the one line on this arm that
   costs the reader something, and a muted tier would file it as a footnote. */
.drland-covered { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text-primary); }

.drland-act { margin: 0; }
.drland-tap { width: 100%; min-height: 56px; font-size: 17px; font-weight: 700; }
.drland-retry { align-self: flex-start; }
.drland-sched { font-size: 13px; text-align: center; }
.drland-foot { border-top: var(--b-border-w) solid var(--border-subtle); padding-top: 12px; font-size: 12.5px; text-align: center; color: var(--text-muted); }

/* == THE DOOR POSTER (TEN-1653, blessed comp board 7e) ======================
 *
 * PAPER IDIOM, ON THE COUNSEL SHEET'S OWN TERMS (TEN-1641) and for the same
 * reason: the object being drawn is a picture of what comes out of the printer,
 * so every colour on the sheet is a LITERAL and a theme switch must not move it.
 * A poster that previewed dark and printed white would be the disagreement between
 * the drawing and the artifact that the paper idiom exists to close. The ink is
 * #17181C on #FFFFFF — about 17:1 — so the fixed palette clears AAA and the
 * high-contrast theme loses nothing by it.
 *
 * THE CHROME AROUND IT IS TOKENED as usual: the Print control belongs to the app,
 * not to the document, and it is gone on paper.
 *
 * THE SQUARE IS 260px AND THE CODE IS 30px, because this is read from across a
 * lobby by a phone held at arm's length and by somebody squinting at a keypad. The
 * QR's own two colours are `.ktqr-ground` / `.ktqr-ink` — already fixed rather than
 * themed (qrsvg.go carries the argument: a QR is read by a camera, and decoders
 * expect dark modules on a light ground).
 */
.drpost-page { max-width: 560px; margin: 0 auto; display: flex; flex-direction: column; gap: 14px; }
.drpost-top { display: flex; align-items: center; justify-content: flex-end; }
/* The NFC hint (TEN-1970 E3): screen chrome, left of the print link, gone in print. */
.drpost-nfc { margin: 0 auto 0 0; }
/* A `.link` that is a real button, because its act is the browser's and not an
   address — the counsel sheet's and the booking-terms sheet's construction. */
.drpost-print { font-size: 13px; font-family: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.drpost-print[hidden] { display: none; }

.drpost { background: #FFFFFF; color: #17181C; border-radius: 4px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); box-sizing: border-box; padding: 44px 40px; display: flex; flex-direction: column; align-items: center; gap: 22px; }
.drpost-head { display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
.drpost-class { font-family: var(--b-font-heading); font-weight: 800; text-transform: uppercase; font-size: 30px; line-height: 1.1; margin: 0; color: #17181C; }
.drpost-when { font-size: 15px; color: #565A62; }

.drpost-qr { border: 1px solid #D3D7DD; border-radius: 8px; padding: 18px; line-height: 0; }
.drpost .ktqr { width: 260px; height: 260px; }

.drpost-say { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; }
.drpost-lead { font-family: var(--b-font-heading); font-size: 22px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.02em; color: #17181C; }
.drpost-line { margin: 0; font-size: 15px; line-height: 1.5; color: #565A62; max-width: 34ch; }

/* THE CODE IN LARGE PRINT. `word-break` rather than a scroll: this value is typed
   at a keypad by somebody whose camera is dead, so every character has to be on
   the sheet at once — and on paper there is no scrolling at all. */
.drpost-chars { display: flex; flex-direction: column; align-items: center; gap: 4px; max-width: 100%; }
.drpost-typelabel { font-family: var(--b-font-mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: #878B94; }
.drpost-code { font-family: var(--b-font-mono); font-size: 30px; font-weight: 700; letter-spacing: 0.06em; line-height: 1.25; word-break: break-all; text-align: center; color: #17181C; }
.drpost-addr { font-size: 12px; line-height: 1.45; word-break: break-all; text-align: center; color: #878B94; }
.drpost-note { margin: 0; font-size: 12.5px; line-height: 1.5; text-align: center; color: #7A5000; border: 1px solid rgba(176, 119, 20, 0.45); background: rgba(176, 119, 20, 0.10); border-radius: 6px; padding: 10px 14px; }
.drpost-foot { border-top: 1px solid #D3D7DD; width: 100%; padding-top: 14px; font-size: 13px; text-align: center; color: #565A62; }

@media (max-width: 480px) {
  .drpost { padding: 28px 20px; }
  .drpost .ktqr { width: 200px; height: 200px; }
  .drpost-code { font-size: 24px; }
}

/* == THE PRINTED POSTER ====================================================
 *
 * THE PAGE BECOMES THE SHEET, exactly as the counsel sheet's rules do it: the
 * app's header and footer go, the on-screen chrome row goes, and the sheet loses
 * the shadow and the rounded corner that were drawing "a page on a desk". On paper
 * it IS the page.
 *
 * THE CHROME IS HIDDEN ONLY WHERE THIS SHEET IS ON THE PAGE (`body:has(.drpost)`),
 * because this stylesheet is the whole book's and every other screen still prints
 * the way it always did.
 *
 * NOTHING HERE HIDES THE CODE, THE SQUARE OR THE INSTRUCTION. The print rules
 * exist to delete chrome, and the mistake a paper-idiom pass makes is carrying on
 * deleting — so the three things somebody tapes to a door are asserted present
 * under print media in the browser gate rather than trusted to a diff.
 */
@media print {
  body:has(.drpost) > .app-header,
  body:has(.drpost) > .app-foot { display: none !important; }
  .drpost-top { display: none; }
  .drpost-page { max-width: none; }
  .drpost { box-shadow: none; border-radius: 0; padding: 0; gap: 20px; background: #fff; color: #000; break-inside: avoid; }
  .drpost-class, .drpost-lead, .drpost-code { color: #000; }
  .drpost-when, .drpost-line, .drpost-foot { color: #333; }
  .drpost-qr { border-color: #000; padding: 12px; }
  .drpost-addr, .drpost-typelabel { color: #555; }
  .drpost-note { border-color: #000; background: transparent; color: #000; }
  .drpost-foot { border-top-color: #000; }
}

/* ===========================================================================
   THE COMMERCE CARDS (K25 / K26 / K28 / K29, TEN-1687; boards A1b + A3) —
   the plan-card, the way-in pill and the offer-card.

   ONE COLOUR IS SPENT ON THIS FAMILY AND IT IS SPENT ONCE. K29: green means
   `covered / confirmed`, a fact about YOU, so it appears on the owned pill and
   the plan-card's holding line and NOWHERE else. Nothing here is red (a price
   promo, and only that), nothing is dark (live now, at most one per page
   state), and the badge hues are declared but unused in R1 because no badge
   has a provenance yet.

   WEIGHT AND SPACE DO THE REST, which is the discipline the polarity law needs:
   the plan-card's number is the heaviest thing on it because the number is what
   the card is about, and the offer-card prints no number at all.
   ======================================================================== */

/* --- THE PLAN CARD (A3). The price is the card's centre of gravity. ---- */

.ktplan {
  display: flex; flex-direction: column; gap: 6px; align-items: flex-start;
  padding: var(--b-space-3) var(--b-space-4);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.ktplan-head { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; width: 100%; }
.ktplan-name { font-size: var(--b-text-lg); font-weight: var(--b-weight-bold); }
/* TEN-1924 UAT-4 C */
/* THE CARD'S NAME IS ALSO ITS LINK (TEN-1924), and it keeps the title's own
   look rather than turning blue: `.ktplan-name` still sets the size and
   weight, so this only removes the anchor's own color/underline and adds one
   back on hover — a link that reads as a title until you reach for it. */
.ktplan-namelink { color: inherit; text-decoration: none; }
.ktplan-namelink:hover { text-decoration: underline; }
/* END TEN-1924 UAT-4 C */

/* K26's three badges. BLUE is derived social proof and the cover has that
   channel, so "Most popular" wears it. "Best value" is arithmetic — neither a
   claim about people nor the club's voice — so it rides the neutral ground.

   FEATURED IS DRAWN AS AN OUTLINE AND NOT AS PURPLE, deliberately. K29 assigns
   purple to the club's curation, and this cover has no purple token: minting
   one here would be minting a meaning-bearing colour (K6's legibility clause)
   for a badge that renders on nothing in R1, in service of the DRESSED band
   that is deferred. What K26 actually requires of this badge is that curation
   is LABELLED — "Featured · picked by the club" — and the label carries that
   with no pigment at all. The purple mint lands with H1, where the featured
   strip it belongs to lives.

   None of the three renders in R1: no badge has a provenance on the wire yet
   (kitcommercecards.go's ktPlanBadge says which and why). */
.ktplan-badge {
  margin-left: auto; font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 2px 10px; white-space: nowrap;
  border: 1px solid transparent;
}
.ktplan-badge-featured { color: var(--text-muted); border-color: var(--border-strong); }
.ktplan-badge-popular { color: var(--blue-ink, var(--text-primary)); background: var(--blue-a); }
.ktplan-badge-value { color: var(--text-primary); background: var(--surface-page); border-color: var(--border-subtle); }

/* THE NUMBER CARRIES THE WEIGHT, THE UNIT CARRIES THE MEANING, and they sit on
   one line because they are one fact (board 0b). */
.ktplan-price { margin: 0; display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.ktplan-amt { font-size: var(--b-text-xl); font-weight: var(--b-weight-bold); }
.ktplan-unit { font-size: var(--b-text-sm); color: var(--text-muted); }

.ktplan-reach { margin: 0; font-size: var(--b-text-sm); line-height: var(--b-leading-relaxed); max-width: 46ch; }
/* THE REACH NAMES PROGRAMS AND EVERY NAME IS A PRESS (board A3, TEN-1699).
   They are drawn as ink rather than as a row of chips: this is a SENTENCE about
   what the plan covers, and a sentence whose nouns are buttons stops being one.
   The link tier is K12's — the primary at text weight, which is the token minted
   for exactly this ("--role-primary is tuned to be a FILL; as ink on a surface
   it needs its own value"). */
.ktplan-prog, .ktplan-more {
  color: var(--role-primary-ink); text-decoration: none; font-weight: var(--b-weight-semibold);
}
.ktplan-prog:hover, .ktplan-more:hover { text-decoration: underline; }
.ktplan-more { white-space: nowrap; }

/* K29: the viewer's own standing, and the only green on the card. */
.ktplan-have {
  margin: 0; display: flex; align-items: center; gap: 6px;
  font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
  color: var(--green-ink, var(--text-primary));
}
.ktplan-tick { font-weight: var(--b-weight-bold); }
/* The PILL form of the same standing (TEN-1768): the pill carries its own
   green, its own tick and its own shape, so the line around it is a plain
   block — no colour, no gap, nothing for the two to disagree about. */
.ktplan-have-pill { display: block; color: inherit; margin-top: 2px; }
/* K30 arm 2 where the act would be (TEN-1704): the club cannot take a card, so
   the button is absent and this says why. MUTED INK, never green and never a
   warning colour — green is a fact about YOU (K29) and an alarm colour would
   tell a stranger something is broken when the club is simply not finished. */
.ktplan-note {
  margin: 4px 0 0 0; font-size: var(--b-text-sm); color: var(--text-muted);
}
.ktplan-act { margin: 4px 0 0 0; }

/* --- THE WAY-IN PILL (A1b). A name, never a price. ------------------- */

.ktpill {
  display: inline-flex; align-items: center; gap: 5px;
  height: 1.65rem; padding: 0 12px; border-radius: var(--b-radius-full);
  border: 1px solid var(--border-strong); background: var(--surface-raised);
  color: var(--text-primary); text-decoration: none;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  white-space: nowrap;
}
a.ktpill:hover { border-color: var(--text-muted); }
/* THE OWNED PILL GOES GREEN, and this is K29's one assignment on this family:
   covered / confirmed, a fact about YOU. */
.ktpill-have {
  background: var(--green-a); border-color: var(--green-a);
  color: var(--green-ink, var(--text-primary));
}
.ktpill-tick { font-weight: var(--b-weight-bold); }
.ktpill-bal { font-weight: var(--b-weight-normal); }

/* --- THE OFFER CARD (A1b). Catalog rhythm: identity, facts, one act. -- */

.ktoffer {
  display: flex; flex-direction: column; gap: var(--b-space-2);
  padding: var(--b-space-3) var(--b-space-4);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.ktoffer-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--b-space-3); flex-wrap: wrap;
}
.ktoffer-id { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; min-width: 0; }
.ktoffer-title { font-size: var(--b-text-md); font-weight: var(--b-weight-bold); }
/* The SHAPE of the thing, in the condensed face the kit uses for labels. */
.ktoffer-type {
  font-family: var(--b-font-heading); text-transform: uppercase;
  letter-spacing: 0.04em; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  color: var(--text-muted); background: var(--surface-page);
  border: 1px solid var(--border-subtle);
  border-radius: var(--b-radius-sm); padding: 2px 8px; white-space: nowrap;
}
.ktoffer-when { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.ktoffer-blurb { margin: 0; font-size: var(--b-text-sm); line-height: var(--b-leading-relaxed); max-width: 72ch; }

/* THE ACT ZONE HAS ONE HOME (TEN-1722, Mark's ruling): the act sits BELOW the
   ways-in band, at the LEFT, on every card — however many coverage pills that
   card happens to carry.

   WHAT IT REPLACES, AND WHY THE OLD RULE LOOKED RIGHT. This was
   `justify-content: space-between` with a ways band that did not claim the line,
   so the act rode hard right BESIDE a short ways-in ("Its own thing — free") and
   dropped to its own line the moment the band grew wide enough to wrap — where
   space-between resolves to flex-start and put it on the LEFT. Four cards on the
   Programs tab, three acts right and one left, and the side a card's Book lands
   on was decided by how much coverage it happened to have. That is the jump: not
   two positions somebody chose, but one rule whose answer changes with the
   content.

   THE BAND CLAIMS THE WHOLE LINE (`flex: 1 1 100%`), so the act always wraps to
   its own, and the pills may wrap to as many lines as they like underneath —
   they and the act no longer share a line to compete for. The phone arm below
   (<=560px) already gave the act its own full-width row, so the narrow screen
   was ALREADY drawing this layout: the wide screen now agrees with it instead of
   having a second answer. */
.ktoffer-foot {
  display: flex; align-items: center; justify-content: flex-start;
  gap: var(--b-space-3); flex-wrap: wrap;
}
.ktoffer-ways { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; min-width: 0; flex: 1 1 100%; }
/* A card with no ways-in at all (no pills, no clause, no solo line) must not pay
   the band's row gap for an empty box it never drew. */
.ktoffer-ways:empty { display: none; }
.ktoffer-inc { font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold); color: var(--text-muted); white-space: nowrap; }
/* The drop-in is the QUIET trailing clause — never a pill, and never drawn at a
   pill's weight either, or it would read as one. */
.ktoffer-dropin { font-size: var(--b-text-sm); color: var(--text-muted); }
.ktoffer-solo { margin: 0; font-size: var(--b-text-sm); }
.ktoffer-act { flex: none; }

/* THE STATED FACT (K15's amendment). Drawn as a line of text and given NO hit
   affordance of any kind: no pointer, no hover, no underline. The inertness is
   a decision rather than an omission, so it is drawn deliberately. */
.ktoffer-next { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }

/* --- THE PROGRAMS TAB'S BANDS (A3). Bundles lead. -------------------- */

.sfbands { display: flex; flex-direction: column; gap: var(--b-space-5); }
.sfband { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfband-h { margin: 0; font-size: var(--b-text-md); font-weight: var(--b-weight-bold); }
/* The plans sit across a wide column and stack on a narrow one; `auto-fit`
   rather than a fixed count, so the band's length and the layout stay
   independent of each other. */
.sfband-plans {
  display: grid; gap: var(--b-space-2);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}
.sfband-catalog { display: flex; flex-direction: column; gap: var(--b-space-2); }

/* --- THE LOCATIONS TAB (S4 + A8). The row answers ONE question. -------

   "How far is this from me, practically." So the row is a two-column split:
   the facts on the left in reading order (name, where, what happens there)
   and the handoffs on the right, and the handoffs do not wrap up into the
   facts — a Map link that landed under an address reads as part of it.

   THE ACTS ARE ALL ONE TIER. Every one of them is K12's link tier, because
   nothing on this page is bought, booked or joined: the only thing a reader
   can do at a location is find it and read more about it. A page with no
   advancing act draws no button, and that is a fact about the page rather
   than an omission in it.

   THE GROUP HEAD IS THE SAME EYEBROW BOTH GROUPED SHAPES WEAR. By-region and
   by-activity are one layout with two partitions (the ladder chose which),
   so they share a head rather than growing one each. The flat shape renders
   one group with NO head, which is why there is no `:only-child` rule here:
   the head is simply absent from the markup. */

.sfvenues { display: flex; flex-direction: column; gap: var(--b-space-4); }
.sfvengroup { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfvengroup-h {
  margin: 0;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted);
}

.sfvenue {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--b-space-3); padding: var(--b-space-3);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.sfvenue-facts { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sfvenue-name { font-size: var(--b-text-md); font-weight: var(--b-weight-semibold); }
.sfvenue-where { font-size: var(--b-text-sm); color: var(--text-muted); }
/* The derived clause. Quieter than the address, because the address is the
   answer and this is the reason somebody would want it. */
.sfvenue-doing { font-size: var(--b-text-sm); color: var(--text-muted); }
.sfvenue-acts {
  flex: none; display: flex; align-items: center;
  gap: var(--b-space-3); flex-wrap: wrap; justify-content: flex-end;
}
.sfvenue-act { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); white-space: nowrap; }

@media (max-width: 560px) {
  /* The acts drop UNDER the facts rather than squeezing beside them: three
     nowrap links and an address cannot share a phone's width, and a row that
     tried would hyphenate the street. */
  .sfvenue { flex-direction: column; align-items: flex-start; }
  .sfvenue-acts { justify-content: flex-start; }
}

@media (max-width: 560px) {
  .ktoffer-head, .ktoffer-foot { align-items: flex-start; }
  .ktoffer-act { width: 100%; }
  .ktplan-badge { margin-left: 0; }
}

/* ===========================================================================
   THE LISTING HEAD (K27, TEN-1688; boards A7 + A8) — the earned bar over
   every listing page in the book.

   ONE QUIET ROW. The head is chrome over somebody else's list, so it spends
   weight and space rather than colour: nothing here is green (that means
   `covered`, a fact about YOU — K29), nothing is dark (`live now`), nothing is
   red, purple or blue. The lit chip is drawn in the SAME channel the tab bar
   uses one rung up — ink and a ground, not an accent — because a chip that
   competed with the page's one advancing act would be a filter shouting over a
   Book button.

   THE LIT CHIP IS A CLASS, NOT A `:has(input:checked)`, and that is the one
   place this family departs from `.ktday`'s idiom. K27 spells a chip's
   scriptless arm as a filter LINK, so a chip is an anchor and there is no
   checked state for CSS to read; assets/ktlist.js moves the class, and its
   header carries the argument.
   ======================================================================== */

.ktlist { display: flex; flex-direction: column; gap: var(--b-space-2); }

/* The anatomy's order is the DOM's order, and the row wraps rather than
   scrolls: a filter that ran off the side of a phone would be a control only a
   wide screen has. */
.ktlist-bar {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--b-space-2) var(--b-space-3);
  padding-bottom: var(--b-space-2);
  border-bottom: 1px solid var(--border-subtle);
}

.ktlist-count {
  margin: 0; white-space: nowrap;
  font-size: var(--b-text-sm); color: var(--text-muted);
}

.ktlist-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.ktlist-chip {
  display: inline-flex; align-items: center; height: 1.75rem;
  padding: 0 13px; border-radius: var(--b-radius-full);
  border: 1px solid var(--border-strong); background: var(--surface-raised);
  color: var(--text-muted); text-decoration: none;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  white-space: nowrap;
}
.ktlist-chip:hover { color: var(--text-primary); border-color: var(--text-muted); }
/* THE ACTIVE STATE IS A PAIR, AND IT IS THE KIT'S OWN PAIR (TEN-1700).
   It used to be `background: var(--text-primary); color: var(--surface-base)`,
   and `--surface-base` IS NOT A TOKEN THIS COVER DEFINES — it is defined
   nowhere in the book. An undefined custom property is invalid at computed
   value time, so `color` fell back to INHERIT: the selected chip drew the
   page's own near-white ink on a near-white ground, and the one thing on the
   bar a reader cannot read was the filter they are inside (Mark, storefront
   UAT r1). A ground stated without its ink is not a style, it is half of one.

   So both halves come from the pair every other selected control in this kit
   already uses — `.ktday:has(input:checked)`, `.ktroom`, `.ktseg-opt`,
   `.ktstep-current .ktstep-num` — which is defined in ALL THREE themes and is
   the only pair guaranteed to read (`--ink-on-primary`'s own note at
   `.kttoggle-knob`). The kit owns the family, so every ktlist consumer — the
   Events tab, Locations, the compare entries, the kit gallery — inherits it.

   AND THE HOVER RULE IS PART OF THE PAIR. `.ktlist-chip:hover` is one
   specificity step above `.ktlist-chip-on`, so hovering the selected chip used
   to repaint its ink and undo the pair from the other end; the on-state's own
   hover is declared here so it cannot be split again. */
.ktlist-chip-on, .ktlist-chip-on:hover {
  background: var(--role-primary); border-color: var(--role-primary);
  color: var(--ink-on-primary);
}
.ktlist-chip:focus-visible { outline: 2px solid var(--role-primary); outline-offset: 2px; }

/* The view toggle is a segmented pair of REAL LINKS — one continuous object,
   because the two rungs are two readings of one thing. */
.ktlist-views {
  display: inline-flex; border: 1px solid var(--border-strong);
  border-radius: var(--control-r); overflow: hidden;
}
.ktlist-view {
  padding: 0 14px; line-height: 1.75rem; text-decoration: none;
  background: var(--surface-raised); color: var(--text-muted);
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  white-space: nowrap;
}
.ktlist-view + .ktlist-view { border-left: 1px solid var(--border-strong); }
/* The segment's active rung takes the chip's pair, for the chip's reason and
   from the same tokens: it had the same undefined `--surface-base` ink and was
   the second unreadable control on the same bar (TEN-1700). */
.ktlist-view-on { background: var(--role-primary); color: var(--ink-on-primary); }

/* The form is one form (the search box, every escalated K15 select, and the
   submit that keeps the scriptless path complete). It sits at the far end and
   takes the slack, so the chips stay left and the box stays right. */
.ktlist-form {
  display: flex; align-items: center; gap: var(--b-space-2);
  margin: 0 0 0 auto; flex-wrap: wrap;
}
.ktlist-find { min-width: 12rem; flex: 1 1 12rem; }
.ktlist-find input {
  border: 0; background: transparent; color: inherit; font: inherit;
  flex: 1; min-width: 0; padding: 0;
}
.ktlist-find input:focus { outline: none; }
.ktlist-find:focus-within { border-color: var(--role-primary); }
.ktlist-go { flex: none; }
.ktlist-add { flex: none; }

.ktlist-none { margin: 0; padding: var(--b-space-4) 0 0 0; max-width: 56ch; }

@media (max-width: 560px) {
  /* The form drops to its own line rather than squeezing the chips off the
     screen; `auto` margins are what would otherwise hold it beside them. */
  .ktlist-form { margin-left: 0; width: 100%; }
}

/* THE GALLERY'S STAND-IN LIST, and it is only that. The head deliberately does
   not own a list's markup — every consumer draws its own cards and the head
   merely NAMES them — so the kit page supplies rows for a reviewer to watch a
   chip bite, exactly as the pager's fixture supplies a schedule. Nothing in
   production wears these two classes. */
.ktlist-demo { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktlist-demo-rows { display: flex; flex-direction: column; gap: 6px; }
.ktlist-demo-row {
  margin: 0; padding: var(--b-space-2) var(--b-space-3);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--b-space-3);
}

/* ===========================================================================
   THE CLUB STOREFRONT (TEN-1685, K21) — the public page of one club.

   R1 is UTILITARIAN by the board's own label: flat grounds, one accent, no
   hero and no scrim. The dressed variant (H1–H4) is deferred until R1 ships
   and real feedback lands, and the reason it can be deferred is that R2
   DECORATES R1 rather than replacing it — so nothing here may encode a
   treatment the dressed band would have to undo.

   COLOUR IS SPENT UNDER K29 AND NOWHERE ELSE. Green is `covered / confirmed`,
   a fact about YOU, and the KIT'S WAY-IN PILL (`.ktpill-have`) is the only
   thing that wears it — this family composes that rung and has no green of its
   own (TEN-1774). Nothing here is dark (that means `live now`, at most one per page
   state), nothing is red (a price promo, and only that) and nothing is purple
   or blue. A surface that wants emphasis reaches for weight and space, which
   is what this block gives it.
   ======================================================================== */

.sfpage { display: flex; flex-direction: column; gap: var(--b-space-4); }

/* --- THE MASTHEAD (K21 rung 1). Composed by cmc-auth and mounted as bytes,
   so these rules are the CONTRACT between the two modules: the class hooks
   are named in both censuses and a rename reds both sides at once. ------ */

.sfmast { display: flex; flex-direction: column; gap: var(--b-space-2); }

.sfmast-banner {
  aspect-ratio: 3 / 1; min-height: 110px; max-height: 210px;
  border-radius: var(--b-radius-md); overflow: hidden;
  /* background-COLOR, never the shorthand: the shorthand resets
     background-image, and the -d* defaults are background-images. */
  background-color: var(--surface-raised);
}
.sfmast-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The three built-in defaults, drawn from the cover's accent roles rather
   than fixed colours so a theme swap carries them. A club with no banner
   gets a quiet ground, never a dead strip (K30). */
.sfmast-banner-default { background-color: var(--surface-raised); }
.sfmast-banner-d0 { background-image: linear-gradient(135deg, var(--blue-a), var(--surface-raised) 62%); }
.sfmast-banner-d1 { background-image: linear-gradient(135deg, var(--green-a), var(--surface-raised) 62%); }
.sfmast-banner-d2 { background-image: linear-gradient(135deg, var(--amber-a), var(--surface-raised) 62%); }

/* THE OVERLAP ROW. The disc rises over the banner and the acts ride the same
   line at the far end. The negative top margin is the whole trick. */
/* `.orgc-facemount` is named here, not given a copy of the number (TEN-1813): the
   org console's avatar chip is positioned over this disc, so it needs the same
   value, and a second declaration of it is a second place for it to drift. Both
   selectors, one number, and the 560px override below does the same. */
.sfmast-full, .orgc-facemount { --sfmast-disc: 96px; }
.sfmast-top {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--b-space-3); flex-wrap: wrap;
  margin-top: calc(var(--sfmast-disc) / -2);
  padding: 0 var(--b-space-1);
}
.sfmast-disc { flex: none; line-height: 0; }
.sfmast-av {
  width: var(--sfmast-disc); height: var(--sfmast-disc);
  border-radius: var(--b-radius-md); /* clubs are squared, as in the people rows */
  object-fit: cover; display: block;
  /* The ring is the page behind it, which is what lifts the square off the
     banner without a shadow the dressed band would have to undo. */
  /* `--surface-page`, and the name is the fix (TEN-1700's sweep): this ring
     said `--surface-base`, which no cover in this book defines, so it drew
     nothing at all and the square sat straight on the banner. */
  border: 4px solid var(--surface-page); background: var(--surface-raised);
}
.sfmast-av-initial {
  display: flex; align-items: center; justify-content: center;
  font-size: var(--b-text-2xl); font-weight: var(--b-weight-semibold);
  color: var(--text-muted); line-height: 1;
}

.sfmast-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 16rem; }
.sfmast-nameline { display: flex; align-items: baseline; gap: var(--b-space-2); flex-wrap: wrap; min-width: 0; }
.sfmast-name { margin: 0; font-size: var(--b-text-2xl); line-height: var(--b-leading-tight); font-weight: var(--b-weight-bold); }
.sfmast-handle { color: var(--text-muted); font-size: var(--b-text-sm); }
.sfmast-tagline { margin: 0; font-size: var(--b-text-md); color: var(--text-primary); }
.sfmast-where {
  margin: 0; display: flex; align-items: center; gap: var(--b-space-1);
  font-size: var(--b-text-sm); color: var(--text-muted);
}
.sfmast-pin { fill: currentColor; flex: none; }
.sfmast-bio { margin: 0; padding: 0 var(--b-space-1); max-width: 62ch; line-height: var(--b-leading-relaxed); }

.sfmast-acts { display: flex; gap: var(--b-space-2); flex-wrap: wrap; padding-bottom: var(--b-space-1); }
.sfmast-acts form { margin: 0; }

/* THE COMPACT LOCKUP — every tab page. A slim bar, and the whole of it is the
   way back to the overview. */
.sfmast-lockup { --sfmast-disc: 40px; }
.sfmast-lockup-link {
  display: flex; align-items: center; gap: var(--b-space-2);
  text-decoration: none; color: inherit; min-width: 0;
}
.sfmast-lockup-link:hover .sfmast-name { text-decoration: underline; }
.sfmast-lockup .sfmast-av { border-width: 0; }
.sfmast-lockup .sfmast-name { font-size: var(--b-text-lg); }
.sfmast-lockup .sfmast-nameline { flex-direction: row; }

.sfmast-blank { padding: var(--b-space-5) var(--b-space-2); }
.sfmast-empty { margin: 0; color: var(--text-muted); }

/* --- THE TAB BAR (K21 rung 2), lit on itself. Real anchors, because every
   one of them is a real address (rung 3) — there is no panel here to
   toggle, which is the whole difference between this and a tab strip. --- */

.sftabs {
  display: flex; gap: var(--b-space-1); flex-wrap: wrap;
  border-bottom: 1px solid var(--border-subtle);
}
.sftab {
  display: inline-flex; align-items: center; height: 2.4rem;
  padding: 0 var(--b-space-3); text-decoration: none;
  font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
  color: var(--text-muted);
  /* The lit state is a border on the SAME element in both states, so the row
     does not shift by two pixels as you move along it. */
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.sftab:hover { color: var(--text-primary); }
/* The state's own hover arm (TEN-1702 sweep; see `.ktlist-chip-on`). */
.sftab-on, .sftab-on:hover { color: var(--text-primary); border-bottom-color: var(--text-primary); }

/* --- THE BODY: doorway sections on the overview, one list on a tab. ---- */

.sfbody { display: flex; flex-direction: column; gap: var(--b-space-5); }
/* THE INCOMING OUTCOME (TEN-1699). It wears the app's own `.flash` family —
   the same treatment every other surface in this book gives the same value —
   and gives up only its trailing margin, because `.sfbody` is a flex column
   that already owns the rhythm and the two would stack. `.ktwiz-flash`'s rule
   one family over, and for the same reason. */
.sfflash { margin-bottom: 0; }
.sfsec { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfsec-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--b-space-2); }
.sfsec-h { margin: 0; font-size: var(--b-text-md); font-weight: var(--b-weight-bold); }
.sfsec-all { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); white-space: nowrap; }

.sfrows { display: flex; flex-direction: column; gap: var(--b-space-2); }
/* The Programs taste sits three-across on a wide column and stacks on a
   narrow one. `auto-fit` rather than a fixed count so the doorway's cap and
   the layout stay independent of each other. */
.sfrows-cards {
  display: grid; gap: var(--b-space-2);
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}

.sfcard {
  display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  padding: var(--b-space-3); border: 1px solid var(--border-subtle);
  border-radius: var(--b-radius-md); background: var(--surface-raised);
}
.sfcard-name { font-size: var(--b-text-md); }
.sfcard-line { font-size: var(--b-text-sm); color: var(--text-muted); }

/* K29'S GREEN LEFT THIS FAMILY WITH ITS LAST CALLER (TEN-1774). `.sfpill-have`
   was a SECOND green vocabulary beside the kit's way-in pill — the same colour
   with no border and no tick — and Mark read a booked row wearing it as plain
   text. One meaning gets one treatment, so the booked markers and the doorway's
   Subscribed pill compose `.ktpill.ktpill-have` now and these rules are gone
   rather than restyled to match: two things to keep in step is how they came
   apart in the first place. */

.sfrow {
  display: flex; align-items: center; gap: var(--b-space-3);
  padding: var(--b-space-2) var(--b-space-3); text-decoration: none; color: inherit;
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
/* The row is no longer one press (TEN-1700), so the lift is scoped to a row
   that still holds something to press — a whole-row hover on a row with nothing
   clickable in it is an affordance the row cannot honour. */
.sfrow:has(a):hover, .sfrow:has(button):hover { border-color: var(--border-strong); }
.sfrow-when { flex: none; min-width: 9.5rem; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.sfrow-what { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* THE WHOLE CARD IS ONE PRESS AGAIN (TEN-1898, Mark's UAT-3 batch G), and the
   title is text. TEN-1700 had moved the link onto the NAME because the card also
   carries a Book act and an act cannot live inside an anchor; that left a card
   whose only press was its underlined title. The card's when-and-what is now ONE
   anchor (`sfrow-press`, the search results' row-is-the-anchor precedent) and
   `sfrow-linked` stretches it over the card — `.ktrow-linked`'s construction one
   family over, three rules and the same reasons: the hover owns the whole card's
   ground and edge, the facts stay UNDER the stretched surface (pressing a price
   opens the night), and only the Book act is lifted above it, because only it
   names a different address. A card with no address gets none of this. */
.sfrow-press {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: var(--b-space-3);
  color: inherit; text-decoration: none;
}
.sfrow-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.sfrow-linked:hover { background: var(--hover); border-color: var(--border-strong); }
.sfrow-press::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.sfrow-linked .sfrow-act { position: relative; z-index: 1; }
/* The focus ring is the CARD's, because the card is what the press opens — a
   ring around the when-and-what alone would outline a region nobody aims at. */
.sfrow-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.sfrow-press:focus-visible { outline: none; }
/* K22's session-row entry, and the two arms wear ONE button (storefrontSession-
   ActInto draws a form for the covered reader and an anchor for everybody
   else). The form is a wrapper, not a control: it exists because a booking is a
   POST, and it must not add a box to the row. */
.sfrow-actform { display: contents; }
.sfrow-act { flex: none; }

/* THE NEXT-UP ROW'S FACTS (TEN-1687) — where, what it costs, what is left.
   Quiet by construction: they are facts a reader scans past on the way to the
   thing they came for, and drawing them at the title's weight would make the
   doorway compete with the tab it is a doorway to. */
.sfrow-where { font-size: var(--b-text-sm); color: var(--text-muted); }
.sfrow-facts { flex: none; display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; }
.sfrow-cost { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.sfrow-spots { font-size: var(--b-text-sm); color: var(--text-muted); }

/* --- THE PEOPLE TAB (TEN-1692, board S5) ------------------------------- */
/* The person-card's public rung: a face, a name with the club's own role line
   beside it, and the club's own paragraph under both. The bio is CAPPED IN
   MEASURE rather than in characters — the board's 780px is a reading measure,
   and ch units keep it one on a narrow column too. */
.sfperson {
  display: flex; gap: var(--b-space-3); align-items: flex-start;
  padding: var(--b-space-3); border: 1px solid var(--border-subtle);
  border-radius: var(--b-radius-md); background: var(--surface-raised);
}
.sfperson-av {
  flex: none; width: 56px; height: 56px; border-radius: var(--b-radius-full);
  object-fit: cover; background: var(--surface-sunken);
}
.sfperson-av-initial {
  display: flex; align-items: center; justify-content: center;
  font-weight: var(--b-weight-bold); color: var(--text-muted);
}
.sfperson-who { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.sfperson-line { margin: 0; display: flex; align-items: baseline; gap: var(--b-space-2); flex-wrap: wrap; }
.sfperson-name { font-size: var(--b-text-md); }
.sfperson-sub { font-size: var(--b-text-sm); color: var(--text-muted); }
.sfperson-bio { margin: 0; font-size: var(--b-text-sm); line-height: 1.55; max-width: 72ch; }
/* THE CARD IS THE PRESS (TEN-1898): a person card with an address IS one anchor
   to that person's page (the search results' row-is-the-anchor precedent), so it
   wears the linked row's hover — the ground and the edge, never an underline —
   and the card's own focus ring. Nothing inside it is pressable, so nothing is
   lifted. A card with no address is the plain card, untouched. */
.sfperson-linked { color: inherit; text-decoration: none; transition: background-color 0.12s ease, border-color 0.12s ease; }
.sfperson-linked:hover { background: var(--hover); border-color: var(--border-strong); }
.sfperson-linked:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--role-primary); }

/* The overview's doorway strip: the same card, smaller, with no paragraph —
   a taste and a link, never a second copy of the tab. */
.sfrows-people { display: grid; gap: var(--b-space-2); grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
.sfperson-door { align-items: center; padding: var(--b-space-2) var(--b-space-3); }
.sfperson-door .sfperson-av { width: 40px; height: 40px; }

.sfquiet { margin: 0; color: var(--text-muted); max-width: 52ch; }

/* ── THE EVENTS TAB (TEN-1691, board S3) ──────────────────────────────────
   Time-derived bands over the date-divider and time-row grammar the place page
   and the class hub already speak. */
.sfev { display: flex; flex-direction: column; gap: var(--b-space-4); }
.sfev-band {
  margin: 0; font-size: var(--b-text-md); font-weight: var(--b-weight-bold);
}

/* K29 — R1's ONE LIVE TREATMENT, and it is QUIET. A green ground and a
   dot-pill, at the same weight as every other row: a club with a class on right
   now is a fact, not an alarm. Nothing else on this page wears a ground, which
   is the half of the ruling a stylesheet can actually hold — a second `.sfev-*`
   rule with a background would break it silently. */
.sfev-live { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfev-row-live {
  background: var(--green-a);
  border-color: var(--green-line);
}
.sfev-dot {
  flex: none; display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  color: var(--role-presence); white-space: nowrap;
}
.sfev-dot::before {
  content: ""; width: 7px; height: 7px; border-radius: var(--b-radius-full);
  background: currentColor;
}

.sfev-list { display: flex; flex-direction: column; gap: var(--b-space-4); }
.sfev-day { display: flex; flex-direction: column; gap: var(--b-space-2); }

/* ── [hidden] MUST ACTUALLY HIDE, AND THIS IS THE WHOLE OF THE DEAD CAPSULE ──
   TEN-1862, Mark's M1 UAT-2: "the org capsules have NO effect."

   They had every effect except the visible one. ktlist.js filters on the EXPLICIT
   KEY — `matches()` compares the address's `org=<id>` against the row's own
   `data-ktlist-facets` pair `org:<id>`, a string equality on a server-stamped
   value, and the fuzzy pass runs only over a search box this surface does not
   mount. So the predicate was right, the address moved, the pressed capsule lit,
   the count updated, and `el.hidden = true` was set on every row that did not
   match. Nothing moved on screen.

   `[hidden]` hides by way of `display: none` in the USER-AGENT stylesheet, so any
   author rule that sets `display` on the same element outranks it — and both row
   families do: `.sfev-row` is `display: flex` (one screen down) and `.ktcal-chip`
   is `display: flex` (the month rung). A row wearing `hidden` therefore stayed
   laid out, and it stayed laid out on the SCRIPTLESS arm too, so an address like
   `?org=viking` had never narrowed anything either.

   THIS IS THE FOURTH TIME (`.msg-row`, `.net-row`, `.people-more`, and now these
   two), which is why the fix is written as the house idiom rather than as a
   one-off: name the class with the attribute so the selector out-specifies the
   class rule that broke it. The lesson for the next family: A ROW FAMILY THAT
   DECLARES `display` OWES A `[hidden]` RULE IN THE SAME BLOCK — and the way to
   catch it is a browser measuring `getClientRects().length`, never a Go test or a
   golden reading the `hidden` property back, which is exactly what the gate cases
   beside this were doing while the page was wrong. */
.sfev-row[hidden], .ktcal-chip[hidden], .ktcal-block[hidden] { display: none; }

/* THE DAY ANCHOR CLEARS THE STICKY TITLE ROW (TEN-1863). `#sfev-day-<key>` is
   the address a week-strip day tap composes (athleteDayHref,
   `athlete-schedule` board A2) and the one a month-view overflow cell falls
   back to (`ktcal-more`) — same anchor, same list, so one rule answers both.
   Scoped to `.athsched` and not to `.sfev-day` everywhere: this class is also
   the storefront's and the console's day list, neither of which wears the
   athlete ladder's sticky `.athd` row, and giving them the same scroll-margin
   would be solving a collision that page does not have. */
.athsched .sfev-day { scroll-margin-top: var(--athd-h); }

/* A DAY WHOSE EVERY ROW IS FILTERED OUT HIDES WITH THEM. The server stamps
   `hidden` on the section for the scriptless arm, which is exact and needs no
   browser; this rule is for the ENHANCED arm, where ktlist.js adds and removes
   `hidden` on rows with no round trip and never touches their container. A
   browser without `:has()` gets a lingering heading — cosmetic, never a wrong
   fact — which is why the server's stamp is the one that carries the meaning. */
.sfev-day:not(:has(.sfev-row:not([hidden]))) { display: none; }

/* AND THE SAME RULE ONE ALTITUDE UP (K30 arm 1, TEN-1700). A filter that
   matched NOTHING leaves the honesty sentence as the whole body: no band head
   over no rows, no live band with nothing under way in it, and no horizon arm
   offering to widen a window for rows that do not exist. The server stamps
   these for the scriptless arm exactly as it stamps a day; this is the enhanced
   arm, where a chip press empties the list with no round trip.

   EVERY VIEW'S ELEMENTS ARE COUNTED — a calendar chip and a day BLOCK are each a
   row as far as the address is concerned (storefrontEventsRowSelector's own rule),
   so no arrangement can leave a band head standing over an empty body. The three
   classes here and the three in that selector are the same list, and a class
   present in one and missing from the other is a head over nothing on exactly one
   of the three views (TEN-1794 added the third).

   `.sfev-cal` LEFT THIS LIST (TEN-1862, Mark's M1 UAT-2). It was here on the
   symmetry — a calendar is a view like the other two, so an empty one hides like
   the other two — and the symmetry was false. The other entries are HEADINGS AND
   ROW CONTAINERS: take their rows away and there is nothing left for them to be
   about. A month grid is a FRAME, which is a thing in its own right: an empty one
   still answers "which month, which dates, where is today, nothing on" and still
   holds the only control that reaches another month. Hiding it replaced an answer
   with a bare sentence and took the way out with it. The plate's own `zero` stamp
   came off the same element for the same reason, so both arms agree; see
   storefront-events.plate.html's calendar block. */
.sfev:not(:has(.sfev-row:not([hidden]))):not(:has(.ktcal-chip:not([hidden]))):not(:has(.ktcal-block:not([hidden]))) .sfev-band,
.sfev:not(:has(.sfev-row:not([hidden]))):not(:has(.ktcal-chip:not([hidden]))):not(:has(.ktcal-block:not([hidden]))) .sfev-live,
.sfev:not(:has(.sfev-row:not([hidden]))):not(:has(.ktcal-chip:not([hidden]))):not(:has(.ktcal-block:not([hidden]))) .sfev-day-view,
.sfev:not(:has(.sfev-row:not([hidden]))):not(:has(.ktcal-chip:not([hidden]))):not(:has(.ktcal-block:not([hidden]))) .sfev-more {
  display: none;
}

/* THE DATE IS SAID ONCE, ON THE DIVIDER (TEN-1610, generalised), so the row
   carries the clock alone and the one fact that varies down a day leads it. */
.sfev-date {
  margin: 0; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted);
}
.sfev-row {
  display: flex; align-items: center; gap: var(--b-space-3);
  padding: var(--b-space-2) var(--b-space-3);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.sfev-time {
  flex: none; min-width: 5.5rem; font-size: var(--b-text-sm);
  font-weight: var(--b-weight-bold);
}
.sfev-what { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* THE TITLE MAY LEAD WITH THE EVENT-TYPE GLYPH (TEN-1812, Sched06). `inline-flex`
   on the name so the drawing sits on the text's own baseline row and the title still
   wraps as text — a floated or absolutely placed glyph would leave a hanging indent
   on the second line of every long class name. The glyph is `currentColor` by the
   kit's stroke law, so it inherits the title's weight of attention rather than
   arriving at its own. */
.sfev-name { font-weight: var(--b-weight-semibold); display: inline-flex; align-items: center; gap: var(--b-space-2); }
.sfev-glyph { flex: none; display: inline-flex; color: var(--text-muted); }
.sfev-sub { font-size: var(--b-text-sm); color: var(--text-muted); }
.sfev-end { flex: none; display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; }
.sfev-fact { font-size: var(--b-text-sm); color: var(--text-muted); }
.sfev-act { flex: none; }
/* THE COVERED READER'S BOOK IS A POST (K22/K24, TEN-1700) and its form is a
   wrapper rather than a control: `display: contents` leaves the button itself
   in the row's own flex line, so the covered arm and the link arm are the same
   object in the same place with the same treatment. */
.sfev-act-form { display: contents; }

/* A CANCELLED NIGHT STAYS IN THE LIST WITH THE FACT ON IT — dropping a session
   somebody holds a spot on is the one thing silence must not do here. It is
   dimmed rather than struck through: the row is still information. */
.sfev-row-off { opacity: 0.72; }
.sfev-off { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); color: var(--role-danger); }

/* COVERED — THE RAIL IS RETIRED, AND THE ROW HAS NO COVERAGE RULE (TEN-1854,
   Mark's M1 UAT). `.sfev-row-covered` was a 3px green border-left plus a
   compensating padding, and it is gone from this stylesheet, from the plate and
   from the composer together — the whole channel rather than a commented-out rule
   somebody restores by accident.

   WHY IT LOST, kept here because a retired treatment is a decision and the next
   person to reach for a hairline should find the argument. TEN-1794's reasoning was
   a wattage ladder: K29 gives green one meaning (a fact about YOU), `.sfev-row-live`
   already owns the single green GROUND this page is allowed, booked owns the PILL,
   so covered took the quietest channel left. The ladder was right and the channel
   was wrong. A hairline down a card's left edge is texture, not a signal: it says
   "something about this row" without saying what, and the only place the what was
   written was four words into a grey sub-line. Mark's verdict was that a reader
   never reads prose to learn coverage — so the row offered a hint and a footnote,
   and no answer.

   WHERE IT WENT: K29's way-in pill (`.ktpill.ktpill-have`), the bordered ticked one
   booked already wears, composed by ktHavePillHTML. One meaning, one treatment —
   the same argument TEN-1774 used to retire `.sfpill-have`, which was likewise a
   second green vocabulary at half the presence. Booked still outranks covered, so
   no row wears two.

   THE CALENDAR CELL KEEPS ITS TINT (`.ktcal-chip.ktcal-covered` and friends, below
   and further down this file). K32 owns what a chip inside a 40px cell may wear and
   a bordered pill is not on that list at that size, which is the reason cells have
   a state ladder of their own. This retirement is the ROW's. */

/* THE CALENDAR — A7's week grid, and a rendering the server owns. Whole weeks,
   so a quiet day is an empty cell: a grid that skipped its quiet days would be
   a list with borders.

   THE GRID IS ALL THIS SURFACE OWNS NOW (K32, TEN-1750). The cells and the
   events inside them are the kit's `ktcal` family, which is the whole point of
   minting one: a calendar COMPOSES rungs and never restyles them, so there is
   no `.sfev-cal-item` rule here for a second calendar to diverge from. */
.sfev-cal { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfev-cal-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--b-space-1); }

/* ONE COMPLETE MONTH, NAMED AND HEADED (TEN-1794). The grid is `classMonth`'s and
   the read is the same month, so every empty cell means "nothing on" rather than
   "nobody asked".

   THE COLUMN HEADS SHARE THE WEEK'S GRID so a head sits over its own column at
   every width — the same `repeat(7, …)`, declared once by reusing the week rule's
   shape rather than by hand-tuning a second row of boxes. */
.sfev-cal-month {
  margin: 0; font-size: var(--b-text-sm); font-weight: var(--b-weight-bold);
  color: var(--text-primary);
}
.sfev-cal-heads {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--b-space-1); padding: 0 var(--b-space-2);
}
.sfev-cal-head {
  font-family: var(--b-font-mono); font-size: var(--b-text-xs);
  letter-spacing: 0.06em; color: var(--text-faint);
}

/* ── THE DAY VIEW (TEN-1794) — rung 3's first production mount ───────────
   A list of ktcal BLOCKS in clock order, at their real durations. Not an hour
   grid: `event.upcoming` answers a whole org's day, typically six sessions over
   fourteen hours, and an hour grid of that is four fifths empty space with the
   evening below the fold. The BLOCK still earns its seat — the venue line and
   duration-as-height are the two facts a day has room for that a month cell does
   not — so the rung is right and the scaffolding around it is not.

   THE BLOCKS GET A READING MEASURE rather than the grid's seventh, which is the
   whole reason this arrangement exists: at this width the block's name does not
   ellipsize and its venue line does not either, so a reader on a day has the
   facts a month cell had to cut. This surface adds NO `.ktcal-*` rule — a
   calendar composes rungs and never restyles them. */
.sfev-day-view { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfev-day-blocks {
  display: flex; flex-direction: column; gap: var(--b-space-1);
  max-width: 44rem;
}
/* A QUIET DAY'S SENTENCE (K30 arm 2). Muted and unadorned: it is a complete
   answer to the question the reader asked, not a problem they have to solve, so it
   gets no box, no icon and no call to action. The mode selector above it is the way
   on, and it is already there. */
.sfev-quiet { margin: 0; color: var(--text-muted); font-size: var(--b-text-sm); }

/* THE HORIZON ARM — a link, because it widens the read's own window. */
.sfev-more { align-self: flex-start; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }

/* THE COLLAPSED PAST TAIL. Absent today — no read on the wire answers past
   events — and styled here so the seam lands dressed when one does. */
.sfev-past {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--b-space-2); padding: var(--b-space-2) var(--b-space-3);
  border-top: 1px solid var(--border-subtle); color: var(--text-muted);
  font-size: var(--b-text-sm);
}
.sfev-past-go { flex: none; font-weight: var(--b-weight-semibold); }

.sfindex { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfindex-h { margin: 0; font-size: var(--b-text-2xl); }
.sfindex-p { margin: 0; max-width: 62ch; color: var(--text-muted); }

@media (max-width: 560px) {
  .sfmast-full, .orgc-facemount { --sfmast-disc: 72px; }
  .sfmast-name { font-size: var(--b-text-xl); }
  /* The acts drop below the identity rather than squeezing beside it. */
  .sfmast-acts { width: 100%; padding-bottom: 0; }
  .sfrow { flex-wrap: wrap; }
  .sfrow-press { flex-wrap: wrap; }
  .sfrow-when { min-width: 0; width: 100%; }
  /* The session row stacks: the clock leads its own line, the act drops below
     the facts rather than squeezing beside them. */
  .sfev-row { flex-wrap: wrap; }
  .sfev-time { min-width: 0; }
  .sfev-end { width: 100%; }
  /* A seven-column month grid at phone width is seven unreadable columns. The
     calendar becomes a day-per-row list, which is the same information in the
     arrangement that fits — and the LIST view is one tap away either way.

     THE RUNG DOES NOT CHANGE HERE, and that is K32's size clause doing its job
     rather than being contradicted: one column at phone width is a WIDE cell,
     so the chip is still the right rung. The dot rung is for cells that are
     narrow, which is a different thing from a screen that is. */
  .sfev-cal-week { grid-template-columns: minmax(0, 1fr); }
  .sfev-cal-week .ktcal-day { min-height: 0; }
  /* AND A DAY WITH NOTHING VISIBLE ON IT COLLAPSES — but ONLY here, and the
     distinction is the whole reason this rule is inside a media query on a page
     whose calendar is otherwise container-driven.

     AN EMPTY CELL IN A SEVEN-COLUMN GRID IS INFORMATION: it is a POSITION, and
     seeing that Tuesday and Thursday are quiet while Wednesday is busy is the one
     thing a month grid does that a list does not. An empty ROW in a single column
     is not information — the column has no positions, each row carries its own
     date, and the quiet days are pure scrolling.

     IT BECAME WORTH FIXING WITH TEN-1794 rather than before it: the grid used to
     be the two weeks the list was reading (about fourteen cells, most of them
     holding something), and a single COMPLETE month is thirty-seven with the
     spilled weeks — of which, on a normal club, most are empty. That is this
     ticket's own doing at phone width, so it is this ticket's to answer.

     VISIBLE rather than PRESENT, so a filter collapses the quiet days with it —
     the same shape `.sfev-day` already has one arrangement over. Without `:has()`
     every cell draws, which is the behaviour that shipped before: longer, never
     wrong.

     A WHOLE QUIET MONTH IS THE EXCEPTION, AND IT IS THE RULE'S OWN LOGIC (TEN-1862).
     "A quiet day is pure scrolling" is true of a quiet day among busy ones; it is
     not true of the last cell on the page. Collapse every cell in an empty month
     and the reader gets a blank where the calendar was — which is the same defect
     the plate's `zero` stamp had, arriving at phone width by a different door, and
     it would have made item 3's fix desktop-only. So the collapse is now gated on
     the CALENDAR holding something: `.sfev-cal:has(…)` scopes it, one altitude up
     from the week, because the question "is there anything on this month" is the
     grid's and not the row's. A month with one session still collapses its other
     thirty-six cells exactly as before. */
  .sfev-cal:has(.ktcal-chip:not([hidden])) .sfev-cal-week
    .ktcal-day:not(:has(.ktcal-chip:not([hidden]))) { display: none; }
  /* AND THE COLUMN HEADS GO WITH THE COLUMNS. Seven weekday heads over a
     one-column list are seven labels over one column, which is worse than no
     labels: every cell already names its own weekday ("Mon 21"), which is why the
     heads are aria-hidden decoration in the first place and why dropping them
     here costs a reader nothing. */
  .sfev-cal-heads { display: none; }
}

/* ── THE ktcal FAMILY (K32, TEN-1750) ────────────────────────────────────
   An event INSIDE a calendar, at three sizes, with ONE state ladder.

   THE SIZE RUNG IS PICKED BY THE CELL AND NEVER BY THE VIEWPORT, which is why
   there is no media query in this block: a day cell's width is a property of
   the grid it sits in, and a narrow sidebar on a desktop wants the same rung a
   phone's month grid does, for the same reason. The composer measures the cell
   and asks for the rung; this file draws whichever it was asked for.

   THE LADDER, IN FORCE ORDER, AND IT IS THE SAME ONE AT EVERY RUNG:

     base        the surface, the border, the ink
     following   accent TINT + a hollow ring. Never green and never a rail —
                 a class you watch is not a spot you hold
     covered     green tint at a FRACTION of booked's, the same ✓ lighter, and
                 NO RAIL. The same green fact one wattage down (TEN-1794)
     booked      green RAIL + green tint + the ✓. The high wattage, and it
                 PERSISTS WITHOUT HOVER, because K29 is that green is a fact
                 about YOU and a fact about you does not wait for a pointer
     past        45% on any of the four
     full        base wattage plus an honest tail. Not a warning, not an error
     cancelled   struck, and it STAYS

   BOOKED BEATS COVERED BEATS FOLLOWING and the Go composer resolves those ties
   before a class is ever written (ktCalClasses), so this file never has to rely
   on source order to settle them — which is the one thing a stylesheet cannot be
   trusted with across a refactor.

   COVERED IS NOT A SECOND GREEN MEANING, which is the one thing to get right
   here. K29's green is "covered / confirmed — a fact about YOU", stated as a pair:
   browsing and owning share the colour "because it is the same fact from two
   sides". So the two states below are ONE meaning at two intensities, and the
   difference between them is wattage — tint strength, and whether the rail is
   there — not hue. Drawing covered in the accent would have made it
   indistinguishable from a followed class; drawing it at booked's wattage would
   have told a reader they hold a spot they have not taken.

   HOVER OWNS THE SURFACE (K15's channel law): full accent ground, ink-on-
   primary, on every rung. That is what tells three events in one day apart
   under the pointer, and it is why hover is NOT a border tweak here.

   AND THE STATE MARKS SURVIVE IT — the TEN-1702 rule, obeyed by construction
   rather than by a `-on:hover` override: the ✓ and the ring are ELEMENTS, not
   backgrounds, so a hover that repaints the ground cannot erase them. Their
   ink is re-declared under hover so they stay legible on the accent.

   GREEN APPEARS IN THIS BLOCK ONLY AS THE VIEWER'S OWN STANDING — the booked
   rail, the booked tint, the covered tint, and the ✓ they share. Nowhere else. A
   `.ktcal-*` rule with a green anything that is not one of those four would spend
   a meaning another surface is relying on. */

/* THE DAY CELL. It is the grid's unit and it does not grow: the overflow line
   is what a busy day gets, because a month grid whose rows grow to their
   busiest day stops lining up — and lining up is the only thing a month grid
   does that a list does not. */
/* THE CELL IS A SIZE CONTAINER, and that is K32's central claim expressed in the
   one place it can actually be measured.

   "The rung is picked by the CELL, never by the viewport" is a rule a stylesheet
   can only keep with a container query: a media query knows the window and knows
   nothing about the seventh of a reading column this cell turns out to be. The
   same grid is 150px wide on a console page and 95px inside a storefront's
   reading measure, at the identical viewport — so the composer picks the RUNG
   (chip or dots, a decision about markup and about what the press target is) and
   the container query trims what a narrow cell cannot hold. */
/* THE CELL'S PADDING IS HALVED (TEN-1862, Mark's M1 UAT-2: "50% less padding
   inside each day cell", for card real estate).

   IT IS A TOKEN STEP AND NOT A NEW NUMBER: --b-space-2 is 0.5rem and --b-space-1
   is 0.25rem, so the scale already held the exact answer and the ask lands on a
   rung rather than on a literal. A day cell is the densest container in this book
   — three chips, a date and an overflow line inside 7rem — and it was spending
   16px of its width on air at a rung meant for a card you read rather than scan.
   `min-height` does NOT move with it: a month grid's only job that a list cannot
   do is line up, so the cells' height stays declared and the room bought here goes
   to the chips' names. */
.ktcal-day {
  display: flex; flex-direction: column; gap: 3px; min-height: 7rem;
  padding: var(--b-space-1); border: 1px solid var(--border-subtle);
  border-radius: var(--b-radius-md); background: var(--surface-raised);
  container-type: inline-size;
}
.ktcal-date {
  font-family: var(--b-font-mono); font-size: var(--b-text-xs);
  color: var(--text-muted);
}
/* TODAY IS THE ACCENT, AND IT NOW CARRIES REAL WATTAGE (TEN-1794).
   It used to be the date's ink and weight alone, which is a fact stated in the
   smallest type in the cell — on a month grid where a reader's first question is
   "where am I", the anchor was the quietest thing on the page.

   MORE WATTAGE, NOT A NEW MEANING. Mark's ask was explicit about that, and K29 is
   why it has to be: a dark ground means LIVE NOW and at most one per page state,
   and green means a fact about YOU. Today is neither — it is the reader's
   POSITION. So it takes two channels that carry no meaning of their own in this
   family (a surface tint and a stroke) in the colour the cell already used for it.

   THE STROKE IS AN INSET RING RATHER THAN A THICKER BORDER, because a border that
   grew would move the cell's content by a pixel and a grid of cells where one is
   a pixel taller is a grid that does not line up — which is the only thing a month
   grid does that a list does not. The tint is derived from the accent's own soft
   token, so the three themes need no second declaration.

   AND IT IS QUIETER THAN ANY STATE ON AN EVENT INSIDE IT. A cell that shouted
   would compete with the booked ✓ it contains, and the day is the container. */
.ktcal-today {
  background: color-mix(in srgb, var(--role-primary-soft) 55%, var(--surface-raised));
  border-color: var(--role-primary-ink);
  box-shadow: inset 0 0 0 1px var(--role-primary-ink);
}
.ktcal-today .ktcal-date { color: var(--role-primary-ink); font-weight: var(--b-weight-bold); }

/* THE DATE AS THE DAY'S PRESS (TEN-1794). The cell cannot be an anchor at the chip
   rung — the chips are, and a link wrapping links is not a thing — so the HEAD is
   the day's own view. It is drawn as the date it always was: a press target that
   announced itself with a colour would put a fifth thing in a cell that already
   carries a date, three chips and an overflow line. Hover and focus are where it
   says so, which is K15's surface channel doing exactly its job. */
.ktcal-date-go { text-decoration: none; color: var(--text-muted); border-radius: var(--b-radius-sm); }
.ktcal-date-go:hover, .ktcal-date-go:focus-visible {
  color: var(--role-primary-ink); text-decoration: underline;
}
.ktcal-today .ktcal-date-go:hover, .ktcal-today .ktcal-date-go:focus-visible {
  text-decoration: underline;
}

/* THE OVERFLOW LINE. A real control in DOM order, so it is reachable by tab in
   the sequence the day runs. */
.ktcal-more {
  align-self: flex-start; padding: 1px var(--b-space-2);
  font-size: var(--b-text-xs); color: var(--text-muted);
  text-decoration: none; border-radius: var(--b-radius-sm);
}
.ktcal-more:hover, .ktcal-more:focus-visible { color: var(--text-primary); }

/* ── RUNG 2 — THE MONTH CHIP ──────────────────────────────────────────── */
/* THE ENTRY TEXT IS THE BOOK'S TINIEST RUNG (TEN-1862, Mark's M1 UAT-2: "tiny
   text for individual entries"). --b-text-2xs is declared once in tokens.css and
   the chip is the reason it exists; see that declaration for why the number does
   not live here. The chip's own two spans move with it rather than only the box,
   because the name is the thing being bought room for. */
.ktcal-chip {
  display: flex; align-items: center; gap: var(--b-space-1);
  height: 22px; padding: 0 5px; overflow: hidden; white-space: nowrap;
  font-size: var(--b-text-2xs); line-height: 1; text-decoration: none;
  color: var(--text-primary); background: var(--surface-overlay);
  border: 1px solid transparent; border-radius: var(--b-radius-sm);
}
/* THE TIME MAY NOT BE CUT and the name may: `flex: none` on the clock and a
   min-width of zero on the name is the whole of that rule, and it is here
   rather than in a comment on a plate because it is a layout fact.
   THE `10.5px` LITERAL IS GONE (TEN-1862): it was the only font size in this
   family that was not a token, and it was the number a second dense surface would
   have copied. It is --b-text-2xs now, which is the rung it was approximating. */
.ktcal-chip .ktcal-t {
  flex: none; font-family: var(--b-font-mono); font-size: var(--b-text-2xs);
  color: var(--text-muted);
}
.ktcal-chip .ktcal-n {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ktcal-tail { color: var(--text-muted); }

/* THE MIDDLE CASE — a cell too narrow for the chip's full anatomy and too wide
   for the dot rung (roughly 64–110px, which is what a seven-column month grid
   inside a reading measure actually gives).

   THE CLOCK GOES AND THE NAME STAYS, which is the opposite of what the chip's
   own layout rule says and is right for exactly this reason: at full width the
   time may not be cut because it is the fact that varies down a day, but a
   two-character stub of a class name ("Su…") identifies nothing at all, and an
   event you cannot name is not an event you can decide about. The time is still
   in the accessible name and in the dialog one press away; the name has nowhere
   else to be.

   It is a CONTAINER query and not a media query because the subject is the cell
   (see .ktcal-day) — the same grid at the same viewport is wide on a console
   page and narrow in a storefront's measure. */
@container (max-width: 110px) {
  .ktcal-chip .ktcal-t { display: none; }
  .ktcal-chip { gap: 5px; }
}

/* ── RUNG 3 — THE WEEK/DAY BLOCK ──────────────────────────────────────── */
.ktcal-block {
  display: flex; flex-direction: column; gap: 1px;
  padding: 7px 9px; text-decoration: none; color: var(--text-primary);
  background: var(--surface-overlay);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--b-radius-sm);
  /* THE HEIGHT IS THE DURATION, and the floor is what a session with no
     declared end draws at — the wire could not say how long, so the block
     does not pretend to. 0.62px per minute puts an hour at ~37px, which is
     the shortest block that still holds two lines of text. */
  min-height: max(2.6rem, calc(var(--ktcal-min, 60) * 0.62px));
}
.ktcal-block .ktcal-bt { font-family: var(--b-font-mono); font-size: 10.5px; color: var(--text-muted); }
.ktcal-block .ktcal-bn {
  font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); line-height: 1.25;
}
.ktcal-block .ktcal-bv {
  font-size: var(--b-text-xs); color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── RUNG 1 — THE DOT ROW ─────────────────────────────────────────────── */
/* The cell IS the press target here, because a 6px circle is not a control
   anybody can hit. No hover surface at this rung: it is the touch-first rung
   by definition, and a hover treatment on a target that is a whole day would
   be painting the day rather than the event. */
.ktcal-day-dots {
  min-height: 0; gap: var(--b-space-1); justify-content: space-between;
  text-decoration: none; color: inherit;
}
.ktcal-day-dots:hover, .ktcal-day-dots:focus-visible { border-color: var(--border-strong); }
.ktcal-dots { display: flex; align-items: center; gap: 3px; }
.ktcal-dot {
  width: 6px; height: 6px; border-radius: var(--b-radius-full);
  background: var(--text-faint);
}
.ktcal-dmore { font-family: var(--b-font-mono); font-size: 10px; color: var(--text-muted); }

/* ── THE STATE LADDER, at all three rungs ─────────────────────────────── */
/* FOLLOWING — evident, and quieter. Tint plus a hollow ring, on the accent
   family. It gets no rail, because the rail is booked's. */
.ktcal-chip.ktcal-following { background: var(--role-primary-soft); }
.ktcal-block.ktcal-following { background: var(--role-primary-soft); border-left-color: var(--role-primary-ink); }
.ktcal-dot.ktcal-following { background: transparent; box-shadow: inset 0 0 0 1.5px var(--role-primary-ink); }
.ktcal-ring {
  flex: none; width: 6px; height: 6px; border-radius: var(--b-radius-full);
  box-shadow: inset 0 0 0 1.5px var(--role-primary-ink);
}

/* BOOKED — the high wattage, and it persists without hover (K29). */
.ktcal-chip.ktcal-booked {
  background: var(--green-a); border-left: 3px solid var(--role-presence); padding-left: 5px;
}
.ktcal-block.ktcal-booked { background: var(--green-a); border-left-color: var(--role-presence); }
/* Drawn slightly larger as well as green, because at 6px a fill is the only
   channel a ladder has — and the composer draws the booked marks FIRST. */
.ktcal-dot.ktcal-booked { width: 7px; height: 7px; background: var(--role-presence); }
.ktcal-ck { flex: none; color: var(--role-presence); font-weight: var(--b-weight-bold); }

/* COVERED — THE SAME GREEN FACT, ONE WATTAGE DOWN (TEN-1794).

   Three differences from booked and all three are wattage: the tint is mixed down
   to a little under half, there is NO RAIL (the rail is booked's, at every rung),
   and the ✓ they share is drawn at text weight rather than bold. Same hue, same
   token lineage, same glyph, same element — which is what keeps this one green
   meaning instead of two.

   THE TINT IS DERIVED FROM `--green-a` RATHER THAN ADDING A TOKEN, so the three
   theme blocks that redefine that token carry this state with them and there is no
   fourth green value for one of them to drift on.

   AND IT STILL HAS A MARK. A tint-only state is a state the pointer erases (hover
   repaints the ground, the TEN-1702 rule), so "quieter" had to mean a quieter
   tick and not no tick — which is also what Mark asked for.

   ON THE CHIP THAT MARK IS NOW A HAIRLINE RAIL RATHER THAN THE TICK (TEN-1862).
   Mark's M1 UAT-2 took the ✓ off the day-cell rung for space, and that collided
   head-on with the paragraph above: drop the tick and `covered` becomes tint-only
   on the one rung where hover repaints the whole ground. The two asks are both
   satisfied by moving the mark to a channel that costs NO horizontal space —
   the chip already carries `border: 1px solid transparent`, so colouring its left
   edge adds a mark with zero layout change and zero pixels taken from the name.

   IT IS THE SAME LADDER, NOT A NEW CHANNEL. K32 said "no rail — the rail is
   booked's at every rung", and what that clause was protecting is the DISTINCTION
   rather than the border: booked's rail is 3px and this is 1px, which is the
   "one green vocabulary, several wattages" rule the storefront row already speaks
   (ground / pill / rail). A reader cannot mistake a hairline for booked's rail,
   and the block rung — which has the room — keeps the ✓ unchanged. */
.ktcal-chip.ktcal-covered,
.ktcal-block.ktcal-covered {
  background: color-mix(in srgb, var(--green-a) 45%, transparent);
}
.ktcal-chip.ktcal-covered {
  border-left-color: color-mix(in srgb, var(--role-presence) 78%, transparent);
}
/* The dot rung: the fill at the same reduced strength, and NOT the 7px size —
   the extra pixel is booked's channel for a ladder drawn entirely in circles. */
.ktcal-dot.ktcal-covered { background: color-mix(in srgb, var(--role-presence) 55%, transparent); }
.ktcal-covered .ktcal-ck {
  font-weight: var(--b-weight-normal);
  color: color-mix(in srgb, var(--role-presence) 78%, transparent);
}

/* PAST rides any state. FULL keeps base wattage and says so in the tail — no
   act beside it, because there is no waitlist on the wire to queue into. */
.ktcal-past { opacity: 0.45; }
.ktcal-off .ktcal-n, .ktcal-off .ktcal-bn { text-decoration: line-through; }
.ktcal-off .ktcal-tail { color: var(--role-danger); text-decoration: none; }

/* ── HOVER — the K15 SURFACE channel, at every rung ───────────────────── */
/* KEYBOARD FOCUS IS DRAWN THE SAME AS HOVER. One treatment, one answer to
   "which of these is the pointer on" — K15's violation list item 6. */
.ktcal-chip:hover, .ktcal-chip:focus-visible,
.ktcal-block:hover, .ktcal-block:focus-visible {
  background: var(--role-primary); border-color: var(--role-primary);
  color: var(--ink-on-primary);
}
.ktcal-chip:hover .ktcal-t, .ktcal-chip:focus-visible .ktcal-t,
.ktcal-chip:hover .ktcal-tail, .ktcal-chip:focus-visible .ktcal-tail,
.ktcal-block:hover .ktcal-bt, .ktcal-block:focus-visible .ktcal-bt,
.ktcal-block:hover .ktcal-bv, .ktcal-block:focus-visible .ktcal-bv {
  color: var(--ink-on-primary); opacity: 0.78;
}
/* THE STATE RULES RE-DECLARED UNDER HOVER (the TEN-1702 pattern). The marks
   are elements and survive on their own; the RAIL is a border on the hovered
   element itself, so without these two lines the hover's `border-color` would
   repaint a booked chip as an unbooked one under the pointer — which is the
   exact defect that census exists to stop, one family along. */
.ktcal-chip.ktcal-booked:hover, .ktcal-chip.ktcal-booked:focus-visible,
.ktcal-block.ktcal-booked:hover, .ktcal-block.ktcal-booked:focus-visible {
  border-left-color: var(--role-presence);
}
.ktcal-block.ktcal-following:hover, .ktcal-block.ktcal-following:focus-visible {
  border-left-color: var(--role-primary-ink);
}
/* AND THE COVERED CHIP'S HAIRLINE, for the same reason and with the same wrinkle
   its ✓ had (TEN-1862). It is now that rung's ONLY non-surface channel, so a hover
   that repainted it would leave `covered` and `base` identical under the pointer —
   the TEN-1702 defect exactly. `--ink-on-primary` rather than the green, because
   the mark is what has to survive and not its pigment (K29 owns the meaning, not
   the hex), and a mixed-down green over the accent ground does not read. The
   WATTAGE still separates it from booked under the pointer: 1px against 3px. */
.ktcal-chip.ktcal-covered:hover, .ktcal-chip.ktcal-covered:focus-visible {
  border-left-color: var(--ink-on-primary);
}
/* The ✓ and the ring on the accent ground: ink-on-primary, because the green
   that reads on a dark surface does not read on a blue one — and the MARK is
   what has to survive, not its pigment (K29 owns the meaning, not the hex).

   THE COVERED ✓ IS RE-DECLARED HERE TOO (the same TEN-1702 rule): its rest colour
   is a mixed-down green, and a `color-mix` against `transparent` over the accent
   ground would leave a tick that is legible on the surface and not on the hover.
   The mark has to survive, and surviving means being readable. */
.ktcal-chip:hover .ktcal-ck, .ktcal-chip:focus-visible .ktcal-ck,
.ktcal-block:hover .ktcal-ck, .ktcal-block:focus-visible .ktcal-ck,
.ktcal-covered:hover .ktcal-ck, .ktcal-covered:focus-visible .ktcal-ck {
  color: var(--ink-on-primary);
}
.ktcal-chip:hover .ktcal-ring, .ktcal-chip:focus-visible .ktcal-ring,
.ktcal-block:hover .ktcal-ring, .ktcal-block:focus-visible .ktcal-ring {
  box-shadow: inset 0 0 0 1.5px var(--ink-on-primary);
}

/* ── THE EVENT CARD the press opens (over .ktsheet's shell) ───────────── */
.ktcalev { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktcalev-when { margin: 0; font-size: var(--b-text-base); }
.ktcalev-where { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.ktcalev-facts {
  margin: 0; display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--b-space-2); font-size: var(--b-text-sm);
}
.ktcalev-fact, .ktcalev-sub { color: var(--text-muted); }
.ktcalev-off { margin: 0; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); color: var(--role-danger); }
.ktcalev-acts {
  display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap;
  padding-top: var(--b-space-1);
}
/* The covered reader's Book is a POST, and its form is a wrapper rather than a
   control — `.sfev-act-form`'s rule, for its reason: the button stays in the
   act row's own flex line, so both arms are the same object in the same place. */
.ktcalev-form { display: contents; }
/* The way out to the whole event sits at the END of the row, away from the
   advancing act, because it is the one control here that commits nothing. */
.ktcalev-page { margin-left: auto; }

/* ── THE ACTIVITY CHIP (K54 — PROPOSED, TEN-1899; Mark's UAT-3 ruling H) ──
   What KIND of session this is, from the class's own first coverage tag: a tile
   carrying a K13 drawing (a standard family) or a monogram (any other tag), in
   one hue off the `--act-1…8` ring declared in tokens.css, plus — at the chip
   rung — the tag's word. One plate (`kit-activity`), mounted by every session
   surface; this block is the only place it is dressed.

   THE HUE IS SET ONCE, AS ONE CUSTOM PROPERTY, AND ONLY THE TILE READS IT. K29
   keeps its meanings in grounds, pills and inks, and K47 settled identity hues
   into MARKS; every rule below that touches `--ktact` is scoped to `.ktact-tile`,
   so a host's ground, rail, border and text are structurally out of reach.

   THE TILE IS OPAQUE — the hue at 14% mixed into the RAISED surface rather than
   into `transparent` — so it reads the same on a month chip's overlay ground, on
   booked's green tint and under the accent hover, which repaints the chip's
   ground and ink but has no rule that reaches a child with its own. That is the
   TEN-1702 rule for a new mark: it survives because it is an element. The 14% is
   the number the ledger's contrast table was measured at; change one, re-measure
   the other. */
.ktact { display: inline-flex; align-items: center; gap: 6px; flex: none; vertical-align: middle; }
.ktact-s1 { --ktact: var(--act-1); }
.ktact-s2 { --ktact: var(--act-2); }
.ktact-s3 { --ktact: var(--act-3); }
.ktact-s4 { --ktact: var(--act-4); }
.ktact-s5 { --ktact: var(--act-5); }
.ktact-s6 { --ktact: var(--act-6); }
.ktact-s7 { --ktact: var(--act-7); }
.ktact-s8 { --ktact: var(--act-8); }
.ktact-tile {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: var(--b-radius-sm);
  color: var(--ktact);
  background: color-mix(in srgb, var(--ktact) 14%, var(--surface-raised));
}
/* The drawing keeps `.ktic`'s stroke law untouched and only takes the tile's
   measure — K13: the law lives once, a size is a modifier. */
.ktact-ic { width: 14px; height: 14px; }
/* THE MONOGRAM IS TEXT, which is why the ring was measured against the TEXT
   floor (4.5:1) and not the 3:1 icon floor: the fallback's letter is the one
   mark here a reader actually reads. */
.ktact-mono {
  font-family: var(--b-font-heading); font-weight: var(--b-weight-bold);
  font-size: var(--b-text-xs); line-height: 1;
}
.ktact-w {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  color: var(--text-muted); white-space: nowrap;
}
/* THE MARK RUNG — the tile alone, a step smaller, for the ktcal anchors. 16px in
   a 22px chip leaves the chip's own 3px of air above and below; the drawing at
   12px keeps every feature at least a stroke wide (K13's minimum-feature clause,
   judged at the size it ships). */
.ktact-mark .ktact-tile { width: 16px; height: 16px; }
.ktact-mark .ktact-ic { width: 12px; height: 12px; }
.ktact-mark .ktact-mono { font-size: var(--b-text-2xs); }

/* THE MOUNTS. Each host keeps its own layout; these rules only seat the chip. */
.ktcal-block .ktcal-bt { display: flex; align-items: center; gap: var(--b-space-1); }
.ktcalev-act { margin: 0; }
/* On a schedule row the chip takes its own line at the head of the sub-line: run
   inline, its word read as the first clause of the venue sentence ("Group classes
   Apex Barbell · …"), which is two facts with no joiner between them. */
.sfev-sub .ktact { display: flex; width: max-content; margin: 1px 0 3px; }
.ktprev-act { margin: 2px 0; }
.ktrow-line.ktrow-act { display: flex; }
/* The first FACT line keeps its primary ink when the chip line sits above it —
   `.ktrow-line:first-child` is what dressed it, and the chip is now first. */
.ktrow-act + .ktrow-line { color: var(--text-primary); }

/* ── THE RESOLVER FLOW (TEN-1689; boards A5, A4, A2) ─────────────────────
   The WHEN sheet, the HOW sheet and the entitlement quickview, plus the
   sheet shell they are mounted in.

   THE SHELL BORROWS .ktmodal's GEOMETRY and changes only what a sheet needs
   that a dialog does not: a wider panel (rows want the width) and a body that
   scrolls rather than a page that does, because a sheet of session rows is
   genuinely longer than a form with two fields.

   EVERY CARD BELOW IS ALSO A PAGE. So nothing here assumes a veil behind it:
   the three bodies are styled as blocks and the shell is what adds the layer.
   A rule that only worked inside .ktmodal would have made the scriptless arm
   a different-looking screen, which is the fallback shipping as the product
   one stylesheet along. */

.ktsheet-panel { max-width: 40rem; width: 100%; }
.ktsheet-head { padding-bottom: var(--b-space-2); border-bottom: 1px solid var(--border-subtle); }
.ktsheet-body { max-height: min(70vh, 34rem); overflow-y: auto; }

/* The WHEN sheet (A5) — times only, whole-row press targets. */
.ktwhen { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktwhen-head { display: flex; flex-direction: column; gap: 2px; }
.ktwhen-name { font-size: var(--b-text-lg); }
.ktwhen-where { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.ktwhen-date {
  margin: var(--b-space-2) 0 0; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted);
}
/* THE WHOLE ROW IS THE PRESS TARGET (K22). The chevron is drawn inside it as
   an affordance and is never the hit area — a reader aiming at a 15px glyph on
   a phone is a reader the sheet made work for nothing. */
.ktwhen-row {
  display: flex; align-items: center; gap: var(--b-space-3);
  padding: var(--b-space-2) var(--b-space-3); text-decoration: none; color: inherit;
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
a.ktwhen-row:hover { border-color: var(--border-strong); }
.ktwhen-time { flex: none; min-width: 8.5rem; font-size: var(--b-text-sm); font-weight: var(--b-weight-bold); }
.ktwhen-sub { flex: 1 1 auto; min-width: 0; font-size: var(--b-text-sm); }
.ktwhen-go { flex: none; color: var(--text-muted); }
/* A FULL ROW IS DIMMED AND HAS NO ACT. It keeps its place, because a session
   that fills does not stop existing and a reader scanning for Saturday needs
   to find out that Saturday is gone. There is no waitlist control here and no
   wire behind one — nothing queues. */
.ktwhen-row--full { opacity: 0.6; cursor: default; }
.ktwhen-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-3);
  flex-wrap: wrap; padding-top: var(--b-space-2); border-top: 1px solid var(--border-subtle);
}
.ktwhen-next { font-size: var(--b-text-sm); }

/* The HOW sheet (A4) — the pinned context, then full consequences. */
.kthow { display: flex; flex-direction: column; gap: var(--b-space-2); }
.kthow-pin {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--b-space-3); border-radius: var(--b-radius-md);
  background: var(--surface-sunken, var(--surface-raised));
}
.kthow-kicker {
  margin: 0; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted);
}
.kthow-title { font-size: var(--b-text-lg); }
.kthow-where { margin: 0; font-size: var(--b-text-sm); }
.kthow-ask { margin: var(--b-space-2) 0 0; font-size: var(--b-text-md); font-weight: var(--b-weight-bold); }
.kthow-opt {
  display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-3);
  margin: 0; padding: var(--b-space-3);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
/* ONE EMPHASIS PER SHEET, AND IT IS THE CHEAPEST COVERING OPTION (K24).
   It is a BORDER rather than a ground, deliberately: K29 reserves the grounds,
   and an emphasis drawn in green would have said "covered" to somebody who is
   being asked precisely because they are not. */
.kthow-opt--pick { border-color: var(--role-primary, var(--border-strong)); border-width: 1.5px; }
.kthow-opt-say { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kthow-opt-name { font-size: var(--b-text-md); }
.kthow-opt-sub { font-size: var(--b-text-sm); }
/* K29's BLUE: derived social proof, and never the org's opinion wearing it. */
.kthow-opt-affix { color: var(--role-info-ink, var(--text-primary)); font-weight: var(--b-weight-semibold); }
.kthow-opt-act { flex: none; }
/* The closing promise moved from the sheet's foot to directly under its
   question (TEN-1703), so its top margin goes with it: that space was there to
   part the sentence from the last option row it used to follow, and left in
   place it would part it from the heading it now belongs to. `.kthow`'s own
   column gap is the rhythm; this paragraph adds nothing to it. */
.kthow-close { margin: 0; font-size: var(--b-text-sm); }
/* K30 arm 2 WHERE THE ROWS WOULD BE (TEN-1704): the club cannot take a card, so
   the sheet states that instead of offering three rails that all refuse. Muted
   ink, like the closing sentence it stands in for — not an alarm colour, since
   nothing is broken and nothing was charged. */
.kthow-note { margin: 0; font-size: var(--b-text-sm); }

/* The entitlement quickview (A2). */
.ktqv { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktqv-head { display: flex; flex-direction: column; gap: 4px; }
.ktqv-id { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; }
.ktqv-name { font-size: var(--b-text-xl); }
.ktqv-kind {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 3px 10px;
  color: var(--text-muted); background: var(--surface-sunken, var(--surface-raised));
}
.ktqv-price { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.ktqv-amount { font-size: var(--b-text-lg); color: var(--text-primary); }
.ktqv-also { display: flex; gap: var(--b-space-2); flex-wrap: wrap; }
.ktqv-also-card {
  display: flex; flex-direction: column; gap: 2px; text-decoration: none; color: inherit;
  padding: var(--b-space-2) var(--b-space-3);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
}
.ktqv-also-card:hover { border-color: var(--border-strong); }
.ktqv-also-name { font-size: var(--b-text-sm); }
.ktqv-also-price { font-size: var(--b-text-xs); }
.ktqv-inc-head {
  margin: var(--b-space-2) 0 0; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted);
}
.ktqv-inc {
  display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-3);
  padding: var(--b-space-2) 0; border-bottom: 1px solid var(--border-subtle);
}
.ktqv-inc:last-of-type { border-bottom: none; }
.ktqv-inc-say { min-width: 0; font-size: var(--b-text-sm); }
.ktqv-inc-name { font-weight: var(--b-weight-semibold); }
.ktqv-inc-note { flex: none; font-size: var(--b-text-xs); }
/* K29 AGAIN: the provenance pin is green because it is a fact about the
   READER's own path — "the one you were looking at" — and it is the only row
   in the list that wears a ground. */
.ktqv-inc--origin {
  background: var(--green-a); border-bottom: none;
  margin: 0 calc(var(--b-space-2) * -1);
  padding-left: var(--b-space-2); padding-right: var(--b-space-2);
  border-radius: var(--b-radius-md);
}
.ktqv-pin {
  flex: none; font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 3px 10px;
  color: var(--green-ink, var(--text-primary)); background: var(--green-b, var(--green-a));
}
.ktqv-not { margin: var(--b-space-2) 0 0; font-size: var(--b-text-sm); max-width: 62ch; }
.ktqv-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-3);
  flex-wrap: wrap; padding-top: var(--b-space-2); border-top: 1px solid var(--border-subtle);
}
.ktqv-buy { margin: 0; }
/* K30 arm 2 in the buy's place (TEN-1704). */
.ktqv-note { margin: 0; font-size: var(--b-text-sm); }

@media (max-width: 560px) {
  .ktwhen-row { flex-wrap: wrap; }
  .ktwhen-time { min-width: 0; width: 100%; }
  .kthow-opt { flex-direction: column; align-items: stretch; }
  .kthow-opt-act { width: 100%; }
}

/* A ROW THAT IS THE COMMIT (K24 case 1). It is a submit rather than an
   anchor, so it needs the button reset the kit never gave .ktwhen-row: a
   browser default border and background would have made one row in the sheet
   look like a different kind of thing from its neighbours, which is exactly
   the ambiguity a whole-row press target exists to remove. */
.ktwhen-form { margin: 0; display: block; }
.ktwhen-row--act {
  width: 100%; text-align: left; font: inherit; cursor: pointer;
  appearance: none; -webkit-appearance: none;
}
.ktwhen-row--act:hover { border-color: var(--border-strong); }

/* ── THE COVERAGE MATRIX (TEN-1693; board A6) ────────────────────────────
   Every program against every plan, with the commitment drawn as rows of the
   same table.

   IT IS A REAL TABLE and stays one at every width. A matrix whose columns
   stacked would have become a list of plans, which is the surface it exists to
   be an alternative to — so the narrow arm SCROLLS SIDEWAYS inside its own box
   with the program column pinned, and the page around it never scrolls. K18's
   "narrow screens are the page" is about which MOUNT you get, not about the
   table giving up being a table.

   NOTHING HERE HIDES BEHIND A HOVER. There is no tooltip rule in this block and
   adding one is the change to refuse: hover does not exist on a phone, and a
   fact that changes the buying decision does not hide behind one. */

.ktmx { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktmx-head { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktmx-h { font-size: var(--b-text-xl); }
/* K23's PINNED CONTEXT, and K29's green: this is the session the reader's own
   coverage question is about, said in words so the promise is legible before it
   is kept. Absent cold, where there is no booking to continue. */
.ktmx-ctx {
  margin: 0; font-size: var(--b-text-sm);
  padding: var(--b-space-2) var(--b-space-3); border-radius: var(--b-radius-md);
  color: var(--green-ink, var(--text-primary)); background: var(--green-a);
}
/* K30 arm 2 for the whole grid (TEN-1704), said once at the head. Plain muted
   ink deliberately — NOT the green strip above it, which is a promise about the
   reader's own booking, and not an alarm colour either. */
.ktmx-note { margin: 0; font-size: var(--b-text-sm); }

/* THE SCROLL BOX OWNS BOTH AXES, AND THAT IS THE BUG FIX (TEN-1749 item 1).
   Mark's UAT: "the horizontal scrollbar is invisible until you scroll to the
   bottom". It was, and the reason was structural rather than cosmetic — this
   container scrolled sideways while the SHEET around it scrolled down, so the
   box was as tall as the whole table and its horizontal bar sat at the foot of
   that height, far below the sheet's 70vh window. The one affordance telling a
   reader the table continues sideways was the one thing they had to already
   know about in order to find.

   So there is now exactly ONE scroller and it is this one. Bounding its height
   puts the horizontal bar at a FIXED DEPTH — the bottom edge of a box that is
   never taller than the window — in both mounts, the page and the sheet alike;
   the page arm had the same defect one scroll container up. `.ktsheet-body`
   hands its vertical scrolling over below, because two nested scrollers is what
   put the bar out of reach in the first place.

   A SECOND SCROLLER IS THE CHANGE TO REFUSE HERE. Anything that re-introduces
   overflow between `.ktsheet-body` and this box buries the bar again, and it
   will look fine on a club with four programs. */
.ktmx-scroll {
  overflow: auto;
  max-height: min(70vh, 34rem);
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
}
/* THE SHEET GIVES UP ITS OWN PANE to the box above (TEN-1749). Every other
   sheet in this family keeps `.ktsheet-body`'s scroller — a list of session
   rows has one axis and no reason to change — so the rule is scoped by `:has()`
   to the one card whose content is horizontal.

   AND THE BOX ABSORBS WHAT IS LEFT rather than taking a height of its own here.
   The panel is already a flex column capped at the window (`.ktmodal-panel`:
   `max-height: 100%`), so the chain below hands the leftover space to the scroll
   box — `min-height: 0` at every link, because a flex item's default minimum is
   its CONTENT, and one link that forgets it pushes the box past the panel's
   bottom edge and buries the scrollbar again by a different route. A fixed 70vh
   here would have done exactly that: 70vh of box under a title and a heading is
   taller than the panel that holds them. */
.ktsheet-body:has(.ktmx) {
  max-height: none; overflow: visible;
  display: flex; flex-direction: column; min-height: 0;
}
.ktsheet-body:has(.ktmx) .ktmx { min-height: 0; }
.ktsheet-body:has(.ktmx) .ktmx-scroll { max-height: none; min-height: 0; flex: 1 1 auto; }

/* `separate` RATHER THAN `collapse` BECAUSE THE HEADS ARE STICKY. A collapsed
   border belongs to the table rather than to the cell, so it does not travel
   with a sticky cell and the pinned column and header row lose their rules the
   moment somebody scrolls. Zero spacing keeps the drawn result identical: every
   border here is a single bottom edge, which collapse had nothing to merge it
   with. */
.ktmx-t { width: 100%; border-collapse: separate; border-spacing: 0; }
.ktmx-t th, .ktmx-t td {
  padding: var(--b-space-2) var(--b-space-3); text-align: center;
  border-bottom: 1px solid var(--border-subtle); vertical-align: middle;
}
/* THE OPAQUE GROUND A STICKY CELL SCROLLS OVER, and it is a variable because
   this card is mounted on two different surfaces: the page (K18's narrow arm
   and the shared link) and the sheet over the Programs tab. A pinned cell
   painted one ground on both would be a pale rectangle on whichever of them it
   guessed wrong about. */
.ktmx { --ktmx-bg: var(--surface-page); }
.ktmodal-panel .ktmx, .ktsheet-panel .ktmx { --ktmx-bg: var(--surface-raised); }

/* THE PROGRAM COLUMN IS PINNED, which this block's own note has claimed since
   TEN-1693 and no rule delivered (TEN-1749). It is what makes scrolling
   sideways legible rather than disorienting: the cells that move are answers,
   and an answer whose question has scrolled off the left is not a comparison.
   The head row pins for the same reason downward — a coverage cell ten rows
   down means nothing without the plan name above it. */
.ktmx-t thead th { position: sticky; top: 0; z-index: 2; background: var(--ktmx-bg); }
.ktmx-rh, .ktmx-rl {
  text-align: left; min-width: 13rem;
  position: sticky; left: 0; z-index: 1; background: var(--ktmx-bg);
}
/* THE CORNER CELL SITS ABOVE BOTH PINS, and the selector carries `thead`
   deliberately: `.ktmx-rh` alone loses to `.ktmx-t thead th` on specificity, so
   the corner inherited that rule's z-index, tied with the column heads, and lost
   to them on DOM order — the plan names slid OVER the pinned "Program" cell. A
   z-index that reads correctly and never applies is the kind of rule that only
   fails once somebody scrolls. */
.ktmx-t thead .ktmx-rh { z-index: 3; }
.ktmx-ch { min-width: 9rem; }
.ktmx-ch--noplan { border-left: 1px solid var(--border-subtle); }
.ktmx-ch, .ktmx-rh { vertical-align: bottom; }
.ktmx-cname { display: block; font-size: var(--b-text-sm); font-weight: var(--b-weight-bold); }
.ktmx-cprice { display: block; font-size: var(--b-text-xs); font-weight: var(--b-weight-regular); }
/* K26's THREE BADGES IN K29's TWO CHANNELS: blue is DERIVED social proof and
   purple is the club's own curation, ALWAYS LABELLED. Curation is allowed;
   unmarked curation is not — which is why "Featured" carries its provenance in
   the word and the other two do not need to. */
.ktmx-badge {
  display: inline-block; margin-bottom: 3px;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 2px 9px;
}
.ktmx-badge--value, .ktmx-badge--popular {
  color: var(--role-info-ink, var(--text-primary)); background: var(--role-info-bg, var(--surface-sunken));
}
.ktmx-badge--featured {
  color: var(--role-accent-ink, var(--text-primary)); background: var(--role-accent-bg, var(--surface-sunken));
}
/* THE VIEWER'S OWN STANDING, and the only green in a column head (K29). */
.ktmx-hold {
  display: block; margin-top: 3px; font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  color: var(--green-ink, var(--text-primary));
}
.ktmx-ghead {
  text-align: left; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-muted);
  border-bottom: none; padding-top: var(--b-space-3);
}
.ktmx-rl { font-weight: var(--b-weight-regular); }
.ktmx-rname, .ktmx-rlink { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.ktmx-rlink { color: inherit; }
.ktmx-rnote { font-size: var(--b-text-xs); }
/* K23's PROVENANCE PIN. The row the reader came in on, marked where they can
   see it, so the bounce back never disorients — the quickview's own pin, one
   surface along, in the same words and the same green. */
.ktmx-row--origin { background: var(--green-a); }
/* AND THE PIN SURVIVES THE PINNED COLUMN. `.ktmx-rl` paints an opaque ground so
   cells do not scroll visibly under it, which would otherwise erase the green
   on exactly the row K23 marked. The tint is laid back over that ground as an
   image rather than a colour, so the cell is both opaque and green — the one
   row in the table whose label must keep its meaning while the answers slide
   past it. */
.ktmx-row--origin .ktmx-rl {
  background-image: linear-gradient(var(--green-a), var(--green-a));
}
.ktmx-pin {
  display: inline-block; margin-left: var(--b-space-2);
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 2px 9px;
  color: var(--green-ink, var(--text-primary)); background: var(--green-b, var(--green-a));
}
.ktmx-cell { font-size: var(--b-text-sm); }
/* K29's GREEN, ON ITS BROWSING SIDE. The ruling's own table says browsing and
   owning share the colour "because it is the same fact from two sides" — "a
   membership covers this" and "your membership covers this" — so a coverage
   cell is green and is not moonlighting. */
.ktmx-cell--yes { color: var(--green-ink, var(--text-primary)); font-weight: var(--b-weight-semibold); }
.ktmx-cell--no { color: var(--text-muted); }
.ktmx-acts .ktmx-act { border-bottom: none; padding-top: var(--b-space-3); }
.ktmx-form { margin: 0; }
.ktmx-btn { width: 100%; }
/* TEN-1945 — THE PLAN IN SCOPE, and every column's own page.
   The reader arrived from one plan's page: that column wears the SELECTED
   channel's quiet tint (K15 — selected owns a soft ground, never a fill, and
   never green, which is K29's `covered`) from head to act. The tint is laid over
   the cell's own ground as an image so the sticky head stays opaque while the
   body scrolls under it — the pinned row's own device. */
.ktmx-t .ktmx-scope {
  background-image: linear-gradient(var(--role-primary-soft), var(--role-primary-soft));
}
.ktmx-t thead .ktmx-scope { box-shadow: inset 0 3px 0 var(--role-primary); }
.ktmx-here {
  display: inline-block; margin-bottom: 3px;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 2px 9px;
  color: var(--role-primary-ink); background: var(--role-primary-soft-strong);
}
.ktmx-clink { color: inherit; text-decoration: none; }
.ktmx-clink:hover, .ktmx-clink:focus-visible { text-decoration: underline; }
.ktmx-cnote { margin: 0 0 var(--b-space-2); font-size: var(--b-text-xs); }
/* /TEN-1945 */
/* (`.ktmx-gov` is gone with the cut it apologised for — TEN-1749. The cold arm
   draws every plan the club sells, so there is no omission to name and no "All
   plans ›" link pointing back at the page already being read.) */

/* The entry points (board A6's three). */
.sfband-top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--b-space-3); flex-wrap: wrap;
}
.sfband-compare { font-size: var(--b-text-sm); }
.kthow-compare { margin: 0; font-size: var(--b-text-sm); }
.ktqv-compare { font-size: var(--b-text-sm); }

/* THE MATRIX'S SHEET IS WIDER THAN THE OTHER THREE, because a comparison is
   the one card here whose content is horizontal. K18: a wide modal, and the
   page on a narrow screen. */
.ktsheet-panel:has(.ktmx) { max-width: 62rem; }

@media (max-width: 720px) {
  .ktmx-rh, .ktmx-rl { min-width: 9rem; }
  .ktmx-ch { min-width: 7.5rem; }
  .ktmx-t th, .ktmx-t td { padding: var(--b-space-2); }
}

/* ─── THE LANDING PAGES (TEN-1717; boards E1, E2 and E3) ──────────────────────
   One spine drawn once, three boards' worth of content hung on it. Every rule
   below is shared by all three pages: what differs between the boards is which
   blocks have anything in them, which is the composition's own posture — data
   presence, never a fork — expressed here as "no block styles itself differently
   per type". */
.sflanding { display: flex; flex-direction: column; gap: var(--b-space-5); }

/* K16's trail, amending K21 clause 2 for LEAF pages only. THE SEPARATOR IS THE
   STYLESHEET'S: a chevron between two rungs is decoration between two links, and
   rendered as text it is read out between every pair on the way to the page
   name. */
.sfcrumbs { display: flex; align-items: center; flex-wrap: wrap; gap: var(--b-space-2); font-size: var(--b-text-sm); }
.sfcrumb { font-weight: var(--b-weight-semibold); }
.sfcrumb + .sfcrumb::before, .sfcrumb-here::before {
  content: "\203A"; margin-right: var(--b-space-2); color: var(--text-faint);
}
.sfcrumb-here { color: var(--text-muted); }

/* THE ANCHOR ROW: what this is, and the one thing to do about it, in one
   screenful. E3 puts its date block first; the two program boards lead with the
   title, and the act zone is the same object on all three. */
.sfland-anchor { display: flex; align-items: flex-start; gap: var(--b-space-4); flex-wrap: wrap; }
.sfland-id { flex: 1 1 22rem; min-width: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfland-titlerow { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; }
.sfland-title { margin: 0; font-size: var(--b-text-2xl); font-weight: var(--b-weight-bold); }
.sfland-chip {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold); letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--text-muted); background: var(--surface-sunken);
  border-radius: var(--b-radius-sm); padding: 2px var(--b-space-2);
}
.sfland-sub { margin: 0; }
.sfland-about { margin: 0; max-width: 42rem; line-height: 1.6; }

/* THE DATE BLOCK — E3's poster instinct, kept utilitarian. It greys when the
   night has passed rather than disappearing: a reader who followed a texted link
   needs to find out that it happened. */
.sfland-date {
  flex: none; width: 5.75rem; overflow: hidden; text-align: center;
  border: 1.5px solid var(--border-strong); border-radius: var(--b-radius-md);
  display: flex; flex-direction: column;
}
.sfland-date-day {
  background: var(--border-strong); color: var(--surface-page);
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.08em; padding: 3px 0;
}
.sfland-date-num { font-size: var(--b-text-xl); font-weight: var(--b-weight-bold); padding-top: 4px; }
.sfland-date-clock { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); padding-bottom: 6px; }
.sfland-date--past { opacity: 0.55; }

/* THE ACT ZONE, and K31's whole census lives in it: where there is an act it is
   the one advancing control on the page, and where there is not, the SAME PLACE
   states the fact. There is no disabled treatment here because there is no
   disabled control — this page never draws one. */
.sfland-act {
  flex: none; display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--b-space-2); text-align: right;
}
.sfland-actform { margin: 0; display: contents; }
.sfland-btn { white-space: nowrap; }
.sfland-fact { margin: 0; font-size: var(--b-text-sm); }
/* TEN-1961: the booking's follow, disclosed under the act. */
.sfland-follows { margin: 0; font-size: var(--b-text-sm); }
/* K29's channels, and neither moonlights: amber is running low, green is a fact
   about YOU. Nothing else on this page takes a colour. */
.sfland-fact--warn { color: var(--role-warn); font-weight: var(--b-weight-semibold); }
.sfland-note { margin: 0; font-size: var(--b-text-xs); }
.sfland-state { margin: 0; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); color: var(--text-muted); }
.sfland-state--ok { color: var(--green-ink, var(--role-good)); }

/* THE K15 BAND — E2's five cells and E3's five Ws, one shape. The authored rows
   carry a hairline that marks the seam the boards draw: derived facts come off
   the wire, authored ones come from the club, and a reader can tell which is
   which without being told. */
.sfland-facts {
  margin: 0; display: flex; flex-wrap: wrap; gap: var(--b-space-4) var(--b-space-5);
  padding: var(--b-space-3) var(--b-space-4);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-sunken);
}
.sfland-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sfland-cell--authored { border-left: 2px solid var(--border-subtle); padding-left: var(--b-space-3); }
.sfland-cell-label {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold); letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--text-muted);
}
.sfland-cell-value { margin: 0; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }

/* THE WAYS-IN LINE (A1b, verbatim). */
.sfland-ways {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--b-space-2);
  padding: var(--b-space-3) var(--b-space-4);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-sunken);
}
.sfland-ways-label { font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold); }
.sfland-ways-tail { font-size: var(--b-text-sm); }
.sfland-compare { margin-left: auto; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }

.sfland-centre, .sfland-wheresec, .sfland-tail { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfland-head { margin: 0; font-size: var(--b-text-md); font-weight: var(--b-weight-bold); }

/* THE WHERE CARD. "More info" and the subtext's own mention name the same
   dialog; both are plain anchors underneath, so the modal is the enhancement and
   the address is the guarantee. */
.sfland-where {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--b-space-3); flex-wrap: wrap;
  padding: var(--b-space-3) var(--b-space-4);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.sfland-where-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sfland-where-name { font-weight: var(--b-weight-bold); }
.sfland-where-addr { font-size: var(--b-text-sm); }
.sfland-where-links { display: flex; align-items: center; gap: var(--b-space-3); flex-wrap: wrap; }
.sfland-maplink { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); white-space: nowrap; }

/* E2's ARC. The rows are STATED FACTS and carry no press target — a course
   enrols as a whole — so nothing here has a hover or a cursor. A greyed week is
   a statement about the CALENDAR: these dates are behind you. */
.sfland-weeks { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfland-week {
  display: flex; align-items: center; gap: var(--b-space-3); flex-wrap: wrap;
  padding: var(--b-space-2) var(--b-space-4);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised);
}
.sfland-week--past { opacity: 0.5; }
.sfland-week-n { flex: none; min-width: 4.5rem; font-size: var(--b-text-sm); font-weight: var(--b-weight-bold); }
.sfland-week-dates { flex: 1 1 12rem; font-size: var(--b-text-sm); }
.sfland-week-note { font-size: var(--b-text-sm); }
.sfland-tailnote { margin: 0; font-size: var(--b-text-sm); }

/* E3's ONE derived row — never a feed. It is the session-row idiom this family
   already speaks, so a reader who knows one list in this book knows this row. */
.sfland-also {
  display: flex; align-items: center; gap: var(--b-space-3); flex-wrap: wrap;
  padding: var(--b-space-2) var(--b-space-4);
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised); color: inherit; text-decoration: none;
}
.sfland-also-when { flex: none; min-width: 6.5rem; font-size: var(--b-text-sm); font-weight: var(--b-weight-bold); }
.sfland-also-name { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
.sfland-also-sub { flex: 1 1 10rem; font-size: var(--b-text-sm); }
.sfland-also-go { flex: none; color: var(--text-faint); }

.sfland-terms { display: block; }
.sfland-foot { margin: 0; font-size: var(--b-text-sm); }

@media (max-width: 720px) {
  /* THE ACT ZONE COMES BACK TO THE LEFT on a phone, under the identity block:
     right-aligned text in a full-width column reads as a mistake rather than as
     an anchor. */
  .sfland-act { align-items: stretch; text-align: left; width: 100%; }
  .sfland-btn { width: 100%; }
  .sfland-compare { margin-left: 0; }
}

/* ─── DESCRIPTION & FACTS, THE EDITOR (TEN-1717) ─────────────────────────────
   The org side of the landing boards, on board 7 and on the wizard's first step.
   It is ONE composition mounted twice, so it takes one set of rules — a second
   block of styles per mount is how two mounts of one editor come to look like
   two editors. */
.clsdesc { display: flex; flex-direction: column; gap: var(--b-space-4); }
.clsdesc-facts { display: flex; flex-direction: column; gap: var(--b-space-2); }
.clsdesc-rows { display: flex; flex-direction: column; gap: var(--b-space-2); }
/* A FACT IS A PAIR and the row says so: the label is narrow because a label is
   sixteen characters, and the value takes the rest. They wrap together on a
   phone rather than the value dropping alone under a half-width label. */
.clsdesc-row { display: flex; gap: var(--b-space-2); flex-wrap: wrap; }
.clsdesc-box { flex: 1 1 8rem; min-width: 0; }
.clsdesc-box--wide { flex: 2 1 14rem; }
.clsdesc-add { align-self: flex-start; }

/* ── THE CONVERSATIONS LAYOUT (TEN-1795) ───────────────────────────────────
   Mark's ruling: ONE layout for every kind of chat. The rail on the left lists
   1-1s, groups and rooms together; the pane on the right is whichever one is
   open, or the placeholder. /account/messages, /account/messages/{thread} and
   /account/rooms/{room} are three STATES of this, not three surfaces.

   Three columns now sit in .console — nav, rail, pane — so the rail takes a
   fixed basis and the pane takes the slack. min-width: 0 on both, because a
   flex child defaults to min-content width and a long room title or an
   unbroken handle would otherwise push the pane off the side of the page. */
/* ONE THIRD AND TWO THIRDS (TEN-1817, Mark's UAT: "force the split"). The rail was a
   FIXED 20rem basis, which is a third of a 960px window and a fifth of a 1600px one — so
   the wider the screen, the more the list looked like a sidebar bolted to a chat app. A
   proportion holds the reading relationship at every width.

   `flex-basis` percentages with `flex-grow: 0`, and the `gap` is taken out of the pane's
   share rather than split, so 33.333% is honestly the rail's third of the row. min-width: 0
   on both, unchanged and load-bearing: a flex child defaults to min-content width and one
   long room title would otherwise push the pane off the side of the page.

   THE RAIL STILL HAS A CEILING. A third of a 2560px window is 850px of conversation list,
   which is a third of the screen spent on names; past that the proportion stops being a
   relationship and becomes waste, so it caps and the pane takes the rest. The cap is
   generous on purpose — it is the point where "a third" stops describing what the eye
   does, not a width somebody preferred. */
/* ── THE FRAME RULE (TEN-1819, Mark's addition 2) ──────────────────────────
   "Both panes are full-height tracks of one grid — top edges flush, bottom edges
   flush, each pane scrolling its own content inside the fixed frame."

   IT WAS A FLEX ROW OF FLOATING CARDS, and Mark's suspicion — that the misaligned
   pane edges and the page's phantom horizontal scrollbar share a cause — is
   correct, though the cause sits one level up from where it looked.

   TWO THINGS WERE WRONG AND THE SECOND IS THE LOUD ONE:

   1. `align-items: flex-start` made each column as tall as its own contents, so
      two columns of different lengths had different bottom edges by construction.
      A grid with stretched tracks cannot do that: the tracks ARE the frame.

   2. THE WHOLE LAYOUT WAS RUNNING INSIDE A 688px READING COLUMN. `main.container`
      is `max-width: 720px` with `var(--b-space-4)` of padding on each side — 688px
      of content on a 1280px screen, on every page of this book that does not
      escape it by name (`body.app:has(.ktpage)` takes 1180, `.clshub` takes 1280).
      The conversations layout never got its escape, so "a third of the row" was a
      third of 688: the rail landed at ~225px and the pane at ~450px, floating in
      the middle of a wide window with everything inside them fighting for space.
      That is the cap ten1818 measured and could not place; it belongs here,
      because it is the frame.

      It is also the horizontal overflow. At 688px of content the rail's third is
      narrower than the widest thing the rows and the composer can shrink to, so
      the pane's min-content width exceeded its track and the page grew sideways —
      a bottom scrollbar that appears on a 1280px screen and vanishes if you make
      the window smaller, which is exactly how it presented. Widening the frame and
      making both tracks `minmax(0, …)` removes the squeeze and the overflow with
      it: a `minmax(0, …)` track may shrink below its content's min-content size,
      which is the grid spelling of the `min-width: 0` this layout already knew it
      needed.

   THE PROPORTION IS UNCHANGED AND SO IS ITS CEILING. A third and two thirds, the
   rail capped at 30rem so a third of a very wide window does not become 850px of
   names, and the gap taken out of the pane's share rather than split — all of that
   is TEN-1817's ruling and none of it moved. What changed is what the third is a
   third OF. */
.conv-layout {
  display: grid;
  grid-template-columns: minmax(0, min(33.333%, 30rem)) minmax(0, 1fr);
  gap: var(--b-space-3); min-width: 0; align-items: stretch;
}
.conv-rail { min-width: 0; }
.conv-pane { min-width: 0; }

/* ── THE RAIL'S HEAD AND ITS COLLAPSE CHEVRON (TEN-1817) ───────────────────
   The checkbox is the state and it is never seen: `.conv-railbox` is the control in the
   tab order (the label deliberately is not — room.plate.html's rule), so it is moved off
   screen rather than `display: none`, which would take it out of the tab order and out of
   the accessibility tree along with it.

   THE COLUMN IS COLLAPSED BY `:has()` AND THE BODY BY A SIBLING RULE, which is two
   mechanisms for one fact because they are two different relationships: the body is the
   checkbox's own sibling, and `.conv-rail` is its ancestor's ancestor. The @supports guard
   is the same one the viewport lock carries — without `:has()` the rail keeps its third and
   the body still hides, which is a degraded but coherent render rather than a broken one. */
.conv-rail-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-2);
}
.conv-rail-head .console-title { margin: 0; }
.conv-railbox { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.conv-railtoggle {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  box-sizing: border-box; width: var(--control-h); height: var(--control-h);
  color: var(--text-muted); cursor: pointer;
  border: var(--b-border-w) solid transparent; border-radius: var(--control-r);
  background: transparent;
}
.conv-railtoggle:hover { color: var(--text-primary); background: var(--surface-raised); border-color: var(--border-strong); }
/* The ring is painted from the CHECKBOX's own focus state, because the checkbox is the
   element that actually takes the focus — the label is a picture of it. */
.conv-railbox:focus-visible + .conv-rail-head .conv-railtoggle {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px;
}
/* DOUBLED (TEN-1826, Mark's round-4 UAT: "chevron and + icons double their size"). 18px was the
   rail's type scale applied to a glyph, and a glyph is not type: at 12.5px body text an 18px chevron
   reads as a punctuation mark rather than as a control, which is exactly how it was being missed.
   36px fills the control box it already had — the box is `--control-h` and did not move, so nothing
   around it reflows and the head's three children keep their places. */
.conv-railtoggle-glyph { font-size: 36px; line-height: 1; transition: transform 120ms ease; }
/* ONE GLYPH, MIRRORED — not two glyphs swapped. Two would be two things to keep in step
   for one fact, and the fact is a direction. */
.conv-railbox:checked + .conv-rail-head .conv-railtoggle-glyph { transform: scaleX(-1); }
.conv-railtoggle-show { display: none; }
.conv-railbox:checked + .conv-rail-head .conv-railtoggle-hide { display: none; }
.conv-railbox:checked + .conv-rail-head .conv-railtoggle-show { display: inline; }
.conv-railbox:checked + .conv-rail-head .console-title { display: none; }
.conv-railbox:checked ~ .conv-rail-body { display: none; }
@supports selector(:has(*)) {
  /* Collapsed, the rail's track shrinks to whatever the chevron needs and the pane
     takes the whole row — which is the point of collapsing at all. `auto` rather
     than a width, so the track is the control's own size in every theme. */
  .conv-layout:has(.conv-railbox:checked) { grid-template-columns: auto minmax(0, 1fr); }
}

/* ── THE RAIL'S TYPE SCALE (TEN-1817, Mark's UAT: "reduce the font sizes in the
   conversations column") ───────────────────────────────────────────────────
   Set ONCE on the rail rather than per element, so a row, a modal opener, a chip and the
   sort control cannot end up on four different scales — and scoped to `.conv-rail` so the
   conversation pane beside it is untouched. The pane is the reading surface; the rail is a
   list of names, and a list of names does not need the reading surface's size. */
/* TIGHTER AGAIN (TEN-1819, Mark's UAT round 2: "smaller type and tighter density
   throughout the column; the header row fits its contents at 1/3 width"). The same
   single-place rule, one notch down, plus the SPACING — which is where most of the
   column was actually going. The card padding around the list, the gap between
   rows and the padding inside each row were all the console's general-purpose
   values, chosen for a full-width surface. A list of names in a third of a window
   is not that surface. */
.conv-rail { font-size: 12.5px; }
.conv-rail .console-title { font-size: var(--b-text-sm); letter-spacing: 0.04em; }
.conv-rail .acct-stub-title { font-size: 12px; }
.conv-rail .acct-value { font-size: 12.5px; }
.conv-rail .acct-label { font-size: 11px; }
.conv-rail .console-micro { font-size: 10.5px; }
/* The list's own card loses most of its padding and all of its bottom margin: it is
   a full-height track now, so the margin was reserving space under a column that
   reaches the floor. */
.conv-rail .msg-inbox { padding: var(--b-space-2); margin-bottom: 0; }
.conv-rail .msg-row { padding: 5px var(--b-space-2); }
.conv-rail .msg-row-people-face { padding: 5px 0 5px var(--b-space-2); }
/* A SUBTLE SEPARATOR BETWEEN ROWS (TEN-1826, Mark's round-4 UAT). The rail's rows were separated by
   1px of AIR, which at this density reads as one block of text rather than as a list — the eye has
   nothing to count by, and the two-line row makes that worse rather than better (name, sub-line,
   name, sub-line, with no rule to say where one row stops).

   `--border-subtle` and nothing louder: this is a rhythm aid, not a table. The hairline is the
   quietest thing in the column and the row's own hover ground still paints over it.

   `:not([hidden]) ~ :not([hidden])` AND NOT `+`, AND THAT IS THE WHOLE TRICK. The rail HIDES rows
   rather than dropping them — the search and the capsules both mark-and-hide so widening finds them
   again (markConversationHits' own rule) — so an adjacent-sibling rule would put a top border on a
   row whose visible predecessors are all hidden, i.e. a hairline floating above the first result of
   every filtered list, under the sticky bar. The general sibling combinator asks the question that
   is actually meant: "is there a VISIBLE row before me?" */
.conv-rail .msg-row-wrap + .msg-row-wrap { margin-top: 1px; }
.conv-rail .msg-row-wrap:not([hidden]) ~ .msg-row-wrap:not([hidden]) {
  border-top: var(--b-border-w) solid var(--border-subtle);
}
/* EVERY ROW IS THE SAME HEIGHT, AND THE PIP IS WHY THIS RULE EXISTS (measured,
   TEN-1819). The name-over-sub-line column is 34px; the corner column was 35.8
   whenever it carried a pip, so rows with unread came out 46px against 44px for
   everyone else. Two pixels is not a collision and it is not nothing either — it is
   the list losing its rhythm exactly where the eye is being asked to notice
   something. So the corner reserves the body's height and the pip is sized to fit
   inside it rather than to the console's general-purpose badge. */
.conv-rail .msg-row-meta { min-height: 34px; justify-content: space-between; }
.conv-rail .msg-pip {
  font-size: 10px; line-height: 14px; padding: 0 0.3rem; min-width: 1.05rem;
}

/* ── THE PIN DECORATION (TEN-1826) ─────────────────────────────────────────
   The corner's BOTTOM LINE: the pin, then the unread badge, the badge holding the outer edge
   because it is the thing that changes and the thing the eye goes to the corner for. The marks are
   their own flex row so that a pinned row with no unread sits the pin exactly where a pinned row
   WITH one sits it — a column that placed the pin differently depending on a neighbour would make
   the decoration move as messages arrive.

   `margin-left: auto` on the row keeps both marks against the right edge under the stamp; `gap` is
   4px because two small marks touching read as one wider mark. */
.conv-row-marks {
  display: flex; align-items: center; justify-content: flex-end;
  gap: 4px; margin-left: auto;
}
.conv-row-pin { display: inline-flex; align-items: center; color: var(--text-faint); }
/* THICK-LINE, at Mark's instruction, and at this size it is legibility rather than taste: the mark
   is 14px on a 34px corner beside a badge, and the kit's 1.8 hairline reads as lint there. */
.conv-pin-ic { width: 14px; height: 14px; stroke-width: 2.4; }
/* Not pinned: the element is still rendered (the plate says why — the optimistic rule below needs
   something to reveal) and `display: none` takes it out of the accessibility tree with it, so the
   "Pinned" text is never read on a row that is not. */
.conv-row-pin-off { display: none; }

/* THE PRESS ANSWERS IN ITS OWN FRAME, WITH NO JAVASCRIPT (TEN-1826; Mark's round-4 UAT, and the
   interactivity SOP's first rule — "the postback confirms, never introduces the feedback").

   Pin and Unpin are rows in the ellipsis menu, so a press used to produce exactly nothing visible
   until a full round trip repainted the list: the menu stayed open, the row was unchanged, and the
   only acknowledgment was press.js's own tactile mark on a control the reader had already moved off.
   On a rail this is worse than on a form, because the reader is looking at the ROW and not at the
   menu item they just left.

   THE MECHANISM IS ALREADY ON THE PAGE. press.js adds `.is-pressed` to any submit button
   SYNCHRONOUSLY inside the click — that is the entire point of that file — and `convRowMenu` stamps
   the press's DIRECTION on the pin button as `data-conv-pin` ("1" pinning, "0" unpinning). So the
   row can read, in CSS, both that a pin press is in flight and which way it goes. No asset, no
   listener, no morph handler, and nothing that could disagree with the server: when the postback
   lands, the re-rendered row carries the real `pinned` and these rules stop applying because the
   pressed button no longer exists.

   TWO RULES BECAUSE THERE ARE TWO DIRECTIONS, and an un-pin must feel as immediate as a pin. The
   accent is the "not yet confirmed" register: a settled pin is `--text-faint`, and for the length of
   the round trip it is `--b-accent`, which says something just changed here without borrowing
   hover's surface (K15) or selection's edge.

   @supports, like every other `:has()` block in this file. Without it the row waits for the
   postback, which is what it did before — a degraded acknowledgment, never a broken control, and
   press.js's own mark on the button is unaffected either way. */
@supports selector(:has(*)) {
  .msg-row-wrap:has([data-conv-pin="1"].is-pressed) .conv-row-pin-off {
    display: inline-flex; color: var(--b-accent);
  }
  .msg-row-wrap:has([data-conv-pin="0"].is-pressed) .conv-row-pin { display: none; }
  /* The row itself, quietly, for the same frame — "this row is being changed" — on the ANCHOR's
     ground, which is where selection paints, and at a fraction of its strength so it cannot be
     mistaken for the open conversation. It ends when the answer lands. */
  .msg-row-wrap:has([data-conv-pin].is-pressed) .msg-row {
    background: color-mix(in srgb, var(--b-accent) 8%, transparent);
  }
}
/* The head's own band. `min-width: 0` on the title is what lets the header FIT at a
   third of the width: without it the `<h2>` takes its min-content width and pushes
   the "+" and the chevron off the end of the row. */
/* THE GAP UNDER THE HEAD MATCHES THE PANE'S (TEN-1826, Mark's round-4 UAT: "tops off by a pixel or
   two below the headers"). `--b-space-3`, the same token `.msg-head` leaves under the chat's header,
   so the two columns' headers are followed by the same air.

   MEASURED ON THE RUNNING BOOK AND NOT IN THE BROWSER GATE, and that distinction is the finding
   worth keeping. The gate serves no design tokens at all — `tokens.css` is composed by the engine
   rather than kept on disk, so nothing serves it there and every `var(--b-space-N)` in this file
   resolves to NOTHING (chatacts_test.go's rig notes say so out loud, and it is why those cases
   assert opacity and never a colour). Every spacing measurement taken there is therefore geometry
   with the whole spacing scale collapsed to zero: real numbers about a document that is not this
   one. The first pass at this item was measured in the gate, read `padding-bottom: 0px` for a rule
   that plainly sets one, and produced a set of offsets that were all wrong.

   The real numbers, at 1867x594 on /account/rooms/room-demo-1, after both changes in this ticket:

     .conv-rail / .conv-pane    75 → 578  both, exactly — the TRACKS were never the problem
     .conv-rail-head            75 → 131  (56px tall: a title beside two 44px icon controls)
     .msg-head                  75 → 113  (38px tall: a back button and a two-line id block)

   SO THE HEADS ARE 18px APART AND IT IS A CONTENT-HEIGHT DIFFERENCE, NOT AN ALIGNMENT ERROR. The
   two columns' headers hold different things and are honestly different heights; the cards under
   them therefore start at different lines, and no amount of padding on one fixes that without
   pinning it to a hand-written height taken from the other.

   THAT IS WHERE THIS STOPS, on Mark's own authority ("if it's fixable it would look a lot smarter,
   but let's not beat ourselves up if it isn't playing nice"). A `min-height` matching one head to
   the other is exactly the move this stylesheet already carries two scars from — the capsule row's
   `top: 3rem` and the layout's 688px cap, both numbers that were right once and wrong the next time
   the content changed — and a room whose title wrapped would reopen the gap with the number then
   wrong in the other direction. What IS fixed is everything structural: the tracks are flush, the
   bottom edges are flush (see the pane card's margin, further down), and the air under each header
   is now the same token. */
.conv-rail-head { padding-bottom: var(--b-space-3); }
.conv-rail-head .console-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The modals a row opens are NOT in the rail's scale, even though they are rendered inside
   it: a dialog is a reading surface of its own and shrinking its body would be the rail's
   density leaking into a sentence somebody has to read carefully before pressing Leave. */
.conv-rail .ss-grid-modal { font-size: var(--b-text-sm); }
.conv-rail .ss-grid-modal .acct-value { font-size: var(--b-text-sm); }
.conv-rail .ss-grid-modal .acct-label { font-size: var(--b-text-xs); }

/* The placeholder. Centred in the pane rather than parked at the top, because
   it is the answer to "nothing is open" and belongs where the conversation
   would have been — not stacked under the header like a note about it. */
.conv-pane-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--b-space-1); text-align: center;
  min-height: 12rem; padding: var(--b-space-4) var(--b-space-3);
  border: var(--b-border-w) dashed var(--border-subtle); border-radius: var(--b-radius-md);
  color: var(--text-muted);
}
.conv-pane-empty .acct-stub-title { margin: 0; color: var(--text-primary); }
.conv-pane-empty .acct-stub-body { margin: 0; }

/* The open row, marked. `aria-current="page"` is the source of truth and the
   style hangs off it rather than off a class, so the thing a screen reader is
   told and the thing an eye is shown cannot drift apart. */
.msg-row[aria-current="page"] {
  background: var(--surface-overlay);
  box-shadow: inset 3px 0 0 0 var(--b-accent);
}
/* AND IT HAS TO BE TELLABLE FROM THE ROW UNDER THE POINTER (TEN-1823). The ground
   above is `--surface-overlay`, which is the exact ground `.msg-row-wrap:hover`
   paints — so pressing a row changed nothing at all under a pointer that was
   already on it, and the whole of "this is the one that is open" rested on a 2px
   edge. That was survivable while opening a row meant a page reload; it is the
   ACKNOWLEDGMENT now that the row opens in place (the interactivity SOP's first
   rule — the control reacts on the same frame as the press).

   K15's channel law is the fix, and it is already written for the calendar's
   cells (K33 §3): hover owns the SURFACE, so selection owns the ACCENT. No
   re-declaration under `:hover` is needed here and that is worth saying, because
   the census rule usually demands one — hover paints the WRAP's background and
   both of these paint the ANCHOR, so there is no property the pointer can take
   away. */
.msg-row[aria-current="page"] .conv-row-name { color: var(--b-accent); }

/* THE PANE'S LOADING VENEER (TEN-1823). A conversation opens in place now, and
   between the press and the answer there is a gap only the network knows the
   size of. `convopen.js` paints this after ~100ms and not before: a skeleton that
   appears for every fast open is a flicker, and a flicker is worse than the quiet
   the row's own highlight already fills.

   IT IS THE SHAPE OF A CONVERSATION, not a spinner — a head band, bubbles left
   and right, a composer bar — so the answer lands into a frame the eye has already
   accepted rather than replacing an unrelated widget. Same register as the table
   skeleton further up: the raised surface at low opacity, and a quiet shimmer.

   NO TEXT, `aria-hidden` on the wrap, and `aria-busy` on the pane instead: three
   empty grey boxes read aloud is noise, and "busy" is the fact. */
.conv-pane-skel {
  display: flex; flex-direction: column; gap: var(--b-space-3);
  flex: 1 1 auto; min-height: 12rem; padding: var(--b-space-3);
}
.conv-pane-skel > * {
  border-radius: var(--b-radius-md); background: var(--surface-raised);
  opacity: 0.6; animation: conv-skel-pulse 1.4s ease-in-out infinite;
}
.conv-skel-head { height: 2.75rem; border-radius: var(--b-radius-sm); }
.conv-skel-in { height: 3.25rem; width: min(62%, 22rem); }
.conv-skel-out { height: 2.5rem; width: min(52%, 18rem); align-self: flex-end; }
.conv-skel-foot { height: 3rem; margin-top: auto; border-radius: var(--b-radius-sm); }
@keyframes conv-skel-pulse { 50% { opacity: 0.3; } }
/* A shimmer is decoration and decoration is the first thing a reader who asked for
   less motion should stop being shown. The boxes stay; only the pulse goes. */
@media (prefers-reduced-motion: reduce) {
  .conv-pane-skel > * { animation: none; }
}

/* A room row's initials tile earns a quieter treatment than a face: it stands
   for a place, not a person, so it reads as a label rather than an avatar. */
.msg-row-room .console-person-initial { border-radius: var(--b-radius-sm); }

/* ── THE ROOM NAME'S LIVE PREVIEW (TEN-1826) ───────────────────────────────
   The create and rename forms show what the room will actually be CALLED as somebody types, with
   the `#` flush against the name and no separator (Mark's ruling). The line is ordinary micro copy;
   what earns a rule is the TOKEN, which has to read as a name rather than as running text — so it
   takes the mono face the rest of this cover uses for values a machine will hold, and the accent,
   so the eye lands on the answer rather than on the sentence carrying it.

   `word-break` because a 120-character name in a 30rem modal would otherwise push the dialog wider
   than the viewport on a phone, and this is the one string on the surface with no spaces in it. */
.room-name-preview { margin-top: calc(var(--b-space-1) * -1); }
.room-name-token {
  font-family: var(--b-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  color: var(--b-accent); word-break: break-all;
}

/* ── THE HEADER'S ONE ACT (TEN-1819) ───────────────────────────────────────
   "+ New" and "+ Room" collapsed into one icon-faced primary button that opens the
   chooser. It sits between the title and the chevron, so the head reads
   name · act · state and the chevron stays at the edge it collapses toward.

   SQUARE, at the control height, so it reads as an icon button rather than a word
   button with one character in it — and so it is the same footprint as the chevron
   across the row from it. `margin-left: auto` pushes the pair to the right and
   leaves the title its own line-length; the head's `justify-content: space-between`
   is what used to do that for two children and cannot for three. */
.conv-head-acts { display: inline-flex; align-items: center; gap: var(--b-space-1); flex: none; margin-left: auto; }
/* SECONDARY, AND THE GLYPH IS DOUBLED (TEN-1826). The tier moved in the plate (K38: primary ink is
   the committing button's, and this one opens a chooser); what is here is the size. 36px, the
   chevron's number across the row from it, so the head's two controls read as one pair — they are
   the same footprint and they should be the same weight.

   THE BOX DID NOT MOVE. Still square at `--control-h`, still `padding: 0`, so doubling the glyph
   changes what the eye finds and not what the layout does. */
.conv-new-btn {
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; width: var(--control-h); height: var(--control-h);
  padding: 0; font-size: 36px; line-height: 1;
}
/* A COLLAPSED RAIL HAS NO ROOM TO CREATE ANYTHING IN. The column is a sliver of
   chevron at that point, and a "+" floating in it would open a modal over a pane
   the reader collapsed the list to see. It comes back with the list. */
.conv-railbox:checked + .conv-rail-head .conv-head-acts { display: none; }

/* ── SEARCH AND SORT ON ONE LINE (TEN-1819) ────────────────────────────────
   Two stacked blocks and an instructional sentence became one row: the box takes
   the slack, the ordering takes what it needs. `flex-wrap` rather than a
   breakpoint, because the rail's width is a proportion of the window and the point
   where these two stop fitting is not a viewport size. */
.conv-findbar {
  display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap;
  padding: var(--b-space-2) 0;
}
.conv-findbar .msg-search { flex: 1 1 10rem; min-width: 0; margin: 0; }
.conv-findbar .conv-sort { flex: 0 0 auto; margin: 0; }
/* The select carries the axis in its own resting value ("Most recent"), so it needs
   no label in front of it — see the plate. Narrow, because it is a chooser of six
   short phrases and not a text field. */
.conv-sort-sel { max-width: 9.5rem; }

/* ── THE FILTER CAPSULES (TEN-1819) ────────────────────────────────────────
   K27's facet chips, at this rail's density. QUIET IS THE WHOLE BRIEF: they are
   text on a hairline at rest, they do not compete with the rows, and the one that
   is pressed is the only coloured thing in the band — which is what makes a pressed
   capsule readable as the scope statement that replaced "All conversations".

   HOVER OWNS THE SURFACE AND PRESSED OWNS THE INK (K15's channel law, K29's "a
   colour never moonlights"), so a pressed capsule survives being hovered: the two
   states never write the same property. */
.conv-caps {
  display: flex; flex-wrap: wrap; gap: 4px;
  padding-bottom: var(--b-space-2);
}
.conv-cap {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px var(--b-space-2); font-size: 11.5px; line-height: 1.6;
  color: var(--text-muted); text-decoration: none; white-space: nowrap;
  border: var(--b-border-w) solid var(--border-subtle); border-radius: 999px;
  background: transparent;
}
.conv-cap:hover { color: var(--text-primary); background: var(--surface-raised); border-color: var(--border-strong); }
.conv-cap:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; }
.conv-cap-on, .conv-cap-on:hover {
  color: var(--b-accent); border-color: var(--b-accent);
  background: color-mix(in srgb, var(--b-accent) 10%, transparent);
}
/* The count is the triage number and it is the quietest thing on the capsule —
   tabular so a column of capsules does not shift by a digit as the numbers move. */
.conv-cap-n { color: var(--text-faint); font-variant-numeric: tabular-nums; font-size: 10.5px; }
.conv-cap-on .conv-cap-n { color: inherit; }

/* THE FIND BAR AND THE CAPSULES STICK, AS ONE BLOCK (TEN-1819). The rail's card is what
   scrolls under the frame rule, and a search box that scrolled away on a long list would be
   a triage surface you have to scroll to the top of in order to triage with. They keep their
   own ground so rows pass behind them rather than through them.

   ONE STICKY WRAPPER AND NOT TWO STICKY SIBLINGS, and that is a measured correction rather
   than tidying. Each was sticky in its own right, with the capsule row offset by a
   hand-written `top: 3rem` meant to clear the bar above it — a number, and the wrong one: at
   this density the bar is shorter than 3rem, so sticky pushed the capsules DOWN to their
   stated offset. A sticky element does not reserve the space it is displaced into, so the
   capsule row sat on top of the first conversation in the list, AT REST, on every render.
   Invisible against a matching background and quite solid to a pointer — the browser gate
   found it by pressing the first row and landing on `DIV.conv-caps`.

   A wrapper at `top: 0` has no number in it to be wrong.

   AND IT ONLY STICKS WHERE THE CARD IS ITS OWN SCROLLER — which is the desktop frame, and
   nowhere else. Below 721px the frame lock is off by design (two tracks would be two
   unusable halves of a phone), so the PAGE scrolls and `.msg-inbox` is not a scrollport at
   all. A `position: sticky` with no scrolling ancestor resolves against the document: the
   bar detached from the list, floated at the top of the viewport while the app header
   scrolled away underneath it, and sat on the first row — the same defect as the one above,
   on the width that has the least room to spare. Measured at 375px: a press at the first
   row's centre hit `conv-caps`.
   Nobody asked for a sticky search on a phone, and a sticky header that covers rows is worse
   than none. So it is a desktop treatment, guarded by the same breakpoint the frame is. */
/* AND THE BLOCK GETS SIDE PADDING (TEN-1826, Mark's round-4 UAT: "the controls are flush").
   Measured rather than guessed: the card (`.msg-inbox`) pads by `--b-space-2`, and a ROW pads by
   `--b-space-2` again INSIDE its own anchor — so a row's name starts two steps in from the card
   edge while the search box, the Sort control and the capsules started at one. The controls were
   not flush to the window, they were flush to the ROWS, which is the misalignment that reads as
   sloppiness: a vertical edge that almost lines up is worse than one that does not.

   `padding-inline` on the wrapper and not a margin, because this element is the sticky ground: a
   margin would inset the ground with the controls and leave a 6px channel on each side where rows
   scrolled past in the open. */
.conv-railtop { padding-inline: var(--b-space-2); }
@media (min-width: 721px) {
  .conv-rail-body .conv-railtop {
    position: sticky; top: 0; z-index: 2; background: var(--surface-page);
  }
}

/* THE VIEWPORT LOCK, EXTENDED THROUGH THE NEW WRAPPERS (TEN-1016, TEN-1795).
   This is not optional decoration. The chain above keys on
   `body.app:has(.msg-screen)` and used to reach the pane as
   `.console-main > .b-component`; this layout puts .conv-layout and .conv-pane
   in between, so without these rungs the pane stops being height-bounded and
   hands its overflow back to the page — the composer walks off the bottom
   again, which is precisely the bug TEN-1016 fixed.

   min-height: 0 at EVERY rung, for the reason the original block states: a
   flex child only gets a bounded height if its parent has one, and the default
   `auto` lets a child grow past its parent. Same @supports guard, so a browser
   without :has() keeps the plain unbounded-but-correct fallback. */
/* THE LOCK NOW KEYS ON THE LAYOUT, NOT ON AN OPEN CONVERSATION (TEN-1819).

   `body.app:has(.msg-screen)` fires on /account/messages/{thread} and
   /account/rooms/{room} and NOT on /account/messages, because .msg-screen is the
   open pane's own element. So the resting state — the state a reader arrives in —
   had no frame at all: the page scrolled, the rail grew as tall as its rows, and
   the placeholder pane sat beside it at whatever height its two sentences needed.
   That is the misaligned edges, and it is why they looked worse with nothing open.

   `.conv-layout` is on all three states (messages-main, message-thread-main,
   room-main all wrap the two columns in it) and on nothing else, so it is the
   superset and the honest selector for "this page is the conversations frame".
   The `:has(.msg-screen)` block further up is left exactly as it is: it says the
   same things about the same elements, plus the pane-internal rules that belong to
   the chat and not to the frame.

   AND IT IS A DESKTOP RULE, GUARDED BY THE LAYOUT'S OWN BREAKPOINT. Below 721px
   (convDesktopMinWidth — the same number convopen.js tests and the one-column media
   query below uses) the rail and the pane are ONE column stacked, and there are no
   two tracks to make flush. Pinning the body to a viewport there would give a phone
   a 50/50 split between a list and a placeholder, each with its own scrollbar,
   which is two unusable halves of one screen. The phone keeps the page scroll it
   has; an OPEN conversation on a phone keeps the `:has(.msg-screen)` lock it
   already had, untouched. */
@media (min-width: 721px) {
@supports selector(:has(*)) {
  body.app:has(.conv-layout) {
    height: 100dvh; min-height: 0; overflow: hidden;
    display: flex; flex-direction: column;
  }
  /* THE 688px CAP DIES HERE. See the frame note at the head of this section: this
     is the one line that was making a third of the row 225px on a 1280px screen,
     and the same line the page's horizontal overflow was coming out of. 1280 is
     `.clshub`'s number, reused rather than invented — this book has two wide
     lenses already and a third width would be a third thing to keep in step.

     THE THIRD PLACE ARRIVED (TEN-1862) and the paragraph above was right about
     what it would cost, so the number is now `--b-lens-wide` in tokens.css and
     this line READS it rather than repeating it. The athlete Schedule's month grid
     is the third lens, and Mark's ask was explicitly for it to take the width this
     page takes — a claim two literals cannot keep. */
  body.app:has(.conv-layout) main.container {
    max-width: var(--b-lens-wide); flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
  }
  body.app:has(.conv-layout) .console { flex: 1 1 auto; min-height: 0; align-items: stretch; }
  body.app:has(.conv-layout) .console-main { display: flex; flex-direction: column; min-height: 0; }
  body.app:has(.conv-layout) .console-main > .b-component {
    display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
  }
  /* The foot would be below the fold of a pinned column and is hidden on the
     conversation screens already; the resting state joins them, or the frame is a
     viewport tall with a footer nobody can reach underneath it. */
  body.app:has(.conv-layout) .app-foot,
  body.app:has(.conv-layout) .judgefoot { display: none; }
  body.app:has(.conv-layout) .console-nav { position: static; align-self: flex-start; }

  body.app:has(.conv-layout) .conv-layout { flex: 1 1 auto; min-height: 0; }
  /* EACH TRACK SCROLLS ITS OWN CONTENT. The rail's HEAD stays put and its BODY is
     the scroller, so the title, the chevron and the "+" are always where the reader
     left them — a header that scrolled away would make the one control that creates
     anything a thing you have to scroll back up to find. min-height: 0 at every rung,
     for the reason the chat's own lock states: a flex child only gets a bounded
     height if its parent has one, and the default `auto` hands the overflow back to
     the page. */
  body.app:has(.conv-layout) .conv-rail {
    display: flex; flex-direction: column; min-height: 0; overflow: hidden;
  }
  /* THE ENGINE'S OWN INSTANCE WRAPPER IS A RUNG OF THIS CHAIN (measured, TEN-1819).
     `.conv-rail` has exactly ONE child and it is not the plate's markup: the rail is
     a component mount, so `<div class="b-component">` sits between the column and
     everything the plate draws. It is `display: block` by default, so the height
     stopped dead here and the list's card ended 160px short of the floor with the
     column around it perfectly full-height — which is precisely the "floating card"
     look the frame rule is about, surviving the fix that was supposed to remove it.
     The chat pane's own lock already carries this rung for the same reason and says
     so ("the engine's instance wrapper sits between the console column and the
     plate, so it is part of the chain whether the author sees it or not"). */
  body.app:has(.conv-layout) .conv-rail > .b-component {
    display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
  }
  body.app:has(.conv-layout) .conv-rail-body {
    flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden;
  }
  /* AND THE COLLAPSE HAS TO BE SAID AGAIN IN HERE (measured, TEN-1819).
     `.conv-railbox:checked ~ .conv-rail-body { display: none }` up in the chevron's
     own block is TWO classes; the rule directly above is `body.app` + `:has()` + a
     class, which is three — so the frame quietly overruled the chevron and a
     "collapsed" rail kept its whole list, at `display: flex`, and GREW: the track
     is `auto` when collapsed, and `auto` sizes to the max-content of a list that
     was never hidden. Pressing the chevron made the column wider, which is the
     opposite of the control's entire purpose.
     Restated here rather than weakened there: this block owns these elements'
     layout now, so it owns telling them when not to have any. */
  body.app:has(.conv-layout) .conv-railbox:checked ~ .conv-rail-body { display: none; }
  /* THE LIST'S CARD IS THE SCROLLER, not the body around it. Two reasons, and the
     second is the load-bearing one: the archived-rooms shelf and the refusal note
     are SIBLINGS of this card inside the body, and a scroller one level up would
     carry them off the bottom of the column — the shelf is a foot, and a foot that
     scrolls away is a link nobody finds twice. Scrolling the card also gives the
     sticky find bar and capsule row a scroll container of their own to stick to. */
  body.app:has(.conv-layout) .conv-rail-body .msg-inbox {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  }
  body.app:has(.conv-layout) .conv-pane {
    display: flex; flex-direction: column; min-height: 0;
  }
  /* THE BOTTOM EDGES (TEN-1826, Mark's round-4 UAT: "bottoms off") — MEASURED ON THE RUNNING
     BOOK, which is the only place this was visible.
     `.msg-inbox, .msg-pane` share one card rule up at the top of this file and it ends
     `margin-bottom: var(--b-space-3)` — right for a card on a scrolling page, wrong inside a
     viewport-bounded frame where the track's floor IS the page's. The rail's copy has been
     overridden to 0 since TEN-1819 (`.conv-rail .msg-inbox`); the pane's never was, so the chat
     card stopped 12px above the list card on every render of every conversation.
     Measured at 1867x594 on /account/rooms/room-demo-1: the two TRACKS are 75→578 exactly, the
     rail's card reaches 578, and the pane's card stopped at 566. One inherited margin, 12px, on
     the one edge Mark could see it on. */
  body.app:has(.conv-layout) .conv-pane .msg-pane { margin-bottom: 0; }
  /* ONLY THE COMPONENT THAT DRAWS THE SCREEN TAKES THE SLACK (measured, TEN-1819).
     The room state mounts TWO components in this pane: the room, and `people-pick`,
     which the content block's own comment says "renders nothing" — it exists to
     ANSWER a re-fetch from peoplepick.js. A blanket `flex: 1 1 auto` handed that
     empty mount an equal share of the growth, and it was taking 78px off the bottom
     of the conversation on every room page: the pane reached the floor and the chat
     inside it stopped short, with nothing on screen to explain the gap. Pre-existing
     and invisible — an empty div has no outline — and it is the frame, so it is
     fixed here. `:has(.msg-screen)` names the one that is the screen; everything
     else in this pane sizes to its own content. */
  body.app:has(.conv-layout) .conv-pane > .b-component {
    display: flex; flex-direction: column; flex: 0 1 auto; min-height: 0;
  }
  body.app:has(.conv-layout) .conv-pane > .b-component:has(.msg-screen) { flex: 1 1 auto; }
  /* The resting pane has no component to hand the height to — it IS the
     placeholder — so it takes the slack itself and centres in the whole track,
     which is what makes its bottom edge flush with the list's. */
  body.app:has(.conv-layout) .conv-pane-empty { flex: 1 1 auto; }
}
}

/* ONE COLUMN ON A PHONE, and the rule picks which one.
   Two columns at 400px would be two unusable columns. When a pane is open the
   pane IS the page and the rail steps aside; the way back to the list is the
   conversation's own Back control, which was already there for exactly this.
   With nothing open there is no .msg-screen, so the rail is what remains —
   which is the old Messages page, unchanged, for a reader on a phone.

   This is also why the resume affordance must never auto-follow at this width:
   arriving and being thrown straight into a conversation would leave a phone
   reader with no list to have chosen from. */
@media (max-width: 720px) {
  .conv-layout { grid-template-columns: minmax(0, 1fr); gap: var(--b-space-3); }
  .conv-rail, .conv-pane { width: 100%; max-width: none; }
  /* THE BUSY PANE STEPS INTO THE STACK AT THE SAME MOMENT THE PRESS LANDS
     (TEN-1823). `.msg-screen` only exists once the answer has arrived, so with the
     first selector alone a phone reader pressing a row got the list, unchanged,
     with a skeleton growing underneath it — the list and the conversation on one
     screen, which is the thing "never both" forbids, arriving in the window the
     reader is most likely to be looking at. The transition is the acknowledgment
     (the interactivity SOP's rule 1: the view changes BEFORE the work begins), so
     the stack moves on the press and the skeleton is what the new screen holds
     until the conversation lands in it. */
  .conv-layout:has(.msg-screen) .conv-rail,
  .conv-layout:has(.conv-pane-busy) .conv-rail { display: none; }
  .conv-pane-busy { min-height: 60vh; }
  /* "NEVER BOTH" CUTS THE OTHER WAY TOO (TEN-1823, Mark's ruling). The rule above
     takes the list away once a conversation is open; nothing took the PLACEHOLDER
     away while the list was open — so the resting state on a phone was the whole
     conversation list with a dashed box under it saying "No conversation open. Pick
     someone or a room from the list to read it here", which is a caption explaining
     the screen it is sitting on, below the fold, to a reader already doing it.

     A stack has one screen at a time. Above the breakpoint the placeholder is
     answering a real question — there are two tracks and one of them is empty —
     and it stays exactly as it is. Here there is one track, it is the list, and
     the honest render of "nothing is open" is nothing (K30: absent, not dead). */
  .conv-pane-empty { display: none; }
  /* THE COLLAPSE CHEVRON IS ABSENT AT THIS WIDTH (TEN-1817) and that is not tidying.
     There are no two columns to redistribute here — the rail IS the page when nothing is
     open, and it is already gone when something is. A chevron that hid the only thing on
     the screen would be a control whose whole effect is a blank page. */
  .conv-railtoggle { display: none; }
  .conv-railbox:checked ~ .conv-rail-body { display: block; }
  /* AND THE PICKED COLUMN IS STILL INSIDE THE VIEWPORT LOCK (TEN-1829, Mark's UAT-M8 on
     v0.1.244, Android Chrome: the chat pane runs off the bottom of the screen and the
     composer cannot be reached — the page will not scroll to it).
     MEASURED, and the unit was not the fault. `body.app:has(.msg-screen)` has been
     `height: 100dvh` since TEN-1016 and dvh is the right unit; the break is that the CHAIN
     under it stops. The lock reaches `.console-main > .b-component`, and TEN-1795/TEN-1819
     put `.conv-layout` and `.conv-pane` between that wrapper and the screen — rungs the
     `:has(.conv-layout)` block adds back ONLY above 721px. Below it the two new rungs are a
     content-sized grid (`flex: 0 1 auto`, `min-height: auto`) and a plain block, so the pane
     sizes to its MESSAGES: measured at 375x812 on /account/messages/thr-demo-1 with a
     realistic history, `.msg-screen` was 3290px tall inside an 812px body, with
     `.msg-thread` at `max-height: none` (the lock's own line, correct only when a parent
     bounds it) never becoming a scroller at all — 3067px of content in a 3067px box.
     And the body it overflows is `overflow: hidden`, which does not scroll for a READER
     (programmatic scroll still moves it, which is why this measures clean and feels broken):
     the composer sat ~2400px below the fold with no way to reach it. That is exactly the
     defect TEN-1016 fixed, arriving again by a route the desktop fix did not cover.
     So: the same four rungs the desktop block carries, at this width, for the one column the
     stack picked. `min-height: 0` on every one of them, because a flex child is only bounded
     if its parent is and the default `auto` hands the overflow back to the page — and the
     `0 1 auto` / `1 1 auto` split on the component wrapper for the reason the desktop copy
     states: a room mounts TWO components here and only the one drawing the screen may take
     the slack. Scoped to `:has(.msg-screen)`, so the resting list keeps its page scroll. */
  body.app:has(.msg-screen) .conv-layout { flex: 1 1 auto; min-height: 0; }
  body.app:has(.msg-screen) .conv-pane {
    display: flex; flex-direction: column; min-height: 0;
  }
  body.app:has(.msg-screen) .conv-pane > .b-component {
    display: flex; flex-direction: column; flex: 0 1 auto; min-height: 0;
  }
  body.app:has(.msg-screen) .conv-pane > .b-component:has(.msg-screen) { flex: 1 1 auto; }
}

/* THE BACK BUTTON IS A NARROW-WIDTH CONTROL (TEN-1817, Mark's UAT: "remove the back
   button above the chat pane").

   Above the breakpoint the rail is sitting beside this pane, so a Back button pointing at
   a list already on screen is chrome saying what the page shows — which is what Mark was
   looking at. Below it, the rule seven lines up hides the rail outright while a pane is
   open and this control is the ONLY way back to the list; its own comment up there says
   so. So it goes where it is redundant and stays where it is the door.

   HIDDEN IN CSS AND STILL RENDERED, deliberately. It is a plain anchor to a real address:
   a reader who reaches it another way — a narrow window widened, a zoom, a stylesheet that
   did not load — still lands on their conversations. Deleting the markup would make the
   breakpoint the thing that decides whether the way back exists at all.

   SCOPED TO `.conv-pane`, WHICH IS NOT COSMETIC. `.msg-back-btn` is a shared class and the
   My Lists composer (list-compose.plate.html) wears it on a single-column page with no rail
   beside it — an unscoped rule here would have deleted that page's only way back to the
   lists, on desktop, as a side effect of a conversations ruling. */
@media (min-width: 721px) {
  .conv-pane .msg-back-btn { display: none; }
}

/* ── THE ktcalnav FAMILY (K34, TEN-1793) ─────────────────────────────────
   A CALENDAR'S FRAME SELECTOR: the words that say which slice you are looking
   at, made pressable, over an attached panel of the other slices.

   ATTACHED, NOT FLOATING, and the construction is K15's own: the trigger and
   the panel are ONE object with one outline and one shadow, the joining edge
   borderless so the outline cannot double, SQUARE AT THE SEAM and ROUNDED AT
   THE SILHOUETTE (Mark's corner rule of record).

   THE PANEL IS POSITIONED AGAINST THE TRIGGER and its collision behaviour is
   SPEC'D rather than left to a default: left edges align, excess grows RIGHT,
   and at the container's right edge the panel flips to align its right edge
   instead. There is no JS placement here — the panel is a positioned child of
   the nav, so it cannot be left behind by a scroll (the chats/rooms off-screen
   scar), and the flip is a `:last-child`-free structural rule on the nav's own
   alignment class rather than a measurement.

   HOVER AND SELECTED ARE SEPARATE CHANNELS (K15's channel law). Hover owns the
   SURFACE — full accent ground, ink-on-primary — which is the wattage that tells
   twelve cells apart under a pointer. Selected owns the ACCENT INK and a quiet
   tint, and it survives hover because the two never write the same property.

   TODAY IS ACCENT INK AT TEXT WEIGHT and never a ground: K29 allows one live
   treatment per page and the grid below this control already spends it. */
.ktcalnav { position: relative; display: inline-flex; align-items: center; gap: 2px; }
.ktcalnav-step { flex: 0 0 auto; }
/* THE CURRENT SETTING IS THE PRESS TARGET. It reads as the label it replaced —
   the words are the same weight and the same colour — and it earns its
   affordance from the caret and the hover, not from a box that would put a
   third button between the two chevrons. */
.ktcalnav-now {
  display: inline-flex; align-items: center; gap: var(--b-space-1);
  box-sizing: border-box; min-height: var(--control-h);
  padding: 0 var(--b-space-2);
  color: var(--text-primary); text-decoration: none;
  border: 1px solid transparent; border-radius: var(--control-r);
  background: transparent; cursor: pointer;
}
.ktcalnav-now:hover, .ktcalnav-now:focus-visible {
  border-color: var(--border-strong); background: var(--surface-raised);
}
.ktcalnav-label { text-decoration: none; }
.ktcalnav-caret { font-size: var(--b-text-xs); color: var(--text-muted); }
.ktcalnav-now:hover .ktcalnav-caret { color: var(--text-primary); }
/* THE SEAM. Open, the trigger keeps the panel's outline and loses the edge they
   share — one object, one line. */
.ktcalnav-open > .ktcalnav-now {
  border-color: var(--role-primary); background: var(--surface-raised);
  border-bottom-color: var(--border-subtle);
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
}
.ktcalnav-open > .ktcalnav-now .ktcalnav-caret { transform: scaleY(-1); }

/* THE PANEL. A positioned child of the nav rather than a body-level layer,
   because this control is never inside a scrolling grid — it is a page's own
   frame selector, at the top of the reading column — and a child cannot be
   left behind by a scroll it does not have. */
.ktcalnav-panel {
  position: absolute; top: 100%; left: 0; z-index: 30;
  box-sizing: border-box; min-width: 14rem; padding: 6px;
  background: var(--surface-raised); color: var(--text-primary);
  border: 1px solid var(--border-strong); border-top: 0;
  border-radius: 0 0 var(--control-r) var(--control-r);
  box-shadow: var(--ktsel-lift);
}
/* THE FLIP. At the container's right edge the panel aligns its RIGHT edge
   instead, which is the alignment clause spelled as a class a composer can set
   rather than as a measurement a script has to take every frame. */
.ktcalnav-right > .ktcalnav-panel { left: auto; right: 0; }

/* THE PANEL'S OWN FRAME ROW — the year in month mode, the month in date mode,
   with its own steps so reaching next March never means close, step, re-open. */
.ktcalnav-frame {
  display: flex; align-items: center; gap: var(--b-space-1);
  padding: 0 var(--b-space-1) var(--b-space-1);
}
.ktcalnav-ftitle {
  flex: 1 1 auto; min-width: 0; text-align: center;
  font-weight: var(--b-weight-bold); font-size: var(--b-text-sm);
}
/* THE TITLE'S PICK STATE (TEN-1868): a real anchor where the composer names a
   month-selecting address (date mode only — classcalnav.go's classHubWeekNav).
   It reads exactly as the inert span did at rest, inheriting `.ktcalnav-ftitle`
   above and overriding only the browser's own link face — the affordance is
   the hover/focus treatment, matching `.ktcalnav-fstep` beside it, not a new
   colour that would tell a reader "this became a link" before they touch it. */
.ktcalnav-ftitle-pick { color: inherit; text-decoration: none; border-radius: var(--b-radius-sm); }
.ktcalnav-ftitle-pick:hover, .ktcalnav-ftitle-pick:focus-visible {
  background: var(--role-primary); color: var(--ink-on-primary);
}
.ktcalnav-fstep {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 1.75rem; height: 1.75rem; border-radius: var(--b-radius-sm);
  color: var(--text-muted); text-decoration: none;
}
.ktcalnav-fstep:hover, .ktcalnav-fstep:focus-visible {
  background: var(--role-primary); color: var(--ink-on-primary);
}
.ktcalnav-x { flex: 0 0 auto; }

/* THE COLUMN HEADS — date mode only, and they are the grid's own dates
   formatted rather than seven typed words (the standing rule on this train:
   a hand-typed "MON" over a column of Tuesdays is the failure a schedule has). */
.ktcalnav-heads {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px;
  padding: 0 var(--b-space-1) 2px;
  font-family: var(--b-font-mono); font-size: var(--b-text-xs);
  color: var(--text-muted); text-align: center;
}

/* THE GRID. Three across in month mode, seven in date mode — and the JS reads
   its step off these very columns rather than assuming one, so the Down key
   moves a row in both. */
.ktcalnav-grid { display: grid; gap: 2px; padding: 0 var(--b-space-1) var(--b-space-1); }
[data-ktcalnav-mode="month"] .ktcalnav-grid { grid-template-columns: repeat(3, 1fr); }
[data-ktcalnav-mode="date"] .ktcalnav-grid { grid-template-columns: repeat(7, 1fr); }
/* 44px TALL CELLS in month mode, the blessed row height; the date grid's cells
   are square and smaller because 42 of them at 44px is a panel taller than the
   page it opens over — a month calendar's cell is a different unit from a list
   row, which is the K15 density clause read honestly. */
.ktcalnav-opt {
  display: flex; align-items: center; justify-content: center;
  box-sizing: border-box; min-height: 44px;
  border-radius: var(--b-radius-sm);
  color: var(--text-primary); text-decoration: none;
  font-size: var(--b-text-sm); cursor: pointer;
}
[data-ktcalnav-mode="date"] .ktcalnav-opt {
  min-height: 32px; font-family: var(--b-font-mono); font-size: var(--b-text-xs);
}
/* HOVER OWNS THE SURFACE. */
.ktcalnav-opt:hover, .ktcalnav-opt:focus-visible {
  background: var(--role-primary); color: var(--ink-on-primary);
}
/* A CELL WITH NO ADDRESS IS DRAWN AND NOT PICKABLE (K11) — never absent, which
   in a year grid would read as a rendering bug. */
.ktcalnav-off { color: var(--text-muted); opacity: 0.45; cursor: default; }
.ktcalnav-off:hover { background: transparent; color: var(--text-muted); }

/* THE UNPICKABLE CELL'S RULES COME FIRST, BEFORE THE SELECTED STATE'S, and the
   order is load-bearing rather than tidy: `-off` and `-on` are mutually exclusive
   by construction, but a stylesheet cannot know that — so TestKitHoverNeverOutranks
   State reads `.ktcalnav-off:hover` as a hover rule that could beat the selected
   state, and the fix it asks for is that the state's own hover arm be the LATER
   rule at the same weight. See the arm below.

/* SELECTED OWNS THE INK, A QUIET TINT AND `aria-current`. */
.ktcalnav-on {
  background: var(--role-primary-soft); color: var(--role-primary-ink);
  font-weight: var(--b-weight-bold);
}
/* HOVERED-SELECTED IS THE HOVER SURFACE, AND IT IS DECLARED (the TEN-1702
   construction, enforced by TestKitHoverNeverOutranksState). K15's channel law
   says hover owns the surface and selected owns the ink — but "selected keeps its
   ink" cannot be left to a rule that a hover rule of equal weight sits above, so
   the state names its own hover arm at the SAME weight rather than relying on
   source order between two families. The BOLD stays, which is the channel a hover
   never writes, so a hovered current month is still legible AS current. */
.ktcalnav-on:hover, .ktcalnav-on:focus-visible {
  background: var(--role-primary); color: var(--ink-on-primary);
}
/* TODAY, at text weight and never as a ground. */
.ktcalnav-today { text-decoration: underline; text-underline-offset: 3px; }
/* A DAY OUTSIDE THE FRAME still works and still says it is not this month. */
.ktcalnav-muted { color: var(--text-muted); }

/* ── THE ROW RUNG, r2 (TEN-1793: Mark's class01–class05 annotations) ──────
   The entity card's LIST-ROW rung grew four additive slots. Every one of them
   is a data-presence conditional in the plate, so a mount that passes none of
   them renders exactly the row it always did — which is what let this land
   without touching the session lists, the venue listing or the storefront.

   THE ROW IS STILL ONE WRAPPING FLEX LINE. None of these introduce a second
   layout: the ident is a flex pair where the lead used to sit, the lines and
   the pills stack INSIDE the middle cell, and the tails stack inside the tail
   cell. That is what keeps the row's columns aligned down a long list, which is
   the only thing a listing does that a set of cards does not. */

/* class05 — THE FOLLOWER COUNT, AFTER THE TITLE. A fact, not a control: it sits
   UNDER the row's stretched lead, so a press anywhere on it opens the class. The
   glyph is 14px rather than the kit's 18 because it rides a line of 14px text
   and an icon taller than its line is a badge. */
.ktrow-ident { display: flex; align-items: center; gap: 8px; width: 170px; }
.ktrow-ident > .ktrow-lead { width: auto; min-width: 0; }
.ktrow-count {
  display: inline-flex; align-items: center; gap: 3px; flex: 0 0 auto;
  color: var(--text-muted); font-size: var(--b-text-xs);
  font-family: var(--b-font-mono);
}
.ktrow-cic { width: 14px; height: 14px; }

/* class01 — VENUE THEN RECURRENCE, AS LINES UNDER THE TITLE. Two facts of
   different kinds, so two lines: a bullet-joined sentence made a reader parse
   prose to find a place. */
.ktrow-mid:has(> .ktrow-line) { display: flex; flex-direction: column; gap: 2px; }
.ktrow-line { display: block; min-width: 0; }
/* THE FIRST LINE IS THE PLACE and it carries the row's ink, because it is the
   fact the ticket moved to the top. The rest stay the muted middle. */
.ktrow-line:first-child { color: var(--text-primary); }

/* class04 — READONLY TAG PILLS IN THE CENTRE COLUMN, and the worded overflow.
   The pills are the kit's own `.ktpill` at a listing's density — no hover, no
   press, because nothing on this row edits a class. */
.ktrow-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.ktrow-pills > .ktpill {
  height: 1.35rem; padding: 0 9px; cursor: default;
  font-weight: var(--b-weight-normal);
}
/* "and 3 more…" — a WORD, with the full set in its title. It is the same answer
   K32's day cell gives an overfull day: the row does not grow, it says so. */
.ktrow-pmore {
  align-self: center; color: var(--text-muted);
  font-size: var(--b-text-xs); cursor: help;
}

/* class02 — THE RIGHT COLUMN STACKS NEXT OVER ENDS. Two dates a reader compares
   belong in one column in one grammar; they used to be a tail and a chip. */
.ktrow-tails {
  display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
  margin-left: auto; text-align: right;
}

/* THE 90s UNDERLINE IS GONE FROM THE CARD FAMILY (TEN-1793, Mark's last
   annotation: "remove the title underline on hover — sweep the card family, not
   one card").

   THE ARGUMENT IS K15's CHANNEL LAW, not taste. A card whose lead is a link
   already answers the pointer with its own SURFACE — `.ktrow-linked:hover`
   repaints the whole card and moves its border — and `text-decoration` on top of
   that is a SECOND affordance for one act, drawn in the one channel that also
   means "this is prose you can click". The underline is right for a link inside a
   sentence, which is why `.link` keeps it everywhere else; it is wrong for the
   title of an object, which is a surface.

   IT IS THE FAMILY AND NOT THE ROW. The row's lead, the row's eyebrow and the
   hub rung's eyebrow are the three anchors a kit CARD wears as its own identity,
   and all three lose it — a sweep that fixed the listing and left the hub would
   be the drift this rule exists to close. Act links (`.ktrow-link`,
   `.ktpcard-go`) and mentions inside prose keep theirs: they are not titles. */
.ktrow-lead:hover, .ktrow-lead:focus-visible,
.ktrow-eyebrow .link:hover, .ktrow-eyebrow .link:focus-visible,
.kthub-eyebrow:hover, .kthub-eyebrow:focus-visible { text-decoration: none; }

/* ── THE +EVENT CHOOSER'S GRID (TEN-1793: "2 columns, and make it work on
      mobile too") ───────────────────────────────────────────────────────────
   The chooser's cards were a vertical list at every width. Four large cards in
   one column is a screenful of scrolling to compare four things, which is the
   one job a chooser has.

   TWO COLUMNS AT EVERY WIDTH, INCLUDING THE PHONE, and the ticket's own reason:
   these are large press targets, so two across at 380px still clears the 44px
   floor comfortably — a card here is ~170px wide and several lines tall. What a
   narrow card cannot carry is the PARAGRAPH and the "e.g." line; those are the
   parts that get dropped below the fold of a reading width, not the card.

   THE EXAMPLES LINE GOES FIRST, THEN THE DESCRIPTION. Both are declared data
   (eventkind's chooser records) and both are illustrations of a label that is
   still fully legible — which is what makes them the honest things to drop,
   rather than truncating the sentence that says what picking commits you to. */
.ktchoices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--b-space-3); }
.ktchoices > .ktchoice { min-width: 0; }
@media (max-width: 560px) {
  .ktchoices { gap: var(--b-space-2); }
  .ktchoices > .ktchoice { padding: var(--b-space-3); }
  .ktchoices .ktchoice-eg { display: none; }
}
/* AT PHONE WIDTH THE SENTENCE TRUNCATES RATHER THAN VANISHING, which is the
   ticket's own word for it. Three lines is enough to reach the verb in every one
   of the four declared paragraphs; the clamp is on the BODY and never on the
   title, because a chooser card whose name was cut would be a card you cannot
   identify. The card stays well over the 44px floor — it is a title plus three
   lines — so the two-column grid holds at 380px. */
@media (max-width: 420px) {
  .ktchoices .ktchoice-body {
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

/* THE CONSOLE SCHEDULE'S ROW IS THE WAY IN (TEN-1812, Sched02 + Sched03), and
   this replaces the two admin text links that used to sit in the trailing slot.

   `.sfev-adm` is gone with them. It styled "Class" and "Session" at the link
   tier, which was the right tier for two navigations and the wrong CONTROL: the
   row was 900 pixels wide and pressable through about sixty of them, and a text
   link is what the hover-underline sweep removed from every other member of this
   family. So the NAME is the one anchor and this stretches it over the card —
   TEN-1602's pattern, spelled the way `.ktrow-linked` spells it one family over,
   because a reader who knows one list in this book knows them all.

   ONLY A ROW WITH AN ADDRESS GETS ANY OF IT. `.sfev-row-linked` is the plate's
   marker for "this row has a way in"; a row whose name is a `<strong>` gets no
   overlay and no hover, because a row that lights up under the pointer and does
   nothing when pressed is the same broken promise the kit row's rule names.

   AND NOTHING IS LIFTED ABOVE THE OVERLAY, which is Mark's "chips not lifted".
   After Sched03 the admin row holds no second address — the occupancy and the
   facts beside it are FACTS, and a fact that intercepted the press would leave
   the card with dead patches, the exact defect this rule exists to remove. */
.sfev-row-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.sfev-row-linked:hover { background: var(--hover); border-color: var(--border-strong); }
.sfev-row-linked .sfev-lead[href]::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
/* The focus ring belongs to the ROW once the row is the target: a name-width
   lead with a card-wide hit area would otherwise outline a fraction of what a
   keyboard press activates. The kit row's own reasoning, unchanged. */
.sfev-row-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.sfev-row-linked .sfev-lead[href]:focus-visible { outline: none; }
/* THE LEAD IS NOT UNDERLINED AND NEVER WAS A `.link`. It is a title that happens
   to be pressable — the row's hover is what says so, which is the whole point of
   moving off the text-link tier. It keeps the name's own weight so a linked row
   and an unlinked one read identically at rest. */
.sfev-lead { color: inherit; text-decoration: none; font-weight: var(--b-weight-semibold); }
/* A cancelled row that is still linked keeps its dimming: hover is a pointer's
   position, never a change of state. */
.sfev-row-off.sfev-row-linked:hover { border-color: var(--border-subtle); }

/* THE ADMIN ROW'S OCCUPANCY COLUMN (TEN-1812, Sched04). It is the same clause the
   public sub-line composes, moved to the row's right boundary so that a fortnight
   of "3 of 14 spots left" is a column a reader runs their eye down rather than the
   tail of a sentence whose length varies with the venue's name. Tabular figures,
   because a column of numbers that shifts by a digit is not a column. */
.sfev-avail {
  flex: none; font-size: var(--b-text-sm); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* THE BOOKING STAGE STACKS ABOVE IT (TEN-1812, Sched05). Two facts at the row's
   right boundary, so the boundary becomes a column of two lines rather than a row of
   two phrases: `.sfev-end` is a flex line everywhere else on this page, and the admin
   row is the one dressing that puts two stacked readings in it.

   THE STACK IS END-ALIGNED so both lines hang off the row's right edge and a reader
   scanning a fortnight reads two straight columns. `column` only where a stage is
   actually present, via `:has()`, which keeps every other consumer of `.sfev-end` —
   the public Book button, the booked pill, the Full fact — on the flex line it has
   always been.

   THE STAGE IS NOT A STATUS COLOUR. It is grey like the occupancy under it: the four
   stages are all ordinary facts about a clock, and tinting "Closed" red would make a
   class that simply stopped taking bookings read as a failure. K29's one live
   treatment is the in-progress band's and this is not it. */
.sfev-end:has(.sfev-stage) { flex-direction: column; align-items: flex-end; gap: 2px; }
.sfev-stage {
  flex: none; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
  color: var(--text-muted);
}

/* THE CONSOLE SCHEDULE'S WRAPPER (TEN-1793). One block, no styling of the
   schedule itself: the bands, rows, head and calendar inside it are sportscore's
   own `sfev-*` and `ktcal-*` families and this cover already skins them — a rule
   here that reached into them would be the console restyling a shared family,
   which is the thing K32's "calendars compose rungs, they never restyle them"
   forbids one component along. */
.orgc-schedule { margin-top: var(--b-space-4); }
/* ── PIN, CATEGORIES AND THE SIX ORDERINGS (TEN-1795 PR 5b) ─────────────────
   The rail gained three things: a sort control under the search box, a strip of
   per-row controls, and the reader's own tags as chips.

   THE ROW CONTROLS SIT UNDER THE ROW, not inside it, and that is a constraint
   rather than a layout choice: the row is an anchor, a form cannot live inside
   one, and an anchor inside one is unclickable. Same reason the avatar cluster
   leads the wrap. */
.conv-sort { display: flex; align-items: center; gap: var(--b-space-1); flex-wrap: wrap; }
.conv-sort .acct-label { flex: none; }
.conv-sort .kit-select { flex: 1 1 8rem; min-width: 0; }
.conv-sort-btn { flex: none; }
/* THE SORT BUTTON RETIRES WHEN THE LIVE REORDER IS ACTUALLY BOUND (TEN-1817) — the class
   comes from listfilter.js, never from the server, which is wireStaff's rule and the reason
   it exists: a server-rendered class would take the submit off a page whose script never
   ran and leave the control with no working arm at all, the fallback broken by the
   enhancement's own styling. */
.conv-sortlive .conv-sort-btn { display: none; }
/* The standing sentence under the control. Quiet, and always there: a reader who
   pins something and then sorts by name has every right to wonder whether the pin
   still means anything, and the answer should not depend on trying it. */
.conv-sort-note { margin: 0 0 var(--b-space-1); color: var(--text-muted); }

/* ── THE ROW'S ONE CONTROL (TEN-1817) ──────────────────────────────────────
   5b put a chip strip, an activity line, a pin star and a Tags button UNDER every row.
   Mark's finding was that the rail had become a list of cards, and the cure is the same
   one K27 applies to a listing head: everything that is not identity travels, and one
   ellipsis stands where two controls were.

   THE CONTROL SITS ON THE ROW'S OWN LINE NOW, not under it. It is one 28px target rather
   than a wrapping strip, so it fits beside the timestamp — which is also what stops the
   rail from being a list where every entry is two rows tall and half as many
   conversations fit on a screen.

   IT IS STILL OUTSIDE THE ROW'S ANCHOR, and that is a constraint rather than a choice:
   the row is an anchor, a form cannot live inside one and an anchor inside one is
   unclickable. Same reason the avatar cluster leads the wrap. */
/* ── AND ON DESKTOP IT GIVES UP ITS COLUMN (TEN-1819, Mark's UAT) ──────────
   The ellipsis had a reserved seat beside the timestamp on every row, which is a
   column of chrome down a third of the screen paid for by every row whether or not
   anybody ever presses one. The corner shows the STAMP at rest; the ellipsis fades
   in OVER that corner on hover — and on `:focus-within`, which is not a nicety: a
   control that only appears under a pointer is a control a keyboard cannot reach,
   and a Tab into the menu that revealed nothing would be K12's worst case.

   THE STAMP FADES AS THE ELLIPSIS ARRIVES, so the two never occupy the corner at
   once. The PIP does not — it sits on its own line underneath and stays visible the
   whole time, because "this one owes you a look" is the fact the list exists for.

   AND IT STAYS OPEN WHEN IT IS OPEN. `:has(.conv-menu-box:checked)` holds the
   ellipsis visible while its own panel is up, so a pointer that drifts off the row
   to reach an item does not delete the control it came from. Guarded, with
   `:focus-within` as the fallback where `:has()` is missing — clicking a label
   focuses its checkbox, so the degraded path is the same behaviour by a different
   route rather than no behaviour.

   MOBILE KEEPS THE PERSISTENT BUTTON, and that is mandatory rather than a taste:
   TOUCH HAS NO HOVER. A control that only exists on hover does not exist on a
   phone. It is also free there — the media query below puts the rail at full width,
   where a 28px target at the row's edge costs nothing. */
.conv-row-acts { display: flex; align-items: center; flex: none; padding-right: var(--b-space-1); }
@media (min-width: 721px) {
  /* `.msg-row-wrap >` IS LOAD-BEARING HERE, and it was measured: the whole-row-press
     block up in the .msg-row family lifts this element above the stretched link with
     `.msg-row-wrap > .conv-row-acts { position: relative; z-index: 1 }`, which is two
     classes. A bare `.conv-row-acts` here is one, and a media query adds no
     specificity — so the corner rule lost, the ellipsis went transparent but kept its
     seat, and the column it was supposed to give up was still being paid for on every
     row. Invisible in a diff and invisible on screen; only a measurement found it. */
  .msg-row-wrap > .conv-row-acts {
    position: absolute; top: 2px; right: 2px; padding-right: 0; z-index: 2;
    opacity: 0; pointer-events: none; transition: opacity 120ms ease;
  }
  .msg-row-wrap:hover > .conv-row-acts,
  .msg-row-wrap:focus-within > .conv-row-acts { opacity: 1; pointer-events: auto; }
  .msg-row-wrap:hover .conv-row-when,
  .msg-row-wrap:focus-within .conv-row-when { opacity: 0; }
  .conv-row-when { transition: opacity 120ms ease; }
  @supports selector(:has(*)) {
    .msg-row-wrap:has(.conv-menu-box:checked) > .conv-row-acts { opacity: 1; pointer-events: auto; }
    .msg-row-wrap:has(.conv-menu-box:checked) .conv-row-when { opacity: 0; }
  }
}

/* THE MENU. The avatar menu's construction, one scale down: a checkbox nobody sees, a
   label that is the button, and a panel positioned against the wrap.

   THE PANEL IS A POSITIONED CHILD and not a body-level layer, which is the opposite of
   K15's choice for a dropdown and for the reason K34's own note gives: `.conv-rail` is a
   scrolling column, and a FIXED panel measured once would be left behind by the very next
   scroll (the chats/rooms off-screen scar). A child scrolls with what it belongs to. The
   cost is that the rail's `overflow-y: auto` can clip it, which is what the flip below is
   for — the panel opens toward the rail's interior rather than off its edge. */
.conv-menu { position: relative; display: inline-flex; flex: none; }
.conv-menu-box { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.conv-menu-btn {
  padding: 0 var(--b-space-1); line-height: 1; min-height: 24px;
  color: var(--text-muted); cursor: pointer;
}
.conv-menu-btn:hover { color: var(--text-primary); }
/* The ring is painted from the checkbox's focus, because the checkbox is what takes it. */
.conv-menu-box:focus-visible + .conv-menu-btn {
  outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px;
}
.conv-menu-panel { display: none; }
.conv-menu-box:checked ~ .conv-menu-panel {
  display: flex; flex-direction: column;
  /* RIGHT-ALIGNED AND OPENING DOWNWARD, spec'd rather than left to a default (K15's
     collision clause, applied to this family's own geometry). The trigger is at the row's
     right edge, so a panel growing rightward would leave the rail; it grows LEFT. */
  position: absolute; top: 100%; right: 0; z-index: 30;
  box-sizing: border-box; min-width: 12rem; padding: 4px;
  background: var(--surface-raised); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--control-r);
  box-shadow: var(--ktsel-lift);
}
/* A row near the FOOT of the rail opens upward, because a panel that opened down there
   would be clipped by the rail's own overflow (which the viewport lock turns on whenever a
   pane is open). Structural rather than measured: a script measuring every frame is what
   K34 refused, and the last few rows of a list are exactly where the problem is.

   `-n+4` AND NOT `-n+3` FOR THE LAST THREE ROWS, because `:nth-last-child` counts among ALL
   of `.msg-inbox`'s children and the none-line ("No conversation matches…") is rendered
   unconditionally after the last row. So the final row is the second-last CHILD, and the
   obvious `-n+3` reaches two rows rather than three. Counting siblings that are not rows is
   the trap this comment exists to stop the next reader re-walking into. */
.msg-row-wrap:nth-last-child(-n+4) .conv-menu-box:checked ~ .conv-menu-panel {
  top: auto; bottom: 100%;
}
/* EVERY ROW IS AN ANCHOR OR A SUBMIT, never a `<button type="button">` — so they are
   given one appearance here rather than each wearing its element's default. */
.conv-menu-form { margin: 0; display: block; }
.conv-menu-item {
  display: block; width: 100%; box-sizing: border-box; text-align: left;
  padding: 6px var(--b-space-2); min-height: 28px;
  font: inherit; color: var(--text-primary); text-decoration: none;
  background: transparent; border: 0; border-radius: var(--b-radius-sm); cursor: pointer;
}
/* HOVER AND KEYBOARD FOCUS ARE ONE TREATMENT (K15's channel law): one surface, one
   cursor, so a pointer and a keyboard never disagree about which row is live. */
.conv-menu-item:hover, .conv-menu-item:focus-visible {
  background: var(--role-primary); color: var(--ink-on-primary); outline: none;
}
/* DANGER-QUIET: the register is in the INK, never a filled ground. These rows ask — they
   open a confirmation — and a filled red row in a menu of six reads as the act itself. */
.conv-menu-danger { color: var(--b-danger, #dc2626); }
.conv-menu-danger:hover, .conv-menu-danger:focus-visible {
  background: var(--b-danger, #dc2626); color: #fff;
}

/* ── THE DETAILS SHEET (TEN-1817) ──────────────────────────────────────────
   Where the row's card went: the facts as a definition list, the state chips above them.
   A `<dl>` and not a table, because these are pairs rather than a grid — R7's own line,
   one scale down. */
.conv-details-chips { display: flex; flex-wrap: wrap; gap: var(--b-space-1); margin-bottom: var(--b-space-2); }
.conv-details-facts {
  display: grid; grid-template-columns: minmax(5rem, auto) 1fr;
  gap: var(--b-space-1) var(--b-space-2); margin: 0;
}
.conv-details-facts dt { margin: 0; color: var(--text-muted); }
.conv-details-facts dd { margin: 0; color: var(--text-primary); }
/* On a phone the pair stacks rather than squeezing the value into a third of a 375px
   screen — the label is sixteen characters and the value is a sentence. */
@media (max-width: 480px) {
  .conv-details-facts { grid-template-columns: 1fr; gap: 2px var(--b-space-2); }
  .conv-details-facts dd { margin-bottom: var(--b-space-1); }
}

/* The editor's own vocabulary line — the reader's existing categories, offered so
   reuse is a copy rather than a retype. */
.conv-vocab { color: var(--text-muted); }
.conv-tags-modal textarea { resize: vertical; min-height: 4.5rem; }

@media (max-width: 720px) {
  .conv-sort .kit-select { flex: 1 1 100%; }
}

/* ══ THE FRONT DOOR AND THE ARRIVAL (TEN-1844, epic TEN-1838; boards B1/B2/B3) ══
 *
 * Three families, appended at the tail on the pass family's precedent (the note at
 * `.ktrow.passrow`): the ask card COMPOSES `.ktcard` and `.btn`, and a rung
 * declared later in this file is the one that wins, so the ordering is the rule
 * rather than an accident of when the work landed.
 *
 * Everything references tokens or roles; no raw hex in a structural rule.
 *
 * ── K46 — THE ASK CARD (PROPOSED, TEN-1839) ──────────────────────────────────
 *
 * ONE COMPOSITION, FOUR MOUNTS. The mount changes the dressing and nothing that
 * is said: the page mount is a card, the row and bubble mounts are quieter
 * neighbours in a stack somebody is scrolling, and the banner mount runs the full
 * width of a page it sits on top of. What is IDENTICAL across all four is the
 * order — asker, ask, subject, grant, answers — because "the same ask implemented
 * three times, so that not now means three different things" is the anti-pattern
 * the family was minted to pre-empt, and a cover that re-ordered one mount would
 * be that anti-pattern arriving through the stylesheet.
 *
 * THE GRANT LINE IS NOT A FOOTNOTE. It is the sentence K46 exists for, so it is
 * set at reading size in muted ink rather than in the fine print an eye skips —
 * a consent line nobody reads is the failure this entry is about.
 *
 * THE ANSWER PAIR SHARES ONE ROW AND ONE BASELINE. The verb and the ghost are
 * `.btn` and nothing else, because K12 owns what an act looks like and an ask card
 * that dressed its own buttons would be a second act grammar. At narrow widths
 * they stack, verb first: a pair that wrapped the ghost above the verb would put
 * the refusal where the thumb is.
 */
.ktask { display: flex; flex-direction: column; gap: 10px; color: var(--text-primary); }
.ktask-page {
  border: 1px solid var(--border-subtle); border-radius: var(--card-r);
  background: var(--surface-raised); padding: 20px;
}
/* The row and bubble mounts are rungs of somebody else's stack: no ground of
 * their own, a hairline above instead of a box, and tighter leading. */
.ktask-row, .ktask-bubble { padding: 12px 0 0; border-top: 1px solid var(--border-subtle); gap: 8px; }
/* The banner runs the page's width and sits ABOVE its tab bar, so it needs one
 * edge that says "this is about the whole page" without becoming a second
 * masthead: the accent as a left rail, and a wash rather than a fill (K29 — one
 * live treatment per screen, and the ask's own Follow act is the live one). */
.ktask-banner {
  border: 1px solid var(--blue-line); border-left: 3px solid var(--role-primary);
  border-radius: var(--card-r); background: var(--blue-a); padding: 14px 16px; gap: 8px;
}
.ktask-who { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ktask-face { flex: 0 0 auto; }
.ktask-initial {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--b-avatar-sm); height: var(--b-avatar-sm); border-radius: 50%;
  background: var(--role-primary-soft); color: var(--role-primary);
  font-weight: 700; font-size: 13px;
}
.ktask-name { font-weight: 700; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ktask-say { margin: 0; font-size: 15px; line-height: var(--b-leading-tight); }
.ktask-subject { display: block; }
.ktask-grant { margin: 0; font-size: 13px; line-height: 1.45; color: var(--text-muted); }
.ktask-acts { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* A form is a layout participant here and nothing else — the button inside it is
 * the object, so the form must not add a box or a baseline of its own. */
.ktask-acts form { display: contents; }
.ktask-aside { margin: 0; font-size: 13px; color: var(--text-muted); }
/* THE CLOSED ASK IS K37's ONE SENTENCE, NEVER A GREYED PAIR. It is drawn at the
 * card's own wattage rather than dimmed: the ask is over, not disabled, and
 * fading the sentence that explains why is the one thing that makes it useless. */
.ktask-shut { margin: 0; font-size: 14px; color: var(--text-muted); }
.ktask-away { margin: 0; font-size: 14px; }
@media (max-width: 520px) {
  .ktask-acts { flex-direction: column; align-items: stretch; }
  .ktask-acts .btn { width: 100%; }
}

/* ── BOARD B1 — THE FRONT DOOR ────────────────────────────────────────────────
 *
 * The citation is the BOARD (`Hero.dc.html`) and the anatomy parked on TEN-1839:
 * K45 was withdrawn at the design seat because one instance is under R6's rule of
 * two. What the cover has to hold up is the sizing that carries the meaning.
 *
 * TWO DOORS, SIZED BY INTENT, AND EXACTLY ONE ACCENT FILL ABOVE THE FOLD. The
 * primary is `.btn--primary` and the secondary is `.btn--secondary`; this family
 * adds no fill of its own, so the "one live treatment" claim is checkable by
 * counting accent grounds in the rendered page rather than by reading this file.
 * TEN-1873 (Mark's ruling) swapped the pair for Register/Sign in; the sizing is
 * unchanged.
 *
 * ONE POSITIONING LINE. The heading is the only display-scale type on the page,
 * which is the cover's half of "no second positioning line": a second line at
 * this size would have nowhere to be.
 *
 * THE ORGANIZER ASIDE IS GONE (TEN-1873, Mark's ruling): the quiet line and its
 * "For organizers ›" link were removed, and `.fdoor-org` with them.
 *
 * THE SHOT IS THE PRODUCT, so it is drawn as a device rather than styled as an
 * illustration: a raised ground, the page's own border, real shadow, and the kit's
 * own cards inside at their own scale. The 2deg rotation is the board's, and it is
 * the one decorative decision on the page — it says "screenshot" without a caption.
 */
.fdoor {
  display: flex; align-items: center; gap: 36px; flex-wrap: wrap;
  padding: 44px 0 0;
}
.fdoor-say { flex: 1 1 22rem; min-width: 0; }
.fdoor-line {
  margin: 0; font-family: var(--b-font-heading); font-weight: 800;
  font-size: clamp(2rem, 5vw, 2.75rem); line-height: 1.05; color: var(--text-primary);
}
.fdoor-sub { margin: 14px 0 0; max-width: 28rem; font-size: 16px; color: var(--text-muted); }
.fdoor-doors { display: flex; gap: 10px; margin-top: 22px; flex-wrap: wrap; }
.fdoor-shot {
  flex: 0 1 20.5rem; min-width: 0; box-sizing: border-box;
  border: 1px solid var(--border-strong); border-radius: 22px;
  background: var(--surface-raised); padding: 12px;
  box-shadow: var(--b-shadow-lg); transform: rotate(2deg);
}
.fdoor-phone { display: flex; flex-direction: column; gap: 8px; }
.fdoor-phone-head {
  font-family: var(--b-font-heading); font-weight: 700; font-size: 14px;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted);
}
/* THREE PROOFS, AND THE GRID SAYS THREE. `repeat(3, ...)` collapsing to one column
 * on a phone is deliberate rather than convenient: an auto-fit grid would accept a
 * fourth card silently, and a fourth proof is an escalation the layout should not
 * quietly absorb. */
.fdoor-proofs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 36px; }
.fdoor-proof { gap: 4px; padding: 16px; }
.fdoor-proof-name { font-size: 15px; }
.fdoor-proof-say { margin: 0; }
.fdoor-quiet { margin: 24px 0 0; font-size: 13px; color: var(--text-faint); }
@media (max-width: 760px) {
  .fdoor { padding-top: 24px; gap: 24px; }
  /* The shot goes BELOW the doors on a phone, and it keeps its rotation: the
   * doors are what the page is for, and a rotated card above them would push the
   * one act the page exists to start under the fold. */
  .fdoor-shot { order: 2; flex-basis: 100%; }
  .fdoor-proofs { grid-template-columns: minmax(0, 1fr); }
}

/* ── BOARDS B2/B3 — THE ARRIVAL ───────────────────────────────────────────────
 *
 * A stranger's first screen, and it is a SINGLE-JOB PAGE: one title, one stack of
 * kit cards, at most one way on. The column is narrow on purpose — this page is
 * read on a phone held in one hand, standing up, having just tapped a link in a
 * message.
 *
 * THE WAY ON IS ONE ACT (K12) and it is the last thing in the flow. There is no
 * rule here for a second, because a second act on an arrival is the decision the
 * reader has not been given enough to make.
 */
.arriv { display: flex; flex-direction: column; gap: 16px; max-width: 30rem; margin: 0 auto; padding: 8px 0 32px; }
.arriv-head { display: flex; flex-direction: column; gap: 6px; }
.arriv-eyebrow { color: var(--role-primary); }
.arriv-title {
  margin: 0; font-family: var(--b-font-heading); font-weight: 800;
  font-size: clamp(1.5rem, 4.5vw, 2rem); line-height: 1.1; color: var(--text-primary);
}
.arriv-lede { margin: 0; font-size: 15px; color: var(--text-muted); }
.arriv-block { display: block; }
.arriv-onward { margin: 4px 0 0; }
.arriv-onward .btn { width: 100%; }
.arriv-quiet { margin: 0; font-size: 13px; color: var(--text-faint); }

/* ===========================================================================
   K44 — THE ATHLETE LADDER, AND BOARD A1's TODAY (TEN-1842, epic TEN-1838).

   THE FAMILY'S ONE BLOCK. K19's console ladder and K21's storefront ladder each
   have one; this is the third, and the reason it is one block rather than rules
   spread through the file is the same reason the ladder exists: a header written
   on the screen that noticed it first is a header five screens grow five slightly
   different copies of.

   COLOUR IS SPENT UNDER K29 AND NOWHERE ELSE. Green is `covered / confirmed`, a
   fact about YOU — the `booked` badge and the strip's booked marks are the only
   things here that wear it, and both compose the existing `ktcal` / `ktchip`
   vocabularies rather than declaring a green of their own. Nothing here is red
   (a price promo, and only that), nothing is purple, and the ACCENT GROUND is
   spent exactly once per page state — on the next-up card, which is board A1's own
   law ("one next thing, never a feed of noise") and K29's one-live-treatment
   clause. A band that wants emphasis reaches for weight and space, which is what
   this block gives it.

   IT IS DRAWN AT 390px FIRST, because that is where the blast recipients live
   (the canvas's own read-me-first). Desktop is the same content at ease, not a
   different app: the page gets a measure and centres, and nothing re-arranges.
   ======================================================================== */

/* --- K47's ACCESSORY RING: the org marks, and ONLY the week strip's ------
   K47 clause 2: a coloured mark is an accessory, and only where an avatar cannot
   fit. There is exactly one such place on the whole canvas — A1's 5px strip marks
   — and clause 1 sends every other surface to the org AVATAR that is already
   composed onto every row naming an org.

   THE RING IS A DECLARED, CLOSED SET FROM WHICH EVERY SEMANTIC ROLE IS ABSENT BY
   CONSTRUCTION, which is what closes the K29 conflict structurally rather than by
   a habit an agent has to remember: there is no rule to recall, because the wrong
   answer is not available. K29's green, red, blue, violet and the dark ground are
   NOT IN THE SET — and neither are this cover's amber (`--role-attention`) or teal
   (`--role-context`), which are roles here even though K47 names only the first
   five. Six hues, hand-picked off the role palette: rose, coral, mustard, moss,
   slate-cyan and plum.

   MARKS ONLY — NEVER A GROUND, A FILL, A PILL OR AN INK. K29's meanings all live
   in grounds, pills and inks, so keeping identity in MARKS is what stops the two
   systems ever writing the same property. Every selector below is scoped to
   `.ktcal-dot`, so a rule that tried to paint anything else has nowhere to land.

   COLLISIONS ARE TOLERATED AND NOT RESOLVED: two of a reader's orgs may share a
   hue and nothing breaks, because the mark's meaning is its POSITION — something
   is on this day — and the hue is a shortcut on top of that. A reader who cannot
   tell two hues apart loses the shortcut and no information, which is the test
   for whether a colour is a channel or a claim. */
:root {
  --orgmark-1: #E06C9F;
  --orgmark-2: #E8845C;
  --orgmark-3: #C9A227;
  --orgmark-4: #6FA96F;
  --orgmark-5: #5B8C9E;
  --orgmark-6: #9B7BA8;
}
/* Tuned in all three themes, as K47 requires. On light the six go a step deeper
   so a 5px disc still reads against a pale ground; on high contrast they go to
   full strength, because hc's whole contract is that nothing is quiet. */
:root[data-theme="light"] {
  --orgmark-1: #C04A80;
  --orgmark-2: #C25E37;
  --orgmark-3: #9A7A12;
  --orgmark-4: #4A8050;
  --orgmark-5: #3D6B7D;
  --orgmark-6: #7A5A88;
}
:root[data-theme="hc"] {
  --orgmark-1: #FF66A8;
  --orgmark-2: #FF8A4D;
  --orgmark-3: #FFD24D;
  --orgmark-4: #66D98A;
  --orgmark-5: #66C2E0;
  --orgmark-6: #C299FF;
}
/* THE MARK NEVER CARRIES A FACT ALONE (K47's third constraint), and the CASCADE is
   what enforces it rather than a convention: the state ladder's
   `.ktcal-dot.ktcal-booked` and `.ktcal-dot.ktcal-covered` are declared EARLIER in
   this file, so these rules would win at equal specificity — which is why the
   booked and covered arms are restated at the end of this block. The identity is
   the shortcut; the state is the fact, and the state always wins the pixel. */
.ktcal-dot.ktcal-om1 { background: var(--orgmark-1); }
.ktcal-dot.ktcal-om2 { background: var(--orgmark-2); }
.ktcal-dot.ktcal-om3 { background: var(--orgmark-3); }
.ktcal-dot.ktcal-om4 { background: var(--orgmark-4); }
.ktcal-dot.ktcal-om5 { background: var(--orgmark-5); }
.ktcal-dot.ktcal-om6 { background: var(--orgmark-6); }
/* THE STATE RESTATED SO IT OUTRANKS THE IDENTITY. Two classes each, so these beat
   the single-class org rules above on specificity and not on order — a stylesheet
   whose correctness depended on which of two equal rules came last is a stylesheet
   one reformat breaks. The VALUES are not re-declared: both read the same
   expressions the ladder's own rules do, so a theme change or a token edit carries
   here with them and there is no second green to drift. */
.ktcal-dot.ktcal-booked { background: var(--role-presence); }
.ktcal-dot.ktcal-covered { background: color-mix(in srgb, var(--role-presence) 55%, transparent); }

/* --- RUNG 1: THE TITLE ROW ---------------------------------------------
   The surface's own name, and since TEN-1854 that is ALL of it: the three standing
   chrome items moved up into the shared app header this family now wears (Mark's
   M1 UAT amending K44 clause 4), so the row has one child instead of two and
   `.athd-chrome` is retired with the `ifa-header` slot that fed it.

   IT STAYS STICKY, and with the shared header above it that reads better rather
   than worse: the app header scrolls away and the surface's own name is what stays
   pinned — which is the right division, because the name is the thing that tells a
   reader which of five subjects they are in, and a thumb that has scrolled a list
   should not have to scroll back to find out. A bar the document can scroll under
   also keeps the page's own top edge honest, which a fixed header does not.

   IT HONOURS THE TOP SAFE-AREA INSET. The shared header is above it now, so on a
   notched phone the inset is normally spent up there; the max() stays because this
   row is still what sits at the viewport top once the page has scrolled, and a
   notch over a sticky header would eat the one word the row exists to say. */
.athapp {
  /* `--athsp-h` WAS HERE — the bottom bar's height, spelled once because the
     body's bottom gutter read it. The bar is retired (TEN-1870, Mark's M1 UAT-2)
     and the token goes with it rather than lingering as a number nothing draws. */
  /* THE TITLE ROW'S OWN HEIGHT (TEN-1863), spelled once from the same box model
     `.athd` builds below it — its padding, its title line and its border — so a
     jump that lands under this sticky bar (a week-strip day tap, a month-view
     day cell, anything addressing `#sfev-day-<key>`) can clear it without a
     second, hand-tuned number to drift out of step with the first. The
     safe-area term is carried through rather than baked flat, because a notched
     phone spends it here exactly as `.athd`'s own padding does. Deliberately
     sized for the no-census row (K35's sentence does not appear on Schedule
     today); a surface that grows one clears the bar by one line less, which is
     the safe side of this trade to be wrong on. */
  --athd-h: calc(
    max(var(--b-space-2), env(safe-area-inset-top)) + var(--b-space-2) +
    (var(--b-text-xl) * var(--b-leading-tight)) + var(--b-border-w)
  );
}
.athd {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--b-space-3);
  padding: max(var(--b-space-2), env(safe-area-inset-top)) var(--b-space-4) var(--b-space-2);
  background: var(--surface-page);
  border-bottom: var(--b-border-w) solid var(--border-subtle);
}
.athd-left { min-width: 0; }
/* `.athd-chrome` is gone (TEN-1854) — nothing emits it. The chrome it laid out is
   in `.app-header .chrome` now, which is where every other page in this book has
   always had it. */

/* THE NAME. The display face and the uppercase are the COVER's, never the data —
   a surface named "Friendly week" is not shouted at by a template. */
.athd-name { display: flex; flex-direction: column; gap: 0; min-width: 0; }
.athd-title {
  margin: 0; font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.04em;
  font-size: var(--b-text-xl); line-height: var(--b-leading-tight);
  font-weight: var(--b-weight-bold); color: var(--text-primary);
}
/* K35's census sentence, where a surface has one. Quiet by construction: it is a
   count, not a claim. */
.athd-census { margin: 0; font-size: var(--b-text-xs); color: var(--text-muted); }

/* --- RUNG 2: THE BODY -------------------------------------------------- */
.athmain {
  /* THE BOTTOM GUTTER IS A GUTTER AGAIN (TEN-1870). It used to be the spine
     bar's height plus the safe-area inset plus a space, because a fixed bar takes
     no space and the page was what had to leave some. The bar is retired, so
     leaving that term would be exactly the dead band Mark's ruling is about —
     ~3.25rem of nothing under the last row, on the one family that is drawn at
     390px. What the page owes the bottom edge now is the same space it owes the
     top, plus the inset a notched phone spends on its home gesture. */
  padding: var(--b-space-4) var(--b-space-4)
           calc(var(--b-space-4) + env(safe-area-inset-bottom));
  /* 390px first; on a wider screen the same content takes a measure and centres,
     which is "desktop is the same content at ease, not a different app". */
  max-width: 34rem; margin: 0 auto;
}

/* THE MONTH GRID TAKES THE WIDE LENS ON A DESKTOP (TEN-1862, Mark's M1 UAT-2:
   "the calendar takes the same horizontal width as the desktop messages page").

   34rem IS RIGHT FOR EVERY OTHER ATHLETE SCREEN and wrong for exactly this one.
   The measure above is a READING measure — "the same content at ease, not a
   different app" — and every other surface in this family is a column of rows,
   which is what a measure is for. A month grid is not a column: it is SEVEN, and
   544px divided seven ways is a 70px cell, which is below K32's own chip threshold
   (~110px) and drops the whole grid to the middle rung on a 27-inch monitor. The
   rung is picked by the cell and never by the viewport, so the honest fix is to
   give the cells room rather than to teach the rung about screens.

   THE WIDTH IS THE MESSAGES PAGE'S, READ FROM THE SAME TOKEN — --b-lens-wide, the
   book's third wide lens (see tokens.css). Same `:has()` move, same @supports
   guard and the same 721px floor the conversation frame uses, so the two pages
   widen at the same instant rather than at two numbers that merely look alike.

   SCOPED TO THE CALENDAR AND NOT TO THE PAGE: /schedule's List arm is a column of
   rows like every other athlete screen and keeps the reading measure, so the
   selector names the grid (`.sfev-cal`) rather than the surface. A reader
   switching views changes measure, which is the view switcher doing exactly what
   it says. */
@supports selector(:has(*)) {
  @media (min-width: 721px) {
    body.app:has(.sfev-cal) .athmain { max-width: var(--b-lens-wide); }
  }
}
.athpage, .athbody { display: flex; flex-direction: column; gap: var(--b-space-4); }

/* --- RUNG 3: THE SPINE BAR IS RETIRED (TEN-1870) ----------------------
   Mark's M1 UAT-2 ruling: "The bottom tabs are not effective; they are the only
   thing in the app that draws the attention to the bottom of the screen, so the
   user never looks there."

   `.athsp`, `.athsp-tab`, `.athsp-on` and `.athsp-mark` were here, with the
   `--athsp-h` token above and `.athmain`'s bar-shaped bottom gutter. All of it is
   DELETED rather than commented out or left unreferenced: the component and plate
   that emitted those classes are gone from the sportscore module, so a rule set
   for them would be a rule set nothing can match — and the next lane reading this
   stylesheet would have to work out which.

   WHAT WENT WITH IT, recorded because each was a ruling and not a detail. The
   bottom anchor was K44's own clause ("a spine a thumb cannot reach is a spine
   that gets reached by scrolling to the top of a list the reader just scrolled
   down") and the answer to it is now that the navigation is at the TOP, where the
   ruling above says a reader is actually looking. The K39 pin — the bar rode the
   VISUAL viewport's bottom, because `bottom: 0` is the layout viewport and does
   not shrink when a keyboard rises — retires with the only fixed bottom element
   this family had, and so does the one line of athlete.js that wrote it. The
   safe-area inset the bar honoured in its own padding is now `.athmain`'s, which
   is where a page that ends at the bottom of the glass should carry it. */

/* --- BOARD A1: THE NEXT-UP CARD's ONE LIVE TREATMENT -------------------
   THE ACCENT GROUND IS SPENT EXACTLY ONCE PER PAGE STATE, and the selector is
   what makes that measurable rather than promised: it is scoped to the next-up
   card's own rung as a DIRECT CHILD of the athlete body, so a preview card in a
   band cannot pick it up and the browsergate arm can count the elements wearing
   it.

   It is a TINT plus a rail, never a fill: a fill at card size is a ground K29
   reserves for `live now`, and this card is *next*, not *now*. */
.athbody > .ktprev-session {
  background: linear-gradient(180deg, var(--role-primary-soft), var(--surface-raised) 70%);
  border-color: var(--role-primary);
}
/* The card's own top line: the eyebrow at the left, the state badge at the right. */
.ktprev-brow {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--b-space-2);
}
.athbody > .ktprev-session .ktprev-eyebrow { color: var(--role-primary-ink); }
.ktprev-acts { display: flex; gap: var(--b-space-2); flex-wrap: wrap; margin-top: var(--b-space-2); }
/* TODAY'S CHECK-IN ANSWER (TEN-1969, board F). `done` is K29's green (a fact
   about you) with its tick; `remedy` stands where the act would be in RESTING
   ink — it is a way forward, not an error; `notice` is a refusal a press came
   back with, in the card. The one-tap act is a real form, inline with its
   siblings. */
.ktprev-done { display: flex; align-items: center; gap: var(--b-space-2); color: var(--green); font-weight: var(--b-weight-bold); margin-top: var(--b-space-2); }
.ktprev-done .ktic { width: 20px; height: 20px; }
.ktprev-remedy { margin: var(--b-space-2) 0 0; font-weight: var(--b-weight-semibold); }
.ktprev-notice { margin: var(--b-space-2) 0 0; }
.ktprev-actform { display: inline-flex; margin: 0; }
/* The Check-in act is a <label> and not a button (R9: a reveal with no scripting
   is a checkbox), so it needs the pointer a button has by default. */
label.btn { cursor: pointer; }

/* --- BOARD A1: THE WEEK STRIP (K34 AMENDED over K32's dot rung) --------
   A FRAME DRAWN OPEN: same cells, same hrefs, same vocabulary as K34's panel,
   with the trigger, the collision spec and the attach geometry dropped because
   there is nothing to attach to.

   SEVEN EQUAL COLUMNS, and the grid is why rather than flex: the whole value of a
   week is that the days line up, and a cell that grew to fit its busiest day
   would break the one thing a strip is for. The dot ceiling is the composer's for
   the same reason (K32's own argument at the month rung). */
/* `align-items: stretch` IS NOT REDUNDANT HERE, and leaving it out cost a
   browsergate run. The strip wears `.ktcalnav` too, whose base rule is
   `display: inline-flex; align-items: center` for the TRIGGER rung — and
   overriding `display` and `flex-direction` leaves the centring behind. In a
   column, the cross axis is horizontal, so both of the strip's rows shrank to
   their content and sat centred: the heads came out 147px wide, the seven cells
   66px, and every geometry claim in `weekstrip_test.go` failed on a strip that
   looked plausible in a screenshot. A rung that changes a family's axis has to
   restate the alignment that axis implied. */
.ktcalnav-strip { display: flex; flex-direction: column; align-items: stretch; gap: 2px; }
.ktstrip-heads, .ktstrip-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--b-space-1);
  /* The panel rung's `.ktcalnav-heads` carries `padding: 0 var(--b-space-1) 2px`
     for a row inside a bordered popover. There is no popover here, and the
     padding is what made the weekday letters sit a few pixels off the cells they
     name — two grids with the same columns and different insets do not line up.
     Zeroed explicitly rather than by not inheriting it, because the heads element
     wears BOTH classes. */
  padding: 0;
}
.ktstrip-heads span {
  text-align: center; font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-size: 10px; font-weight: var(--b-weight-semibold); color: var(--text-faint);
}
/* The cell is the press target at this rung, because a 5px circle is not a
   control anybody can hit — the dot rung's own reasoning, and why the whole cell
   is the anchor. */
.ktstrip-grid .ktcal-day {
  min-height: 0; padding: var(--b-space-1) 2px;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  border-radius: var(--b-radius-sm);
}
.ktstrip-grid .ktcal-date { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); }
/* The marks are 5px here rather than the month grid's 6: seven cells across
   390px is about 48px a cell, and four marks plus their gaps have to fit inside
   one. */
.ktstrip-grid .ktcal-dot { width: 5px; height: 5px; }
.ktstrip-grid .ktcal-dot.ktcal-booked { width: 6px; height: 6px; }
.ktstrip-grid .ktcal-dots { min-height: 6px; }
/* TODAY IS MARKED BY A GROUND AND NOT BY COLOUR: the ground is a surface, which
   K29 leaves unspoken for, and the ink stays the reader's. A hue here would be
   competing with the org marks the cell is drawn to carry. */
.ktstrip-grid .ktcal-today { background: var(--surface-overlay); }

/* --- BOARD A1: THE STRIP EXPANDS (TEN-1892) -----------------------------
   A native <details> under the seven cells. The chevron is the disclosure's own
   summary — no script decides whether the week is open — and what it opens is the
   same week as seven COLUMNS on the strip's own grid, so each column sits under
   the cell that names its day (measured: todayquickzoom_test.go).

   THE MARKER IS REPLACED, NOT HIDDEN BEHIND A SECOND CONTROL: the summary IS the
   button, and the caret turning over is the only state it draws. */
.ktstrip-more { margin: 0; }
.ktstrip-toggle {
  display: flex; justify-content: center; align-items: center;
  /* A THUMB's TARGET, not a glyph's: the caret is small and the row it sits in
     is the whole width of the strip and tall enough to hit on the phone Mark
     UATs on — the reason the ruling picked a chevron over hover. */
  min-height: 32px; list-style: none; cursor: pointer;
  color: var(--text-muted); border-radius: var(--b-radius-sm);
}
.ktstrip-toggle::-webkit-details-marker { display: none; }
.ktstrip-toggle::marker { content: ""; }
.ktstrip-toggle:hover, .ktstrip-toggle:focus-visible { color: var(--text-primary); background: var(--surface-overlay); }
.ktstrip-chev { font-size: 14px; line-height: 1; transition: transform 0.15s ease; }
.ktstrip-more[open] .ktstrip-chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .ktstrip-chev { transition: none; } }
/* THE SAME SEVEN TRACKS AS THE CELLS, and the same zero inset — two grids with
   the same columns and different insets do not line up (the heads' own scar). */
.ktstrip-cols {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--b-space-1);
  padding: var(--b-space-1) 0 0;
}
.ktstrip-col { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* K32's BLOCK RUNG AT A SEVENTH OF A PHONE. The rung is the block because it is
   the anatomy that stacks the time over the name; what a 48px column cannot hold
   is the block's hour-grid floor and its roomy padding, so those two give way and
   the NAME WRAPS rather than ellipsizing to a two-letter stub — "an event you
   cannot name is not an event you can decide about" (the chip's own middle case). */
.ktstrip-col .ktcal-block {
  min-height: 0; padding: 3px 4px; gap: 0;
  border-left-width: 2px;
}
.ktstrip-col .ktcal-bt { font-size: var(--b-text-2xs); }
.ktstrip-col .ktcal-bn {
  font-size: var(--b-text-2xs); line-height: 1.2;
  overflow-wrap: anywhere; hyphens: auto;
}

/* --- THE CLASS QUICKZOOM (TEN-1892): the class card over the invitation's own
   answers, inside the sheet shell. Two composed blocks stacked, with the rhythm
   the sheet body already keeps between its parts. */
.ktsheet-body > .ktprev + .ktask { margin-top: var(--b-space-3); }
/* THE SAME INVITATION ON THE CLASS'S OWN PAGE (TEN-1892) — K46's banner mount
   between the trail and the page's identity block, spaced like the blocks around
   it rather than wearing a treatment of its own. */
.sfland-invite { margin: var(--b-space-3) 0; }
/* The class chat's door (TEN-1903) sits between the class head and its sessions, on the invite banner's rhythm. */
.sfland-chat { margin: var(--b-space-3) 0; display: flex; flex-direction: column; gap: var(--b-space-2); }

/* --- BOARD A1: THE BANDS ----------------------------------------------
   Quiet by construction. A band is a heading and its rows, and the heading is
   K35's voice — a census sentence, not a title. */
.athband { display: flex; flex-direction: column; gap: var(--b-space-2); }
/* THE STRIP's OWN TARGET CLEARS THE STICKY TITLE ROW TOO (TEN-1863). Clause C's
   press (athlete.js) calls `target.scrollIntoView({block:"start"})` on purpose —
   "the bar is fixed to the visual viewport's bottom and the header is sticky, so
   anything this file measured itself would be a second copy of the cover's
   geometry" (athlete.js's own comment) — which makes this rule that geometry's
   one declaration rather than the script's guess at it. Without it, a press on
   TODAY's own cell scrolled the reader to a heading hidden under `.athd` exactly
   as the cross-page `#sfev-day-<key>` jump did. */
[data-athscroll] { scroll-margin-top: var(--athd-h); }
.athband-hd {
  margin: 0; font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  color: var(--text-faint);
}
.athwallet { display: flex; flex-wrap: wrap; gap: var(--b-space-2); }

/* --- K46's ASK CARD IS NOT DRESSED HERE (TEN-1842's merge with TEN-1844) ---
   Board A1's needs-you row mounts `kit-ask-card`, and this block deliberately
   carries NO rule for it: TEN-1844 minted the same family for boards B2 and B3,
   landed first, and its `.ktask*` block is above — four mounts, dressed per mount,
   which is more than this board needed and is the right amount for the family.

   A SECOND `.ktask` BLOCK HERE WOULD HAVE BEEN THE EXACT THING K46 EXISTS TO
   PREVENT, one layer down from where the ruling was written: K46 says ONE
   composition serves a page, a row, a banner and a bubble, and two stylesheets for
   it would let *not now* look like a different offer in two places even with one
   plate drawing it. Named rather than silently absent, because "this block has no
   ask-card rules" is otherwise indistinguishable from somebody forgetting. */

/* --- K30 arm 2: THE ZERO STATE ----------------------------------------
   A SENTENCE AND ONE ACT, never an empty shell and never a void with a sad icon.
   The dashed edge is the book's own staged/absent shape (K12's shape rule), which
   is what says "this is where something will be" without a placeholder pretending
   to be content.

   THE ONE ACT LIVES WHERE THE SCHEDULE WILL BE — board B4's own note, "the
   emptiness itself points at the fix" — so the block sits in the body's flow at
   the top rather than being centred in the viewport. */
.athempty {
  display: flex; flex-direction: column; align-items: center; gap: var(--b-space-2);
  padding: var(--b-space-6) var(--b-space-4); text-align: center;
  border: var(--b-border-w) dashed var(--border-strong);
  border-radius: var(--b-radius-md); background: var(--surface-raised);
}
.athempty-t { margin: 0; font-size: var(--b-text-lg); font-weight: var(--b-weight-bold); }
.athempty-b { margin: 0; max-width: 32ch; color: var(--text-muted); }

/* --- K44's ONE NAMED ACT: the athlete's boarding pass -----------------
   It is the `.pass` family's second mount and it wears that family's rules
   unchanged — the overlay, the B&W sheet, the QR's machine contrast. All this adds
   is the athlete surface's own z-index, because the pass has to sit over a spine
   bar that the competition-day mount does not have. */
.athpass { z-index: 60; }

/* ===========================================================================
   BOARD A3 — HISTORY & PATTERNS, AND THE THREE DERIVED-READING FAMILIES
   (K40, K41, K42 [PROPOSED, TEN-1839]; built TEN-1882, epic TEN-1838).

   ITS OWN REGION, beneath the A1 block rather than inside it, because the three
   families dressed here are the KIT's and not this page's: board A3 is their
   first consumer, board E5's week comparison and its barless open-streak panel
   are the next, and a rule written inside one board's block is a rule the second
   board copies. The page's own two sentences are at the end of the region and
   there is nothing else here that names A3.

   COLOUR IS SPENT UNDER K29 AND NOWHERE ELSE, and this region spends it exactly
   once: the census tile row's `have` tone, on the OPEN streak's number, which is
   the one meaning green carries in this book (*a fact about YOU*). Nothing else
   here is a hue at all —

     - THE RHYTHM GRID'S RAMP IS NOT A SEMANTIC COLOUR (K41 clause 2). It is the
       surface's own neutral-to-accent ramp at three wattages, and it makes no
       claim: a darker cell means MORE and nothing else. It is built by mixing
       `--role-primary` into the page ground rather than by declaring new tokens,
       so all three themes follow the cover's own primary without a second
       palette to tune.
     - THE STANDINGS FILL IS THE SAME INK FOR EVERY ROW (K42's K29 clause). A
       per-row hue would be the subject's identity channel (K47) and this book
       draws no org marks outside A1's 5px strip, so the rows are told apart by
       their NAMES and by their ORDER, which is the ranking.
     - THE READER'S OWN ROW IS WEIGHT AND NEVER COLOUR — `.ktstand-mine` sets
       `font-weight` and nothing else, so a lane that reached for green here has
       to add a declaration rather than remove one.

   AND NOTHING IN THIS REGION HAS A WARNING ROLE. There is no rule for an empty
   column, a broken run or a downward trend — not a muted one, not a dashed one,
   not a red one. K41's closing clause and K42's guilt clause are the same ruling
   ("the app never shames a break"), and a stylesheet with nowhere to put a
   reproach is how that ruling survives the next lane.

   DRAWN AT 390px FIRST, like the rest of this family. The grid's cells and the
   tiles' numbers are sized so twelve columns and three tiles fit one phone row
   without shrinking a number to do it.
   ======================================================================== */

/* --- K40: THE CENSUS TILE ROW -----------------------------------------
   EQUAL WIDTHS, AND IT WRAPS TO TWO-UP BEFORE IT SHRINKS A NUMBER (the anatomy
   row's own clause): `flex: 1 1 7rem` gives every tile the same share and lets
   the third drop to a second line on a narrow phone rather than squeezing three
   display-face numbers into 390px. A number that shrank to fit is a number a
   reader squints at.

   NO BORDER BETWEEN TILES, no press, no tooltip — the anatomy row, verbatim. The
   row has no ground of its own either: it sits in the body's flow, because K29's
   accent ground is spent once per page state and board A1 already spends it. */
.ktcensus {
  display: flex; flex-wrap: wrap; gap: var(--b-space-3);
}
.ktcensus-tile {
  flex: 1 1 7rem; min-width: 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: var(--b-space-3);
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle);
  border-radius: var(--b-radius-md);
}
/* THE VALUE in the display face. It is the only thing on a tile a reader takes
   in at a glance, so it gets the size and the weight and the words get neither. */
.ktcensus-v {
  font-family: var(--b-font-heading); font-weight: var(--b-weight-bold);
  font-size: var(--b-text-2xl); line-height: var(--b-leading-tight);
  color: var(--text-primary);
}
/* THE ONE TONE THIS BOOK'S TILES WEAR, and it is on the VALUE alone (K40's
   anatomy). `--role-presence` is the same expression every other green in this
   cover reads, so there is no second green to drift. */
.ktcensus-have { color: var(--role-presence); }
/* THE UNIT LABEL, in two short lines beneath the value. `ktcensus-k2` is the
   second line and it is a block so the break is the COMPOSER's — a label that
   wrapped where the box happened to end would move with the measure. */
.ktcensus-k {
  font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  line-height: var(--b-leading-tight); color: var(--text-faint);
}
.ktcensus-k2 { display: block; }
/* THE BUBBLE DENSITY (K40's fourth state): fewer tiles, the unit labels inline,
   no ground of their own — the row is already inside a bubble. Declared here
   rather than in the chat region because the DENSITY is a rung of this family
   and the bubble is only where it is mounted. */
.ktcensus-bubble { gap: var(--b-space-2); }
.ktcensus-bubble .ktcensus-tile {
  flex: 0 1 auto; flex-direction: row; align-items: baseline; gap: var(--b-space-1);
  padding: 0; background: none; border: 0;
}
.ktcensus-bubble .ktcensus-v { font-size: var(--b-text-lg); }
.ktcensus-bubble .ktcensus-k2 { display: inline; }
.ktcensus-bubble .ktcensus-k2::before { content: " "; }

/* --- K41: THE RHYTHM GRID ---------------------------------------------
   TWELVE EQUAL COLUMNS, and the grid is why rather than flex: the whole value of
   a rhythm is that the weeks line up under each other, and a column that grew to
   fit its own content would break the one thing the picture is for. It is the
   same argument board A1's week strip makes at seven columns.

   THE CELLS ARE 12px AND THEY ARE NOT CONTROLS. K41's escalate-when says so
   outright ("a 12px cell is not a control"), so there is no hover treatment, no
   focus ring and no cursor change in this block — the cell carries its words in a
   title attribute and nothing here invites a press. */
.ktrhythm { margin: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktrhythm-hd {
  font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  color: var(--text-faint);
}
.ktrhythm-body { display: flex; flex-direction: column; gap: 3px; }
.ktrhythm-row { display: flex; align-items: center; gap: var(--b-space-2); }
/* THE SLOT RAIL. A fixed measure so every row's cells start at the same x — a
   rail that sized itself to "Wed" and "Fri" separately would stagger the grid. */
.ktrhythm-rail {
  flex: 0 0 2.25rem;
  font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-size: 10px; font-weight: var(--b-weight-semibold); color: var(--text-faint);
}
.ktrhythm-cells {
  flex: 1 1 auto; min-width: 0;
  display: grid; grid-template-columns: repeat(12, 1fr); gap: 3px;
}
.ktrhythm-cell {
  aspect-ratio: 1; min-width: 0; border-radius: var(--b-radius-sm);
  background: var(--surface-overlay);
}
/* THE LADDER — three wattages of ONE ink, mixed from the cover's primary into
   the page ground rather than declared as new tokens, so light, dark and high
   contrast all follow the cover they already have.

   `ktrhythm-s1` IS THE EMPTY RUNG and it is the cells' own resting ground above,
   restated here so a reader of this block can see all three steps in one place
   and so the ladder is complete rather than implied. `s4` is declared because the
   family's ceiling is four (K41 clause 1) and a mount that can honestly answer a
   fourth step must not have to edit this cover to use it. */
.ktrhythm-s1 { background: var(--surface-overlay); }
.ktrhythm-s2 { background: color-mix(in srgb, var(--role-primary) 32%, var(--surface-overlay)); }
.ktrhythm-s3 { background: color-mix(in srgb, var(--role-primary) 68%, var(--surface-overlay)); }
.ktrhythm-s4 { background: var(--role-primary); }
/* A TRAINED DAY IS A WAY IN (TEN-1898). Only a cell the composer handed an
   address becomes an anchor, and it keeps its step's ground exactly — the ladder
   is the finding, so the press must not repaint it. The affordance is the EDGE:
   an outline in the text ink on hover and focus, and the pointer. That is K15's
   hover-owns-the-edge rule at a cell's scale, and it leaves K41's "a darker cell
   means MORE, and nothing else" true under the pointer. */
.ktrhythm-press { display: block; cursor: pointer; }
.ktrhythm-press:hover,
.ktrhythm-press:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 1px; }
/* THE PERIOD RAIL, sparse by ruling: first, last, and nothing in between.
   Indented by the slot rail's own measure plus its gap so the two labels sit over
   the cells they name rather than over the letters. */
.ktrhythm-periods {
  display: flex; justify-content: space-between;
  margin-left: calc(2.25rem + var(--b-space-2));
  font-size: var(--b-text-xs); color: var(--text-faint);
}
/* THE READING. It is the block's point rather than its caption, so it takes the
   body measure and the primary ink — a sentence in the faint tier would be a
   sentence readers skip, and K41 refuses to draw the grid without it. */
.ktrhythm-read { margin: 0; max-width: 48ch; color: var(--text-primary); }

/* --- K42: THE STANDINGS ROW -------------------------------------------
   A ROW IS A LINE OF FACTS OVER A TRACK. The top line holds the optional face,
   the name, the tail and the value; the track sits under it at full width, which
   is what lets a 390px row carry a long club name without the bar shrinking to
   nothing. */
.ktstand { display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktstand-hd {
  margin: 0; font-family: var(--b-font-heading);
  text-transform: uppercase; letter-spacing: 0.06em;
  font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  color: var(--text-faint);
}
.ktstand-row { display: flex; flex-direction: column; gap: 3px; }
.ktstand-top { display: flex; align-items: baseline; gap: var(--b-space-2); }
.ktstand-face { flex: 0 0 auto; align-self: center; }
.ktstand-name { min-width: 0; flex: 0 1 auto; color: var(--text-primary); }
/* THE STATUS TAIL — the slot a row uses to say something a number cannot. Quiet,
   because it is context and not a verdict. */
.ktstand-tail { flex: 1 1 auto; min-width: 0; font-size: var(--b-text-xs); color: var(--text-muted); }
/* THE VALUE at the row's END, which is the anatomy's own order: the ranking is
   read from the shape and the value from the number. `margin-left: auto` so it
   still lands at the end on a row with no tail. */
.ktstand-v {
  margin-left: auto; flex: 0 0 auto;
  font-family: var(--b-font-heading); font-weight: var(--b-weight-semibold);
  color: var(--text-primary);
}
/* THE READER'S OWN ROW IS MARKED BY WEIGHT, AND THIS IS THE WHOLE RULE. K29 gives
   green one meaning and "this row is me" is not it, so there is no colour
   declaration in here to remove later. */
.ktstand-mine .ktstand-name { font-weight: var(--b-weight-bold); }
/* THE TRACK IS DRAWN WHERE THE MEASURE HAS ONE AND THE FILL WHERE THERE IS A VALUE
   — two data-presence decisions since TEN-1867. A track with no fill is the
   NO-VALUE state ("same measure, nothing on it yet"); NO TRACK is the BARLESS RUNG,
   which an unbounded measure needs because there is no maximum in view for a track
   to be the width of. The block declares `barless`, because the rung is a property
   of the MEASURE rather than of a row. */
.ktstand-bar {
  height: 8px; border-radius: var(--b-radius-sm);
  background: var(--surface-overlay); overflow: hidden;
}
.ktstand-fill { display: block; height: 100%; background: var(--role-primary); }
/* THE STAKE. Same tier as the rhythm grid's reading, for the same reason: the
   sentence is why the block is allowed to exist. */
.ktstand-read { margin: 0; max-width: 48ch; color: var(--text-primary); }

/* --- BOARD A3's OWN TWO SENTENCES --------------------------------------
   The tile row's reading (K40: the sentence belongs to the BLOCK and never to a
   tile) and the contract's `floor` line. They are the page's and not the kit's,
   which is why they are the only A3-named rules in this region.

   THE FLOOR LINE IS K12's SECONDARY TIER, deliberately: it is a statement about
   the DATA — where the record starts — and not about the reader, so it must not
   read as loudly as the sentences that are. */
.athhist-read { margin: 0; max-width: 48ch; color: var(--text-primary); }
.athhist-floor { margin: 0; font-size: var(--b-text-xs); color: var(--text-faint); }

/* --- K42's ACTS FOOT (TEN-1867) --------------------------------------------
   The family's anatomy names an OPTIONAL acts foot, and board E5's week panel is
   the first mount to use one ("Share your week" — a lateral anchor to the sharing
   control on the same page). A form is a layout participant here and nothing else:
   the button inside it is the object, so the form must not add a box or a baseline
   of its own. */
.ktstand-acts { display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap; margin-top: 2px; }
.ktstand-acts form { display: contents; }

/* --- BOARD E5's STANDING CONTROL: Activity sharing (TEN-1867) ---------------
   A management block and nothing louder. It carries no accent ground, because
   board A1's law is one high-wattage object per page and that object is the
   next-up card (K29's one-live-treatment) — a settings control that competed with
   it would be the feed of noise the board refuses.

   THE ZERO STATE IS A SENTENCE, drawn at the band's own wattage rather than
   dimmed: "you're not sharing with anyone" is a fact and not an apology. */
.athshare { display: flex; flex-direction: column; gap: var(--b-space-3); }
.athshare-none { margin: 0; color: var(--text-muted); font-size: var(--b-text-sm); }
.athshare-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
/* ONE ROW IS ONE DIRECTION, and the act that ends it sits on the row it ends —
   a "stop sharing" gathered into a footer would make the reader aim. */
.athshare-row {
  display: flex; align-items: center; gap: var(--b-space-2); flex-wrap: wrap;
  padding: 8px 0; border-top: var(--b-border-w) solid var(--border-subtle);
  font-size: var(--b-text-sm);
}
.athshare-say { flex: 1 1 18ch; min-width: 0; }
.athshare-since { flex: 0 0 auto; color: var(--text-faint); font-size: var(--b-text-xs); }
.athshare-stop { flex: 0 0 auto; }
/* AN OFFER IS A GRANT LINE, ONE FIELD AND ONE VERB, in that order — the sentence
   before the button is K46's ruling and the order is the whole of it. */
.athshare-offer {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--b-space-2);
  padding-top: var(--b-space-2); border-top: var(--b-border-w) solid var(--border-subtle);
}
.athshare-grant {
  margin: 0; font-size: var(--b-text-xs); line-height: 1.45;
  color: var(--text-muted); max-width: 52ch;
}
.athshare-f { display: flex; flex-direction: column; gap: 4px; min-width: min(26ch, 100%); }

/* --- THE SHARING FAMILY AS A SHARED COMPONENT (TEN-1896) ----------------------
   The band on the page is a TITLE LINE and ONE SENTENCE; the list and the merged
   picker live in the modal (kit-sheet). Board A1 and My Training print the same
   bytes, so these rules are the family's, not a page's.

   THE TITLE LINE IS K35's FLOAT AT BAND SCALE: the heading at the left edge, the
   one act at the right, one baseline. The act is a secondary launcher (K38); it
   takes no size of its own, so the band's heading sets the line's height. */
.athband-top {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--b-space-2); flex-wrap: wrap;
}
.athband-act { flex: 0 0 auto; }
.athshare-sum { margin: 0; font-size: var(--b-text-sm); }
/* THE PICKER FILLS ITS ROW, and inside the sheet its list joins the FLOW — the
   `.ss-grid-modal` reason (TEN-1152): a sheet's body is its own scroll box, so an
   absolutely-positioned list would be clipped under the terms it covers. In flow
   it pushes them down and the sheet scrolls, which is what a sheet is for. */
.athshare-pick { width: 100%; }
.athshare-pick .tokpick, .athshare-pick .athshare-to { width: 100%; }
.athshare-pick .tokpick-list { position: static; margin-top: 0; box-shadow: none; }
/* THE THREE GRANT LINES, each under the word for the tokens it governs — the
   offer's terms, at the grant line's own quiet wattage. */
.athshare-terms { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.athshare-term { font-weight: var(--b-weight-semibold); color: var(--text-primary); }

/* ══ BOARD C1 — FORWARD: the share family (TEN-1888, epic TEN-1838) ══════════
 *
 * Three pieces, appended at the tail on the ask card's precedent (a rung declared
 * later is the one that wins): the ktcal BUBBLE rung (K32 amended A), the chat
 * pane's two new bubble keys (`link`, `card`), and the share sheet.
 *
 * ── ktcal RUNG 4, THE BUBBLE BLOCK ──────────────────────────────────────────
 *
 * A BUBBLE IS A CELL, so this is the week/day block's anatomy at a DM's measure:
 * the same left rail, the same state ladder, and no height-means-duration (there
 * is no hour grid in a bubble to be true to). The booked/covered tints are the
 * SAME rules the block wears — green is a fact about the reader, and a bubble is
 * drawn once per reader. */
.ktcal-bubble {
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
  padding: 8px 10px; color: var(--text-primary);
  background: var(--surface-overlay);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--b-radius-sm);
  min-width: min(15rem, 100%);
}
.ktcal-bubble-n { margin: 0; font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); line-height: 1.25; }
.ktcal-bubble-when, .ktcal-bubble-facts { margin: 0; font-size: var(--b-text-xs); color: var(--text-muted); line-height: 1.35; }
.ktcal-bubble-body { width: 100%; padding-top: 2px; }
.ktcal-bubble-act { margin-top: 4px; }
.ktcal-bubble.ktcal-booked { background: var(--green-a); border-left-color: var(--role-presence); }
.ktcal-bubble.ktcal-covered { border-left-color: color-mix(in srgb, var(--role-presence) 55%, var(--border-subtle)); }
.ktcal-bubble.ktcal-full .ktcal-bubble-facts { color: var(--text-primary); }
.ktcal-bubble.ktcal-off .ktcal-bubble-n { text-decoration: line-through; color: var(--text-muted); }

/* ── THE ADDRESS IS THE MESSAGE: `link` and `card` on a bubble ─────────────
 *
 * A body that IS one link is an anchor, in the bubble's own ink with an
 * underline — the plain arm every other client shows. Where a card came back,
 * the card is the bubble's content and the link's text stays in the DOM, visually
 * hidden, as the `data-msg-text` Copy text reads — the address, never the drawing.
 * A card bubble loses its own padding to the card's, so the card sits flush in the
 * bubble's shape rather than a box inside a box. */
.msg-link { color: inherit; text-decoration: underline; text-underline-offset: 2px; overflow-wrap: anywhere; }
.msg-bubble-card { padding: 3px; }
.msg-bubble-card .msg-card { display: block; }
.msg-bubble-card .msg-body-link { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.msg-bubble-card .msg-foot { padding: 2px 6px 0; }
/* An ASK card inside a bubble is K46's bubble mount; the mount's own hairline
 * is the bubble's edge here, so the top rule and padding step aside. */
.msg-card .ktask-bubble { border-top: 0; padding: 7px 8px 5px; }
.msg-card .ktask-bubble .ktask-acts { flex-wrap: wrap; }

/* ── THE SHARE SHEET (board C1 screen 1) ──────────────────────────────────
 *
 * The board's order, top to bottom: what is shared, the recent faces over the one
 * field, Send, then the same link out of the app. Nothing here is a second picker:
 * the faces are links that fill the one select. */
.share { display: flex; flex-direction: column; gap: var(--b-space-4); max-width: 34rem; }
.share-subject { max-width: 24rem; }
.share-lede { margin: 0; color: var(--text-muted); font-size: var(--b-text-sm); }
.share-form { display: flex; flex-direction: column; align-items: flex-start; gap: var(--b-space-2); }
.share-form .ss-grid-q { width: 100%; }
.share-recents { display: flex; gap: var(--b-space-3); flex-wrap: wrap; }
.share-recent {
  display: inline-flex; flex-direction: column; align-items: center; gap: 3px;
  text-decoration: none; color: var(--text-muted); font-size: var(--b-text-xs);
}
.share-recent-av {
  width: 38px; height: 38px; border-radius: 50%; object-fit: cover;
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-subtle);
}
.share-recent:hover .share-recent-av, .share-recent:focus-visible .share-recent-av { border-color: var(--role-primary); }
.share-fanout { margin: 0; }
.share-out { display: flex; flex-direction: column; gap: var(--b-space-2); }
.share-link { width: 100%; font-family: var(--b-font-mono); font-size: var(--b-text-xs); }
.share-out-acts { display: flex; gap: var(--b-space-2); flex-wrap: wrap; }
.share-out-acts [hidden] { display: none; }

/* The Schedule row's door and My Network's invite door — launchers, so the link
 * tier and the secondary console button respectively (K38). */
.sfev-share { flex: none; font-size: var(--b-text-xs); }
.net-invite { margin: 0 0 var(--b-space-2); }
/* A face with no picture is its initials, never a broken image. */
.share-recent-initial {
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: var(--b-text-xs); color: var(--text-primary);
}

/* ── CLASSMATES — My Network's third tab (TEN-1859, board 19) ──────────────
 * THE CONTEXT LINE is the row's second line: the classes this person shares with
 * the reader, read and never worked out. Muted ink, because it is provenance
 * (the TINY tier), and a block so it sits under the name as the board draws it. */
.net-ctx { display: block; font-size: var(--b-text-xs); color: var(--text-muted); }
/* The relation word is DATA PRESENCE (K31) — a quiet fact, not a control. */
.net-relation { white-space: nowrap; }
.net-cm-caps { margin: 0 0 var(--b-space-3); }
/* THE GATE — the board's invitation card: the one primary act is List me, and
 * Not now is a way out that writes nothing. Primary-tinted edge, as drawn. */
.net-cm-gate { border-color: var(--role-primary); }
.net-cm-gate-title { margin: 0 0 var(--b-space-1); font-weight: var(--b-weight-semibold); color: var(--text-primary); }
.net-cm-gate-acts { display: flex; gap: var(--b-space-2); flex-wrap: wrap; margin-top: var(--b-space-2); }
.net-cm-footer { margin: var(--b-space-2) 0 var(--b-space-3); }
/* THE SETTINGS CARD — a disclosure over `ktrule` rows (below). */
.net-cm-settings > summary { cursor: pointer; font-weight: var(--b-weight-semibold); color: var(--text-primary); }
.net-cm-settings[open] > summary { margin-bottom: var(--b-space-2); }

/* ── THE RESOLVED-SETTING ROW — `ktrule` (K53, proposed; TEN-1890) ──────────
 * One row of an inherited-resolution setting: the label, the RESOLVED state with
 * the rule that decided it under the label, and the tri-state control on the
 * right. Minted out of the Classmates settings card (TEN-1859) when the
 * Organizations page's club links (TEN-1890) needed the same anatomy — two cards,
 * one family, so a person who has read one can read the other.
 *   .ktrule-resolved b.ktrule-yes / .ktrule-no — the resolved state's tone
 *   .ktrule-tri — the ktseg control; .ktrule-sel the stored segment,
 *   .ktrule-opt-yes / -no colour the stored segment by what it does. */
.ktrule { display: flex; align-items: center; justify-content: space-between; gap: var(--b-space-3);
  padding: var(--b-space-1) 0; border-bottom: var(--b-border-w) solid var(--border-subtle); flex-wrap: wrap; }
.ktrule:last-child { border-bottom: 0; }
.ktrule-resolved { display: block; font-size: var(--b-text-xs); color: var(--text-muted); }
.ktrule-resolved .ktrule-yes { color: var(--good); }
.ktrule-resolved .ktrule-no { color: var(--warn); }
.ktrule-tri .ktseg-opt { border: 0; font: inherit; font-size: var(--b-text-xs); padding: 4px 10px; }
.ktrule-tri .ktseg-opt.ktrule-sel { color: var(--text-primary); background: var(--surface-overlay); }
.ktrule-tri .ktrule-opt-yes.ktrule-sel { color: var(--good); background: var(--good-a); }
.ktrule-tri .ktrule-opt-no.ktrule-sel { color: var(--warn); background: var(--amber-wash); }

/* ── The entry-transaction audit page + the per-club history list (TEN-1900,
   board T-1900-B). Flat rows: when · a dot · one sentence. The TONE is the
   row's, drawn as a dot beside the sentence rather than a coloured row, so no
   colour is spent on a meaning another surface relies on (K29): confirmed is the
   kit's presence green, a warning its attention amber, an ending its danger
   role, and money a RING (a shape, not a hue — K29 reserves red for a price
   promo and green for "covered, a fact about you"). ─────────────────────────── */
.sfaudit { display: flex; flex-direction: column; gap: var(--b-space-4); max-width: 640px; }
.sfaudit-back { font-size: var(--b-text-sm); }
.sfaudit-headcard { flex-direction: row; justify-content: space-between; align-items: flex-start; gap: var(--b-space-3); padding: 14px 16px; }
.sfaudit-headmain { min-width: 0; }
.sfaudit-title { font-family: var(--b-font-heading); font-weight: var(--b-weight-bold); font-size: var(--b-text-lg); color: var(--text-primary); margin: 0; }
.sfaudit-when { font-weight: var(--b-weight-semibold); color: var(--text-muted); }
.sfaudit-headside { text-align: right; flex: none; }
.sfaudit-money { font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold); color: var(--text-primary); }
.sfaudit-story { gap: var(--b-space-2); padding: 14px 16px; }
.sfaudit-sechead { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.06em; font-size: 12px; font-weight: 700; color: var(--text-muted); margin: 0; }
.sfaudit-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.sfaudit-row { display: grid; grid-template-columns: 7.5em 12px 1fr; align-items: baseline; gap: var(--b-space-2); padding: 7px 0; border-bottom: 1px dotted var(--border-subtle); font-size: var(--b-text-sm); line-height: 1.5; }
.sfaudit-row:last-child { border-bottom: 0; }
.sfaudit-rowwhen { color: var(--text-muted); font-size: var(--b-text-xs); white-space: nowrap; }
.sfaudit-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--text-faint); align-self: center; }
.sfaudit-dot--ok { background: var(--role-presence); }
.sfaudit-dot--warn { background: var(--role-attention); }
.sfaudit-dot--bad { background: var(--role-danger); }
.sfaudit-dot--money { background: transparent; box-shadow: inset 0 0 0 2px var(--text-primary); }
.sfaudit-rowtext { color: var(--text-primary); min-width: 0; }
.sfaudit-zero, .sfaudit-note { margin: 0; }
.sfaudit-listhead { display: flex; flex-direction: column; gap: 2px; }
.sfaudit-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfaudit-entry { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--b-space-3); padding: 12px 16px; text-decoration: none; }
.sfaudit-entry:hover { background: var(--hover); }
.sfaudit-entrymain { display: flex; flex-direction: column; min-width: 0; }
.sfaudit-entryname { font-weight: var(--b-weight-semibold); color: var(--text-primary); }
.sfaudit-entryview { flex: none; font-size: var(--b-text-sm); }
@media (max-width: 480px) {
  .sfaudit-row { grid-template-columns: 12px 1fr; }
  .sfaudit-rowwhen { grid-column: 2; }
  .sfaudit-dot { grid-column: 1; grid-row: 1 / span 2; align-self: start; margin-top: 5px; }
}
/* TEN-1912 L3 dispute door */
.sfaudit-dispute { margin: var(--b-space-2) 0 0; }
.sfaudit-dispute-link { display: inline-flex; align-items: center; gap: 4px; }
/* The payment-history hop under each club's subscriptions (TEN-1900). */
.ss-subgroup-history { display: inline-block; margin-top: var(--b-space-2); font-size: var(--b-text-sm); }

/* ── The session page (TEN-1900, board T-1900-A). One column, phone-first:
   WHAT → WHERE → WHAT TO KNOW → THE ACT → THE RECORD. The chip keeps K29 —
   green only for a fact about YOU, the kit's danger role for a called-off
   session, the dark ground for live and nothing else. ───────────────────── */
.sfsess { display: flex; flex-direction: column; gap: var(--b-space-3); max-width: 640px; }
.sfsess-class { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--b-space-3); padding: 11px 14px; text-decoration: none; background: var(--surface-overlay); }
.sfsess-class:hover { background: var(--hover); }
.sfsess-classmain { display: flex; flex-direction: column; min-width: 0; }
.sfsess-classlabel { font-size: var(--b-text-xs); }
.sfsess-classname { font-weight: var(--b-weight-bold); color: var(--role-primary); }
.sfsess-classchip { flex: none; font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold); color: var(--text-muted); border: 1px solid var(--border-strong); border-radius: var(--b-radius-full); padding: 1px 8px; }
.sfsess-head { flex-direction: row; justify-content: space-between; align-items: flex-start; gap: var(--b-space-3); padding: 14px 16px; }
.sfsess-headmain { min-width: 0; }
.sfsess-when { font-family: var(--b-font-heading); font-size: var(--b-text-xl); font-weight: var(--b-weight-bold); margin: 0; color: var(--text-primary); }
.sfsess-sub { margin: 2px 0 0; }
.sfsess-chip { flex: none; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold); border: 1px solid var(--border-strong); border-radius: var(--b-radius-full); padding: 2px 10px; color: var(--text-muted); white-space: nowrap; }
.sfsess-chip--ok { color: var(--role-presence); border-color: var(--role-presence); background: var(--green-a); }
.sfsess-chip--bad { color: var(--role-danger); border-color: var(--role-danger); }
.sfsess-chip--live { color: var(--ink-on-primary); background: var(--text-primary); border-color: var(--text-primary); }
.sfsess-where { padding: 0; overflow: hidden; gap: 0; }
.sfsess-map { height: 120px; overflow: hidden; display: flex; align-items: center; justify-content: center; background: var(--surface-overlay); }
.sfsess-map img { max-width: none; height: auto; }
.sfsess-credit { margin: 0; padding: 2px 14px 0; font-size: 11px; }
.sfsess-whereline { display: flex; justify-content: space-between; align-items: center; gap: var(--b-space-3); padding: 9px 14px; font-size: var(--b-text-sm); }
.sfsess-place { color: var(--text-primary); min-width: 0; }
.sfsess-know { gap: var(--b-space-2); padding: 14px 16px; }
.sfsess-about { margin: 0; font-size: 14.5px; line-height: 1.55; color: var(--text-muted); }
.sfsess-whisper { margin: 0; font-style: italic; font-size: var(--b-text-xs); }
.sfsess-facts { margin: var(--b-space-1) 0 0; }
.sfsess-fact { display: flex; justify-content: space-between; gap: var(--b-space-3); padding: 4px 0; border-bottom: 1px dotted var(--border-subtle); font-size: var(--b-text-sm); }
.sfsess-fact:last-child { border-bottom: 0; }
.sfsess-fact dt { font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; font-weight: 700; color: var(--text-muted); }
.sfsess-fact dd { margin: 0; color: var(--text-primary); }
.sfsess-act { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfsess-ladder { flex-direction: row; align-items: center; flex-wrap: wrap; gap: var(--b-space-3); padding: 12px 16px; }
.sfsess-boundary { font-size: var(--b-text-sm); color: var(--role-attention); }
.sfsess-record { gap: 4px; padding: 12px 16px; background: var(--surface-overlay); }
.sfsess-rechead { margin: 0; font-family: var(--b-font-heading); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; font-weight: 700; color: var(--text-muted); }
.sfsess-recline { display: flex; justify-content: space-between; gap: var(--b-space-3); font-size: var(--b-text-sm); color: var(--text-primary); }
.sfsess-recview { font-size: var(--b-text-sm); }
.sfsess-qract { margin: 0; }
.sfsess-qr { display: flex; flex-direction: column; align-items: center; gap: var(--b-space-2); }
.sfsess-qr svg { width: 200px; height: 200px; background: #fff; padding: 8px; border-radius: var(--b-radius-md); }
.sfsess-qraddr { word-break: break-all; text-align: center; margin: 0; }
.kthow--embedded { margin: 0; }

/* TEN-1912 L2 staff console */
/* The club's Inbox (boards OI-B / OI-D): the tab's amber count, the list, one
   conversation with its K52 acting-as composer and the member panel, Settings ›
   Messages, and the Staff tab's who-answers facts. Amber is spent on "waiting on
   you" (K51) and violet on the reader's acting identity (K52) — nothing else. */
.orgc-count {
  display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px;
  border-radius: var(--b-radius-full); background: var(--role-attention); color: var(--surface-raised);
  font-size: 10.5px; font-weight: 700; line-height: 16px; text-align: center; vertical-align: middle;
}
.orgc-inbox-chips { display: flex; flex-wrap: wrap; gap: var(--b-space-2); margin-bottom: var(--b-space-3); }
.orgc-inbox-chip {
  display: inline-flex; align-items: center; gap: 4px; padding: 2px 10px;
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-full);
  font-size: var(--b-text-sm); color: var(--text-muted); text-decoration: none;
}
.orgc-inbox-chip-on { color: var(--text-primary); border-color: var(--b-accent); background: var(--blue-a); font-weight: 600; }
.orgc-inbox-chipcount { font-weight: 700; }
.orgc-inbox-row { align-items: flex-start; }
.orgc-inbox-press { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; color: inherit; text-decoration: none; }
.orgc-inbox-press:hover .orgc-person-name { text-decoration: underline; }
.orgc-inbox-top { display: flex; align-items: baseline; gap: var(--b-space-2); }
.orgc-inbox-when { margin-left: auto; font-size: var(--b-text-xs); color: var(--text-muted); white-space: nowrap; }
.orgc-inbox-new { font-size: var(--b-text-xs); font-weight: 700; color: var(--b-accent); }
.orgc-inbox-unread .orgc-person-name { font-weight: 700; }
.orgc-inbox-ctx {
  align-self: flex-start; padding: 0 8px; border-radius: var(--b-radius-full);
  border: var(--b-border-w) solid var(--role-attention); background: var(--amber-a);
  color: var(--role-attention); font-size: var(--b-text-xs); font-weight: 600;
}
.orgc-inbox-preview { font-size: var(--b-text-sm); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.orgc-inbox-by { font-size: var(--b-text-xs); font-style: italic; color: var(--text-muted); }
.orgc-inbox-open { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: var(--b-space-3); align-items: start; }
@media (max-width: 760px) { .orgc-inbox-open { grid-template-columns: minmax(0, 1fr); } }
.orgc-inbox-main { display: flex; flex-direction: column; gap: var(--b-space-3); min-width: 0; }
.orgc-inbox-acts { display: flex; justify-content: flex-end; align-items: center; gap: var(--b-space-2); }
.orgc-inbox-more { position: relative; }
.orgc-inbox-more > summary { list-style: none; cursor: pointer; }
.orgc-inbox-more > summary::-webkit-details-marker { display: none; }
.orgc-inbox-menu {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 5; min-width: 220px; padding: var(--b-space-1);
  background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-md);
}
.orgc-inbox-menuitem { display: block; padding: var(--b-space-2); color: var(--role-danger); text-decoration: none; font-size: var(--b-text-sm); }
.orgc-inbox-menuitem:hover { background: var(--surface-raised); }
.orgc-inbox-convo { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.orgc-inbox-msg { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; max-width: 88%; }
.orgc-inbox-msg-club { align-self: flex-end; align-items: flex-end; }
.orgc-inbox-bubble { display: flex; flex-direction: column; gap: 5px; padding: 8px 11px; border-radius: 14px; background: var(--surface-overlay); }
.orgc-inbox-msg-club .orgc-inbox-bubble { background: var(--role-primary); color: var(--ink-on-primary); }
.orgc-inbox-body { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.orgc-inbox-sig { font-size: var(--b-text-xs); color: var(--text-muted); }
.orgc-inbox-msg-club .orgc-inbox-sig { color: var(--role-identity); font-weight: 600; }
.orgc-inbox-ctxbox {
  display: flex; flex-direction: column; gap: 2px; padding: 6px 8px; font-size: var(--b-text-xs);
  border: var(--b-border-w) solid var(--role-attention); border-radius: var(--b-radius-sm); background: var(--amber-a);
}
.orgc-inbox-ctxtitle { font-weight: 700; }
.orgc-inbox-composer { display: flex; flex-direction: column; gap: var(--b-space-2); }
.orgc-inbox-acting {
  padding: 6px 10px; font-size: var(--b-text-sm); border-left: 3px solid var(--role-identity);
  background: var(--role-identity-soft); border-radius: 0 var(--b-radius-sm) var(--b-radius-sm) 0;
}
.orgc-inbox-actingword { font-weight: 700; color: var(--role-identity); }
.orgc-inbox-input { width: 100%; box-sizing: border-box; resize: vertical; }
.orgc-inbox-send { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--b-space-2); }
.orgc-inbox-whisper { font-size: var(--b-text-xs); font-style: italic; color: var(--text-muted); }
.orgc-inbox-panel { display: flex; flex-direction: column; gap: var(--b-space-2); }
.orgc-inbox-who { display: flex; align-items: center; gap: var(--b-space-2); }
.orgc-inbox-facts { display: grid; grid-template-columns: auto 1fr; gap: 4px var(--b-space-3); margin: 0; font-size: var(--b-text-sm); }
.orgc-inbox-facts dt { color: var(--text-muted); }
.orgc-inbox-facts dd { margin: 0; }
.orgc-inbox-links { display: flex; flex-wrap: wrap; gap: var(--b-space-3); margin: 0; font-size: var(--b-text-sm); }
.orgc-inbox-never { margin: 0; padding-top: var(--b-space-2); border-top: 1px dotted var(--border-subtle); font-size: var(--b-text-xs); color: var(--text-muted); }
.orgc-inbox-intakeedit > summary { list-style: none; display: inline-flex; cursor: pointer; }
.orgc-inbox-intakeedit > summary::-webkit-details-marker { display: none; }
.orgc-inbox-intakeedit[open] > summary { display: none; }
.orgc-inbox-opts { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.orgc-inbox-legend { padding: 0; margin-bottom: var(--b-space-1); font-weight: 600; }
.orgc-inbox-opt { display: flex; gap: var(--b-space-2); align-items: flex-start; padding: 8px 10px; border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md); cursor: pointer; }
.orgc-inbox-opt:has(input:checked) { border-color: var(--b-accent); background: var(--blue-a); }
.orgc-inbox-opt > span { display: flex; flex-direction: column; }
.orgc-inbox-hint { font-size: var(--b-text-xs); color: var(--text-muted); }
.orgc-inbox-intakeacts { display: flex; justify-content: flex-end; gap: var(--b-space-2); margin-top: var(--b-space-2); }
.orgc-inbox-answers { font-size: var(--b-text-xs); font-style: italic; color: var(--text-muted); }
.orgc-inbox-answers-on { font-style: normal; font-weight: 600; color: var(--text-primary); }
.orgc-inbox-hears { margin: var(--b-space-2) 0 0; font-size: var(--b-text-sm); font-style: italic; color: var(--text-muted); }
/* TEN-1912 L2 staff console: the "Answers club messages" switch. */
.orgc-inbox-switchform { display: inline-flex; margin: 0; }
.orgc-inbox-switch {
  display: inline-flex; align-items: center; gap: var(--b-space-2); padding: 0; border: 0; background: none;
  color: var(--text-primary); font: inherit; font-size: var(--b-text-xs); text-decoration: none; cursor: pointer;
}
.orgc-inbox-knob { position: relative; width: 34px; height: 19px; border-radius: var(--b-radius-full); background: var(--surface-overlay); border: var(--b-border-w) solid var(--border-strong); flex: none; }
.orgc-inbox-knob::after { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: var(--text-muted); }
.orgc-inbox-switch-on .orgc-inbox-knob { background: var(--b-accent); border-color: var(--b-accent); }
.orgc-inbox-switch-on .orgc-inbox-knob::after { left: 17px; background: var(--ink-on-primary); }
.orgc-inbox-switch:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 2px; border-radius: var(--b-radius-sm); }

/* TEN-1912 L1 member side — the club inbox (board OI-A). The K55 mark beside "Message the club";
   a club reply's signature line; a dispute's context card inside its bubble; the composer's one
   whisper under the box. Faces and chips reuse `.av-org` and `.conv-row-chip` unchanged. */
.sfmast-msg .ktic { margin-right: 8px; }
.msg-sig { margin: 0 0 0.2rem; font-size: var(--b-text-xs); font-weight: 600; color: var(--text-muted); }
.msg-ctx {
  display: flex; flex-direction: column; gap: 2px; margin: 0 0 0.35rem; padding: 6px 8px;
  border: 1px solid var(--border-subtle); border-radius: var(--b-radius-md); font-size: var(--b-text-xs);
}
.msg-club-note { margin: 0.35rem 0 0; }
/* TEN-1914 staff report: a member message's ··· menu and the report dialog's two fields. */
.orgc-inbox-meta { display: flex; align-items: center; gap: var(--b-space-1); }
.orgc-inbox-msgmore { display: inline-block; }
.orgc-inbox-msgtrigger { padding: 0 4px; border-radius: var(--b-radius-sm); color: var(--text-muted); font-weight: 700; }
.orgc-inbox-msgtrigger:hover, .orgc-inbox-msgmore[open] > .orgc-inbox-msgtrigger { background: var(--surface-overlay); color: var(--text-primary); }
.orgc-inbox-msgmore > .orgc-inbox-menu { left: 0; right: auto; min-width: 140px; }
.orgc-inbox-reportform { display: flex; flex-direction: column; gap: var(--b-space-3); }
.orgc-inbox-reportfield { display: flex; flex-direction: column; gap: var(--b-space-1); }
.orgc-inbox-reportlabel { font-weight: 600; }
.orgc-inbox-reportfield .field { width: 100%; box-sizing: border-box; }

/* TEN-1922 UAT-4 A */
/* THE MAP ACTS KEEP AN OPAQUE GROUND ON HOVER/FOCUS. `.plbanner-act` wears
   `.btn--ghost`, and that tier's hover ground is `var(--hover)` — a
   TRANSLUCENT overlay, correct everywhere else because a ghost button
   normally rests on a solid card. Here it rests on the map photo
   (`.plbanner-tile`), so the hover state let the photo show through the
   button: over a light patch of imagery the ink (`--text-primary`, often
   near-white) landed on near-white and the label read as gone — Mark's UAT,
   hovering "Open map" made it vanish. The fix keeps the SAME opaque ground
   every map act already wears AT REST (`.btn--ghost`'s `--surface-raised`,
   kit tokens, both themes) rather than stepping down to a tint: a
   translucent step — even a heavy one — still lets the photo pull
   `.plbanner-act-go`'s accent ink (`--role-primary-ink`, itself already
   near the 4.5:1 floor by design) under it in the dark theme. Fully opaque
   removes the photo from the equation on hover and on keyboard focus, the
   same as it already is at rest. */
.plbanner-act:hover,
.plbanner-act:focus-visible {
  background: var(--surface-raised);
  border-color: var(--text-faint);
  color: var(--text-primary);
}
/* Directions keeps its accent ink (the resting `.plbanner-act-go` rule's own
   call) rather than falling to the neutral ghost ink the rule above sets. */
.plbanner-act-go:hover,
.plbanner-act-go:focus-visible {
  color: var(--role-primary-ink);
}
/* TEN-1925 UAT-4 D */
/* ── THE TOKEN PICKER'S PANEL FLOATS (TEN-1925, Mark's UAT-4) ─────────────────
   picker.js now places the panel against the WINDOW — `data-tokpick-float`,
   `position: fixed`, left/top/min-width measured on every draw — which is K15's
   own construction ("THE PANEL IS PLACED AGAINST THE VIEWPORT") and the one an
   ancestor's `overflow` cannot clip. That retires the IN-FLOW rung three rules
   above gave the modals (`.ss-grid-modal`, `.ktmodal-panel`, `.athshare-pick`,
   TEN-1152 / TEN-1664): in flow, the panel pushed the dialog's content down as it
   opened and pulled it back as it closed, so the dialog bounced — and a blur on
   MOUSEDOWN shrank and re-centred it before MOUSEUP, which is why the chip's ✕
   and the dialog's Share each took two clicks. Floating, the panel OVERLAYS what
   is below it and closing it moves nothing.

   Out-specified rather than edited: `.tokpick > .tokpick-list[data-tokpick-float]`
   is (0,3,0) against those rules' (0,2,0), so every in-flow override loses to it
   the moment the script has placed the panel — and a page with no script never
   gets the attribute. The width is TEN-1703's (floor = the field, growth to the
   right, a ceiling); the height is K15's 6.5 rows; the shadow is the family's. */
.tokpick > .tokpick-list[data-tokpick-float] {
  position: fixed; right: auto; margin: 0;
  width: max-content;
  max-width: min(var(--pickpanel-max, 30rem), calc(100vw - 16px));
  max-height: calc(var(--ktsel-row) * 6.5 + 6px);
  box-shadow: var(--ktsel-lift);
}
/* FLIPPED UP (K15's collision clause): the join mirrors — the panel's seam edge is
   its BOTTOM, and the field's top edge is the recoloured hairline. */
.tokpick > .tokpick-list[data-tokpick-float][data-tokpick-up] {
  border-top: 1px solid var(--border-strong); border-bottom: 0;
  border-radius: var(--control-r) var(--control-r) 0 0;
}
.tokpick:has(> .tokpick-list[data-tokpick-up]:not(:empty)) > .tokpick-field {
  border-bottom-color: var(--border-strong);
  border-bottom-left-radius: var(--b-radius-sm); border-bottom-right-radius: var(--b-radius-sm);
  border-top-color: var(--border-subtle);
  border-top-left-radius: 0; border-top-right-radius: 0;
}

/* ── THE SHEET SHELL'S LEDE AND HELP CARD (TEN-1925) ─────────────────────────
   The title and its info mark ride one line; the ✕ keeps the head's right edge.
   The lede is ordinary text under the head — "normal text", Mark's words — at
   the body's own ink, not a hint's. */
.ktsheet-titlerow { display: inline-flex; align-items: center; gap: var(--b-space-2); min-width: 0; }
.ktsheet-lede { margin: 0; font-size: var(--b-text-sm); color: var(--text-primary); }
.ktsheet-help { position: relative; }
.ktsheet-help > summary { list-style: none; }
.ktsheet-help > summary::-webkit-details-marker { display: none; }
/* The cover's info mark at a size a thumb can find: the ring stays small, the
   hit area is the control height's half on every side (K12's hit-target floor). */
.ktsheet-helpmark {
  width: 18px; height: 18px; margin-left: 0; font-size: 11px; cursor: pointer;
  outline-offset: 3px;
}
.ktsheet-helpmark:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); }
.ktsheet-help[open] > .ktsheet-helpmark { color: var(--text-primary); border-color: var(--b-accent); }
/* THE CARD IS A LAYER OVER THE SHEET, not a block in it — it opens without
   moving the picker or the records under it (the same no-bounce rule as the
   picker's panel). The tip tier (70) is the cover's topmost, one clear of the
   modals, for the reason the `[data-tip]` block states. */
.ktsheet-helpcard {
  position: absolute; z-index: 70; left: 0; top: calc(100% + 6px);
  width: min(26rem, calc(100vw - 48px));
  padding: var(--b-space-3);
  background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--control-r);
  box-shadow: var(--ktsel-lift);
  text-transform: none; letter-spacing: normal; font-family: var(--b-font-body, inherit);
  font-weight: normal;
}
.ktsheet-helplines { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
.ktsheet-helplines li { margin: 0; font-size: var(--b-text-xs); line-height: 1.45; color: var(--text-muted); }
.ktsheet-helpterm { font-weight: var(--b-weight-semibold); color: var(--text-primary); }

/* ── THE TRAINING-SHARING MODAL (TEN-1925, Mark's UAT-4) ─────────────────────
   A FIXED HEIGHT, and the records are the one scroller. The panel is a column;
   the body stops being the sheet's scroll box (`.ktsheet-body`'s max-height) and
   becomes the column's flexible remainder, so hundreds of records scroll the
   list and never move the picker above them. `:has(.athshare)` is the shell's
   own per-body hook (`.ktsheet-panel:has(.ktmx)`'s precedent). */
.ktsheet-panel:has(.athshare) { height: min(36rem, 100%); }
.ktsheet-panel:has(.athshare) .ktsheet-body {
  flex: 1 1 auto; min-height: 0; max-height: none; overflow: visible;
  display: flex; flex-direction: column;
}
.ktsheet-body > .athshare { flex: 1 1 auto; min-height: 0; }
/* THE PICKER AND ITS SHARE ON ONE LINE, the button at the right. The field grows
   with its tokens, so the button holds the TOP of the row rather than drifting
   to the middle of a three-line field. The old offer's rule and its top rule are
   undone here: the offer is the head of the body now, not a footer under a list. */
.athshare .athshare-offer {
  flex-direction: row; align-items: flex-start; gap: var(--b-space-2);
  padding-top: 0; border-top: 0;
}
.athshare .athshare-pick { flex: 1 1 auto; min-width: 0; width: auto; }
.athshare .athshare-go { flex: none; }
/* THE RECORDS: one scroller, the rest of the dialog's height. */
.athshare .athshare-rows {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; gap: 0;
  border-top: var(--b-border-w) solid var(--border-subtle);
}
.athshare .athshare-row {
  flex-wrap: nowrap; gap: var(--b-space-2); padding: 6px 2px;
  border-top: 0; border-bottom: var(--b-border-w) solid var(--border-subtle);
}
.athshare-av { flex: none; }
.athshare-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; font-size: var(--b-text-sm); color: var(--text-primary); }
.athshare-row-rung .athshare-name { font-weight: var(--b-weight-semibold); }
/* THE ✕ IS A REAL SUBMIT, drawn as the head's own dismiss glyph at a thumb's
   size: muted at rest, primary on hover and focus. Its name is the row's
   ("Stop sharing with Sam Winchester"), on aria-label and on the cover's tip. */
.athshare-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--control-r);
  background: none; color: var(--text-muted); cursor: pointer; font-size: 14px; line-height: 1;
}
.athshare-x:hover, .athshare-x:focus-visible { color: var(--text-primary); background: var(--surface-overlay); }
.athshare-x:focus-visible { outline: var(--b-ring-w) solid var(--b-accent); outline-offset: 1px; }
/* A tip on the ✕ opens LEFTWARD and below — the button sits at the list's right
   edge, inside the list's own scroller. */
.athshare-x[data-tip]:hover::after { left: auto; right: 0; bottom: auto; top: calc(100% + 4px); }

/* TEN-1930 UAT-5 B */
/* THE OVERVIEW CATALOG BLOCK IS ONE PRESS (TEN-1930) — `.sfrow-linked`'s
   construction on the taste block: the name-and-line anchor (`sfcard-press`)
   is stretched over the block by its ::after, the hover owns the block's ground
   and edge, and the focus ring is the block's. Any act a block may carry is
   lifted above the stretched surface rather than nested in the anchor. */
.sfcard-press {
  display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
  color: inherit; text-decoration: none;
}
.sfcard-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.sfcard-linked:hover { background: var(--hover); border-color: var(--border-strong); }
.sfcard-press::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.sfcard-linked form, .sfcard-linked .btn { position: relative; z-index: 1; }
.sfcard-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.sfcard-press:focus-visible { outline: none; }
/* The plan page's standing (TEN-1930): the catalog card's green line/pill,
   in the quickview's act slot. */
.ktqv-have {
  margin: 0; display: flex; align-items: center; gap: 6px;
  font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
  color: var(--green-ink, var(--text-primary));
}
.ktqv-have-pill { display: block; color: inherit; }
/* END TEN-1930 UAT-5 B */

/* TEN-1932 UAT-5 C */
/* THE CATALOG'S PROGRAM CARD IS ONE PRESS (TEN-1932) — the same construction as
   `.sfcard-linked` above: the title's anchor (`ktoffer-press`) is stretched over
   the card by its ::after, the hover owns the card's ground and edge, and the
   focus ring is the card's. The Book/Enroll act is lifted above the stretched
   surface rather than nested in the anchor. */
.ktoffer-press { color: inherit; text-decoration: none; }
.ktoffer-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.ktoffer-linked:hover { background: var(--hover); border-color: var(--border-strong); }
.ktoffer-press::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.ktoffer-linked .btn, .ktoffer-linked a:not(.ktoffer-press) { position: relative; z-index: 1; }
.ktoffer-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.ktoffer-press:focus-visible { outline: none; }
/* END TEN-1932 UAT-5 C */

/* TEN-1942 UAT-6 B */
/* THE PLAN CARD IS ONE PRESS (TEN-1942) — `.ktoffer-linked`'s construction on
   the Memberships & passes card: the name's anchor (`ktplan-press`) is stretched
   over the card by its ::after, the hover owns the card's ground and edge, and
   the focus ring is the card's. The reach's program links, the standing pill's
   links and the act are lifted above the stretched surface, not nested. */
.ktplan-linked { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.ktplan-linked:hover { background: var(--hover); border-color: var(--border-strong); }
.ktplan-press:hover { text-decoration: none; }
.ktplan-press::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.ktplan-linked form, .ktplan-linked .btn, .ktplan-linked a:not(.ktplan-press) { position: relative; z-index: 1; }
.ktplan-linked:focus-within { box-shadow: 0 0 0 2px var(--role-primary); }
.ktplan-press:focus-visible { outline: none; }
/* END TEN-1942 UAT-6 B */

/* TEN-1929 UAT-5 A */
/* THE CLASS PAGE'S WAY IN — choose a plan, pick the first session, confirm. The
   step pages wear the landing family's trail and title; what they add is the
   step kicker (how far along the reader is) and the chosen plan pinned under the
   title with its "Change plan" way back. */
.sfway { display: flex; flex-direction: column; gap: var(--b-space-3); max-width: 44rem; }
.sfway-step {
  margin: 0; font-size: var(--b-text-xs); font-weight: var(--b-weight-bold);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted);
}
.sfway-plan { margin: 0; }
/* END TEN-1929 UAT-5 A */

/* TEN-1934 UAT-5 E */
/* THE SCHEDULE'S SESSION CARD PEEKS (K50). A public row's name is now the
   trigger of its session quickzoom and `.sfev-row-linked` stretches it over the
   card, as on the console's rows. Unlike the admin row, a public row DOES hold
   second presses — the Book act and the athlete's share door — so those are
   LIFTED above the stretched surface: each stays its own press, and the card
   around them is the peek. */
.sfev-row-linked .sfev-end a, .sfev-row-linked .sfev-end button { position: relative; z-index: 1; }
/* The live band keeps its ONE green ground under the pointer (K29): hover is a
   position, never a change of which band a row is in. */
.sfev-row-live.sfev-row-linked:hover { background: var(--green-a); border-color: var(--green-line); }
/* THE SHARE DOOR IS AN ICON BUTTON (K13 line art, the sprite's `ktg-share`): a
   square ghost button the glyph sits centred in, sized to a thumb. */
.sfev-share.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.25rem; min-height: 2.25rem; padding: var(--b-space-1);
}
/* The peek's next-gate sentence — the session page's own boundary line. */
.ktcalev-note { margin: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
/* The org hop sits beside the page link at the row's right end; the page link
   stays last (K50). */
.ktcalev-org { margin-left: auto; }
.ktcalev-org + .ktcalev-page { margin-left: 0; }
/* END TEN-1934 UAT-5 E */

/* TEN-1944 UAT-6 D */
/* The session peek's club face (K47, cmc-auth's compact lockup) leads the card,
   set off from the session's own facts by a hairline — it answers "whose is
   this", which is a different question from "when and where". */
.ktcalev-orgcard { padding-bottom: var(--b-space-2); border-bottom: var(--b-border-w) solid var(--border-subtle); }
.ktcalev-orgcard .sfmast-lockup { margin: 0; }
/* The fetched sheet's wait line, while the share page is on its way. */
.ktsheet-wait { margin: 0; }
/* The share sheet's message box: the chat composer's own field treatment. */
.share-note {
  width: 100%; box-sizing: border-box; resize: vertical; font: inherit;
  padding: var(--b-space-2); background: var(--surface-overlay); color: var(--text-primary);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-sm);
}
/* A share's words sit ABOVE its card, inside the one bubble — the bubble's own
   text treatment, padded to the card's inset. */
.msg-note { margin: 0; padding: 5px 7px 4px; white-space: pre-wrap; overflow-wrap: anywhere; }
/* END TEN-1944 UAT-6 D */

/* TEN-1947 UAT-6 G */
/* THE OVERVIEW'S STAFF CARD PEEKS, AND ITS FACE HOPS (K50, the avatar rule). Two
   presses on one card and never one anchor inside another: the name block's
   anchor (`sfperson-press`) is stretched over the card by its ::after — the
   TEN-1932 construction — and the face's anchor is lifted above it, so a press
   on the face goes to the person's page and a press anywhere else opens their
   staff bio quickzoom. Hover owns the card's ground and edge; the focus ring is
   the card's for the press and the face's own for the face. */
.sfperson-peekable { position: relative; transition: background-color 0.12s ease, border-color 0.12s ease; }
.sfperson-peekable:hover { background: var(--hover); border-color: var(--border-strong); }
.sfperson-press { color: inherit; text-decoration: none; }
.sfperson-press::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.sfperson-press:focus-visible { outline: none; }
.sfperson-peekable:has(.sfperson-press:focus-visible) { box-shadow: 0 0 0 2px var(--role-primary); }
.sfperson-face { position: relative; z-index: 1; flex: none; display: inline-flex; border-radius: var(--b-radius-full); }
.sfperson-face:focus-visible { outline: 2px solid var(--role-primary); outline-offset: 2px; }
/* The quickzoom's body: the staff bio card, then the page link last. */
.sfperson-peek .sfperson-card { border: 0; padding: 0; background: none; }
.sfperson-peek-acts { margin: var(--b-space-3) 0 0; }
/* The All staff row a scriptless card press lands on. */
.sfperson:target { border-color: var(--role-primary); }
/* END TEN-1947 UAT-6 G */
/* TEN-1943 UAT-6 C */
/* THE SESSION PAGE'S CONFIRMATION (K51's block rung, third instance). The
   way-in flow's last step waits on the reader, so it rides straight under the
   parent-class card in the call-to-action block rather than in the act zone
   below the fold. The page's column already spaces its blocks (`.sfsess`'s
   gap), so the block's own bottom margin is dropped; the embedded how sheet
   inside it keeps its own anatomy. */
.sfsess-confirm { margin-bottom: 0; display: flex; flex-direction: column; gap: var(--b-space-2); }
/* THE AMBER IS A GROUND AND AN EDGE, NEVER THE WORDS (K51): on this ground the
   act's deadline sentence steps back to the resting muted tier instead of the
   attention ink it wears in the ordinary act zone. */
.sfsess-confirm .sfsess-boundary { margin: 0; color: var(--text-muted); }
/* TEN-1961: the booking's follow, disclosed under the confirm or the act. */
.sfsess-follows { margin: var(--b-space-2) 0 0; font-size: var(--b-text-sm); color: var(--text-muted); }
/* END TEN-1943 UAT-6 C */

/* TEN-1941 UAT-6 A */
/* K46's QUEUE MOUNT (.ktasklist): a stack of row-mount ask cards under one
   section head — the Payments desk's plan requests are the first. It is the row
   mount laid out for a list somebody works down: the asker's face and name, the
   ask under it, and the answer pair on the right, one hairline between asks.
   No ground and no rail of its own: the amber rail `.recbox` wore is the
   attention family's (K51), and a queue of asks is not a warning. The pair is
   `.btn` and nothing else, so K12 still owns what an act looks like. */
.ktasklist { margin: 0 0 var(--b-space-4); }
.ktasklist > .polhint { margin: 0 0 var(--b-space-2); }
.ktasklist .ktask-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--b-space-4); row-gap: 2px; align-items: center;
  padding: var(--b-space-3) 0;
}
.ktasklist .ktask-row .ktask-who, .ktasklist .ktask-row .ktask-say { grid-column: 1; }
.ktasklist .ktask-row .ktask-face { width: var(--b-avatar-md); height: var(--b-avatar-md); font-size: 13px; object-fit: cover; }
.ktasklist .ktask-row .ktask-say {
  padding-left: calc(var(--b-avatar-md) + 8px);
  font-size: var(--b-text-sm); color: var(--text-muted);
}
.ktasklist .ktask-row .ktask-acts { grid-column: 2; grid-row: 1 / span 2; }
@media (max-width: 520px) {
  .ktasklist .ktask-row { grid-template-columns: minmax(0, 1fr); }
  .ktasklist .ktask-row .ktask-acts { grid-column: 1; grid-row: auto; margin-top: var(--b-space-2); }
}
/* END TEN-1941 UAT-6 A */

/* TEN-1951 */
/* THE PLAN PAGE, DIRECTION A — "THE TICKET" (board TEN-1946, blessed by Mark
   2026-09-30). Left, the plan explains itself; right, the pinned purchase card;
   at phone width the act moves to a bar pinned to the foot of the screen.

   FONTS ARE THE COVER'S TOKENS AT BUNDLED WEIGHTS ONLY (Mark's condition):
   --b-font-heading (Barlow Condensed 600/700/800/900), --b-font-body (Barlow
   400/500/600 — never 700, which the board drew on some names and which would
   be synthesized), --b-font-mono (JetBrains Mono 400/500/700). Every number is
   the heading face with tabular numerals. A browser gate asserts the computed
   family and weight of the title, the price and the tiles. */

/* THE PAGE TAKES THE BOARD'S 1040px, by the :has() move the kit page and the
   class hub already use — scoped to this page alone. */
@supports selector(:has(*)) {
  body.app:has(.sfplan-ticket) main.container { max-width: 1040px; }
}
.sfplan-ticket p, .sfplan-ticket h1, .sfplan-ticket h2, .sfplan-ticket h3 { margin: 0; }
.sfplan-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--b-space-6); align-items: start;
}
.sfplan-main { display: flex; flex-direction: column; gap: var(--b-space-5); min-width: 0; }
.sfplan-id { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfplan-eyebrow {
  font-family: var(--b-font-heading); font-weight: var(--b-weight-bold);
  font-size: var(--b-text-sm); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-muted);
}
/* THE DISPLAY TITLE — the --b-text-display token's first consumer. */
.sfplan-title {
  font-family: var(--b-font-heading); font-weight: 800;
  font-size: var(--b-text-display); line-height: 0.98;
  text-transform: uppercase; letter-spacing: 0.005em; text-wrap: balance;
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
  color: var(--text-primary);
}
.sfplan-lede { font-size: var(--b-text-lg); color: var(--text-muted); max-width: 46ch; }
.sfplan-sec {
  font-family: var(--b-font-heading); font-weight: var(--b-weight-bold);
  font-size: var(--b-text-lg); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-primary);
}

/* K40 ON A PUBLIC PAGE — the strip density (TEN-1951). The same tiles joined
   into one ruled strip: four-up on a desk, two-up on a phone. The value is the
   heading face at 800 with tabular numerals; the words are the body face. */
.ktcensus-strip {
  gap: 0; flex-wrap: nowrap;
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  font-variant-numeric: tabular-nums;
}
.ktcensus-strip .ktcensus-tile {
  flex: 1 1 0; background: none; border: 0; border-radius: 0;
  padding: var(--b-space-3) var(--b-space-4);
}
.ktcensus-strip .ktcensus-tile + .ktcensus-tile { border-left: var(--b-border-w) solid var(--border-subtle); }
.ktcensus-strip .ktcensus-v { font-weight: 800; line-height: 1.05; font-variant-numeric: tabular-nums; }
.ktcensus-strip .ktcensus-k {
  font-family: var(--b-font-body); text-transform: none; letter-spacing: 0.02em;
  font-weight: var(--b-weight-semibold); color: var(--text-muted);
}

.sfplan-inc { display: flex; flex-direction: column; gap: var(--b-space-3); }
.sfplan-inc-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--b-space-3); flex-wrap: wrap; }
.sfplan-progs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--b-space-3); }
.sfplan-pc {
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  padding: var(--b-space-3) var(--b-space-4) var(--b-space-4);
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-width: 0;
}
/* THE PROVENANCE PIN (K23): the program the reader came in on, first and
   green — green because it is a fact about the reader (K29). */
.sfplan-pc--origin { border-color: var(--green-line, var(--border-strong)); }
.sfplan-pc-pin {
  font-size: var(--b-text-xs); font-weight: var(--b-weight-semibold);
  border-radius: var(--b-radius-full); padding: 2px 10px;
  color: var(--green-ink, var(--text-primary)); background: var(--green-b, var(--green-a));
}
.sfplan-pc-name { font-family: var(--b-font-body); font-weight: var(--b-weight-semibold); font-size: var(--b-text-base); line-height: var(--b-leading-tight); }
.sfplan-pc-name a { color: var(--text-primary); }
.sfplan-pc-name a:hover { text-decoration: underline; }
.sfplan-pc-line { display: flex; align-items: center; gap: 7px; min-width: 0; font-size: var(--b-text-sm); color: var(--text-muted); }
.sfplan-pc-line a { color: inherit; }
.sfplan-pc-line a:hover { color: var(--text-primary); text-decoration: underline; }
.sfplan-ticket .ktic { width: 16px; height: 16px; color: var(--text-muted); }
.sfplan-pc-next {
  margin-top: 4px; font-family: var(--b-font-mono); font-weight: 500;
  font-size: var(--b-text-xs); letter-spacing: -0.01em; text-transform: uppercase;
  font-variant-numeric: tabular-nums; color: var(--text-muted);
}
.sfplan-not { font-size: var(--b-text-sm); color: var(--text-muted); }
.sfplan-also { display: flex; flex-direction: column; gap: var(--b-space-2); }
.sfplan-also-row { display: flex; flex-wrap: wrap; gap: var(--b-space-2); }
.sfplan-also-card {
  display: flex; flex-direction: column; gap: 2px; padding: var(--b-space-2) var(--b-space-3);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
  background: var(--surface-raised); color: var(--text-primary); font-size: var(--b-text-sm);
}
.sfplan-also-card:hover { border-color: var(--border-strong); }
.sfplan-also-name { font-weight: var(--b-weight-semibold); }

/* THE PINNED ACT RAIL, DESK RUNG — the sticky purchase card. */
.ktactrail { position: sticky; top: var(--b-space-4); display: flex; flex-direction: column; gap: var(--b-space-3); }
.sfplan-card {
  background: var(--surface-raised);
  border: var(--b-border-w) solid var(--border-strong); border-radius: var(--b-radius-lg);
  padding: var(--b-space-5); display: flex; flex-direction: column; gap: var(--b-space-4);
  box-shadow: var(--b-shadow-md);
}
.sfplan-price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; }
.sfplan-amt {
  font-family: var(--b-font-heading); font-weight: 800; font-size: 2.6rem; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--text-primary);
}
.sfplan-per { font-size: var(--b-text-base); font-weight: 500; color: var(--text-muted); }
.sfplan-facts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; font-size: var(--b-text-sm); }
.sfplan-facts li { display: flex; align-items: center; gap: 8px; }
.sfplan-have {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--b-text-sm); font-weight: var(--b-weight-semibold);
  color: var(--green-ink, var(--text-primary));
}
.sfplan-have-pill { display: block; color: inherit; }
.sfplan-note { font-size: var(--b-text-sm); }
.sfplan-act { margin: 0; }
.ktactrail .btn--block { width: 100%; }
.sfplan-foot { text-align: center; }
.sfplan-compare { font-size: var(--b-text-sm); }
.sfplan-seller {
  display: flex; align-items: center; gap: 10px;
  padding: var(--b-space-3) var(--b-space-4);
  border: var(--b-border-w) solid var(--border-subtle); border-radius: var(--b-radius-md);
}
.sfplan-seller p { font-size: var(--b-text-sm); line-height: 1.3; }
.sfplan-seller-by { font-weight: var(--b-weight-semibold); }
.sfplan-seller-by a { color: var(--text-primary); }
.sfplan-seller-by a:hover { text-decoration: underline; }

/* THE PINNED ACT RAIL, PHONE RUNG — kit-act-bar. Absent from the markup when
   there is no act (K30); hidden on a desk, where the card carries the act. */
.ktactbar { display: none; }

@media (max-width: 720px) {
  .sfplan-grid { grid-template-columns: minmax(0, 1fr); gap: var(--b-space-5); }
  .ktcensus-strip { flex-wrap: wrap; }
  .ktcensus-strip .ktcensus-tile { flex: 1 1 50%; }
  .ktcensus-strip .ktcensus-tile:nth-child(odd) { border-left: 0; }
  .ktcensus-strip .ktcensus-tile:nth-child(n+3) { border-top: var(--b-border-w) solid var(--border-subtle); }
  .sfplan-progs { grid-template-columns: minmax(0, 1fr); }
  .sfplan-lede { font-size: var(--b-text-base); }
  .ktactrail { position: static; }
  .sfplan-card { box-shadow: none; padding: var(--b-space-4); gap: var(--b-space-3); }
  /* ONE ACT PER WIDTH: the card's act yields to the bar. */
  .sfplan-card .ktactrail-act { display: none; }
  .ktactbar {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    position: sticky; bottom: 0; z-index: 20;
    margin: var(--b-space-4) calc(var(--b-space-3) * -1) 0;
    padding: 10px 14px calc(14px + env(safe-area-inset-bottom, 0px));
    background: var(--bg); border-top: var(--b-border-w) solid var(--border-subtle);
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.18);
  }
  .ktactbar-p { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; margin: 0; }
  .ktactbar-price {
    font-family: var(--b-font-heading); font-weight: 800; font-size: 1.45rem;
    font-variant-numeric: tabular-nums; color: var(--text-primary);
  }
  .ktactbar-line { font-size: var(--b-text-xs); color: var(--text-muted); }
  .ktactbar-act { margin: 0; flex: none; }
  .ktactbar .btn { padding: 0 16px; }
}
/* END TEN-1951 */

/* TEN-1952 — the clocks' gap: a time the place's clocks skip is warned about
   beside the field (never rewritten), and a preview row that will not happen is
   struck through and says why. */
.kttime-checked { flex-wrap: wrap; }
.kttime-error, .kttime-warn { flex-basis: 100%; margin-top: 6px; font-size: 13px; line-height: 1.4; color: var(--role-danger); }
.kttime-warn[hidden] { display: none; }
.ktrecur-nextitem-gap time { text-decoration: line-through; color: var(--text-muted); }
.ktrecur-nextitem-gap .ktrecur-nextmeta { color: var(--role-danger); }
/* TEN-1964 — the repeated hour: on the night the clocks go back a time happens
   twice, and the box asks which one (two radios, labelled by compose.ZoneLabel);
   a preview row the schedule names in that hour is marked, never resolved. */
.kttime-fold { flex-basis: 100%; margin: 8px 0 0; padding: 0; border: 0; display: grid; gap: 6px; min-width: 0; }
.kttime-fold[hidden] { display: none; }
.kttime-fold-legend { padding: 0; margin-bottom: 2px; font-size: 13px; font-weight: 600; color: var(--text-primary); }
.kttime-fold-option .ktradio-title { font-size: 14px; line-height: 1.4; }
.ktrecur-nextitem-fold time { text-decoration: line-through; color: var(--text-muted); }
.ktrecur-nextitem-fold .ktrecur-nextmeta { color: var(--role-danger); }
.clssess-name .clssess-nameacts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
/* the staff warning for sessions the clocks skip */
.clshub-gaps { border-color: var(--role-danger); }
.clshub-gaps-list { margin: 8px 0; padding-left: 20px; }
.clshub-gaps-list li { margin: 2px 0; }
/* /TEN-1952 */
/* TEN-1960 UAT-8 D */
/* A GROUP ROW IN A PEOPLE PICKER WEARS THE SIGIL TILE (ADR-0033 amended, rulings
   ledger "ADR-0033 AMENDED — PROPOSED (TEN-1960)"). One of the reader's Lists is
   offered beside people in the session dialog's "Who this is for", and its badge
   is the `#` it is typed with, drawn in the disc's place on a SQUARE tile — the
   room tile's own rule (`.msg-row-room .console-person-initial`): it stands for a
   set of people, not a face, so it reads as a label rather than an avatar. */
.console-person-initial.tokpick-sigil { border-radius: var(--b-radius-sm); }
/* END TEN-1960 UAT-8 D */
/* TEN-1958 */
/* A REFUSED SUBMIT COMES BACK IN THE DIALOG (kit-modal's `error` + `matches`
   rungs). The sentence wears the flash's own error voice (`.flash-error`'s
   tokens), because it IS that flash, printed where it belongs. The look-alike
   rows are a quiet list — a hairline between rows (K2's gap rule) — each with
   its facts on the left and its one act on the right; at a phone's width the
   act drops under the facts rather than squeezing them. */
.ktmodal-error { margin: 0; padding: 10px 12px; border-radius: var(--control-r); background: var(--bad-a); color: var(--bad-fg); border: var(--b-border-w) solid var(--role-danger); font-size: 14px; line-height: 1.5; }
.ktmodal-matches { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.ktmodal-matchlist { list-style: none; margin: 0; padding: 0; width: 100%; }
.ktmodal-match { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; flex-wrap: wrap; }
.ktmodal-match + .ktmodal-match { border-top: 1px solid var(--border-subtle); }
.ktmodal-match-facts { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 200px; }
.ktmodal-match-name { font-weight: 600; color: var(--text-primary); overflow-wrap: anywhere; }
.ktmodal-match-act { flex: none; }
/* END TEN-1958 */
/* TEN-1959 */
/* A FIELD'S REFUSAL LIVES ON THE FIELD (K58, proposed). The kit's INVALID STYLE
   keys off `aria-invalid="true"` on the real control — the same attribute a
   screen reader announces — so the marking and the semantics cannot disagree:
   the skinned box takes the danger edge (and the danger ring while focused, so
   focus never paints a refused box in the "all good" hue), the sentence sits
   under it in the danger ink, the field group's own label turns with it, and the
   card holding it draws a danger edge and rail. Colour carries the warning only
   alongside words (K29): every one of these has the server's sentence beside it. */
.ktinput:has([aria-invalid="true"]),
.msg-composer:has([aria-invalid="true"]) { border-color: var(--role-danger); }
.ktinput:has([aria-invalid="true"]):focus-within,
.msg-composer:has([aria-invalid="true"]):focus-within { border-color: var(--role-danger); box-shadow: 0 0 0 3px var(--bad-a); }
.ktfield-error { display: block; flex-basis: 100%; margin-top: 4px; font-size: 13px; line-height: 1.4; color: var(--role-danger); }
:where(.ktfield, .clsfield, .clsdesc-facts):has([aria-invalid="true"], .ktfield-error) > .ktlabel { color: var(--role-danger); }
.ktcard.ktcard-invalid { border-color: var(--role-danger); box-shadow: inset 3px 0 0 var(--role-danger); }
.ktcard-invalid > .ktlabel { color: var(--role-danger); }
.clsverdict-go { color: inherit; text-decoration: underline; }
/* the fact row's remove: the kit's ✕, beside the row's two boxes */
.clsdesc-row { align-items: center; }
.clsdesc-x { flex: none; }
.clsdesc-x[hidden] { display: none !important; }
/* END TEN-1959 */
/* TEN-1911 */
/* THE CLUB MASTHEAD'S VENUE LINE ("Runs at <place> · <place> · +N more"). It is
   `.sfmast-where`'s quiet row in every respect, and it sits UNDER the top row
   beside the bio rather than in the text column (a third line there makes the
   column taller than the face and drags the face off the banner), so it takes
   the bio's inset. The names are links and wrap like words; one long building
   name breaks rather than pushing the page sideways. */
.sfmast-venues { padding: 0 var(--b-space-1); }
.sfmast-venues > span { min-width: 0; overflow-wrap: anywhere; }
/* END TEN-1911 */
/* TEN-1910 */
/* MY CLUBS AND COACHES: THE CLUB'S LISTED STAFF ON ITS CARD (K49 Org → staff).
   Everything inside is the club page's own drawing — `.sfsec-head` for the
   eyebrow and "All staff ›", `sfpeople-door`'s peekable cards for the rows —
   so this block owns only the strip's stack inside the card: the head over the
   rows at the card's own rhythm (`.circles-org`'s gap). */
.circles-staff { display: flex; flex-direction: column; gap: var(--b-space-2); }
/* END TEN-1910 */
