/* ============================================================
   AMBER PHOSPHOR — components
   The fifteen components of DESIGN-SYSTEM.md section 6, plus the
   scaffold chrome Base.astro still uses (card T3 replaces that).

   Rules that hold for every block below:
     - No raw hex. Colors come from tokens.css or they do not exist.
     - No border-radius. --radius is 0 and base.css applies it globally.
     - No blur on a shadow. --shadow is a hard offset.
     - Every control that has no fill of its own carries --rule-strong as
       its boundary. --rule composites to 1.83:1 and is decorative only
       (DESIGN-SYSTEM.md 3.3), so it can never bound a control alone.
     - No inline style attribute ever ships: style-src 'self' blocks
       attributes as well as elements, and scripts/csp-gate.mjs fails the
       build on one (DECISIONS.md D26).
   ============================================================ */

/* ============================================================
   BRAND WORDMARK + PANEL GRID
   T2 marked the scaffold header and footer rules (.bar, .bar-inner,
   .nav, .foot, .follow) as T3's to replace. T3 replaced them with
   SiteHeader / SiteNav / SiteFooter and the SITE CHROME block at the
   end of this file, so they are deleted rather than left to rot.
   The wordmark and the panel grid survive: both are still used.
   ============================================================ */

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--display);
  font-size: 22px;   /* display face floor. 22 is a multiple of 11 (4.5). */
  line-height: 24px;
  letter-spacing: -0.01em;
  color: var(--cream);
  text-decoration: none;
}
/* Moe rides beside the wordmark. Fixed box so the header never reflows on load;
   the picture ladder is the same asset the home hero used (D43). */
.brand__moe {
  width: 26px;
  height: 26px;
  flex: none;
  display: block;
}
.brand__word { display: inline-block; }
.brand .x { color: var(--eye); }
.brand:visited { color: var(--cream); }

.panels {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  margin-top: var(--s6);
}
.panels > .panel { margin-block: 0; }

/* ============================================================
   6.1 PANEL
   The bordered container every block of readable text sits inside.
   Texture goes behind panels, never behind the text (trap 6).
   ============================================================ */

.panel {
  background: var(--panel);
  border: var(--bw) solid var(--rule-strong);
  box-shadow: var(--shadow);
  margin-block: var(--s5);
}
.panel__bar {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--row-h);
  padding: 0 var(--s2);
  background: var(--night2);
  border-bottom: var(--bw) solid var(--rule-strong);
}
.panel__title {
  font-family: var(--mono);
  font-size: var(--fs);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--eye);
  margin-right: auto;
}
.panel__glyphs { color: var(--rule); letter-spacing: -0.08em; }
.panel__meta { font-size: var(--fs); color: var(--muted); font-variant-numeric: tabular-nums; }
.panel__body { padding: var(--s3); }
.panel__body > * + * { margin-top: var(--s3); }

/* A collapsible panel is a real <details>, never a fake close button (trap 5). */
details.panel > summary.panel__bar { cursor: pointer; list-style: none; }
details.panel > summary.panel__bar::-webkit-details-marker { display: none; }
details.panel > summary.panel__bar::after { content: "▾"; color: var(--eye); }
details.panel[open] > summary.panel__bar::after { content: "▴"; }

/* Scaffold panel heading, still used by the committed home route.
   T3 replaces those panels with the Panel component and this goes. */
.panel > h2, .panel > h3 {
  margin: 0;
  padding: var(--s2) var(--pad-lg);
  font-family: var(--mono);
  font-size: max(12px, 0.78em);
  line-height: 1.25;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--night);
  background: var(--eye);
  font-weight: 600;
}
.panel > .body { padding: var(--pad-lg); }
.panel > .body > * + * { margin-top: var(--s3); }

/* ============================================================
   6.2 FACETRAIL
   Two mechanisms in one box. PATH facets are <a> elements: crawlable,
   single-select, working with JS off. QUERY facets are form controls
   applied client-side by public/js/facets.js (T5), and they are hidden
   entirely without script because there is no server to normalize a
   native form GET (DECISIONS.md D28). The split is per category
   (D27); the component renders whatever keys it is handed.
   ============================================================ */

.rail {
  position: sticky;
  top: var(--s5);
  z-index: var(--z-rail);
  align-self: start;
  width: 232px;
  max-height: calc(100dvh - var(--s7));
  overflow-y: auto;
}
.rail__group {
  border: var(--bw) solid var(--rule);   /* decorative: the label carries the meaning */
  padding: var(--s2);
  margin-bottom: var(--s3);
}
.rail__legend,
.rail__group legend {
  font-size: var(--fs);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--eye);
  padding-inline: var(--s1);
}
.rail__paths ul { list-style: none; }
.rail__paths a,
.rail__group label {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  cursor: pointer;
}
.rail__paths a { color: var(--cream); text-decoration: none; padding-inline: var(--s1); }
.rail__paths a:hover { color: var(--eye); text-decoration: underline; }
.rail__paths a:visited { color: var(--cream); }
.rail__paths [aria-current="page"] { color: var(--eye); font-weight: 600; }
.rail__paths b, .rail__group label b { margin-left: auto; color: var(--muted); font-weight: 400; }

/* A real <input type="checkbox"> with appearance:none. State, role and
   keyboard behavior stay native; never rebuild one out of <div>s.
   min-height:0 overrides the global --tap floor on controls: the 44px
   target is the surrounding <label>, which is what the pointer hits. */
