/* ===========================================================================
   Base styles. Mobile first throughout — every media query widens, never
   narrows. If you find yourself writing max-width, something has gone wrong.
   ======================================================================== */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;   /* stop iOS inflating text in landscape */
  scroll-behavior: smooth;
  /* Offset for the sticky header when jumping to an anchor */
  scroll-padding-top: 5rem;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  min-height: 100dvh;              /* dvh, not vh — mobile browser chrome */
  overflow-x: hidden;              /* belt and braces against overflow */
  /* Column layout so .site-footer's margin-block-start:auto pins it to the bottom
     on short pages without position:fixed stealing viewport height. */
  display: flex;
  flex-direction: column;
}

img, picture, svg, video {
  display: block;
  max-width: 100%;
  height: auto;
}

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

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;              /* stops orphaned words in headings */
}

/* Each step down is a clear one. h2 sat at --text-2xl, which is 26px against the
   h1's 32px at 390px — close enough that a section heading read as a second title
   rather than as something beneath it. The whole scale shifts down one stop to keep
   the gaps even: 32 / 22 / 19 / 17 on a phone. */
h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-xl);  margin-block-start: var(--space-lg); }
h3 { font-size: var(--text-lg);  margin-block-start: var(--space-md); }
h4 { font-size: var(--text-base); margin-block-start: var(--space-md); font-family: var(--font-ui); }

p, ul, ol, blockquote, table {
  margin-block-end: var(--space-sm);
}

p { text-wrap: pretty; }

a {
  color: var(--accent-strong);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
a:hover { color: var(--accent); }

blockquote {
  border-inline-start: 3px solid var(--accent);
  padding-inline-start: var(--space-sm);
  color: var(--fg-muted);
  font-style: italic;
}

code, pre { font-family: var(--font-mono); font-size: 0.9em; }

hr {
  border: 0;
  border-block-start: 1px solid var(--border);
  margin-block: var(--space-lg);
}

/* --- Icons and ornament --------------------------------------------------
   Icons are inline SVG rendered by Components/Shared/Icon.razor, drawn for this
   project rather than downloaded — see docs/GRAPHICS.md for why. They take
   currentColor, so one drawing is correct in both themes.

   Watch out — an inline SVG is replaced content with no baseline of its own, so
   it hangs below the text beside it unless the container centres it or the icon
   is nudged. Prefer a flex container with align-items: center over per-icon
   vertical-align, which has to be re-tuned at every size. */

.icon {
  flex: 0 0 auto;
  /* For the cases that are genuinely inline — inside a badge or a sentence —
     rather than in a flex row. */
  vertical-align: -0.14em;
}

.site-header__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
}
.site-header__brand .icon { color: var(--accent); }

.index__section-name .icon--section { color: var(--accent); }

/* The gated badge is the one place an icon is load-bearing: it is what says the
   row is staff-only, and it is the production layer's colour, not danger's. */
.gated {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
}

/* Ornament, for surfaces that should read as lit rather than filled. Both are
   generated in tokens.css; these are the two ways to spend them. A game screen's
   decision panel is .veil and nothing else — see docs/DESIGN.md. */
.veil  { background-image: var(--veil); }
.grain { background-image: var(--grain); }
.veil.grain { background-image: var(--veil), var(--grain); }

/* The lozenge as a section break. Masked rather than a background image, so it
   takes the accent in both themes instead of baking one colour into a data URI. */
.prose hr {
  border: 0;
  height: 14px;
  margin-block: var(--space-xl);
  background: var(--accent);
  -webkit-mask: var(--lozenge-mask) center / 14px 14px no-repeat;
          mask: var(--lozenge-mask) center / 14px 14px no-repeat;
  opacity: 0.8;
}

/* Eyebrow labels, metadata, small caps chrome */
.label {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* --- Layout -------------------------------------------------------------- */

.page {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* The reading column. Body text must never exceed this, at any width. */
.prose {
  max-width: var(--measure);
  margin-inline: auto;
}

.prose > * + * { margin-block-start: var(--space-sm); }
.prose h2 + *, .prose h3 + * { margin-block-start: var(--space-xs); }

.stack     { display: flex; flex-direction: column; gap: var(--space-sm); }
.stack-lg  { display: flex; flex-direction: column; gap: var(--space-lg); }

/* Cards reflow by available space, not viewport width, so they behave the same
   in the main column and inside a narrower sidebar. */
.grid {
  display: grid;
  gap: var(--space-sm);
  grid-template-columns: 1fr;
}
@media (min-width: 40rem) {
  .grid { grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }
}

.card {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-sm);
  box-shadow: var(--shadow-sm);
}

/* --- Header and navigation ----------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-block-end: 1px solid var(--border);
  padding-block: var(--space-2xs);
}

/* Mobile first: the brand on the left, the menu control hard right, and the links in an
   overlay beneath. Widened at 40rem into a single row.

   It was one row at every width once, with the brand pushing the nav right - and that was
   measured with the TWO links a signed-out reader sees. A signed-in admin has five, which
   left the nav a narrow column squeezed against the right edge. The header is 44px in both
   states now and at every width, because the open menu is out of flow. */
.site-header__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  min-height: var(--tap-min);
}

.site-header__brand {
  font-family: var(--font-display);
  /* Fixed at mobile rather than fluid: --text-lg wrapped "The Fourth Epoch" onto two
     lines at 390px once the nav was beside it, which doubled the header's height. */
  font-size: var(--text-base);
  font-weight: 600;
  text-decoration: none;
  color: var(--fg);
  white-space: nowrap;
}

/* A menu at mobile, a row at 30rem and up.

   Measured rather than assumed: the five links a signed-in admin sees need 373px at
   390px wide against 358 available. Beside the brand they wrapped into a cramped column
   against the right edge; on their own full-width row they fitted, but cost a second row
   of a STICKY header - 101px, a ninth of the screen, permanently.

   <details> is decision 108's answer: native, nothing to re-run on enhancedload, works
   with JavaScript off, and a menu built this way cannot fail to open. */
/* The control sits at the far right of the bar, on the brand's row. It used to take a
   row of its own (flex-basis: 100%), which made a 44px header into a 70px one before
   anybody had opened anything.

   WATCH OUT - the auto margin goes on the <nav>, not on .site-nav. The flex items of
   .site-header__inner are the brand and the <nav> wrapper; .site-nav is the <details>
   INSIDE that wrapper, so a margin there does nothing to where the control sits and
   reads as though it should. It was on .site-nav for one build and the button stayed
   in the middle of the bar. */
.site-header__inner > nav {
  margin-inline-start: auto;
}

.site-nav__toggle {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--fg-muted);
  cursor: pointer;
  /* The default triangle sits left of the word and reads as a list marker rather than as
     a control. The caret after the label is the affordance, and it turns over on open. */
  list-style: none;
}
.site-nav__toggle::-webkit-details-marker { display: none; }
.site-nav__toggle::after { content: " \25BE"; }
.site-nav[open] .site-nav__toggle::after { content: " \25B4"; }
.site-nav__toggle:hover { color: var(--accent); }

