/* ============================================================================
   app.css — shared components.
   Depends on tokens.css + base.css. Class names are plain English.
   Every value comes from tokens.css. If you need a new value, add a token.
   ========================================================================= */

/* ============================================================== PANEL =====
   One recipe, THREE elevations, and the elevation is where the emphasis is
   spent. Before this, every panel on every page carried the same fill, the
   same 3px cyan top edge and the same single 0 15px 35px drop — seven of them
   stacked on the Schedule page at identical altitude, so the eye had no
   reason to prefer one, and the accent edge had stopped meaning anything
   because everything had one. One is the same as none.

     .panel        elevation 2. The default. --panel-fill, quiet edge.
     .panel-lead   elevation 3 + the cyan accent edge. ONE per page. This is
                   the thing the page is for.
     .panel-quiet  elevation 1, flat with the floor. A grouping, not a
                   surface: structure drawn rather than shaded.

   The accent edge now travels with .panel-lead rather than with .panel, so
   spending it is a deliberate act. DESIGN.md §4's "exactly one accent edge
   per panel" still holds and is still measured; what changed is that most
   panels now correctly have zero.

   RADIUS IS ASYMMETRIC on any accented surface: square on the accented side,
   4px away from it, so the accent bar runs corner to corner and the panel
   hangs off it. A 4px radius all round left the cyan edge stopping 4px shy at
   both ends, reading as a coloured border rather than as a bar the surface is
   mounted on. The ceiling stays 4px; this costs nothing and is most of why
   the map's chips look like hardware.

   No backdrop-filter here, on purpose: a panel is a long static region the
   page scrolls past, and DESIGN.md §4 reserves blur for surfaces that
   genuinely overlap something (nav, tab bar, sheet, toast, gate).
   ========================================================================= */
.panel {
  position: relative;
  background: var(--panel-fill);
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-2);
  padding: var(--space-5);
}
@media (min-width: 720px) { .panel { padding: var(--space-6); } }

/* THE lead panel. Higher fill, deeper stack, and the one accent edge. */
.panel-lead {
  background: var(--panel-fill-hi);
  border-top: var(--edge) solid var(--accent);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  box-shadow: var(--elev-3);
}

/* Edge variants — pick at most one per panel. Each squares the corners on the
   side its edge is on. */
/* Moving the edge must move the colour too: narrowing border-top to a hairline
   without also resetting border-top-color leaves a second, thinner accent edge
   across the top — two accent edges on one panel, which DESIGN.md §4 bans. */
.panel-left {
  border-top: 1px solid var(--chrome-line);
  border-left: var(--edge) solid var(--accent);
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
}
.panel-bottom {
  border-top: 1px solid var(--chrome-line);
  border-bottom: var(--edge) solid var(--accent);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
/* Demotion, and it is a real one: down a fill step, down to elevation 1, no
   accent. This is what a panel looks like when it is holding something the
   page is not about. */
.panel-quiet {
  background: transparent;
  border-color: var(--chrome-line);
  border-top: 1px solid var(--chrome-line);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-0);
}
/* `.panel-good` and `.panel-danger` are DELETED. They were a status-coloured
   accent edge on a whole panel, and on the Schedule page that is precisely how
   the "one lead edge per page" rule was being broken without anyone typing
   `.panel-lead` twice: the availability panel carried the cyan edge and "Best
   dates" carried a mint one, so the page rendered two accent-edged panels in
   every arrangement and at every width. Status belongs to the thing that has a
   status — a square, a pill, a session card's left edge — never to the surface
   holding it. `.panel-danger` shipped on zero pages; `.panel-good` shipped on
   one, where it was the second edge. */

/* A well: a RECESSED area inside a panel. Notes, previews, read-only blocks,
   and the bed the availability grid sits in. The inner shadow is the exact
   inverse of a raised surface's — dark along the top edge, lit along the
   bottom — because a hole is lit by the same lamp everything else is. That
   inversion is the whole reason a well reads as depth rather than as a
   darker rectangle. */
.well {
  background: var(--surface-sunk);
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius);
  box-shadow: var(--elev-sunk);
  padding: var(--space-4);
}

/* Panel header. The rule under it is the Mothership print device, and it is
   now actually printed: a 3px DOUBLE rule at 34% rather than a 2px line at
   22%, which rendered as a slightly thick hairline and carried none of the
   "structure is drawn rather than shaded" the device exists for. Border-style
   is the second structural channel this site was not using: solid is a fact,
   dashed is inherited or provisional, double is a printed rule. It appears
   here and under a sheet heading, and nowhere else. */
.panel-head {
  display: flex;
  flex-wrap: wrap;      /* a heading plus a control must not push a phone sideways */
  align-items: baseline;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  border-bottom: var(--rule-print) double var(--chrome-rule);
}
.panel-head h2, .panel-head h3 { margin: 0; }

/* The sentence under a panel heading that explains what the controls below it
   do. It is PROSE — read once, by somebody deciding whether to touch
   anything — and it shipped as `.meta`: 13px, the size this system reserves
   for timestamps and helper text. That is most of why the Schedule page
   measured 206 of 230 rendered text nodes at 13-14px while the Wiki and
   Sessions pages broadened: the front door set every sentence it owned at the
   bottom rung of the ladder. Same argument base.css already makes for the
   page intro, and the same measure, because both are read once and fast. */
.note {
  font-size: var(--size-body);
  color: var(--text-secondary);
  max-width: var(--measure-lead);
}
.panel-head .row-end { font-size: var(--size-meta); color: var(--text-secondary); }

/* Section label. One per panel at most. LEFT-aligned in a stretched box, so
   it takes no correction: the trailing letter-space is on the right, where
   nothing meets it, and the negative indent this used to carry was pushing
   the L of "MARKING" 1.4px left of the column every other line on the panel
   shares. Measured: padLeft went from -1.39px to 0. base.css §optical. */
.label {
  display: block;
  font-size: var(--size-label);
  font-weight: var(--weight-black);
  letter-spacing: var(--track-header);
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* ============================================================= BUTTONS ====
   Elevation and interaction are linked, and that link is the whole physical
   argument: a button at rest sits at elevation 1, a hovered button rises to
   2, and a pressed button LOSES its height — the drop goes inside, because
   the surface has taken load. The transform and the shadow change in the
   same 90ms so the two never disagree.

   Four button classes with four different jobs get four different state
   matrices. They are not one treatment tinted four ways.
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-comfort);
  /* The trailing letter-space is taken off the RIGHT padding, not off the
     front of the line: it is a real space sitting inside the box after the
     last glyph, so the only way to centre the ink is to stop the box counting
     it. This is correct whether the button hugs its label or is stretched by
     .btn-full, which a text-indent is not. base.css §optical. */
  padding: 0 calc(var(--space-5) - var(--track-header)) 0 var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: var(--size-label);
  font-weight: var(--weight-black);
  letter-spacing: var(--track-header);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  /* Colour and border acknowledge the pointer on the house curve. Elevation
     is part of the same acknowledgement so it shares that timing. Transform
     is direct manipulation and gets the press duration, and only that. */
  transition:
    background-color var(--dur-hover) var(--ease),
    border-color var(--dur-hover) var(--ease),
    color var(--dur-hover) var(--ease),
    box-shadow var(--dur-hover) var(--ease),
    transform var(--dur-press) var(--ease);
}
.btn:active {
  transform: translateY(1px);
  box-shadow: var(--elev-press);
  /* The one place a duration is allowed to go DOWN: a press must not wait on
     the pointer timing to show that it took. */
  transition-duration: var(--dur-press);
}