.rail__group input[type="checkbox"] {
  appearance: none;
  width: 16px;
  height: 16px;
  min-height: 0;
  flex: none;
  padding: 0;
  border: var(--bw) solid var(--rule-strong);
  background: var(--night);
}
.rail__group input:checked { background: var(--eye); box-shadow: inset 0 0 0 2px var(--night); }
.rail__query select {
  width: 100%;
  min-height: var(--tap);
  padding: 0 var(--s2);
  background: var(--night);
  color: var(--cream);
  border: var(--bw) solid var(--rule-strong);
}
#rail-apply { width: 100%; margin-top: var(--s3); }

/* No .rail__query display rule here. The form carries `js-only`, and the
   two-line gate at the bottom of this file does the work. There is no
   `.no-js` class in this codebase: a CSP of script-src 'self' forbids the
   inline script the classic removal trick needs. */

.filterbar__count { color: var(--muted); font-variant-numeric: tabular-nums; }

/* ============================================================
   6.3 DATATABLE
   NOT a <table> element, and it never becomes one (DECISIONS.md D29).
   content-visibility is a no-op on display:table-row-group (D13), and
   skipping the offscreen two thirds of a results page is the entire
   reason this component exists. So: display:grid, explicit ARIA roles,
   rows chunked into groups of 20 that carry the containment.
   ============================================================ */

.dt {
  /* One template, shared by the header and every group, so columns line up
     across group boundaries. Every track has a fixed minimum and an fr max;
     no track is `auto`, because an auto track sizes to its own group's
     content and the groups would then disagree with each other. */
  --dt-cols: minmax(14ch, 3fr) 6ch 9ch 5ch minmax(8ch, 0.8fr) minmax(13ch, 1fr) 9ch;
  display: grid;
  grid-template-columns: 1fr;
  font-size: var(--fs);
  line-height: var(--lh);
  border: var(--bw) solid var(--rule-strong);
}
.dt__head, .dt__group { display: grid; grid-template-columns: var(--dt-cols); }
.dt__row { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; }

.dt__row > * { padding: var(--pad) var(--s2); border-bottom: 1px solid var(--rule-quiet); }
.dt__head { position: sticky; top: 0; z-index: 1; background: var(--night2); }
.dt__head [role="columnheader"] {
  color: var(--eye);
  font-weight: 600;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  border-bottom: var(--bw) solid var(--rule-strong);
}
.dt__group .dt__row:nth-child(even) { background: var(--night2); }
.dt__group .dt__row:hover { background: color-mix(in srgb, var(--eye) 7%, transparent); }
.dt--num { text-align: right; font-variant-numeric: tabular-nums; }
.dt a { color: var(--eye); text-decoration: underline; text-underline-offset: 2px; }

/* The point of the whole structure: containment on a real box.
   Groups are exactly 20 rows, so :nth-child(even) parity stays continuous
   across a seam and the zebra never doubles a stripe. */
/* DEVIATION from 6.3, and it is a correctness fix, not taste.
   The spec writes `contain-intrinsic-size: auto calc(20 * var(--row-h))`.
   The shorthand applies a single length to BOTH axes, so a group that is
   skipped (which is every group below the fold, which is the point of the
   component) also gets that length as its WIDTH: measured 4800px on a
   375px viewport, and the page then scrolls sideways. The height is the
   only axis that needs an estimate. The inline size of a block-level grid
   item already comes from its container, so leaving the width unset is
   both correct and what the technique intends. */
.dt__group {
  content-visibility: auto;
  contain-intrinsic-height: auto calc(20 * var(--row-h));
}

/* ============================================================
   6.4 BEVELBUTTON
   border-top and border-left take the light color, border-bottom and
   border-right take the dark color. That is literally how Win95 chrome
   worked. Pressed swaps them and translates by the shadow offset.
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: 0 var(--s4);
  font: 600 var(--fs)/1.2 var(--mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  transition: none;                  /* the press is a state, not a tween */

  background: var(--night2);
  color: var(--eye);
  border: var(--bw) solid;
  border-color: var(--bevel-lit) var(--bevel-dim) var(--bevel-dim) var(--bevel-lit);
  outline: 1px solid var(--rule-strong);   /* the 3:1 boundary, under the bevel */
  outline-offset: 0;
  box-shadow: var(--shadow);
}
a.btn:visited { color: var(--eye); }

.btn:active {
  border-color: var(--bevel-dim) var(--bevel-lit) var(--bevel-lit) var(--bevel-dim);
  background: var(--night);
  transform: translate(4px, 4px);
  box-shadow: none;
}

/* filled primary: amber ground, night ink. 11.48:1. */
.btn--primary {
  background: var(--eye);
  color: var(--night);
  border-color: var(--bevel-lit-a) var(--bevel-dim-a) var(--bevel-dim-a) var(--bevel-lit-a);
}
a.btn--primary:visited { color: var(--night); }
.btn--primary:active {
  background: color-mix(in srgb, var(--eye) 88%, var(--night));
  border-color: var(--bevel-dim-a) var(--bevel-lit-a) var(--bevel-lit-a) var(--bevel-dim-a);
}

/* ember variant: the one CTA per page that means "do the thing" */
.btn--cta {
  background: var(--ember);
  color: var(--night);
  border-color: var(--bevel-lit-a) var(--bevel-dim-a) var(--bevel-dim-a) var(--bevel-lit-a);
}
a.btn--cta:visited { color: var(--night); }
.btn--cta:active {
  background: color-mix(in srgb, var(--ember) 88%, var(--night));
  border-color: var(--bevel-dim-a) var(--bevel-lit-a) var(--bevel-lit-a) var(--bevel-dim-a);
}

