/* ===========================================================================
   ogma.css

   Purpose:      The project's single stylesheet. Defines the design tokens and
                 the minimum styling needed for the skeleton to be legible.
   Dependencies: None.
   Status:       First pass, deliberately thin. The terminal visual treatment
                 is a separate piece of work; this exists so that the pages are
                 readable while the deployment pipeline is being proven, and so
                 that there is one place for colour and type to live from the
                 outset rather than being retrofitted.

   The interface commits to a single dark palette rather than supporting a
   light theme, so every colour is declared explicitly.
   =========================================================================== */

:root {
  color-scheme: dark;

  /* Ground and surface. A cold, slightly cyan-shifted black, to sit warm
     amber against a complementary rather than a neutral ground. */
  --surface-void:   #06080A;
  --surface-deep:   #0A0E11;
  --surface-panel:  #10161A;
  --surface-raised: #172025;

  /* Rules and edges */
  --edge-faint:  #1B2429;
  --edge:        #253138;
  --edge-strong: #35464F;

  /* Signal colours. Amber carries identity and emphasis. Cyan is reserved for
     system state, rose exclusively for failure and green exclusively for a
     check that has passed, so that a red mark and a green one each always
     mean the same thing. */
  --amber:        #F5A623;
  --amber-bright: #FFC15E;
  --amber-dim:    #9C6B22;
  --cyan:         #58C8D4;
  --green:        #5BD08A;
  --green-deep:   #3C8C5E;
  --rose:         #E2566F;
  /* Rose taken past the point of being a signal colour. Used once, on the
     one line that is supposed to stop somebody reading the page. */
  --neon:         #FF3355;

  /* Text */
  --ink:       #CDD8DC;
  --ink-bright:#FFFFFF;
  --ink-quiet: #8A9AA1;
  --ink-faint: #566670;
  --ink-invert:#06080A;

  /* Type */
  /* One face, everywhere. There was a reading face as well, a serif, for the
     places holding more than a line or two of prose. It read as a different
     site pasted into this one. This is a public terminal and everything on it
     is output from the same machine, so everything on it is set in the
     machine's face. */
  --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

  --track-wide: 0.09em;
  --measure: 66ch;

  /* Space, on a four pixel base step */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;

  --gutter: 20px;
  --shell-max: 1180px;

  /* How far under the status bar a page's content starts. Held as a token
     because the landing page's two panels have to open on one line and every
     other layout has to clear the bar by the same amount. */
  --deck-top: var(--space-4);

  /* Width of a self-contained task panel. Wide enough that a question, its
     note and its fields do not feel cramped, narrow enough to stay a screen
     rather than a page. */
  --panel-max: 720px;

  /* Two questions need laying out rather than listing: the ten Roles and
     the ten STATs. Both are read across, so they get their own width. */
  --panel-wide: 960px;
}

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

/* Scripts hide things with the attribute rather than with a class, so it
   has to beat whatever display the element was otherwise given. */
[hidden] { display: none !important; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--surface-void);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  color: var(--amber);
  line-height: 1.15;
  margin: 0 0 var(--space-4);
  text-wrap: balance;
}
h1 { font-size: 1.75rem; letter-spacing: 0.02em; text-transform: uppercase; }

p { margin: 0 0 var(--space-4); max-width: var(--measure); }

a { color: var(--amber); }
a:hover { color: var(--amber-bright); }

:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }

/* ---------------------------------------------------------------------------
   Status bar. The persistent machine identity strip.
   --------------------------------------------------------------------------- */
.statusbar {
  background: var(--surface-void);
  border-bottom: 1px solid var(--edge);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
}
/* One row, always. The bar is the terminal's chrome rather than part of the
   content column, so it takes the whole window rather than stopping at the
   content's maximum width, and it does not wrap: a status bar that becomes
   two status bars has stopped being a status bar.

   What gives instead is the contents. Each item below is ranked by how much
   it tells an operator, and the least useful are dropped at the widths where
   the row would otherwise overflow. The clock's time, the operator and the
   meter are never dropped. Everything returns as the window widens. */
.statusbar__inner {
  padding: var(--space-2) var(--gutter);
  display: flex;
  flex-wrap: nowrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
}
.statusbar__identity,
.statusbar__meter {
  display: flex;
  flex-wrap: nowrap;
  align-items: baseline;
  gap: var(--space-4);
}

/* The last defence, for the widths between two breakpoints where a long
   operator name still will not fit. The meter never gives up room, because
   it is the one part of the bar that is about money; the identity gives up
   its right-hand end instead, and the page is never pushed sideways. */
.statusbar__identity {
  min-width: 0;
  overflow: hidden;
}
.statusbar__meter { flex: none; }

/* Items keep their own words together, and draw the separator that precedes
   them. Adjacency is a fact about the markup rather than about what is on
   screen, so a hidden item removes its own separator and the items either
   side of it stay correctly divided. */
.statusbar__item { white-space: nowrap; }
.statusbar__item + .statusbar__item::before {
  content: "|";
  margin-right: var(--space-4);
  color: var(--edge-strong);
}
.statusbar b { color: var(--amber); font-weight: 500; }

/* The one item in the bar that can be pressed, and drawn as a word all the
   same. It was boxed to say so and the box sat proud of a row that is
   otherwise plain text, which made the bar look broken rather than the word
   look pressable. It wears the terminal's own colour instead, which is what
   every value in this row is already set in. */
.statusbar__admin {
  color: var(--amber);
  font-weight: 500;
  text-decoration: none;
}
.statusbar__admin:hover,
.statusbar__admin:focus-visible { color: var(--amber-bright); }

/* The clock. Read from the browser, so the figures change width as they
   count; tabular digits stop the bar shuffling every second. */
.clock { font-variant-numeric: tabular-nums; }
.clock__time { margin-left: var(--space-2); }

/* The separators blink rather than the whole clock, which is how a clock
   signals it is running without becoming unreadable while it does. */
.clock__tick {
  font-style: normal;
  animation: clock-tick 1s steps(1, end) infinite;
}
@keyframes clock-tick {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0.15; }
}

/* Stopped for anyone who has asked their system not to animate things. A
   blinking colon is exactly the sort of thing that request is about. */
@media (prefers-reduced-motion: reduce) {
  .clock__tick { animation: none; }
}

/* The meter sits at the far end of the bar, away from the identity, because
   it is the one thing on there that is about money. */
.statusbar__meter { font-variant-numeric: tabular-nums; }
.statusbar__meter b { color: var(--cyan); }

/* The fee is the one figure on the bar that costs the operator something, so
   it is the one figure that is not cyan. */
.statusbar__charge b { color: var(--amber); }

/* What the row sheds, and in what order. Read downwards: each width removes
   one more item, and the item removed is the one saying least about the
   session at that point.

   The pool goes first, being the one line that is the same for everybody on
   it. Then the terminal's designation, then the clock's caption, then the
   date, then the operator's own name, which they can be assumed to know. */
@media (max-width: 1240px) {
  .statusbar__item--pool { display: none; }
}
@media (max-width: 1090px) {
  .statusbar__item--term { display: none; }
}
@media (max-width: 960px) {
  .statusbar__zone { display: none; }
}
@media (max-width: 830px) {
  .statusbar__date { display: none; }
  .clock__time { margin-left: 0; }
}
@media (max-width: 700px) {
  .statusbar__item--operator { display: none; }
}

/* Narrower still, the control goes too. Everything in the bar below this
   width has to earn its place against the clock and the meter, and a
   shortcut to a page reachable by typing its address does not. */
@media (max-width: 560px) {
  .statusbar__item--admin { display: none; }
}

/* Narrower than a phone held upright, there is nothing left to drop that
   would not take the meter with it. The row is allowed to become two, and is
   centred so that it reads as a deliberate arrangement rather than as a bar
   that has come apart. */
@media (max-width: 480px) {
  .statusbar__inner {
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-1) var(--space-4);
  }
  .statusbar__identity { justify-content: center; }
}

/* ---------------------------------------------------------------------------
   Layouts
   --------------------------------------------------------------------------- */
.shell {
  max-width: var(--shell-max);
  margin: 0 auto;
  padding-inline: var(--gutter);
  padding-block: var(--space-7);
}

.gate {
  min-height: calc(100vh - 40px);
  display: grid;
  place-items: center;
  padding-inline: var(--gutter);
  padding-block: var(--space-7);
}
.gate > * {
  width: 100%;
  max-width: 420px;
  background: var(--surface-panel);
  border: 1px solid var(--edge);
  padding: var(--space-6);
}

/* ---------------------------------------------------------------------------
   Forms
   --------------------------------------------------------------------------- */
label {
  display: block;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  margin-bottom: var(--space-2);
}

input[type="text"],
input[type="password"],
/* A search box is a text box with a cross in it. Left out of this rule it
   was drawn by the browser instead, and sat shorter and paler than the
   lists beside it in the catalogue's filters. */
input[type="search"] {
  width: 100%;
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  padding: var(--space-3);
}
input:focus { border-color: var(--amber); outline: none; }

/* A button element and an anchor carrying .btn are the same object to the
   operator, so they are styled together and any control reads identically
   whether it submits a form or follows a link. Inline flex is used so that a
   directional arrow sits beside the label with a controlled gap. */
button,
.btn {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  padding: var(--space-3) var(--space-5);
  border: 1px solid var(--amber);
  background: var(--amber);
  color: var(--ink-invert);
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}
/* The label is restated on hover, and deliberately. An anchor carrying .btn
   is still an anchor, and a:hover further up this file sets a link's colour
   to the same amber the control's background brightens to, which leaves an
   amber label on an amber field and nothing to read. Saying the colour again
   here settles it for both kinds of control at once, so a link and a button
   behave the same way under the pointer. Variants that hover to a colour of
   their own, such as deletion, say so after this and keep it. */
button:hover,
.btn:hover {
  background: var(--amber-bright);
  color: var(--ink-invert);
}

button:disabled,
.btn--disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
button:disabled:hover { background: var(--amber); }

/* ---------------------------------------------------------------------------
   Messages
   --------------------------------------------------------------------------- */
.messages { list-style: none; margin: 0; padding: var(--space-3) var(--gutter); }
.message {
  border-left: 2px solid var(--edge-strong);
  background: var(--surface-raised);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-2);
  font-size: 0.875rem;
}
.message--error { border-left-color: var(--rose); }
.message--success { border-left-color: var(--cyan); }

[role="alert"] { color: var(--rose); }

/* ---------------------------------------------------------------------------
   Authentication gate
   One panel, one control. The heading and the button are centred on it, which
   is what a screen with a single thing to do should look like.
   --------------------------------------------------------------------------- */
.login__heading { text-align: center; }

.login__actions {
  display: flex;
  justify-content: center;
  margin-top: var(--space-5);
}

/* Small print, and held far enough from the button that nobody reads it as
   part of the control. */
.login__note {
  margin: var(--space-7) 0 0;
  max-width: none;
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--ink-quiet);
  /* Justified, which is how a notice printed by an institution is set. The
     type is monospaced, so the word spacing cannot stretch unevenly enough
     to open rivers down the block. */
  text-align: justify;
  text-wrap: pretty;
}

/* ---------------------------------------------------------------------------
   Terminal panel
   A bezelled frame standing in for a physical screen. Used by the creation
   wizard and anything else that is one self-contained task.
   --------------------------------------------------------------------------- */
.gate > .terminal,
.gate > .record {
  /* The generic .gate child styling is for the login panel. These two bring
     their own framing, so the padding is removed and the width relaxed. */
  max-width: var(--panel-max);
  padding: 0;
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

.terminal__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--edge);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
}
.terminal__bar-id { color: var(--amber); }
.terminal__progress { color: var(--ink-faint); }

.terminal__body { padding: var(--space-6); }

/* A panel sets its own measure by being a fixed, modest width. The global cap
   on paragraph width is therefore redundant inside one, where it only leaves a
   ragged right margin against a straight edge.

   The advertising window is the same case and was missing from this list, so
   its two paragraphs of small print ran down a third of the window and took
   four lines each to say what fits in two. */
.terminal__body p,
.popup__body p,
.record__body p { max-width: none; }

/* ---------------------------------------------------------------------------
   The Role Specific Lifepath has no styles of its own. It is a list with room
   to answer outside it, which is the shape most of the assessment takes, so it
   is asked on that page and wears the .choice rules above.
   --------------------------------------------------------------------------- */

/* What a Role answered, on the sheet. */
.story__list { margin: 0; }

.story__question {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
  margin-bottom: var(--space-1);
}

.story__said {
  margin: 0 0 var(--space-4);
  max-width: none;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--ink);
}

/* ---------------------------------------------------------------------------
   Creation, as a room in the terminal rather than a room with the door shut.

   The shared navigation down the left and the question beside it, started at
   the top rather than centred in the window: a long question centred is a
   question an operator scrolls to the top of before they can read it.
   --------------------------------------------------------------------------- */
/* Three columns while there is room for three: the site's navigation, the
   question, and this flow's own contents. The contents column is narrower
   than the navigation because it holds short labels rather than section
   names, and giving it the same width would push the question off centre. */
.workbench {
  display: grid;
  grid-template-columns: minmax(14rem, 20rem) minmax(0, 1fr) minmax(12rem, 16rem);
  gap: 0;
  align-items: stretch;
  min-height: calc(100vh - 40px);
}

/* A page of the flow with nothing to list, such as the compiled profile,
   still uses this layout. Without this the question would sit left of centre
   under an empty third column. */
.workbench--plain { grid-template-columns: minmax(14rem, 20rem) minmax(0, 1fr); }

.workbench__panel {
  display: grid;
  align-content: start;
  justify-items: center;
  min-width: 0;
  padding: var(--deck-top) var(--gutter) var(--space-7) var(--space-6);
}

.workbench__panel > .terminal {
  width: 100%;
  max-width: var(--panel-max);
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

/* The four questions that are a grid rather than a list of fields. Declared
   beside the rule it widens rather than left to the one written for .gate,
   which this layout does not use. */
.workbench__panel > .terminal--wide { max-width: var(--panel-wide); }

/* The flow's other pages: what registration accepted, what the assessment
   concluded, the advertising between the two, and the confirmation that
   discards it all. Framed like the questions because they are the same
   encounter continuing. */
.workbench__panel > .record,
.workbench__panel > .popup {
  width: 100%;
  max-width: var(--panel-max);
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

/* Below this the two columns cannot both have the room they need, so the
   navigation goes above the question, which is the order they are wanted in. */
@media (max-width: 1400px) {
  .workbench { grid-template-columns: minmax(14rem, 20rem) minmax(0, 1fr); }

  /* Below the width that fits three columns the contents follows the
     question rather than squeezing it, which is also the order it is wanted
     in: answer what is in front of you, then jump. */
  .contents {
    grid-column: 1 / -1;
    border-left: 0;
    border-top: 1px solid var(--edge);
  }
}

@media (max-width: 1080px) {
  .workbench,
  .workbench--plain { grid-template-columns: minmax(0, 1fr); }

  .workbench__panel { padding: var(--space-5) var(--gutter) var(--space-7); }
}

/* ---------------------------------------------------------------------------
   Contents
   The flow down the right: every question in it, grouped under the headings
   the pages themselves carry. Plain links, no script, no state.
   --------------------------------------------------------------------------- */
.contents {
  border-left: 1px solid var(--edge);
  padding: var(--deck-top) var(--space-5) var(--space-7);
  min-width: 0;
  align-self: start;
  position: sticky;
  top: 40px;
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}

.contents__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--edge);
}

.contents__list,
.contents__sub {
  list-style: none;
  margin: 0;
  padding: 0;
}

.contents__group + .contents__group { margin-top: var(--space-2); }

.contents__heading {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  margin: var(--space-3) 0 var(--space-1);
}
.contents__heading--done { color: var(--ink-faint); }

.contents__sub { margin-left: var(--space-2); }

.contents__link {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-1) var(--space-2);
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--ink-faint);
  text-decoration: none;
  border-left: 2px solid transparent;
}
.contents__link:hover,
.contents__link:focus-visible { color: var(--ink); background: var(--surface-void); }

/* Answered reads quieter than outstanding, which is the way round that makes
   the bar useful: what is left is what an operator is looking for. */
.contents__link--done .contents__text { color: var(--ink-faint); }
.contents__link--done .contents__mark { color: var(--amber); }

.contents__link--here {
  color: var(--amber);
  border-left-color: var(--amber);
  background: var(--surface-void);
}
.contents__link--here .contents__text { color: var(--amber); }

.contents__link--alone { font-size: 0.75rem; }

.contents__mark { flex: 0 0 auto; font-family: var(--font-mono); }
.contents__text { min-width: 0; overflow-wrap: anywhere; }

/* The two ways of taking an answer back, in the bar. Drawn as the bar's own
   type rather than as buttons: they are available, not wanted, and a filled
   control up here would outshout Continue. */
.terminal__tools {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.terminal__tool {
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--ink-faint);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  cursor: pointer;
}
.terminal__tool:hover,
.terminal__tool:focus-visible {
  color: var(--amber);
  border-bottom-color: var(--amber);
}

/* One of the two cannot be undone, and says so by the colour it turns. */
.terminal__tool--grave:hover,
.terminal__tool--grave:focus-visible {
  color: var(--rose);
  border-bottom-color: var(--rose);
}

.terminal__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--edge);
  background: var(--surface-void);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------------------
   The question itself
   --------------------------------------------------------------------------- */
.prompt {
  font-size: 1.375rem;
  line-height: 1.25;
  text-transform: none;
  letter-spacing: 0;
  margin: var(--space-2) 0 var(--space-3);
}
.prompt__note {
  color: var(--ink-quiet);
  font-size: 0.8125rem;
  margin-bottom: var(--space-5);
}

.field { display: grid; gap: var(--space-2); }
.field + .field { margin-top: var(--space-5); }
.field__hint { color: var(--ink-faint); font-size: 0.75rem; margin: 0; }
.field__error { color: var(--rose); font-size: 0.8125rem; margin: 0; }
.field--invalid input,
.field--invalid select { border-color: var(--rose); }

/* The native control is replaced wholesale. Its chrome is drawn by the
   operating system and cannot be brought into the palette, so the arrow is
   supplied here as a background image and the box is styled as an input. */
select {
  width: 100%;
  appearance: none;
  background-color: var(--surface-void);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0h10L5 6z' fill='%23F5A623'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
  background-size: 10px 6px;
  border: 1px solid var(--edge);
  border-radius: 0;
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  padding: var(--space-3);
  /* Room for the arrow, so that a long option does not run underneath it. */
  padding-right: var(--space-7);
}
select:hover { border-color: var(--edge-strong); }
select:focus { border-color: var(--amber); outline: none; }

/* The open list is drawn by the operating system. Only its colours can be
   influenced, and only on some platforms, so this is an improvement where it
   lands rather than a guarantee. */
select option { background: var(--surface-panel); color: var(--ink); }

/* Full width, because nearly every one of these is a field in a column of
   fields. A box meant to hold two or three digits has to say so with the
   type in its own selector: an attribute selector outweighs a class, so a
   plain .something { width: 4rem } loses to this and renders full width.
   See balance__amount and the dice boxes for the shape that works. */
input[type="date"],
input[type="number"] {
  width: 100%;
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  padding: var(--space-3);
}
input[type="date"]:focus,
input[type="number"]:focus { border-color: var(--amber); outline: none; }

/* The calendar button is a fixed dark glyph supplied by the browser. The
   filter is an approximation of the amber, arrived at by inversion, which is
   the only lever available for it. */
input[type="date"]::-webkit-calendar-picker-indicator {
  cursor: pointer;
  filter: invert(63%) sepia(72%) saturate(560%) hue-rotate(347deg)
          brightness(101%) contrast(96%);
}

/* ---------------------------------------------------------------------------
   A field with its derived readouts alongside it
   --------------------------------------------------------------------------- */
.row {
  display: grid;
  gap: var(--space-4);
  align-items: start;
  /* Stacked by default. The wider arrangements are the exception and are
     asked for by a media query rather than being undone by one. */
  grid-template-columns: minmax(0, 1fr);
}

/* The stacking rule between fields is for a column. Once they sit in a row it
   would push each one down a line on its own. */
@media (min-width: 620px) {
  .row--derived { grid-template-columns: minmax(0, 1fr) 7rem 11rem; }
  .row--derived .field + .field { margin-top: 0; }
}

/* A figure the terminal worked out rather than something to fill in. Built
   to the same measurements as an input so the row lines up, and set in cyan,
   which on this interface means the machine talking about itself. */
/* A plan the register will not issue. The flash is the terminal objecting;
   the actual refusal happens on submit, where it can say why. */
.readout--refused {
  border-style: solid;
  border-color: var(--rose);
  color: var(--rose);
  animation: readout-refused 1.1s steps(1, end) infinite;
}
@keyframes readout-refused {
  0%, 55% { background: var(--surface-deep); }
  55.01%, 100% { background: rgb(226 86 111 / 0.16); }
}

/* Held solid for anyone who has asked their system not to animate things. The
   colour already carries the objection; the flash only raises its voice. */
@media (prefers-reduced-motion: reduce) {
  .readout--refused {
    animation: none;
    background: rgb(226 86 111 / 0.16);
  }
}

.readout {
  display: block;
  width: 100%;
  padding: var(--space-3);
  border: 1px dashed var(--edge);
  background: var(--surface-deep);
  color: var(--cyan);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  text-align: center;
}

/* Two and three across, for questions answered as one description. */
@media (min-width: 620px) {
  .row--pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .row--triple { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .row--pair .field + .field,
  .row--triple .field + .field { margin-top: 0; }
}

/* A block following another block of fields, which the field stacking rule
   cannot reach across. */
.stack { margin-top: var(--space-5); }

/* ---------------------------------------------------------------------------
   A list with no fixed length
   --------------------------------------------------------------------------- */
.repeat { display: grid; gap: var(--space-2); }

.repeat__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2);
}

.repeat__drop {
  display: grid;
  place-items: center;
  width: 2.875rem;
  padding: 0;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--ink-faint);
}
.repeat__bin {
  width: 18px;
  height: 18px;
}
.repeat__drop:hover {
  background: transparent;
  border-color: var(--rose);
  color: var(--rose);
}

.repeat__add {
  justify-self: start;
  margin-top: var(--space-3);
  border: 1px dashed var(--edge-strong);
  padding-inline: var(--space-4);
}
.repeat__add:hover {
  background: transparent;
  border-color: var(--amber);
  color: var(--amber);
}

/* ---------------------------------------------------------------------------
   A field that checks itself while it is being filled in
   The state lives on the wrapper as a data attribute, set by script, and
   everything below is driven from it. Nothing here decides anything: the mark
   is decoration and the status line carries the meaning.
   --------------------------------------------------------------------------- */
.verify { position: relative; }

/* Room for the mark, so a long handle does not run underneath it. */
.verify input[type="text"] { padding-right: var(--space-7); }

.verify__mark {
  position: absolute;
  top: 50%;
  right: var(--space-3);
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  font-size: 0.875rem;
  line-height: 1;
}

