/* Civic Clean base: hairlines + type weight carry hierarchy.
   No shadows, no gradients, radius capped at --radius-default
   (--radius-full only for badges/chips).

   Sizes are rem so user browser font-size preferences are honored
   (senior accommodation); px equivalents note the DESIGN.md values
   at the 16px default. */

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  /* The board (21.1): Public Sans says things; the page is paper and
     panels sit on it, never the reverse */
  font-family: var(--font-body);
  /* 1rem = 16px default — the body floor. Only uppercase panel labels
     render smaller (13px equiv), per the board's floors. */
  font-size: 1rem;
  line-height: var(--line-body);
  color: var(--ink);
  background: var(--page);
}

/* Keyboard is the primary instrument: the ring must be obvious at arm's
   length on club hardware */
:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
}

a {
  color: var(--color-text);
}

button,
input,
select,
textarea {
  font: inherit;
}

button,
.button,
input[type="submit"] {
  min-width: 44px;
  min-height: 44px;
}

img {
  max-width: 100%;
}

/* --- Button components (Story 10.6) ---
   Story 1.2 shipped the tokens and deferred the components; nothing
   picked them up, so every button outside run.css rendered as a browser
   default until now.

   These are CLASS selectors on purpose. The shared element rule above
   matches every <button> in the app — .nav-toggle, the run-view controls
   run.css owns, .link-button — so a fill declared there would leak onto
   all of them. Declare appearance here, never there.

   Nothing animates: base.css is asserted file-wide to contain no
   transition, no animation and no reduced-motion guard. */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--s-5);
  /* The board's filled action (21.1): amber with ink text, 52px — one
     per panel, at most one per page; every other control is outlined */
  min-height: var(--target-primary);
  font-weight: 700;
  color: var(--action-ink);
  background: var(--action);
  border: none;
  border-radius: var(--radius-default);
  /* Story 10.8's "Create an account" is the first <a> to carry this
     class, and an anchor brings the UA underline no <button> ever had */
  text-decoration: none;
  cursor: pointer;
}

/* Outlined, never filled. --color-danger is a LIGHT red in dark mode
   (#FF8177), where white-on-fill is 2.43:1 and fails AA; danger on the
   text clears AA in both modes. A solid red block would also outrank the
   run view's single amber attention point, and _checkin_row.html renders
   this once per check-in row. Mirrors .run-btn-danger. */
.button-danger {
  color: var(--color-danger);
  background: transparent;
  border: 1px solid var(--color-danger);
}

/* --color-surface, not none: three of these sit inside .ci-row, which
   can carry a success or warning tint that would otherwise bleed through
   the control. .flag and .tg chose the same for the same reason.

   The boundary is --color-text-muted, NOT --color-border. Fill and text
   both match their surroundings here, so the border alone carries "this
   is a control" — which puts it under SC 1.4.11, needing 3:1. The
   divider token gives 1.32:1 on surface and 1.16:1 on a checked row.
   text-muted gives 6.01:1 / 5.28:1, and is what DESIGN.md already
   specifies for {components.input}'s boundary. */
.button-neutral {
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-text-muted);
  border-radius: var(--radius-sm);
}

/* Story 12.1 — the first control born under the Control-boundaries
   clause. The spine's follow-toggle frontmatter, verbatim: 2px
   text-muted boundary (never the 1.32:1 divider token .flag/.tg still
   carry — their retrofit is its own ledgered story), radius-default,
   set state surface-alt + bold + the leading check the template
   renders. Ground is --color-surface: these sit on tintable rows.
   The 44px floor comes from the shared element rule. */
.follow-form {
  display: inline-block;
}

.follow-toggle {
  display: inline-flex;
  align-items: center;
  padding-inline: calc(var(--space-unit) * 1.5);
  font-family: inherit;
  font-size: 1rem;
  color: var(--color-text);
  background: var(--color-surface);
  border: 2px solid var(--color-text-muted);
  border-radius: var(--radius-default);
  cursor: pointer;
}

.follow-toggle[aria-pressed="true"] {
  background: var(--color-surface-alt);
  font-weight: 700;
}

/* The ring must stay tellable apart from the pressed fill — the .flag
   precedent, on the compliant boundary token */
.follow-toggle:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
  border-color: var(--color-text);
}

/* No :disabled rule here on purpose. No template renders a disabled
   control carrying .button, and no script sets one — the disabled
   controls that exist are run-view ones run.css already covers. A rule
   with no consumer is the Story 1.2 mistake this whole block corrects,
   so the first story that disables a .button adds it then:
   `opacity: 0.5; cursor: not-allowed`, matching run.css's four
   control families. */

/* --- Typography roles --- */

/* Newsreader names things (21.1): the page title is the board's
   32 / 1.1 / 400 / -.015em, in rem so text-size preferences hold */
.page-title {
  font-family: var(--font-name);
  font-size: 2rem; /* 32px */
  line-height: 1.1;
  font-weight: 400;
  letter-spacing: -0.015em;
}

.body-script {
  font-size: 1.125rem; /* 18px */
  line-height: 1.65;
}

/* The PAGE section label (21.1): sans 14 / 700 / .12em uppercase. A
   panel's label is the mono 13 register (.panel-label, arriving with
   the first rebuilt panel) — the two scales are deliberate. */