.btn:hover, .btn--ghost:hover { background: color-mix(in srgb, var(--eye) 7%, var(--night2)); }
.btn--primary:hover, .btn--cta:hover { filter: brightness(1.06); }

/* ghost: no fill, so the border must carry 3:1 on its own */
.btn--ghost {
  background: transparent;
  border: var(--bw) solid var(--rule-strong);
  box-shadow: none;
}
.btn--ghost:active { background: var(--night2); transform: translate(2px, 2px); }

.btn--sm { min-height: var(--tap); padding: 0 var(--s2); }

/* Disabled changes color AND drops the shadow AND flattens the bevel:
   three signals, not color alone (SC 1.4.1). */
.btn:disabled, .btn[aria-disabled="true"] {
  background: var(--night2);
  color: var(--muted);
  border-color: var(--rule);
  box-shadow: none;
  cursor: default;
  transform: none;
}
@media (prefers-reduced-motion: reduce) { .btn:active { transform: none; } }

/* ============================================================
   6.5 STOCKPIP
   Three channels carry the state: glyph shape, color, and text. Never a
   bare colored dot (SC 1.4.1). The glyph is aria-hidden; the label is
   the accessible content.
   ============================================================ */

.pip { display: inline-flex; align-items: baseline; gap: var(--s1); white-space: nowrap; }
.pip__glyph { letter-spacing: -0.05em; }
.pip__label { font-size: var(--fs); font-variant-numeric: tabular-nums; }
.pip[data-state="in"]  { color: var(--ok); }
.pip[data-state="low"] { color: var(--warn); }
.pip[data-state="out"] { color: var(--muted); }

/* ============================================================
   6.6 BREADCRUMB
   The drill-down path IS the navigation model: each segment is a
   one-click un-filter. It scrolls, it never collapses to an ellipsis.
   ============================================================ */

.crumbs {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid var(--rule);
}
.crumbs::-webkit-scrollbar { height: 3px; }
.crumbs::-webkit-scrollbar-thumb { background: var(--rule); }
.crumbs ol {
  display: flex;
  align-items: center;
  gap: var(--s1);
  list-style: none;
  padding: var(--s1) 0;
}
.crumbs li { flex: none; scroll-snap-align: start; }
.crumbs li + li::before { content: "/"; color: var(--muted); margin-right: var(--s1); }
.crumbs a {
  display: inline-block;
  min-height: var(--tap);
  line-height: var(--tap);
  padding-inline: var(--s1);
  font-size: var(--fs);
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: var(--eye);
  text-decoration: none;
}
.crumbs a:hover { text-decoration: underline; }
.crumbs a:visited { color: var(--eye); }
.crumbs [aria-current="page"] { color: var(--cream); scroll-snap-align: end; }

/* ============================================================
   6.7 EVENTROW
   A .dt__row with its own column set. Events are a DataTable, not a
   second table system: everything else is inherited, including the
   mobile record-card transform.
   ============================================================ */

.dt--events {
  --dt-cols: 10ch 8ch minmax(12ch, 1.4fr) 10ch 7ch minmax(14ch, 1.6fr) minmax(12ch, 1fr) 9ch;
}

/* Activity feed is five columns of ids and sentences. The default template
   is the seven-column card grid, and those tracks are narrower than a member
   id, so the text paints across the next cell. */
.dt#activity,
.dt#history {
  --dt-cols: minmax(10ch, 0.9fr) minmax(8ch, 1.2fr) minmax(8ch, 1fr) minmax(8ch, 1.2fr) minmax(12ch, 2fr);
}
.dt#activity .dt__row > *,
.dt#history .dt__row > * {
  min-width: 0;
  overflow-wrap: anywhere;
}
.ev time { color: var(--eye); font-weight: 600; }
.ev:hover { background: color-mix(in srgb, var(--violet) 8%, transparent); }

/* ============================================================
   6.8 STATUSBAR
   One line of live, true information. Every cell is computed at build
   from real data; a hardcoded value here is the costume failure.
   PAUSE is the WCAG 2.2.2 mechanism, not decoration: hover pause alone
   is unreachable by keyboard and touch.
   ============================================================ */