[data-state="free"] .verify__mark::after {
  content: "\2713";
  color: var(--green);
}
[data-state="taken"] .verify__mark::after {
  content: "\2715";
  color: var(--rose);
}
[data-state="unknown"] .verify__mark::after {
  content: "?";
  color: var(--ink-faint);
}

/* The wait. A ring rather than a spinner of dots, because it reads at this
   size and costs one element. */
[data-state="checking"] .verify__mark::after {
  content: "";
  width: 14px;
  height: 14px;
  border: 2px solid var(--edge-strong);
  border-top-color: var(--amber);
  border-radius: 50%;
  animation: verify-spin 0.7s linear infinite;
}
@keyframes verify-spin {
  to { transform: rotate(360deg); }
}

/* A spinner is exactly what a reduced-motion request is about, so it stops
   turning and holds still as a plain ring. */
@media (prefers-reduced-motion: reduce) {
  [data-state="checking"] .verify__mark::after { animation: none; }
}

[data-state="free"] input[type="text"] { border-color: var(--green-deep); }
[data-state="taken"] input[type="text"] { border-color: var(--rose); }

.verify__status {
  margin: 0;
  font-size: 0.75rem;
  min-height: 1.2em;
  color: var(--ink-faint);
}
[data-state="free"] .verify__status { color: var(--green); }
[data-state="taken"] .verify__status { color: var(--rose); }

/* ---------------------------------------------------------------------------
   Small print attached to a step's answers
   Set apart from the fields and from the controls, so that it reads as the
   terminal adding a condition rather than as another thing to fill in.
   --------------------------------------------------------------------------- */
.notes {
  margin-top: var(--space-5);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-void);
  border-left: 2px solid var(--edge-strong);
}
.notes__label {
  margin: 0 0 var(--space-1);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.notes__body + .notes__body { margin-top: var(--space-2); }
.notes__body {
  margin: 0;
  max-width: none;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------------------
   A question offering a list, with the dice as an alternative to deciding
   --------------------------------------------------------------------------- */
/* A grid takes the full width, so its Roll goes on the label's line at the far
   end, which is where the personality grid's own Roll sits. Beside the grid it
   stood one line tall against four rows of tiles and stole the width the
   answers needed to hold their own words. */
.field__label--rolled {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  /* It is a paragraph rather than a label, because it holds a control as well
     as a name, so it has to be told what a label looks like here. */
  margin: 0 0 var(--space-2);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.field__label--rolled .choice__roll { margin-top: 0; }

/* Long answers wrap inside their tile rather than running out of it. */
.tile__name { overflow-wrap: anywhere; }

.choice {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
}
.choice select { flex: 1 1 auto; min-width: 0; }

/* Quiet, because rolling is the alternative to the question rather than the
   answer to it, and should not read as the thing to press. */
.choice__roll {
  flex: none;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--ink-quiet);
  font-weight: 400;
  text-transform: uppercase;
  padding-inline: var(--space-4);
}
.choice__roll:hover {
  background: transparent;
  border-color: var(--amber);
  color: var(--amber);
}

/* Three slots: back, the way home, and the action. The outer two take what
   they need and the middle one sits on the row's centre line whatever their
   labels are, rather than being centred between them. A row with nothing to
   put in a slot supplies an empty span, so the centre stays where it is. */
.wizard__actions {
  display: grid;
  /* Three of one width. Sized to their labels, Continue was half the width of
     Main Menu and the row read as an accident rather than as a set of
     choices. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: center;
  gap: var(--space-3);
  /* Deliberately generous. Continue is irreversible in feel if not in fact,
     and should not sit within a thumb's width of the last answer. */
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid var(--edge-faint);
}
/* Each fills its column, and the label sits in the middle of it. A .btn is
   an inline flex row, which packs its contents to the start unless told
   otherwise, so widening one alone would have left every label hard left. */
.wizard__actions > * {
  width: 100%;
  justify-content: center;
  text-align: center;
}

/* An action of lesser weight than the primary one, outlined rather than
   filled: the same shape and the same size, and plainly the lesser of the
   two. Backward navigation in the wizard, and the controls on a card.

   Scoped to .wizard__actions until the record grew buttons of its own, which
   meant every other page asking for a quiet button was handed the loud one
   and nobody noticed, because the markup said what it wanted and only the
   stylesheet disagreed. */
.btn--quiet {
  background: transparent;
  border-color: var(--edge-strong);
  color: var(--ink-bright);
}
.btn--quiet:hover {
  background: transparent;
  border-color: var(--amber);
  color: var(--amber);
}

/* An icon set beside a label, drawn in whatever colour the control is. */
.btn__icon {
  width: 0.875rem;
  height: 0.875rem;
  /* Buttons align on the baseline, which an svg has no notion of, so it is
     nudged onto the text's rather than sitting above it. */
  align-self: center;
  flex: none;
}

/* Secondary controls sit together on the left, so that the primary action
   keeps the right-hand side to itself however many of them there are. */
.wizard__actions-side {
  display: flex;
  align-items: baseline;
  gap: var(--space-5);
}

/* ---------------------------------------------------------------------------
   Confirmation
   Shown in place of the primary action when an irreversible step has been
   asked for and not yet agreed to. Framed rather than floated, because it is
   part of the document being confirmed.
   --------------------------------------------------------------------------- */
.confirm {
  margin-top: var(--space-7);
  border: 1px solid var(--amber);
  background: var(--surface-raised);
  padding: var(--space-5);
}
.confirm__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
  margin-bottom: var(--space-3);
}
.confirm__body {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink);
  margin-bottom: var(--space-3);
}
.confirm .wizard__actions {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
}

/* ---------------------------------------------------------------------------
   Advertising window
   A window drawn inside the page rather than opened as one, which is the only
   way an advert reliably appears at all. It has a title bar, it has the three
   controls a window has, and the one that would close it does nothing. That
   is the joke and it is also the point.
   --------------------------------------------------------------------------- */
.gate > .popup {
  max-width: var(--panel-wide);
  padding: 0;
  background: var(--surface-panel);
  border: 1px solid var(--edge-strong);
  /* Lifted off the page, because a window is a thing sitting on top of
     something else rather than a panel set into it. */
  box-shadow: 0 24px 60px rgb(0 0 0 / 0.55);
}

.popup__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--edge-strong);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}

/* A way out of a box from its own title bar, such as Local News on the pool
   feed or Full Report on the outlook. The same colour as the title, so the
   bar reads as one line, and brighter under the pointer, so it reads as a
   control. Not a button: it opens a window rather than doing anything. */
.popup__mark {
  flex: none;
  color: var(--amber-dim);
  text-decoration: none;
  white-space: nowrap;
}

.popup__mark:hover,
.popup__mark:focus-visible { color: var(--amber-bright); }

.popup__controls {
  display: flex;
  gap: var(--space-1);
  flex: none;
}
.popup__button {
  display: grid;
  place-items: center;
  width: 1.25rem;
  height: 1.25rem;
  border: 1px solid var(--edge);
  color: var(--ink-faint);
  font-size: 0.75rem;
  line-height: 1;
}

/* The close control. It is drawn, it is greyed, and it is not a button. */
.popup__button--dead {
  color: var(--edge-strong);
  border-color: var(--edge-faint);
}

.popup__body { padding: var(--space-5); }

.popup__slug {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--space-3);
}

/* The advert itself. A fixed shape whether or not there is anything to put
   in it, so the window does not resize when one loads or fails to. */
.popup__screen {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--surface-void);
  border: 1px solid var(--edge);
  object-fit: contain;
}
.popup__screen--empty {
  display: grid;
  place-content: center;
  gap: var(--space-2);
  padding: var(--space-5);
  text-align: center;
  color: var(--ink-quiet);
  font-size: 0.875rem;
}
.popup__fault { color: var(--ink-faint); font-size: 0.75rem; max-width: 36ch; }

.popup__sound {
  margin-top: var(--space-3);
  background: transparent;
  border-color: var(--edge);
  color: var(--ink-quiet);
  font-size: 0.6875rem;
  padding: var(--space-1) var(--space-3);
}
.popup__sound:hover {
  background: transparent;
  border-color: var(--amber);
  color: var(--amber);
}

.popup__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--edge-faint);
}

/* Leaving early is not the primary action and is not dressed as one, but it
   is not hidden either: the price is on the control, because a charge an
   operator did not see coming is a different joke and a worse one. */
.popup__skip { color: var(--rose); }
.popup__skip:hover { color: var(--rose); text-decoration: underline; }

/* The one line in either window that is a threat rather than small print,
   so it is the one line that is not grey. */
.popup__warning {
  margin-top: var(--space-4);
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--rose);
}
.courtesy__warning {
  margin-top: var(--space-3);
  margin-bottom: 0;
  /* The global cap on paragraph width is a reading measure, and at this size
     it is narrower than the window. Left in place it centres the line inside
     a box that is itself sitting against the left edge, which looks exactly
     like text-align doing nothing. */
  max-width: none;
  font-size: 0.6875rem;
  text-align: center;
}

.popup__terms {
  margin-top: var(--space-4);
  font-size: 0.6875rem;
  line-height: 1.6;
  color: var(--ink-faint);
  text-align: justify;
}

/* ---------------------------------------------------------------------------
   Modal notice
   A one-shot announcement laid over the page beneath it. Dismissal is an
   ordinary link, because the notice has already been taken out of the session
   by the time it is displayed and reloading is enough to clear it.
   --------------------------------------------------------------------------- */
.modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  background: rgb(0 0 0 / 0.78);
}
.modal__panel {
  width: 100%;
  max-width: 460px;
  background: var(--surface-panel);
  border: 1px solid var(--amber);
  padding: var(--space-6);
}
.modal__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--space-3);
}
.modal__heading {
  font-size: 1rem;
  line-height: 1.4;
  color: var(--amber);
  margin-bottom: var(--space-4);
}
.modal__body {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink-quiet);
  margin-bottom: var(--space-3);
}
/* The billing notice runs longer than the intrusion one and reads as a
   statement rather than an alarm, so its heading is set at the size of a
   figure being quoted at you. */
.modal__panel--charge { max-width: 480px; }
.modal__panel--charge .modal__heading { font-size: 1.125rem; }

.modal__actions {
  margin-top: var(--space-5);
  display: flex;
  justify-content: flex-end;
}

/* ---------------------------------------------------------------------------
   The deck
   The terminal's two panels. The side carries what the terminal has to say
   about itself and is the same on every screen; the main panel beside it is
   where the modules go. The rule between them runs the height of the window
   rather than the height of the side's contents, so it reads as the edge of
   a panel rather than as an underline that turned a corner.
   --------------------------------------------------------------------------- */
.deck {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  min-height: calc(100vh - 40px);
}

/* The two panels open at the same height, so the pool feed and the
   advertising hoarding beside it sit on one line across the terminal. Held as
   a token rather than as the same number written twice, because two panels
   that are meant to be level should not be able to drift apart. */

.deck__side {
  /* The height the feed and the outlook both take once their scripts are
     running. Declared here rather than on either of them, because the two
     boxes are required to match and a figure written down twice is a figure
     that drifts.

     Written as the sum of what the taller of the two actually contains,
     rather than as a count of lines. A count is a guess, and this one was
     wrong twice in both directions: three clipped the tides and five left a
     quarter of the box empty.

     The taller is the outlook. A card is its dated heading, the padding
     under it, the dashed rule, the gap below that and two readings. The
     feed's longest bulletin is three lines, which is less, so it takes the
     outlook's figure and sits level with it. */
  --ticker-line: 1.3rem;      /* 0.8125rem of type at 1.6 line-height. */
  --outlook-head: 1.0563rem;  /* 0.8125rem at 1.3, the heading being one line. */
  --reel-height: calc(
    var(--outlook-head) + var(--space-2) + 1px + var(--space-2) + var(--ticker-line) * 2
  );

  display: grid;
  align-content: start;
  gap: var(--space-4);
  padding: var(--deck-top) var(--space-5) var(--space-7) var(--gutter);
  border-right: 1px solid var(--edge);
}

/* The character panel decides how wide the side is, being the widest thing
   in it. The feed and the session controls therefore contribute nothing to
   that measurement and simply fill whatever it comes to, which is what makes
   all three share one right edge without a width being written down. */
.deck__side > .ticker,
.deck__side > .outlook,
.deck__side > .console {
  max-width: 0;
  min-width: 100%;
}
.deck__side > * { margin: 0; }

/* The advertising on the left at its own fixed width, and the social panel
   filling everything between its right edge and the edge of the terminal.
   The width is a token because both the column and the box inside it need
   it, and two places holding the same number is two places to change. */
.deck__main {
  --advert-width: 520px;

  display: grid;
  grid-template-columns: var(--advert-width) minmax(0, 1fr);
  grid-template-rows: auto auto;
  gap: var(--space-6);
  /* The row is only as tall as its tallest column, so the panel ends where the
     advertising ends rather than being drawn down the viewport by the deck's
     minimum height. */
  align-content: start;
  /* Stretched rather than started, so the boards frame takes the height the
     advertising sets and the two read as one row of the terminal. */
  align-items: stretch;
  padding: var(--deck-top) var(--gutter) var(--space-6);
}

/* Placed rather than flowed. The boards frame is required to match the
   advertising's height, which it can only do by sharing its row, so the
   fortune takes the row underneath and the left column alone. */
.courtesy { grid-area: 1 / 1; }
.social { grid-area: 1 / 2; }

/* The card, the resources and the radio in one row under the advertising
   and the boards. Separate boxes, because a fortune, a set of reference links
   and a radio are three things and one box holding them would be three things
   wearing one label. The row spans both columns so the radio can follow the
   resources at the same gap the resources keep from the card, and it wraps
   rather than overflowing where the panel is too narrow for all three. */
.deck__pair {
  grid-area: 2 / 1 / 3 / 3;
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  gap: var(--space-4);
}

/* ---------------------------------------------------------------------------
   Courtesy advertising
   A hoarding in the corner of the main panel: muted, continuous, and free to
   pause, unmute or skip. It borrows the advertising window's title bar,
   because it is the same company saying the same thing in a smaller voice.
   --------------------------------------------------------------------------- */
.courtesy {
  /* The column it sits in is sized to this, so the box simply fills it and
     the number lives in one place. */
  width: 100%;
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

.courtesy__body { padding: var(--space-3); }

.courtesy__screen {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--surface-void);
  border: 1px solid var(--edge-faint);
  object-fit: contain;
}
/* Everything refused, which means a content blocker rather than bad files.
   The frame stays, the screen is hidden, and the notice under it explains
   itself rather than the corner of the page sitting black. */
.courtesy--unavailable .courtesy__screen,
.courtesy--unavailable .courtesy__controls { display: none; }
.courtesy--unavailable .courtesy__body::after {
  content: "Advertising could not be displayed. Your connection may be filtering it.";
  display: grid;
  place-content: center;
  aspect-ratio: 16 / 9;
  padding: var(--space-5);
  border: 1px solid var(--edge-faint);
  background: var(--surface-void);
  text-align: center;
  font-size: 0.8125rem;
  color: var(--ink-faint);
}

.courtesy__screen--empty {
  display: grid;
  place-content: center;
  padding: var(--space-5);
  text-align: center;
  font-size: 0.8125rem;
  color: var(--ink-faint);
}

/* Small, quiet controls. They are conveniences rather than actions, and
   should not carry the weight of the buttons in the side panel. */
.courtesy__controls {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.courtesy__control {
  flex: 1;
  justify-content: center;
  background: transparent;
  border-color: var(--edge);
  color: var(--ink-quiet);
  font-size: 0.6875rem;
  padding: var(--space-2) var(--space-3);
}
.courtesy__control:hover {
  background: transparent;
  border-color: var(--amber);
  color: var(--amber);
}

/* Nothing to show yet, said plainly. An empty panel with no explanation
   reads as something that failed to load. */
.deck__empty { max-width: var(--measure); }

/* ---------------------------------------------------------------------------
   The social half
   Thirds, as the terminal is meant to read: the boards and their traffic take
   the left two, and who is on the pool takes the right. Below the fold of a
   narrow window they stack, boards first, because the boards are the thing
   somebody came for.
   --------------------------------------------------------------------------- */
/* One frame holding the whole of it: the boards, who is on the pool, and the
   traffic across every board.

   Laid out in rows so that the feed takes whatever height is left once the
   boards have had theirs, and scrolls inside it. The frame's own height comes
   from the advertising beside it, so without this the traffic would either
   stretch the box past its neighbour or leave it half empty. */
.social {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

/* Everything under the title bar. The padding lives here rather than on the
   frame, so the bar runs to the frame's edges as it does on every other box.
   min-height is what lets this row, and the feed inside it, shrink below
   their content and scroll rather than push the frame taller. */
.social__body {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  min-height: 0;
  padding: var(--space-4);
}

/* The boards take the room they need and the pool takes a narrow column
   beside them, running the height of the grid rather than sitting as a strip
   at the top of it. */
.social__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 11rem);
  gap: var(--space-3);
}

/* Set into the panel rather than sitting on it, so it reads as part of the
   frame rather than as a seventh board. */
.social__pool {
  padding: var(--space-3) var(--space-4);
  background: var(--surface-void);
  border: 1px solid var(--edge-faint);
  /* Laid out as a column so the list below the heading can take whatever
     height is left and scroll inside it, rather than growing the panel and
     pushing the boards beside it out of line. */
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* The handles, which scroll once there are more of them than the column
   beside the boards can hold. The panel is as tall as the boards are, and a
   list that grew it would push the posts underneath further down the page
   every time somebody new finished a character.

   Not called pool, though the panel is: the point buy took that name first,
   and it is a flex row. Inheriting it laid the handles out side by side and
   wrapped each of them a letter to a line.

   One handle per row, stated rather than left to the list, because a flex
   rule elsewhere taking this name again would otherwise do the same thing
   twice. */
.operators {
  display: flex;
  flex-direction: column;
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  min-height: 0;
  overflow-y: auto;
}

/* Cyan, as every other name a person is called by on this terminal is: the
   signature on a post, the operator on a message, the handle on the wire. */
.operators__way {
  display: block;
  padding: var(--space-1) 0;
  color: var(--cyan);
  font-size: 0.8125rem;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.operators__way:hover,
.operators__way:focus { color: var(--amber); }

.operators__none {
  margin: var(--space-2) 0 0;
  color: var(--ink-faint);
  font-size: 0.75rem;
}

/* The backstory as somebody else reads it: the same room under the handle
   that the box takes on the owner's own page, printed rather than typed
   into. */
.backstory--read { margin-top: var(--space-4); }

.backstory__said {
  margin: var(--space-2) 0 0;
  color: var(--ink-quiet);
  font-size: 0.875rem;
  line-height: 1.6;
}

/* The boards carry a top margin of their own, for the two pages where they
   sit under a heading. Here they sit beside the pool instead, and the margin
   started them a step below it, so the two columns of one row began on
   different lines. The row is the only thing that knows they are meant to be
   level, so the row is where it is taken off. */
.social__row .boards { margin-top: 0; }

/* Two classes rather than one, and deliberately. This element also carries
   .eyebrow, whose margin shorthand is declared further down the sheet and
   therefore wins at equal weight: written as .social__since alone, this rule
   was never applied and the caption sat hard against the boards above it. */
.eyebrow.social__since {
  /* A rule between the boards and the feed, with the same room either side
     of it, so the caption reads as opening the posts rather than as a label
     trailing off the buttons. The same faint rule the posts are divided by. */
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--edge-faint);
  /* Released from the reading measure every paragraph is held to, or the
     rule stops two thirds of the way across the panel. */
  max-width: none;
}


/* The way into each board. Address above, name below, so the eye lands on the
   thing a character would actually say out loud. */
/* Two across and three down, fixed rather than reflowing. Six boards is the
   shape this is meant to be read in, and a grid that rearranged itself as the
   window moved would put a different board under the pointer each time. */
/* Two columns filled downwards rather than across. Filled across, the table's
   order interleaves the two columns and the grouping the order is there to
   express is lost. */
.boards {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(3, auto);
  grid-auto-flow: column;
  gap: var(--space-2);
}
.boards__way {
  display: grid;
  gap: 2px;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  text-decoration: none;
  color: var(--ink-quiet);
}
.boards__way:hover {
  border-color: var(--amber);
  color: var(--amber);
}
.boards__way--here {
  border-color: var(--amber);
  background: var(--surface-raised);
  color: var(--amber);
}
.boards__address {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  color: inherit;
  /* An address has no spaces in it, so a long one has nowhere a browser is
     willing to break and runs straight out through the side of its tile.
     Breaking anywhere is the only thing that holds it in, and a board
     addressed at length is a thing the referee can invent at any time. */
  overflow-wrap: anywhere;
}
.boards__name {
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: var(--track-wide);
  line-height: 1.4;
  color: var(--ink-faint);
}

/* The traffic, on the landing page and on the board index alike. */
.feed,
.thread,
.replies {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
}

/* The traffic on the landing page scrolls within whatever the frame has left
   for it. min-height is what lets a grid row shrink below its content, which
   is the whole of why it scrolls rather than pushing the box taller. */
.social .feed {
  min-height: 0;
  overflow-y: auto;
  padding-right: var(--space-2);
}
.feed__row,
.thread__row { padding: var(--space-3) 0; }

/* A thread's posts are divided by a rule. The latest posts are not: there are
   few of them, and the rule above their caption already sets them apart. */
.thread__row { border-bottom: 1px solid var(--edge-faint); }
.feed__heading,
.thread__heading {
  display: block;
  font-size: 0.8125rem;
  color: var(--ink);
  text-decoration: none;
}
.feed__heading:hover,
.thread__heading:hover { color: var(--amber); }
.feed__by,
.thread__by {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: var(--space-1) 0 0;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}
.feed__board { color: var(--amber-dim); }
.feed__signature,
.thread__signature { color: var(--cyan); }
.feed__row--empty p,
.thread__row--empty p,
.replies__row--empty p {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--ink-faint);
}

/* A post, and the replies under it. */
.written__body {
  margin-top: var(--space-4);
  padding: var(--space-4);
  background: var(--surface-panel);
  border-left: 2px solid var(--amber-dim);
}
.replies__row {
  padding: var(--space-3) 0 var(--space-3) var(--space-4);
  border-left: 1px solid var(--edge);
  border-bottom: 1px solid var(--edge-faint);
}
.written__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0 0 var(--space-2);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}
.written__signature { color: var(--cyan); }
.written__words {
  margin: 0;
  max-width: none;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--ink);
}

/* The box for adding to a board. */
.written { margin-top: var(--space-5); }
.written__as {
  margin: 0 0 var(--space-3);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}
.written__as b { color: var(--cyan); }
.written__actions { display: flex; justify-content: flex-end; }

/* Two classes rather than one, for the reason given on .social__since: the
   element also carries .eyebrow, whose margin shorthand is declared further
   down this sheet and wins at equal weight, so this rule written on its own
   never reached the page. */
.eyebrow.board__since { margin-top: var(--space-6); }