.btn-primary {
  background: var(--accent);
  color: var(--text-on-accent);
  /* A dark drop, not a cyan glow. Glow on this site is a state and never a
     resting property; a solid cyan slab on near-black is already the loudest
     thing on the page without help. */
  box-shadow: var(--elev-1);
}
.btn-primary:hover { background: var(--accent-press); box-shadow: var(--elev-2); }

.btn-ghost {
  background: var(--accent-wash);
  border-color: var(--chrome-accent);
  color: var(--text-accent);
  box-shadow: var(--elev-0);
}
.btn-ghost:hover { background: var(--panel-active); border-color: var(--accent); color: var(--text-strong); box-shadow: var(--elev-1); }

/* A quiet button is a drawn outline, not a surface: it never gains height,
   because it is not offering to be the thing you press. */
.btn-quiet {
  background: transparent;
  border-color: var(--chrome-line-firm);
  color: var(--text-secondary);
  box-shadow: var(--elev-0);
}
.btn-quiet:hover { border-color: var(--chrome-rule); color: var(--text-strong); background: var(--chrome-wash); }
.btn-quiet:active { box-shadow: none; }

/* Destructive: it rises further than anything else on hover, because a thing
   you might regret should feel like it is being handed to you. */
.btn-danger {
  background: var(--status-busy-fill);
  color: var(--text-on-accent);
  box-shadow: var(--elev-1);
}
.btn-danger:hover { background: var(--status-busy-press); box-shadow: var(--elev-2); }

.btn-full { width: 100%; }
.btn-small { min-height: var(--tap-min); padding: 0 calc(var(--space-3) - var(--track-header)) 0 var(--space-3); font-size: var(--size-label); }

/* Unavailable, not merely faint: the fill goes flat, the surface loses all
   height, and the border states the boundary that the low opacity alone
   used to have to imply. */
.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.45;
  background: transparent;
  border-color: var(--chrome-line-firm);
  color: var(--text-muted);
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

/* Working state: the label is replaced by the caller, the bar underneath
   is the only thing that moves. */
.btn[data-busy] { position: relative; overflow: hidden; pointer-events: none; }
.btn[data-busy]::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  height: 2px; width: 40%;
  background: currentColor;
  animation: sweep var(--dur-sweep) var(--ease) infinite;
}
@keyframes sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(350%); }
}

/* =============================================================== FORMS ====
   Label above input, always. Helper below label, error below input.
   No placeholder-as-label anywhere in this system.
   ========================================================================= */
.field-group { display: block; }
.field-group + .field-group { margin-top: var(--space-5); }

.field-label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--size-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.field-help {
  margin: calc(var(--space-1) * -1) 0 var(--space-2);
  font-size: var(--size-meta);
  color: var(--text-muted);
}
.field-error {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--size-meta);
  font-weight: var(--weight-medium);
  color: var(--text-danger);
}
/* Ink-centred against the cap-height of the sentence it opens, like every
   other × on the site. tokens.css §mark-lift. */
.field-error::before {
  content: '\00D7';
  margin-right: var(--space-2);
  font-family: var(--font-mono);
  font-weight: var(--weight-black);
  display: inline-block;
  line-height: 1;
  transform: translateY(var(--mark-lift-cross));
}

/* An input is a hole you type into, so it is drawn as one: the sunk
   inversion, same lamp, dark along the top edge. */
.field {
  width: 100%;
  min-height: var(--tap-comfort);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-sunk);
  border: 1px solid var(--chrome-line-firm);
  border-radius: var(--radius);
  box-shadow: var(--elev-sunk);
  color: var(--text-primary);
  font-size: var(--size-body);   /* 16px — anything smaller zooms iOS Safari */
  letter-spacing: var(--track-data);
  transition: border-color var(--dur-hover) var(--ease),
              background-color var(--dur-hover) var(--ease),
              box-shadow var(--dur-hover) var(--ease);
}
.field:hover { border-color: var(--chrome-rule); }
.field:focus { border-color: var(--accent); background: var(--ink); }
textarea.field { min-height: 8rem; resize: vertical; line-height: var(--leading-body); }
select.field {
  appearance: none;
  padding-right: var(--space-8);
  background-image: linear-gradient(45deg, transparent 50%, var(--text-secondary) 50%),
                    linear-gradient(135deg, var(--text-secondary) 50%, transparent 50%);
  background-position: right var(--space-5) center, right var(--space-4) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}
select.field option { background: var(--ink-raised); color: var(--text-primary); }
.field[aria-invalid="true"] { border-color: var(--status-busy-edge); }

/* Checkbox / radio rows are whole-row tap targets, not 14px squares. */
.check {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--dur-hover) var(--ease),
              border-color var(--dur-hover) var(--ease);
}
.check:hover { background: var(--panel-hover); border-color: var(--chrome-accent); }
.check:active { background: var(--panel-active); transition-duration: var(--dur-press); }
.check input { width: 20px; height: 20px; accent-color: var(--accent); cursor: pointer; flex: 0 0 auto; }

/* ================================================================= NAV ====
   Desktop: a top bar. Mobile: a bottom tab bar, because the schedule is
   filled in one-handed and the thumb does not reach the top of a phone.
   Four destinations, always visible — a hamburger would hide the whole site
   behind a tap for no gain at this size.
   ========================================================================= */
.nav {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-2) var(--gutter);
  background: var(--panel);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  border-bottom: 1px solid var(--chrome-line);
  box-shadow: var(--elev-overlay);
}

/* Both of these are real destinations — one goes home, one changes who you
   are — so both carry the 44px floor rather than the height of their text. */
.nav-brand {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  gap: var(--space-2);
  font-size: var(--size-label);
  font-weight: var(--weight-black);
  letter-spacing: var(--track-header);
  text-transform: uppercase;
  color: var(--text-strong);
  text-decoration: none;
}
/* The wordmark used to spend a cyan word. Above the fold on the Schedule page
   at 390 that made six countable accent regions — the wordmark, the active tab
   and its rule, and three selected day chips — against a discipline of fewer
   than five. The chips are information a player set; the wordmark is a name.
   Emphasis is spent, and it is not spent on saying where you already are. */