.section-label {
  font-size: 0.875rem; /* 14px — the floor */
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.callsign {
  font-family: var(--font-data);
  font-size: 1.0625rem; /* 17px */
  font-weight: 700;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.callsign-input {
  font-family: var(--font-data);
  font-size: 1.5rem; /* 24px — the one field that shouts */
  font-weight: 700;
  text-transform: uppercase;
  min-height: 56px;
}

.form-field .callsign-input {
  border: 2px solid var(--ink);
}

.data-mono {
  font-family: var(--font-data);
  font-size: 0.9375rem; /* 15px */
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Muted tags beside a name: a soft-retired roster row, an archive
   guest. The WORD is the signal (never color-alone); muted and small so
   the tag reads apart from the callsign it annotates. */
.inactive-label,
.archive-guest {
  font-family: var(--font-sans);
  font-size: 0.875rem; /* 14px — the floor */
  font-weight: 400;
  text-transform: none;
  color: var(--color-text-muted);
}

/* Delivery outcome labels (7.4): the WORD carries the meaning; a failed
   row adds a warning tint AND a border so it never relies on color
   alone. The error text beside it is the sweep's scrubbed message. */
.status-label {
  font-family: var(--font-sans);
  font-size: 0.875rem; /* 14px — the floor */
  font-weight: 600;
  white-space: nowrap;
}

.status-label-failed {
  padding: 0.0625rem 0.375rem;
  border: 1px solid var(--color-warning);
  border-radius: var(--radius-sm);
  background: var(--color-warning-tint);
  color: var(--color-on-warning-tint);
}

/* A missed reminder (8.8) is settled, not broken: muted border, same
   word-first posture */
.status-label-missed {
  padding: 0.0625rem 0.375rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
}

/* The per-row attempt record (8.8): small, under the error text */
.attempt-list {
  margin: 0.25rem 0 0;
  padding-left: 1rem;
  font-size: 0.875rem; /* 14px — the floor */
  color: var(--color-text-muted);
}

.attempt-list .status-error {
  display: inline;
  min-width: 0;
}

.status-error {
  display: block;
  /* Wide enough that a scrubbed httpx message reads as a sentence
     rather than a one-word-per-line column; the table-scroll region
     absorbs the width on narrow screens */
  min-width: 18rem;
  font-size: 0.875rem; /* 14px — the floor */
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}

.recipients {
  margin-top: 0.25rem;
  font-size: 0.875rem; /* 14px — the floor */
}

.recipient-list {
  margin: 0.25rem 0 0;
  padding-left: 1rem;
}

/* --- Forms (project-wide pattern: labels above, summary + inline errors) --- */

.form-field {
  margin-block: var(--space-section-gap);
  max-width: 44ch;
}

.form-field label {
  display: block;
  font-weight: 700;
  margin-block-end: calc(var(--space-unit) / 2);
}

/* A group widget (checkbox list) is a fieldset whose legend plays the
   label's part; the browser's box is reset so it sits like any field,
   and the per-option labels inside stay plain weight so the legend
   reads as the name of the group */
fieldset.form-field {
  border: 0;
  padding: 0;
  margin-inline: 0;
  min-width: 0;
}

fieldset.form-field legend {
  padding: 0;
  font-weight: 700;
  margin-block-end: calc(var(--space-unit) / 2);
}

fieldset.form-field label {
  font-weight: 400;
}

/* Text-like widgets only: checkboxes/radios/files must not become
   full-width bordered boxes when later forms use this pattern */
.form-field
  input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
.form-field textarea,
.form-field select {
  display: block;
  width: 100%;
  min-height: 52px;
  padding: var(--space-row-pad);
  /* The board (21.2): 1px --ink-muted at 52px; only a data input (the
     callsign) shouts with the 2px ink border */
  border: 1px solid var(--ink-muted);
  border-radius: var(--radius-default);
  background: var(--color-surface);
  color: var(--color-text);
}

.field-help {
  color: var(--color-text-muted);
  margin-block: 0 calc(var(--space-unit) / 2);
}

.field-error {
  color: var(--color-danger);
  font-weight: 700;
  margin-block: calc(var(--space-unit) / 2) 0;
}

.error-summary {
  border: 1px solid var(--color-border);
  border-top: 5px solid var(--danger);
  background: var(--panel);
  border-left: 4px solid var(--color-danger);
  padding: var(--space-row-pad) var(--space-gutter);
  margin-block: var(--space-section-gap);
}

/* Outcomes of redirect-after-POST actions (Story 4.4). Edge colour
   carries the tone; the message itself always says what happened, so
   the treatment is never colour alone. */
.messages {
  list-style: none;
  margin-block: var(--space-unit) 0;
  padding: 0;
}

.message {
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-border);
  padding: var(--space-row-pad) var(--space-gutter);
  margin-block: calc(var(--space-unit) / 2);
  /* The joined-scope sentence interpolates a club name (free text to
     100 chars); an unbreakable one must wrap, not push the page wide
     at 320px — same guard .club-scope and .signed-in-strip carry. */
  overflow-wrap: anywhere;
}

.message-success {
  border-left-color: var(--color-success);
}

.message-error {
  border-left-color: var(--color-danger);
  font-weight: 700;
}

.error-summary ul {
  margin: 0;
  padding-inline-start: var(--space-gutter);
}

/* --- Structure --- */

.rule {
  border: 0;
  border-top: 1px solid var(--color-border);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Skip link: visually-hidden until focused (robust at any zoom/text size;
   :focus fallback covers engines where programmatic focus isn't
   :focus-visible) */
.skip-link {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link:focus,
.skip-link:focus-visible {
  width: auto;
  height: auto;
  margin: 0;
  clip-path: none;
  left: var(--space-gutter);
  top: 0;
  padding: var(--space-row-pad);
  background: var(--color-surface);
  z-index: 10;
}

/* Desktop is the default. This 640px query is the PAGE's breakpoint
   (the nav and the column stack); run.css adds 900px and 760px for the
   desk, and the public row detail adds 400px. Fractional widths always
   match exactly one state of each. */
.page {
  max-width: calc(var(--content-width) + 2 * var(--space-margin-desktop));
  margin-inline: auto;
  padding-inline: var(--space-margin-desktop);
}

/* --- Top navigation --- */

/* The masthead (21.1): slate on every screen, 64px, the darkest band.
   It never changes colour to signal anything; a live net ADDS to it. */
.site-header {
  background: var(--slate);
  color: var(--slate-ink);
}

.site-header a {
  color: var(--slate-ink);
}

/* Header row: brand, toggle, nav share ONE row on desktop (the mockup's
   brand + links row); the signed-in strip keeps its own row beneath. The
   breakpoint block below drops the nav onto its own row so the collapse
   rules stay untouched. */
.site-header > .page {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  min-height: var(--masthead-height);
  column-gap: var(--s-5);
  /* The banner wraps at narrow widths (320px anonymous: brand,
     Sign in, the button and the theme select take several lines);
     column-gap alone leaves wrapped lines abutting flush. */
  row-gap: calc(var(--space-unit) / 2);
}

/* The club's workspace row sits beneath the account row (2026-09-13:
   the account nav rides every page once signed in), so the bar's top
   line never changes between pages */
.site-header .site-nav {
  flex: 1 1 100%;
  order: 1;
}

.site-header .signed-in-strip {
  flex-basis: auto;
}

/* The wordmark: the mockup's 16px/700 text brand, no graphic; the
   global :focus-visible ring applies unchanged, and hover underlines so
   it reads as a link without colour (SC 1.4.1) */
.brand {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: #ffffff;
  text-decoration: none;
  padding: var(--space-row-pad) 0;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.brand:hover,
.brand:focus-visible {
  text-decoration: underline;
}

/* The mark beside the wordmark (21.1): 26px in the 64px bar, 19px of
   air above and below — the mark's own clear space. */
.brand-mark {
  width: 26px;
  height: 26px;
  margin-right: 9px;
  flex: none;
}

.embed-header .brand-mark {
  width: 14px;
  height: 14px;
  margin-right: 0.375rem;
}

/* Embed shell (9.1): the brand is a compact source line — no border, no
   44px row — so a host page's fixed-height iframe loses almost nothing */
.embed-header .brand {
  min-height: 0;
  padding: 0;
  font-size: 0.875rem;
  color: var(--ink);
}

.site-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-6);
}

/* Nav items on slate (21.1): --slate-nav at rest, white 700 with the
   3px --action underline when current. The 64px height makes the
   underline sit on the bar's bottom edge. */
.site-nav a,
.account-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--masthead-height);
  font-size: 0.9375rem; /* 15px */
  color: var(--slate-nav);
  text-decoration: none;
  border-bottom: 3px solid transparent;
}

.account-nav {
  display: flex;
  align-items: center;
  gap: var(--s-6);
  margin-inline-start: auto;
}

/* A club's menu in the bar (2026-09-13): a <details> whose summary is a
   nav item with a caret, its list a slate dropdown under the bar. No
   script: the disclosure is the platform's. */
.nav-menu {
  position: relative;
}

.nav-menu > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: var(--masthead-height);
  font-size: 0.9375rem;
  color: var(--slate-nav);
  border-bottom: 3px solid transparent;
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}

.nav-menu > summary::-webkit-details-marker {
  display: none;
}

.nav-menu > summary::after {
  content: "";
  width: 0.45em;
  height: 0.45em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-0.15em) rotate(45deg);
}

.nav-menu[open] > summary::after {
  transform: translateY(0.1em) rotate(225deg);
}

.nav-menu > summary[data-current],
.nav-menu[open] > summary {
  color: #ffffff;
  font-weight: 700;
}

.nav-menu > summary[data-current] {
  border-bottom-color: var(--action);
}