@media (max-width: 1500px) {
  /* The constraint is the main panel rather than the window: the character
     panel beside it takes some five hundred pixels before this one starts.
     Below this the advertising and the boards cannot sit side by side
     without the boards being squeezed, so they stack instead. */
  .deck__main { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 900px) {
  /* The pool drops under the boards rather than squeezing beside them. */
  .social__row { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 520px) {
  .boards { grid-template-columns: minmax(0, 1fr); }
}
.deck__empty-line {
  margin-top: var(--space-3);
  font-size: 0.9375rem;
  color: var(--ink);
}
.deck__empty-note {
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--ink-faint);
}

/* Below the width where two panels fit, the side stops being a column and
   becomes the top of the page, with the rule turned underneath it. */
@media (max-width: 900px) {
  .deck {
    grid-template-columns: minmax(0, 1fr);
    min-height: 0;
  }
  .deck__side {
    border-right: 0;
    border-bottom: 1px solid var(--edge);
    justify-content: start;
    padding-right: var(--gutter);
  }
}

/* ---------------------------------------------------------------------------
   Pool feed
   A small bezelled panel in the corner under the status bar, showing one line
   at a time. It is the terminal talking about itself rather than anything the
   operator asked for, so it sits outside the content column and hard against
   the left, lined up with the status bar above it.

   Every line is in the markup; the script hides all but one, so with no
   script this is a bulletin list rather than an empty box.
   --------------------------------------------------------------------------- */
.ticker {
  /* The panel is a fixed size, because a box that resized itself every four
     seconds as the feed moved between a short line and a wrapped one would
     drag the whole page up and down with it.

     Short lines sit centred in the space rather than leaving it at the top.
     The width is the side panel's, which the character panel below sets, and
     the height comes from the two figures the column declares. */
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}


.ticker__reel {
  list-style: none;
  margin: 0;
  padding: var(--space-4);
}
.ticker__line {
  font-size: 0.8125rem;
  line-height: 1.6;
}

/* The label is the feed announcing what sort of line this is, in the
   terminal's own amber; everything after it is what the line actually says,
   and is left plain. Two colours on every line, so the prefix is always
   distinguishable from the message. */
.ticker__label {
  color: var(--amber);
  text-transform: uppercase;
  letter-spacing: var(--track-wide);
}
.ticker__text { color: var(--ink-bright); }

/* With two exceptions, and both are the palette's own rules rather than this
   panel's. Cyan is reserved for system state, so the lines where the terminal
   is reporting on itself carry it and the bulletins do not: the uptime is
   state, the Body Count Lottery is news. Rose is reserved for failure, so the
   fault takes it outright, label and all. */
.ticker__line--state .ticker__text { color: var(--cyan); }
.ticker__line--fault .ticker__label,
.ticker__line--fault .ticker__text { color: var(--rose); }

/* The one line the feed does not survive. Neon rather than rose: brighter
   than anything else on the terminal, lit from inside, and holding twice as
   long as its neighbours before the fault that follows it.

   The glow is three shadows of the same colour at increasing blur, which is
   how a tube looks and what a flat colour cannot do. */
.ticker__line--takeover .ticker__label,
.ticker__line--takeover .ticker__text {
  color: var(--neon);
  text-transform: uppercase;
  letter-spacing: var(--track-wide);
  text-shadow:
    0 0 4px currentColor,
    0 0 14px rgb(255 51 85 / 0.75),
    0 0 34px rgb(255 51 85 / 0.45);
}
.ticker--running .ticker__line--takeover {
  animation: takeover 9s linear forwards;
}

/* Strikes, catches, holds, and goes out. The flicker at the start is a tube
   deciding whether it is going to work. */
@keyframes takeover {
  0% { opacity: 0; }
  3% { opacity: 1; }
  5% { opacity: 0.2; }
  7% { opacity: 1; }
  9% { opacity: 0.35; }
  12% { opacity: 1; }
  82% { opacity: 1; }
  100% { opacity: 0.1; }
}

/* Somebody who has asked their system not to animate things gets the neon
   and none of the flickering, which is the part that would bother them. */
@media (prefers-reduced-motion: reduce) {
  .ticker--running .ticker__line--takeover { animation: none; }
}

/* With no script every line is on screen at once, so they stack and want
   separating, and the panel is as tall as the bulletin. That is the fallback
   and it is allowed to be. Once the script is running only one line is ever
   shown, and the reel takes the fixed height above. */
.ticker__line + .ticker__line { margin-top: var(--space-3); }
.ticker--running .ticker__reel {
  display: flex;
  align-items: center;
  height: calc(var(--reel-height) + var(--space-4) * 2);
  overflow: hidden;
}
.ticker--running .ticker__line { margin-top: 0; }

/* The two session controls, sitting under the feed and sharing its width.
   Two equal columns, so the shorter label stretches to match the longer one
   rather than the pair reading as a primary action and an afterthought. The
   sign-out control is wrapped in its own form, which is why the width is set
   on the buttons rather than on the grid's children. */
.console {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}
/* The sign-out control is a button inside a form, and the other is a bare
   anchor, so the two are not siblings and do not size against each other on
   their own: the anchor stretches to the row while the button sizes to its
   own text. Making the form a grid passes the row's height through to the
   button inside it, and the pair match whether or not a label wraps. */
.console form { display: grid; }
.console .btn,
.console button {
  width: 100%;
  height: 100%;
  align-items: center;
  justify-content: center;
  text-align: center;
}

/* ---------------------------------------------------------------------------
   Character panel
   The landing page's one piece of content: the character as the terminal
   holds them. Art on the left, who they are beside it, and the figures that
   change during play under those.
   --------------------------------------------------------------------------- */
/* The panel is as wide as the two things in it and no wider: the art's
   column, the gap, and the Role grid. Sizing it to its content rather than to
   a figure is what lets the controls and the figures beneath span exactly
   from the art's left edge to the rank table's right one, without either
   measurement being written down anywhere. */
.profile {
  --art-width: 200px;

  display: grid;
  grid-template-columns: var(--art-width) max-content;
  gap: var(--space-5);
  width: max-content;
  max-width: 100%;
}

/* The art's frame. There is nothing to put in it yet, so it holds its shape
   and says so rather than collapsing and letting the layout shift when one
   arrives. */
.profile__art {
  display: grid;
  place-items: center;
  aspect-ratio: 3 / 4;
  background: var(--surface-void);
  border: 1px solid var(--edge);
  padding: var(--space-4);
  text-align: center;
}
/* Filling the frame rather than fitting inside it, because the frame's three
   by four is the shape the record asks operators to supply and a picture that
   is not quite it should be cropped rather than letterboxed. */
.profile__portrait {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.profile__art-mark {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The Role grid decides how wide the details column is, so the two lines of
   text above it must not get a vote. Contributing nothing to the column's
   width and then filling whatever it turns out to be means a long handle or
   an elaborate full name wraps inside the panel rather than stretching it and
   dragging the figures out of line with the rank table. */
/* The details column is a column, so that the Role grid can be pushed to the
   bottom of it: the art is the taller of the two, and the grid's last row
   sitting on the art's bottom edge is what squares the pair off. */
.profile__detail {
  display: flex;
  flex-direction: column;
}
.profile__detail .ranks { margin-top: auto; }

.profile__handle,
.profile__name {
  max-width: 0;
  min-width: 100%;
}

.profile__handle {
  margin-bottom: var(--space-1);
  font-size: 1.5rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.profile__handle--empty { color: var(--ink-faint); }

.profile__name {
  margin-bottom: var(--space-5);
  font-size: 0.875rem;
  color: var(--ink-quiet);
}

/* The ten Roles, two columns of five, each with its rank in a box. Column
   flow rather than row flow, so the first five read down the left and the
   second five down the right, which is the order the rulebook lists them in.

   Named for what it holds rather than what it lists. The Role picker in
   creation is the .roles component; this is a table of ranks, and the two
   living under one name is how a fieldset on another page came to inherit
   width: max-content and cluster its tiles into one corner.

   The grid is as wide as its contents and no wider. A row is the longest
   Role name, four spaces, then the rank: the type is monospaced, so ch is
   exactly that measure and the columns line up without a single figure being
   guessed at. Rockerboy and Netrunner are the longest at nine. */
.ranks {
  --role-name: 9ch;
  --role-space: 4ch;
  --role-rank: 2.25rem;

  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  width: max-content;
  max-width: 100%;
  grid-template-columns: repeat(2, max-content);
  grid-template-rows: repeat(5, auto);
  grid-auto-flow: column;
  gap: var(--space-1) var(--space-5);
}
.ranks__row {
  display: grid;
  grid-template-columns: var(--role-name) var(--role-rank);
  align-items: center;
  gap: var(--role-space);
  padding: var(--space-1) 0;
  font-size: 0.8125rem;
  color: var(--ink-faint);
  border-bottom: 1px solid var(--edge-faint);
}

/* A Role the character actually holds is the point of the grid, so it is the
   one that is lit. The rest are there to show what they are not. */
.ranks__row--held { color: var(--ink); }
.ranks__row--held .ranks__rank {
  border-color: var(--amber);
  color: var(--amber);
}

.ranks__rank {
  border: 1px solid var(--edge);
  text-align: center;
  color: var(--ink-faint);
}

/* The controls and the figures run the full width of the panel rather than
   sitting in the details column, so the figures' left edge lines up with the
   art's above it. */
/* No top margin of its own: the panel's own row gap is the whole of the
   space between the art and the controls, which puts them directly under it
   and brings the figures up with them. */
.profile__console {
  grid-column: 1 / -1;
  display: grid;
  gap: var(--space-4);
}

/* Two equal halves of the row, so the controls match each other in width and
   in height and the gap between them falls on the centre line with nothing
   left over at either end. Their outer edges are the art's and the rank
   table's, which is where the figures below start and stop too. */
.profile__controls {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  align-items: stretch;
}
.profile__controls > * {
  width: 100%;
  justify-content: center;
  text-align: center;
}

/* Deletion is the one control here that cannot be undone, so it does not
   wear the colour every other action wears. */
.btn--danger {
  background: transparent;
  border-color: var(--rose);
  color: var(--rose);
}
.btn--danger:hover {
  background: var(--rose);
  color: var(--ink-invert);
}
.btn--danger.btn--disabled:hover {
  background: transparent;
  color: var(--rose);
}

.figures {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  width: 100%;
  margin: 0;
  background: var(--edge-faint);
  border: 1px solid var(--edge-faint);
}
.figures > div {
  background: var(--surface-void);
  padding: var(--space-3) var(--space-4);
}
.figures dt {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  margin-bottom: var(--space-1);
}
.figures dd {
  margin: 0;
  font-size: 1rem;
  color: var(--ink-bright);
}

/* Below the width where two columns are worth having, the art stops being a
   column and the whole panel reads as one. */
@media (max-width: 720px) {
  /* One column, sized by the widest thing in it, which is the Role grid.
     The art stretches to match rather than the grid overhanging it. */
  .profile { grid-template-columns: max-content; }
  .profile__art { aspect-ratio: 4 / 3; }
  .profile__console { grid-column: 1; }
  .ranks { grid-template-columns: minmax(0, 1fr); grid-auto-flow: row; }
}

/* ---------------------------------------------------------------------------
   Competencies
   Sixty-six rows, grouped as the rulebook groups them. Each is a name, the
   aptitude it is measured against, a level, the Base those come to, and a
   marker that opens what the Base is worth.
   --------------------------------------------------------------------------- */

/* What is left of the pool, held at the top of the panel while the operator
   scrolls the list, because it is the only figure that governs the rest. */
.budget {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--edge);
}
/* One size across the bar, as on the aptitude page. The figure was twice the
   size of the words either side of it, which made one sentence read as three
   statements; colour and weight carry the emphasis instead. */
.budget { font-size: 1rem; }
.budget__label {
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.budget__figure { color: var(--cyan); }
.budget__of { color: var(--ink-faint); }

/* Over the pool is a fault rather than a warning: the form will refuse it. */
.budget--over { border-color: var(--rose); }
.budget--over .budget__figure { color: var(--rose); }

/* Under the pool is only a fault where every point has to be placed, which is
   the Specialities page and nowhere else. Marked in amber rather than rose
   because an operator part way through an allocation has not done anything
   wrong yet, they have simply not finished. */
.budget--short { border-color: var(--amber-dim); }
.budget--short .budget__figure { color: var(--amber); }

/* The Specialities list reuses the skills rows without their aptitude column
   or their running Base: a Speciality is a count of points and nothing is
   rolled against it directly. */
.specialities .skills__row {
  grid-template-columns: minmax(0, 1fr) 3.5rem 1.5rem;
}

.skills__group { margin-bottom: var(--space-6); }

/* The group heading is the control that folds it, so it takes the whole
   width and says so on hover. The chevron is drawn here rather than with the
   browser's own marker, which cannot be positioned and differs between
   them. */
.skills__heading {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  cursor: pointer;
  list-style: none;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  margin-bottom: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--edge);
}
.skills__heading::-webkit-details-marker { display: none; }
.skills__heading:hover { color: var(--amber); }
.skills__heading:hover .skills__title { color: var(--amber); }

/* Turns a quarter when the group opens. */
.skills__heading::before {
  content: "\25B8";
  font-size: 0.75rem;
  line-height: 1;
  transition: transform 120ms ease;
}
.skills__group[open] > .skills__heading::before { transform: rotate(90deg); }

/* The heading is a summary now, so the name inside it is the h2 and carries
   none of the block layout a heading would bring with it. */
.skills__title {
  flex: 1;
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
}

/* What the group is costing, so that one folded away still says what is in
   it. Quiet until there is something to report. */
.skills__spend { color: var(--ink-faint); }
.skills__spend b { color: var(--cyan); }

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

/* Name, aptitude, level, Base, marker. The name takes what is left, the rest
   are fixed, so every row in every group lines up down the panel. */
.skills__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 3rem 3.5rem 2.5rem 1.5rem;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--edge-faint);
  font-size: 0.8125rem;
}

.skills__name { display: flex; align-items: baseline; gap: var(--space-2); }

/* The two things the rulebook marks, marked. Both are abbreviations with a
   title, so hovering says what they mean and a screen reader reads it. */
.skills__cost,
.skills__basic {
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--edge);
  padding: 0 var(--space-1);
  color: var(--ink-faint);
  cursor: help;
}
.skills__row--basic .skills__basic { border-color: var(--amber-dim); color: var(--amber-dim); }

.skills__stat {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  color: var(--ink-faint);
}

.skills__level {
  width: 100%;
  padding: var(--space-1) var(--space-2);
  text-align: center;
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

.skills__base { color: var(--cyan); text-align: right; }

/* The marker. A details element, so it opens and closes without a line of
   script and is reachable by keyboard for nothing. */
.skills__what { position: relative; justify-self: end; }
.skills__marker {
  display: grid;
  place-items: center;
  width: 1.25rem;
  height: 1.25rem;
  border: 1px solid var(--edge-strong);
  border-radius: 50%;
  color: var(--ink-faint);
  font-size: 0.6875rem;
  font-style: italic;
  cursor: pointer;
  list-style: none;
}
.skills__marker::-webkit-details-marker { display: none; }
.skills__what[open] .skills__marker { border-color: var(--amber); color: var(--amber); }

/* Laid over the list rather than pushing it apart, so opening one does not
   move every row below it. */
.skills__panel {
  position: absolute;
  right: 0;
  z-index: 3;
  width: min(30rem, 70vw);
  margin-top: var(--space-2);
  padding: var(--space-4);
  background: var(--surface-panel);
  border: 1px solid var(--edge-strong);
  box-shadow: 0 18px 40px rgb(0 0 0 / 0.5);
}
.skills__blurb {
  margin-bottom: var(--space-3);
  font-size: 0.8125rem;
  color: var(--ink);
}
.skills__bases { margin: 0; display: grid; gap: var(--space-2); }
.skills__bases > div {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: var(--space-3);
}
.skills__bases dt {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.skills__bases dd { margin: 0; font-size: 0.75rem; line-height: 1.5; color: var(--ink-quiet); }

/* A competency taken once per language, field, form or place takes the whole
   row and stacks its own. */
/* A competency taken more than once keeps its name, its aptitude and its
   marker in the same columns as every other row, and gives everything between
   them to the picker. The name column is capped rather than fluid so that the
   pickers line up down the page instead of each one starting wherever its
   competency's name happens to end. */
.skills__row--many {
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr) 1.5rem;
  align-items: start;
}
.skills__row--many .skills__name { padding-top: var(--space-1); }

.skills__many {
  display: grid;
  gap: var(--space-2);
}

/* The same four columns the rest of a row has, in the same order and at the
   same widths, so that the aptitude, the level and the Base line up down the
   page whether the competency was taken once or five times. */
.skills__take {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 3rem 3.5rem 2.5rem;
  align-items: center;
  gap: var(--space-3);
}
.skills__pick {
  width: 100%;
  padding: var(--space-1) var(--space-2);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.skills__add { justify-self: start; font-size: 0.6875rem; padding: var(--space-1) 0; }

/* Clearing work away should not look like the inviting thing to do, so these
   are outlined rather than filled and turn to the deletion colour under the
   pointer rather than brightening like everything else. */
.skills__reset {
  background: transparent;
  border-color: var(--edge-strong);
  color: var(--ink-quiet);
  font-size: 0.6875rem;
  padding: var(--space-1) var(--space-3);
}
.skills__reset:hover {
  background: transparent;
  border-color: var(--rose);
  color: var(--rose);
}

/* Held to the far end of the counter, away from the figure it would undo. */
.budget__reset { margin-left: auto; }

/* The section's own control, set below its rows and indented to the column
   the levels sit in rather than the names, so it reads as belonging to the
   section rather than heading the next one. */
.skills__foot {
  display: flex;
  justify-content: flex-end;
  padding: var(--space-3) 0 var(--space-1);
}

@media (max-width: 640px) {
  .skills__row { grid-template-columns: minmax(0, 1fr) 2.5rem 3rem 2rem 1.5rem; gap: var(--space-2); }
  .skills__panel { width: min(20rem, 80vw); }
}

/* ---------------------------------------------------------------------------
   People
   One group of friends, enemies or love affairs. Each entry is a face beside
   a stack of answers, and each group is allowed to be empty.
   --------------------------------------------------------------------------- */
.people { margin-bottom: var(--space-6); }

.people__entry + .people__entry {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px dashed var(--edge-faint);
}

/* The picture sits beside the answers on anything wide enough for both, and
   above them on a phone, where a column of 11rem of face would push the name
   off the screen. */
.person {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: flex-start;
}

/* Everything under the frame is centred on it: the control, the file name,
   the way to take it back and the advice, in that order. A column of
   left-aligned fragments beneath a square reads as debris. */
.person__face {
  flex: 0 0 11rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-2);
}

/* A fixed frame whether or not it holds anything, so that choosing a picture
   does not shunt every field below it down the page. */
.person__portrait,
.person__facehint {
  width: 11rem;
  height: 11rem;
  background: var(--surface-void);
  border: 1px solid var(--edge);
}
.person__portrait { object-fit: cover; }
.person__facehint {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}

/* Our own control, worn by a label standing in for the file input and by the
   button beside it. Both are the same object, so both are declared here. */
.person__button {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--amber);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-align: center;
  cursor: pointer;
}
.person__button:hover,
.person__button:focus-visible {
  background: var(--surface-panel);
  color: var(--ink);
}

/* The browser's own control, shown only where no script replaced it. Its
   button face is styled all the same, because it is what an operator
   without a script actually clicks. */
.person__file { width: 100%; font-size: 0.75rem; color: var(--ink); }
.person__file::file-selector-button,
.person__file::-webkit-file-upload-button {
  margin-right: var(--space-2);
  padding: var(--space-1) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--amber);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  cursor: pointer;
}
.person__file::file-selector-button:hover,
.person__file::-webkit-file-upload-button:hover {
  background: var(--surface-panel);
  color: var(--ink);
}

/* The one thing the browser will not let us restyle on its own control, so
   it is written out here instead: what the operator picked. */
.person__chosen {
  width: 100%;
  font-size: 0.6875rem;
  line-height: 1.4;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.person__drop {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* What the frame expects, said where it is asked for rather than in the
   footnotes, which nobody reads before choosing a file. */
.person__guidance {
  font-size: 0.6875rem;
  line-height: 1.4;
  color: var(--ink-faint);
}

.person__fields {
  flex: 1 1 22rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.person__field { display: block; }
.person__label {
  display: block;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  margin-bottom: var(--space-1);
}

.person__text {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.person__text--long { resize: vertical; }

/* An enemy carries five lists and a love affair one, so these wrap rather
   than sitting in fixed columns: a layout built for the worst case would
   leave the affairs mostly empty. */
.person__picks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.person__pick {
  flex: 1 1 13rem;
  min-width: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.person__pick--narrow { flex: 0 1 9rem; }

.people__add {
  margin-top: var(--space-4);
  font-size: 0.6875rem;
  padding: var(--space-1) 0;
}

@media (max-width: 640px) {
  .person__face { flex: 1 1 100%; }
  .person__portrait,
  .person__facehint { width: 100%; height: auto; aspect-ratio: 1 / 1; }
}

/* ---------------------------------------------------------------------------
   Official record
   The registration confirmation. Styled as a document the terminal has issued
   rather than as part of the interface: paler, squarer, bureaucratic.
   --------------------------------------------------------------------------- */
.record__head {
  padding: var(--space-6) var(--space-6) var(--space-5);
  border-bottom: 1px solid var(--edge);
}
.record__head h1 {
  font-size: 1.5rem;
  margin: var(--space-2) 0 var(--space-3);
}
.record__reference {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  color: var(--ink-faint);
  margin: 0;
}

/* The panel's own side padding, named here as well as applied, because a
   section heading inside it pulls itself back out by exactly this much so
   its rule reaches the panel's edges. Unnamed, that heading's whole margin
   and padding shorthand was invalid and dropped, which left the rule
   drawn hard against whatever sat above it. */
.record__body {
  --panel-x: var(--space-6);

  padding: var(--space-6);
}
.record__lede { color: var(--ink); margin-bottom: var(--space-5); }

.record__fields {
  display: grid;
  gap: 1px;
  background: var(--edge-faint);
  border: 1px solid var(--edge-faint);
  margin: 0 0 var(--space-5);
}
.record__fields > div {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: var(--space-3);
  background: var(--surface-void);
  padding: var(--space-2) var(--space-3);
}
.record__fields dt {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  align-self: center;
}
/* The values are set apart from their labels by colour, so that the record
   scans as a readout of what was given rather than as a block of prose. */
.record__fields dd {
  margin: 0;
  font-size: 0.875rem;
  color: var(--cyan);
}

/* The quiet part, said out loud. Deliberately set as small print, because
   that is exactly what it would be. */
.record__notice {
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--ink-faint);
  border-left: 2px solid var(--edge-strong);
  padding-left: var(--space-3);
  margin-bottom: var(--space-6);
}

.record__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--edge);
  background: var(--surface-void);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

@media (max-width: 480px) {
  .record__fields > div { grid-template-columns: minmax(0, 1fr); gap: 0; }
}

/* ---------------------------------------------------------------------------
   Small shared pieces
   Used across several screens, so declared once here rather than beside any
   one of them.
   --------------------------------------------------------------------------- */
.eyebrow,
.label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  margin: 0 0 var(--space-2);
}

/* Figures that are read down a column line up when they are the same width. */
.tabular { font-variant-numeric: tabular-nums; }

.alert {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  border-left: 2px solid var(--edge-strong);
  background: var(--surface-raised);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-5);
  font-size: 0.875rem;
}
.alert--fail { border-left-color: var(--rose); }
.alert--pass { border-left-color: var(--green); }

/* A caution rather than a refusal: something is about to be cleared, but
   nothing has gone wrong and nothing has been filed. */
/* The register's own entry rather than the terminal's small print, so it
   reads as something worth reading instead of something to skip.

   Fixed height, for the same reason the pool feed is: a box that changed
   size with every district an operator flipped past would walk the controls
   underneath it up and down the screen while they were trying to read.

   Seven lines holds the longest entry in the register with one to spare at
   this width. Anything longer scrolls rather than being cut off, so an entry
   written later cannot quietly lose its last sentence. */
.notes--entry {
  --entry-line: 1.2rem;    /* 0.75rem of type at 1.6 line-height. */
  --entry-lines: 7;
}
.notes--entry .notes__body {
  height: calc(var(--entry-line) * var(--entry-lines));
  overflow-y: auto;
  color: var(--ink);
}

.alert--warn { border-left-color: var(--amber); }
.alert--warn .alert__mark { color: var(--amber); }
.alert__mark {
  flex: none;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--rose);
}