.nav-brand span { color: var(--text-strong); }

.nav-links { display: none; gap: var(--space-1); margin-left: auto; }

.nav-link {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  /* Hugs its label between two paddings, so the trailing letter-space comes
     off the right one — otherwise the word sits 0.8px left inside a box that
     looks symmetrical. base.css §optical. */
  padding: 0 calc(var(--space-4) - var(--track-label)) 0 var(--space-4);
  border-bottom: var(--rule) solid transparent;
  font-size: var(--size-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color var(--dur-hover) var(--ease),
              border-color var(--dur-hover) var(--ease),
              background-color var(--dur-hover) var(--ease);
}
.nav-link:hover { color: var(--text-strong); background: var(--panel-hover); }
/* A nav link had THREE states, not four: it acknowledged the pointer and the
   keyboard and said nothing at all to the finger or the click. Every control
   on this site answers a press; this one is a destination, so it takes the
   press the way a drawn thing does — the underline arrives early, before the
   page it points at does. */
.nav-link:active { color: var(--text-strong); border-bottom-color: var(--accent); background: var(--panel-active); transition-duration: var(--dur-press); }
.nav-link[aria-current="page"] { color: var(--text-accent); border-bottom-color: var(--accent); }

.nav-me {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  /* A four-letter name made this 42.5px wide — a tap target below the floor,
     decided by how short someone's name is. The height was pinned and the
     width was not. */
  min-width: var(--tap-min);
  padding: 0 var(--space-2);
  margin-left: auto;
  font-size: var(--size-meta);
  color: var(--text-secondary);
}
.nav-links + .nav-me { margin-left: var(--space-4); }

/* Bottom tab bar — mobile only. */
.tabbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: var(--z-nav);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--panel);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  border-top: 1px solid var(--chrome-line);
  box-shadow: var(--elev-sheet);
}
.tab {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  overflow: hidden;
  min-height: var(--tap-min);
  border-top: var(--rule) solid transparent;
  /* Four labels share a 390px phone, so this is the one place the ladder is
     capped at the 13px floor rather than the 14px label step: "CHARACTERS"
     at 13px/0.06em is 97px in a 97.5px column. Wider tracking here would
     push the page sideways, which costs more than the extra pixel buys. */
  font-size: var(--size-meta);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-data);
  /* Stretched grid column, text centred in it: a centred line moves by half
     its indent, so one whole tracking unit cancels the half-unit the trailing
     letter-space adds on the right. base.css §optical. */
  text-indent: var(--track-data);
  text-transform: uppercase;
  color: var(--text-secondary);
  text-decoration: none;
  transition: color var(--dur-hover) var(--ease),
              background-color var(--dur-hover) var(--ease),
              border-color var(--dur-hover) var(--ease);
}
.tab[aria-current="page"] { color: var(--text-accent); border-top-color: var(--accent); background: var(--accent-wash); }
.tab:active { background: var(--panel-active); }

/* At 320px an even quarter is 80px and "CHARACTERS" measures 89px, so it
   clipped. The 13px floor is not negotiable and neither is the word, so the
   columns stop being equal instead: "WIKI" needs 40px and lends the rest. */
@media (max-width: 359px) {
  .tabbar { grid-template-columns: 1fr 0.8fr 1.2fr 1fr; }
}

@media (min-width: 720px) {
  .nav-links { display: flex; }
  .tabbar { display: none; }
  body { padding-bottom: 0; }
}

/* ============================================================== STATUS ====
   free / maybe / busy. Colour is never alone: each carries a glyph via
   ::before and, where it is a surface rather than a word, a hatch pattern.
   Markup contract: any element carrying availability sets data-status.
   ========================================================================= */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 3px var(--space-3);
  border: 1px solid currentColor;
  border-radius: var(--radius-sm);
  box-shadow: var(--elev-1);
  font-size: var(--size-label);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
}
/* The glyph is the status's second channel, so it is centred by INK — against
   the CAP-HEIGHT of the word beside it, which is what a reader's eye uses as
   the line, not the baseline and not the em box.

   `line-height: 1` alone did not do this and could not: the pill's own line
   box is taller than the glyph's, so the STRUT decides where the baseline
   goes and the glyph is hung off it like any other character. A ✓ is drawn
   nearly to the cap line and comes out close; a ~ or a × sits on the maths
   axis and comes out low. The nudge is the measured difference, in em of the
   glyph's own size, and it is a token so it can be re-measured rather than
   re-guessed. See tokens.css §mark-lift and verify/runs/20-css/probe.mjs. */
.pill::before {
  font-family: var(--font-mono);
  font-weight: var(--weight-black);
  /* `display: block` makes the pseudo-element a flex ITEM in its own right
     rather than an inline box inside an anonymous one. That deletes the
     anonymous item's strut — the taller, inherited 1.6 line box that was
     deciding the baseline — so the glyph's own em box is what flex centres,
     and one set of measured nudges is valid in the pill, the square and the
     calendar mark alike instead of three sets for three geometries. */
  display: block;
  line-height: 1;
  transform: translateY(var(--mark-lift, 0));
}

/* The measured ink correction per MARK. Set on the element, inherited by the
   pseudo — so any glyph-bearing ::before under a data-status is corrected
   once, wherever it is drawn. tokens.css §mark-lift. */
[data-status="unset"] { --mark-lift: var(--mark-lift-dash); }
[data-status="busy"]  { --mark-lift: var(--mark-lift-cross); }
[data-status="maybe"] { --mark-lift: var(--mark-lift-tilde); }

[data-status="free"]  { color: var(--status-free-fill);  }
[data-status="maybe"] { color: var(--status-maybe-fill); }
[data-status="busy"]  { color: var(--status-busy-fill);  }
[data-status="unset"] { color: var(--status-unset-fill); }
[data-status="shut"]  { color: var(--status-shut-fill);  }

.pill[data-status="free"]  { background: var(--status-free-bg);  }
.pill[data-status="maybe"] { background: var(--status-maybe-bg); background-image: var(--hatch-maybe); }
.pill[data-status="busy"]  { background: var(--status-busy-bg);  background-image: var(--hatch-busy); }
.pill[data-status="unset"] { background: var(--status-unset-bg); border-color: var(--chrome-line-firm); }
/* Shut by the Warden. Not an answer, so no hue from the answer set and no
   hatch, exactly as on the square: hairline box, ▦, --text-secondary. The
   grid's key needs this chip, and a key that drew the fifth mark by hand
   instead of from the same tokens is a key that drifts. */