.statusbar {
  position: sticky;
  bottom: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--s3);
  height: 32px;
  padding-inline: var(--s2);
  background: var(--night2);
  border-top: var(--bw) solid var(--rule-strong);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
}
.statusbar__cell { flex: none; }
.statusbar__cell[data-tone="ok"]  { color: var(--ok); }
.statusbar__cell[data-tone="bad"] { color: var(--muted); }
.statusbar__track { flex: 1; overflow: hidden; min-width: 0; }
.statusbar__run {
  display: flex;
  gap: var(--s8);
  width: max-content;
  animation: tick 45s linear infinite;
}
.statusbar__run > span::before { content: "▚ "; color: var(--rule); }
.statusbar__track:hover .statusbar__run,
.statusbar__track:focus-within .statusbar__run,
.statusbar[data-paused] .statusbar__run { animation-play-state: paused; }
@keyframes tick { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.statusbar__btn {
  flex: none;
  min-height: 24px;
  padding-inline: var(--s1);
  background: transparent;
  color: var(--eye);
  border: 1px solid var(--rule-strong);
  font: 600 12px/1 var(--mono);
  letter-spacing: 0.06em;
  cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
  .statusbar__run { animation: none; }
  .statusbar__track { overflow-x: auto; }   /* becomes a scrollable strip */
}
/* Mobile: the height stays 32px and PHASE, HOURS and BUILD collapse behind
   the ticker. NEXT stays, because it is the one cell that changes. */
@media (max-width: 1023px), (pointer: coarse) {
  .statusbar__cell:nth-child(1),
  .statusbar__cell:nth-child(2),
  .statusbar__build { display: none; }
}
/* Under 400px the density toggle goes too: touch is already comfortable.
   PAUSE stays, because it is a compliance mechanism, not chrome. */
@media (max-width: 399px) {
  .statusbar [data-density-toggle] { display: none; }
}

/* ============================================================
   6.9 BUTTON88X31
   The footer badge row. One sprite sheet, one request, one
   background-position per class. The row index is a CLASS, never a
   style attribute: style-src 'self' blocks attributes too, and every
   badge would silently fall back to row 0 (DECISIONS.md D26).
   ============================================================ */

.b88row { display: flex; flex-wrap: wrap; gap: var(--s1); list-style: none; padding: 0; }
.b88row li { padding-block: 7px; }   /* 31px art, 45px hit area */
.b88 {
  display: block;
  width: 88px;
  height: 31px;
  background-image: image-set(url("/img/88x31.png") 1x, url("/img/88x31@2x.png") 2x);
  background-repeat: no-repeat;
  background-size: 88px auto;
  border: 1px solid var(--rule);
}
.b88--0 { background-position: 0 0; }
.b88--1 { background-position: 0 -31px; }
.b88--2 { background-position: 0 -62px; }
.b88--3 { background-position: 0 -93px; }
.b88--4 { background-position: 0 -124px; }
.b88--5 { background-position: 0 -155px; }
.b88:hover { border-color: var(--rule-strong); }

.hero { position: relative; isolation: isolate; padding-block: var(--s8); text-align: center; }
.hero__fx > * { position: absolute; inset: 0; z-index: var(--z-fx); pointer-events: none; }
.hero__h1 {
  font-family: var(--display);
  font-size: 33px;
  line-height: 36px;
  color: var(--cream);
}
@media (min-width: 1024px) and (pointer: fine) {
  .hero__h1 { font-size: 44px; line-height: 48px; }
}
.hero__tag { color: var(--violet); font-size: 16px; margin-inline: auto; }

/* Moe, flat. No glow bloom, no flicker, no atmosphere layers (DECISIONS.md D39). */
.eyebox {
  position: relative;
  width: min(236px, 63vw);
  aspect-ratio: 1 / 1;
  margin: 0 auto var(--s2);
}
.eyebox .eye {
  width: 100%;
  height: auto;
  user-select: none;
}

/* Scaffold hero text, still used by the committed home route (T3 replaces it). */
.hero .tagline {
  color: var(--violet);
  font-style: italic;
  font-size: max(16px, 1.1em);
  margin-top: var(--s3);
  margin-inline: auto;
}
.hero p { margin-inline: auto; }

/* ============================================================
   6.11 SEARCHBOX
   The box dominates the header. The listbox ships empty and stays
   empty without script; public/js/typeahead.js (T6) owns the fetch,
   the swap, the keyboard, and every ARIA attribute.

   One deviation from 6.11, deliberate: the listbox rules key on
   [role="listbox"] rather than the literal #sx, so a page can host two
   search boxes without one of them losing its popup styling. Specificity
   still beats the `.js .js-only` reveal (0,3,0 against 0,2,0), which is the
   property 6.11 depends on to keep an empty listbox from painting a bare
   bordered strip under the input.
   ============================================================ */

.search { position: relative; display: flex; gap: 0; flex: 1; min-width: 0; }
.search input {
  flex: 1;
  min-width: 0;
  min-height: var(--tap);
  padding: 0 var(--s2);
  font: 400 max(var(--fs), 16px)/1.3 var(--mono);   /* 16px floor: no iOS zoom */
  color: var(--cream);
  background: var(--night);
  border: var(--bw) solid var(--rule-strong);
}
.search input::placeholder { color: var(--muted); }
.search [role="listbox"] {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: var(--z-pop);
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--panel);
  border: var(--bw) solid var(--rule-strong);
  box-shadow: var(--shadow);
}
.search [role="listbox"]:empty { display: none; }
.search [role="listbox"] li {
  min-height: var(--tap);
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 0 var(--s2);
  cursor: pointer;
}
.search [role="listbox"] li[aria-selected="true"] { background: var(--eye); color: var(--night); }
.search [role="listbox"] li b { margin-left: auto; color: var(--muted); font-weight: 400; }
.search [role="listbox"] li[aria-selected="true"] b { color: var(--night); }
.sx__kind {
  flex: none;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.10em;
  color: var(--muted);
}
.search [role="listbox"] li[aria-selected="true"] .sx__kind { color: var(--night); }

/* ============================================================
   6.12 PAGINATION
   Real URLs. The path form is the only form. No infinite scroll.
   ============================================================ */

.pag {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  padding: var(--s4) 0;
}
.pag__state {
  color: var(--muted);
  font-size: var(--fs);
  font-variant-numeric: tabular-nums;
  margin-inline: auto;
}
.pag [aria-current="page"] { background: var(--eye); color: var(--night); }