/* ---------------------------------------------------------------------------
   The Role question
   Ten tiles, with the chosen Role's listing opening beneath them. The radio
   input is hidden from sight but not from the keyboard: the tile beside it is
   what is drawn, and it is driven entirely by the input's own state.
   --------------------------------------------------------------------------- */
.gate > .terminal--wide { max-width: var(--panel-wide); }

.roles {
  border: 0;
  margin: 0;
  padding: 0;
}
.roles__legend {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
  padding: 0;
  margin-bottom: var(--space-3);
}

/* Five across is the shape this is meant to be read in. The narrower steps
   exist so that a phone gets something usable rather than five slivers. */
.roles__grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 520px) {
  .roles__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 780px) {
  .roles__grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

.role { display: block; cursor: pointer; }

/* Off the screen rather than display: none, so that it keeps its place in the
   tab order and the arrow keys still move between options. */
.role__pick {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.role__tile {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--space-2);
  /* Square while the tiles are small, which is the shape a phone shows them
     in. Across the full width of a wide panel a square would be a hundred and
     seventy pixels holding one icon and one short word, so the height stops
     growing and the tile settles into a landscape card instead. */
  aspect-ratio: 1;
  max-height: 8rem;
  padding: var(--space-3);
  text-align: center;
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink-quiet);
}
.role:hover .role__tile {
  border-color: var(--edge-strong);
  color: var(--ink);
}
.role__pick:checked + .role__tile {
  border-color: var(--amber);
  background: var(--surface-raised);
  color: var(--amber);
}
.role__pick:focus-visible + .role__tile {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
}

/* The icons are stroked in currentColor, so they take the tile's state with
   it and need no rules of their own beyond a size. */
.role__icon {
  width: 34px;
  height: 34px;
}
.role__name {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

/* ---------------------------------------------------------------------------
   The listing
   A modal over the picker. The dialog element is closed by script on load, so
   the rules below describe the open state only; what a closed dialog does is
   left to the browser, which already hides it.
   --------------------------------------------------------------------------- */
.listing[open] {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  width: min(680px, calc(100vw - 2 * var(--gutter)));
  max-height: min(80vh, 760px);
  padding: 0;
  background: var(--surface-panel);
  border: 1px solid var(--amber);
  color: var(--ink);
}
.listing::backdrop { background: rgb(6 8 10 / 0.84); }

.listing__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--edge);
}
.listing__head .eyebrow { margin: 0; }

.listing__close {
  padding: var(--space-1) var(--space-3);
  border: 0;
  background: transparent;
  color: var(--ink-quiet);
  font-size: 1.375rem;
  line-height: 1;
}
.listing__close:hover { background: transparent; color: var(--amber); }

/* The one part that scrolls, so that the heading and the control stay put
   however long a listing runs. */
.listing__body {
  overflow-y: auto;
  padding: var(--space-5);
}

.listing__foot {
  display: flex;
  justify-content: flex-end;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--edge);
  background: var(--surface-void);
}

/* Separation for the fallback, where all ten run down the page at once.
   Both sides must be visible: a hidden listing is still a sibling as far as
   the selector is concerned, so without the guard every Role after the first
   would draw a rule against a neighbour nobody can see. */
.role-detail:not([hidden]) + .role-detail:not([hidden]) {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--edge-faint);
}

.role-detail__name {
  margin: 0 0 var(--space-3);
  font-size: 1.125rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--amber);
}
.role-detail__pitch {
  margin: 0 0 var(--space-4);
  font-size: 0.8125rem;
  line-height: 1.7;
  color: var(--ink-quiet);
}

/* The rules, set apart from the pitch and set as the small print they are. */
.role-detail__rules {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--edge-faint);
}

/* The ability's name gets its own line, because it is the one thing in this
   block a player is likely to be looking for. */
.role-detail__ability {
  margin: 0 0 var(--space-2);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: var(--track-wide);
  color: var(--amber-dim);
}

.role-detail__mechanic {
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------------------
   The point buy
   --------------------------------------------------------------------------- */
.pool {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin: 0 0 var(--space-5);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-raised);
  border: 1px solid var(--edge);
  max-width: none;
}
/* One size across the whole bar. The figure was set three times larger than
   the words beside it, which made "0/62 points remaining" read as three
   separate statements rather than one sentence. Colour and weight carry the
   emphasis instead, which they were already doing. */