.nav-menu > ul {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  min-width: 18rem;
  margin: 0;
  padding: var(--s-2) 0;
  list-style: none;
  background: var(--slate);
  border: 1px solid var(--slate-meta);
  border-top: 3px solid var(--action);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

/* Each club is one row; its tabs fly out to the right on hover, focus,
   or a tap on the name (touch has no hover) */
.nav-menu .nav-group {
  position: relative;
}

.nav-menu .nav-group:first-of-type {
  margin-top: var(--s-2);
  padding-top: var(--s-2);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
}

.nav-menu .nav-group-name {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  width: 100%;
  min-height: 48px;
  padding-inline: var(--s-5);
  background: none;
  border: none;
  border-inline-start: 3px solid transparent;
  font: inherit;
  font-size: 0.9375rem;
  text-align: start;
  color: var(--slate-nav);
  cursor: pointer;
}

.nav-menu .nav-group-name::after {
  content: "";
  flex: none;
  width: 0.45em;
  height: 0.45em;
  border-right: 2px solid currentColor;
  border-top: 2px solid currentColor;
  transform: rotate(45deg);
}

.nav-menu .nav-group[data-current] > .nav-group-name,
.nav-menu .nav-group:hover > .nav-group-name,
.nav-menu .nav-group:focus-within > .nav-group-name,
.nav-menu .nav-group[data-open] > .nav-group-name {
  color: #ffffff;
  background: rgba(255, 255, 255, 0.06);
}

.nav-menu .nav-group[data-current] > .nav-group-name {
  font-weight: 700;
  border-inline-start-color: var(--action);
}

.nav-menu .nav-group > ul {
  display: none;
  position: absolute;
  top: 0;
  left: 100%;
  min-width: 13rem;
  margin: 0;
  padding: var(--s-2) 0;
  list-style: none;
  background: var(--slate);
  border: 1px solid var(--slate-meta);
  border-top: 3px solid var(--action);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}

.nav-menu .nav-group:hover > ul,
.nav-menu .nav-group:focus-within > ul,
.nav-menu .nav-group[data-open] > ul {
  display: block;
}

.nav-menu ul a {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding-inline: var(--s-5);
  font-size: 0.9375rem;
  color: var(--slate-nav);
  text-decoration: none;
  border-inline-start: 3px solid transparent;
}

.nav-menu ul a:hover,
.nav-menu ul a:focus-visible {
  color: #ffffff;
  background: rgba(255, 255, 255, 0.06);
}

.nav-menu ul a[aria-current="page"] {
  color: #ffffff;
  font-weight: 700;
  border-inline-start-color: var(--action);
}

/* Active marker: the amber underline — always paired with aria-current
   + bold, never the underline alone */
.site-nav a[aria-current="page"],
.account-nav a[aria-current="page"] {
  color: #ffffff;
  font-weight: 700;
  border-bottom-color: var(--action);
}

.nav-toggle {
  display: none;
}

/* Scope label (Story 10.5, A12 M-11): with no club switcher in v1 the
   banner names the open workspace beside the wordmark. Muted and never
   a link — it indicates scope, it does not navigate. min-width and
   overflow-wrap mirror .signed-in-strip so a 100-char club name cannot
   force horizontal overflow at 320px effective width (SC 1.4.10). */
.club-scope {
  font-size: 0.9375rem;
  color: var(--slate-meta);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Story 10.8 (G1, D6): the anonymous entry gate in the public banner —
   the mockup's navlink + btn-primary pair mapped onto the design
   system ("Sign in" text link, "Create an account" as .button). The
   auto margin pushes the pair, and the theme control after it, to the
   banner's right edge; the sibling rule below hands the push to this
   link so the theme control keeps its own auto margin on signed-in
   public pages, where no auth link renders. */
.auth-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-inline-start: auto;
  color: var(--slate-ink);
  text-decoration: underline;
}

.auth-link ~ .theme-control {
  margin-inline-start: 0;
}

/* Story 13.3 (D4): the directory link keeps the brand's company on the
   left; the auth pair's auto margin (or the theme control's, when the
   visitor is signed in) still pushes the right-hand group over. The
   same 44px target and underline the sign-in link states. */
.banner-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--slate-nav);
  text-decoration: underline;
}

/* Story 10.7. JS-gated exactly as .nav-toggle and .refresh-now are:
   the control cannot work without a script, so without one it never
   appears and the OS decides — which is what the site did before. */
.theme-control {
  display: none;
  align-items: center;
  gap: var(--space-unit);
  margin-inline-start: auto;
}

.js .theme-control {
  display: inline-flex;
}

.theme-control label {
  font-family: var(--font-data);
  font-size: 0.8125rem; /* 13px, the panel-label register */
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--slate-meta);
}

/* Mirrors .run-lockselect, the codebase's one other <select>. */
.theme-select {
  min-height: 44px;
  padding: 4px var(--space-unit);
  font-family: inherit;
  font-size: 0.9375rem;
  color: var(--slate-ink);
  background: var(--slate);
  border: 1px solid var(--slate-outline);
  border-radius: var(--radius-default);
}

/* Signed-in strip: username + sign-out, on its own row after the nav.
   overflow-wrap inherits so a long username can never force horizontal
   overflow at 320px effective width (SC 1.4.10). */
.signed-in-strip {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-gutter);
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: 0.9375rem;
}

/* The callsign up here is the operator's name (21.1): mono, no link
   underline through it */
.signed-in-strip .callsign {
  font-size: 0.9375rem;
}

.signed-in-strip > a:first-child {
  text-decoration: none;
}

/* Sign-out must POST (Django 5 LogoutView rejects GET), so it is a real
   button presented as a text link */
.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--slate-meta);
  text-decoration: underline;
  cursor: pointer;
}

/* Width-driven collapse only (SC 1.4.10): at 400% zoom (~320px effective)
   this applies. Never JS width sniffing. The column stack applies with or
   without JS (no-JS must reflow at 320px too); only the collapsed-hide is
   JS-gated. Collapsed is the default under .js — the pre-paint inline js
   class means no flash of expanded nav; JS opts the menu open via
   data-expanded. */
@media (max-width: 640px) {
  .page {
    padding-inline: var(--space-margin-mobile);
  }

  .site-nav ul {
    flex-direction: column;
    gap: 0;
  }

  .js .nav-toggle {
    display: inline-block;
  }

  /* Brand and Menu share the first row; the nav list drops beneath */
  .site-header .site-nav {
    flex-basis: 100%;
  }

  .js .site-nav:not([data-expanded]) ul {
    display: none;
  }
}

/* --- Derived-figure blocks (Story 4.10, shared with History 5.1) --- */

/* The ended session's figures render on two surfaces now: the run
   view's summary and the archive detail, which loads base.css alone.
   The rules live here so one block cannot style itself two ways — the
   section labels above them included, or History heads its figures
   with unstyled text. */
.run-seclabel {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  color: var(--color-text-muted);
  padding-block-end: var(--space-unit);
  border-bottom: 1px solid var(--color-border);
}

.run-summary-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-section-gap);
  padding-block-end: var(--space-section-gap);
}

.run-summary-block {
  flex: 1 1 200px;
}

.run-summary-dl {
  margin-block: var(--space-unit) 0;
}

.run-summary-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-unit);
  padding-block: 2px;
}

.run-summary-item dd {
  margin-inline-start: 0;
}

/* The total is the one figure that always renders, so it carries the
   weight that says "this is the number you came for". */
.run-summary-total {
  font-weight: 700;
  border-top: 1px solid var(--color-border);
  padding-block-start: var(--space-unit);
}

.run-summary-coverage {
  font-weight: 600;
  margin-block: var(--space-unit) 0;
}

.run-summary-duration {
  color: var(--color-text-muted);
}

/* --- Record tables (Story 5.1) --- */

/* A session's table is as wide as the net's field config makes it, so
   IT scrolls — the page body never does (SC 1.4.10). */
.table-scroll {
  overflow-x: auto;
  max-width: 100%;
}

/* The container is a TAB STOP (WCAG 2.1.1: a mouse wheel must not be
   the only way to reach a column), so it has to SHOW when it holds
   focus. Inset, because the scroller spans the full content width and
   a ring drawn outside it would sit in the page gutter. */
.table-scroll:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: -3px;
}

/* The workspace's one table treatment (15.2, D1): hairline rows under a
   2px header rule, left-aligned 700 headers. .archive-table is the
   archive's older name for the same family and stays as an alias. */
.data-table,
.archive-table {
  border-collapse: collapse;
  width: 100%;
}

.data-table th,
.data-table td,
.archive-table th,
.archive-table td {
  text-align: start;
  vertical-align: top;
  padding: var(--space-unit);
  border-bottom: 1px solid var(--color-border);
}

.data-table th,
.archive-table th {
  font-weight: 700;
  border-bottom: 2px solid var(--color-border);
}