/* The links are a SIBLING of the <details>, not its children, and that is the whole fix
   for a desktop nav that was broken at every width above 30rem.

   WATCH OUT - do not move them back inside. A closed <details> contributes NO intrinsic
   size, and CSS that forces its children visible does not restore it: the element measured
   0px wide, collapsed the <nav> flex item to 0, and the links - which wrap - then took one
   line each and spilled off the right edge as an unreachable column. Measured closed vs
   open at 1280px: 0px against 56px. Forcing `display` on the panel shows the links and
   changes nothing about the sizing, and `display: contents` on the <details> does not help
   either, because the hiding follows the element rather than its box.

   As siblings there is nothing to override. The disclosure still drives the panel, through
   the adjacent combinator below, and `<details>` stays the mechanism decision 108 chose.

   It went unseen because `body` has overflow-x:hidden, so the spill CLIPPED rather than
   scrolled; because signed out there are only two links, which reads as a slightly odd
   header rather than a broken one; and because no test renders this component at a desktop
   width. The browser audit cannot see it for the overflow-x reason. */
.site-nav__items {
  display: none;
}

/* An overlay under the header rather than a panel that grows it. position:absolute takes
   the menu out of flow, so opening it neither moves the page nor makes a sticky header
   taller — it covers what is beneath, which is what a menu should do.

   Anchored to .site-header, which is the nearest positioned ancestor because it is
   sticky. top:100% puts it on the header's lower edge at any header height, so nothing
   here has to know a number that the brand's font size could change.

   Stacked and full width, because a menu that reproduces the cramped row inside a panel
   has moved the problem rather than solved it. */
.site-nav[open] + .site-nav__items {
  display: flex;
  flex-direction: column;
  position: absolute;
  inset-inline: 0;
  top: 100%;
  z-index: 2;
  background: var(--bg-raised);
  border-block-end: 1px solid var(--border);
  box-shadow: var(--shadow);
  padding: var(--space-2xs) var(--gutter) var(--space-xs);
}

/* NO SCRIM, deliberately, and it was tried.

   A dimmed backdrop that closes on an outside tap has to be the <summary>'s own
   pseudo-element, because with a native <details> nothing else can toggle it without
   script (108). That does not work: making the summary paint above the scrim needs a
   z-index on the summary, which gives it a stacking context — and the scrim, being its
   child, is then trapped above the panel rather than below it. The alternatives are a
   scrim that dims the control you press to dismiss it, or one that looks dismissable and
   is not, which is worse than none.

   So the panel is opaque, bordered and shadowed, and Menu toggles it. Revisit if the
   menu ever grows past a handful of rows. */

/* 40rem, not 30rem, and the number is measured. The row needs the brand plus the links:
   a signed-in admin's five come to 341px against a 219px brand, so it does not fit until
   about 620. At 30rem the header wrapped to two rows between 480 and 620 - the very thing
   the menu exists to avoid - and then unwrapped again, so widening the window made the
   header taller and then shorter. One breakpoint, at the width the row actually fits. */
@media (min-width: 40rem) {
  /* Wide enough for the row, so the disclosure goes entirely - summary and all - and the
     links lay out as the row they always were. Nothing here has to fight the user agent,
     because the links were never inside the <details>. */
  .site-nav { display: none; }

  .site-nav__items {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs);
    padding-block-end: 0;
  }

  /* Room again once the nav shares the brand's row rather than owning one, and the
     negative block margin that keeps a 44px target from growing the header - safe here,
     because this is the width at which the links are a row. */
  .site-nav__link {
    padding-inline: var(--space-2xs);
    margin-block: calc(var(--tap-min) / -2 + 0.7em);
  }

  .site-header__brand {
    font-size: var(--text-lg);
    /* Pushes the nav to the far edge, which only works once they share a row. */
    margin-inline-end: auto;
  }
}

.site-nav__link {
  display: inline-flex;
  align-items: center;
  /* The padding is what gives a text link a real hit area.

     WATCH OUT - the negative block margin that cancels the 44px in the header's row is
     in the 30rem media query, NOT here, and that is deliberate. In the open mobile menu
     these are stacked, and a negative block margin there overlaps every target with its
     neighbour: measured at 390px, five links sat 20px apart with 44px hit areas, so a
     thumb between two of them hit whichever won the z-order. It is the right trick in a
     row and a bug in a column. */
  min-height: var(--tap-min);
  padding-inline: var(--space-3xs);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--fg-muted);
  text-decoration: none;
}
.site-nav__link:hover { color: var(--accent); }

.site-nav__link--button {
  background: none;
  border: 0;
  cursor: pointer;
  padding-inline: var(--space-2xs);
}

/* Log out is a button inside a form, because logging out has to be a POST. The form
   was an ordinary block-level flex item, so the button inside it did not sit on the
   row's centre line and "Log out" hung below the links beside it. display:contents
   drops the form from layout — submission is unaffected, since that is a DOM
   relationship rather than a visual one — and makes the button a direct flex child
   treated identically to the anchors.

   WATCH OUT - the selector is .site-nav__items, not .site-nav. The links moved OUT of
   the <details> to fix the desktop nav and this rule still said .site-nav for one build:
   it stopped matching, the form became an ordinary flex item again, and Log out sat
   three pixels below every other link in the row. Exactly the bug the paragraph above
   describes, reintroduced by a selector nobody re-read. */
.site-nav__items form { display: contents; }

/* --- Page frame ---------------------------------------------------------- */

.site-main { padding-block: var(--space-lg) var(--space-2xl); }

.site-footer {
  border-block-start: 1px solid var(--border);
  padding-block: var(--space-md);
  margin-block-start: auto;
}

/* --- Hero ---------------------------------------------------------------- */

.hero { padding-block: var(--space-lg) var(--space-xl); }
.hero h1 { margin-block-start: var(--space-2xs); }

.lede {
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  color: var(--fg-muted);
  text-wrap: pretty;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-block-start: var(--space-md);
}

/* --- Page index ---------------------------------------------------------- */

.page-list {
  max-width: var(--measure);
  margin-inline: auto;
  list-style: none;
  padding: 0;
}

.page-list__item {
  padding-block: var(--space-sm);
  border-block-end: 1px solid var(--border);
}

.page-list__link {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 600;
  text-decoration: none;
  color: var(--fg);
}
.page-list__link:hover { color: var(--accent-strong); text-decoration: underline; }

/* A chronicle entry with no week of its own to open - a collapsed absence, a skipped week, a week
   the record cannot account for - is a heading rather than a link. It takes the same type so the
   list reads as one list, and NOT the hover, because a thing that lights up under the cursor is a
   thing a reader will click. Same argument as .choice[data-barred] carrying no cursor and no hover:
   nothing should look tappable that is not. */