.pool { font-size: 1rem; }
.pool__label {
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.pool__tally {
  /* The figure and the total are one token and are written as one, with no
     white space between the spans to be turned into a gap. */
  font-weight: 600;
}
.pool__count { color: var(--amber); }
/* Overspent and exactly spent are both states worth seeing without reading
   the number, so neither is carried by the figure alone. */
.pool__count--over { color: var(--rose); }
.pool__count--settled { color: var(--cyan); }

/* The total is read with the figure rather than against it, so it is the same
   size and weight in a quieter colour instead of a second statement further
   along. */
.pool__total { color: var(--ink-faint); }

/* The limits travel together to the far end, which is why they are wrapped:
   pushed over one at a time they would separate by the flex gap and read as
   two unrelated notes. */
.pool__limits {
  display: flex;
  gap: var(--space-5);
  margin-left: auto;
}
.pool__scale { color: var(--ink-faint); }

/* Rows rather than a flowing grid, because the order these are read in is not
   the order they are asked in. Three columns to a row, and a row holding one
   or two keeps the empty columns: EMP sits where BODY sits, under the figure
   its neighbour decides, and the alignment is the whole point of laying them
   out by hand.

   Below the narrow width the columns collapse and the rows become the reading
   order, which is still BODY, WILL, HP. */
.stats {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.stats__row {
  display: grid;
  gap: var(--space-5) var(--space-4);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 40rem) {
  .stats__row { grid-template-columns: 1fr; }
}
.stat {
  display: grid;
  gap: var(--space-2);
  align-content: start;
}
.stat__label {
  margin: 0;
  color: var(--amber);
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
}

/* The box and its controls, which travel together and stay left so that the
   boxes line up down the column whatever the name above them is. */
.stat__entry {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
}
.stat input[type="number"] {
  width: 4.5rem;
  text-align: center;
}

/* Two controls, one above the other, the height of the box between them. */
.stat__steps {
  display: flex;
  flex-direction: column;
}
.stat__step {
  flex: 1;
  min-width: 1.75rem;
  padding: 0 var(--space-2);
  /* A flex box of its own rather than a line of text, so that a glyph with no
     descender sits in the middle of the control instead of on its baseline. */
  display: flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-size: 0.8125rem;
  line-height: 1;
  color: var(--amber);
  background: var(--surface-raised);
  border: 1px solid var(--edge);
  cursor: pointer;
}
.stat__step + .stat__step { border-top: 0; }
.stat__step:hover { background: var(--surface-lifted, var(--surface-raised)); color: var(--ink); }
.stat__step:disabled { color: var(--ink-faint); cursor: default; }
.stat__hint {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.6875rem;
  line-height: 1.5;
  color: var(--ink-faint);
}
.stat .field__error { grid-column: 1 / -1; }

/* A derived figure is not a box and should not look like one. Same grid, same
   label, and the figure where the input would be, set in the colour the rest
   of the terminal uses for something the operator did not type. */
.stat--derived .stat__label { color: var(--amber-dim); }

/* The same box as a STAT's, so that a row reads as one row: same width, same
   edge, same ground. It carries no controls because there is nothing here to
   change, which is the only difference worth showing. */
.stat__derived {
  display: block;
  width: 4.5rem;
  padding: var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  text-align: center;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--cyan);
}

/* ---------------------------------------------------------------------------
   The tile grid

   Used wherever a question's answers are short enough to read at a glance:
   the personality adjectives, which take up to five, and the values, which
   take one. Rows rather than a wrapping field, because the rows carry a
   meaning the page does not print and a grid that reflowed at a narrow width
   would scramble the grouping with nobody ever knowing it had been there.
   --------------------------------------------------------------------------- */
.traits { margin-bottom: var(--space-6); }

.traits__count {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  font-size: 1rem;
}
.traits__label {
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.traits__figure { font-weight: 600; color: var(--amber); }
.traits__of { color: var(--ink-faint); }
.tiles--full .traits__figure { color: var(--cyan); }

/* The far end of the counter's own line, which is where the page's other Roll
   controls sit relative to what they roll. */
.traits__roll { margin-left: auto; }

.tiles__row {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

/* The whole tile is the control, so the target is the word rather than the
   box beside it. */
.tile {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  cursor: pointer;
  font-size: 0.8125rem;
}
.tile:hover { border-color: var(--edge-strong); }
.tile input[type="checkbox"],
.tile input[type="radio"] { accent-color: var(--amber); cursor: pointer; }
.tile:has(input:checked) {
  border-color: var(--amber);
  color: var(--amber);
}

/* A box to write in is a tile like the others, so a word of your own looks
   like an answer rather than an afterthought. The dashed edge is the only
   thing marking it as empty and yours to fill. */
.tile--write { border-style: dashed; }
.tile--write input[type="text"] {
  min-width: 0;
  flex: 1;
  padding: 0;
  background: transparent;
  border: 0;
  color: var(--amber);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}
.tile--write input[type="text"]:focus { outline: none; }
.tile--write:focus-within { border-color: var(--amber); border-style: solid; }
.tile--write:has(input:disabled) { opacity: 0.45; }

/* At the limit the untaken ones step back rather than disappearing, so the
   grid still reads as a whole and the way out is obvious: untick something. */
.tile:has(input:disabled) {
  color: var(--ink-faint);
  cursor: default;
}
.tile:has(input:disabled):hover { border-color: var(--edge); }

@media (max-width: 48rem) {
  .tiles__row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------------------------------------------------------------------------
   Anything holding a profile back
   --------------------------------------------------------------------------- */
.record__problems {
  margin: 0 0 var(--space-5);
  padding-left: var(--space-5);
  font-size: 0.875rem;
  color: var(--rose);
}

/* ---------------------------------------------------------------------------
   The interruption
   The end of the assessment, where the terminal stops being Ogma's. Both
   halves are in the markup; the script hides the second and reveals it on a
   timer, so everything below describes states that script produces.
   --------------------------------------------------------------------------- */
.gate > .hack {
  max-width: var(--panel-wide);
  padding: 0;
  background: transparent;
  border: 0;
}

.hack__official {
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}
.hack__line { color: var(--ink-quiet); font-size: 0.875rem; max-width: none; }

/* The segment name, being eaten. */
.hack__cut {
  color: var(--amber);
  font-variant-numeric: tabular-nums;
}

/* Coming apart. Two offset copies in the signal colours, which is what a
   failing screen has always looked like. */
.hack__official--failing {
  animation: hack-shear 0.18s steps(2, end) infinite;
}
.hack__official--failing .terminal__body {
  text-shadow:
    -2px 0 var(--rose),
    2px 0 var(--cyan);
}
@keyframes hack-shear {
  0% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  50% { transform: translateX(2px); }
  75% { transform: translateX(-1px); }
  100% { transform: translateX(0); }
}

/* Gone. Left on screen underneath, dimmed, because he did not close it. */
.hack__official--lost {
  opacity: 0.25;
  filter: saturate(0.4);
  pointer-events: none;
}

/* His half. Rose rather than amber: nothing else on this interface is. */
.hack__savant {
  margin-top: var(--space-4);
  background: var(--surface-void);
  border: 1px solid var(--rose);
}
.hack--taken .hack__savant {
  animation: hack-arrive 0.12s steps(3, end) 2;
}
@keyframes hack-arrive {
  0% { opacity: 0.2; transform: translateX(-6px); }
  50% { opacity: 1; transform: translateX(4px); }
  100% { opacity: 1; transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
  .hack__official--failing,
  .hack--taken .hack__savant { animation: none; }
}

.hack__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  background: rgb(226 86 111 / 0.12);
  border-bottom: 1px solid var(--rose);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}
.hack__mark { color: var(--rose); font-weight: 600; }
.hack__where { color: var(--ink-faint); text-transform: none; }

.hack__body { padding: var(--space-6); }
.hack__body p {
  max-width: none;
  color: var(--ink);
  font-size: 0.875rem;
  line-height: 1.7;
}

/* The two lines he opens with, before the lecture starts. Set apart because
   they are the only part of this that is not addressed to the reader. */
.hack__body .hack__open {
  color: var(--ink-quiet);
  font-size: 0.9375rem;
  line-height: 1.6;
}
/* The second line closes the epigraph, so it carries the rule under it.
   :last-of-type cannot be used here, because the last p in this body is the
   signature rather than an opening line. */
.hack__body .hack__open + .hack__open {
  margin-top: var(--space-3);
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--edge);
}

/* What the file says about them, set apart because it is the only part of
   this that is about the reader personally. */
.hack__file {
  border-left: 2px solid var(--rose);
  padding-left: var(--space-4);
  color: var(--ink-quiet);
}

.hack__sign {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--edge);
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------------------
   Weather
   The metropolitan report: what the sky, the wind, the water and the air are
   doing. Warnings first and figures second, because a warning is what an
   operator acts on and a table is what they check afterwards.
   ------------------------------------------------------------------------ */

.report__heading {
  margin: var(--space-6) 0 var(--space-3);
  font-size: 0.875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

.report__actions {
  margin-top: var(--space-6);
}

/* An advisory is the one thing on this page that is allowed to shout. */
.advisories,
.strange {
  display: grid;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.advisory,
.strange__event {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--edge);
  border-left-width: 4px;
  background: var(--surface-raised);
}

.advisory--severe {
  border-left-color: var(--rose);
  background: color-mix(in srgb, var(--rose) 8%, var(--surface-raised));
}

.advisory__name,
.strange__name {
  margin: 0;
  font-size: 0.9375rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.advisory--severe .advisory__name { color: var(--rose); }

.advisory__detail,
.strange__detail {
  margin: var(--space-2) 0 0;
  max-width: none;
  font-size: 0.9375rem;
}

.strange__when {
  margin: var(--space-2) 0 0;
  max-width: none;
  font-size: 0.8125rem;
  color: var(--ink-quiet);
}

/* The reading itself: the temperature large, everything else in a grid of
   labelled figures beside it. */
.now {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
  padding: var(--space-4) 0;
  border-top: 1px solid var(--edge);
  border-bottom: 1px solid var(--edge);
}

.now__temperature {
  margin: 0;
  max-width: none;
  line-height: 1;
}

.now__temperature b {
  font-size: 3.5rem;
  color: var(--amber);
}

.now__condition {
  margin: var(--space-2) 0 0;
  max-width: none;
  font-size: 1.25rem;
}

.now__band {
  margin: var(--space-1) 0 0;
  max-width: none;
  font-size: 0.8125rem;
  color: var(--ink-quiet);
}

.now__figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-3) var(--space-5);
  margin: 0;
}

/* A figure whose value is a sentence rather than a reading. Given a column
   like the others it wrapped to three lines while four columns sat empty
   beside it. */
.figure--wide { grid-column: 1 / -1; }

/* The day's water, kept as its own grid so that it always begins a row. */
.now__figures--tides { margin-top: var(--space-3); }

.figure dt {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

.figure dd {
  margin: 2px 0 0;
  font-size: 0.9375rem;
}

/* Carried under its figure rather than beside it, so that the labels stay on
   one baseline however long the detail under any one of them runs. */
.figure__note {
  display: block;
  font-size: 0.75rem;
  color: var(--ink-quiet);
}

.tides__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.tides__height { color: var(--ink-quiet); }

/* Twenty-four rows is more than the panel is tall, so the table scrolls
   within the page rather than pushing the forecast off the bottom of it. */
.hours__scroll {
  max-height: 22rem;
  overflow-y: auto;
  border: 1px solid var(--edge);
}

.hours__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.hours__table th,
.hours__table td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--edge);
}

.hours__table thead th {
  position: sticky;
  top: 0;
  background: var(--surface-raised);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}

.hours__row--now {
  background: color-mix(in srgb, var(--amber) 12%, transparent);
}

.hours__row--now th { color: var(--amber); }

/* Seven across, always. The free outlook is a week and the paid one is two,
   so fourteen wraps into a second row of the same shape rather than into a
   longer ribbon of a different one. Fixed rather than auto-filled, because a
   week that reflowed to six and one would read as a fault. */
.ahead__days {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* About a hundred pixels a day at this width, which is why the card carries
   the afternoon rather than all four watches. The rest is on the title, so
   hovering still gives the shape of the day. */
/* A day is a control now rather than a tile: pressing one opens that day's
   report underneath the row. Drawn as a button rather than as a link, because
   it behaves like one and an operator should not have to discover that. */
.ahead__day {
  display: block;
  padding: var(--space-3) var(--space-2);
  border: 1px solid var(--edge);
  background: var(--surface-raised);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

.ahead__day:hover,
.ahead__day:focus-visible { border-color: var(--amber); }

/* The day being read, marked so that an operator can see which report is
   underneath without scrolling back up to work it out. */
.ahead__day--open {
  border-color: var(--amber);
  background: var(--surface-panel);
  box-shadow: inset 0 2px 0 0 var(--amber);
}

.ahead__day--warned { border-color: var(--rose); }

.ahead__day > p,
.ahead__day > span {
  display: block;
  margin: 0;
  max-width: none;
}

.ahead__date {
  margin: 0;
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-bright);
}

.ahead__when {
  font-size: 0.6875rem;
  color: var(--ink-faint);
}

.ahead__range,
.ahead__reading {
  margin-top: var(--space-2) !important;
  line-height: 1.2;
}

.ahead__range b,
.ahead__reading b {
  font-size: 1.125rem;
  color: var(--amber);
}

.ahead__low {
  margin-left: 4px;
  font-size: 0.75rem;
  color: var(--ink-quiet);
}

.ahead__sky {
  margin-top: var(--space-1) !important;
  font-size: 0.6875rem;
  line-height: 1.35;
  color: var(--ink);
}

.ahead__wind {
  margin-top: 2px !important;
  font-size: 0.625rem;
  letter-spacing: 0.04em;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------------------
   One day's report, opened by the row above it.

   Hidden rather than removed, so that the whole fortnight is in the markup
   and a browser with no script reads it as a fortnight of reports one after
   another. That is long, and it is a great deal better than nothing.
   --------------------------------------------------------------------------- */
.report__day { margin-top: var(--space-6); }

.report__day[hidden] { display: none; }

.report__heading--day {
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--edge);
  color: var(--amber);
}

.report__heading--hours { margin-top: var(--space-5); }

/* A column that is empty on most rows and the point of the table on a few. */
.hours__warning { color: var(--rose); }

/* A week will not fit seven across on a phone, so it falls to a pair of
   columns rather than to seven unreadable slivers. */
@media (max-width: 640px) {
  .ahead__days { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The bill, shown for a few seconds on the way in and then gone. Fixed rather
   than in the flow, so it does not move the report under the reader, and
   above the window because it can be raised by one.

   Timed by the animation rather than by a script: the panel is injected into
   the window with innerHTML, and a script tag inside it would never run. */
.charge {
  position: fixed;
  top: var(--space-5);
  left: 50%;
  z-index: 90;
  margin: 0;
  max-width: none;
  padding: var(--space-3) var(--space-5);
  background: var(--surface-raised);
  border: 1px solid var(--amber);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  color: var(--ink-bright);
  animation: charge-shown 5s ease forwards;
}

@keyframes charge-shown {
  0% { opacity: 0; transform: translate(-50%, -8px); }
  6% { opacity: 1; transform: translate(-50%, 0); }
  85% { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -8px); visibility: hidden; }
}

/* Still brief, and still gone, for anyone who has asked the machine to stop
   moving things at them. */
@media (prefers-reduced-motion: reduce) {
  .charge { animation: charge-held 5s steps(1) forwards; }

  @keyframes charge-held {
    0% { opacity: 1; transform: translate(-50%, 0); }
    100% { opacity: 0; transform: translate(-50%, 0); visibility: hidden; }
  }
}

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

/* The strip on the status bar. Sized to disappear before the terminal's own
   identity does, since the weather is the least load-bearing thing up there. */
.statusbar__item--weather {
  color: var(--ink);
  text-decoration: none;
}

.statusbar__item--weather:hover,
.statusbar__item--weather:focus-visible {
  color: var(--amber-bright);
}

.statusbar__item--warned { color: var(--rose); }

@media (max-width: 1100px) {
  .statusbar__item--weather { display: none; }
}

/* ---------------------------------------------------------------------------
   Metropolitan outlook
   The days ahead, under the pool feed and built to match it: same width, same
   height, same one-at-a-time rotation. The last card in the rotation asks for
   money, which is the only card the pool has any interest in.
   ------------------------------------------------------------------------ */

.outlook {
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}


.outlook__reel {
  list-style: none;
  margin: 0;
  padding: var(--space-4);
}

.outlook__card {
  font-size: 0.8125rem;
  line-height: 1.6;
}

.outlook__card > p {
  margin: 0;
  max-width: none;
}

/* The dated heading every card carries. One card is visible at a time, so
   this is the only thing telling an operator what they are looking at, and it
   is sized and weighted accordingly.

   The rule under it is drawn with a gradient rather than with a dashed
   border, because a border's dash length is the browser's to choose and these
   want to be the same dashes in every browser. Six on, four off, which at
   this size reads as a line of hyphens in the terminal's own face. */
.outlook__day {
  padding-bottom: var(--space-2);
  background-image: repeating-linear-gradient(
    to right,
    var(--edge-strong) 0 6px,
    transparent 6px 10px
  );
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: 0 100%;
  font-size: 0.8125rem;
  font-weight: 600;
  /* Tighter than the readings under it. A heading is one line by
     construction, so it needs no room for a second, and the four pixels
     saved are what let a card carry a third reading. */
  line-height: 1.3;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

/* ---------------------------------------------------------------------------
   The outlook's cards, each a dated heading and a line or two of labelled
   readings.

   One reading to a line. Side by side, with or without a pipe between them,
   the second label started a column that nothing else on the card lined up
   with, and the eye kept going back to check.
   --------------------------------------------------------------------------- */
/* The child selector is load bearing and is not decoration. Every paragraph
   in a card is zeroed by `.outlook__card > p`, which carries an element in
   its selector and therefore outweighs a single class: written as
   `.outlook__line` alone, the margin below was set three times and applied
   none of them, and the gap stayed at nothing while the number in the file
   went from eight to twenty-four. */
.outlook__card > .outlook__line {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--space-6);
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--ink);
}

/* The first line clears the dashed rule above it; the ones after it are the
   same card continuing and close up behind it. */
.outlook__card > .outlook__line + .outlook__line { margin-top: 0; }

/* A label and its value are one thing and break as one. Split by a wrap,
   "Sunrise:" on one line and "07:12" on the next reads as two separate facts,
   which at this size is the whole of what the card says. */
.outlook__pair { white-space: nowrap; }

/* Except where the value is a list of warnings, which has to be allowed to
   wrap or a bad day runs off the side of the box. */
.outlook__pair--wide { white-space: normal; }

.outlook__label {
  color: var(--ink-faint);
  font-weight: 600;
}

.outlook__reading b {
  font-size: 1.375rem;
  line-height: 1.1;
  color: var(--ink-bright);
}

.outlook__low,
.outlook__sky { color: var(--ink); }

.outlook__low { margin: 0 var(--space-2); }

.outlook__note { color: var(--ink-quiet); }

.outlook__card--warned .outlook__note { color: var(--rose); }

/* The card that is not weather. Marked as an advertisement by looking like
   one, rather than by being labelled. */
.outlook__card--sell .outlook__day { color: var(--cyan); }

/* Set smaller than a forecast, because the copy is three times as long as a
   day's and the reel is a fixed three lines. A card that overflowed would be
   clipped rather than scrolled, and the clipped part is the price. */
.outlook__pitch {
  margin-top: var(--space-1) !important;
  font-size: 0.6875rem;
  line-height: 1.45;
  color: var(--ink-bright);
}

/* With no script every card is on screen at once, so they stack and want
   separating, and the panel is as tall as the outlook. That is the fallback
   and it is allowed to be: a list of six days is more use than nothing.
   Once the script is running one card shows at a time and the reel takes the
   same fixed height the feed above it does. */
.outlook__card + .outlook__card {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--edge-faint);
}

.outlook--running .outlook__reel {
  display: flex;
  align-items: flex-start;
  height: calc(var(--reel-height) + var(--space-4) * 2);
  overflow: hidden;
}

.outlook--running .outlook__card {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
  width: 100%;
}

/* ---------------------------------------------------------------------------
   Your Daily Fortune
   The pool's other trade. One card of the major arcana, the same for everybody
   on the pool, turning over at midnight and not repeating inside three weeks.
   ------------------------------------------------------------------------ */

.fortune {
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

.fortune__body {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  gap: var(--space-4);
  padding: var(--space-4);
}

.fortune__card {
  display: block;
  border: 1px solid var(--edge);
  line-height: 0;
}

.fortune__card:hover,
.fortune__card:focus-visible { border-color: var(--amber); }

.fortune__face {
  display: block;
  width: 100%;
  height: auto;
}

.fortune__reading > p {
  margin: 0;
  max-width: none;
}

.fortune__name {
  font-size: 0.9375rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.fortune__omen {
  margin-top: var(--space-2) !important;
  font-family: var(--font-mono);
  font-style: italic;
  font-size: 0.9375rem;
  color: var(--ink-bright);
}

.fortune__effect {
  margin-top: var(--space-3) !important;
  font-size: 0.8125rem;
  color: var(--ink-quiet);
}

/* With the script running the reading lives in the viewer, so the box is the
   card and nothing else: the artwork is the whole point of it and the words
   beside it were a caption nobody needed twice. Without a script the reading
   stays, because then there is nowhere else for it to be. */
.fortune--running .fortune__reading { display: none; }

/* The card alone, now that the sections have a box of their own. */
.fortune--running .fortune__body {
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
}

/* Sized to the card rather than to the column. With the reading gone the box
   was still as wide as the advertising above it, which left a card floating in
   the middle of half a metre of nothing. The panel now takes its width from
   what is in it, and the padding is the whole of the space around it. */
.fortune--running {
  width: fit-content;
  max-width: 100%;
}

/* Sized so the box finishes above the fold on an ordinary desktop. A card is
   tall for its width, so the height is what runs out first and the width is
   what governs it: at two hundred the panel ran a little past the bottom of
   the page. */
.fortune--running .fortune__card { width: 160px; }

/* The resources box, built to match the fortune beside it: same bar, same
   ground, same border, so the pair read as one row of the terminal. */
.resources {
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

.resources__body { padding: var(--space-4); }

/* The three reference sections. Built like the CitiNet board tiles rather than
   as filled buttons: they are ways into a section rather than actions, three
   of them stack, and three filled controls beside a dark card would carry more
   weight than the card they stand next to. */
.stacks {
  display: grid;
  align-content: start;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.stacks__way {
  display: grid;
  gap: 2px;
  padding: var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  text-decoration: none;
  color: var(--ink-quiet);
}

.stacks__way:hover,
.stacks__way:focus-visible {
  border-color: var(--amber);
  color: var(--amber);
}

.stacks__name {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: inherit;
}

.stacks__holds {
  font-size: 0.625rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--ink-faint);
}

/* Not built yet, and saying so rather than pretending. The cursor and the
   dimming are the whole of the difference, so the modifier comes off and a
   real href goes on when the section arrives. */
.stacks__way--pending {
  cursor: not-allowed;
  opacity: 0.55;
}

.stacks__way--pending:hover,
.stacks__way--pending:focus-visible {
  border-color: var(--edge);
  color: var(--ink-quiet);
}

/* ---------------------------------------------------------------------------
   Live Radio Streams
   The dial, under the boards and level with the card and the resources. A
   display that scrolls what is on, and a row for each station with the
   control that tunes to it. Built from the same bar, ground and border as the
   pair beside it, so the three read as one row of the terminal.
   ------------------------------------------------------------------------ */
.radio {
  /* Third in the row with the card and the resources, at a fixed width that
     the row does not squeeze. */
  flex: none;
  width: 300px;
  max-width: 100%;
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

.radio__body {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
}

/* The display. A slot of fixed height that clips what scrolls through it,
   so the box does not change size between still and scrolling. */
.radio__dial {
  overflow: hidden;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  font-size: 0.75rem;
  line-height: 1.6;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

.radio__dial > p {
  margin: 0;
  max-width: none;
}

.radio__idle {
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ink-faint);
}

/* Two copies of the same line side by side, moved left by exactly one copy's
   width and started again, which is a loop with no visible seam. */
.radio__crawl {
  display: inline-flex;
  color: var(--amber);
  animation: radio-crawl 20s linear infinite;
}

.radio__crawl-text {
  flex: none;
  white-space: pre;
}

@keyframes radio-crawl {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Still, for anyone who has asked the machine to stop moving things at them.
   The line is shown once and cut short where it does not fit. */
@media (prefers-reduced-motion: reduce) {
  .radio__crawl {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    animation: none;
  }
  .radio__crawl-text[aria-hidden] { display: none; }
}

.radio__stations {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.radio__station {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
}

.radio__station-name {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.radio__call {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  color: var(--amber);
}

.radio__brand {
  font-size: 0.625rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Quiet until pressed, like the advertising's controls: a convenience rather
   than an action. Filled while tuned, so the station playing is the one
   lit control in the box. */
.radio__tune {
  flex: none;
  justify-content: center;
  min-width: 9ch;
  padding: var(--space-2) var(--space-3);
  background: transparent;
  border-color: var(--edge);
  color: var(--ink-quiet);
  font-size: 0.6875rem;
}

.radio__tune:hover,
.radio__tune:focus-visible {
  background: transparent;
  border-color: var(--amber);
  color: var(--amber);
}

.radio__tune[aria-pressed="true"],
.radio__tune[aria-pressed="true"]:hover {
  background: var(--amber);
  border-color: var(--amber);
  color: var(--ink-invert);
}

/* The radio's own volume. A label, the slider and the level, on one line
   between the display and the stations, as quiet as the station names. */
.radio__volume {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) 3ch;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-1);
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Restated, because the display rule above would otherwise win over the
   attribute and show a slider the browser has said it will not honour. */
.radio__volume[hidden] { display: none; }

.radio__volume-slider {
  width: 100%;
  margin: 0;
  accent-color: var(--amber);
  cursor: pointer;
}

.radio__volume-level {
  text-align: right;
  color: var(--ink-quiet);
}

.radio__none {
  font-size: 0.75rem;
  color: var(--ink-faint);
}

/* The player on the station's own page, for a browser with no script. */
.radio__fallback {
  display: block;
  width: 100%;
  margin-bottom: var(--space-5);
}

/* The viewer. Over the page rather than beside it, because the card is the
   only thing worth looking at once it is open. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: var(--space-5);
  background: color-mix(in srgb, var(--surface-void) 88%, transparent);
}

.viewer[hidden] { display: none; }

body.is-viewing { overflow: hidden; }

.viewer__sheet {
  position: relative;
  display: grid;
  justify-items: center;
  gap: var(--space-3);
}

.viewer__close {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
}

/* The card turns rather than swapping, so that the back is understood as the
   same card rather than as a different picture. */
.viewer__card {
  position: relative;
  width: min(70vh * 0.55, 90vw);
  aspect-ratio: 743 / 1355;
  cursor: pointer;
  transform-style: preserve-3d;
  transition: transform 500ms ease;
}

.viewer__card--turned { transform: rotateY(180deg); }

.viewer__side {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  border: 1px solid var(--edge-strong);
  overflow: hidden;
}

.viewer__side img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.viewer__side--back { transform: rotateY(180deg); }

/* The effect over the back of the card, in white, with the card itself dimmed
   under it so that the text is readable rather than merely present. */
.viewer__told {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-5);
  background: color-mix(in srgb, var(--surface-void) 72%, transparent);
  overflow-y: auto;
}

.viewer__name {
  margin: 0;
  font-size: 1rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-bright);
}

.viewer__omen,
.viewer__effect {
  margin: 0;
  max-width: none;
  color: var(--ink-bright);
}

.viewer__omen {
  font-family: var(--font-mono);
  font-style: italic;
}

.viewer__effect {
  font-size: 0.875rem;
  line-height: 1.7;
}

.viewer__hint {
  margin: 0;
  max-width: none;
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* A card that snaps rather than turns, for anyone who has asked the machine
   to stop moving things at them. */
@media (prefers-reduced-motion: reduce) {
  .viewer__card { transition: none; }
}

@media (max-width: 1100px) {
  .deck__pair { grid-area: auto; }
  .social { grid-area: auto; }
  .courtesy { grid-area: auto; }
}

/* ---------------------------------------------------------------------------
   Windows
   A page opened over the one you are on rather than in place of it. The shell
   is in base.html and the contents are fetched from the same view that serves
   the page, so there is one copy of each panel and no chance of two.
   ------------------------------------------------------------------------ */

.window {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--space-5);
  background: color-mix(in srgb, var(--surface-void) 86%, transparent);
}

.window[hidden] { display: none; }

body.is-windowed { overflow: hidden; }

.window__sheet {
  display: flex;
  flex-direction: column;
  width: min(1040px, 100%);
  max-height: min(880px, 92vh);
  background: var(--surface-panel);
  border: 1px solid var(--edge-strong);
}

.window__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4);
  background: var(--surface-raised);
  border-bottom: 1px solid var(--edge);
}

.window__title {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The body scrolls rather than the page behind it, so a long report is read
   inside the window and the landing page stays where it was. */
.window__body {
  padding: var(--space-5);
  overflow-y: auto;
}

.window__body:focus-visible { outline: none; }

.window__waiting,
.window__failed {
  margin: 0;
  max-width: none;
  color: var(--ink-quiet);
}

.window__failed { color: var(--rose); }

.window__failed a { color: var(--amber); }

/* ---------------------------------------------------------------------------
   Local news
   The day's headlines: a lead, then the wire by section.
   ------------------------------------------------------------------------ */

.lead {
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--edge);
}

.lead__kicker {
  margin: 0;
  max-width: none;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.lead__headline {
  margin: var(--space-2) 0 0;
  font-size: 1.375rem;
  line-height: 1.3;
  color: var(--ink-bright);
}

.lead__outlet,
.wire__by {
  margin: var(--space-2) 0 0;
  max-width: none;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Columns rather than a grid. As grid items the sections lined up in rows and
   every row grew to its tallest member, so a section holding one story left a
   hole under it the height of the section beside it holding three. Flowing
   them as columns packs them the way a newspaper page does, and a section is
   kept whole rather than broken across a column break. */
.wire {
  column-count: 3;
  column-gap: var(--space-6);
  margin-top: var(--space-5);
}

.wire__section {
  break-inside: avoid;
  margin-bottom: var(--space-6);
}

.wire__theme {
  margin: 0 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--edge-faint);
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
}

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

.wire__story + .wire__story {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--edge-faint);
}

/* The same face as the lead, a size down, and mono like the rest of the
   terminal. This is a newsfeed on a public terminal rather than a newspaper,
   and a serif headline read as a different site pasted into this one. */
.wire__headline {
  margin: 0;
  max-width: none;
  font-size: 0.9375rem;
  line-height: 1.45;
  color: var(--ink-bright);
}

.wire__foot,
.wire__quiet {
  margin: var(--space-4) 0 0;
  max-width: none;
  font-size: 0.75rem;
  color: var(--ink-faint);
}

@media (max-width: 900px) {
  .wire { column-count: 2; }
}

@media (max-width: 620px) {
  .wire { column-count: 1; }
}


/* ---------------------------------------------------------------------------
   The reference library
   Two written sections, their indexes and their articles. The Catalogue is a
   database rather than prose and is its own application, so none of this is
   shared with it.
   ------------------------------------------------------------------------ */

/* The section tiles again, given room when they are the page rather than a
   column beside a tarot card. */
.stacks--wide {
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-3);
}

.entry__standfirst {
  margin: 0 0 var(--space-4);
  max-width: 62ch;
  font-family: var(--font-mono);
  font-size: 1.0625rem;
  line-height: 1.5;
  color: var(--ink);
}

/* The reading room. Where you can go, what you are reading, what else is in
   here. The room takes the whole window rather than sitting in a centred
   column: the side columns are the width they need and the middle takes
   everything left over, which is the same arrangement as the landing page and
   reads as the same machine. */
.reading {
  display: grid;
  /* The side columns are given a floor and a ceiling rather than a share of
     the width. A quarter each looked right on paper and came out as two
     cramped columns on a wide screen while the middle grew past what prose
     can use. Held between fourteen and twenty rems, they stay legible at any
     width and the middle takes whatever is left. */
  grid-template-columns:
    minmax(14rem, 20rem)
    minmax(0, 1fr)
    minmax(14rem, 22rem);
  gap: 0;
  /* Stretched rather than started, so the rules between the columns run the
     full height of the page. Each column's own contents stay stuck to the top
     from inside it. */
  align-items: stretch;
  /* The window's height less the status bar, so that a short article still
     leaves the rules running from the bar to the bottom of the screen rather
     than stopping wherever the text happens to end. The same figure the
     landing page uses. */
  min-height: calc(100vh - 40px);
}

/* The rules between the three. On the side columns rather than on the middle,
   because the middle is a panel with a border of its own and a second line
   beside it would read as a mistake.

   The room itself carries no padding: every edge of space belongs to one of
   the three columns, so that the rules between them are the full height of
   the room and nothing indents them away from the top and the bottom of the
   window. */
.terminal-nav {
  padding: var(--deck-top) var(--space-5) var(--space-7) var(--gutter);
  border-right: 1px solid var(--edge);
}

.reading__index {
  padding: var(--deck-top) var(--gutter) var(--space-7) var(--space-5);
  border-left: 1px solid var(--edge);
}

.reading__text { margin: var(--deck-top) var(--space-6) var(--space-7); }

/* Both side columns hold still while the middle scrolls, because navigation
   that scrolls away is navigation you have to go back for. Applied to what is
   inside each column rather than to the column itself, which now runs the full
   height of the page to carry its rule. */
.terminal-nav > *,
.reading__index > * { position: relative; }

.terminal-nav,
.reading__index { align-self: stretch; }

.reading__text {
  /* The panel's own side padding, held as a token because the rules inside it
     are pulled back out by exactly this much to reach its edges. Written twice
     it is two numbers that can drift, and a rule that misses the edge by four
     pixels looks worse than one that never tried. */
  --panel-x: var(--space-7);

  min-width: 0;
  /* Tall enough that a section with nothing in it still reads as a panel
     rather than as a stub somebody forgot to finish. */
  min-height: 60vh;
  padding: var(--space-6) var(--panel-x);
  background: var(--surface-panel);
  border: 1px solid var(--edge);
}

/* The record opens on a label rather than on prose, and a label wants less
   air above it than a paragraph does: thirty two pixels over four small
   capitals read as a gap somebody forgot to fill. Sixteen puts the eyebrow
   level with the first heading in the navigation beside it.

   On the record alone rather than on every page of the reading room, because
   an article and a board open on a title and a title earns the room.
   --------------------------------------------------------------------------- */
.reading__text--record { padding-top: var(--space-3); }
.reading__text--record > .entry__head > .record__naming > .eyebrow { margin-top: 0; }

/* The head is the official record's own component and brings its padding with
   it, which on this page lands on top of the panel's and holds everything
   forty five pixels down a box that starts twelve. The sides and the foot are
   the component's business; the top is this page's. */
.reading__text--record > .record__head { padding-top: var(--space-1); }

/* The prose holds its own measure inside a panel that may be much wider, so
   the column can breathe without the lines becoming unreadable. */
/* The catalogue reads in the same three columns as the Library and the
   record, with one change: its third column holds three short labels
   rather than a search box and a list of article titles, so it is given
   less and the middle is given more. A list of four columns of figures
   wants the room, and at the reading room's own widths the price column
   was wrapping "200 eb" onto two lines.

   Written here, above the rule that stacks the three columns on a narrow
   screen, because both selectors weigh the same and the last one wins.
   Written below it, this came out as three columns on a phone. */
.reading--catalogue {
  grid-template-columns:
    minmax(14rem, 20rem)
    minmax(0, 1fr)
    minmax(10rem, 14rem);
}

.reading__text > * { max-width: 68ch; }

/* A list is not prose, and neither is a rule. The threads on a board, the
   replies in one and the box for writing are rows and controls rather than
   sentences, so the measure that keeps prose readable does not apply to them
   and only stopped them short. */
.reading__text > .entry__head,
.reading__text > .threads__head,
.reading__text > .thread,
.reading__text > .replies,
.reading__text > .vitals,
.reading__text > .compose,
/* The Inventory page's tab strip and the section under it, for the same
   reason: a row of tabs is a control and the section holds tables. Held to
   the measure, the strip wrapped onto two lines and its rule stopped a third
   of the way short of the panel's edge. */
.reading__text > .tabs,
.reading__text > .inventory,
/* And the pages behind it: the catalogue list an item is picked from, the
   forms that add and change a thing, and a held thing's sheet. Held to the
   measure, the list stopped at six hundred pixels and scrolled sideways in
   a panel twice that wide. */
.reading__text > .kit-section,
.reading__text > .adding,
/* The block the sheet's controls redraw. It is the direct child now, so the
   measure lands on it rather than on the grids and tables inside it, which
   were then overflowing a box narrower than they were. */
.reading__text > [data-live-region],
/* The catalogue's filters, its list, its pager and the tables on an item's
   page. Four columns of figures and a row of six controls, none of them
   sentences: held to the measure they stopped two hundred pixels short of
   the panel and the price column wrapped "200 eb" onto two lines. */
.reading__text > .sieve,
.reading__text > .table-wrap,
.reading__text > .sheet-box,
.reading__text > .pages,
/* And the catalogue's hoardings, which are pictures and a moving line of
   offers. Held to the reading measure they stopped a third of the way
   short of the panel, which on a hoarding reads as a mistake. */
.reading__text > .hoardings { max-width: none; }

/* A rule across the panel is the edge of something: the end of the heading,
   the end of a thread. It is drawn to the panel's own sides rather than to
   where the words happen to stop, which is why these are pulled out by the
   padding and then put their own back. The words do not move; only the line
   under them reaches further.

   On the row rather than on the list, because the rule belongs to the row and
   padding on the list would simply move the problem inwards one level. */
.reading__text > .entry__head,
.reading__text > .threads__head,
.reading__text > .thread > li {
  margin-inline: calc(-1 * var(--panel-x));
  padding-inline: var(--panel-x);
}

/* ---------------------------------------------------------------------------
   The head of the thread list: how many there are, and the way to add one.

   One line rather than two, because a count and the control that changes it
   are one statement about the list, and the rule under them belongs to the
   list below rather than to the heading above.
   --------------------------------------------------------------------------- */
.threads__head {
  display: flex;
  flex-wrap: wrap;
  /* Centred against each other and started at the left edge. The count is a
     line of small type and the control is a button, so left to themselves
     they would sit on a shared baseline and look dropped. */
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-4);
  /* The same above as below, measured from the rule over the bar to the rule
     under it. Sixteen and eight put the pair a third of the way down a bar
     that looked centred until you measured it. */
  margin-top: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--edge);
}

/* Together rather than pushed to the two ends. The count and the control that
   changes it are one statement about the list, and a board of three threads
   had them a screen apart with nothing in between. */
.threads__count { margin: 0; }

.threads__head .compose { margin: 0; }

/* Open, the form takes the row underneath and the whole width, because a
   column sized to a button is no width to write in. The control goes with it
   and stops being a button, which is what the open state already says. */
.threads__head .compose[open] { flex: 1 0 100%; }

/* The rules in the list separate one thread from the next, so the last one
   has nothing to separate itself from. Left in, it drew a second line under a
   list that already has one above it and read as a box somebody failed to
   close. */
.reading__text > .thread > li:last-child { border-bottom: 0; }

.ways,
.contents {
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

/* A rule under each group, so that the bar reads as four short lists rather
   than as one long one with headings scattered through it. On the list rather
   than on the heading below it, because the last group has no heading after
   it and would otherwise be the only one left open. */
.terminal-nav .ways {
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--edge-faint);
}

.ways__way,
.contents__way { line-height: 1.5; }

/* A board address has no spaces in it and is the one entry in the bar that
   will not wrap on its own. */
.ways__way--address {
  font-size: 0.75rem;
  overflow-wrap: anywhere;
}

.ways__heading { margin-top: var(--space-5); }

.ways__way,
.contents__way {
  display: block;
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid transparent;
  font-size: 0.8125rem;
  color: var(--ink-quiet);
  text-decoration: none;
}

.ways__way:hover,
.ways__way:focus-visible,
.contents__way:hover,
.contents__way:focus-visible {
  border-left-color: var(--amber);
  color: var(--amber);
}

.ways__way--here,
.contents__way--here {
  border-left-color: var(--amber);
  background: var(--surface-raised);
  color: var(--amber);
}

/* The Catalogue again, still not built, still saying so rather than pretending
   to be a place you can go. */
.ways__way--pending {
  opacity: 0.45;
  cursor: not-allowed;
}

.ways__way--pending:hover { border-left-color: transparent; color: var(--ink-quiet); }



.contents__title { display: block; }

.contents__mark {
  display: block;
  margin-top: 2px;
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.contents__mark--held { color: var(--rose); }

/* ---------------------------------------------------------------------------
   The board index, which is the right hand column of a board page: every
   board CitiNet carries, under the section it is filed in.

   The boards sit in the same column the library's contents do and are built
   the same way, so the two pages read as one terminal rather than as two
   places that happen to share a status bar.
   --------------------------------------------------------------------------- */
.board-index__heading { margin-top: var(--space-5); }

.board-index__list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0 0 var(--space-3);
  border-bottom: 1px solid var(--edge-faint);
}

/* Address above, name below, so the eye lands on the thing a character would
   actually say out loud. The name is there for anybody who has not learnt the
   addresses yet and is not meant to be read twice. */
.board-index__way {
  display: grid;
  gap: 1px;
  padding: var(--space-2) var(--space-3);
  border-left: 2px solid transparent;
  color: var(--ink-quiet);
  text-decoration: none;
}
.board-index__way:hover,
.board-index__way:focus-visible {
  border-left-color: var(--amber);
  color: var(--amber);
}

/* The board being read is marked rather than left out. A list that loses an
   entry when you are standing on it makes an operator count the rows. */
.board-index__way--here {
  border-left-color: var(--amber);
  background: var(--surface-raised);
  color: var(--amber);
}

.board-index__address {
  font-size: 0.75rem;
  overflow-wrap: anywhere;
}

.board-index__name {
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}
.board-index__way:hover .board-index__name,
.board-index__way--here .board-index__name { color: inherit; }

.board-index__empty {
  margin: var(--space-3) 0 0;
  font-size: 0.75rem;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------------------
   Writing something. A disclosure rather than a page of its own, so that an
   operator starts a thread where the threads are.

   A details element, which is the browser's own: it opens with no script, it
   is a button to a keyboard and to a screen reader without being told to be
   one, and the form inside it posts whether it was ever opened or not.
   --------------------------------------------------------------------------- */
.compose { margin-top: var(--space-5); }

/* The marker is the browser's triangle and is dropped: the control is drawn
   as a control, and a disclosure triangle on something that looks like a
   button reads as two different widgets wearing one coat. */
.compose__control {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border: 1px solid var(--amber);
  background: var(--amber);
  color: var(--ink-invert);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  cursor: pointer;
  list-style: none;
}
.compose__control::-webkit-details-marker { display: none; }
.compose__control::marker { content: ""; }
.compose__control:hover { background: var(--amber-bright); border-color: var(--amber-bright); }
.compose__control:focus-visible { outline: 2px solid var(--amber-bright); outline-offset: 2px; }

/* Open, the control is a heading for what it opened rather than a thing to
   press again, so it stops shouting and the box below it takes over. */
.compose[open] .compose__control {
  background: transparent;
  color: var(--amber);
}

.compose__body {
  margin-top: var(--space-4);
  padding: var(--space-4);
  background: var(--surface-void);
  border: 1px solid var(--edge);
}
.compose__body .written { margin-top: 0; }


/* Three columns need the room. Below that the navigation goes above the
   article and the contents below it, which is the order they are wanted in. */
/* Narrower than this and three columns stop working, so they stack and the
   rules between them become rules above and below. */
@media (max-width: 1080px) {
  .reading {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
  }

  .terminal-nav {
    padding: var(--deck-top) var(--gutter) var(--space-4);
    border-right: 0;
    border-bottom: 1px solid var(--edge);
  }

  .reading__index {
    padding: var(--space-4) var(--gutter) var(--space-7);
    border-left: 0;
    border-top: 1px solid var(--edge);
  }

  .reading__text { margin: 0 var(--gutter); }
}

/* Search, which is a plain GET form so that a result is a link somebody can
   send on and so that it works with no script at all. */
/* The field and its control on one line. Stacked, the button ran the full
   width of the column and shouted louder than everything it sat above. */
.entry__search {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2);
  align-items: center;
  margin: var(--space-3) 0 var(--space-5);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--edge);
}

.entry__search .btn {
  padding: var(--space-2) var(--space-4);
  font-size: 0.75rem;
}

.entry__label {
  grid-column: 1 / -1;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.entry__field {
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  font-family: var(--font-mono);
  font-size: 0.875rem;
  color: var(--ink-bright);
}

.entry__field:focus-visible {
  border-color: var(--amber);
  outline: none;
}

.entry__count,
.entry__quiet {
  margin: 0 0 var(--space-3);
  max-width: none;
  font-size: 0.8125rem;
  color: var(--ink-quiet);
}

/* What a section holds. A list rather than tiles, because a holdings list is
   read down and a title is the thing being scanned for. */
.holdings {
  margin: 0;
  padding: 0;
  list-style: none;
}

.holdings__entry {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--edge-faint);
}

.holdings__title {
  font-size: 1rem;
  color: var(--amber);
  text-decoration: none;
}

.holdings__title:hover,
.holdings__title:focus-visible { color: var(--amber-bright); }

.holdings__summary {
  margin: var(--space-2) 0 0;
  max-width: 68ch;
  font-size: 0.875rem;
  color: var(--ink);
}

.holdings__mark {
  margin: var(--space-2) 0 0;
  max-width: none;
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.holdings__held { color: var(--rose); }

/* One article. The body is authored prose and is the one place on this site
   set in the reading face rather than the terminal's mono. */
.entry__head {
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--edge);
}

.entry__title {
  margin: var(--space-2) 0 var(--space-3);
  font-size: 1.5rem;
  line-height: 1.25;
  color: var(--ink-bright);
}

.entry__body {
  margin-top: var(--space-5);
  font-family: var(--font-mono);
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--ink);
}