/* Only the columns a line break makes unreadable refuse to wrap: a
   time, a callsign, a tally. Names, places and comments are prose and
   wrap like prose — holding them on one line is what made the table
   wide enough to need scrolling in the first place. */
.data-table .data-mono,
.data-table .callsign,
.archive-table .data-mono,
.archive-table .callsign {
  white-space: nowrap;
}

/* A row's action group (DESIGN.md buttons, rule 1): every member boxed
   alike, a gutter apart, wrapping as a unit at narrow widths. */
.row-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-unit);
}

/* The one alignment rule for every workspace table (15.3, D3): the
   row's prose meets its 44px action group in the middle. It has to be
   the ROW — vertical-align positions a cell's own content inside the
   row box, and the action cell IS the tallest cell, so aligning it
   alone moved nothing (the review caught that). Workspace cells hold
   one-line prose, so middle is safe everywhere. */
.data-table tbody td,
.archive-table tbody td {
  vertical-align: middle;
}

/* Forms inside an action group are members, not blocks. display:
   contents is safe on a <form>: submission is DOM-driven, its hidden
   inputs and form= associations are untouched, and an unnamed <form>
   has no landmark semantics for the historical a11y-tree removal to
   drop. */
.row-actions form {
  display: contents;
}

.archive-comment {
  min-width: 24ch;
}

.pagination {
  display: flex;
  align-items: center;
  gap: var(--space-gutter);
  margin-block: var(--space-section-gap);
}

/* Paging is spelled out, and the steps are outlined 48px targets */
.pagination a {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding-inline: var(--s-4);
  font-weight: 700;
  text-decoration: none;
  border: 1px solid var(--ink-muted);
  border-radius: var(--radius-default);
  background: var(--panel);
}

/* Breadcrumbs are NAVIGATION: the chain up out of one session to the
   whole record, not a line of help text wearing a form class. */
.breadcrumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-unit);
  margin-block: var(--s-4) var(--s-4);
  font-size: 0.9375rem;
}

.breadcrumb-sep {
  color: var(--color-text-muted);
}

/* Story 8.10: the mode group sits above the flag row in the logger */
.moderow {
  margin-bottom: var(--space-2, 0.5rem);
}

/* Member home cards (Story 9.6): one hairline-bordered block per club.
   Weight and words carry state — "On the air now" is the signal, never
   a colour. */
/* Alternate sections band, computed from RENDERED POSITION rather than
   a class per section: Epic 11 ("My check-ins") and Epic 12
   ("Following") insert sections between these two, and parity re-bands
   them without anyone editing this rule. A hairline alone does not
   separate one list's own rows from the end of the list (finding
   M-13). */
.dashboard > section {
  padding: var(--space-gutter);
}

/* Banding is gone (board decision 4): sections separate with a label
   and a hairline, never positional parity */
.dashboard > section + section {
  border-top: 1px solid var(--hairline);
}

/* DESIGN.md "Dashboard sections" raises the 13px section-label to a
   16px floor here. It RAISES the token, it does not replace it: the
   uppercase and the 0.1em tracking stay, so these headings keep the
   label register the rest of the app uses. Cited by name — line
   numbers into a separate document rot on the next edit. rem, never
   px: the file-wide test rejects an absolute font size. */
.dashboard .section-label {
  font-size: 1rem;
}

/* All FOUR hairlines stay, which is what M-13's fix asked for in terms.
   An earlier pass spent the left border on the state edge —
   `border-left: 4px solid transparent` — and a quiet card then drew on
   three sides and stopped, reading as a failed stylesheet rather than a
   card. The net row survives that trick because it carries only
   border-bottom; a closed box does not.

   The state edge is a bar inside the hairline instead. Padding reserves
   its width on every card, live or not, so the text aligns down one
   column either way. */
/* 12.2: the follower card shares every rule by grouping, never by
   class reuse (twelve pins hang on the club-card literal) and never
   by copying — one rule, two names. */
.club-card,
.follow-card {
  background: var(--panel);
  padding-block: var(--s-5);
  margin-block-end: var(--s-4);
  position: relative;
  border: 1px solid var(--color-border);
  padding: var(--space-row-pad) var(--space-gutter);
  padding-inline-start: calc(var(--space-gutter) + 4px);
  margin-block: var(--space-unit);
}

/* The net row's treatment — var(--color-accent) rather than the light
   amber literal, so tokens.css themes it per mode and it stays legible
   on a banded surface-alt section (finding L-6). Inset by the hairline
   so the card's own border still closes the box around it. */
.club-card[data-live]::before,
.follow-card[data-live]::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 4px;
  background: var(--color-accent);
}

.club-card h3,
.follow-card h3 {
  font-family: var(--font-name);
  font-size: 1.1875rem; /* 19px, the object title */
  font-weight: 600;
  margin-block: 0 calc(var(--space-unit) / 4);
}

/* Muted text, never a coloured badge: a role is not a state. */
.club-role {
  color: var(--color-text-muted);
}

/* --- Public net row (Stories 10.1/10.8) ---
   Hairline-divided full-width rows, never cards (DESIGN.md "Net row
   (public)"). Named grid areas place the columns, which frees the
   badge to lead the row in the DOM: a screen reader reading the <li>
   linearly announces the state before the net name (finding H-7)
   while the badge still sits in the status column on screen.
   10.8 (G5): the mockup's five columns — identity, frequency, where,
   status/when stacked, actions. The mockup's fixed widths (165/200/145
   = 1 : 1.21 : 0.88) are deliberately NOT kept: retuned to 1/1.1/1.1
   so "Thu 2026-08-27" holds one line in the when column at the 90ch
   page width. Fractional columns, not fixed widths, because the
   narrow-viewport rule below still has to stack them: five columns
   leave the badge and the frequency — both unbreakable atoms —
   nothing at 320px and they would push the page sideways. */
.net-list {
  list-style: none;
  margin-block: 0;
  padding-inline: 0;
  /* The board (21.2): rows sit on a panel, hairlines between them */
  background: var(--panel);
  border: 1px solid var(--rule);
  border-top: 0;
}

.net-row {
  display: grid;
  grid-template-columns:
    minmax(0, 1.8fr) minmax(0, 1fr) minmax(0, 1.1fr) minmax(0, 1.1fr) auto;
  grid-template-areas:
    "identity freq where status actions"
    "identity freq where when actions"
    "detail detail detail detail detail";
  /* Row gap 0 (10.8): the reserved detail row is EMPTY on every
     collapsed row, and a row gap above it still renders — a phantom
     sliver of dead space over the row's bottom padding. The cells
     that need vertical separation carry block margins instead. */
  gap: 0 var(--space-gutter);
  align-items: baseline;
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--hairline);
  /* Reserved on EVERY row and coloured only when live, so the two
     lists align down one edge and amber never carries state alone */
  border-left: 4px solid transparent;
}

.net-list > .net-row:last-child {
  border-bottom: 0;
}

.net-row.is-live {
  border-left-color: var(--color-accent);
  /* Story 13.4: the on-air band is the page's attention point — the
     mockup's taller 16px row, with no wash, gradient or card. On the
     same rule as the edge so a pending mover (className copied from
     the incoming row) gains both in place, exactly as it gains the
     badge. */
  padding-block: calc(var(--space-unit) * 2);
}

.net-identity {
  grid-area: identity;
}