/* ============================================================
   6.13 TAG / CHIP
   The active-facet row. Each chip is an <a> pointing at the URL WITHOUT
   that facet, so it works with JS off.
   ============================================================ */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
  list-style: none;
  padding: 0;
  margin-block: var(--s3);
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--tap);
  padding-inline: var(--s2);
  background: var(--night2);
  color: var(--eye);
  border: var(--bw) solid var(--rule-strong);
  text-decoration: none;
  font-size: var(--fs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.chip:visited { color: var(--eye); }
.chip:hover { background: color-mix(in srgb, var(--eye) 10%, transparent); }
/* Deviation from 6.13, deliberate and reported: the snippet there gives
   .chip--clear a --rule border, which composites to 1.83:1 and cannot bound
   an interactive control on its own (SC 1.4.11, and DESIGN-SYSTEM.md 3.3
   says so itself). CLEAR ALL is a link, so it keeps the 3:1 boundary and
   differentiates by ink alone. */
.chip--clear { color: var(--muted); }
.chip--clear:visited { color: var(--muted); }

/* ============================================================
   6.14 EMPTYSTATE
   The category's most visible bug, fixed. Real count, a sentence that
   names the facets, one-click removal, and a link up one level.
   ============================================================ */

.empty {
  border: var(--bw) dashed var(--rule-strong);
  padding: var(--s5);
  background: var(--panel);
}
.empty__head {
  font-family: var(--display);
  font-size: 22px;
  line-height: 24px;
  color: var(--eye);
}
.empty__body { margin-top: var(--s3); max-width: 60ch; line-height: 1.55; }
.empty__alt { margin-top: var(--s3); color: var(--muted); }

/* ============================================================
   6.15 PREOPENBADGE
   Everything that would be a purchase becomes a signal of intent.
   Prints no date, no countdown, no street address, under either
   ADDRESS_MODE. Inline variant sits on a single claim: a price, a
   seat count, a date (DECISIONS.md D37).
   ============================================================ */

.preopen {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  border: var(--bw) solid var(--rule-strong);
  border-left-width: var(--bw-thick);
  background: var(--night2);
  padding: var(--s3);
  font-size: var(--fs);
  line-height: 1.5;
}
.preopen__tag {
  flex: none;
  background: var(--eye);
  color: var(--night);
  font-weight: 600;
  letter-spacing: 0.12em;
  padding: 2px var(--s2);
}
.preopen--inline {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s1);
  border: 0;
  background: transparent;
  padding: 0;
  white-space: nowrap;
}
.preopen--inline .preopen__tag { font-size: 12px; letter-spacing: 0.10em; padding: 1px var(--s1); }

/* ============================================================
   MOTION (section 7). The caret is the one animation on the home H1.
   Everything here is zeroed by the reduced-motion blanket in base.css.
   ============================================================ */

.caret { animation: caret 1.06s steps(1, end) infinite alternate; color: var(--eye); }
@keyframes caret { from { opacity: 1; } to { opacity: 0; } }

/* Scaffold caret, still used by the committed home route. */
.cursor {
  display: inline-block;
  width: 0.6em;
  color: var(--eye);
  animation: caret 1.06s steps(1, end) infinite alternate;
}

/* ============================================================
   MOBILE TRANSFORMS (INFORMATION-ARCHITECTURE.md 7)
   Same markup, repositioned. There is never a second template.
   Gated on width AND pointer: a touchscreen laptop reports coarse at
   1440px and still needs 44px targets.
   ============================================================ */

@media (max-width: 1023px), (pointer: coarse) {

  /* --- 6.3 DataTable: record cards, not a horizontal scroller ---
     The header row is visually hidden with clip-path, NOT display:none:
     display:none would drop the column headers out of the accessibility
     tree, and the accessible name of a cell still comes from its column
     position. [data-label]::before is visual only. */
  .dt { border: 0; gap: var(--s3); }
  .dt__head {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .dt__group {
    grid-template-columns: 1fr;
    gap: var(--s3);
    /* Height only. See the note on .dt__group above. */
    contain-intrinsic-height: auto calc(20 * 240px);
  }
  .dt__row {
    grid-template-columns: 1fr;        /* subgrid is off; the card is its own box */
    border: var(--bw) solid var(--rule-strong);
    box-shadow: var(--shadow);
    background: var(--panel);
  }
  .dt__group .dt__row:nth-child(even) { background: var(--panel); }   /* no zebra on cards */
  .dt__row > * {
    display: grid;
    grid-template-columns: 9ch 1fr;
    gap: var(--s2);
    align-items: baseline;
    min-height: 32px;
  }
  .dt__row > *:last-child { border-bottom: 0; }
  .dt__row > *::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: 12px;
    letter-spacing: 0.10em;
    text-transform: uppercase;
  }
  .dt--num { text-align: left; }

  /* --- 6.2 FacetRail: the same #rail, repositioned into a sheet ---
     Gated on .js, not on the breakpoint alone. Without the .js gate,
     visibility:hidden would hide the path-facet LINKS too, and the only
     control that reveals them is a button that needs script. With JS off
     the rail stays in normal flow above the results and every path facet
     is a live link (DECISIONS.md D28). */
  .js .rail {
    position: fixed;
    inset: auto 0 0 0;
    z-index: var(--z-sheet);
    width: 100%;
    max-height: 88dvh;
    background: var(--panel);
    border-top: var(--bw) solid var(--rule-strong);
    transform: translateY(100%);
    visibility: hidden;
  }
  .js .rail[data-open] { transform: none; visibility: visible; }
  .rail__scrim { position: fixed; inset: 0; z-index: var(--z-scrim); background: rgba(0, 0, 0, 0.6); }
  /* Three classes deep on purpose. The `.js .js-only { display: revert }`
     reveal is (0,2,0) and reverts to the UA default, which for a <div> is
     `block`; a plain `.filterbar { display: flex }` would lose to it and the
     bar would stack instead of sitting on one line. Any js-only component
     that needs a non-default display has to win this the same way. */
  .js .filterbar.js-only,
  .filterbar {
    position: sticky;
    bottom: 0;
    z-index: var(--z-sticky);
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s2);
    background: var(--night2);
    border-top: var(--bw) solid var(--rule-strong);
  }
}