.entry__body h1,
.entry__body h2,
.entry__body h3 {
  margin: var(--space-6) 0 var(--space-3);
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.entry__body p { max-width: 68ch; }

.entry__body ul,
.entry__body ol { max-width: 68ch; }

.entry__body a { color: var(--amber); }

.entry__body a:hover,
.entry__body a:focus-visible { color: var(--amber-bright); }

.entry__body blockquote {
  margin: var(--space-4) 0;
  padding-left: var(--space-4);
  border-left: 2px solid var(--edge-strong);
  color: var(--ink-quiet);
}

.entry__body code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  color: var(--cyan);
}

.entry__body table {
  width: 100%;
  margin: var(--space-4) 0;
  border-collapse: collapse;
  font-family: var(--font-mono);
  font-size: 0.875rem;
}

.entry__body th,
.entry__body td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--edge);
}

.entry__body th {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
}

/* A link to an article nobody has written yet. Shown rather than hidden, so
   that a typo or an intention is visible on the page instead of being found
   later by somebody clicking it. */
.dead-link {
  color: var(--rose);
  border-bottom: 1px dotted var(--rose);
  cursor: help;
}

.entry__withheld {
  margin: var(--space-5) 0 0;
  padding: var(--space-4);
  max-width: 62ch;
  border: 1px solid var(--rose);
  border-left-width: 3px;
  background: color-mix(in srgb, var(--rose) 8%, transparent);
  font-size: 0.9375rem;
  color: var(--ink);
}

.entry__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-6);
  padding-top: var(--space-3);
  border-top: 1px solid var(--edge);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.entry__foot a {
  color: var(--amber);
  text-decoration: none;
}

/* A character's record. The reading room's three columns with the record in
   the middle, and these few pieces for what a record holds that an article
   does not: sections of questions and answers, and the people in a life. */
.record__section { margin-top: var(--space-6); }
.record__label { margin-bottom: var(--space-3); }

/* Read as unknown rather than as an answer, so a gap is not mistaken for
   something the operator said. */
.record__unknown { color: var(--ink-faint); }

/* One fact to a line, inside an answer that holds several. */
.record__line { display: block; }

/* One person, with room between them and the next. */
.record__person {
  display: block;
  margin-bottom: var(--space-4);
}

.record__portrait {
  display: block;
  width: 6rem;
  height: 6rem;
  margin: var(--space-2) 0;
  object-fit: cover;
  border: 1px solid var(--edge);
}

/* ---------------------------------------------------------------------------
   Vitals
   The figures a character carries through play, at the top of the Stats page.
   Read in pairs, so laid out in rows of pairs: what you have against what you
   started with, and the control that moves it between them.
   --------------------------------------------------------------------------- */
.vitals {
  border: 1px solid var(--edge);
  background: var(--surface-raised);
  margin-bottom: var(--space-6);
}

.vitals__row {
  display: grid;
  grid-template-columns: 8rem 5rem 5rem auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
}
.vitals__row + .vitals__row { border-top: 1px solid var(--edge); }

/* The row that carries two figures rather than one. The wound state takes
   what is left between them, so Luck sits hard against the right edge and the
   two names line up with the rows below. */
.vitals__row--paired {
  grid-template-columns:
    8rem 5rem 5rem auto minmax(6rem, 1fr)
    5rem 5rem 5rem auto;
}

.vitals__name {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
  margin: 0;
}

.vitals__cell { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; }
.vitals__label {
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.vitals__figure { font-size: 1.25rem; line-height: 1.1; color: var(--ink); }
.vitals__state { font-size: 0.8125rem; color: var(--ink); overflow-wrap: anywhere; }

/* The worse it gets the louder it reads, because this is the cell somebody
   glances at rather than reads. Unharmed says nothing at all. */
.vitals__state--lightly-wounded { color: var(--amber); }
.vitals__state--seriously-wounded { color: var(--amber); font-weight: 700; }
.vitals__state--mortally-wounded { color: var(--rose); font-weight: 700; }

/* Two buttons stacked, matching the stat controls on the creation pages:
   the same act deserves the same control wherever it appears. */
.vitals__steppers { display: flex; flex-direction: column; margin: 0; }

/* A control rather than a pair of steppers, sized to its own words. The row's
   columns are auto, so without this the button would take the whole share the
   grid handed it and read as the loudest thing in the box. */
.vitals__acts { margin: 0; display: flex; }
.vitals__roll { padding: var(--space-2) var(--space-3); font-size: 0.6875rem; }
.vitals__reset {
  padding: var(--space-2) var(--space-3);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

.stepper {
  min-width: 1.75rem;
  padding: var(--space-1) var(--space-2);
  display: flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  font-size: 0.8125rem;
  line-height: 1;
  color: var(--amber);
  background: var(--surface-panel);
  border: 1px solid var(--edge);
  cursor: pointer;
}
/* Only where they are stacked as a pair with no gap between them, which is
   the plus and minus on a figure. Two of these standing apart in a column
   each want all four of their sides. */
.vitals__steppers .stepper + .stepper { border-top: 0; }
.stepper:hover,
.stepper:focus-visible { background: var(--surface-void); color: var(--ink); }

@media (max-width: 1100px) {
  /* Below this the pair cannot both have the room they need, so Luck drops
     under Hit Points and the row reads as two. */
  .vitals__row--paired {
    grid-template-columns: 8rem 5rem 5rem auto minmax(6rem, 1fr);
  }
  .vitals__row--paired > .vitals__name:nth-of-type(2) { grid-column: 1; }
}

@media (max-width: 720px) {
  .vitals__row,
  .vitals__row--paired {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3);
  }
  .vitals__name { grid-column: 1 / -1; }
  .vitals__cell--wide { grid-column: 1 / -1; }
}

/* ---------------------------------------------------------------------------
   Portrait
   The character's own likeness at the head of their record, beside the name.
   The same three by four frame the landing page draws, so the two pages show
   the same picture at the same shape.
   --------------------------------------------------------------------------- */
.record__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-5);
  justify-content: space-between;
}

/* Takes what is left after the frame, and may shrink: a long name wraps
   rather than pushing the picture off the panel. */
.record__naming { flex: 1 1 18rem; min-width: 0; }

.portrait {
  flex: 0 0 9rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.portrait__frame {
  display: grid;
  place-items: center;
  width: 9rem;
  aspect-ratio: 3 / 4;
  background: var(--surface-void);
  /* Amber rather than the usual hairline: the likeness is the one thing on
     the record that is nobody else's, and the frame says so. */
  border: 1px solid var(--amber);
  overflow: hidden;
}
.portrait__image { width: 100%; height: 100%; object-fit: cover; }
.portrait__mark {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
  text-align: center;
  padding: var(--space-3);
}

.portrait__controls {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
}

/* The browser draws this control with its own colours, which on a dark panel
   means grey on grey. Declared for both the standard selector and WebKit's
   older one, because Safari still answers only to the latter. */
.portrait__file { width: 9rem; font-size: 0.6875rem; color: var(--ink); }
.portrait__file::file-selector-button,
.portrait__file::-webkit-file-upload-button {
  margin-right: var(--space-2);
  padding: var(--space-1) var(--space-2);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--amber);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  cursor: pointer;
}

/* Under the Role grid, because what it is rolled on is the figures above it.
   Three parts on one line: the throw, what it came to, and the way to drop it
   when the fight is over. The figure takes the slack between the two
   controls, so both sit at the ends of the row the grid above them ends at. */
.initiative {
  /* The width both controls on this row are drawn at. The throw and the way
     to drop it are a pair, and a pair drawn at two sizes reads as one control
     and one afterthought. */
  --initiative-control: 6rem;

  display: flex;
  align-items: stretch;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.initiative form { margin: 0; }
.initiative__clear { width: var(--initiative-control); }
.initiative__clear .stepper { width: 100%; height: 100%; }

/* Two lines rather than one: the verb and what it acts on, which keeps the
   control narrow enough to leave the figure beside it room, and tall enough
   to be the thing on this row a hand goes for first. */
.initiative__roll {
  width: var(--initiative-control);
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: var(--space-2) var(--space-3);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  font-size: 0.6875rem;
  line-height: 1.5;
}
.initiative__what { color: var(--ink-quiet); }

/* What the last roll came to, boxed like the ranks above it because it is the
   same kind of thing: a figure the table reads off the record. Sized to its
   own contents rather than taking the slack, so the row reads left to right
   and stops where it stops. */
.initiative__rolled {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--edge);
  background: var(--surface-void);
}
.initiative__label {
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
}
.initiative__figure { font-size: 1rem; color: var(--amber); }

/* The grid sits under the handle rather than beside it, with air above it,
   because it is a block of figures following a heading rather than a line of
   text continuing one. */
.record__ranks { margin-top: var(--space-4); }

/* The Profile page puts the backstory where the Role grid stands on every
   other page, so it takes the same room above it. */
.backstory { margin-top: var(--space-4); }

.backstory__label { display: block; }

/* Resized up and down only. Dragged sideways it leaves the column it was
   written to fit and lands under the likeness beside it, which is a thing a
   box should not be able to do to the page around it. */
.backstory__text {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  resize: vertical;
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.875rem;
  line-height: 1.5;
  padding: var(--space-3);
}

.backstory__text:focus { border-color: var(--amber); outline: none; }

.backstory__actions { margin: var(--space-2) 0 0; }

/* ---------------------------------------------------------------------------
   A character who did not make it
   Said across the likeness rather than beside it, because it is the first
   thing anybody reading the record needs to know and the last thing they
   should have to look for. The record is otherwise untouched: dying does not
   archive a character, and retiring one is done by hand.
   --------------------------------------------------------------------------- */

/* Both frames centre a single child. Stacking the word over the picture means
   putting them in the same cell rather than in two rows of one. */
.portrait__frame > *,
.profile__art > * { grid-area: 1 / 1; }

.portrait__frame--dead { border-color: var(--rose); }

/* The picture stays under the word, drained and dimmed. Who they were is
   still the point of the frame; it is only in the past tense now. */
.portrait__frame--dead .portrait__image,
.portrait__frame--dead .profile__portrait,
.portrait__frame--dead .portrait__mark,
.portrait__frame--dead .profile__art-mark {
  opacity: 0.4;
  filter: grayscale(1);
}

.portrait__dead {
  place-self: center;
  padding: var(--space-1) var(--space-3);
  background: var(--rose);
  color: var(--ink-invert);
  font-family: var(--font-mono);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

/* The chosen file, on its own line above the control. Allowed to wrap, since
   a name is worth reading in two lines and worth nothing truncated. */
.portrait__chosen {
  width: 9rem;
  font-size: 0.625rem;
  line-height: 1.4;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.portrait__pick { width: 9rem; }

/* Centred under the frame they describe, because they are a caption to the
   picture above rather than a column of text in their own right. */
.portrait__guidance {
  width: 9rem;
  font-size: 0.625rem;
  line-height: 1.4;
  color: var(--ink-faint);
  text-align: center;
}

/* Two sentences, two lines, one block. A paragraph's own spacing between them
   read as two unrelated remarks rather than one piece of advice, so the column
   gap is all that separates them. */
.portrait__guidance { margin: 0; }

/* ---------------------------------------------------------------------------
   The Role Ability panel
   Between the character's name and their likeness, because it is read while
   the rest of the page is being used rather than worked through once. The
   same parts as the rest of the sheet: a name, a figure, a cost, a stepper.
   --------------------------------------------------------------------------- */
.ability {
  /* Narrow enough that the name, the panel and the likeness still make one
     row on a laptop. Past that the head wraps and the picture drops below
     the panel, which reads as two headers rather than one. */
  flex: 1 1 15rem;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ability__pick { display: flex; gap: var(--space-2); margin: 0; }

/* The browser draws a select in its own colours, which on a dark panel means
   black on grey. Declared here rather than left to it. */
.ability__select {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-1) var(--space-2);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--amber);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

/* How much of the pool is gone, in the sheet's own hand: the figure that
   matters set in type, what it is made of beside it in small. */
.ability__pool {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
  font-size: 0.75rem;
  color: var(--ink-quiet);
}
.ability__spent,
.ability__pool-size { font-size: 1rem; color: var(--amber); }
.ability__of { font-size: 0.625rem; text-transform: uppercase; }
.ability__left { margin-left: auto; font-size: 0.625rem; text-transform: uppercase; }

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

/* Name, what the points buy, what they cost, and the control, on one line
   each. The name takes the slack so the figures stay in a column. */
.ability__line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--edge-faint);
  font-size: 0.6875rem;
}
.ability__line:last-child { border-bottom: 0; }

.ability__name {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  color: var(--ink-quiet);
}

/* What the points actually buy, which is the point of the panel. Lit only
   where it is something: a nought is the absence of an effect rather than an
   effect of nought, and lighting it would say otherwise. */
.ability__effect { font-size: 0.875rem; color: var(--ink-faint); }
.ability__line--held .ability__name { color: var(--ink); }
.ability__line--held .ability__effect { color: var(--amber); }

/* What it cost, held apart from what it bought, because a Solo reading this
   mid fight is deciding whether to move points rather than admiring them. */
.ability__cost { width: 1.5rem; text-align: right; color: var(--ink-faint); }
.ability__line--held .ability__cost { color: var(--ink-quiet); }

.ability__clear { margin: 0; }
.ability__clear .stepper { width: 100%; }

/* An ability that affords something different at every rank rather than
   dividing a pool. A heading, the control that rolls it where it is rolled,
   and what this character's rank affords beneath. */
.ability__band {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--edge-faint);
}
.ability__band:last-child { border-bottom: 0; }

.ability__band-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin: 0 0 var(--space-1);
}

.ability__band-name {
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}

/* An ability bought once and kept rather than spent and respent: a Tech's
   Maker, a Medtech's Medicine. Two across where there is room for two and one
   where there is not, because a Speciality's name is not short and the panel
   beside the character's name is narrow. */
.specialities {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--space-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

.specialities__cell {
  display: flex;
  /* The top rather than the middle: one name wraps to two lines and the one
     beside it does not, and figures that drifted apart down a row would read
     as a mistake rather than as a long name. */
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--edge-faint);
  background: var(--surface-void);
  font-size: 0.6875rem;
}

.specialities__name { color: var(--ink-faint); }
.specialities__points { font-size: 1rem; color: var(--ink-faint); }

/* Lit only where something was put. A Speciality at nought is a decision the
   character made as much as one at three, but it is not a capability. */
.specialities__cell--held .specialities__name { color: var(--ink); }
.specialities__cell--held .specialities__points { color: var(--amber); }

/* A fold inside the Role Ability panel. The panel sets its own measure, so
   these are drawn to its sides rather than pulled out to the page's as the
   skill folds below them are. */
.ability__fold {
  margin-inline: 0;
  padding-inline: 0;
}
.ability__fold .fold__head {
  margin-inline: 0;
  padding-inline: 0;
  font-size: 0.625rem;
}
.ability__fold .ability__lines { padding-top: var(--space-1); }

/* The three difficulties are one control repeated, so they are drawn at one
   width. Left to their own labels, DV8 came out narrower than DV12 and the
   column of them read as ragged. Wide enough for the longest of them. */
.ability__band .sheet__roll .stepper { min-width: 4.5rem; }

.ability__band-text {
  margin: 0;
  font-size: 0.6875rem;
  line-height: 1.45;
  color: var(--ink-quiet);
}

/* An ability whose parts nobody has written down yet. Said rather than hidden:
   a rank in it is a fact about the character either way. */
.ability__pending {
  margin: 0;
  font-size: 0.6875rem;
  line-height: 1.4;
  color: var(--ink-faint);
}

/* A control with nothing left to do is shown refusing rather than removed, so
   the row does not change shape as the last point goes on or comes off. */
.stepper[disabled] { opacity: 0.35; cursor: not-allowed; }

/* ---------------------------------------------------------------------------
   Sheet
   The two tables under the condition box: the aptitudes, and the whole
   competency catalogue folded into its sections. Figures right, controls
   narrow, and the total picked out because it is the number that gets rolled.
   --------------------------------------------------------------------------- */

/* A label with a control on the far side of it. */
.record__banner {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}
.record__banner form { margin: 0; }

/* A table sizes to its content unless told otherwise, which left the skill
   names wrapping inside a table half the width of the panel it sits in. The
   figures and controls take what they need and the name takes everything
   left, so a name wraps only when it genuinely cannot fit. */
.sheet {
  width: 100%;
  table-layout: auto;
  border-collapse: collapse;
}

/* A browser gives a cell no padding of its own, which left every column piled
   against the one before it and the whole row reading as a single run of
   figures. The padding is what separates a skill from its stat from its
   level, and it is what lets the row use the width the panel already has. */
.sheet td,
.sheet th {
  vertical-align: middle;
  padding: var(--space-2) var(--space-3);
}

/* A heading sits over its own column rather than in the middle of it, which
   for a figure means hard right, the same side as the figure beneath it. */
.sheet th {
  text-align: left;
  font-weight: 400;
  color: var(--ink-quiet);
}
.sheet th.sheet__figure { text-align: right; }