.pill[data-status="shut"]  { background: var(--status-shut-bg);  border-color: var(--status-shut-edge); }

.pill[data-status="free"]::before  { content: var(--status-free-mark);  }
.pill[data-status="maybe"]::before { content: var(--status-maybe-mark); }
.pill[data-status="busy"]::before  { content: var(--status-busy-mark);  }
.pill[data-status="unset"]::before { content: var(--status-unset-mark); }
.pill[data-status="shut"]::before  { content: var(--status-shut-mark);  }

/* One size down, and not shouted: a name is a name, and so is a session's
   title. Same triple — colour, glyph, hatch — at 13px, which is the floor for
   anything a player reads and not below it. */
.pill-mini {
  font-size: var(--size-meta);
  font-weight: var(--weight-medium);
  letter-spacing: var(--track-data);
  text-transform: none;
  gap: var(--space-1);
  padding: 1px var(--space-2);
}

/* ---- a session's state, wherever a session is drawn ----------------------
   Moved here from index.html's <style> block, where it shipped while this file
   was being rewritten in parallel. It belongs in the shared layer because two
   pages now draw a called-off session — the Schedule card and the Sessions
   feed's byline — and a treatment that lives on one page is a treatment the
   other page will get wrong.

   Proposed / booked / called off is separated on four channels at once: the
   word in the pill, the ✓ ~ × glyph the pill already carries above, the
   colour, and a solid left edge on the card. Reconciled with §4's depth
   ladder rather than pasted: a session card is a ROW inside a panel, not a
   surface of its own, so it takes no fill, no shadow and no radius — the
   asymmetric-radius rule has nothing to hang off. The edge is the whole
   device, and it is `solid` because §1's border taxonomy reserves solid for a
   fact somebody set, which is exactly what the Warden's decision is. */
.ses[data-state="confirmed"] { border-left: var(--edge) solid var(--status-free-fill); padding-left: var(--space-3); }
.ses[data-state="cancelled"] { border-left: var(--edge) solid var(--status-busy-fill); padding-left: var(--space-3); }
/* Called off, drawn rather than dimmed: dimming a heading takes it below the
   4.5:1 floor and says "unimportant" where the page means "not happening". */
.struck { text-decoration: line-through; text-decoration-thickness: 2px; }
.ses-verdict,
.ses-waiting { margin: var(--space-2) 0 0; font-size: var(--size-meta); color: var(--text-secondary); }
.ses-verdict[data-state="confirmed"] { color: var(--status-free-fill); font-weight: var(--weight-medium); }
.ses-verdict[data-state="cancelled"] { color: var(--text-danger); }
/* The Warden's two buttons. Their own row under the answers, because deciding
   the session and answering it are different jobs done by different people. */
.ses-decide { display: flex; gap: var(--space-2); margin-top: var(--space-4); flex-wrap: wrap; }
.ses-decide .btn { flex: 0 1 auto; }
.ses-blocked { flex: 1 1 100%; margin: 0; font-size: var(--size-meta); color: var(--text-secondary); }

/* ---- the calendar mark --------------------------------------------------
   The mark's LAYOUT is page-scoped (index.html owns the month grid); its
   STATUS treatment is this file's job, like every other data-status surface
   above, so a booked day reads the same here as a free hour does on the grid
   — in greyscale and to a red-green reader. */
.cal-mark[data-status="free"]  { background: var(--status-free-bg); }
.cal-mark[data-status="maybe"] { background: var(--status-maybe-bg); background-image: var(--hatch-maybe); }
/* Called off by the Warden: the third state the mark can be in, drawn like
   every other refusal on the site — ×, red, cross-hatch. */
.cal-mark[data-status="busy"]  { background: var(--status-busy-bg); background-image: var(--hatch-busy); }
.cal-mark[data-status="free"]::before  { content: var(--status-free-mark); }
.cal-mark[data-status="maybe"]::before { content: var(--status-maybe-mark); }
.cal-mark[data-status="busy"]::before  { content: var(--status-busy-mark); }
/* A booked session is the one thing on the calendar that is settled, so it is
   the one thing set in the heavy weight. Word + glyph + colour + weight; the
   colour is never doing it alone. */
.cal-mark[data-status="free"] { font-weight: var(--weight-black); }

/* Neutral count/info badge — no availability meaning. */
.badge {
  display: inline-flex;
  align-items: center;
  min-width: 22px;
  padding: 1px var(--space-2);
  background: var(--accent-wash);
  border: 1px solid var(--chrome-accent);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--size-meta);
  font-weight: var(--weight-bold);
  color: var(--text-accent);
}

/* ================================================= AVAILABILITY SQUARE ====
   The one control this whole site exists for: one hour, one square, ≥44px at
   every width from 320 up. It used to be defined in index.html's <style>
   block while app.css and shell.html documented a DIFFERENT, unused grid
   (.grid-day / .choices / .cell) — two implementations, and the polish was
   on the one nobody shipped. That block is deleted; this is the shipped
   component, in the shared layer, where shell.html can document it and where
   the confirmation below can be written once.

   The grid's BED is a well (--elev-sunk, --surface-sunk). The squares are
   pressed into the page, not stacked on it, which is both the correct
   material reading of a control you push and — because the bed is darker
   than the panel — worth between 0.2 and 0.3 of contrast on every one of the
   five statuses measured through it.
   ========================================================================= */
/* The bed. Ten one-hour squares, five to a line, under a date that spans the
   full width — one layout at every width from 320 to 1440, so the keyboard
   model, the drag model and the reading order are the same everywhere and
   there is no second arrangement to get wrong. */
.agrid {
  display: block;
  padding: var(--space-3);
  background: var(--surface-sunk);
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius);
  box-shadow: var(--elev-sunk);
}
.arow {
  display: grid;
  grid-template-columns: repeat(5, minmax(var(--tap-min), 1fr));
  gap: var(--space-1);
  margin-bottom: var(--space-3);
}
.arow > .adate { grid-column: 1 / -1; }
/* The date is the HEADING of its row of squares, and it shipped at 13px — the
   same size as the "in three days" note hanging off it — so a row of the
   site's primary surface had no head, just two identical greys at the bottom
   rung of the ladder. It takes the body size; the note stays at the floor,
   which is what makes the pair read as a heading and a caption instead of one
   run-on line. */
.adate {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--size-body);
  font-weight: var(--weight-bold);
  letter-spacing: var(--track-data);
  color: var(--text-primary);
  line-height: var(--leading-snug);
  padding-top: var(--space-1);
}
.adate small { font-size: var(--size-meta); font-weight: var(--weight-body); color: var(--text-muted); }