.net-freq {
  grid-area: freq;
  font-family: var(--font-data);
  font-size: 1rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.net-where {
  grid-area: where;
}

.net-status {
  grid-area: status;
}

.net-when {
  grid-area: when;
  /* The badge sits directly above in the status area; with the row
     gap at 0 this margin is what keeps them apart (and is overridden
     to the tighter stacked rhythm at the narrow breakpoint) */
  margin-block-start: calc(var(--space-unit) / 2);
}

/* A quiet row's status cell is empty, and leaving `when` in the
   second track parks the date below an invisible neighbour — the
   mockup top-aligns it with the net name. Span both tracks instead;
   the badge-separation margin above only exists for the badge. */
.net-row:not(.is-live) .net-when {
  grid-row: status-start / when-end;
  grid-column: status-start;
  align-self: start;
  margin-block-start: 0;
}

/* Newsreader names the net (21.2); the club beneath in the meta register */
.net-name {
  font-family: var(--font-name);
  font-size: 1.125rem; /* 18px */
  font-weight: 600;
}

.net-club,
.net-where,
.net-day {
  display: block;
  color: var(--color-text-muted);
}

/* --- Story 13.4: the three bands' row variants ---
   All of them live HERE, in the .net-row family before the 640px
   block: equal-specificity narrow overrides win only by source order
   (13.3's lesson), and the soon variant — which outranks .net-row —
   restates its stack inside that block. The five-column map above and
   its pinned areas are untouched for later rows. Nothing animates. */

/* The club name is the row's one link to the club's public page (13.1):
   the mockup's underlined text link in the muted club register. */
.net-club-link {
  color: var(--color-text-muted);
  text-decoration: underline;
}

.net-row.is-live .net-name {
  font-size: 1.1875rem; /* 19px, the board's object title */
}

/* The panel's bleed (.net-detail, below) is sized to --space-row-pad,
   but the live band pads taller, so its opened panel stopped 4px short
   of the hairline on exactly the rows that draw the eye. Sized to the
   band's own padding, and kept with the band's rules: specificity, not
   position, is what wins over .net-detail here. */
.net-row.is-live .net-detail {
  margin-block-end: calc(-1 * var(--space-unit) * 2);
}

/* "since HH:MM zone" in the mono time register, the elapsed line and
   the soon row's countdown muted beneath (the mockup's live-since /
   elapsed and soon-time / rel pairs). */
.net-since {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.9375rem; /* 15px */
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.net-elapsed,
.net-rel {
  display: block;
  color: var(--color-text-muted);
}

/* The soon row: the time leads in the identity column's place at the
   mockup's 19px mono, the countdown beneath, then the net and club.
   Keyed on the LIST, not the row: a pending mover copies className from
   the incoming row, and a later row must keep its section's shape
   until the reconciler actually moves it. The status area keeps the
   first track's top row so a pending-live badge still leads; the
   quiet-row span rule above then seats the time across both rows. */
.net-list-soon .net-row {
  grid-template-columns:
    minmax(0, 1fr) minmax(0, 1.8fr) minmax(0, 1fr) minmax(0, 1.1fr) auto;
  grid-template-areas:
    "status identity freq where actions"
    "when identity freq where actions"
    "detail detail detail detail detail";
}

/* Scoped to the row: the mono weight is the net list's, not a site
   rule for anything that borrows the class name. */
.net-row .net-time {
  display: block;
  font-family: var(--font-mono);
  font-size: 1.1875rem; /* 19px */
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* A text label, never a bare coloured dot: the words are the state and
   the amber edge only reinforces them (findings H-7, M-12). */
.net-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-family: var(--font-data);
  font-size: 0.8125rem; /* 13px, the panel-label register */
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  white-space: nowrap;
}

/* The 9px dot: the one sanctioned accent fill */
.net-badge::before {
  content: "";
  display: inline-block;
  width: 9px;
  height: 9px;
  background: var(--accent);
  border-radius: 9999px;
}

/* --- Public net row detail (Stories 10.2/10.8) ---
   The disclosure and the Net page link stack in a column at EVERY
   width, not only the narrow one — DESIGN.md's "stacked, a gutter
   apart" — which holds the committed one-gutter separation and their
   44px targets with no extra breakpoint (M-10 requires the stack only
   at <=400px, and always-stacked satisfies it everywhere). 10.8 (D3)
   moved the panel OUT of this column to the row's last child: the
   five-column row puts the actions at the right while the panel spans
   full width below, and aria-controls carries the button-panel
   association that DOM adjacency used to. */
.net-actions {
  grid-area: actions;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-gutter);
  min-width: 0;
}

/* Progressive enhancement, the phone nav's rule exactly: the panel's
   facts must be readable with or without JS, so only the
   collapsed-hide is JS-gated. Without JS the panel is simply open and
   the button — which could not work — never appears. Never the
   `hidden` attribute on the panel: that would collapse it for a no-JS
   visitor too. */
.net-disclose {
  min-height: 48px;
  font-weight: 700;
  display: none;
  align-items: center;
  gap: var(--space-unit);
  padding-inline: calc(var(--space-unit) * 1.5);
  border: 1px solid var(--ink-muted);
  border-radius: var(--radius-default);
  background: none;
  color: var(--color-text);
  cursor: pointer;
}

.js .net-disclose {
  display: inline-flex;
}

/* aria-expanded is the ONE state channel (finding M-9): the visible
   label stays "Details" pressed or unpressed, and the ground and the
   chevron only reinforce what the attribute already says. */
.net-disclose[aria-expanded="true"] {
  background: var(--color-surface-alt);
}

/* Instant, never animated. L-9 allows instant or a <=150ms OPACITY
   fade and no position animation; a transform transition is a position
   animation. Instant needs no prefers-reduced-motion guard, and
   wrapping the rotation itself in one would take away the only visual
   state cue the control has. */
.net-disclose[aria-expanded="true"] .chev {
  transform: rotate(90deg);
}

/* overflow-wrap inherits, so no panel value can force horizontal
   overflow at 320px effective width (SC 1.4.10) — `repeater` is a
   100-char free-text field and every node id is 50, none of them with
   a break opportunity the browser would find on its own.
   grid-area (10.8 D3): the panel claims the explicit full-width
   detail row in BOTH width maps — without it, auto-placement drops
   the panel into column 1 at one-fifth width the moment it opens. */
.net-detail {
  grid-area: detail;
  align-self: stretch;
  min-width: 0;
  margin-block-start: calc(var(--space-unit) / 2);
  /* The mockup's panel spans the full list width. The detail row sits
     inside the li's own padding box, so without these negatives the
     surface-alt band floats a gutter short of each row edge and a
     row-pad short of the hairline below. Both width maps pad the row
     with the same vars, so one set of negatives serves both. */
  margin-inline: calc(-1 * var(--space-gutter));
  margin-block-end: calc(-1 * var(--space-row-pad));
  padding: var(--space-row-pad) var(--space-gutter);
  background: var(--color-surface-alt);
  overflow-wrap: anywhere;
}

.js .net-row:not([data-expanded]) .net-detail {
  display: none;
}

/* 10.8 (G6): the mockup's two-column definition table — a 190px label
   column, values right, a hairline under every pair but the last. */
.net-detail dl {
  display: grid;
  grid-template-columns: 11.875rem minmax(0, 1fr);
  margin-block: 0;
}

/* display: contents is what lets the dl become a grid without touching
   the template's [data-net-detail-row] hooks — but it strips the
   div's own box, so the hairlines MUST live on the dt/dd. A border
   declared on the div itself would never render, and no CSS-text
   assertion would notice. */
.net-detail [data-net-detail-row] {
  display: contents;
}

/* 17.1: the same table standing alone on the operator record — no
   row edges to bleed into, so no negative margins */
.net-detail.operator-record {
  margin: 0 0 var(--space-section-gap);
}

.net-detail dt {
  font-size: 0.8125rem; /* 13px, the section-label register */
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  padding: var(--space-row-pad) var(--space-gutter) var(--space-row-pad) 0;
  border-bottom: 1px solid var(--color-border);
}

.net-detail dd {
  margin-inline-start: 0;
  padding-block: var(--space-row-pad);
  border-bottom: 1px solid var(--color-border);
}

/* The last WRAPPER'S children, never a bare dt:last-of-type — each
   wrapper holds exactly one dt, so the bare form matches the only dt
   in EVERY wrapper and clears every hairline on the panel. */
.net-detail [data-net-detail-row]:last-of-type dt,
.net-detail [data-net-detail-row]:last-of-type dd {
  border-bottom: none;
}

/* An <a> inherits none of the 44px floor the button rule gives every
   button, so this secondary action states its own — with the underline
   secondary actions carry. */
.net-page-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: underline;
}