.page-list__link--plain:hover { color: var(--fg); text-decoration: none; }

.page-list__summary {
  color: var(--fg-muted);
  margin-block: var(--space-3xs);
}

/* --- Long-form article --------------------------------------------------- */

/* Images break out of the reading measure. A 66ch column is right for text and
   too narrow for a map. */
.prose img {
  width: 100%;
  max-width: var(--measure-wide);
  margin-inline: calc((100% - min(var(--measure-wide), 100%)) / 2);
  margin-block: var(--space-md);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.prose__footer {
  margin-block-start: var(--space-xl);
  padding-block-start: var(--space-sm);
  border-block-start: 1px solid var(--border);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--fg-faint);
}

.notice {
  padding: var(--space-sm);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg-muted);
}

/* A notice that asks something: its answers in a row that wraps at 390px. The game-news question (009). */
.notice__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin-block-start: var(--space-sm);
}

/* Search is primary navigation on mobile. Nobody browses a 500-page hierarchy
   on a phone, so it stays visible in the header at every width. */
.site-search {
  flex: 1 1 auto;
  max-width: 22rem;
}

/* Drawer nav below the tablet breakpoint, persistent sidebar above it. */
.nav-toggle { display: inline-flex; }

.drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: var(--z-drawer);
  width: min(20rem, 85vw);
  background: var(--bg-raised);
  border-inline-end: 1px solid var(--border);
  padding: var(--space-sm);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translateX(-100%);
  transition: transform var(--dur) var(--ease);
}
.drawer[data-open] { transform: translateX(0); }

.drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-drawer) - 1);
  background: rgb(0 0 0 / 0.4);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}
.drawer-scrim[data-open] { opacity: 1; pointer-events: auto; }

@media (min-width: 60rem) {
  .nav-toggle, .drawer-scrim { display: none; }
  .drawer {
    position: sticky;
    top: 5rem;
    transform: none;
    height: fit-content;
    max-height: calc(100dvh - 6rem);
    border: 1px solid var(--border);
    border-radius: var(--radius);
  }
  .with-sidebar {
    display: grid;
    grid-template-columns: 16rem minmax(0, 1fr);
    gap: var(--space-lg);
    align-items: start;
  }
}

/* --- Admin ---------------------------------------------------------------- */

.admin-table { font-size: var(--text-sm); }
.admin-table th, .admin-table td { vertical-align: top; }
.admin-table__email { font-weight: 600; }
.admin-table__none { color: var(--fg-faint); }

/* The roster, which is a RECORD under docs/DESIGN.md - consulted rather than read, so a table is
   right and density is not a fault. It borrows .admin-table because the shape is identical and a
   second table style would be two things to keep in step; what it adds is one rule.

   NOTE - docs/DESIGN.md used to say game screens belong in game.css. That file does not exist and
   the character sheet's .sheet rules are here too, so this follows the code. DESIGN.md now says the
   same, and docs/FUTURE.md carries the move with what it costs. Do it in one pass or not at all. */
.roster__rung {
  color: var(--fg-faint);
  font-size: var(--text-xs);
  /* On its own line below the trade at 390px, beside it once there is room. A rung is a
     qualification on the trade rather than a second fact, so it should never read as a column. */
  display: block;
}

@media (min-width: 30rem) {
  .roster__rung {
    display: inline;
    margin-inline-start: var(--space-3xs);
  }
}

/* What the town has named somebody, under their name at every width: a title is recognition of the
   person, so it belongs with the name rather than beside it, and a name followed by "Vaticinator,
   Assessor" inline would read as a longer name. Regular weight so the name stays the row's heading. */
.roster__titles {
  display: block;
  color: var(--fg-muted);
  font-size: var(--text-xs);
  font-weight: 400;
}

/* Wraps at 390px rather than forcing the table wider than the screen. The table
   itself scrolls inside .table-wrap if it still cannot fit. */
.admin-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3xs);
}

.admin-actions select { width: auto; min-width: 7rem; }

.admin-actions .btn {
  min-height: var(--tap-min);
  padding-inline: var(--space-xs);
  font-size: var(--text-xs);
}

.tag {
  display: inline-block;
  margin-inline-start: var(--space-3xs);
  padding: 0 var(--space-3xs);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--fg-faint);
  white-space: nowrap;
}

.tag--warn { border-color: var(--danger); color: var(--fg-muted); }

/* --- Account pages -------------------------------------------------------- */

/* A sign-in form has no reason to be as wide as an article. Narrower than
   --measure so the fields read as a form rather than as prose. */
.auth {
  max-width: 26rem;
  margin-inline: auto;
}

/* Wider, for a page whose content needs more than a form column — the creation steps
   that list every species and every profession. The comment here used to say "recovery
   codes or a table"; no page has ever used it for that. */
.auth--wide { max-width: var(--measure); }

/* Left over from the Bootstrap grid: the pages wrap their form in a .row/.col pair.
   Renaming rather than unwrapping keeps the element count identical, which is what
   makes the sweep safe to do mechanically across thirty files.

   WATCH OUT — margin-inline is the fix for a real bug and is not decorative. Without
   it this column is left-aligned inside its parent. On a plain .auth that is invisible,
   because both are 26rem and there is no slack to be aligned within; on .auth--wide the
   parent is 66ch, so the heading and the progress rules spanned the full width while the
   content sat in a 26rem column hugging the left edge. It read as the list being off
   centre, and it was really the list being the only thing NOT centred. */
.auth__body {
  max-width: 26rem;
  margin-inline: auto;
}

/* A wide page asked to be wide, so its body fills it rather than re-narrowing to the
   form column. Choice blocks are the reason: they carry a label and a sentence of
   consequence, and re-capping them at 26rem wastes the width the page asked for. */
.auth--wide .auth__body { max-width: none; }
/* A reference mark on the error page is one unbroken word, longer than a phone is wide (259). */
.auth__body code { overflow-wrap: anywhere; }

.auth__alt {
  margin-block-start: var(--space-md);
  padding-block-start: var(--space-sm);
  border-block-start: 1px solid var(--border);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
}

.auth__alt p { margin-block: var(--space-3xs); }

/* One column at 390px; the nav moves beside the panel once there is room. */
.account-layout {
  display: grid;
  gap: var(--space-md);
  align-items: start;
}

@media (min-width: 48rem) {
  .account-layout { grid-template-columns: 14rem minmax(0, 1fr); }
}

.account-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.account-nav__link {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-xs);
  border-radius: var(--radius);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--fg-muted);
  text-decoration: none;
}

.account-nav__link:hover { background: var(--paper-sunken); color: var(--fg); }

.account-nav__link.active {
  background: var(--bg-raised);
  border-inline-start: 3px solid var(--accent);
  color: var(--fg);
  font-weight: 600;
}