.acell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-height: var(--tap-min);
  border: 1px solid var(--chrome-line-firm);
  border-radius: var(--radius-sm);
  /* A drag across the grid must paint, never scroll the page under the finger. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  transition:
    /* Colour and border stay on the pointer duration. The status has to be
       legible the instant it changes — a longer fill would smear a seven-
       square drag into a wipe trailing the finger. */
    background-color var(--dur-hover) var(--ease),
    border-color var(--dur-hover) var(--ease),
    /* `color` is deliberately NOT transitioned. The glyph is the status's
       colour-blind-safe channel and it is either ✓ or ×, never an in-between:
       a cell caught mid-transition renders its mark in a colour that belongs
       to neither status, which is exactly the ambiguity the triple exists to
       prevent. The fill may settle; the mark snaps. */
    /* The confirmation decays on the tick duration, which is longer because
       it is a REACTION to a completed save and not an acknowledgement of an
       input. It is expressed as a transition rather than a keyframe so a
       second save mid-decay retargets from where the light is instead of
       restarting from full. */
    box-shadow var(--dur-tick) var(--ease-out),
    transform var(--dur-press) var(--ease-snap);
}
/* CENTRED BY INK, and this is the correction that repeats 300 times.

   `line-height: 1` on the pseudo-element centred nothing, because the square's
   own line box is the taller of the two: the cell inherits 16px/1.6 from the
   body, so a 25.6px STRUT was deciding where the baseline sat and the 17px
   glyph was hung off that baseline like any other character. Measured in the
   shipped build, the × and the – landed 1.75 CSS px below the square's centre
   while the ✓ happened to land true — a systematic error across every cell of
   a 300-square grid, in the one control the whole site exists for.

   Two fixes, both required. `line-height: 0` on the cell deletes the strut, so
   the line box IS the glyph's own em box and flex centring can reach it. Then
   the per-mark nudge moves the INK, because ✓ ~ × ▦ – do not share an optical
   centre inside that em box either. Both numbers are measured, not chosen. */
.acell::before {
  font-family: var(--font-mono);
  font-size: var(--size-mark);
  font-weight: var(--weight-black);
  display: block;      /* deletes the strut — see .pill::before above */
  line-height: 1;
  order: 2;            /* the label heads the square; the mark answers under it */
  transform: translateY(var(--mark-lift, 0));
}

/* ---- THE WHOLE HOUR, IN THE SQUARE ---------------------------------------
   Ten hours cannot be ten columns at 320px, so they wrap five and five — and
   for as long as the hour lived only in a sticky axis that wrapped the same
   way, column 1 was simultaneously "Noon" and "5pm" depending on which LINE
   you were reading. Correct line for line; unreadable at a glance, and a
   player duly reported the grid as ranges: "1-6, 2-7, 3-8". DESIGN.md §6
   allows exactly two fixes and the wrap cannot be made obvious at 246px of
   content, so this is the other one: the square states its own hour, and
   position has nothing left to contradict.

   It states the SPAN — "12-1PM", not "7PM". A start time is a point until
   something off the square says it means the hour beginning there, and the
   things saying so were a sentence above 4,000px of grid and an `aria-label`
   only a screen reader hears. §6 forbids exactly that shape: fix the visible
   text and let the accessible name follow it.

   It is paid 300 times, so it is paid in ink and not in height, and the span
   costs neither. Six characters of 13px mono set at 42.89px against a 45.19px
   content box at 320 — one size DOWN from the 14px start-hour it replaces, so
   13 + the 4px gap + the 17px mark is 34px of content in a square whose floor
   is 44 and the grid body is exactly the height it has always been: measured
   3875.81 at 320 and 3995.81 above it, before and after. Note the 10px of
   headroom: raise either type size or the gap and the floor stops absorbing
   it, at which point 300 squares each grow.

   Mono, because these are ten values off one axis and they must set in one
   width whatever the hour: 12-1PM and 9-10PM are the same object, tabular.
   The letterspacing the other uppercase labels carry is off here and that is
   the whole reason six characters fit: 0.12em would set them at 56.27px and
   overflow the square at 320. It costs nothing legible — tracking is for
   words, and this is a number, a rule and a number.

   --text-secondary, not currentColor: the hour is a fact about the square,
   not the answer in it. Tinting it per status would say the time changed when
   the answer did, spend a third channel on something that is not a status,
   and put ten shades of the same word on one line. 9.5:1 on --ink and
   measured at or above 8.9:1 on all five cell fills. */
.ahour {
  order: 1;
  /* NOT HIT-TESTABLE, and this is load-bearing. `touch-action` does not
     inherit: the square carries `touch-action: none` so a drag paints instead
     of scrolling, but a finger landing on a child span would be governed by
     that span's own `auto` and scroll the page out from under the paint —
     the same class of bug the sticky strip's pointer-events note describes.
     Nothing in a square is a target except the square. */
  pointer-events: none;
  font-family: var(--font-mono);
  font-size: var(--size-meta);
  font-weight: var(--weight-bold);
  line-height: 1;
  letter-spacing: normal;   /* see above: 0.12em does not fit six characters */
  color: var(--text-secondary);
  /* The label is the one thing in the square that must never be the reason a
     44px target shrinks or the page scrolls sideways. At 320px the content box
     is 45.19px and the widest of the ten sets at 42.89 in the shipped mono. It
     is deliberately NOT `nowrap`: on a platform whose fallback mono runs wider
     the hyphen is a break opportunity, so the worst case is a square that grows
     downward, never a grid that pushes the page sideways. */
  text-transform: uppercase;
}
/* A shut hour is struck through, which is this site's existing word for
   "called off" (.struck, on a cancelled session's heading). It is the second
   channel that separates ▦ from –: an unanswered square offers you its hour,
   a shut one crosses it out. Greyscale-safe, hue-free, costs no height. */
.acell[data-status="shut"] .ahour {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  color: var(--text-muted);
}

/* Signature moment: committing a square. The press pushes in and the release
   snaps back through the resting size on the one overshoot curve. 90ms. It
   is the best-tuned number on this site and nothing may lengthen it. */
.acell:active { transform: scale(0.94); }

/* Desktop had NO hover on 300 squares — the pointer crossed the entire
   primary surface of the site and got nothing back. It previews, it does not
   commit, so it is border and colour only and never the fill. */
@media (hover: hover) and (pointer: fine) {
  .acell:hover { border-color: var(--chrome-rule); }
  .acell[data-status="unset"]:hover { border-color: var(--chrome-accent); }
  .acell[data-status="shut"]:hover { border-color: var(--status-shut-edge); cursor: default; }
}