/* The filter bar is the sheet trigger, so above the breakpoint (where the
   rail is already visible in the page) it is dead chrome and goes. The
   selector is deliberately three classes deep so it beats the `.js .js-only`
   reveal below it regardless of source order. Below the breakpoint `js-only`
   still gates it: the button opens a sheet, and a control that does nothing
   is worse than no control. */
@media (min-width: 1024px) and (pointer: fine) {
  .js .filterbar.js-only { display: none; }
}

/* ============================================================
   JS-GATED CONTROLS
   Query-facet controls, the filter bar, and the typeahead listbox only
   make sense with script. Default hidden; public/js/nav.js adds `.js` to
   <html> as its first statement. Additive, not subtractive: a CSP of
   script-src 'self' forbids the inline script a .no-js removal needs.
   DECISIONS.md D28.
   ============================================================ */
.js-only { display: none; }
.js .js-only { display: revert; }

/* ============================================================
   STYLEGUIDE ONLY (/styleguide/, noindex, not in the sitemap).
   Two layout helpers so the proof page can lay components out beside
   each other. They are NOT part of the section 6 inventory, nothing
   customer-facing uses them, and no card should reach for them.
   ============================================================ */
.sg-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); max-width: none; }
.sg-catalog { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: var(--s5); align-items: start; }
@media (max-width: 1023px), (pointer: coarse) {
  .sg-catalog { grid-template-columns: minmax(0, 1fr); }
}

/* ============================================================
   SITE CHROME (T3)
   Header, primary nav, SHOP disclosure, footer.
   INFORMATION-ARCHITECTURE.md 3.1 (header order), 3.3 (SHOP panel
   grouping), 3.4 (footer), 3.5 (mobile). This is layout for the
   site shell, not a section 6 component; the components the shell
   composes (SearchBox, Button88x31, StatusBar) keep their own rules
   above and are not restyled here.
   ============================================================ */

.sitehead {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: var(--night2);
  border-bottom: var(--bw) solid var(--rule);
}
.sitehead__in {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  padding-block: var(--s2);
}
.sitehead__brand { flex: none; }

/* IA 3.1 item 2: the largest single allocation of header width, never
   collapsed behind an icon at any width. */
.sitehead__search { flex: 1 1 40%; min-width: min(40%, 320px); }

.sitenav { display: flex; align-items: center; gap: var(--s4); }
.sitenav--primary { flex: 1 1 auto; min-width: 0; }
.sitenav--util { flex: none; margin-left: auto; }
.sitenav__menu { flex: 1 1 auto; min-width: 0; }

/* The mobile MENU button. Hidden above the breakpoint, where the panel is
   laid out inline instead. */
.sitenav__menubtn {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding-inline: var(--s3);
  border: var(--bw) solid var(--rule-strong);
  color: var(--eye);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  cursor: pointer;
}
.sitenav__menubtn::marker, .sitenav__menubtn::-webkit-details-marker { content: ''; display: none; }
.sitenav__menu[open] > .sitenav__menubtn { background: color-mix(in srgb, var(--eye) 10%, transparent); }

.sitenav__panel { max-height: 78dvh; overflow-y: auto; }
.sitenav__primary,
.sitenav__utillist {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s4);
  list-style: none;
  padding: 0;
}
.sitenav__utillist { gap: var(--s3); }
.sitenav a,
.shopnav__btn {
  color: var(--cream);
  text-decoration: none;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: var(--fs);
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
}
.sitenav__utillist a { color: var(--muted); }
.sitenav a:hover, .shopnav__btn:hover { color: var(--eye); }
.sitenav a:visited { color: var(--cream); }
.sitenav__utillist a:visited { color: var(--muted); }
.sitenav [aria-current="page"] { color: var(--eye); border-bottom: var(--bw-thick) solid var(--eye); }

/* The build-time open/closed pip. One character, and the only thing it reads
   is the FILED hours (DECISIONS.md D1). public/js/nav.js re-checks it on load
   so a cached document does not keep yesterday's answer. */
.hourpip { display: inline-flex; align-items: center; min-height: var(--tap); }
.hourpip__dot { color: var(--muted); }
.hourpip[data-state="open"] .hourpip__dot { color: var(--eye); }