/* live_idle.html has carried this class since Story 4.1 and no
   stylesheet ever defined it, so the link into a running session was
   styling-free. Its own rule, never joined to .net-page-link's selector
   list: tests split base.css on the literal ".net-page-link {" and
   grouping the two would delete that marker. An <a> inherits none of the
   button target floor, which is why the min-height is restated. */
.live-session-open {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  text-decoration: underline;
}

/* --- Public list region and its refresh control (Stories 10.3/10.8) ---
   PR-3 puts the control FIRST inside the list region, so the region
   needs to be a real element: .net-lists is it. 10.8 (G3) reverses
   10.3's composed-affordance layout by Project Lead approval
   (2026-08-27): the bar is the mockup's full-width row — stamp at the
   left edge, Refresh at the right — and the dot separator died with
   it. The bar still states no font-size of its own: the mock's 14px
   breaks the standing "Don't drop body text below 16px". Nothing here
   animates (finding L-9): no spinner, no fade, no pulse. */
.net-lists {
  margin-block-start: var(--space-gutter);
}

.refreshbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-unit);
  margin-block: 0 var(--space-unit);
  color: var(--color-text-muted);
}

/* Story 10.8 (G4, D2): the section header row — heading left, count
   or horizon note right, hairline beneath (the mockup's seclabel).
   The annotations are spans BESIDE the h2, never inside it (an
   in-heading count reads into the accessible name) and never a <p>
   (the refresh's pass 3 keys each section's empty-state swap on its
   first <p>). */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-gutter);
  margin-block-start: var(--space-gutter);
  /* The board (21.2): the departure board's slate strip — mono 13
     uppercase labels on --slate; the hairline beneath is the panel's */
  padding: 11px var(--s-5);
  background: var(--slate);
  color: var(--slate-nav);
  border-bottom: 1px solid var(--color-border);
}

/* The heads carried the sections' vertical rhythm through the h2
   margins; the flex row takes that over. */
.section-head .section-label {
  margin-block: 0;
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--slate-nav);
}

/* The mockup's mono count register: 15px/600, tabular. */
.section-count {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--slate-meta);
  white-space: nowrap;
}

.section-horizon {
  font-size: 0.8125rem; /* 13px, the section-label register */
  color: var(--slate-meta);
}

/* Progressive enhancement, the disclosure's rule exactly: a control
   that cannot work must not be offered. With no JS the stamp alone
   renders — and it is still true, because the server composed it from
   the snapshot it just served. */
.refresh-now {
  display: none;
  align-items: center;
  min-height: 48px;
  padding-inline: var(--s-4);
  font-weight: 700;
  border: 1px solid var(--ink-muted);
  border-radius: var(--radius-default);
  background: var(--panel);
  color: var(--color-text);
  cursor: pointer;
}

.js .refresh-now {
  display: inline-flex;
}

/* Story 13.3 (D2): the directory's Refresh is a plain link to the page
   wearing the button's classes for the same bordered target. A link
   needs no script, so it must NOT inherit the button's gate — hiding
   a control that works would be the opposite of the gate's reason.
   Same specificity, later in the file, so it wins with or without .js.
   An anchor inherits the UA underline the button never had. */
.refresh-link {
  display: inline-flex;
  text-decoration: none;
}

/* Below 640px the three columns leave the badge and the frequency less
   room than their unbreakable text needs, so the row becomes one
   column. The 320px floor holds with no JS (see the nav rule above). */
/* The .club-row family sits BEFORE the 640px block on purpose: the
   narrow override has equal specificity and wins only by source
   order — the .net-row family's rule, learned again at 13.3's review
   when the block below silently lost to a rule appended at EOF. */
/* --- Public club directory (Story 13.3) ---
   The net row's family shaped around a club: identity, grid square,
   published-net count, status — the mockup's four columns, retuned to
   fractions so the narrow rule below can still stack them. The edge is
   reserved on every row and coloured only when live, the net row's
   rule exactly, so the columns align and amber never carries state
   alone (the badge and the "On the air:" line do). Live rows put the
   badge in the status column; quiet rows put the next session there.
   Only one of the two cells exists per row, so both may claim the one
   area without overlapping. The narrow rules sit in the public 640px
   block above, where the tests look for them. Nothing here animates. */
.club-row {
  display: grid;
  grid-template-columns:
    minmax(0, 2fr) minmax(0, 0.9fr) minmax(0, 0.7fr) minmax(0, 1.4fr);
  grid-template-areas: "identity grid nets status";
  gap: 0 var(--space-gutter);
  align-items: baseline;
  padding: var(--space-row-pad) var(--space-gutter);
  border-bottom: 1px solid var(--color-border);
  border-left: 4px solid transparent;
}

.club-row.is-live {
  border-left-color: var(--color-accent);
}

.club-identity {
  /* A 100-char club name with no break opportunity must wrap, not
     scroll the page sideways (SC 1.4.10) */
  min-width: 0;
  overflow-wrap: anywhere;
  grid-area: identity;
}

.club-grid {
  grid-area: grid;
}

.club-nets {
  grid-area: nets;
  color: var(--color-text-muted);
}

.club-status,
.club-when {
  grid-area: status;
}

.club-name {
  font-weight: 700;
  color: var(--color-text);
  text-decoration: underline;
}

/* The mockup's muted second line under a live club's name */
.club-onair {
  display: block;
  color: var(--color-text-muted);
}


@media (max-width: 640px) {
  .net-row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "status"
      "identity"
      "freq"
      "where"
      "when"
      "actions"
      "detail";
    gap: 0;
  }

  /* The stacked cells' separation moved off the row gap and onto the
     cells (see the wide map's phantom-row rationale) */
  .net-identity,
  .net-freq,
  .net-where,
  .net-when {
    margin-block-start: calc(var(--space-unit) / 4);
  }

  /* The actions carry their own gutter in every state, so the row's
     tighter stacked rhythm must not close the gap above them */
  .net-actions {
    margin-block-start: var(--space-unit);
  }

  /* Story 13.4: the soon variant outranks .net-row, so without this
     restatement it would keep its five columns at 320px and push the
     page sideways. One column, the time right after the state. */
  .net-list-soon .net-row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "status"
      "when"
      "identity"
      "freq"
      "where"
      "actions"
      "detail";
  }

  /* Found on 13.4's 320px walk: the wide map's quiet-row span
     (status-start / when-end) covers rows 1-5 of the one-column map
     above, so the time was painted over the net name. Same
     specificity, later in the file: the cell takes its own area and
     the stacked rhythm the lower-specificity rule above could not
     give it. */
  .net-row:not(.is-live) .net-when {
    grid-area: when;
    margin-block-start: calc(var(--space-unit) / 4);
  }

  /* "Updated 19:12 UTC" and a bordered 44px button are two more
     unbreakable atoms on one row. At 320px they take two lines rather
     than push the page sideways (SC 1.4.10). */
  .refreshbar {
    flex-wrap: wrap;
  }

  /* Story 13.3: the directory row's narrow rules live INSIDE this
     block, not in one of their own beside the club-row family — three
     suites split the sheet on this block's query string (even quoting
     it here would move their split) and take the LAST piece as "the
     narrow rules", and the 400px block below must stay the last query.
     The four columns leave the badge and a time no room at 320px, so
     the row becomes one column: state first, as on the net row, and a
     quiet row's next session last, under the count. */
  .club-row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "status"
      "identity"
      "grid"
      "nets"
      "when";
    gap: 0;
  }

  .club-when {
    grid-area: when;
  }

  .club-identity,
  .club-grid,
  .club-nets,
  .club-when {
    margin-block-start: calc(var(--space-unit) / 4);
  }
}

/* Below 400px the panel's 190px label column would leave values less
   room than the labels get: the pairs stack, dt over dd, and only the
   value keeps the hairline so each pair still reads as one row. */
@media (max-width: 400px) {
  .net-detail dl {
    grid-template-columns: minmax(0, 1fr);
  }

  .net-detail dt {
    padding-block-end: 0;
    border-bottom: none;
  }
}