/* The status banner. Success is quiet; a failure earns the accent. */
.notice--error {
  border-color: var(--danger);
  color: var(--fg);
}

/* The alarms at the front of /admin - decision 091. A notice rather than a danger box: an alarm is
   something to do, not something broken, and --danger is kept for errors and destruction. Each alarm is a
   link, and the whole line is the target at 44px, because an admin reads this on a phone too. */
.alarms { margin-block: var(--space-sm) var(--space-md); }
.alarms__none { color: var(--fg-muted); }
.alarms__list {
  list-style: none;
  margin: 0;
  display: grid;
  gap: var(--space-3xs);
}
.alarms__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}

/* Every admin edit of a draft, on the draft (106): a disclosure per edit, its changed lines as "Was" and "Now".
   A line is authored text and can be one long sentence, so it wraps anywhere rather than widening the page. */
.admin-edits { margin-block: var(--space-sm) var(--space-md); }
.admin-edits__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3xs); }
.admin-edits__lines { margin: var(--space-2xs) 0 0; display: grid; gap: var(--space-3xs); }
.admin-edits__line { display: grid; grid-template-columns: 3rem 1fr; gap: var(--space-2xs); }
.admin-edits__line dt { font-size: var(--text-xs); color: var(--fg-muted); }
.admin-edits__line dd { margin: 0; overflow-wrap: anywhere; }
.admin-edits__line--was dd { color: var(--fg-muted); text-decoration: line-through; }

/* What the town meets next, under the alarms - the front page's one panel that is not an alarm (091). A card,
   because it is a standing fact rather than something to do. */
.coming { margin-block: 0 var(--space-md); }
.coming > * + * { margin-block-start: var(--space-2xs); }
.coming h2 { margin-block-start: 0; }

/* A checkbox is the one input that must not stretch to the full width. */
.field--check {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: var(--tap-min);
}

/* The scaffolding wraps the box inside its own label, so the selector has to reach
   through one level rather than assume they are siblings. */
.field--check input[type="checkbox"] {
  width: auto;
  min-height: 0;
  flex: 0 0 auto;
  margin-inline-end: var(--space-2xs);
}

.field--check label {
  display: flex;
  align-items: center;
  margin: 0;
  font-family: var(--font-ui);
}

/* --- Forms --------------------------------------------------------------- */

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

input, select, textarea {
  /* 16px minimum. Anything smaller makes iOS Safari zoom the viewport on focus,
     which looks broken and strands the user at the wrong scale. */
  font-size: max(1rem, var(--text-base));
  font-family: var(--font-ui);
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-2xs) var(--space-xs);
  background: var(--bg-raised);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
}

input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

label {
  display: block;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  margin-block-end: var(--space-3xs);
}

.field { margin-block-end: var(--space-sm); }

.field__hint {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--fg-muted);
  margin-block-start: var(--space-3xs);
}

/* A link in a hint takes vertical padding, and it is the same rule and the same
   arithmetic as .sheet__note a. 14px type is a 15px hit area, barely a third of
   --tap-min, and the links that carry a reader between the week and the record
   are hints. Padding on an INLINE element grows the hit box without touching the
   line box, so unlike .site-nav__link this needs no compensating negative margin
   and moves nothing — measured before and after at 390, 768 and 1280. */
.field__hint a { padding-block: calc((var(--tap-min) - 1.15em) / 2); }

/* Errors state what happened and what to do. They don't apologise. */
.field__error {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--danger);
  margin-block-start: var(--space-3xs);
}
.field[data-invalid] input,
.field[data-invalid] select,
.field[data-invalid] textarea {
  border-color: var(--danger);
  background: var(--danger-wash);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3xs);
  min-height: var(--tap-min);
  padding-inline: var(--space-md);
  font-family: var(--font-ui);
  font-weight: 600;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--accent-strong);
  color: var(--paper-raised);
  cursor: pointer;
  /* An anchor styled as a button inherits the global link underline, which reads as a
     text link sitting inside a filled rectangle. */
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease);
}
/* WATCH OUT — the colour here is not redundant, and leaving it out made every
   anchor-button on the site unreadable on hover.

   `a:hover { color: var(--accent) }` is an element plus a pseudo-class, so its
   specificity is (0,1,1) and it BEATS `.btn`'s (0,1,0). An <a class="btn"> therefore
   took `--accent` text on the `--accent` background this rule sets: purple on the
   identical purple, invisible. A <button class="btn"> was fine, which is why it looked
   intermittent — it depended on which element the button happened to be.

   `.btn:hover` is (0,2,0) and wins, so restating the colour is the whole fix. Any
   future .btn variant must restate its own colour on hover for the same reason. */
.btn:hover {
  background: var(--accent);
  color: var(--paper-raised);
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn--quiet {
  background: transparent;
  color: var(--fg);
  border-color: var(--rule-strong);
}
.btn--quiet:hover {
  background: var(--paper-sunken);
  color: var(--fg);
}

.btn--danger { background: var(--danger); }

/* Full-width primary actions on mobile. A thumb should not have to aim. */
.btn--block { width: 100%; }
@media (min-width: 40rem) {
  .btn--block { width: auto; }
}

/* --- Multi-step forms ---------------------------------------------------- */

.steps {
  display: flex;
  gap: var(--space-3xs);
  list-style: none;
  padding: 0;
  margin-block-end: var(--space-md);
}
.steps li {
  flex: 1;
  height: 3px;
  border-radius: 2px;
  background: var(--rule);
}
.steps li[data-done]    { background: var(--accent); }
.steps li[data-current] { background: var(--accent-strong); }

/* --- Accessibility ------------------------------------------------------- */

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute;
  inset-inline-start: var(--space-2xs);
  inset-block-start: -100%;
  z-index: calc(var(--z-header) + 1);
  padding: var(--space-2xs) var(--space-sm);
  background: var(--accent-strong);
  color: var(--paper-raised);
  border-radius: var(--radius);
}
.skip-link:focus { inset-block-start: var(--space-2xs); }

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

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- Tables, which are the usual cause of horizontal overflow ------------ */

.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;

  /* WATCH OUT — position: relative is what CONTAINS the absolutely-positioned
     .visually-hidden captions and column labels inside the table. Without it their
     containing block is the initial one, so a hidden label sitting at x=520 inside a
     scrollable table extends the DOCUMENT's scrollWidth to 521 while the table itself
     scrolls correctly inside this element. body has overflow-x: hidden, so that is an
     invisible clip rather than a scrollbar, and the only way to see it is to measure
     documentElement.scrollWidth. Found on /admin/events at 390px. */
  position: relative;
}

table { border-collapse: collapse; width: 100%; font-family: var(--font-ui); }
th, td {
  text-align: start;
  padding: var(--space-2xs);
  border-block-end: 1px solid var(--border);
}
th { font-size: var(--text-sm); color: var(--fg-muted); font-weight: 600; }