/* The name takes every spare pixel and starts at the panel's left edge, so
   the columns after it are pushed out to the right rather than huddling. */
.sheet td:first-child,
.sheet th:first-child { width: 100%; padding-left: 0; }
.sheet__figure { text-align: right; width: 3.5rem; }
.sheet__figure--total { color: var(--amber); }

/* Roll ends the row at the panel's right edge, a clear gap from the figure it
   is rolling against, because it is the one control in the row that asks a
   question rather than changing the record. */
.sheet__roll-cell {
  width: 1%;
  text-align: right;
  white-space: nowrap;
  padding-right: 0;
  padding-left: var(--space-4);
}

/* A narrow screen scrolls the table sideways rather than crushing it, which
   keeps a skill name on one line at any width. */
.table-wrap { overflow-x: auto; }

/* A modifier standing against something reads as a change rather than as a
   figure, because that is what it is: a reason the number below it is not the
   number on the record. */
.sheet__figure--moved,
.vitals__figure--moved { color: var(--amber); }
.vitals__figure--total { color: var(--amber); }

/* Side by side rather than stacked, because a table row is already short and
   two stacked buttons would double every row's height. */
.sheet__steppers { flex-direction: row; gap: var(--space-1); }
.sheet__steppers .stepper { min-width: 1.5rem; padding: 0 var(--space-2); }

/* Shut on arrival, because seventy competencies open is a wall rather than a
   sheet. Native details and summary, so it folds without a line of script. */
.fold {
  margin-inline: calc(-1 * var(--panel-x));
  padding-inline: var(--panel-x);
  border-bottom: 1px solid var(--edge-faint);
}
.fold__head {
  cursor: pointer;
  /* Out past the fold's own padding and back again, so the rule under an open
     section reaches the panel's sides as the headings' rules do. */
  margin-inline: calc(-1 * var(--panel-x));
  padding: var(--space-3) var(--panel-x);
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}
.fold__head::marker { color: var(--amber-dim); }
.fold[open] .fold__head { border-bottom: 1px solid var(--edge-faint); }

/* Said for screen readers and nobody else: a column of controls needs a
   heading, and the heading would be noise on screen. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   Flash
   What just happened, over the row it happened on, gone again in a few
   seconds. No script: a CSS animation ends at hidden, so it clears itself
   however the page is being read.
   --------------------------------------------------------------------------- */
.vitals__row--saving { position: relative; }

.flash {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-panel);
  border-left: 2px solid var(--edge-strong);
  font-size: 0.875rem;
  /* Never in the way. A message about the last roll must not stop the next
     one, however long it lingers. */
  pointer-events: none;
  animation: flash-away 5s forwards;
}
.flash--passed { border-left-color: var(--green); color: var(--green); }
.flash--failed { border-left-color: var(--rose); color: var(--rose); }

@keyframes flash-away {
  0%,
  70% { opacity: 1; }
  100% { opacity: 0; visibility: hidden; }
}

/* Somebody who has asked for less movement gets no fade, and the message
   clears on the next thing they do instead. It cannot block anything while it
   waits, which is what makes that acceptable. */
@media (prefers-reduced-motion: reduce) {
  .flash { animation: none; }
}

/* ---------------------------------------------------------------------------
   Roller
   The dialogue a competency is rolled in: what is being rolled, the modifier
   that applies to this roll alone, and the result in the same place.
   --------------------------------------------------------------------------- */
.roller {
  width: min(26rem, calc(100vw - 2 * var(--gutter)));
  padding: var(--space-5);
  background: var(--surface-panel);
  border: 1px solid var(--edge-strong);
  color: var(--ink);
}
.roller::backdrop { background: rgb(0 0 0 / 0.6); }

/* The name of what is being rolled sits on the same line as the way out, so
   the dialogue opens on its contents rather than on a row holding one button.
   The name takes the slack and wraps rather than pushing the control away. */
.roller__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.roller__close { display: flex; margin: 0; }

.roller__name {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.roller__form {
  /* The height both controls in this row are drawn at. */
  --roller-control: 2.5rem;

  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  margin: 0;
}
/* No margin of its own: a label carries one by default, and with the row
   aligned to its bottom that margin lifted the box clear of the button beside
   it by exactly its own height. */
.roller__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  flex: 1 1 auto;
  margin: 0;
}
.roller__label {
  font-size: 0.625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber-dim);
}
.roller__input {
  width: 100%;
  height: var(--roller-control);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-void);
  border: 1px solid var(--edge);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.875rem;
}

/* The box and the button are one row of controls and are drawn as one. Left to
   their own padding they came out different heights, and bottom alignment then
   put the button's top below the box's. A shared height settles it: the two
   line up top and bottom whatever either is padded by. */
.roller__form .btn {
  height: var(--roller-control);
  align-items: center;
  padding-block: 0;
}

/* Centred, because the result is the one thing in the dialogue that is read
   rather than used, and a verdict of two words over a short sum has no left
   edge worth lining anything up against. */
.roller__result {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--edge-faint);
  text-align: center;
}

/* Said before the number and in the loudest thing on the panel, because a
   critical changes what the number means. */
.roller__critical {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  margin-bottom: var(--space-1);
}
.roller__critical--success { color: var(--green); }
.roller__critical--failure { color: var(--rose); }

/* The answer. Everything else in here is working.

   Coloured by the verdict where the check was rolled against a difficulty, so
   that a pass and a failure are told apart before either is read. Amber where
   there was no difficulty and the roll is a figure rather than an outcome. */
.roller__total {
  font-size: 1.5rem;
  line-height: 1.2;
  color: var(--amber);
}
.roller__total--passed { color: var(--green); }
.roller__total--failed { color: var(--rose); }

.roller__detail {
  margin-top: var(--space-1);
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--ink-faint);
}

/* The sum under a verdict, on its own line and small, for the operator
   without a script who gets the report in the page rather than a dialogue. */
.flash__sum {
  display: block;
  font-size: 0.75rem;
  color: var(--ink-faint);
}

/* The same report where no script opened a dialogue to put it in: in the flow
   of the page rather than over a row, since there is no one row it belongs
   to. */
.flash--static {
  position: static;
  margin-bottom: var(--space-4);
}

/* Roll sits apart from the steppers beside it: one changes the record and the
   other asks the dice a question. */
.sheet__roll { margin: 0; }
.sheet__roll .stepper { white-space: nowrap; padding: var(--space-1) var(--space-2); }

/* ---------------------------------------------------------------------------
   Sheet sections
   Condition, Stats and Competencies, each announced rather than labelled, and
   each separated from the last by a rule across the panel.
   --------------------------------------------------------------------------- */
.sheet__title {
  /* Pulled out by the panel's side padding and given its own back, so the
     rule reaches the panel's edges while the words stay where they were. */
  margin: var(--space-6) calc(-1 * var(--panel-x)) var(--space-4);
  padding: var(--space-5) var(--panel-x) 0;
  border-top: 1px solid var(--edge);
  font-size: 1.125rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

/* The first of them follows the header's own rule, and two lines a few pixels
   apart is a mistake rather than a division. */
.sheet__title:first-of-type {
  margin-top: var(--space-4);
  padding-top: 0;
  border-top: 0;
}

/* The heading and the control that belongs to the whole section, on one line.

   The rule that divides the section from the one above it is drawn by the
   banner rather than by the heading inside it. Wrapping the heading made it
   the first of its type within its own parent, which handed it the exception
   written for Condition below and took its dividing line away. */
.sheet__banner {
  align-items: center;
  margin: var(--space-6) calc(-1 * var(--panel-x)) var(--space-4);
  padding: var(--space-5) var(--panel-x) 0;
  border-top: 1px solid var(--edge);
}
.sheet__banner .sheet__title {
  /* All of it, not only the top. The banner already pulls itself out to the
     panel's sides and puts the padding back, so a heading that kept its own
     side padding was indented by it a second time while the control beside it
     sat where it should. */
  margin: 0;
  padding: 0;
  border-top: 0;
}

/* ---------------------------------------------------------------------------
   Stats
   Nine aptitudes, three by three, each laid out as the Condition rows are:
   a name, the figures it is made of, the control, and the total. The parts
   are the Condition box's own, so the two sections read as one sheet rather
   than as two designs.
   --------------------------------------------------------------------------- */
.stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  margin-bottom: var(--space-6);
  border: 1px solid var(--edge);
  background: var(--surface-raised);
}

.stat-cell {
  display: grid;
  /* The first column holds a box now rather than four bare letters, and a
     box needs its own padding and border on top of the longest name. */
  grid-template-columns: 4.5rem minmax(0, 1fr) minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  /* One hairline between cells rather than a box around each: three boxes to
     a row with a gap between them was nine panels where one is wanted. */
  border-top: 1px solid var(--edge-faint);
  border-left: 1px solid var(--edge-faint);
}
.stat-cell:nth-child(-n + 3) { border-top: 0; }
.stat-cell:nth-child(3n + 1) { border-left: 0; }

/* Smaller than the Condition box's, because nine of them share the width
   three rows had. */
.stat-cell .vitals__name { font-size: 0.6875rem; }
.stat-cell .vitals__figure { font-size: 1rem; }
.stat-cell .vitals__label { font-size: 0.5625rem; }
.stat-cell .stepper { min-width: 1.25rem; font-size: 0.6875rem; padding: 0 var(--space-1); }

/* Fewer columns as the room runs out, and the hairlines follow: whichever
   cells now start a row lose their left edge, and the first row loses its
   top one. */
@media (max-width: 1100px) {
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-cell:nth-child(3n + 1) { border-left: 1px solid var(--edge-faint); }
  .stat-cell:nth-child(-n + 3) { border-top: 1px solid var(--edge-faint); }
  .stat-cell:nth-child(-n + 2) { border-top: 0; }
  .stat-cell:nth-child(2n + 1) { border-left: 0; }
}

@media (max-width: 620px) {
  .stats { grid-template-columns: minmax(0, 1fr); }
  .stat-cell { border-left: 0; border-top: 1px solid var(--edge-faint); }
  .stat-cell:first-child { border-top: 0; }
}

/* ---------------------------------------------------------------------------
   The Inventory page's tabs
   --------------------------------------------------------------------------- */

/* Links rather than script, so a tab can be linked to and a refresh comes
   back to the same one. Pulled out by the panel's side padding, as the sheet
   headings are, so the rule under the strip reaches the panel's edges while
   the words stay where they were. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  margin: var(--space-4) calc(-1 * var(--panel-x)) var(--space-5);
  padding: 0 var(--panel-x);
  border-bottom: 1px solid var(--edge);
}

/* The current tab's underline sits on the strip's own rule rather than above
   it, which is what makes the open tab read as continuous with the panel
   below instead of as a button floating over it. */
.tabs__way {
  display: block;
  margin-bottom: -1px;
  padding: var(--space-3) var(--space-4);
  border-bottom: 2px solid transparent;
  font-size: 0.8125rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
  text-decoration: none;
}

.tabs__way:hover,
.tabs__way:focus-visible {
  border-bottom-color: var(--edge-strong);
  color: var(--amber);
}

.tabs__way--here {
  border-bottom-color: var(--amber);
  color: var(--amber);
}

/* A line under a grid of figures, saying where one of them came from. Pulled
   up against the grid it explains, because the grid's own margin was written
   for the heading that used to follow it and leaves a note floating between
   the two sections it could belong to. */
.sheet__note {
  /* To the width of the grid it explains rather than to the reading measure.
     Held to the measure it stopped fifty pixels short of the grid's right
     edge, which reads as a line that has not finished rather than as one
     that has. */
  max-width: none;
  margin: calc(-1 * var(--space-5)) 0 var(--space-6);
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--amber);
  background: var(--surface-raised);
  font-size: 0.875rem;
  color: var(--ink-quiet);
}

.sheet__note-name { color: var(--amber); }

/* The first heading under the strip has the strip's rule above it already. */
.inventory .sheet__title:first-of-type { margin-top: 0; }

/* ---------------------------------------------------------------------------
   The Profile page
   --------------------------------------------------------------------------- */

/* Released from the reading measure for the reason the sheet's controls and
   the Inventory's tabs are: these are grids and cards rather than prose, and
   held to 68ch the page was one tall column with two thirds of the panel
   empty beside it.

   Called dossier rather than profile because the landing page's own .profile
   is a two column grid of artwork and detail, and a wrapper sharing the name
   inherited it. */
.reading__text > .dossier { max-width: none; }

/* A label and a short value are a cell, several to a row. The pairs wrap
   themselves rather than being placed, so the same markup is three columns
   on a desktop and one on a phone without a breakpoint for each. */
.facts,
.story__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  margin: 0 0 var(--space-5);
  /* Neighbours share one rule rather than drawing two against each other:
     every cell draws its right and bottom, and the table draws the top and
     left they have no neighbour on. Separate boxes with a gap between them
     read as a scattering of cards; a table reads as one thing with parts,
     which is what a record is.

     The right and bottom are the table's too, so that a short last row
     still closes: with the cells alone, a section whose answers did not
     fill the final row left the rule stopping two thirds of the way across
     and the corner hanging open. Where a cell does reach the edge its own
     rule lands on the table's, which is one line in the same colour. */
  border: 1px solid var(--edge);
  /* The same back as the cells carry, so that the space left by a short
     last row is an empty cell rather than a hole: enclosed by the same
     rules and filled in the same colour, it reads as part of the table. */
  background: var(--surface-raised);
}

/* The Lifepath is a column of it: the questions are answered in the
   operator's own words and run to sentences, which a quarter of the panel
   cannot hold, but it is the same table and reads as one. */
.story__list { grid-template-columns: minmax(0, 1fr); }

.facts__pair {
  padding: var(--space-3) var(--space-4);
  border-right: 1px solid var(--edge);
  border-bottom: 1px solid var(--edge);
  background: var(--surface-raised);
}

.facts__pair > .story__said { margin-bottom: 0; }

/* Amber, as every other heading on the record is. The size tells a cell's
   label apart from the section's title above it; the colour says they are
   the same kind of thing. */
.story__question,
.connection__label { color: var(--amber); }

/* A cell holding several short facts rather than one, given the whole row so
   that its list does not wrap to four lines in a quarter of the panel and
   push everything beside it down.

   Its own top rule, because a cell's top is normally drawn by the bottom of
   whatever sits above it, and a full width cell under a short row has
   nothing above most of itself: the line ran under the one filled cell and
   stopped, leaving the rest of the box open along the top. */
.facts__pair--wide {
  grid-column: 1 / -1;
  border-top: 1px solid var(--edge);
}

.facts__list {
  margin: 0;
  padding-left: var(--space-5);
  list-style: disc;
}

.facts__list li { margin-bottom: var(--space-1); }



/* Each group of people, under the one People heading. */
.connections__group + .connections__group { margin-top: var(--space-5); }

.connections__group .sheet__title {
  font-size: 0.875rem;
  color: var(--ink);
}

.connections__none { margin: 0; }

/* Cards rather than a list, because each of these is a small record and not
   a value. They wrap at whatever width the panel is. */
/* One card to a row, the width of the panel. A grid of narrow cards wrapped
   every line in them and made an enemy four lists tall. */
.connections {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.connection {
  display: flex;
  flex-direction: column;
  /* Said rather than left to the default, because the creation wizard owns a
     .person that sets flex-start, and while this block was called .person
     the card inherited it: every line in it sized to its own text and the
     grid across the card collapsed to one narrow column. Renamed since, and
     this stays as the statement of what the card wants. */
  align-items: stretch;
  padding: var(--space-4);
  border: 1px solid var(--edge);
  background: var(--surface-raised);
}

.connection__face {
  display: block;
  width: 7rem;
  margin-bottom: var(--space-3);
  border: 1px solid var(--amber-dim);
}

.connection__name {
  margin: 0 0 var(--space-3);
  font-weight: 700;
  color: var(--amber);
}

/* Across the card rather than down it, now that a card is the width of the
   panel: an enemy's four lists sit side by side instead of stacking. */
.connection__lines {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
}

.connection__said:last-child { margin-bottom: 0; }

.connection__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  margin-bottom: var(--space-1);
}

.connection__said {
  margin: 0 0 var(--space-3);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--ink);
}

.connection__notes {
  margin: 0 0 var(--space-3);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--ink-quiet);
}

/* Pushed to the foot of the card, so that the controls sit below whatever
   the card holds rather than floating up beside it. */
.connection__actions {
  display: flex;
  gap: var(--space-2);
  margin: auto 0 0;
  padding-top: var(--space-3);
}

/* Small enough that Add beside a heading does not outweigh it. */
.btn--small {
  padding: var(--space-1) var(--space-3);
  font-size: 0.75rem;
}

/* The add, edit and remove form, in the window or on the page behind it. */
.connection-form { max-width: 34rem; }

.connection-form__warning {
  margin: 0 0 var(--space-5);
  color: var(--ink);
}

.connection-form__actions {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

/* ---------------------------------------------------------------------------
   The Inventory's tables
   --------------------------------------------------------------------------- */

/* The same hairline table the Profile uses, because a list of what somebody
   owns and a list of what they answered are the same kind of thing to read.

   Laid out fixed rather than by content. Left to size itself the table was
   as wide as its longest row and took the whole page sideways with it on a
   phone: the scrolling wrap around it scrolled, and the page scrolled too.
   Fixed, it divides the panel it is given and the cells wrap inside their
   columns. */
.kit {
  width: 100%;
  /* Below this it stops being readable: five columns in a phone's width
     gave a column of single letters down the page. It scrolls sideways
     inside its wrap instead, which is what the wrap is for. */
  min-width: 34rem;
  table-layout: fixed;
  border-collapse: collapse;
  background: var(--surface-raised);
}

.kit th,
.kit td {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--edge);
  text-align: left;
  vertical-align: middle;
  font-size: 0.8125rem;
}

/* Long names break rather than widening their column, which a fixed table
   would not let them do anyway. Not on the headings: a column called
   CATEGORY broke after CATEGOR and read as a mistake. */
.kit tbody td,
.kit tbody th { overflow-wrap: anywhere; }

.kit thead th {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.kit__name { font-weight: 400; }

/* Out to the panel's own edges, which is what --panel-x is held for. A
   list of eleven columns inside the panel's side padding is a list reading
   through a letterbox, and the padding is there for prose rather than for a
   table. */
.inventory .table-wrap {
  margin-inline: calc(-1 * var(--panel-x));
  padding-inline: var(--panel-x);
}

/* Sized by its content rather than by a written division. The eight tabs
   carry between four and eleven columns: the Ranged tab holds a magazine, a
   range table and what is loaded, and the Gear tab holds a name and a
   number. One set of percentages cannot divide both, and the fixed layout
   that served one five column table gave the Gear tab four columns of
   whitespace and the Ranged tab eleven columns of single letters.

   It scrolls sideways inside its wrap instead, which is what the wrap is
   for, and the panel around it does not move. */
.inventory .kit {
  table-layout: auto;
  min-width: 0;
}

/* The tab's name with its Add beside it. Not record__banner, which pushes
   its second child to the far edge: that is right for a Clear control,
   which should be nowhere near the thing it clears, and wrong for an Add,
   which belongs against the heading of the list it adds to. */

/* The aptitude's own panel, opened by its name. The name is the control,
   so it has to look like one; the panel is lifted out of the flow, so
   opening it does not move the eight cells around it, which is what put
   the figures out of line when the form sat in the grid. */
/* Centred in the row rather than sitting on its own baseline. Every other
   thing in the cell is two lines, a label over a figure, so a one line
   control underlined along the bottom of it read as though it had fallen
   to the floor. */
.stat-cell__panel {
  position: relative;
  align-self: center;
}

/* A box rather than an underline. It says the name is a control without
   putting a line under it, and a box has a height of its own, so centring
   it in the row is something the eye can actually see. */
.stat-cell__opens {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-2) var(--space-2);
  border: 1px solid var(--edge-strong);
  background: var(--surface-void);
  cursor: pointer;
  list-style: none;
  text-align: center;
}

.stat-cell__opens::-webkit-details-marker { display: none; }

/* The same control on the Humanity row's name, which sits in a grid cell
   rather than a stat cell: held to its own width and ranged left with the
   other names, and kept off the chooser's flex basis, which in a column
   stretched the whole row. */
.vitals__panel {
  flex: none;
  justify-self: start;
}

.vitals__opens { justify-content: flex-start; }

.stat-cell__opens:hover {
  border-color: var(--amber-dim);
  color: var(--amber);
}

/* Open, it wears the colour of the panel hanging off it, so that a glance
   down the grid says which one is being changed. */
.stat-cell__panel[open] .stat-cell__opens {
  border-color: var(--amber);
  color: var(--amber);
}

/* Wide enough for the longest thing in it, which is the question about the
   hit points, and anchored left so a panel on the third column does not
   open off the edge of the page. */
.stat-cell__panel .chooser__list {
  left: 0;
  width: 14rem;
  max-height: none;
}

/* The two figures side by side, to be read against each other. */
.aptitude {
  width: 100%;
  border-collapse: collapse;
}

.aptitude th,
.aptitude td {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--edge);
  text-align: left;
  font-size: 0.8125rem;
}

.aptitude thead th {
  font-size: 0.5625rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.aptitude tbody th {
  font-weight: 400;
  color: var(--ink-faint);
}

.aptitude__figure { color: var(--ink); }

.aptitude__actions {
  margin: var(--space-3) 0 0;
  text-align: center;
}

/* Two figures wide, which is every aptitude a character will ever carry. The
   box sets the width of the right hand column, and on BODY and WILL the
   column beside it has to hold the question about the hit points, so a box
   with room to spare was pushing the pair of them past the edge of the panel
   and raising a scrollbar under them.

   The spinner goes with it: Luke types the figure, and two arrows that step
   a number by one are a control nobody uses on a number read off a piece of
   chrome. Both spellings, because the standard one is recent and the
   browsers that matter still answer to the other. */
input[type="number"].stat-cell__typed {
  width: 3.25rem;
  min-width: 0;
  appearance: textfield;
  -moz-appearance: textfield;
}

input[type="number"].stat-cell__typed::-webkit-outer-spin-button,
input[type="number"].stat-cell__typed::-webkit-inner-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

.kit__heading {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  margin: 0 0 var(--space-4);
}

.kit__heading .sheet__title { margin: 0; }

/* ---------------------------------------------------------------------------
   The form that writes the next line
   --------------------------------------------------------------------------- */

/* A block rather than a row of cells. A control in a table cell sets that
   column's width, and the Ranged tab's thirteen ammunition boxes set the
   width of the page.

   A grid that wraps, so a form of four fields is one line and a form of ten
   is three, without either being told how many it has. */
.adding {
  margin: var(--space-5) 0 0;
  padding: var(--space-4);
  border: 1px solid var(--edge);
  background: var(--surface-deep);
}

.adding__title {
  margin: 0 0 var(--space-4);
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.adding__form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  align-items: flex-end;
  margin: 0;
}

.adding__field {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  flex: 1 1 11rem;
}

.adding__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.adding__field input,
.adding__field select,
.adding__field textarea {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
}

.adding__actions {
  margin: 0;
  flex: 0 0 auto;
}

/* Thirteen kinds of round is thirteen boxes, and thirteen boxes in a form
   of ten fields is taller than everything else in it put together. Folded
   behind its own name, which is what a details element is for and what
   works with no script at all. */
.chooser {
  align-self: flex-end;
  flex: 1 1 11rem;
}

.chooser__summary {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--edge);
  background: var(--surface-raised);
  cursor: pointer;
  font-size: 0.8125rem;
  list-style-position: inside;
}