/* --- Account notifications (Story 12.3) ---
   The New label carries the unread state (state never color alone);
   the surface tint only reinforces it. Hairline-divided list rows,
   the archive/net-row treatment, never cards. */
.notification-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.notification-list li {
  padding: var(--space-row-pad) var(--space-gutter);
  border-bottom: 1px solid var(--color-border);
}

.notification-list li[data-unread] {
  background: var(--color-surface-alt);
}

.note-new {
  font-weight: 700;
  text-transform: uppercase;
  font-size: 0.875rem;
  letter-spacing: 0.04em;
}

.note-title {
  font-size: 1rem;
  font-weight: 700;
  margin-block: 0 calc(var(--space-unit) / 4);
}

.note-when,
.channel-unavailable {
  color: var(--color-text-muted);
}

.channel-prefs {
  list-style: none;
  padding: 0;
}

.channel-prefs li {
  margin-block: calc(var(--space-unit) / 2);
}

/* --- Public club page (Story 13.1) ---
   The mockup's club header: h1, a muted facts line, the description
   (the page's own 90ch cap already holds it) and the follow area under
   one hairline. No card, no hero. The mockup's 15px facts and 14px
   footnote are NOT kept: the standing 16px floor wins, so both inherit
   the body size and carry the muted colour alone. */
.club-head {
  padding-block-end: var(--space-gutter);
  border-bottom: 1px solid var(--color-border);
}

/* The club's logo (2026-09-13): 64px beside the name on its page,
   28px inline on the directory row and the net page's club line.
   object-fit keeps any shape inside the box; the name carries the
   meaning, so the image is decorative (alt=""). */
.club-logo {
  width: 64px;
  height: 64px;
  object-fit: contain;
  vertical-align: -0.35em;
  margin-inline-end: var(--s-3);
}

.club-logo-small {
  width: 28px;
  height: 28px;
  margin-inline-end: var(--s-2);
}

.club-head .club-logo {
  float: inline-start;
  margin-block-start: 0.35rem;
}

.club-head::after {
  content: "";
  display: block;
  clear: both;
}

/* Help page figures (2026-09-13): the screenshot at the column's width
   under a hairline, the caption in the meta colour */
.help-figure {
  margin: var(--s-4) 0 var(--s-6);
}

.help-shot {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--hairline);
  background: #ffffff;
}

.help-figure figcaption {
  margin-top: var(--s-2);
  font-size: 0.875rem; /* 14px — the floor */
  color: var(--ink-mid);
}

/* The stats page (2026-09-13): the pair panels in a responsive grid */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s-4);
}

.stat-panel {
  padding: var(--s-4);
  background: var(--panel);
  border: 1px solid var(--hairline);
}

.stat-panel h3 {
  margin: 0 0 var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--hairline);
}

.stat-list {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: var(--s-4);
  row-gap: var(--s-2);
  margin: 0;
}

.stat-list dt {
  color: var(--ink-mid);
}

.stat-list dd {
  margin: 0;
  font-weight: 700;
  text-align: end;
}

.logo-preview {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.club-facts,
.follow-offer,
.section-note {
  color: var(--color-text-muted);
}

/* Epic 18: the pages row under the club head — plain links, a gap
   between them, the current page marked by weight and no underline so
   it reads as "you are here", not as a control. */
.club-pages {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s-6);
  margin-block: var(--space-unit) 0;
  border-bottom: 1px solid var(--rule);
}

/* 19.1: the forum's posts — a plain list, each post its author line
   and body, a hairline between; no cards, no avatars. */
.post-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* The board (21.7): each post a panel; the opening post takes the
   accent rule because it is the subject */
.post {
  margin-block: 0 var(--s-4);
  padding: var(--s-4) var(--s-5);
  background: var(--panel);
  border: 1px solid var(--rule);
}

.post-list > .post:first-child {
  border-top: 5px solid var(--accent);
}

.post-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin: 0 0 var(--s-3);
  padding-block-end: var(--s-3);
  font-size: 0.9375rem;
  border-bottom: 1px solid var(--rule-inner);
}

.post-body > p:last-child {
  margin-block-end: 0;
}

.panel.accented {
  border-top: 5px solid var(--accent);
  margin-block: var(--s-4) var(--s-6);
}

.panel.accented .panel-label {
  border-bottom: 0;
  padding-block-end: 0;
}

.data-table .board-name {
  font-family: var(--font-name);
  font-size: 1.125rem;
  font-weight: 600;
  text-decoration: none;
}

.post-meta,
.post-removed,
.post-former {
  color: var(--color-text-muted);
}

/* 19.2: a thread's state (Pinned, Locked) — a quiet bordered label in
   the muted register, never the on-air tint. */
.thread-state {
  display: inline-block;
  padding: 2px var(--s-2);
  border: 1px solid var(--ink-muted);
  border-radius: var(--radius-badge);
  color: var(--ink);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.board-admin {
  margin-block-start: var(--space-unit);
}

/* 18.3/18.4: a day's rows and the contact list, in the shell's own
   registers — no new colours, no card. */
.schedule-day {
  margin-block-start: var(--space-unit);
}

.schedule-day .net-row {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--space-unit) / 2) var(--space-unit);
  align-items: baseline;
}

.contact-list dt {
  color: var(--color-text-muted);
  margin-block-start: var(--space-unit);
}

.contact-list dd {
  margin-inline-start: 0;
}

.club-pages a {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding-inline: var(--s-1);
  text-decoration: none;
  border-bottom: 3px solid transparent;
}

.club-pages a[aria-current="page"] {
  font-weight: 600;
  text-decoration: none;
  color: var(--color-text);
  border-bottom: 3px solid var(--accent);
}

/* The identity cell's schedule phrase and the quiet row's "No session
   scheduled": the net-club / net-day register on the club page's own
   hooks — the front door's rows never carry either. */
.net-schedule,
.net-no-session {
  display: block;
  color: var(--color-text-muted);
}

/* --- The three doors (21.2, turn 14a): visitor, member, club --- */
.doors {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-4);
  margin-block-start: var(--s-7);
}

.door {
  padding: var(--s-5);
  background: var(--panel);
  border: 1px solid var(--rule);
}

.door .section-label {
  color: var(--ink-muted);
  margin-block: 0 var(--s-3);
}

.door p {
  margin-block: 0 var(--s-4);
  color: var(--ink-mid);
}

.door .button-neutral {
  min-height: 48px;
  padding-inline: var(--s-4);
  font-weight: 700;
  text-decoration: none;
}

.list-note {
  margin-block: var(--s-3) 0;
  font-size: 0.875rem;
  color: var(--ink-muted);
}

/* 700px, not the page's 640px: three suites read the LAST 640px block
   as "the narrow row rules", so the doors stack at their own width */
@media (max-width: 700px) {
  .doors {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* --- The board's primitives (21.2): panel, labels, pair list, board --- */
[data-public-club],
.eyebrow {
  margin-block: 0 var(--s-2);
}

.title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
}

.lead {
  max-width: 64ch;
  color: var(--ink-mid);
}

.panel {
  padding: var(--s-5) var(--s-5) var(--s-4);
  background: var(--panel);
  border: 1px solid var(--rule);
}

/* A panel's label: the mono 13 register, never the sans page label */
.panel-label {
  margin-block: 0 var(--s-3);
  padding-block-end: var(--s-3);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
  border-bottom: 1px solid var(--rule-inner);
}

.panel-note {
  margin-block: var(--s-3) 0;
  font-size: 0.9375rem;
  color: var(--ink-mid);
}

.panel-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-5);
  margin-block: var(--s-6) var(--s-7);
}

.plain-list {
  margin: 0;
  padding-inline-start: 1.2em;
}

.plain-list li + li {
  margin-block-start: var(--s-2);
}

/* Label left, value right, a hairline between pairs; only the pairs the
   club filled in render (the template's business) */
.pair-list {
  margin: 0;
}