/* --- The chronicle index -------------------------------------------------- */

/* Two levels of native <details>: a category, then a subcategory, then the articles.
   <details> rather than a click handler for decision 108's reasons — nothing to re-run
   on enhancedload, works with JavaScript off, and keyboard and screen-reader behaviour
   come free. Which branch arrives open is a server-rendered attribute driven by ?open=,
   because CSS cannot force one open and revealing its children instead is decision 125's
   trap: the closed element measures 0px and collapses whatever holds it. */
.index { margin-block-start: var(--space-lg); }

/* Watch out — .index carried `columns: 2` at 48rem and `columns: 3` at 72rem, to stop a
   103-item flat list running down one enormous column. Collapsed, the list is FIVE rows,
   and multi-column then does active harm: five rows distribute across three columns, so a
   reader scanning downward meets them in the order 1,2 | 3,4 | 5 rather than the order
   content/navigation.txt carefully gives them, and expanding one makes its column nine
   hundred pixels tall while the others stay at eighty. The rule was solving the problem
   this change removes, so it is gone from here and moved inside an open subcategory, where
   the length actually is. */

.index__section {
  border-block-end: 1px solid var(--rule-strong);
}

.index__section-name,
.index__group-name {
  display: flex;
  /* Centre rather than baseline: an SVG has no baseline of its own, so a
     baseline-aligned icon hangs below the cap line of the text beside it. */
  align-items: center;
  gap: var(--space-2xs);
  cursor: pointer;
  /* The whole row is the target, so it carries the floor itself rather than relying on
     the type inside it — which is measured after a webfont loads and is not a guarantee. */
  min-height: var(--tap-min);
  /* Our own chevron is the affordance, so the user agent's triangle goes. Both spellings
     are needed; ::-webkit-details-marker is still what WebKit reads. */
  list-style: none;
}
.index__section-name::-webkit-details-marker,
.index__group-name::-webkit-details-marker { display: none; }

.index__section-name {
  font-size: var(--text-lg);
  margin-block: 0;
}

.index__group-name {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--fg-faint);
  margin-block: 0;
}

.index__section-name:hover,
.index__group-name:hover { color: var(--accent-strong); }

.index__count {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--fg-faint);
  margin-inline-start: auto;
}

/* \25BE and \25B4, never \2303/\2304 — the latter render as a thin stray caret in several
   system stacks and read as a typo. The same pair .reveal already uses. */
.index__section-name::after,
.index__group-name::after {
  content: "\25BE";
  color: var(--fg-faint);
  font-size: var(--text-sm);
}
.index__section[open] > .index__section-name::after,
.index__group[open] > .index__group-name::after { content: "\25B4"; }

/* A subcategory is indented once so the two levels read as a tree. Once, not twice: at
   390px a second indent on the entries themselves is width a title needs more. */
.index__group { padding-inline-start: var(--space-2xs); }

.index__pages {
  list-style: none;
  padding: 0;
  margin-block: var(--space-3xs) 0;
}

.index__link {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2xs);
  /* Padding gives a thumb something to hit; the row is the target, not the words. */
  padding-block: var(--space-2xs);
  text-decoration: none;
  color: var(--fg);
  border-block-end: 1px solid var(--border);
}

/* The mark sits quieter than the title it labels — it is a category, not the name. */
.index__link .icon--entry {
  color: var(--fg-faint);
  /* Optically centres the icon on the first line of the title rather than on the
     whole wrapped block, which is where flex-start would otherwise put it. */
  margin-block-start: 0.15em;
}
.index__link:hover .icon--entry { color: var(--accent-strong); }

.index__title { min-width: 0; }
.index__link:hover { color: var(--accent-strong); background: var(--bg-raised); }

/* The width earns its keep here rather than at the top level: an open subcategory is
   where the length is — Monsters is twelve entries, Regions eleven — and reflow is
   contained inside the one group a reader has opened, so nothing above or below it moves.
   `.index__entry` takes the `break-inside` that stops a title being orphaned from its
   summary across a column break, and it takes it in lore.css where that block already
   lives rather than in a second block here. */
@media (min-width: 48rem) {
  .index__pages { columns: 2; column-gap: var(--space-xl); }
}
@media (min-width: 72rem) {
  .index__pages { columns: 3; }
}

/* --- The invitation above the categories ---------------------------------- */

/* Always visible, never behind a tap: it is the page that explains what the categories
   are, so putting it inside one inverts that. A .card rather than something new, because
   the point is that it reads as a different kind of object from the list below it. */
.index__featured {
  display: block;
  margin-block-start: var(--space-lg);
  text-decoration: none;
  color: var(--fg);
}
.index__featured:hover { border-color: var(--accent); }
.index__featured:hover .index__featured-title { color: var(--accent-strong); }

.index__featured-title {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  margin-block-start: var(--space-3xs);
}

.index__featured-summary {
  display: block;
  color: var(--fg-muted);
  margin-block-start: var(--space-3xs);
  /* The panel spans the index's full width — 1072px at 1280 — and this is the one line of
     real prose on the page. `.index__summary` already caps an entry's summary the same way;
     without it the invitation is the only sentence on the site set wider than it reads. */
  max-width: var(--measure);
}

/* --- The crumb back to an article's own branch of the index ----------------- */

/* Inside a .label, so it inherits the small uppercase face. An underline rather than the
   accent colour: it is navigation a reader can take, and an accent breadcrumb above every
   article heading on the site would be the loudest thing on the page.
   Padding on an INLINE element grows the hit box without touching the line box, so unlike
   .site-nav__link it needs no compensating negative margin and moves nothing —
   the same measurement .sheet__note a and .field__hint a already record. */
.crumb {
  color: inherit;
  text-decoration-color: var(--rule-strong);
  text-underline-offset: 0.25em;
  padding-block: calc((var(--tap-min) - 1.15em) / 2);
}
.crumb:hover { color: var(--accent-strong); text-decoration-color: currentColor; }

/* --- Search --------------------------------------------------------------- */

.search {
  display: flex;
  gap: var(--space-2xs);
  max-width: var(--measure);
  margin: var(--space-md) auto 0;
}

.search__input { flex: 1 1 auto; }

.search__button { flex: 0 0 auto; }

.search__summary {
  max-width: var(--measure);
  margin: var(--space-md) auto var(--space-2xs);
}

/* --- Back link ------------------------------------------------------------ */

/* The wrapper carries the width, in the body font, so it lines up with the reading
   column. --measure is 66ch and ch is font-relative: 66ch of the UI font is much
   narrower than 66ch of the body font, so putting the max-width on the link itself
   indented it well inside the text below. */