/* SHOP disclosure. Links and headings only, no thumbnails, no per-SKU markup. */
.shopnav { position: static; }
.shopnav__btn { cursor: pointer; list-style: none; }
.shopnav__btn::marker, .shopnav__btn::-webkit-details-marker { content: ''; display: none; }
.shopnav__btn::after { content: ' \25BE'; color: var(--muted); }
.shopnav[open] > .shopnav__btn { color: var(--eye); }
.shopnav__panel {
  display: grid;
  gap: var(--s5);
  padding: var(--s4);
  background: var(--panel);
  border: var(--bw) solid var(--rule-strong);
}
.shopnav__group h2 {
  font-size: var(--fs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.10em;
  margin-bottom: var(--s2);
}
.shopnav__group ul { list-style: none; padding: 0; }
.shopnav__group li { padding-block: 2px; }
.shopnav__group a { text-transform: none; letter-spacing: 0; }
.shopnav__note { display: block; color: var(--muted); font-size: max(13px, 0.85em); }
.shopnav__all { margin-top: var(--s2); }
.shopnav__all a { color: var(--eye); }

/* --- footer -------------------------------------------------- */

.sitefoot {
  margin-top: var(--s8);
  background: var(--night2);
  border-top: var(--bw) solid var(--rule);
  padding-top: var(--s6);
}
.sitefoot__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s5);
}
.sitefoot__col h2 {
  font-size: var(--fs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.10em;
  margin-bottom: var(--s2);
}
.sitefoot__col ul { list-style: none; padding: 0; }
.sitefoot__col li { padding-block: 2px; }
.sitefoot__col a { color: var(--cream); text-decoration: none; }
.sitefoot__col a:hover { color: var(--eye); text-decoration: underline; }
.sitefoot__col a:visited { color: var(--cream); }
.sitefoot__badges { margin-block: var(--s5) var(--s3); }
.sitefoot__addr { max-width: none; color: var(--cream); padding-bottom: var(--s5); }
.sitefoot__addr .quiet { display: block; }

/* --- catalog and article shells ------------------------------ */

.cat__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s3); }
.cat__meta { color: var(--muted); font-variant-numeric: tabular-nums; margin-left: auto; }
.cat__body { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s5); align-items: start; }
.art { max-width: var(--measure); }
.art__lede { color: var(--violet); }

/* --- home ---------------------------------------------------- */

.hero__copy { margin-inline: auto; max-width: var(--measure); text-align: left; }
.hero__cta { display: flex; flex-wrap: wrap; gap: var(--s3); justify-content: center; max-width: none; }
/* Hours + address, the hero subheading beneath the tagline (D46). */
.hero__meta {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  align-items: center;
  margin-inline: auto;
  color: var(--muted);
}
.hero__hours {
  color: var(--cream);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.hero__addr { color: var(--muted); }

/* Organized-play table: short name, check-in, play. Two time columns, right
   aligned and tabular so they read as a schedule down the column (D45). */
.op { width: 100%; border-collapse: collapse; }
.op caption { text-align: left; }
.op th[scope="col"] {
  font-size: max(11px, 0.7em);
  letter-spacing: 0.1em;
  color: var(--muted);
  text-align: right;
  padding: 0 0 var(--s1);
  border: 0;
}
.op th[scope="col"]:first-child { text-align: left; }
.op th[scope="row"] {
  font-weight: 600;
  color: var(--eye);
  text-align: left;
  padding: 2px var(--s2) 2px 0;
  border: 0;
  white-space: nowrap;
}
.op td {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--cream);
  padding: 2px 0 2px var(--s2);
  border: 0;
  white-space: nowrap;
}
.op tbody tr:nth-child(2n) { background: transparent; }

/* ============================================================
   HOME BOARD (D47/D48)
   One table does the work two panels used to: GAME, DAY, CHECK-IN, PLAY.
   Because it is a single table the browser aligns the columns; there is no
   second table to drift against. Times are :MM-padded and right-aligned so the
   time columns read as one clean edge. A game with no standing night shows
   blank day and times rather than a fabricated one (D45).
   ============================================================ */
.home .panels { grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .home .panels { grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr); }
}
.board { width: 100%; border-collapse: collapse; }
.board caption { text-align: left; }
.board thead th {
  font-size: max(11px, 0.7em);
  letter-spacing: 0.1em;
  color: var(--muted);
  font-weight: 600;
  text-transform: uppercase;
  text-align: right;
  padding: 0 0 var(--s2);
  border: 0;
  border-bottom: 1px solid var(--rule-quiet);
  white-space: nowrap;
}
.board thead th:first-child { text-align: left; }
.board tbody th[scope="row"] {
  font-weight: 400;
  text-align: left;
  padding: 0;
  border: 0;
  white-space: nowrap;
}
.board tbody th[scope="row"] a { display: inline-flex; align-items: center; min-height: 32px; }
.board tbody td {
  height: 32px;
  border: 0;
  text-align: right;
  padding-left: var(--s3);
  font-variant-numeric: tabular-nums;
  color: var(--cream);
  white-space: nowrap;
}
.board__day {
  color: var(--eye);
  font-weight: 600;
  letter-spacing: 0.06em;
  width: 4ch;
}
.board tbody tr + tr th, .board tbody tr + tr td { border-top: 1px solid var(--rule-quiet); }

/* Game name: full on desktop, short on narrow so all four columns fit without a
   horizontal scroll (SC 1.4.10). Both ship; CSS picks one. */
.board__short { display: none; }
.board__regnote { display: none; }
@media (max-width: 680px), (pointer: coarse) {
  .board__long { display: none; }
  .board__short { display: inline; }
  .board__reg { display: none; }            /* check-in is play minus 30, noted below */
  .board__prize { white-space: normal; }
  .board__regnote { display: block; margin-top: var(--s3); }
  .board tbody td { padding-left: var(--s2); }
  .board thead th, .board tbody td { font-size: max(12px, 0.85em); }
}