.pair-list [data-public-detail] {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  padding-block: var(--s-3);
  border-bottom: 1px solid var(--hairline);
}

.pair-list [data-public-detail]:last-child {
  border-bottom: 0;
}

.pair-list dt {
  color: var(--ink-muted);
}

.pair-list dd {
  margin: 0;
  font-weight: 600;
  text-align: end;
}

.section-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  margin-block: var(--s-6) var(--s-3);
}

.section-title {
  margin: 0;
  font-family: var(--font-name);
  font-size: 1.5625rem; /* 25px */
  line-height: 1.15;
  font-weight: 600;
}

/* The departure board: a slate strip of mono labels, rows on the panel */
.board {
  background: var(--panel);
  border: 1px solid var(--rule);
}

.board-head {
  display: grid;
  gap: var(--s-4);
  padding: 11px var(--s-5);
  background: var(--slate);
  color: var(--slate-nav);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.board-rows {
  list-style: none;
  margin: 0;
  padding: 0;
}

.board-row {
  display: grid;
  gap: var(--s-4);
  align-items: center;
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--hairline);
}

.board-row:last-child {
  border-bottom: 0;
}

.board-cols-sessions {
  grid-template-columns: 12rem minmax(0, 1fr) auto;
}

.board-status {
  text-align: end;
  color: var(--ink-muted);
}

/* A cancelled session: struck through AND said in a word (rule 5) */
.board-row.is-cancelled .data-mono {
  text-decoration: line-through;
  color: var(--ink-muted);
}

.state-word {
  display: inline-block;
  padding: 2px var(--s-2);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  border: 1px solid var(--ink-muted);
  border-radius: var(--radius-badge);
}

@media (max-width: 700px) {
  .panel-pair {
    grid-template-columns: minmax(0, 1fr);
  }

  .board-cols-sessions {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* --- The board (21.3): dashboard blocks, tables, the profile --- */
.card-action {
  margin-block: var(--s-3) 0;
}

.card-action .button-neutral,
.button-neutral {
  min-height: 48px;
  padding-inline: var(--s-4);
  font-weight: 700;
  border-radius: var(--radius-default);
  text-decoration: none;
}

.dashboard > section {
  padding-inline: 0;
}

/* Every table wears the departure board: a slate strip of mono labels
   over hairlined rows on the panel */
.table-scroll {
  background: var(--panel);
  border: 1px solid var(--rule);
}

.data-table thead th,
.archive-table thead th {
  background: var(--slate);
  color: var(--slate-nav);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 11px var(--s-4);
  border-bottom: 0;
}

.data-table tbody td,
.archive-table tbody td {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--hairline);
}

[data-near-you] ul,
[data-passkey-list] {
  list-style: none;
  margin: 0;
  padding: 0;
}

[data-near-club],
[data-passkey-list] li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-3);
  min-height: 48px;
  padding-block: var(--s-2);
  border-bottom: 1px solid var(--hairline);
}

/* The profile: one panel per concern, the mono label on each */
.profile > section,
.panels > section {
  margin-block: var(--s-5) 0;
  padding: var(--s-5);
  background: var(--panel);
  border: 1px solid var(--rule);
}

.profile > section > h2,
.panels > section > h2 {
  margin-block: 0 var(--s-3);
  padding-block-end: var(--s-3);
  font-family: var(--font-data);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
  border-bottom: 1px solid var(--rule-inner);
}

.profile > section[data-danger],
.panels > section[data-danger] {
  border-top: 5px solid var(--danger);
}

.profile > section[data-danger] > h2,
.panels > section[data-danger] > h2 {
  color: var(--danger);
}

.profile .identity {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--s-4);
  margin: 0;
}

.profile .identity dt {
  font-size: 0.875rem;
  color: var(--ink-muted);
}

.profile .identity dd {
  margin: 0;
  font-weight: 600;
}

.profile .identity dt + dd {
  margin-block-end: var(--s-2);
}

.profile .form-field,
.panels .form-field {
  margin-block: var(--s-4);
}

.channel-prefs {
  list-style: none;
  margin: 0;
  padding: 0;
}

.channel-prefs li {
  display: flex;
  align-items: center;
  min-height: 48px;
  border-bottom: 1px solid var(--hairline);
}

.channel-prefs label {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  min-height: 48px;
}

.channel-unavailable {
  color: var(--ink-muted);
}

/* A page index (21.3): sticky under the masthead, one link per panel */
.page-index {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s-4);
  margin-block: var(--s-4) 0;
  padding-block: var(--s-1);
  background: var(--page);
  border-bottom: 1px solid var(--rule);
}

.page-index a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 0.9375rem;
  color: var(--ink-mid);
  text-decoration: none;
  border-bottom: 3px solid transparent;
}

.page-index a:hover,
.page-index a:focus-visible {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/* As tabs (script on): the chosen panel's entry is the current one */
.page-index[data-tabs] a[aria-current="page"] {
  color: var(--ink);
  font-weight: 700;
  border-bottom-color: var(--action);
}

.profile > section > h2,
.panels > section > h2 {
  scroll-margin-top: 4rem;
}

/* --- The board (21.4): the end-of-net figures as stat panels --- */
.run-summary-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--s-4);
  margin-block: var(--s-5);
}

.run-summary-block {
  padding: var(--s-4) var(--s-5);
  background: var(--panel);
  border: 1px solid var(--rule);
}

.run-summary-item {
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
  padding-block: var(--s-3);
  border-bottom: 1px solid var(--hairline);
}

.run-summary-item dd {
  margin: 0;
  font-weight: 600;
}

.run-summary-total {
  font-weight: 700;
  border-bottom: 0;
}

/* --- The board (21.5): object blocks, the admin panel --- */
.object-grid {
  list-style: none;
  margin: var(--s-5) 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--s-4);
}

.object {
  padding: var(--s-5);
  background: var(--panel);
  border: 1px solid var(--rule);
}

.object-title {
  margin: 0 0 var(--s-2);
  font-family: var(--font-name);
  font-size: 1.1875rem;
  font-weight: 600;
  line-height: 1.2;
}

.object-figure {
  margin: 0;
  font-size: 1rem;
}

.object-figure .data-mono {
  font-size: 1.75rem; /* 28px, the headline figure */
  font-weight: 600;
}

.object-meta {
  margin: var(--s-1) 0 var(--s-4);
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

.admin-panel {
  margin-block: var(--s-6);
}

.admin-panel .row-actions {
  margin-block: var(--s-3);
}

/* A search row: label, field and one outlined action on a line */
.search-row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--s-3);
  margin-block: var(--s-4);
}

.search-row label {
  flex-basis: 100%;
  font-weight: 700;
}

.search-row input[type="search"] {
  flex: 1 1 16rem;
  min-height: 52px;
  padding: var(--space-row-pad);
  font: inherit;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--ink-muted);
  border-radius: var(--radius-default);
}

/* --- The board (21.6, turn 10b): the nets list as object blocks --- */
.object-list {
  list-style: none;
  margin: var(--s-4) 0 var(--s-7);
  padding: 0;
  display: grid;
  gap: var(--s-4);
}

.object-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2) var(--s-4);
}

.object-state {
  font-size: 0.9375rem;
  color: var(--ink-muted);
}

.object-sentence {
  margin: var(--s-1) 0 0;
}

.object-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  margin: var(--s-3) 0 0;
  padding: 0;
  list-style: none;
  color: var(--ink-mid);
}

.object .row-actions {
  margin-block-start: var(--s-4);
  padding-block-start: var(--s-4);
  border-top: 1px solid var(--rule-inner);
}

.object .row-actions .button-danger {
  margin-inline-start: auto;
}

/* --- 21.8: the thread's controls share one row --- */
.thread-subscribe {
  display: inline-block;
  vertical-align: top;
  margin-block: var(--s-2) var(--s-4);
  margin-inline-end: var(--s-3);
}

.thread-subscribe + .row-actions {
  display: inline-flex;
  vertical-align: top;
  margin-block: var(--s-2) var(--s-4);
}