.acell[data-status="unset"] { background: var(--status-unset-bg); color: var(--text-muted); }
.acell[data-status="unset"]::before { content: var(--status-unset-mark); }
.acell[data-status="free"]  { background: var(--status-free-bg);  border-color: var(--status-free-edge);  color: var(--status-free-fill); }
.acell[data-status="free"]::before  { content: var(--status-free-mark); }
.acell[data-status="maybe"] { background: var(--status-maybe-bg); background-image: var(--hatch-maybe); border-color: var(--status-maybe-edge); color: var(--status-maybe-fill); }
.acell[data-status="maybe"]::before { content: var(--status-maybe-mark); }
.acell[data-status="busy"]  { background: var(--status-busy-bg);  background-image: var(--hatch-busy);  border-color: var(--status-busy-edge);  color: var(--status-busy-fill); }
.acell[data-status="busy"]::before  { content: var(--status-busy-mark); }

/* Shut by the Warden: nobody's answer, so no hue from the answer set, no hatch,
   and no press animation — there is nothing to press. The sr-only text inside
   says why in words; ::before is decoration on top of it. */
.acell[data-status="shut"] {
  background: var(--status-shut-bg);
  border-color: var(--status-shut-edge);
  border-style: solid;
  color: var(--status-shut-fill);
  cursor: default;
}
.acell[data-status="shut"]::before { content: var(--status-shut-mark); }
.acell[data-status="shut"]:active { transform: none; }

/* ---- THE KEY TO THE FIVE MARKS -------------------------------------------
   Every state word on a square is sr-only, so a square's only visible content
   is its hour and a ✓ ~ × – or ▦, and the grid shipped with no visible key
   anywhere: the MARKING FREE/MAYBE/BUSY bar is a brush selector, not a legend.
   A reader without a key read ▦ ("the Warden cannot run a game then") as –
   ("nobody has answered yet") — "impossible" and "unanswered" inverted, on the
   surface where that mistake is most expensive.

   Built from .pill-mini, so the colour, the ::before glyph and the hatch come
   from the same tokens as the squares and cannot drift from them. On the
   Schedule page this list is what the sticky strip carries, because a key
   stated once at the top of a 4,000px scroll labels nothing past the fold
   (DESIGN.md §6). No shadow: a key is a printed legend, not five raised
   controls, and .pill's --elev-1 read as something you could press. */
.akey {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}
.akey .pill { box-shadow: none; }
/* `.akey-lede` is deleted. It set the sentence "each square is one hour,
   starting at the time printed on it", which existed only because the square
   printed a start time. The square prints "12-1PM" now, so the sentence
   restated what 300 squares already say and was shipped on zero pages the
   moment they did. */

/* Border-style as taxonomy, which is free contrast-wise and survives
   greyscale: inherited from your usual week is DASHED, an answer you set by
   hand is SOLID and thicker — on a date row AND on a usual-week row, where
   everything is by definition set by hand. Structure drawn rather than shaded.

   This is the pair that carries the whole cascade now that both halves are in
   one grid: the seven "Every…" rows at the top are solid, and every date that
   is only showing what they say is dashed. Change a solid square and you watch
   the dashed ones follow. It used to be one dashed edge whose solid
   counterpart was a chip in a different panel. */
.acell[data-source="pattern"] { border-style: dashed; }
.acell[data-source="override"] { border-width: 2px; }

/* ---- The two halves of the grid, named in place ---------------------------
   The grid answers one question with two kinds of row — "every Tuesday" and
   "Tuesday 4 August" — and the difference is the single most expensive thing
   on this page to misread. So it is said three ways at once: the row header
   begins with the word "Every", the squares are solid rather than dashed, and
   the group carries this heading directly above the rows it governs.

   It is a heading, not a row, so it takes the label treatment the column heads
   and controls take (DESIGN.md §1: uppercase is for things you aim at) and a
   hairline under it. NOT the 3px double rule — that belongs under a panel
   heading and nowhere else, and there are two of these. */
.agroup {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-3);
  border-bottom: 1px solid var(--chrome-rule);
}
/* The first group sits directly under the sticky key, which already has its
   own rule; a second one 8px below it is two lines doing one job. */
.ahead + .agroup { margin-top: var(--space-2); }
/* The two groups are ONE grid but two different promises — a rule you set once
   that fills everything, and thirty real dates you correct one at a time. At
   20px apart with matching hairlines they read as two halves of one list, and a
   player editing a date thought they were editing the rule. So the second group
   gets a genuine section break: 40px of air, matching the gap between panels,
   and a DOUBLE rule, which in this system means a printed rule and appears only
   at a panel heading (DESIGN.md §1's border-style taxonomy). Same device, same
   meaning, one level down. */
.agroup + [role="grid"] + .agroup { margin-top: var(--space-10); }
/* The heading IS the control now, so the whole row is the target rather than
   the words inside it. 44px floor like every other control on this page. */
.agroup { margin: 0; padding: 0; border-bottom: 0; }
.agroup-toggle {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-2) 0;
  margin-bottom: var(--space-3);
  background: none;
  border: 0;
  border-bottom: 1px solid var(--chrome-rule);
  border-radius: 0;
  text-align: left;
  cursor: pointer;
}
/* A disclosure triangle that points at what it does: right when shut, down when
   open. Rotation rather than two glyphs, so the two states are one object
   moving and not two icons swapping. */
.agroup-toggle::before {
  content: '\25B8';
  font-family: var(--font-mono);
  color: var(--text-secondary);
  transition: transform var(--dur-hover) var(--ease);
}
.agroup-toggle[aria-expanded="true"]::before { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .agroup-toggle::before { transition: none; } }
.agroup-toggle:hover .agroup-name { color: var(--text-primary); }
.agroup-major .agroup-toggle { border-bottom-width: var(--rule-print); border-bottom-style: double; }
/* The note stops being a caption here and becomes the instruction that keeps
   the two halves apart in the reader's head, so it gets a readable measure
   rather than trailing the name on one line. */
.agroup-major .agroup-note { flex-basis: 100%; max-width: var(--measure-lead); }
.agroup-toggle .agroup-note { font-weight: var(--weight-body); text-transform: none; letter-spacing: normal; }
.agroup-name {
  font-size: var(--size-label);
  font-weight: var(--weight-black);
  letter-spacing: var(--track-header);
  text-transform: uppercase;
  color: var(--text-secondary);
}
/* Sentence case, because this one is read rather than aimed at (DESIGN.md §1).
   It says what the rows below DO, which is the sentence the deleted chip panel
   used a paragraph for. */
.agroup-note {
  font-size: var(--size-meta);
  color: var(--text-muted);
}