/* Sticky beneath the site header, which is itself sticky at top:0. --header-h is
   measured by wiki-back.js because the header grows at 30rem when the brand does;
   the fallback here is the mobile height and is only ever used before the script
   runs, or if it does not. */
.back-row {
  position: sticky;
  top: var(--header-h, 3.875rem);
  z-index: calc(var(--z-header) - 1);
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  max-width: var(--measure);
  margin-inline: auto;
  padding-block: var(--space-3xs);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
}

/* The title appears only once the real heading has scrolled away, so the two are
   never on screen together. */
.back-row__title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: end;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  color: var(--fg-muted);
  opacity: 0;
  transition: opacity 120ms ease;
}

.back-row--titled .back-row__title { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .back-row__title { transition: none; }
}

.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  min-height: var(--tap-min);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--fg-muted);
  text-decoration: none;
}
.back-link:hover { color: var(--accent-strong); }


/* --- Choices — archetype 4, and the pick step of a form flow ---------------
   docs/DESIGN.md owns this. A choice is NOT a button in a row: it is a block
   the width of the text, with a verb as its label and a sentence of consequence
   under it, tappable across its whole area. Stacked, never side by side — at
   390px a row of choices is three cramped columns, and on desktop a row invites
   a scan rather than a read. A weekly decision deserves to be read.

   The state attributes are data-* rather than classes because the server renders
   them, which is this project's own naming rule. */

.choice-list {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.choice {
  display: block;
  padding: var(--space-sm);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--bg-raised);
  cursor: pointer;
}

.choice:hover { border-color: var(--rule-strong); }

/* The whole block is the target, so the radio itself is hidden rather than
   shrunk — a 13px radio beside a tappable block is two hit areas for one
   choice, and the small one is the one a thumb finds first. */
.choice--pick input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Focus has to land on the block, because the input it belongs to is hidden. */
.choice--pick:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.choice--pick:has(input:checked) {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}

.choice__label {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  flex-wrap: wrap;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--fg);
}

.choice__consequence {
  display: block;
  margin-block-start: var(--space-3xs);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  color: var(--fg-muted);
}

/* A count is a record, not prose — Archivo, like every other figure on a record
   surface. */
.choice__count {
  display: block;
  margin-block-start: var(--space-2xs);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--fg-faint);
}

/* The chosen one, and the ones that were not. A record of a decision with the
   alternatives erased is not a record of a decision, and in two hundred weeks
   the chronicle is the only artefact a late arrival has. */
.choice[data-chosen] {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}

.choice[data-past] {
  border-style: dashed;
  background: transparent;
  color: var(--fg-faint);
}
.choice[data-past] .choice__label { color: var(--fg-muted); }

/* Barred: shown, never hidden, and NOT a control — no cursor, nothing to focus,
   nothing that looks tappable. Styled quieter rather than broken: --danger is
   for something going wrong, and a choice you have not grown into yet is not an
   error. */
.choice[data-barred] {
  cursor: default;
  border-style: dashed;
  background: transparent;
  opacity: 0.75;
}
.choice[data-barred]:hover { border-color: var(--rule); }

/* What taking a choice costs — Dangerous, Strenuous — as a pill inside the
   label (ChoiceTagList.razor), the author's call of 2026-09-29. It replaced a
   leading edge that the selected state painted over, so a pill must stay the
   same whether or not its choice is checked: it sets its own ground.
   Madder on the madder wash, for every tag: the one place madder marks
   something that is not an error or a destruction (docs/DESIGN.md §1). Named as
   the hue rather than --danger, since nothing has gone wrong; working hurt is a
   legitimate choice. Contrast, computed: 6.30:1 light, 4.95:1 dark.
   Orthogonal to barred — barred means cannot be taken, a tag means takeable and
   it costs you — so a barred choice carries none. */
.choice__tag {
  margin-inline-start: 0; /* the label's flex gap spaces it */
  border-color: var(--madder);
  background: var(--madder-wash);
  color: var(--madder);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

@media (prefers-reduced-motion: no-preference) {
  .choice { transition: border-color var(--dur-fast) var(--ease); }
}

/* --- The decision screen — archetype 4 -------------------------------------
   docs/DESIGN.md: "A situation in prose, then choices. It is closest to the
   reading page and should feel like it: the same measure, the same body face,
   the same tooltips on [[slug]] terms." And: "--veil and nothing else behind
   it", which is why the markup pairs .decision with the .veil utility rather
   than this rule setting a background of its own — one bloom, declared in one
   place, used by whatever wants to feel lit.

   It carries the gutter and the vertical room, because the screen is a panel a
   week sits inside rather than a document that starts at the top of the page.
   The reading column itself is .prose, unchanged, so a decision measures exactly
   what a wiki page measures — which is the whole of "should feel like it". */

.decision {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: var(--space-lg);
  border-radius: var(--radius);
}

/* The choices are a block of their own, not another paragraph. .prose sets a
   rhythm between siblings and a list of tappable blocks wants more air above it
   than a sentence does. */
.decision .choice-list { margin-block-start: var(--space-md); }

/* --- The telling ------------------------------------------------------------
   docs/DESIGN.md names `.outcome` for this and says what it has to achieve:
   "the telling has to be distinguishable from the situation above it". The hard
   part is that a block of choices sits BETWEEN them, so proximity cannot do it
   and the distinction has to survive the gap.

   A ruled leading edge in the accent does it, and it is the signature .facts and
   .sheet__field already carry — so the telling reads as the same kind of object
   those do: the part of the page that is about THIS character rather than about
   the week. Everything else is left alone. It is prose at the same measure and
   the same face as the situation, because it is the answer to the question the
   screen asked and the part a player comes back for; making it smaller would say
   the opposite. */

.outcome {
  margin-block-start: var(--space-lg);
  padding-inline-start: var(--space-sm);
  border-inline-start: 3px solid var(--accent);
}

/* Mirrors .prose's own rhythm rather than inheriting it: .prose sets
   `> * + *`, and these paragraphs are one level deeper than that selector
   reaches. The global `* { margin: 0 }` means they would otherwise run
   together. */
.outcome > * + * { margin-block-start: var(--space-sm); }

/* A lapse is the telling's absence, and it is not an error and not a rebuke.
   Muted, and deliberately not --danger: decision 047 is explicit that a missed
   week costs nothing, and a week passing without you is an ordinary thing in a
   chronicle. */
.outcome--lapsed {
  border-inline-start-color: var(--rule-strong);
  color: var(--fg-muted);
}

/* --- An absent character on the roster --------------------------------------
   Decision 086: a character deactivated after eight missed decisions stays on
   the roster, marked away. NOTHING WAS TAKEN from them — the rung, the aspect
   and the whole record are where they were left, and the player brings them
   back by choosing to.

   So this reads as AWAY, never as gone and never as an error. --danger is for
   something going wrong, and a town has people who are travelling. Same
   argument as .choice[data-barred] one archetype over: quieter, not broken. */

.roster tr[data-absent],
.roster tr[data-dead] { color: var(--fg-muted); }

/* The word rather than an icon, for the reason the strenuous marker is a
   sentence: a reader should not have to learn a symbol to read a table. */
.roster__away {
  margin-inline-start: var(--space-2xs);
  padding: 0 var(--space-2xs);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--fg-faint);
  white-space: nowrap;
}