.board__foot { margin-top: var(--s4); display: flex; flex-direction: column; gap: var(--s2); }
.board__also { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); }
.board__also a { color: var(--cream); }
.board__cta { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); margin-top: var(--s1); }
.board__cta a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--eye);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: max(12px, 0.8em);
}
.carry { list-style: none; padding: 0; }
.carry li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  justify-content: space-between;
  align-items: baseline;
  padding-block: 3px;
  border-bottom: 1px solid var(--rule-quiet);
}
/* The REG/PLAY label is a unit. Let the game name wrap instead of breaking a
   schedule across two lines, and keep it right-aligned when it does wrap. */
.carry li > .quiet {
  white-space: nowrap;
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

/* --- desktop ------------------------------------------------- */

@media (min-width: 1024px) and (pointer: fine) {
  /* MENU is a mobile control. Above the breakpoint the panel is laid out
     inline, which needs the UA's own ::details-content containment lifted.
     Every evergreen engine ships ::details-content as of 2025; on an engine
     that does not, the desktop nav collapses behind a MENU button that still
     works, which is a degradation, not a break. */
  .sitenav__menubtn { display: none; }
  .sitenav__menu::details-content { content-visibility: visible; block-size: auto; }
  .sitenav__panel { max-height: none; overflow: visible; }

  .sitenav__primary { flex-wrap: nowrap; }

  /* The SHOP panel hangs from the sticky header rather than pushing the page
     down on a wide screen. .sitehead is position: sticky, which is a
     containing block for this. */
  .shopnav__panel {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    z-index: var(--z-nav);
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .cat__body { grid-template-columns: 232px minmax(0, 1fr); }
}

/* --- mobile -------------------------------------------------- */

@media (max-width: 1023px), (pointer: coarse) {
  /* IA 3.5: wordmark row, then a persistent full-width search row. Search
     never goes behind the disclosure. The MENU panel opens in normal document
     flow between them, pushing search down rather than covering it: no
     overlay, no focus trap, no JavaScript. */
  /* IA 3.5 caps the header at 96px of viewport. Two 44px rows plus the
     default block padding lands at 102, so the padding tightens here. The tap
     targets stay 44px; only the gap around them shrinks. */
  /* Three rows: wordmark plus utility, then the persistent full-width search,
     then MENU and its panel. That is 140px, over IA 3.5's 96px sticky cap, so
     the header stops being sticky here. The cap exists to keep the header off
     a phone viewport and a header that scrolls away costs none of it. */
  .sitehead { position: static; }
  .sitehead__in { gap: var(--s2); padding-block: var(--s1); }
  .sitehead__brand { order: 1; }
  .sitenav--util { order: 2; margin-left: auto; }
  .sitehead__search { order: 3; flex: 1 1 100%; min-width: 100%; }
  .sitenav--primary { order: 4; flex: 1 1 100%; min-width: 100%; }
  .sitenav__menu { width: 100%; }

  .sitenav__panel { padding-top: var(--s3); }
  .sitenav__menubtn { width: 100%; }
  .sitenav__primary { flex-direction: column; align-items: stretch; gap: 0; }
  .sitenav__primary > li { border-bottom: 1px solid var(--rule-quiet); }
  .sitenav__primary a { min-height: var(--tap); }
  .shopnav__panel { grid-template-columns: minmax(0, 1fr); }
}

/* ============================================================
   PRODUCT DETAIL  (card T7)
   /product/<handle>/ only. No new tokens, no raw hex, no
   border-radius, no blurred shadow, and no inline style
   attribute: style-src 'self' blocks attributes as well as
   elements (DECISIONS.md D26).

   Layout is grid AREAS, not `order`. The DOM sequence is
   price then description then facts, which is the right order
   on a phone; the desktop rule places the buy column right of
   the body without moving anything in the accessibility tree.
   ============================================================ */

.pd__head { margin-block: var(--s5) var(--s4); }
.pd__sub { color: var(--muted); margin-top: var(--s2); }

.pd {
  display: grid;
  gap: var(--s5);
  align-items: start;
  grid-template-areas: "buy" "body";
}
.pd__buy { grid-area: buy; }
.pd__body { grid-area: body; }
.pd__related { margin-top: var(--s6); }

/* Image frame. The box is reserved by the width/height attribute pair on the
   <img>, so this adds a border and nothing that can move. */
.pg {
  margin: 0 0 var(--s4);
  border: var(--bw) solid var(--rule);
  background: var(--panel);
}

/* The price card. --rule-strong, not --rule: this is a control boundary and
   --rule composites to 1.83:1 (DESIGN-SYSTEM.md 3.3). */
.pb {
  border: var(--bw) solid var(--rule-strong);
  background: var(--panel);
  padding: var(--pad-lg);
}
.pb__price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: 0;
}
.pb__now {
  font-family: var(--display);
  font-size: 33px;
  line-height: 36px;
  color: var(--eye);
  font-variant-numeric: tabular-nums;
}
.pb__was { color: var(--muted); font-variant-numeric: tabular-nums; }
.pb__stock { margin-top: var(--s3); }
.pb__act { margin-top: var(--s4); display: flex; flex-wrap: wrap; gap: var(--s3); }
.pb__note { color: var(--muted); margin: 0; }

.pd__specs { margin-top: var(--s5); }
.pd__specs th { width: 12ch; white-space: nowrap; }

@media (min-width: 1024px) and (pointer: fine) {
  .pd {
    grid-template-columns: minmax(0, 1fr) 320px;
    grid-template-areas: "body buy";
  }
}