/* ---- The save, marked -----------------------------------------------------
   The one action this site exists for was its least-marked event: a 13px word
   changed from "Saving" to "Saved" and that was the entire ceremony, for the
   thing the whole product is. This is the missing sentence, and it says
   exactly one thing — YOUR ANSWER IS ON THE SERVER NOW.

   It is the map's negative-spread halo: a light with no visible rim, so the
   square confirms without growing a cyan ring. schedule.js stamps
   [data-confirm] on the squares that were in the batch that just saved and
   drops it on the next frame, so the light snaps on and decays over
   --dur-tick, once, finite, on those squares only.

   Every clause of DESIGN.md §9.8 and the critique's "what not to do" holds:
   this is not continuous, not an iteration, not scroll-triggered, not a
   staggered reveal, and it never touches the 290 squares that did not change.
   It fires AFTER the network round-trip the player is not waiting on, so it
   costs zero taps and zero milliseconds on the six-interaction flow. */
.acell[data-confirm] {
  box-shadow: var(--glow-confirm);
  transition-duration: 0ms;   /* light on instantly, decay on release */
}

/* ============================================================== TABLES ====
   Wrapped so wide tables scroll themselves and never the page.
   ========================================================================= */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius);
  -webkit-overflow-scrolling: touch;
}
.table { font-size: var(--size-body); }
.table th {
  position: sticky;
  top: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--ink-raised);
  border-bottom: var(--rule) solid var(--chrome-rule);
  font-size: var(--size-label);
  font-weight: var(--weight-black);
  letter-spacing: var(--track-label);
  /* Left-aligned: no correction. The old negative indent pulled every column
     head 0.8px off the left edge of the cells below it. base.css §optical. */
  text-transform: uppercase;
  color: var(--text-secondary);
  text-align: left;
  white-space: nowrap;
}
.table td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--chrome-line);
  color: var(--text-primary);
  vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr { transition: background-color var(--dur-hover) var(--ease); }
.table tbody tr:hover { background: var(--panel-hover); }
.table .num { text-align: right; color: var(--text-secondary); }

/* Tappable list row — the mobile stand-in for a table. */
/* The best micro-interaction on the site, and for a long time the only one:
   the row physically indents under the cursor and grows a cyan bar, so it
   reads as being pulled out of the list rather than tinted. Now it also
   gains a step of elevation, so the thing that indents is also the thing
   that lifts. `translate` rather than `padding-left` — the visual result is
   identical and it is a transform, so it never touches layout. */
.row-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-comfort);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--chrome-line);
  border-left: var(--edge) solid transparent;
  color: var(--text-primary);
  text-decoration: none;
  transition: background-color var(--dur-hover) var(--ease),
              border-color var(--dur-hover) var(--ease),
              box-shadow var(--dur-hover) var(--ease),
              transform var(--dur-hover) var(--ease);
}
.row-link:hover {
  background: var(--panel-hover);
  border-left-color: var(--accent);
  transform: translate3d(4px, 0, 0);
  box-shadow: var(--elev-1);
  color: var(--text-strong);
}
.row-link:active { transform: translate3d(4px, 1px, 0); box-shadow: var(--elev-press); transition-duration: var(--dur-press); }
.row-link:last-child { border-bottom: none; }
/* The date never breaks mid-date, so at 320 a row of pill + long title + date
   was 35px wider than the panel and pushed the whole page sideways. It only
   showed up once the Schedule agenda had rows in it; the bug is in the shared
   row, so it is fixed in the shared row. The date drops to its own line rather
   than the title being clipped, and `min-width: 0` lets the title wrap at all —
   a flex item defaults to min-content, which is the longest word in the title. */
.row-link { flex-wrap: wrap; }
.row-link > * { min-width: 0; }
.row-link .meta { margin-left: auto; white-space: nowrap; }

/* ========================================================= EMPTY STATE ====
   Says what is missing and gives the one control that fixes it. */
/* Dashed, because dashed is this site's word for "provisional / not yet
   filled in" — the same taxonomy the inherited-from-your-week square uses.
   The empty state is composed rather than short: its own vertical rhythm, a
   measure of its own, and exactly one control. */
.empty {
  padding: var(--space-12) var(--space-5);
  text-align: center;
  background: var(--surface-sunk);
  border: 1px dashed var(--chrome-line-firm);
  border-radius: var(--radius);
  box-shadow: var(--elev-sunk);
}
/* No ornament above the message. A 28px row of em-dashes on --chrome-faint
   said nothing the heading below it did not already say, and it was the last
   thing on the site sitting under 4.5:1. The dashed border already marks the
   empty state as a placeholder. Markup that still emits `.empty-mark` renders
   nothing; delete it when you next touch the file. */
.empty-mark { display: none; }
.empty h3 { margin: var(--space-3) 0 var(--space-2); color: var(--text-primary); }
.empty p { max-width: 44ch; margin: 0 auto var(--space-5); color: var(--text-secondary); }
/* The availability grid's empty state is the one that JOINS content rather
   than replacing it — the week ahead is above it and this is the plate where
   the rest of the month would be — so it needs the gap a replacement never
   does, and nothing under its last line. */
.grid-empty { margin-top: var(--space-4); padding-block: var(--space-8); }
.grid-empty p:last-child { margin-bottom: 0; }

/* A search hit, wherever it is marked. The browser default is black on yellow —
   two colours from outside the system. One rule, shared by Wiki and Sessions. */
mark {
  background: var(--accent-wash);
  color: var(--text-accent);
  padding: 0 2px;
  border-radius: var(--radius-sm);
}

/* ============================================================== TOASTS ==== */
.toasts {
  position: fixed;
  left: var(--space-4);
  right: var(--space-4);
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--space-4));
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
}
@media (min-width: 720px) {
  .toasts { left: auto; right: var(--space-8); bottom: var(--space-8); width: min(380px, 40vw); }
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--panel);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  border: 1px solid var(--chrome-line);
  border-left: var(--edge) solid var(--accent);
  /* Square on the accented side so the bar runs edge to edge. */
  border-radius: 0 var(--radius) var(--radius) 0;
  box-shadow: var(--elev-overlay);
  color: var(--text-primary);
  font-size: var(--size-meta);
  /* Arriving, so ease-out. Leaving, so ease-exit at 0.55× the duration —
     nothing needs to watch a toast go. */
  animation: toast-in var(--dur-toast) var(--ease-out) both;
}
.toast[data-kind="good"]  { border-left-color: var(--status-free-edge); }
.toast[data-kind="warn"]  { border-left-color: var(--status-maybe-edge); }
.toast[data-kind="error"] { border-left-color: var(--status-busy-edge); }
.toast-mark { font-family: var(--font-mono); font-weight: var(--weight-black); color: var(--text-accent); }
.toast[data-kind="good"]  .toast-mark { color: var(--status-free-fill); }
.toast[data-kind="warn"]  .toast-mark { color: var(--status-maybe-fill); }
.toast[data-kind="error"] .toast-mark { color: var(--status-busy-fill); }