/* --- The record sheet — archetype 5 ----------------------------------------
   docs/DESIGN.md names `.sheet` as "a .facts grid at full width". The reason it
   is not just .facts: a facts block sets its label and value on ONE line, which
   runs the question and the answer together. A sheet stacks them — label above,
   in Archivo, small and lettered-out; value below, in the body face — so a
   glance separates what is being asked from what was answered.

   This is the base for the eventual character sheet as well as the creation
   summary, which is why the field is a component rather than a one-off layout. */

.sheet {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2xs);
  margin: 0 0 var(--space-md);
}

/* Two across once there is room. At 390px a sheet is one column and stays
   readable; side-by-side fields at that width are two cramped columns. */
@media (min-width: 34rem) {
  .sheet { grid-template-columns: 1fr 1fr; }
  .sheet__field--wide { grid-column: 1 / -1; }
}

.sheet__field {
  padding: var(--space-2xs) var(--space-xs);
  background: var(--bg-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  /* The ruled leading edge is the corpus facts block's own signature, kept so a
     record reads as the same kind of object wherever it appears. */
  border-inline-start: 3px solid var(--accent);
}

.sheet__label {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
}

.sheet__value {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-xs);
  margin: 0;
  margin-block-start: var(--space-3xs);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--fg);
}

/* An answer in the player's own words is prose, not a datum, so it takes the
   reading face and wraps like a sentence. */
.sheet__value--prose {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
}

/* Something not yet known, which is a real state rather than a gap — an aspect
   is not measured until somebody tests for it. Quiet, never --danger. */
.sheet__value--absent {
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--fg-faint);
  font-style: italic;
}

/* Per-field review link. Small and set apart from the value so it never reads as
   part of the answer. */
.sheet__edit {
  flex: none;
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
}
.sheet__value--prose .sheet__edit {
  display: inline-block;
  margin-inline-start: var(--space-2xs);
}

/* A second <dd> under the value, saying what the value means. The full character
   sheet is where this became necessary: a rung is seeded as "Striker" and a
   condition as "Injured", and neither word says how far along a ladder it is or
   what moving off it depends on. The name is the value; this is the note.

   Archivo at --text-sm, matching .field__hint, because it is chrome explaining a
   datum rather than prose to be read at --measure. */
.sheet__note {
  margin: var(--space-3xs) 0 0;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--fg-muted);
}

/* A link in a note is 14px type, which is a 19px hit area and half of what a thumb
   needs. Vertical padding on an INLINE element expands the hit box without touching
   the line box, so this needs no compensating negative margin — the .site-nav__link
   pattern without its cost, and it works because the element is never made a block. */
.sheet__note a { padding-block: calc((var(--tap-min) - 1.15em) / 2); }

/* Several answers in one field — the characteristics a character brought, which
   are rows rather than a sentence and have no fixed number. Inside a
   .sheet__value--prose, which supplies the block display and the reading face. */
.sheet__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.sheet__list li {
  position: relative;
  padding-inline-start: var(--space-sm);
}

.sheet__list li + li { margin-block-start: var(--space-2xs); }

/* The site's own lozenge as the mark, masked so it takes the accent in both
   themes — the same motif as .prose hr and the header sigil, at bullet size. */
.sheet__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.55em;
  width: 7px;
  height: 7px;
  background: var(--accent);
  -webkit-mask: var(--lozenge-mask) center / 7px 7px no-repeat;
          mask: var(--lozenge-mask) center / 7px 7px no-repeat;
}

/* --- The condition track — a state machine, not a gauge --------------------
   docs/DESIGN.md forbids progress bars on a reading surface and makes .steps the
   only bar in the system. This is neither: a record surface may be dense, and what
   this draws is a POSITION ON A LADDER rather than a proportion of something done.
   The difference is legible in the markup — five named rungs, one marked, nothing
   filled behind it. A filled run would say "you have achieved Weakened", which is
   the reading a progress bar would impose on a scale that only goes down.

   Every rung is drawn rather than only the one held, because the machine is only
   legible as a machine: somebody who cannot see there is a step below Debilitated
   cannot understand why the risky choices went away. */

.track {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-3xs);
  margin-block-start: var(--space-3xs);
}

.track__step {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  min-width: 0;
  text-align: center;
}

.track__mark {
  height: 4px;
  border-radius: 2px;
  background: var(--rule);
}

.track__name {
  font-family: var(--font-ui);
  /* 12px, and case as authored: uppercase plus tracking would widen "Debilitated"
     by about a third and it is already the widest rung at 390px. */
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  color: var(--fg-faint);
  overflow-wrap: anywhere;
}

/* States as markup rather than classes, the same as .steps and .choice. */
.track__step[data-current] .track__mark { background: var(--accent); height: 6px; }
.track__step[data-current] .track__name { color: var(--fg); font-weight: 600; }

/* --- Reveal — a note that opens, with no JavaScript ------------------------
   <details> is native, so it survives an enhanced navigation without a script to
   re-run and needs no circuit. The whole summary is the hit target, which on the
   condition field means the whole track is. */

.reveal__summary {
  cursor: pointer;
  /* The default triangle sits beside a five-column grid and pushes it off centre.
     .reveal__hint is the affordance instead, and it says what it will do. */
  list-style: none;
}
.reveal__summary::-webkit-details-marker { display: none; }

.reveal__hint {
  display: block;
  margin-block-start: var(--space-2xs);
  /* Padding for the hit area, and it is inside the summary, which is already the
     target — this is about the words looking tappable rather than about reaching. */
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent-strong);
}

/* Filled triangles rather than the chevron characters at U+2303/2304, which render
   as a thin stray caret in several system stacks and read as a typo. */
.reveal__hint::after { content: " \25BE"; }                 /* down */
.reveal[open] .reveal__hint::after { content: " \25B4"; }   /* and up */

/* --- Footnote — read once, then never again -------------------------------
   Set below the body size deliberately. It explains three absences on the record
   above it, which is worth saying and is not worth the weight of what it explains. */

.footnote {
  margin-block-start: var(--space-lg);
  padding-block-start: var(--space-sm);
  border-block-start: 1px solid var(--rule);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--fg-muted);
}

.footnote p { margin-block-end: var(--space-2xs); }
.footnote p:last-child { margin-block-end: 0; }
.footnote strong { color: var(--fg); font-weight: 600; }

/* --- Creation asides ------------------------------------------------------- */

/* The "read about this species" link under a choice. Deliberately outside the
   <label>: a link inside one steals the click meant to select the option. */