/* Over what follows rather than pushing it down, so that opening the list
   does not move the button somebody was about to press. */
.chooser[open] { position: relative; }

.chooser__list {
  position: absolute;
  z-index: 20;
  min-width: 100%;
  max-height: 14rem;
  overflow-y: auto;
  padding: var(--space-3);
  border: 1px solid var(--amber-dim);
  background: var(--surface-raised);
}

.chooser__list ul,
.chooser__list div {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Against the right hand edge, with the box after the name. Thirteen names
   of wildly different lengths ranged left leaves thirteen boxes in a ragged
   column; ranged right they line up and the eye runs down them. */
.chooser__list label {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  justify-content: flex-end;
  padding: 2px 0;
  font-size: 0.8125rem;
  white-space: nowrap;
  text-align: right;
}

/* The box after its name rather than before it, which is what ranging the
   row right means for a control of two parts. */
.chooser__list label input { order: 2; }

/* The same folded list, in a table cell. Narrower, because it shares a row
   with ten other columns, and its summary is the answer rather than the
   question: a weapon's line says what is in it, and opening it is how you
   change that. */
.chooser--row { flex: none; }

.chooser--row .chooser__summary {
  padding: var(--space-1) var(--space-2);
  font-size: 0.75rem;
  white-space: nowrap;
}

.chooser--row .chooser__list { right: 0; }

.chooser__actions {
  margin: var(--space-2) 0 0;
  text-align: right;
}

.kit__control { overflow: visible; }

/* kit.js moves the block above into this. Styled here rather than written
   by the script, so that what a thing looks like stays with everything else
   that has a look, and so the form reads the same in the page as in the
   window. */
.adding__window {
  width: min(44rem, calc(100vw - 2 * var(--space-5)));
  padding: 0;
  border: 1px solid var(--amber-dim);
  background: transparent;
  color: var(--ink);
}

.adding__window::backdrop { background: rgb(0 0 0 / 0.6); }

/* The window a catalogue item is bought in, which carries the item's whole
   sheet: wider, and scrolling inside itself rather than past the screen. */
.adding__window--wide {
  width: min(60rem, calc(100vw - 2 * var(--space-5)));
  max-height: calc(100vh - 2 * var(--space-5));
  overflow-y: auto;
}

/* Drawn open with no script, it sits over the list where it is drawn
   rather than pushing the list down. */
.adding__window--wide[open]:not(:modal) {
  position: absolute;
  z-index: 30;
  inset-inline: 0;
  margin-inline: auto;
}

.adding__sheet { margin: 0 0 var(--space-4); }

.adding__close a { text-decoration: none; }

.adding__window .adding {
  margin: 0;
  border: 0;
}

.adding__close {
  display: flex;
  justify-content: flex-end;
  margin: 0 0 var(--space-2);
}

.kit__remove { white-space: nowrap; }

/* Said once, across the whole table, when there is nothing on the tab. */
.kit__empty {
  color: var(--ink-faint);
  font-style: italic;
}

/* ---------------------------------------------------------------------------
   The balance, at the head of the Inventory
   --------------------------------------------------------------------------- */

/* Where the Roles grid stands on every other page of the record. A page
   about what somebody is carrying wants the money at its head: a player
   buying something reads it, spends from it and writes the thing down
   without leaving the page. */
.balance {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  margin: var(--space-4) 0 0;
}

/* Beside the box rather than above it. Given no flex basis of its own, so
   it takes the width of the figure it is holding and the controls follow
   it along the same line. */
.balance__held {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  margin: 0;
}

.balance__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.balance__figure {
  font-size: 1.25rem;
  color: var(--ink);
}

.balance__unit {
  font-size: 0.8125rem;
  color: var(--ink-faint);
}

/* Room for five figures and no more. It holds an amount of eurodollars,
   which is three or four digits, and a box the width of the panel invited
   the reader to think something longer belonged in it. */
input[type="number"].balance__amount { width: 5.5rem; }

/* The balance, the home and the Lifestyle along one line, wrapping on a
   phone. Asked for by Luke on 19 October 2026: the choices sit beside the
   money they are paid from. */
.holdings-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-6);
  align-items: flex-start;
}

.holdings-head .balance { margin: 0; }

.living {
  display: grid;
  gap: var(--space-1);
  margin: 0;
}

.living--total {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
}

.living__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

/* Folded behind its answer, as a weapon's loading is, so the head of the
   page stays three figures until somebody wants to change one. */
.living__chooser[open] { position: relative; }

.living__answer {
  cursor: pointer;
  color: var(--ink);
}

.living__quiet {
  font-size: 0.8125rem;
  color: var(--ink-faint);
}

.living__form {
  position: absolute;
  z-index: 20;
  display: grid;
  gap: var(--space-3);
  min-width: 18rem;
  margin: var(--space-2) 0 0;
  padding: var(--space-4);
  border: 1px solid var(--amber-dim);
  background: var(--surface-raised);
}

/* A section per catalogue section, one under another on the tab. */
.kit-section + .kit-section { margin-top: var(--space-6); }

.kit__manual {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.adding__ways {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0;
}

.adding__kinds {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  padding: 0;
  border: 0;
}

.adding__remove { margin: var(--space-5) 0 0; }

/* ---------------------------------------------------------------------------
   The Combat page
   --------------------------------------------------------------------------- */

/* The two armour figures, side by side, because a fight asks about both at
   once and reading one above the other puts the body below the fold on a
   phone. */
.armour {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: 0 0 var(--space-5);
}

.armour__piece {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--edge);
  background: var(--surface-raised);
}

input[type="number"].armour__standing { width: 4.5rem; }

/* The loose dice: how many, what to add, and a button per die. One row,
   because the whole control is read and pressed in a single glance during
   somebody else's turn. */
.dice {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  margin: 0 0 var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--edge);
  background: var(--surface-deep);
}

.dice__label {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

input[type="number"].dice__count,
input[type="number"].dice__modifier { width: 4.5rem; }

/* The modifier beside a roll, on the Combat page where the throw is a plain
   submission rather than a dialogue. Narrow, because it holds a number that
   is almost always one or two digits and a sign. */
input[type="number"].roll__modifier { width: 4rem; }

/* ---------------------------------------------------------------------------
   The Costco Catalogue
   --------------------------------------------------------------------------- */

/* The panel is a centred grid item, and a grid item is sized by its content
   unless it is told not to be. The list inside it keeps a minimum width so
   its four columns stay readable, and on a phone that minimum was pushing
   the whole page wider than the screen: the panel, the heading and the
   footer all came out at 610 pixels on a 390 pixel screen, and the page
   scrolled sideways as well as the table.

   Told not to be. The table still scrolls inside its own wrap, which is
   what the wrap is for, and nothing around it moves. Scoped to the
   catalogue rather than every record, because the same is probably true of
   any panel holding a wide table and that is a change worth making with
   every page in front of us. */
.record--catalogue { min-width: 0; }

/* What a kind of thing is, said once under its heading, and what an item
   does, said once under its facts. Both are prose, and prose is held to a
   measure everywhere else on the terminal, which is right for an article and
   wrong here: the catalogue's middle column is a column of tables, and a
   sentence stopping two thirds of the way across one reads as a line that
   failed to load rather than as a line that has finished. So both run the
   width of the panel, with the tables they sit between.

   The standfirst is reached through the catalogue rather than on its own,
   because the Library's own pages are articles and their measure is the
   point of them. */
.reading--catalogue .entry__standfirst,
.catalogue__rule {
  max-width: none;
}

.catalogue__rule {
  margin: 0 0 var(--space-5);
  color: var(--ink);
}

/* The filters. A row of fields that wraps rather than a column, because
   there are six of them and a column of six pushes the list itself off a
   laptop screen before anybody has read a price. */
.sieve {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  align-items: flex-end;
  margin: 0 0 var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--edge);
  background: var(--surface-deep);
}

/* Wide enough for the longest option any of the lists holds, which is an
   availability: "Specialist Seller". */
.sieve__field {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  flex: 1 1 12rem;
  min-width: 0;
}

/* A price takes four digits and never will take more, so it is not given
   the same share as a list of weapon types. */
.sieve__field--short { flex: 0 1 7rem; }

.sieve__field label {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
}

.sieve__field input,
.sieve__field select {
  width: 100%;
  min-width: 0;
}

.sieve__actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  margin: 0;
  flex: 0 0 auto;
}

/* The tick-box filters, for a kind that declares them. A row of drop-downs
   that wraps, so nine of them sit in two lines on a laptop rather than
   pushing the list off the screen. Each opens over what is below it rather
   than shoving it down, because a filter is opened to tick and closed. */
.sieve__facets {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  flex: 1 1 100%;
  margin: 0;
}

.sieve__facet {
  position: relative;
  flex: 0 1 auto;
}

.sieve__summary {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--edge);
  background: var(--surface-panel);
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}

.sieve__summary::-webkit-details-marker { display: none; }

.sieve__summary::after {
  content: "\25BE";
  color: var(--ink-faint);
}

.sieve__facet[open] > .sieve__summary {
  border-color: var(--amber-dim);
}

.sieve__name {
  font-size: 0.75rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ink-quiet);
}

.sieve__state { color: var(--amber); }

.sieve__options {
  position: absolute;
  z-index: 5;
  top: calc(100% + var(--space-1));
  left: 0;
  display: grid;
  gap: var(--space-1);
  min-width: 14rem;
  max-height: 20rem;
  overflow-y: auto;
  margin: 0;
  padding: var(--space-3);
  border: 1px solid var(--edge-strong);
  background: var(--surface-raised);
}

/* Ordinary text rather than the small capitals every other label on the
   site wears: these are things to pick, not headings. */
.sieve__option {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  cursor: pointer;
  font-size: 0.875rem;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
}

.sieve__none {
  margin: 0;
  color: var(--ink-faint);
}

/* A heading that sorts is a link, but reads as a heading: amber and
   capitals like the others, with an arrow on the one sorted by. */
.kit__sort {
  color: inherit;
  text-decoration: none;
}

.kit__sort:hover,
.kit__sort:focus-visible {
  color: var(--amber-bright);
  text-decoration: underline;
}

.kit__sorted {
  margin-left: var(--space-1);
  font-size: 0.625rem;
}

/* Four columns, and the name is the one being read down, so it takes what
   the three short ones do not want. Legality is given enough for
   "Confederation Licence" on one line: the table breaks a word rather than
   overflow a cell, and "Confederatio n Licence" is not worth the narrower
   column. */
.kit__col--thing { width: 40%; }
.kit__col--price { width: 13%; }
.kit--catalogue .kit__col--legality { width: 26%; }
.kit__col--sold { width: 21%; }

/* A list of three hundred links is not prose with a reference in it: every
   row's name is one, and underlining all of them turns the column into a
   fence. The amber marks them, and the underline comes back under the
   pointer and on the keyboard, so a row still answers when asked. */
/* The catalogue's tables size their columns to what is in them, so a
   heading such as CLASSIFICATION and a band such as Confederation Licence
   keep their words whole, and the name takes what the short columns leave.
   Words break only between themselves here: breaking anywhere let the
   browser squeeze a column down to a few letters. */
.kit.kit--catalogue { table-layout: auto; }

.kit--catalogue tbody td,
.kit--catalogue tbody th { overflow-wrap: normal; }

/* Six columns in the reading panel: a little less air either side of each
   is what keeps the last one on the page. */
.kit--catalogue th,
.kit--catalogue td { padding-inline: var(--space-3); }

.kit--catalogue .kit__name a {
  text-decoration: none;
}

.kit--catalogue .kit__name a:hover,
.kit--catalogue .kit__name a:focus-visible {
  text-decoration: underline;
}

/* A list of forty weapons scrolls inside its own box rather than taking the
   page with it, so the heading, the filters and the pager stay where they
   were put and a reader can see what they are narrowing while they narrow
   it. Tall enough to be worth scrolling, short enough to leave the filters
   above it on the screen. */
.table-wrap--deep {
  max-height: min(60vh, 34rem);
  overflow-y: auto;
}

/* And the column headings stay with the rows they are heading, which is the
   whole point of scrolling the table rather than the page. Given their own
   back, or the rows would show through from underneath. */
.table-wrap--deep thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-raised);
  /* The table collapses its borders, and a collapsed border belongs to the
     table rather than to the cell, so it scrolls away and leaves the
     headings floating. Drawn as a shadow instead, which belongs to the
     cell and travels with it. */
  border-bottom-color: transparent;
  box-shadow: inset 0 -1px 0 var(--edge);
}

/* A price never wraps. It needs no rule of its own now: the catalogue's
   cells break only between words, and "1,000eb" has none. */

/* A page of facts is a label and a value rather than a grid to be scanned
   across, so it keeps no minimum width and the label column is only as
   wide as a label needs. */
.kit--facts { min-width: 0; }
.kit__col--label { width: 34%; }
.kit__col--value { width: 66%; }

/* A fact's label is a phrase rather than a name, so it breaks between
   its words and never inside one: at 34% on a phone the column rendered
   "Suppressi / ve fire" and "Concealab / le", which read as a fault in
   the page. The wider column is what lets the words stay whole.
   Measured at 390px: every label but one now sits on two lines. */
.kit--facts > tbody > tr > th { overflow-wrap: normal; }


/* Below this two columns stop holding a fact at all. Measured at 320px,
   "Suppressive fire" wanted 102px of a label column 86px wide and pushed
   the whole page sideways; at 470px the four magazine sizes had 35px a
   column to put "Standard" in. Widening the labels only moved the
   problem to the values, so below 620px the label goes above its answer
   and each takes the full width. */
@media (max-width: 620px) {
  /* Direct children throughout: a fact whose value is a little table of
     its own has rows and cells too, and stacking those would turn four
     magazines side by side into four lines. */
  .kit--facts,
  .kit--facts > tbody,
  .kit--facts > tbody > tr,
  .kit--facts > tbody > tr > th,
  .kit--facts > tbody > tr > td {
    display: block;
    width: auto;
  }

  .kit--facts > tbody > tr {
    border: 1px solid var(--edge);
    margin-bottom: -1px;
  }

  .kit--facts > tbody > tr > th {
    border: 0;
    padding-bottom: 0;
    color: var(--ink-quiet);
    font-size: 0.6875rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  .kit--facts > tbody > tr > td {
    border: 0;
    padding-top: var(--space-2);
  }

  .kit--facts > tbody > tr > td.kit__nested { padding: var(--space-2) 0 0; }
}

.pages {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  margin: var(--space-4) 0 0;
}

.pages__where {
  font-size: 0.75rem;
  color: var(--ink-quiet);
}

/* The count against a kind on the catalogue's front page. Sits with the
   blurb rather than in a column of its own, because the list is a stack of
   links and a third column would make it a table. */
.stacks__count {
  color: var(--amber-dim);
  letter-spacing: var(--track-wide);
}

/* ---------------------------------------------------------------------------
   The catalogue's hoardings
   --------------------------------------------------------------------------- */

/* Nothing here is named after what it is. A filter list hides an element
   whose class says banner, advert, promo or sponsor as readily as it blocks
   an address containing them, so the words are avoided in the markup for the
   same reason they are avoided in the paths. */

/* The three of them together, stood off the rule under the heading rather
   than hard against it: a hoarding touching the masthead reads as part of
   the page rather than as something hung on it. */
.hoardings { margin-top: var(--space-6); }

/* The wide artwork running across the top. The rail holds the same pictures
   twice and is slid half its own width, so what leaves one side is already
   arriving at the other and the loop has no seam in it.

   Amber around all three, which is the colour this terminal uses for the
   things it wants looked at. */
.hoard {
  overflow: hidden;
  margin: 0 0 var(--space-5);
  border: 1px solid var(--amber);
  background: var(--surface-void);
}

.hoard__rail {
  display: flex;
  width: max-content;
  animation: hoard-run 90s linear infinite;
}

.hoard__plate {
  display: block;
  height: 72px;
  width: auto;
  flex: 0 0 auto;
}

@keyframes hoard-run {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* The pages turning underneath: three frames side by side, each on its own
   clock. Every page is in the markup and only the first of each frame is
   shown, so a reader with no script gets three adverts and nothing moves. */
.showcase {
  display: flex;
  gap: var(--space-3);
  margin: 0 0 var(--space-5);
}

/* A frame keeps the shape of the artwork in it, so the column does not jump
   as a page turns, and it keeps that shape at any width because it is given
   a ratio rather than a height. */
.showcase__frame {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  aspect-ratio: 2550 / 3300;
  overflow: hidden;
  border: 1px solid var(--amber);
  background: var(--surface-void);
}

/* A page arrives from the right and settles, rather than appearing where
   the last one was. The movement is small, a twentieth of the frame: enough
   that a reader who looked away knows the page turned, not so much that it
   competes with the strip running above it. */
.showcase__plate {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transform: translateX(5%) scale(0.98);
  transition:
    opacity 700ms ease,
    transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.showcase__plate--here {
  opacity: 1;
  transform: translateX(0) scale(1);
}

/* The one going out leaves to the left, so the two read as a sequence
   rather than as a stack being shuffled. Set by the script as it turns. */
.showcase__plate--going {
  opacity: 0;
  transform: translateX(-5%) scale(0.98);
}

/* The offers along the bottom, on the same principle as the strip above:
   written twice, slid half the rail's width. */
/* Three portrait pages across a phone are three columns of postage stamps,
   so below the width the reading room stacks at, only the first is shown
   and it takes the room the three had. */
@media (max-width: 1080px) {
  .showcase__frame:nth-child(n + 2) { display: none; }
}

.crawl {
  overflow: hidden;
  padding: var(--space-3) 0;
  border: 1px solid var(--amber);
  background: var(--surface-deep);
}

.crawl__rail {
  display: flex;
  width: max-content;
  margin: 0;
  max-width: none;
  animation: crawl-run 240s linear infinite;
}

.crawl__line {
  flex: 0 0 auto;
  padding-right: var(--space-7);
  font-size: 0.8125rem;
  letter-spacing: var(--track-wide);
  color: var(--amber-bright);
  white-space: nowrap;
}

@keyframes crawl-run {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* An operator who has asked their machine for less movement gets a street
   that stands still. The artwork is all still there to be read; it simply
   does not go anywhere, and the rails are wrapped so that what is written
   twice does not run off the side. */
@media (prefers-reduced-motion: reduce) {
  /* The page still changes, it simply does not travel to do it. */
  .showcase__plate {
    transform: none;
    transition: opacity 400ms ease;
  }

  .showcase__plate--going { transform: none; }

  .hoard__rail,
  .crawl__rail {
    animation: none;
    width: 100%;
    flex-wrap: wrap;
  }

  .hoard__rail > [aria-hidden="true"],
  .crawl__rail > [aria-hidden="true"] { display: none; }

  /* Ten strips that would have gone past in a minute are ten strips down
     the page when they stand still, which is more hoarding than anybody
     asked for. Three is a street corner. */
  .hoard__plate:nth-child(n + 4) { display: none; }

  .crawl__line { white-space: normal; }
}

/* A fact whose value is a little table of its own: the four magazine
   sizes, as a row of names over a row of numbers. The inset table drops
   the outer one's padding and its outer border, so it reads as part of
   the cell rather than as a second table dropped into it. */
.kit__nested { padding: 0; }

.kit--inset { width: 100%; min-width: 0; border-collapse: collapse; }

.kit--inset th,
.kit--inset td {
  border: 0;
  border-right: 1px solid var(--edge);
  padding: var(--space-2) var(--space-3);
  text-align: center;
}

.kit--inset th:last-child,
.kit--inset td:last-child { border-right: 0; }

.kit--inset thead th {
  font-size: 0.625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  background: none;
  box-shadow: none;
}

/* The figures carry the row: the headings above them are a key, so the
   numbers are what the eye should land on. */
.kit--inset tbody td {
  border-top: 1px solid var(--edge);
  font-weight: 700;
}

/* A magazine is named in one word, so it never breaks inside it. */
.kit--inset th,
.kit--inset td { overflow-wrap: normal; }

/* Between the stacking width and the full-width layout the value column
   is at its narrowest, about 46px a magazine, and the headings overflowed
   it. Measured at 560px: without the tracking and the capitals they fit. */
/* The inset keeps its own tighter padding on a narrow screen, where it
   has the full width but not much of it, and drops the tracking and the
   capitals from its headings. Measured at 390px: a column is 62px, and
   "STANDARD" spaced out wanted 58 of them before its padding, so it
   broke after STANDAR and read as a fault. */
@media (max-width: 620px) {
  .kit--inset th,
  .kit--inset td { padding: var(--space-2) var(--space-1); }

  .kit--inset thead th {
    letter-spacing: 0;
    text-transform: none;
    font-size: 0.625rem;
  }
}

/* A page laid out as a sheet, as Luke sketches one: boxes of label and
   value pairs, two to a row, with a paragraph across a box where one is
   wanted. Four columns, a label and its value twice over, falling to two on
   a phone so that each pair reads on its own line. A heading in place of a
   pair takes the pair's two columns. */
.sheet-box {
  display: grid;
  margin: 0 0 var(--space-5);
  border: 1px solid var(--edge);
  background: var(--surface-raised);
}

/* The label columns are a fixed width rather than sized to what is in
   them, so that every row of every box on the page lines up, and the
   values start in the same place down the whole sheet. Wide enough for
   UPGRADE TYPE on one line. */
.sheet-box__row {
  display: grid;
  grid-template-columns: 10rem minmax(0, 1fr) 10rem minmax(0, 1fr);
  align-items: baseline;
}

.sheet-box__row + .sheet-box__row,
.sheet-box__row + .sheet-box__text,
.sheet-box__text + .sheet-box__row {
  border-top: 1px solid var(--edge);
}

.sheet-box__label,
.sheet-box__value,
.sheet-box__heading {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  font-size: 0.8125rem;
}

.sheet-box__label,
.sheet-box__heading {
  font-size: 0.6875rem;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--amber);
}

.sheet-box__heading {
  grid-column: span 2;
  font-weight: inherit;
}

/* A long word breaks rather than pushing its row past the box's edge, and
   a long label, such as AUTOFIRE/SUPPRESSIVE, rather than running into the
   value beside it. */
.sheet-box__value {
  color: var(--ink);
  overflow-wrap: anywhere;
}

.sheet-box__label { overflow-wrap: anywhere; }

/* Across the whole box, as the sketch draws it, rather than held to the
   reading measure every other paragraph on the site keeps. */
.sheet-box__text {
  max-width: none;
  margin: 0;
  padding: var(--space-4);
  color: var(--ink);
  line-height: 1.6;
}

@media (max-width: 40rem) {
  .sheet-box__row { grid-template-columns: 8.5rem minmax(0, 1fr); }
}