@keyframes toast-in  { from { opacity: 0; transform: translate3d(0, 12px, 0); } to { opacity: 1; transform: none; } }
/* No toast-out keyframe: a toast leaves through depart() in ui.js, the one
   definition of a departure the whole site uses. */

/* ============================================== SHEET (modal / drawer) ====
   Bottom sheet on mobile, centred panel on desktop. Use only when the task
   genuinely needs protected focus — proposing a session, confirming a
   delete. Never for something that could be inline.
   ========================================================================= */
.scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-scrim);
  background: var(--scrim-fill);
  backdrop-filter: var(--blur-overlay);
  -webkit-backdrop-filter: var(--blur-overlay);
  animation: fade-in var(--dur-panel) var(--ease-out) both;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.sheet {
  position: fixed;
  z-index: var(--z-sheet);
  left: 0; right: 0; bottom: 0;
  max-height: 88dvh;
  /* A short sheet (a read-only character, a confirm) otherwise opens as a thin
     strip pinned to the bottom bezel, with its content jammed under the home
     indicator and 59% of the phone dead scrim. Open to a usable height so the
     content lands in the thumb zone with room under it. Desktop resets this. */
  min-height: 52dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-5) var(--gutter) calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
  background: var(--panel);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  border: 1px solid var(--chrome-line);
  border-top: var(--edge) solid var(--accent);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  /* The only surface on the site whose shadow points UP, because it is the
     only one lit from below its own leading edge as it rises. */
  box-shadow: var(--elev-sheet);
  /* A sheet travels most of a screen, so it is the longest motion here and
     it lands on the arrival curve. */
  animation: sheet-up var(--dur-panel) var(--ease-out) both;
}
@keyframes sheet-up { from { transform: translate3d(0, 100%, 0); } to { transform: none; } }

.sheet-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-4);
  border-bottom: var(--rule-print) double var(--chrome-rule);
}
.sheet-close {
  margin-left: auto;
  width: var(--tap-min); height: var(--tap-min);
  display: grid; place-items: center;
  border: 1px solid var(--chrome-line-firm);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 1rem;
  transition: color var(--dur-hover) var(--ease), border-color var(--dur-hover) var(--ease),
              box-shadow var(--dur-hover) var(--ease), transform var(--dur-press) var(--ease);
}
.sheet-close:hover { color: var(--text-strong); border-color: var(--chrome-rule); }
.sheet-close:active { transform: translateY(1px); box-shadow: var(--elev-press); transition-duration: var(--dur-press); }
.sheet-actions { display: flex; gap: var(--space-3); margin-top: var(--space-6); }
.sheet-actions .btn { flex: 1; }

@media (min-width: 720px) {
  .sheet {
    left: 50%; bottom: auto; top: 50%; right: auto;
    width: min(560px, 92vw);
    min-height: auto;
    transform: translate(-50%, -50%);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
    animation: sheet-in var(--dur-panel) var(--ease-out) both;
  }
  @keyframes sheet-in {
    from { opacity: 0; transform: translate(-50%, -46%) scale(0.98); }
    to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  }
}

/* =============================================================== GATE ====
   The shared-password curtain. One field, one button, no branding theatre.
   ========================================================================= */
.gate {
  position: fixed;
  inset: 0;
  z-index: var(--z-gate);
  display: grid;
  place-items: center;
  padding: var(--gutter);
  /* The last raw hex on the site. DESIGN.md §2: tokens.css is the only place a
     raw value may be written, and the curtain's own glow is a surface value
     like any other. */
  background: radial-gradient(circle at 50% 34%, var(--panel-fill) 0%, var(--ink) 70%);
}
.gate-card {
  width: min(400px, 100%);
  padding: var(--space-8) var(--space-6);
  background: var(--panel);
  backdrop-filter: var(--blur-panel);
  -webkit-backdrop-filter: var(--blur-panel);
  border: 1px solid var(--chrome-line);
  border-top: var(--edge) solid var(--accent);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  box-shadow: var(--elev-overlay);
  animation: enter var(--dur-enter) var(--ease-out) both;
}
.gate-card h1 { font-size: var(--size-h2); margin-bottom: var(--space-2); letter-spacing: var(--track-display); }
.gate-card p { color: var(--text-secondary); font-size: var(--size-meta); margin-bottom: var(--space-6); }

/* ========================================================== SKELETONS ====
   Bones match the shape of the content that replaces them. A slow sweep,
   not a pulse — a pulse on six rows is six things blinking at you. */
.skeleton {
  background: var(--chrome-faint);
  border-radius: var(--radius-sm);
  position: relative;
  overflow: hidden;
  opacity: 0.5;
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--sweep-sheen), transparent);
  animation: shimmer var(--dur-sweep) var(--ease) infinite;
}
.skeleton-line { height: 12px; margin-bottom: var(--space-2); }
.skeleton-line:nth-child(2n) { width: 72%; }
.skeleton-line:nth-child(3n) { width: 88%; }
.skeleton-cell { height: var(--tap-comfort); }
@keyframes shimmer { from { transform: translateX(-100%); } to { transform: translateX(100%); } }

@media (prefers-reduced-motion: reduce) {
  .skeleton::after { display: none; }
  .btn[data-busy]::after { animation: none; width: 100%; opacity: 0.5; }
}

/* ============================================================ MARKDOWN ====
   Output of md() in ui.js. Wiki and session notes render into .prose. */
.prose {
  max-width: var(--measure);
  text-wrap: pretty;
  hanging-punctuation: first last;
}
.prose > * + * { margin-top: var(--space-4); }
.prose h2 { font-size: var(--size-h3); }
/* Mixed case. This is the wiki — the one surface on the site that is
   genuinely READ rather than scanned — and a shouted sub-heading inside a
   paragraph of prose is the clearest case of a label treatment applied to
   something that is not a label. Weight and colour separate it instead. */
.prose h3 {
  font-size: var(--size-body);
  font-weight: var(--weight-black);
  letter-spacing: var(--track-tight);
  color: var(--text-secondary);
}
.prose ul, .prose ol { padding-left: var(--space-6); }
.prose li + li { margin-top: var(--space-2); }
.prose ul { list-style: square; }
.prose ol { list-style: decimal; }
.prose li::marker { color: var(--chrome-dim); }
.prose code {
  padding: 1px var(--space-1);
  background: var(--panel-inset);
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius-sm);
  color: var(--text-accent);
}
.prose blockquote {
  padding-left: var(--space-4);
  border-left: var(--rule) solid var(--chrome-rule);
  color: var(--text-secondary);
}

/* The note is the only prose inside a control, and <b> inside a button inherits
   the button's flattened type. Give the two words the weight they are there for
   - they name the two border styles a player is looking at while reading. */
.agroup-note b { font-weight: var(--weight-bold); color: var(--text-secondary); }