.choice__aside {
  margin: var(--space-3xs) 0 0;
  padding-inline-start: var(--space-sm);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
}

/* The way back through a flow. Quiet by design — the forward action is what the
   screen is asking for, and two controls of equal weight make a player choose
   between them rather than answer the question. */
.creation-back {
  margin-block-start: var(--space-md);
  font-size: var(--text-sm);
}

/* --- Event editor ---------------------------------------------------------
   Here rather than in game.css because this is an admin screen: docs/DESIGN.md
   assigns admin to this file, and game.css is reserved for the player-facing
   game surfaces and the .decision components section 4 named.

   The dashboard is exempt from the reading rules and may be as dense as it needs
   to be. What it is not exempt from is rhythm — the choice cards, the buttons that
   add one and the actions that end the form were all flush against each other,
   which read as one undifferentiated column with no way to tell a container from
   a control. */

/* A stage on a town track is the same KIND of object as a choice on an event - a
   container holding fields, and a thing in its own right rather than a qualifier on
   the thing above it. So it takes the same treatment rather than a second one, and
   it deliberately does not borrow .editor__filter, which is quieter precisely
   because a filter IS a qualifier. */
.editor__choice,
.editor__stage { margin-block-end: var(--space-sm); }

/* The default triangle is the affordance here, unlike .reveal, which hides it
   because it sits beside a five-column grid. A dense form wants the cheapest
   disclosure there is, and the writer is operating rather than reading. */
.editor__choice > summary,
.editor__stage > summary {
  cursor: pointer;
  padding-block: var(--space-2xs);
  font-family: var(--font-ui);
  font-weight: 600;
}

/* A filter block, at any of its three positions. It takes .editor__choice's summary
   treatment rather than defining its own - one disclosure style on one form, or a
   writer learns two - and it is quieter than a choice because it is a qualifier on
   the thing above it rather than a thing of its own. */
.editor__filter {
  margin-block: var(--space-sm);
  background: transparent;
  border-style: dashed;
}

.editor__filter > summary {
  cursor: pointer;
  padding-block: var(--space-2xs);
  font-family: var(--font-ui);
  font-weight: 600;
  color: var(--fg-muted);
}

/* Two fields that are one idea - a minimum and a maximum, a stat and its threshold.
   Side by side above 30rem and stacked below it, because at 390px two number inputs
   in a row are two cramped columns. */
.editor__range {
  gap: var(--space-sm);
}

@media (min-width: 30rem) {
  .editor__range {
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  }
}

/* An outcome belongs to the choice above it, and nesting is the only thing that
   says so once the form is three fields deep. A rule rather than another card:
   a card inside a card flattens the hierarchy until neither reads as a container. */
.editor__outcome {
  margin-block: var(--space-sm);
  padding-inline-start: var(--space-sm);
  border-inline-start: 2px solid var(--rule);
}

/* Buttons sit apart from the fields they act on, or they read as another field. */
/* Authored prose shown back rather than edited — the approval screen, where
   somebody reads what a writer wrote. The reading face, because it is prose, and
   the measure because it is read rather than scanned. */
.editor__readonly {
  font-family: var(--font-body);
  max-width: var(--measure);
  white-space: pre-wrap;
}

.editor__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-block: var(--space-sm);
}

/* NOT a .stack. A stack stretches its children, which turned Save and Submit into
   two full-width bars — and a full-width block is the decision screen's shape for a
   player's choice. These are controls on a tool, and claiming that shape here would
   say something about them that is not true. */
.editor__actions .btn { flex: 0 0 auto; }

/* The form's own end, set off from everything above it. */
.editor__actions--end {
  margin-block-start: var(--space-lg);
  padding-block-start: var(--space-sm);
  border-block-start: 1px solid var(--rule);
}

/* Decision 066's preview, below the form and outside it. A record surface — dense
   and consulted rather than read — so it takes the UI face throughout. */
.editor__preview {
  margin-block-start: var(--space-lg);
  font-family: var(--font-ui);
}

/* The findings. They sit OUTSIDE the cast disclosure on purpose: a finding only
   visible once somebody opens a <details> is not a finding, which is decision 091's
   shape for an alarm. Decision 125's trap is why that placement needs saying — a
   closed <details> has no size, so anything laid out while it is shut is a sibling. */
.editor__findings {
  margin-block: var(--space-sm);
  padding-inline-start: var(--space-md);
  border-inline-start: 3px solid var(--accent);
}

.editor__cast > summary {
  cursor: pointer;
  padding-block: var(--space-2xs);
  font-weight: 600;
  color: var(--fg-muted);
}

/* One person's whole reading, separated from the next by a rule rather than a card:
   eight cards inside a card is a stack of containers and no hierarchy at all. */
.editor__reading {
  margin-block: var(--space-md);
  padding-block-start: var(--space-sm);
  border-block-start: 1px solid var(--rule);
}

.editor__reading h4 { margin-block-end: var(--space-3xs); }

/* What lands and what does not. The arrow in the markup carries the meaning; weight
   and colour only reinforce it, so this reads the same to anyone who cannot see the
   difference between the two colours. */
.editor__lands { font-weight: 600; }
.editor__misses { color: var(--fg-muted); }

/* --- The town ledger ------------------------------------------------------ */

/* Cards rather than rows, and that follows from a question rather than from taste: a row is
   right when entries are compared down a column, a card when each is read on its own. The
   roster settled that and is the counter-example — four short parallel fields, compared. A
   ledger entry is a name and a SENTENCE, so there is nothing to scan down.

   .page-list is reused rather than restated, the way .editor__stage reuses .editor__choice:
   it already carries the measure, the centring and the list reset, and it is already tested
   at 390px in both themes. What is added here is only what a card needs that a row did not. */
.ledger {
  display: grid;
  gap: var(--space-sm);
}

/* The group heading sits ABOVE the cards rather than inside one, so the three kinds read as
   three parts of one record. Spaced from the group before it and not from its own list. */
.ledger__group + .ledger__group { margin-block-start: var(--space-lg); }

.ledger__track {
  /* .card supplies the ground, border and padding. Only the border-block-end from
     .page-list__item would be wrong here — a card already has an edge — and this element
     never takes that class, so there is nothing to undo. */
  display: grid;
  gap: var(--space-3xs);
}

.ledger__name {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  margin: 0;
}

/* The rung's short name, in the same key .facts uses for a field label: this is the record's
   own word for where the town stands, and the sentence under it is the prose. It is NOT a
   figure — decisions 043 and 052 keep the index out of the page entirely. */
.ledger__stage { margin: 0; }

/* The prose inside a card takes the card's width rather than the page measure, or a one-line
   sentence would sit in a column narrower than the card holding it. */
.ledger__track .prose { max-width: none; }
.ledger__track .prose p { margin: 0; }
