/* --------------------------------------------------------------------------
   Portal styles (F5, extended by F7) — host and admin screens.

   Separate from the public site's style.css: different audience, different
   nav, and the public pages are Stage 3.

   Mobile-first on purpose. Hosts use this on a phone, in a dim and loud room,
   often one-handed: large tap targets, high contrast, minimal typing. Admin
   screens may assume a desktop.
   -------------------------------------------------------------------------- */

:root {
    --wg-ink:        #16181d;
    --wg-paper:      #ffffff;
    --wg-muted:      #5b6270;
    --wg-line:       #d8dce4;
    --wg-accent:     #b4231f;
    --wg-accent-ink: #ffffff;
    --wg-error-bg:   #fdecea;
    --wg-error-ink:  #8c1d18;
    --wg-panel:      #f4f6f9;

    /* WGT_15382 (👤 Alex, 2026-09-22, the `WGT_15016` reading walk, decision 1)
       — RED MEANS DANGER ONLY. The bare `button` rule below filled EVERY button
       on EVERY portal screen with --wg-accent, so *Save*, *Add an event here*
       and *Duplicate this event* all shouted louder than *Delete event*, which
       was the quiet one. Ordinary buttons take this slate; red is now reserved.
       ⚠️ The same slate as --wg-nav-head-bg below, on purpose: the palette gains
       a MEANING here, not a new hue. 10.2:1 against white, so --wg-button-ink
       on it clears AAA at every size.
       🛑 --wg-danger IS NOT --wg-accent, and the two must not be collapsed.
       --wg-accent keeps the meaning RV88 §6 gave it — "selected": albums.php's
       filter chips (.pick-filter.is-on) and .qual-toggle:has(input:checked)
       read it, and portal.css has said since RV158 that it cannot also mean
       "destructive". They hold the same value today because Alex asked the
       danger button to take the red the ordinary button gave up; they are two
       names because they are two meanings, and either may move alone. */
    --wg-button-bg:  #3a4150;
    --wg-button-ink: #ffffff;
    --wg-danger:     #b4231f;
    --wg-danger-ink: #ffffff;

    /* RV103 — the middle step of the nested-panel border scale. --wg-muted is
       the darkest line and --wg-line the lightest; a box one level in from the
       outside needs a third tone, or two depths render identically and the
       progression Alex asked for reads as a swap. The exact midpoint of the
       two: (91,98,112) and (216,220,228) average to (154,159,170). Borders
       only — it is far too light for text. */
    --wg-line-mid:   #9a9faa;

    /* Sidebar group headings (2026-08-11). Deliberately NOT --wg-ink: at full
       black the bars read as the loudest thing on an admin screen, louder than
       the page's own <h1>. This is the same slate as --wg-muted taken darker,
       so the bars sit behind the content and still clear 10:1 against white. */
    --wg-nav-head-bg: #3a4150;

    /* Coverage banner (F7). Warm enough to read as a warning without looking
       like a failure — these gaps are standing facts, not errors. */
    --wg-warn-bg:    #fff8e6;
    --wg-warn-ink:   #6b4a00;
    --wg-warn-line:  #e8c268;

    /* "Cannot be checked yet" is a different thing from "here is a problem",
       and must not read as either an error or an all-clear. */
    --wg-info-bg:    #eef3fb;
    --wg-info-ink:   #2b4a7a;
    --wg-info-line:  #bcd0ee;

    /* WGT_12250 — the portal's link colour, minted 2026-09-06 because nothing
       in the palette was free to borrow. --wg-accent is the alarm red RV65
       ruled out for links by name, and --wg-info-ink means "info panel" and
       "gap unknown" already. Alex's call: links must be visible AT REST, so
       `inherit` was rejected — with no underline until hover, colour is the
       only thing marking a link, and inherit marks it with nothing.
       Chosen for separation as much as contrast: crude perceptual distance 111
       from --wg-info-ink and 289 from --wg-ink, so it reads as neither an info
       panel nor body text. Contrast measured against every background a link
       can land on — paper 7.16, panel 6.62, error-bg 6.27, warn-bg 6.76,
       info-bg 6.43, view-as-bg 4.86. All clear AA; all but the yellow
       view-as banner clear AAA. */
    --wg-link:       #1552b8;

    /* View-as banner (F31/F33). Must stay a colour used nowhere else in the
       portal: an admin who forgets they are impersonating misreads every
       screen after this one. */
    --wg-view-as-bg:   #ffcf40;
    --wg-view-as-line: #8a5a00;
    --wg-view-as-ink:  #3a2600;

    /* Never smaller than 16px on inputs: iOS zooms the page on focus below that,
       which is miserable one-handed in a dark bar. */
    --wg-tap: 48px;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 17px;
    line-height: 1.5;
    color: var(--wg-ink);
    background: var(--wg-paper);
    -webkit-text-size-adjust: 100%;
}

/* RV21 — Alex, on activity.php's own name: "make the header text like trivia
   bigger and more bold… It reads as a subtitle where it is the page's
   subject." Sitewide, not scoped to activity.php alone — no page-specific
   hook exists without editing portal.php's portalHeader(), outside this
   task's grant, and every page's <h1> is the same "page title" RV19 already
   treats as one sitewide thing.

   RV27 — Alex, stop 11, on the To-do board title: more space underneath it.
   Same constraint as RV21 right above (portalHeader() emits every page's
   <h1>, and it is outside this task's grant too), so this is sitewide again
   rather than a page-specific hook invented just for one screen. */
h1 { font-size: 1.9rem; font-weight: 700; margin: 0 0 .75rem; }
h2 { font-size: 1.2rem; margin: 0 0 .5rem; }
/* RV182 — h3 had no base rule, so it fell back to the browser's 1.17em
   (19.89px) and outranked the 19.2px h2 above it. font-size only: margin
   is deliberately untouched, since several call sites already set their own. */
h3 { font-size: 1.05rem; }

/* WGT_12250 — the portal's FIRST base link rule. Every link rule in this file
   was component-scoped (`.entity-link` RV16, `a.row-action` RV181,
   `.rp-export-list a` RV65), so any link nobody had reached yet fell through to
   the UA stylesheet and rendered rgb(0,0,238) with a permanent underline. That
   blue belongs to no part of the palette, and the per-component patch had
   already been re-applied three times — which is what this rule owns instead.

   🛑 `:where()` — ZERO specificity, on purpose, and the rule BREAKS without it.
   `a:hover` is (0,1,1) and would OUT-RANK the (0,1,0) class rules that keep
   `.add-event-button`, `.filter-chip`, `.portal-nav-link`, `.portal-brand`,
   `.portal-role-tab` and `.night-link` un-underlined — every button on the
   portal would have grown an underline on hover. Wrapped in `:where()` this is
   the weakest rule in the file, so all 17 existing `text-decoration`
   declarations still win and this only fills the gap they leave.

   ⚠️ That zero specificity is also what makes a fixed colour safe here. A base
   rule reaches the warn, error and view-as panels and the dark sidebar too, but
   every element on a dark ground (`.portal-nav-link`, `.portal-role-tab`,
   `.filter-chip[aria-current]`, `.pri-3`, `.pri-4`, `.sub-tab.active`) is a
   class rule setting its own `color`, so none of them can be reached by this.
   Checked one by one, 2026-09-06.

   🛑 `color: inherit` was written first and REJECTED by Alex, 2026-09-06. It is
   what `.entity-link` does, so it looked like the consistent choice — but with
   no underline until hover, colour is the ONLY thing marking a link, and
   `inherit` marks it with nothing: a link became indistinguishable from body
   text until a mouse touched it, and unreachable on a phone. See --wg-link.

   🛑 WGT_15421 — READ THIS BEFORE YOU ACT ON THE PARAGRAPH ABOVE. That
   reasoning is about a link whose ONLY cue is its colour — a link in prose,
   with nothing clickable around it. It does NOT reach `.entity-link` on a
   `tr.row-link` row, where the WHOLE ROW is the tap target and the name needs
   no cue of its own. Alex, 2026-09-22: "if it's a table where they're clicking
   to go to a specific night ... we should keep our general style where there's
   no underline or blue at all and they click anywhere on the row."

   ⚠️ WGT_15391 read the paragraph above, correctly, and stripped `.entity-link`
   off question-week.php's week name — turning a clickable table row blue. Alex
   reversed it the same morning (WGT_15473). The comment was not wrong; it was
   incomplete, which is what this note fixes. The full rule, with both dates and
   why they do not contradict each other, is ../../../CLAUDE/specs/design_guide.md
   → "The link rule". */
:where(a) {
    color: var(--wg-link);
    text-decoration: none;
}

:where(a:hover, a:focus-visible) {
    text-decoration: underline;
}

.muted { color: var(--wg-muted); font-size: .95rem; }

/* --- Sign-in ------------------------------------------------------------- */

.portal-auth {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    padding: 1.25rem;
    background: var(--wg-panel);
}

.auth-card {
    width: 100%;
    max-width: 24rem;
    padding: 1.75rem 1.5rem 1.5rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 12px;
}

.auth-sub  { margin: 0 0 1.25rem; color: var(--wg-muted); }
.auth-note { margin: 1.25rem 0 0; font-size: .9rem; color: var(--wg-muted); }
/* The signed-in "Add game alerts" offer on signup.php (Alex, 2026-09-17). */
.auth-card-centred { text-align: center; }

/* SU2 (2026-08-26) — Alex, on the invited door: "move down the email label. So it
   looks like it belongs with the address below it instead of with the not colin
   section above it."

   ⚠️ The cause is the assumption stated on `.auth-note` above — it sets NO bottom
   margin because it is normally the LAST thing on the card. On the invited signup
   door and on the resend door it is not last any more: a form follows it, so the
   form's first <label> sat hard against the note. Measured before this rule, both
   doors: 0px above the Email label. The label read as the tail of the "Not Colin?"
   sentence rather than as the heading of the field under it.

   🛑 Scoped to the ADJACENCY, not to the class and not to a section-slug id (a
   standing rule in this project). The note only needs the space when something
   follows it, so every door where it genuinely IS last — login.php's two, the
   "Already have a login?" line at the foot of every auth card — matches nothing
   here and is untouched. 1.25rem is what `.auth-sub` already puts between the
   intro line and the form on the public door, so both doors open on one rhythm.
   ⚠️ When validation errors render, `.wg-alert` sits between the note and the
   form and this stops matching — which is correct: the alert carries its own
   margins and the note is no longer the thing above the form. */
.auth-note + form { margin-top: 1.25rem; }

/* SU2 — the invited door shows the invited address as TEXT where every other door
   has an <input> (SU1: Graham owns that value, so it is not the signer-up's to
   type). A bare <p> keeps the browser's 1em margins, which put 17px between the
   label and its own value while the label had 0px above it — the exact inversion
   that made the label look like it belonged to the note above. This puts the value
   in the slot an input occupies: the label's own .35rem above it, an input's 1rem
   below. 🛑 Use this class, not a bare <p>, for any value shown in place of a
   field on an auth card. */
.auth-static { margin: 0 0 1rem; }

/* SU3 (2026-08-26) — the password requirements, shown on the signup form before
   anything is typed, and ticked off by that page's inline <script> once the
   field has been left once.

   🛑 THE LIST IS NOT AN ERROR STATE. It is the same quiet grey as .auth-note
   until the browser has something to say about it, because on first paint the
   person has not got anything wrong yet — five red crosses greeting somebody who
   has not typed a character is the failure this styling exists to avoid. That is
   why every `unmet` colour below hangs off `[data-state]`, which the script sets
   on BLUR and never before.

   ⚠️ COLOUR IS NEVER THE ONLY SIGNAL — the ✓/✗ in .pw-mark carries the same
   information for anybody who cannot see the difference between the two inks,
   and .pw-state carries it in words for anybody using a screen reader. If a
   later change drops the marks, the state stops being legible to both. */
.pw-rules {
    margin: -.5rem 0 1rem;
    font-size: .9rem;
    color: var(--wg-muted);
}

.pw-rules-lead { margin: 0 0 .35rem; }

.pw-rules ul {
    list-style: none;
    margin: 0;
    padding: 0;
}

.pw-rules li {
    /* The mark sits in a fixed column so a wrapped requirement lines up under
       its own text rather than under the tick. ⚠️ The requirement is wrapped in
       its own `.pw-text` in the markup rather than left as a bare text node —
       an anonymous item works today, but the moment anything else is added to
       the line the two would silently share a cell. */
    display: grid;
    grid-template-columns: 1.1rem 1fr;
    align-items: start;
    margin: 0 0 .2rem;
    line-height: 1.4;
}

.pw-mark::before { content: "•"; }
.pw-rules li[data-state="met"]   .pw-mark::before { content: "✓"; }
.pw-rules li[data-state="unmet"] .pw-mark::before { content: "✗"; }

/* A green dark enough to clear 4.5:1 on --wg-paper. Deliberately a literal and
   not a new :root token: this is the only place in the portal that says "this
   one thing is satisfied", and minting a sitewide colour for one list invites
   the next screen to borrow it for something it does not mean. */
.pw-rules li[data-state="met"]   { color: #1e6b2b; }
.pw-rules li[data-state="unmet"] { color: var(--wg-error-ink); }

/* Screen-reader only, both of them — .pw-state names the state in words on each
   line, .pw-rules-status announces the tally once per blur instead of letting
   five separate lines all speak. Kept out of the layout entirely so the visible
   list is unchanged whether or not the script has run. */
.pw-state,
.pw-rules-status {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* F27's SMS consent block — the box and the disclosure beside it have to read
   as ONE unit, and the disclosure must not butt against the submit button.
   `.auth-note` carries no bottom margin (it is normally the last thing on the
   card), so the consent copy sets its own.

   ⚠️ The label needs the extra specificity to beat `.setting-check { margin: 0 }`
   further down the file — it borrows that class for the flex row. */
.auth-card .auth-consent { margin: .25rem 0 0; }

.auth-consent-note {
    margin: .5rem 0 1.4rem;
    font-size: .9rem;
    line-height: 1.45;
    color: var(--wg-muted);
}

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

label {
    display: block;
    margin: 0 0 .35rem;
    font-weight: 600;
    font-size: .95rem;
}

/* ⚠️ `date`, `number` and `time` belong in this list too. F14's date fields were
   left on browser defaults and looked like a different app next to everything
   else; F16 adds number and time, so all three are covered here rather than
   styled separately per screen. The 16px minimum is what stops iOS zooming the
   page when a field takes focus. */
/* RV113 (2026-08-15) — `search` was the one text-entry type MISSING from the list, so
   every `<input type="search">` in the app rendered at the browser default and looked
   like a different app next to the control beside it. Measured on questions.php: the
   search box was 153×21 with a 13.3px font, a 2px inset border and no radius, against a
   280×48 select with a 1px --wg-line border and an 8px radius — TEN differing computed
   properties, the font size among them, which is the 16px iOS-zoom minimum this block's
   note above exists to guarantee. Alex asked for the pair to read as parallel on
   questions.php; a page-scoped rule would have forked a base element style for one
   screen, which portal_classes.md forbids, so the fix is to finish the list. It reaches
   the other two search boxes as well — hosts.php:1399 and event.php:2287 — and that is
   the point: all three now match their own neighbours. */
input[type="email"],
input[type="password"],
input[type="text"],
input[type="search"],
input[type="tel"],
input[type="date"],
input[type="number"],
input[type="time"],
textarea {
    display: block;
    width: 100%;
    min-height: var(--wg-tap);
    margin: 0 0 1rem;
    padding: .6rem .75rem;
    font-family: inherit;       /* textarea defaults to monospace otherwise */
    font-size: 1rem;            /* see --wg-tap note: 16px+ prevents iOS zoom */
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

/* WT9 — question-set.php's #body was the only field on this list with no
   width rule at all, so it rendered at the browser's ~20-column default
   (180px, measured) regardless of its container. resize: vertical, not the
   both/horizontal default: a paste box now spanning its full container
   should not be draggable back into the same narrow-column look. */
textarea {
    resize: vertical;
}

/* --- WGT_18302 — FORM LABELS AND SHORT CONTROLS CENTRE, PORTAL-WIDE (2026-10-02)

   👤 Alex, 2026-10-02, after the sixth form he asked to have centred one at a
   time: centred form labels are the BASE rule for the whole portal. His words
   on the venue page set the shape: "Center the text and labels on everything.
   But let the about the venue input area be left aligned, though the label can
   still be centered."

   🛑 THIS REVERSES RV19's "form `.field`/`.field-row` labels and inputs are
   left alone" AND RV146's refusal to widen `.field > p.muted`. Both are
   retired sitewide; portal_centring.md has the record. Do not put a form's
   labels back to the left without asking him.

   WHAT CENTRES — only a label or control that has its line to itself:
   · a label that is a direct child of a `.field`, a `<form>` or a `<fieldset>`,
     or that sits directly above its control, and the hint under it
     (`.field > p.muted`, `.field > p.field-help`);
   · the typed text of a one-line control in that same place: `select` and
     `input` of type text, email, tel, number, date, time, search, url, password.

   WHAT DOES NOT, and each is deliberate:
   · 🛑 a `textarea` — its typed text stays LEFT. That is the one thing he
     excluded. Inside `.portal-main` the RV88 opt-out near the foot of this file
     (`.portal-main textarea { text-align: start }`) already holds it; the bare
     `textarea` rule below is the same guard for a form outside `.portal-main`
     (the login and sign-up cards), where a centred wrapper would otherwise
     hand it `center` by inheritance.
   · a label that WRAPS a checkbox or radio (`:has(input)`) — a tick row, and a
     column of ticks must keep one left edge.
   · a control that SHARES ITS LINE: `.field-inline`, `.setting-input`,
     `.shift-change-row`, `.nl-inline`, `.label-line`, and anything in a table
     cell. Those rows centre with `justify-content`, or stay as they are; the
     control gets no `text-align` from here. The selectors below cannot reach
     most of them (the control is not a child of `.field`), and the two rows
     where a label does sit directly before its control are named in the
     `:not()`.

   ⚠️ `:where()` KEEPS THIS AT ELEMENT SPECIFICITY, so any class rule still
   wins: `.field-centred`, `#findZip`, `.headcount-input` and the rest keep
   doing their own job without a fight. */
:where(.field, form, fieldset) > label:not(:has(input)),
label:not(:has(input)):where(:has(+ select, + textarea, + input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"]))),
:where(.field) > p:where(.muted, .field-help) {
    text-align: center;
}

:where(.field) > select,
:where(.field) > input:where([type="text"], [type="email"], [type="tel"], [type="number"], [type="date"], [type="time"], [type="search"], [type="url"], [type="password"]),
:where(.field > .combobox) > input,
:where(:not(.field-inline, .shift-change-row, .nl-inline)) > label:not(:has(input)) + select,
:where(:not(.field-inline, .shift-change-row, .nl-inline)) > label:not(:has(input)) + input:where([type="text"], [type="email"], [type="tel"], [type="number"], [type="date"], [type="time"], [type="search"], [type="url"], [type="password"]) {
    text-align: center;
    text-align-last: center;
}

textarea {
    text-align: left;
}

/* --- WGT_21519 — THE SHOW / HIDE BUTTON ON EVERY PASSWORD BOX (2026-10-17)

   Amanda asked, through Graham: a way to see the password you typed.
   `password-toggle.js` wraps each `input[type=password]` in `.pw-toggle` and
   puts `.pw-toggle-button` inside it, over the box's right edge — so the
   button takes no line of its own and the form below does not move.
   🛑 NONE OF THIS IS IN THE MARKUP: with JavaScript off there is no wrapper,
   no button, and the box is the plain one above.

   ⚠️ The wrapper TAKES OVER THE BOX'S `margin-bottom`, and the box gets
   EQUAL padding on both sides, so its typed text stays centred on the same
   line as before (WGT_18302's rule above centres a box only when it is a
   direct child of its `.field` or label, which the wrapper now is instead —
   so the centring is restated here). Every password box in the app was
   centred under that rule, so none changes. */
.pw-toggle {
    display: block;
    position: relative;
    margin: 0 0 1rem;
}
.pw-toggle > input {
    margin: 0;
    padding-inline: 4.5rem;
    text-align: center;
    text-align-last: center;
}
.pw-toggle > .pw-toggle-button {
    position: absolute;
    top: 1px;
    right: 1px;
    bottom: 1px;
    width: 4.25rem;
    min-height: 0;
    padding: 0;
    font-size: .9rem;
    font-weight: 600;
    color: var(--wg-link);
    background: transparent;
    border-radius: 0 7px 7px 0;
}
.pw-toggle > .pw-toggle-button:hover { filter: none; text-decoration: underline; }
.pw-toggle > .pw-toggle-button:focus-visible { outline-offset: -3px; }

input:focus-visible,
textarea:focus-visible,
button:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: 2px;
}

/* 🛑 WGT_15382 — THIS RULE IS WHY EVERY PORTAL BUTTON USED TO BE RED, and it is
   the whole of the fix. Nothing had to ask for the accent: a bare `<button>`
   got it, so a screen with eight buttons and one destructive one rendered eight
   identical red bars (measured on venue-edit.php?venue=10, 2026-09-22 — *Save*,
   *Save contact and notes*, *Add an event here*, *Attach or invite*, *Replace*
   x2, *File this contract*, *Close this venue*). The slate is the default now;
   a button that really is destructive asks for `.button-danger` below.
   ⚠️ So the fix is ONE declaration pair here, not a class on a hundred buttons —
   every safe button on every portal screen went neutral without being touched. */
button {
    display: block;
    width: 100%;
    min-height: var(--wg-tap);
    padding: .7rem 1rem;
    font-size: 1rem;
    font-weight: 600;
    color: var(--wg-button-ink);
    background: var(--wg-button-bg);
    border: 0;
    border-radius: 8px;
    cursor: pointer;
}

button:hover { filter: brightness(1.08); }

.link-button {
    width: auto;
    min-height: auto;
    padding: .35rem .5rem;
    font-size: .95rem;
    font-weight: 500;
    color: var(--wg-ink);
    background: none;
    text-decoration: underline;
}

/* --- Alerts -------------------------------------------------------------- */

.wg-alert {
    margin: 0 0 1rem;
    padding: .75rem .9rem;
    border-radius: 8px;
    font-size: .95rem;
}

.wg-alert p:first-child { margin-top: 0; }
.wg-alert p:last-child  { margin-bottom: 0; }

.wg-alert-error {
    color: var(--wg-error-ink);
    background: var(--wg-error-bg);
    border: 1px solid #f5c6c2;
}

.wg-alert-info {
    color: var(--wg-info-ink);
    background: var(--wg-info-bg);
    border: 1px solid var(--wg-info-line);
}

/* 👤 Alex, 2026-09-18 — the amber block alert: something is working but will
   stop if nobody acts (first caller: the admin home's *Scheduled jobs* intro
   when every job is `ok` and one carries a heartbeat `warning`). Same shape as
   .wg-alert-info; only the colours differ, and they are the --wg-warn-* tokens
   .venue-flag-warn and the coverage banner already use — so amber means one
   thing across the portal. Not red: nothing has failed. */
.wg-alert-warn {
    color: var(--wg-warn-ink);
    background: var(--wg-warn-bg);
    border: 1px solid var(--wg-warn-line);
}

/* --- Signed-in chrome (F7) ----------------------------------------------- */

/* View-as banner (F31, moved out of inline styles in F33). */

.wg-view-as-banner {
    background: var(--wg-view-as-bg);
    border-bottom: 3px solid var(--wg-view-as-line);
    color: var(--wg-view-as-ink);
    padding: .75em 1em;
    font-weight: bold;
    text-align: center;
}

.wg-view-as-banner a {
    margin-left: 1em;
    color: var(--wg-view-as-ink);
}

.portal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem 1rem;
    flex-wrap: wrap;
    padding: .75rem 1rem;
    border-bottom: 1px solid var(--wg-line);
}

.portal-brand {
    font-weight: 700;
    color: var(--wg-ink);
    text-decoration: none;
}

/* 🛑 WGT_14713 (2026-09-19) — THIS ROW, NOT THE NAV, IS WHAT PUSHED EVERY
   PORTAL PAGE 24px SIDEWAYS AT 360. The row was raised against the nav after
   a scan named `.portal-nav-link` (right edge 589) as an overflowing element.
   It is not: `.portal-nav` is `overflow-x: auto`, so a link past its right
   edge scrolls INSIDE the strip and reaches the page not at all. Spec:
   CLAUDE/specs/portal_nav.md → "The 24px at 360 was the header, not the nav".

   The real cause. `.portal-header` wraps, so at 360 this div drops onto its
   own line with 328px of room. The div itself did NOT wrap, and its three
   children cannot shrink: `.portal-public-link` is `white-space: nowrap`,
   the `.portal-who` email is one long unbreakable token, and the Sign out
   button is a button. Min-content came to 368px — 40px more than the line
   had — so the page grew by 24px and "Sign out" sat off the right edge.

   MEASURED against the real stylesheet, admin, six portal screens
   (event-add, index, venues, settings, reports, hosts):
   before 24px at 360 on ALL SIX, 0 at 390 and 1440; after 0 at all three.

   Two rules, and both are needed — wrapping alone still leaves one flex line
   holding a 185px email that nothing may break.
   UNDO: delete `flex-wrap` here and the two `.portal-who` lines below. */
.portal-identity {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .25rem .75rem;
}

.portal-who {
    font-size: .95rem;
    color: var(--wg-muted);
    /* A flex item will not shrink past min-content without this, and an email
       address IS one min-content word. `anywhere` lets it break mid-address
       rather than take the line width with it. */
    min-width: 0;
    overflow-wrap: anywhere;
}

.portal-role {
    padding: .1rem .4rem;
    margin-left: .15rem;
    font-size: .75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--wg-accent);
    border: 1px solid var(--wg-accent);
    border-radius: 4px;
}

/* 🛑 WGT_15383 (2026-09-22) — THE EMAIL GOES AWAY ON A PHONE, THE BADGE STAYS.
   👤 Alex's call in the WGT_15016 reading walk (decision 3). At 360 this header
   wrapped to FOUR lines — brand / "View website ↗" / email + badge / Sign out —
   159px of a ~640px phone screen, before any page content, on every
   authenticated screen.

   Only `.portal-who-email` is hidden, and that class is the ACCOUNT EMAIL alone:
   portal.php picks it over `.portal-who-name` by whether there is a host record.
   A host's nickname is short AND is the only identity a host sees, because the
   'host' role renders no badge — so it stays at every width. The badge is a
   SIBLING of the hidden span, not a child, so hiding one keeps the other.
   ⚠️ Not truncation: a cut-off address reads as a rendering fault.

   40rem is this file's phone breakpoint — the same one `.portal-brand`'s tap
   target uses near the foot of this file.

   MEASURED against the real stylesheet, admin, /portal/index.php — header
   height, and the number of lines the header wraps to:
     360   159px / 4 lines  →  105px / 2 lines
     390   132px / 4 lines  →  105px / 2 lines
     640    69px / 1 line   →   69px / 1 line   (unchanged: the 44px brand tap
                                                 target sets this height, not
                                                 the email)
    1440    53px / 1 line   →   53px / 1 line   (email still shown, 191px wide)
   ⚠️ 105px is not 53px: the remaining phone height is the 44px brand row plus
   the identity row, and neither is this row's to remove.
   UNDO: delete this block; the email comes back at every width. */
@media (max-width: 40rem) {
    .portal-who-email { display: none; }
}

.logout-form { margin: 0; }

/* 👤 Alex, 2026-09-18: the portal had no way back to the public site. A plain link in the
   identity row, opening a new tab so the portal page stays where it was. */
.portal-public-link {
    font-size: .95rem;
    color: var(--wg-muted);
    white-space: nowrap;
}
.portal-public-link:hover,
.portal-public-link:focus-visible { color: var(--wg-ink); }

/* --- Role switcher (AC6) ---------------------------------------------------

   Only rendered for a multi-role account — one role shows no switcher at
   all, per Alex's dictation on this task: the common single-role case must
   stay uncluttered. Distinct from `.portal-nav` below, which switches
   SCREENS within one role; this switches which role's screens are on
   offer at all, so it sits above that nav, not inside it.

   Same full-bleed scrolling-strip idiom as `.portal-nav` below the 64rem
   breakpoint (F43) — a host+player account on a phone still reaches both
   tabs with the same one-handed swipe this app is built around. It never
   moves into the sidebar at wide widths the way `.portal-nav` does: two or
   three tabs never need a column of their own. */
.portal-role-switcher {
    display: flex;
    gap: .25rem;
    overflow-x: auto;
    padding: 0 .5rem;
    background: var(--wg-panel);
    border-bottom: 1px solid var(--wg-line);
    -webkit-overflow-scrolling: touch;
}

.portal-role-tab {
    display: flex;
    align-items: center;
    min-height: var(--wg-tap);
    padding: 0 1rem;
    white-space: nowrap;
    font-size: .9rem;
    font-weight: 700;
    color: var(--wg-muted);
    text-decoration: none;
    border-bottom: 3px solid transparent;
}

.portal-role-tab:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

.portal-role-tab.is-current {
    color: var(--wg-ink);
    border-bottom-color: var(--wg-accent);
}

/* --- Nav (F5, regrouped into a sidebar by F43) ----------------------------

   ONE set of markup, two layouts, and the narrow one is still the original.

   🛑 A HOST IS ON A PHONE AND MUST KEEP THE SIDEWAYS STRIP. Below the
   breakpoint `.portal-nav-group` is `display: contents`, so its heading and
   links become direct flex children of `.portal-nav` and the strip is byte-for
   -byte the screen that shipped with F5 — with the group names riding along in
   it as small inline captions. Above the breakpoint the same divs become
   blocks in a sticky left column. `display: contents` is what makes that
   possible without a second nav in the DOM or a line of JavaScript.

   ⚠️ Nothing here is a `<details>`: an open/closed state that CSS cannot set is
   a state the desktop layout would be stuck with collapsed.
   -------------------------------------------------------------------------- */

/* Scrolls sideways rather than wrapping into a tall block on a phone. */
.portal-nav {
    display: flex;
    gap: .25rem;
    overflow-x: auto;
    padding: 0 .5rem;
    border-bottom: 1px solid var(--wg-line);
    -webkit-overflow-scrolling: touch;
}

.portal-nav-group { display: contents; }

.portal-nav-heading {
    display: flex;
    align-items: center;
    margin: 0;
    padding: 0 .5rem 0 .75rem;
    font-size: .7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    white-space: nowrap;
    color: var(--wg-muted);
    /* Reads as a divider between runs of links while scrolling sideways. */
    border-left: 1px solid var(--wg-line);
}

/* The first run carries no heading, so nothing needs a leading divider. */
.portal-nav-group:first-child .portal-nav-heading { border-left: 0; }

.portal-nav-link {
    display: flex;
    align-items: center;
    min-height: var(--wg-tap);
    padding: 0 .75rem;
    white-space: nowrap;
    font-size: .95rem;
    font-weight: 600;
    color: var(--wg-muted);
    text-decoration: none;
    border-bottom: 3px solid transparent;
}

.portal-nav-link:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

.portal-nav-link.is-current {
    color: var(--wg-ink);
    border-bottom-color: var(--wg-accent);
}

/* W34 — the Tonight "⚠ N" / "⚠ ?" badge (W3), shipped unstyled because
   portal.css was W4's exclusive grant at the time. inline-flex + nowrap
   keeps it riding the link's own nowrap rather than wrapping to a second
   line; the warning tokens (shared with the coverage banner) make it read
   as a warning at a glance without the weight of --wg-accent's red. */
.portal-nav-badge {
    display: inline-flex;
    flex: 0 0 auto;
    margin-left: .35rem;
    padding: .05rem .4rem;
    font-size: .75rem;
    font-weight: 700;
    white-space: nowrap;
    color: var(--wg-warn-ink);
    background: var(--wg-warn-bg);
    border: 1px solid var(--wg-warn-line);
    border-radius: 999px;
}

.portal-main {
    max-width: 40rem;
    margin: 0 auto;
    padding: 1.25rem 1rem 3rem;
}

/* F47 — 40rem is a deliberate reading measure for prose screens and
   night.php's phone-first board; it is too narrow for a wide .data-table
   (measured: questions.php's nights table needs 779px and clipped by 237px
   at 40rem). portal.php builds <main class="portal-main"> and is outside
   this task's file grant, so :has() widens the container from a marker
   class on the table itself rather than a per-page opt-in higher up —
   opt in with .data-table--wide, never change .portal-main globally. */
.portal-main:has(.data-table--wide) {
    /* RV5 (2026-08-14) — THE 60rem IS GONE. Alex: the table should be as wide
       as its content needs, "still float in the middle", and push the PAGE
       sideways when the room runs out. 60rem was that limit in all but name:
       the column stopped at 960px, so `.table-wrap` had to hide the rest of a
       wide table inside its own scrollbox.
       ✅ `100%`, NOT `none` — and this is the whole of RV5's judgement.
       `100%` resolves against the grid area, so the column is exactly its
       track and PROSE KEEPS THE WINDOW'S MEASURE, while `.table-wrap`
       (fit-content — its rule is further down this file) is free to overflow
       it to the right and take the page with it. 🛑 `none` was tried first and is WRONG: with the cap off
       entirely, `margin: 0 auto` makes this a fit-content grid item whose
       floor is min-content, so the widest table drags the whole column out
       with it — measured 2026-08-14 on reports.php at 1440: column 1349px,
       page overflow 133px, and every lede and panel ran off the right edge of
       the window, so a PARAGRAPH needed sideways scrolling to read. That is
       F47's real harm, and it is not what Alex asked for. `100%` gives the
       same table widths and LESS page overflow (133px → 84px there).
       ⚠️ So the page overflow that remains is the table's own overflow and
       nothing else. It is deliberate — do not re-add a cap to remove it.
       ⚠️ Below 64rem `.portal-shell` is not a grid, so this is an ordinary
       block filling the viewport — unchanged, and the nav still stacks on top. */
    max-width: 100%;
}

/* WT9 — question-set.php's #body paste box holds a whole night's rounds,
   questions and answers, and 40rem is a prose measure, not a paste-box one
   (Alex: "very skinny and terrible looking"). Same :has() mechanism as
   .data-table--wide above and .scan-log below, reused rather than
   duplicated — a marker class on the field itself, because portal.php's
   <main class="portal-main"> is outside this task's file grant too.
   🛑 `width`, NOT `max-width` alone — measured. .data-table--wide's table
   has real min-content (nowrap cells) that pulls the fit-content-sized grid
   item wide on its own; a width:100% textarea has none (percentage widths
   don't count toward a parent's intrinsic size), so with only `max-width`
   set .portal-main stayed shrink-wrapped near 738px regardless of viewport.
   An explicit `width` is a definite size and bypasses that shrink-to-fit
   computation outright. */
.portal-main:has(.field--wide) {
    width: min(60rem, 100%);
    max-width: min(60rem, 100%);
}

/* Widening the page for the box would otherwise widen every prose block on
   it too. The title field and both help captions (the one under the title
   AND the one under the box) are text meant to be read, not pasted
   material — they stay at the reading measure even though the page around
   them just got wider. Only .field--wide itself, and the textarea inside
   it, take the extra room. */
.portal-main:has(.field--wide) .lede,
.portal-main:has(.field--wide) .field:not(.field--wide),
.portal-main:has(.field--wide) .field--wide .muted {
    max-width: 40rem;
}

/* RV68 — event.php's shift-primary redesign put `about` and `shifts` flat on
   the page, but photos/dates/retire still fold behind `.wg-section`, and a
   closed <details>'s content isn't rendered at all, so it contributes nothing
   to .portal-main's size. Same shrink-to-fit gap WT9 found for `.field--wide`:
   a grid item with `margin: 0 auto` and no explicit `width` sizes to whatever
   IS visible, so with every foldable panel closed on first paint the whole
   page rendered skinny and then jumped to 40rem the moment one opened wide
   enough to need it (Alex: "can we just start with them at that width
   already so it's less jarring"). An explicit `width` bypasses that
   computation the same way `.field--wide` does, so the page starts at its
   final width instead of growing into it. */
.portal-main:has(.event-detail) {
    width: min(40rem, 100%);
}

/* RV68 — a flag's on/off control sits beside its label, not under it. A
   <form> is on HTML5's list of elements that implicitly close an open <p>,
   so `<p><strong>Facebook:</strong> On <form>…</form></p>` was already being
   split into two separate block boxes by the parser before any CSS applied
   — that split, not a missing style, is what put the button on its own
   line. `.wg-flag-row` keeps the label paragraph and the form as flex-row
   siblings instead. */
.wg-flag-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
}

.wg-flag-row p {
    margin: 0;
}

.wg-flag-row form {
    display: inline;
    margin: 0;
}

/* WGT_19429 — the event page's *Facebook albums* panel lists one `.album-row`
   per attached album (WGT_19350); without this the rows touch at 390. */
#facebook .album-row + .album-row {
    margin-top: .75rem;
}

/* --- Wide screens: the nav becomes a grouped left sidebar (F43) ------------

   Admin screens may assume a desktop (see the header of this file), and an
   admin sees eighteen links where a host sees four. 64rem ≈ 1024px, which is
   a landscape tablet and up — a portrait tablet keeps the strip.
   -------------------------------------------------------------------------- */

@media (min-width: 64rem) {
    .portal-shell {
        display: grid;
        grid-template-columns: 14rem minmax(0, 1fr);
        align-items: start;
    }

    /* 🛑 PY2 (2026-08-23) — A ROLE WITH NO SIDEBAR WAS BEING PUT IN THE SIDEBAR'S
       TRACK. Spec: CLAUDE/specs/portal_nav.md (split out of portal.md 2026-09-07).
       TRACK. `portalHeader()` suppresses `<nav class="portal-nav">` at
       `count($navItems) > 1`, but the shell above is emitted unconditionally
       (F43) — so with no nav, `<main>` becomes the FIRST grid item and lands in
       the fixed 14rem column, with the whole 1fr column left empty beside it.

       MEASURED on the real `venue.php` at 1440px, against the real stylesheet:
       `grid-template-columns: 224px 1216px`, `.portal-main` **left 0, right 519**
       — jammed against the left edge of the window with 921px of nothing to its
       right; and on the same page's refusal state, a **224px** column of prose.

       ⚠️ IT IS NOT A NEW SCREEN'S PROBLEM. `player.php` (AC6) is the other role
       with exactly one nav item, so it renders through the same branch and has
       done since it shipped.

       ✅ Collapse to ONE track when there is no nav to fill the first one.
       `.portal-main` keeps its own `max-width: 40rem; margin: 0 auto`, so the
       reading measure and the centring both come from the rule that already owns
       them — this changes WHERE the column may sit, never how wide prose gets.
       🛑 Do NOT "fix" this by widening `.portal-main`: RV5 tried `max-width: none`
       on the content column and every lede on `reports.php` ran off the window.
       UNDO: delete this rule; nothing else keys on it. */
    .portal-shell:not(:has(.portal-nav)) {
        grid-template-columns: minmax(0, 1fr);
    }

    .portal-nav {
        position: sticky;
        top: 0;
        flex-direction: column;
        gap: 0;
        max-height: 100vh;
        overflow-x: visible;
        overflow-y: auto;
        padding: .5rem 0 2rem;
        border-bottom: 0;
        border-right: 1px solid var(--wg-line);
    }

    .portal-nav-group {
        display: block;
        padding: .35rem 0;
    }

    /* Every run but the first is fenced off from the one above it. */
    .portal-nav-group + .portal-nav-group {
        border-top: 1px solid var(--wg-line);
    }

    /* A headed run fences itself — the dark bar below IS the divider, and a
       hairline a few pixels above it just reads as grime. The last run carries
       no heading (Reports, Settings), so it keeps the line. */
    .portal-nav-group + .portal-nav-group:has(.portal-nav-heading) {
        border-top: 0;
        padding-top: 0;
    }

    /* ⚠️ Reversed out, not just bolder: at .7rem in muted grey the group names
       sat in the same visual layer as the links under them and stopped reading
       as headings at all. Full-bleed across the column — the inset version
       reads as another (unclickable) nav item.
       🛑 Desktop only. The phone strip keeps them as inline captions; a dark
       pill mid-scroll there reads as a button. */
    .portal-nav-heading {
        display: block;
        margin-bottom: .25rem;
        padding: .4rem 1rem;
        color: var(--wg-paper);
        background: var(--wg-nav-head-bg);
        border-left: 0;
    }

    .portal-nav-link {
        /* Vertical hit targets can be shorter than a thumb-sized tap on a
           phone, and 18 of them at 48px would not fit a laptop screen. */
        min-height: 0;
        padding: .45rem 1rem;
        font-weight: 500;
        border-bottom: 0;
        border-left: 3px solid transparent;
    }

    .portal-nav-link:hover { color: var(--wg-ink); }

    .portal-nav-link.is-current {
        color: var(--wg-ink);
        background: var(--wg-panel);
        border-bottom-color: transparent;
        border-left-color: var(--wg-accent);
    }

    /* The column is narrower than the page was, so the reading measure stays
       put rather than sliding left under the sidebar. */
    .portal-main { padding: 1.25rem 2rem 3rem; }
}

/* --- RV19 — centre the main content, site-wide (2026-08-14) ---------------

   Alex: "Make a note to look through and we should just center pretty much
   all the text on every page… Not the left-hand navigation, but the main
   content." 🛑 NOT `.portal-main { text-align: center }` — that cascades
   into .data-table, .detail-list, form labels and the rota grid, all of
   which read as broken centred (a number column is harder to compare, a
   long paragraph is harder to read). This is the pass, done by targeting
   the element types that are safe rather than the container that holds
   them, same idiom as RV5's `:has()` scoping below.

   What IS centred, and why each is safe:
   · the page's own `<h1>` (portalHeader() emits exactly one, as
     `.portal-main`'s first child) — the literal "page titles" Alex named.
   · `.button-row` — every Save/Cancel/Continue/Confirm action sitewide
     already lives in this one class (portal.php never hand-rolls a bare
     button outside it), so centring it once here is RV21's "the Save
     button" for every screen, not just activity.php.
   · a `.muted` or `.field-help` caption that sits DIRECTLY above a
     `.button-row` — RV21's "the text above it". The `:has(+ …)` scope
     means only the note immediately before the action centres; the same
     classes used as a long aside earlier in a form, or as inline help next
     to a field, are untouched.
   · a `.lede` whose only content is a single back-link (`← All activities`
     on activity.php, hosts.php, night.php) — one short line of navigation
     text, not prose.
     🛑 THAT RULE IS GONE — RV88, 2026-08-14, §2 of RV79's survey. `:only-child`
     counts ELEMENT children, not text nodes, so it fired on ANY lede holding
     exactly one link however much prose was wrapped around it: measured,
     hosts.php's and host-settings.php's three-line descriptive ledes came back
     `center` while five near-identical ledes (venue-edit, the now-retired
     venue-cover, questions, reports, event.php's landing) stayed `start`,
     purely by accident of link count. Divergence by accident is the failure
     RV64→RV81 names.
     Every `.lede` centres under RV88's sitewide rule at the foot of this file,
     back-link and description alike, so the accidental mechanism went with it.

   What is deliberately LEFT ALONE, and why:
   · `.lede` in general — it is reused for genuine multi-line descriptions
     (activities.php, hosts.php's roster intro, questions.php, reports.php)
     as well as one-line navigation. Centring the class would centre the
     descriptions too, which is exactly the "paragraph is harder to read
     centred" case Alex flagged as the one to bring back rather than ship.
     ⚠️ KNOWINGLY OVERRULED by RV88 (2026-08-14), on Alex's answer to §1/§2:
     multi-line ledes DO centre now. RV79 asked the question because the
     accident above had already half-decided it. This paragraph is kept as the
     record of what the earlier call was, not as a live exclusion.
   · `.data-table`, `.detail-list`, form `.field`/`.field-row` labels and
     inputs, `.confirm-list` — numbers, labels and facts-in-a-list read
     worse centred; RV5's table centring (`.table-wrap`) is a different
     mechanism (a box, not text) and is untouched here.
     ⚠️ THE FORM-LABEL AND INPUT HALF OF THIS BULLET IS OVERRULED — 👤 Alex,
     2026-10-02 (WGT_18302): form labels, field hints and short one-line
     controls centre portal-wide now, by the base rule in the Forms block at
     the top of this file. Tables, `.detail-list` and `.confirm-list` stand.
   · small in-card captions — left as found. ⚠️ `.equipment-action-title` was
     one of them until 👤 Alex named it on 2026-10-04 (WGT_18965): it is now a
     centred header band on every action card, both pages at once, in its own
     rule beside `.equipment-action`. */
.portal-main > h1 { text-align: center; }

/* `.lede:has(> a:only-child)` stood here until RV88 (2026-08-14) — see the
   docblock above for why it went and what replaced it. */

p.muted:has(+ .button-row),
p.field-help:has(+ .button-row) { text-align: center; }

/* RV21 gates this here: "Centre the Save button, the text above it and the
   page titles" — quoting Alex on activity.php's Retire panel specifically.
   The `<h2>` is deliberately excluded, matching every other panel heading.

   🛑 THE `#retire form` HALF IS GONE — RV107, 2026-08-15, because it never
   worked and the comment that stood here said it did. It claimed the bare
   `<button class="row-action button-quiet">` was "still `display:
   inline-block`". It is NOT: `button` above sets `display: block; width:
   100%` and `.row-action` overrides only the width to `auto`, which leaves a
   block-level shrink-to-fit box HUGGING THE LEFT. `text-align` cannot reach a
   block-level child, so the button rendered 241px left of `.portal-main`'s
   centre at 1440 — measured, temporary/rv107/. RV21 was written under the
   constraint that activity.php sat outside its `Files:`; RV107 is the task
   that could change the markup, and it did: the button is in a `.button-row`
   now, the idiom portal_sections.md mandates for exactly this case ("a new
   centred control belongs in a `.button-row`… do not answer that by writing a
   second one-off rule").

   `#retire p` STAYS, and not merely out of caution: the retired branch's
   `.wg-alert p` is a DESCENDANT, so the sitewide rule at the foot of this
   file — which is `>`-scoped and excludes `.wg-alert` — does not reach it.
   This selector is the only thing centring that paragraph. */
#retire p { text-align: center; }

/* `.field:has(> #formatLabel) > p.muted` stood here (event.php's *Style* help
   line, 2026-08-17) — the one field hint on the portal that centred. RETIRED by
   WGT_18302: every `.field > p.muted` centres under the base rule now. */

/* 🛑 WGT_15869 (👤 Alex, 2026-09-23) — *"center everything (including the custom
   label input area)"*. The rule above centred only the help PARAGRAPH under that
   field, and that paragraph was deleted by `WGT_15865`, so the field was reading
   fully left-aligned when he asked. This centres the field itself — its label,
   its input and anything else it holds.

   ⚠️ SINCE WGT_18302 (2026-10-02) THE LABEL AND THE TYPED TEXT CENTRE ON EVERY
   FIELD, by the base rule. What this class still does that the base rule does
   not: it NARROWS the box to 24rem and centres the box.

   ⚠️ `margin-inline: auto` AND `max-width` ARE BOTH NEEDED. `text-align: center`
   alone centres the label text but leaves the input full-bleed across the panel,
   which reads as a centred caption over a left-aligned box. */
.field-centred { text-align: center; }
.field-centred > label { display: block; }
.field-centred > input,
.field-centred > select,
.field-centred > textarea {
    margin-inline: auto;
    max-width: 24rem;
    text-align: center;
}

.lede { margin: 0 0 1rem; font-size: 1.05rem; }

.detail-list {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: .35rem 1rem;
    margin: 1rem 0 1.5rem;
}

.detail-list dt { font-weight: 600; color: var(--wg-muted); }
.detail-list dd { margin: 0; }

/* 👤 Alex, 2026-09-24 — the venue page's per-format figures (*Usually*, *Best night*,
   *Most recent*) sit centred under their heading, as ONE block: the grid keeps its
   label column, so the labels still line up. `venue.php` only. */
.detail-list.venue-figures { width: fit-content; margin-inline: auto; }

/* 👤 Alex, 2026-09-27 (the WGT_17086 walk) — checkin.php: each of the venue's
   answers for its host (*Where to set up*, *Who to ask for*, …) is its own section,
   the answer centred under the section's heading. */
.venue-host-answer { text-align: center; overflow-wrap: anywhere; }
.venue-host-answer img { display: block; margin: 0 auto; }

/* 👤 Alex, 2026-09-24 — the cancellation policy on venue.php (its own panel and the
   cancel confirm step), centred throughout. Each rule is a bold label line with its
   text under it; the holiday list keeps its bullets beside the words. */
.policy-body { text-align: center; }
.policy-body ul { list-style-position: inside; padding-left: 0; }

/* WGT_17077 — the bar's *Help & FAQ* panel on venue.php: one `.cover-reveal`
   pill per question, the answer centred under it like the policy above. */
.venue-help-intro { text-align: center; }
.venue-help-item { margin-top: .75rem; }
.venue-help-answer { text-align: center; overflow-wrap: anywhere; padding: .25rem 0 .5rem; }
.venue-help-answer ul { list-style-position: inside; padding-left: 0; }

/* 👤 Alex, 2026-09-24 — the password form, centred: the rules list and its
   lead line (its labels centre under the base rule since WGT_18302). A COMPONENT rule, not a page rule — portalPasswordPanel() is the one
   drawer, on venue.php, host-profile.php and player.php. The inputs stay full width.
   The rules LIST is centred as one block, its lines left inside it, because each line
   is a grid with the ✓/✗ mark in a fixed column (`.pw-rules li` above). ⚠️ `#pw-rules`, not
   `.pw-rules`: the server markup carries only the id, and the class is the script's. */
.password-panel #pw-rules { text-align: center; }
.password-panel #pw-rules ul { width: fit-content; margin-inline: auto; text-align: left; }

.panel {
    margin: 0 0 1.5rem;
    padding: 1rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

/* --- Shift boxes (event-add.php, RV8's markup — styled here by RV35) -----

   RV8 shipped these four classes on the borrowed `.panel`/`.button-row`
   look because portal.css wasn't in its grant. `.shift-box` already carries
   `.panel`, so it only needs the gap between consecutive boxes; the header
   just needs its heading and delete button on one row.

   The delete button is deliberately NOT the plain `button` fill: --wg-accent
   IS this site's red, so a filled button here would read as the page's
   primary action (Save/Continue), not as a secondary, destructive one.
   Quiet like `.button-quiet`, but in the error tokens `.wg-alert-error`
   already uses, small enough to sit beside a heading rather than stretch
   full-width like the rest of the form's buttons. `.button-danger` (RV158)
   is the same token triple at ordinary button size, for a standalone
   destructive button rather than one inline beside a heading.

   🛑 WGT_15382 (2026-09-22) OVERTOOK THE PARAGRAPH ABOVE — it is kept as the
   reasoning of its day, not as current instruction. A filled button no longer
   reads as "primary action", because the bare `button` rule stopped being red;
   red now means DESTRUCTIVE and nothing else (👤 Alex, `WGT_15016` walk). Both
   this class and `.button-danger` are solid --wg-danger now. The SIZE argument
   above still stands and is untouched: this one is small and inline beside a
   heading, `.button-danger` is ordinary button size and stands alone. */
.shift-box + .shift-box { margin-top: 1rem; }

.shift-box-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .75rem;
}

.shift-box-heading { margin: 0; }

/* 🛑 WGT_15382 — SOLID RED NOW, FOR THE SAME REASON `.button-danger` IS. The
   comment above describes the world before 2026-09-22, where a filled button
   meant "primary action" because EVERY button was filled. It does not any more:
   red is reserved for destructive, and deleting a shift is destructive. The
   small size and the inline position beside the heading are unchanged — only
   the fill moved from the pale error pair to the danger pair. */
.shift-box-delete {
    display: block;
    width: auto;
    min-height: 0;
    padding: .4rem .75rem;
    font-size: .85rem;
    font-weight: 600;
    color: var(--wg-danger-ink);
    background: var(--wg-danger);
    border: 0;
    border-radius: 8px;
}

/* RV103 — THE THREE-TONE NESTING SCALE, DARKEST OUTSIDE → LIGHTEST INSIDE.
   🛑 This REVERSES half of RV72, which was Alex's own call; he was shown the
   quote and confirmed. RV72 read the nesting one level too far in and put the
   dark line on `.shift-box`, leaving `#section-shifts` outside it light.
   Alex, 2026-08-15: "we want the outside ones darker and then get lighter as
   it goes in so it more captures the progression. It looks strange to have the
   darker stuff inside and the lighter stuff outside because it emphasizes the
   middle and kind of loses the outside edge." 🛑 Do not re-invert it, and do
   not "fix" it back to RV72 — CLAUDE/plans/_ARCHIVE_reference/details_rv_walk.md RV103 carries both quotes.

   🛑 NOT a straight swap of the outer pair: that would leave two of the three
   levels identical. Three distinct tones, one per depth:

     L1  #section-*        --wg-muted     #5b6270  darkest
     L2  .shift-box etc.   --wg-line-mid  #9a9faa  mid
     L3  .shift-activities --wg-line      #d8dce4  lightest (the .panel default)

   ⚠️ ALL FOUR L1 sections take the dark line, not just Shifts. They are the
   same depth, and RV70 made them siblings; darkening one of four would read as
   emphasis on Shifts, which is the very "emphasizes the middle" complaint this
   task exists to answer. Their own L2 children (the two venue fieldsets) take
   the mid tone for the same reason.

   🛑 RV138 (2026-08-16) — `#recurrence-choice` WAS one of those L2 children and
   is NOT any more. Alex asked for that box to lose its border and read like the
   fields beside it, so event-add.php swapped its `.panel` for the shared
   `.choice-set` — a fieldset of options that reads as one control, no border,
   legend sized like a `<label>`. 🛑 It is unboxed by NOT MATCHING this rule,
   never by an exception keyed on the id: no fourth tone was minted, no
   `border: 0` override exists, and the L1/L2/L3 tones below are byte-identical
   to what RV103 shipped. Do not "restore" it here.

   ⚠️ `#create` is a FIFTH, outermost `.panel` wrapping the whole form and is
   deliberately LEFT at `--wg-line`: it is the content column's frame, not a
   step in the progression, and Alex did not name it. Measured before the
   change: L0 light · L1 light · L2 DARK · L3 light.
   🛑 THAT IS NOW SETTLED, NOT PENDING — RV120 closed 2026-08-16 and Alex's
   answer was LEAVE IT. The fifth panel stays at the lightest tone; do not fold
   it into the scale and do not mint a fourth tone for it.

   ⚠️ These four ids and `.shift-box` are rendered by event-add.php alone
   (grepped across Code/ 2026-08-15), so nothing else on the portal moves. */
#section-venue.panel,
#section-datetime.panel,
#section-shifts.panel,
#section-misc.panel { border-color: var(--wg-muted); }

#section-venue .panel,
#section-datetime .panel,
#section-shifts .shift-box.panel { border-color: var(--wg-line-mid); }

/* RV138 — SPACING ONLY, and deliberately keyed on the id.

   🛑 Read this against the note above: the BORDER is not overridden here and
   must never be. #recurrence-choice loses its box by dropping `.panel` and so
   not matching the mid-tone rule directly above — no exception, no fourth tone.
   What an id rule IS for is the one thing the shared class cannot know: where
   in a form the fieldset sits. `.choice-set { margin: .85rem 0 0 }` is authored
   for its two other callers, where it is the LAST thing before a submit button;
   here it is the FIRST thing inside #section-datetime, so the top margin is
   wasted and the missing bottom one let "Single — just once" sit 13px above
   "Night of the week" — measured, and it read as a THIRD radio option rather
   than as the next field. 1rem is not a new number: it is `margin: 0 0 1rem`
   off the base input/select rule, i.e. the gap every other field on this form
   already leaves. ⚠️ Do not move this into `.choice-set` — hosts.php and
   availability.php would both take spacing nobody has looked at. */
#section-datetime > #recurrence-choice { margin: 0 0 1rem; }

/* WGT_20952 — 👤 Alex, 2026-10-13, walk tab 8 of Add an event: "the time not set
   yet, and the paragraph below, it should be centered". The *Close this venue
   after the event* tick above it has the same shape, so it takes the same class.
   `.setting-check` is flex, so `justify-content` centres the tick and its words. */
.tick-centred .setting-check { justify-content: center; }
.tick-centred > p,
.tick-centred > div > p { text-align: center; }

/* WGT_20952 — 👤 Alex, 2026-10-13, walk tab 8 again: "put a little tooltip next to
   close this venue after the event and then show that description if they hover
   over the tooltip instead of always having the paragraph. Do the same thing with
   the time not set yet." `.tick-with-tip` is the tick and a "?" on one centred line.
   The "?" sits OUTSIDE the <label>, so pressing it never ticks the box.
   The bubble opens on hover AND on focus — Tab reaches the "?", and a tap focuses
   it on a phone, where there is no hover. It is placed against the whole line,
   not the "?", so at 390 it can never hang off the right edge. */
.tick-with-tip {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: .4rem;
    margin: 0 0 1rem;
}

.tick-with-tip .setting-check { width: auto; }

.tip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--wg-tap);
    min-height: var(--wg-tap);
    cursor: help;
}

.tip > [aria-hidden] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 50%;
    font-weight: 700;
    line-height: 1;
}

.tip:hover > [aria-hidden],
.tip:focus > [aria-hidden] { color: var(--wg-paper); background: var(--wg-ink); }

.tip:focus-visible { outline: 3px solid var(--wg-accent); outline-offset: 2px; border-radius: 50%; }

.tip-body {
    display: none;
    position: absolute;
    z-index: 20;
    top: calc(100% + .25rem);
    left: 50%;
    transform: translateX(-50%);
    width: min(24rem, 100%);
    padding: .6rem .85rem;
    background: var(--wg-ink);
    color: var(--wg-paper);
    border-radius: 8px;
    font-size: .875rem;
    font-weight: 400;
    line-height: 1.45;
    text-align: center;
    box-shadow: 0 4px 14px rgba(0, 0, 0, .18);
}

.tip:hover > .tip-body,
.tip:focus > .tip-body { display: block; }

/* WGT_20952 — Close this venue's *When to close it* drop-down (venue-edit.php),
   centred above the one red button, its label and "?" on one line. */
.close-when { display: flex; flex-direction: column; align-items: center; margin: 0 0 .9rem; }
.close-when-head { margin: 0 0 .35rem; align-self: stretch; }   /* full width, so the bubble is too */
.close-when-head .setting-label { margin: 0; }
.close-when select { width: auto; min-width: 16rem; max-width: 100%; margin: 0; }
.close-when-line { text-align: center; white-space: pre-line; }   /* WGT_20952 — one sentence a line; the "\n" is in the text */

/* WGT_20952 — 👤 Alex, same walk: "center the headers like venue, date and time,
   shifts and miscellaneous". The four L1 fieldsets' own legends only; inner
   legends (Activities, Only if the venue is somewhere new …) are untouched. */
#section-venue > legend,
#section-datetime > legend,
#section-shifts > legend,
#section-misc > legend { margin-inline: auto; padding-inline: .5rem; }

.shift-box-body {
    display: grid;
    gap: 1rem;
}

/* Activities as plain words, Week 1/Week 2 stacked beside them — "consolidate"
   per RV72's dictation. Single column until there is room for a real second
   column; measured at 1440/1152/1024 in temporary/rv72/. */
@media (min-width: 36rem) {
    .shift-box-body {
        grid-template-columns: 1fr 14rem;
        align-items: start;
    }
}

.shift-activities {
    margin: 0;
    padding: .75rem;
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

/* RV103 — ONE VERTICAL COLUMN. This was `flex-wrap: wrap` in a row, so with
   four activities Trivia and Bingo shared the first line and Extra and General
   event fell below (measured 2026-08-15: 4 items on 3 rows at 1440). Alex asked
   for the activities to read as a column. ⚠️ The FLOW changes, not the TYPE —
   RV72's light inline weight and small checkbox below both stay, and
   `align-items: flex-start` keeps each label hugging its own words instead of
   stretching into the full-width rows RV72 argued against. */
.shift-activities-list {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: .3rem;
}

/* Plain words, not stacked full-width rows: .setting-check is a deliberately
   heavy block-level tap target elsewhere on the portal (qualifications,
   settings toggles) — here it only needs to read as a short inline choice. */
.shift-activities-list .setting-check {
    flex: 0 0 auto;
    font-weight: 400;
}

.shift-activities-list .setting-check input[type="checkbox"] {
    width: 1.1rem;
    height: 1.1rem;
}

.shift-week-pair {
    display: grid;
    gap: .75rem;
}

/* Belt-and-braces over the `hidden` attribute, same reason as
   `.step-actions[hidden]` below: `.shift-week-pair` sets its own `display`,
   which ties the UA stylesheet's `[hidden] { display: none }` on
   specificity — without this, source order alone decides which wins. RV175. */
.shift-week-pair[hidden] {
    display: none;
}

/* --- Admin tables and forms (F14; reused by F8/F15) ---------------------- */

/* RV5 (2026-08-14) — NOTHING SCROLLS INSIDE THIS BOX ANY MORE; THE PAGE DOES.
   Alex: "let the table be as wide as it needs to be but still float in the
   middle". So: a table takes the width its content needs, it is centred in
   whatever room the column has, and when it needs more room than that it
   PUSHES THE PAGE SIDEWAYS. 🛑 Non-zero page overflow on a wide-table screen
   is now the intended outcome, not the F47 bug — see specs/portal.md.

   🛑 `width: fit-content` + `margin-inline: auto`, NEVER flex
   `justify-content: center` and never `text-align: center` on an
   inline-block. A flex-centred child that overflows spills in BOTH
   directions: its left edge sits at a negative offset that no scroll can
   reach, so the first column is lost for good. Auto margins have no free
   space to split once the box is wider than its container, so they resolve
   to 0 and the overflow is right-only and fully scrollable. One rule covers
   both the fits and the does-not-fit case; no media query switches them.

   ✅ W50's `overflow-x: auto` STAYS, and specs/portal.md is still right that
   it must never be removed as dead styling. 🛑 IT WAS EXPECTED TO FIGHT
   `fit-content` AND IT DOES NOT — measured 2026-08-14, both declarations
   live, albums.php at 1024: the wrap sized to the table's own 1061px with
   `scrollWidth === clientWidth` and an inner scrollLeftMax of 0. (The
   scroll-container rule that zeroes min-content in the scroll axis governs a
   flex/grid item's automatic minimum size, not the fit-content sizing of a
   block from its contents.) So it is now a measured no-op that costs nothing
   and remains the last-resort net against a CLIPPED table — the failure mode
   between W4 and W50, where venues.php hid 1172px of itself unreachably. */
.table-wrap {
    margin: 0 0 1.5rem;
    width: fit-content;
    /* 🛑 THIS CAP IS LOAD-BEARING — DO NOT REMOVE IT, and see `RV5` below before
       you think it contradicts something. Without it, `fit-content` lets a wide
       table's min-content push the WRAP past the viewport, so the whole PAGE
       scrolls sideways instead of the table scrolling inside its panel. With it,
       `overflow-x: auto` on this same block takes the rest.
       Measured by `WGT_14302` (2026-09-16) across every `Code/portal/` screen at
       390 and 360: FOURTEEN screens moved the page, worst 749px
       (`hosts.php?view=qualifications`). Injecting this one declaration at
       runtime took all thirteen table cases to EXACTLY 0 at both widths, and
       removing it put every number back. 1440 is unchanged — `fit-content`
       still wins below the cap.
       ⚠️ It was shipped scoped to `.portal-main:has(.night-page)` by `WGT_14229`
       and made global here on 👤 Alex's call, 2026-09-16, which RETIRES the
       `RV5` rule that said a table pushing the page is the design. */
    max-width: 100%;
    margin-inline: auto;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* WGT_14494 — THE EDGE FADE: a wide table scrolling inside its `.table-wrap`
   fades out on the RIGHT while more columns are hidden there, and on the LEFT
   once scrolled. No text. 👤 Alex, 2026-09-17 (question 14458-3): phones hide
   the scrollbar, so a venue on `/portal/venue.php` had no sign there was more.

   🛑 CSS ONLY, ON PURPOSE — `portal.js` loads on `event.php` alone, so a script
   there would not reach every `.table-wrap`. A scroll-driven animation moves
   two lengths, and a `mask-image` fades the wrap's own painted content by them.
   ⚠️ A MASK, NOT THE `background-attachment: local` SCROLL-SHADOW TRICK: that
   paints on the wrap's background, so any cell or row background (hover,
   `.equipment-row`, a tinted `<td>`) covers it. A mask fades whatever is there.

   ✅ A TABLE THAT FITS SHOWS NO FADE, AND NOTHING HERE DECIDES THAT. With
   `scrollWidth === clientWidth` the `scroll()` timeline is INACTIVE, so neither
   animation applies and both lengths sit at their registered 0 — the mask is
   then solid black edge to edge, i.e. no mask. The same holds wherever the wrap
   is not a scroll container at all (`#working`/`#waiting` at 40rem set
   `overflow-x: visible`).
   ⚠️ A browser without scroll-driven animations (Firefox; Safari before 26)
   fails the `@supports` and gets exactly what it had: no fade, table unchanged. */
@property --wg-fade-l {
    syntax: '<length>';
    inherits: false;
    initial-value: 0px;
}

@property --wg-fade-r {
    syntax: '<length>';
    inherits: false;
    initial-value: 0px;
}

@keyframes wg-table-fade-l {
    from { --wg-fade-l: 0px; }
    to   { --wg-fade-l: 2.5rem; }
}

@keyframes wg-table-fade-r {
    from { --wg-fade-r: 2.5rem; }
    to   { --wg-fade-r: 0px; }
}

@supports (animation-timeline: scroll()) {
    .table-wrap {
        /* The shorthand RESETS `animation-timeline`, so it must come first. */
        animation: wg-table-fade-l linear both, wg-table-fade-r linear both;
        animation-timeline: scroll(self inline);
        /* Each fade eases over the first / last 2.5rem of scroll, so the left
           one is full a thumb's-width in and the right one gone at the end. */
        animation-range: 0 2.5rem, calc(100% - 2.5rem) 100%;
        -webkit-mask-image: linear-gradient(to right,
            transparent, #000 var(--wg-fade-l),
            #000 calc(100% - var(--wg-fade-r)), transparent);
        mask-image: linear-gradient(to right,
            transparent, #000 var(--wg-fade-l),
            #000 calc(100% - var(--wg-fade-r)), transparent);
    }
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: .95rem;
}

.data-table th,
.data-table td {
    padding: .6rem .7rem;
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--wg-line);
    white-space: nowrap;
}

.data-table th {
    font-size: .8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--wg-muted);
    border-bottom-width: 2px;
}

/* The LAST row keeps no rule under it. Alex, 2026-08-17, on hosts.php?host='s
   Sub Availability list: "there's an HR at the bottom even though there is no
   option below it. We'd like to go through all the pages and all the accordions
   that have lists like this and take out that bottom HR."

   ✅ THIS IS A CONSISTENCY FIX WITH TWO PRECEDENTS ALREADY IN THIS FILE, not a new
   idiom: `.album-match-item:last-child` and `.setting:last-of-type` both zero
   their own trailing border. `.data-table` is the one list family that was
   missed, which is why the seam only ever showed up on a table.

   🛑 A ROW SEPARATOR AND A LIST TERMINATOR ARE DIFFERENT THINGS, and that is the
   whole argument: between two rows the rule separates them, but under the last
   one it separates the list from nothing — inside a `.wg-section` it lands a
   hairline a full 1rem of panel padding above the panel's own edge, so it reads
   as a rule promising another row.

   ⚠️ `tbody tr:last-child`, NOT `tr:last-child` — scoped so a table that is all
   `<thead>` keeps its 2px header underline (`.data-table th` above), which is a
   heading rule and not a row separator. ✅ There is NO `<tfoot>` anywhere in
   `Code/` (grepped 2026-08-17), so nothing legitimately needs a rule between the
   last body row and a foot; if one is ever added, this needs a `:not()`. */
.data-table tbody tr:last-child td { border-bottom: 0; }

.data-table td.wrap { white-space: normal; }

/* WGT_21751 — /portal/full-schedule.php: six columns a host reads on a phone.
   Wrapping headers; the Take button sits under "Open shift" in its own cell.
   👤 Alex, 2026-10-18 — everything centred both ways (`.cells-middle` does the
   text and the td; the th is here), one section per day under a shaded day
   row, and the table "just wide enough to hold everything with a bit of
   padding": `.table-wrap` is fit-content, so the cell padding IS the width.
   Below 40rem the cells go back to tight, so less of it scrolls in the wrap. */
.data-table.full-schedule th,
.data-table.full-schedule td { padding: .5rem .75rem; vertical-align: middle; }
.data-table.full-schedule th { white-space: normal; }
.data-table.full-schedule form { margin: .35rem 0 0; }
.data-table.full-schedule button { padding: .3rem .6rem; font-size: .85rem; }
.data-table.full-schedule .day-row th {
    padding-block: .55rem;
    background: var(--wg-line);
    font-size: 1rem;
    text-transform: none;
    letter-spacing: 0;
    color: var(--wg-ink);
}
/* WGT_21810 — one table per week, This week on top (👤 Graham, 2026-10-18).
   Each week's heading (`h2.full-schedule-week`, centred by the `.portal-main > h2`
   rule) needs clear air above it once a table sits there, so the two weeks read
   as two blocks, not one long list. */
.table-wrap + .full-schedule-week,
.empty-state + .full-schedule-week { margin-top: 2.5rem; }
@media (max-width: 40rem) {
    .data-table.full-schedule th,
    .data-table.full-schedule td { padding-inline: .45rem; }
}

/* Alex, 2026-09-17 — a row holding a button (venue-edit.php's owner table)
   centres its text on the button. A sibling class, so .data-table's top
   alignment stays the default everywhere else. */
.data-table.cells-middle td { vertical-align: middle; }
/* Alex, 2026-09-17 — the owner table sits in a centred section, so its
   headers and values centre too. Same sibling class, so nothing else moves. */
.data-table.cells-middle th,
.data-table.cells-middle td { text-align: center; }

/* `.field.field-label-centred label` stood here (venue-edit.php's owner box,
   2026-09-17). RETIRED by WGT_18302 — the base rule centres every field label.
   ⚠️ The class is still written in venue-edit.php and now styles nothing. */

/* Alex, 2026-09-17 (WGT_14485 AJAX mop-up) — the green and red boxes on
   venue-edit.php's Owner account tab (who a looked-up login is, and the
   attached / removed / invite notices) centre their text. Scoped to that tab's
   #accountBox; .wg-alert stays left-aligned everywhere else. The box is a
   focus target after an in-place swap, so it gets no focus ring of its own. */
.venue-owner-box .wg-alert { text-align: center; }
.venue-owner-box:focus { outline: none; }

/* 👤 Alex, 2026-09-27 (WGT_17222, walking the WGT_17086 page): *"center this"*,
   of the whole Owner account section — its paragraphs, *No account is attached
   yet.*, *Their email* and the invite line under it. Inherited from the one box
   (#accountBox), so nothing outside that section moves. ⚠️ `.venue-owner-box
   input { text-align: start }` stood under it until WGT_18302: the typed email
   centres now, like every other short control (the base rule). */
.venue-owner-box { text-align: center; }

/* Alex, 2026-09-23 (WGT_16060) — venues.php's "<name> added." banner and its
   "Add its first event · Open its page" line centre. A modifier on that one
   alert, so .wg-alert stays left-aligned everywhere else on the page. */
.wg-alert.wg-alert-centred { text-align: center; }

/* Alex, 2026-09-23 (WGT_16060) — venue-edit.php's "Show this venue on the
   public website" tick centres with its words, on the add form and the edit
   form alike. `.setting-check` is flex, so `justify-content`, never
   `text-align`. Keyed on the checkbox's own id, not a section slug. */
.setting-check:has(> #showPublicProfile) { justify-content: center; }

/* 👤 Alex, 2026-09-27 (the WGT_17086 walk) — the same-name confirm tick
   (WGT_17061) centres the same way, with room under it before Add venue. */
.setting-check:has(> #confirmSameName) { justify-content: center; margin-bottom: 1rem; }

/* `.venue-add-form .field > label` stood here (the Add a venue form's four
   labels, 2026-09-24). RETIRED by WGT_18302 — the base rule centres them.
   ⚠️ The class is still written in venue-edit.php and now styles nothing. */

/* 👤 Alex, 2026-10-02 (the WGT_18223 walk) — *"Center the text and labels on
   everything. But let the about the venue input area be left aligned, though
   the label can still be centered."* A wrapper on the venue's OWN screen only
   (venue.php's *Your venue page* and, the same sitting, *For your host*).
   The notice at the top puts each sentence on its own line, as he asked.
   ⚠️ WGT_18302, the same day: he made the labels-and-controls part the BASE rule
   for every portal form, so the four label / select / input / textarea lines
   that stood here are gone and venue-edit.php's copy of these forms matches by
   the same rule. What is left is this wrapper's own job — its PROSE (notes,
   tick rows, anything not a label or control) centres by inheritance. */
.venue-page-centred { text-align: center; }
.venue-page-centred .sentences > span { display: block; }

/* W51 — headers' nowrap min-width, not cell content, is the real overflow
   ceiling on report tables with many short-value columns. A sibling class,
   never a change to .data-table th itself. */
.data-table th.wrap { white-space: normal; }

/* WGT_13684 — a column that holds a SENTENCE, wrapping on a desktop only.
   👤 Alex, 2026-09-13 (WGT_13682): "A + D — leave the phone alone, fix only the
   23px at 1440." 

   🛑 1024px WAS ASKED AND ANSWERED ON 2026-09-16 — DO NOT RE-RAISE IT.
   👤 Alex: "accept it — leave 1024 alone", the same shape as his 2026-09-13
   answer above. At 1024 the #working *Next priority* cell measures 99px wide
   and NINE lines tall, and that is accepted.

   ⚠️ AND THE OBVIOUS CAUSE IS THE WRONG ONE, so do not "fix" it by widening the
   breakpoint below. MEASURED at 1024 on the seeded page: getComputedStyle(cell)
   .whiteSpace is `normal`, so THIS CLASS IS ALREADY IN EFFECT — 64rem resolves
   to exactly 1024px and the query matches. The cell is 99px because the table
   is only 732px inside a 734px panel there, and the five `nowrap` sibling
   columns take their min-content out of that first, leaving 99px for the one
   prose column. Widening `min-width: 64rem` changes nothing at all.
   The only real levers are: let a second column wrap at 1024 (*Ringing now* is
   the only other non-value column), or shorten calloutGapProjection()'s
   `detail` sentence. Both were offered and both declined. A desktop page must not scroll sideways, because desktop is
   where Graham works; the phone's sideways scroll is RV5's design and stays.

   🛑 IT IS A MEDIA-QUERIED CLASS AND NOT PLAIN `.wrap` BECAUSE OF THAT ANSWER,
   not because of the mechanism. `.wrap` would work at every width, and at 390
   it would have shrunk the table and grown the row — which is exactly the
   phone change Alex declined. `min-width: 64rem` is THIS FILE'S existing
   desktop breakpoint (the sidebar grid, above) reused rather than a new
   number, and it is also where `.data-table--wide` starts doing anything at
   all: below 64rem `.portal-main` is already the full viewport, so there is no
   cap to lift and nothing this class could buy. Desktop-only by construction.

   ⚠️ MEASURED on /portal/missing-shifts.php at 1440 with one ringing callout,
   through a cache-busted portal.css, per column, before anything changed —
   #working's six columns were Date 147 · Event 79 · Time 76 · Ringing now 134 ·
   Next priority 546 · You get told 209, table 1192px inside a 1118px panel
   content box, .table-wrap right edge at 1465 and PAGE OVERFLOW 25px.
   **"Next priority" was 546px because it was holding a 74-character SENTENCE**
   ("The cover list has 16 member(s) but none of them can be texted or
   emailed." — calloutGapProjection()'s `detail`), not the short moment the
   column usually shows. 🛑 THAT IS RV5'S OWN EXCEPTION, not a break with it:
   CLAUDE.md, *Seeing a screen* — a wide TABLE scrolling sideways is the design,
   but A PARAGRAPH THAT NEEDS SIDEWAYS ROOM IS A BUG.

   ✅ `white-space: normal` ALONE IS THE WHOLE FIX, and 25px → 0 is not a
   coincidence: `.data-table` is `width: 100%`, so the table only exceeds its
   panel because its MIN-CONTENT does. Let one fat column wrap and the table
   snaps to exactly the 1118px available. Measured after: table 1118px, that
   column 473px, page overflow 0, and the table now sits inside its own panel
   (portal_classes.md records that a table escaping its panel while the page
   stays put is itself a plain defect — hosts.php, 2026-08-17).
   ⚠️ Wrapping Date (col 0) or You get told (col 5) reaches 1118px too — both
   were measured. Neither was taken: breaking a date or a time mid-value is the
   defect RV200 named when it refused `wrap-anywhere` on a phone number. Only
   the column whose content is prose gets to wrap.

   🛑 NOT ID-SCOPED, and that is deliberate — the opposite call to the `#waiting`
   stacking rule below. That rule had to be fenced to one screen because it
   re-points `display` on the shared `.data-table` selector, so it would have
   moved fourteen unmeasured screens. A brand-new class can only reach markup
   that asks for it, so there is nothing unmeasured to protect: any other table
   with a sentence in a nowrap column should use this rather than copy it.
   ⚠️ `overflow-wrap` is NOT included. A sentence breaks at its spaces; a single
   unbreakable token wants `.wrap-anywhere` (W34, below) as well. */
@media (min-width: 64rem) {
    .data-table th.wrap-wide,
    .data-table td.wrap-wide { white-space: normal; }
}

.data-table tr.is-past td { color: var(--wg-muted); }

/* RV99 — a row that opens something when you click anywhere in it. Alex, of
   event.php's upcoming dates: clicking the row should take you to that night
   instead of offering row actions.

   🛑 THE STYLE IS ALL THIS RULE DOES. The navigation is a delegated handler
   keyed off `data-row-href`, and the row still carries a REAL anchor (the
   date, as `.entity-link`) because a click handler reaches neither the
   keyboard nor a screen reader. Never ship the class without the anchor.

   ⚠️ Deliberately generic, not `.event-dates-row`: RV101's two landing tables
   and RV114's tonight.php rows are the same job, and `.tonight-row` /
   `.equipment-row` showed what happens when each screen writes its own —
   three hover rules kept in step by hand. Reuse this one. The hover matched
   both of them exactly (--wg-panel), so no screen changed appearance adopting
   it. 🛑 RV114 SHIPPED tonight.php ONTO THIS RULE and `.tonight-*` is deleted
   below; `.equipment-row` is the one screen still on its own copy, and it
   keeps it because its row opens an inline PANEL rather than navigating.

   🛑 THE HANDLER IS `Code/assets/js/row-link.js`, loaded by portalFooter() on
   every portal screen since RV114 — it was inline on event.php (RV99/RV101)
   and copied into questions.php (RV113) before that. A new clickable row is
   markup only; never write a second handler. ../specs/portal_classes.md. */
.data-table tr.row-link { cursor: pointer; }
.data-table tr.row-link:hover td { background: var(--wg-panel); }

/* RV21 — activity.php's "who can run it" host table runs to 46 rows; Alex:
   "more vertically squished, so it's not such a long list." Half the base
   .data-table padding, scoped by ID so no other table on the site changes,
   and nothing about GV1's client-side filter (activity.php:377) is touched —
   this is padding only, same rows and cells. */
#hostTable th,
#hostTable td {
    padding: .3rem .7rem;
}

/* W34 — a single unbroken token (a long serial/tag) can be longer than
   .wrap's white-space: normal alone can break. A sibling class, not an
   addition to .wrap itself: other .wrap cells sitewide (dates among them)
   must never break mid-word. */
/* ⚠️ The bare `.wrap-anywhere` was added 2026-09-16 so the class also works OUTSIDE
   a table. `WGT_14302` measured the only text-block overflow in the whole portal:
   `hosts.php`'s Details paragraph prints the bare sign-up URL inside a `<strong>`,
   one unbreakable 345px token in a 324px (390) / 294px (360) column. 🛑 Prose being
   dragged past the viewport is a bug at ANY width — that half of `RV5` never
   changed, and it is the reason this is a wrap and not a shortened sentence. */
.data-table td.wrap-anywhere,
.wrap-anywhere { overflow-wrap: anywhere; }

/* F47 — the accordion's `.wg-section` padding (portal_sections.md) cost every
   converted table 2rem of width. Stacking the time under the date buys back
   the most width for the least change; each run stays `nowrap` on its own
   line rather than using `.wrap`, which would let the date break mid-word. */
.data-table td .night-time {
    display: block;
}

/* WGT_13670 — under 40rem the "Waiting on you" table stops being a table and
   becomes stacked label/value rows, one card per shift.

   ⚠️ WGT_14273 ADDED `#working`, THE OTHER TABLE ON THE SAME SCREEN. WGT_13670
   measured a page with NO ringing callout, so the *Working through the
   priorities* section was not rendered at all and was never covered. Seed one
   and the page scrolls 823px sideways at 390 (scrollWidth 1213 vs clientWidth
   390; 853px at 360), ALL of it this table — 1180px inside a 356px panel —
   while the waiting table beside it sits at 324px and does not. The two halves
   of one screen disagreed, which is the whole reason this is not RV5's design
   rule: RV5 is about a TABLE scrolling, and it was the PAGE.

   🛑 THE REASON IS THAT NOTHING ELSE CAN REACH IT. `.data-table--wide` (F47,
   above) widens `.portal-main` off its 40rem prose measure — but below 64rem
   `.portal-main` is ALREADY the full viewport, so there is no extra width to
   hand over. The four `nowrap` columns (date, time, and the two header cells)
   take their min-content out of the viewport FIRST, and the one paragraph
   column, "Where it stands", is left whatever is over.
   ⚠️ MEASURED 2026-09-12 at 390px with four callouts seeded: the cell was 95px,
   a `WGT_13303` sentence wrapped to 16 lines, and the PAGE overflowed by 165px.
   At 820px and up the same row set measured 320–674px and 2–4 lines with no page
   overflow at all, which is why the breakpoint and not a width tweak.

   🛑 THE STANDING RULE IS NOT BROKEN HERE. `CLAUDE.md`, *Seeing a screen*: a wide
   TABLE scrolling sideways is the DESIGN (RV5), not a bug — and it still is, at
   every width above this breakpoint and on every other table on the site. The
   exception this rule takes is the one the rule itself names: a PARAGRAPH that
   needs sideways room IS a bug, and "Where it stands" is a paragraph column.

   🛑 SCOPED TO `#waiting`, DELIBERATELY NOT TO `.data-table`. `.data-table` is
   shared by 15+ portal screens and exactly ONE of them has been measured at
   phone width; a global stacking rule would silently move fourteen screens
   nobody has looked at. `#waiting` is `portalSectionStart('waiting')` and
   `#working` is `portalSectionStart('working')`, both on missing-shifts.php and
   nowhere else in `Code/` (re-grepped 2026-09-16) — so this is still a
   one-screen rule by construction, not by convention, with both of that
   screen's two tables now inside it rather than one.

   ⚠️ 40rem is THIS FILE'S existing phone breakpoint (`.page-head-row`, below),
   reused rather than a new number. It is also `.portal-main`'s own measure, so
   the switch happens exactly where the prose column stops shrinking.

   🛑 THE `role=` ATTRIBUTES IN missing-shifts.php ARE PART OF THIS RULE. A
   browser drops a table's implicit ARIA roles when its `display` stops being a
   table display, so a stacked table announces as five unrelated blocks unless
   the roles are stated explicitly in the markup. Never ship this rule onto a
   table that has not had them added. */
@media (max-width: 40rem) {
    /* fit-content + overflow-x:auto is what made the page scroll sideways: the
       table's min-content was wider than the viewport. A stacked table has no
       min-content worth speaking of, so the box goes back to being ordinary. */
    #working .table-wrap,
    #waiting .table-wrap {
        width: auto;
        overflow-x: visible;
    }

    #working .data-table,
    #working .data-table tbody,
    #working .data-table tbody tr,
    #working .data-table tbody td,
    #waiting .data-table,
    #waiting .data-table tbody,
    #waiting .data-table tbody tr,
    #waiting .data-table tbody td {
        display: block;
        width: auto;
    }

    /* The column headers are printed per cell by the ::before below instead. */
    #working .data-table thead,
    #waiting .data-table thead {
        display: none;
    }

    /* The card separator moves from the cell to the ROW — between two cells of
       one shift a rule would read as a row boundary that is not there. */
    #working .data-table tbody tr,
    #waiting .data-table tbody tr {
        padding: .7rem 0;
        border-bottom: 1px solid var(--wg-line);
    }

    #working .data-table tbody tr:last-child,
    #waiting .data-table tbody tr:last-child {
        border-bottom: 0;
    }

    #working .data-table tbody td,
    #waiting .data-table tbody td {
        padding: 0 0 .25rem;
        border-bottom: 0;
        white-space: normal;
    }

    #working .data-table tbody td.row-action-cell,
    #waiting .data-table tbody td.row-action-cell {
        padding-top: .35rem;
        padding-bottom: 0;
    }

    #working .data-table tbody td[data-label]::before,
    #waiting .data-table tbody td[data-label]::before {
        content: attr(data-label) ": ";
        font-size: .8rem;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: .04em;
        color: var(--wg-muted);
    }
}

/* WGT_15385 — THE SAME STACKING, MADE OPT-IN BY CLASS. 👤 Alex, 2026-09-22
   (`WGT_15016` reading walk, decision 5): the question-set history table
   stacks into one card per use below the phone breakpoint.

   🛑 READ COLD AT 390 THAT TABLE LOOKED BROKEN, NOT MERELY WIDE. Measured on
   `temporary/WGT_15098/shots/questionset2_history_full-390.png` (390x2574):
   the `HOST` header was cut mid-word to `HOS…` and its value with it; the
   fourth column — "booked", and "taken off this night — does not count as
   used" — sat entirely off-screen until the panel was dragged sideways; and
   because that hidden column's prose is TALL, every override row carried
   ~200px of blank vertical space beside three short lines.
   ⚠️ PAGE overflow was 0 at 1440 AND at 390 (`WGT_15098`) — the table scrolled
   inside its own `.table-wrap` (client 324 / scroll 390), which is the designed
   behaviour. The defect was what that cost on THIS table, never that it
   scrolled, so nothing here changes what a scrolling table does anywhere else.

   🛑 A CLASS, NOT AN ID, AND THAT IS THE WHOLE POINT. The `#working`/`#waiting`
   rule above scopes by id because those two ids exist on ONE screen. `#history`
   does NOT: `portalSectionStart('history')` is also venue.php, equipment.php and
   settings.php (grepped 2026-09-22), so an `#history .data-table` rule would
   silently restyle three screens nobody has measured at phone width. The class
   goes on the ONE table its author has looked at.
   ⚠️ `.data-table` is shared by 15+ portal screens, so this stays opt-in for the
   same reason the rule above does — never promote either to bare `.data-table`.

   🛑 A TABLE MAY ONLY TAKE THIS CLASS ONCE ITS MARKUP CARRIES `role=` ON THE
   TABLE, BOTH ROW GROUPS, EVERY ROW AND EVERY CELL, AND `data-label=` ON EVERY
   `<td>`. A browser drops a table's implicit ARIA roles the moment its `display`
   stops being a table display, so a stacked table announces as unrelated blocks
   without them; and the card prints each cell's heading from `data-label`,
   because `thead` is hidden here.

   ✅ THE EDGE FADE (`WGT_14494`, above) SWITCHES ITSELF OFF and needs nothing
   here: `overflow-x: visible` stops the wrap being a scroll container, so the
   `scroll()` timeline is inactive and both fade lengths stay at 0.

   ⚠️ 40rem is this file's existing phone breakpoint, reused rather than a new
   number, and it is `.portal-main`'s own measure. */
@media (max-width: 40rem) {
    /* `fit-content` + `overflow-x: auto` is what made the panel scroll sideways.
       A stacked table has no min-content worth speaking of, so the box goes back
       to being an ordinary block. */
    .table-wrap--stack {
        width: auto;
        overflow-x: visible;
    }

    .data-table--stack,
    .data-table--stack tbody,
    .data-table--stack tbody tr,
    .data-table--stack tbody td {
        display: block;
        width: auto;
    }

    /* The column headings are printed per cell by the ::before below instead. */
    .data-table--stack thead {
        display: none;
    }

    /* The card separator sits on the ROW, never the cell — a rule between two
       cells of one use would read as a boundary between two uses. */
    .data-table--stack tbody tr {
        padding: .7rem 0;
        border-bottom: 1px solid var(--wg-line);
    }

    .data-table--stack tbody tr:last-child {
        border-bottom: 0;
    }

    .data-table--stack tbody td {
        padding: 0 0 .25rem;
        border-bottom: 0;
        white-space: normal;
    }

    .data-table--stack tbody td[data-label]::before {
        content: attr(data-label) ": ";
        font-size: .8rem;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: .04em;
        color: var(--wg-muted);
    }
}

/* Inline actions inside a table row must not stretch to full width. */
.data-table form { display: inline; margin: 0; }

.row-action {
    width: auto;
    min-height: 2.2rem;
    padding: .3rem .6rem;
    font-size: .85rem;
    font-weight: 600;
}

/* RV181 — gate [60] answered "yes, portal-wide": `.row-action` loses the
   browser-default blue/purple underline on its five remaining `<a>` uses
   (index.php, missing-shifts.php, event.php, unavailability.php,
   venues.php x2), matching what RV16 already did for `.entity-link`.
   Scoped to `a.row-action` — `button` (line 191) sets its own
   `color: var(--wg-accent-ink)`, and an unscoped `.row-action { color:
   inherit }` would out-specificity that element rule and repaint every
   `<button class="row-action">` too, which this task must not touch. */
a.row-action {
    color: inherit;
    text-decoration: none;
}

a.row-action:hover,
a.row-action:focus-visible {
    text-decoration: underline;
}

/* RV74 — the filter chip family, replacing W34's `.row-action[aria-current]`
   rule and the `.row-action`/`.button-quiet` pair hosts.php borrowed for its
   filters. MEASURED before touching it (temporary/rv74/), because RV65 had
   just DISPROVED the same complaint on its own screen: here the premise held
   — the active chip really rendered rgb(180,35,31) (--wg-accent) at radius
   999px while the other three were white at radius 0. Two faults, and Alex
   led with the second: red reads as alarm, and active/inactive were different
   SHAPES.

   🛑 Both states are now ONE shape and differ only in fill, which is what
   makes a set read as a choice rather than as four buttons. The selected fill
   is --wg-nav-head-bg, the palette's existing "dark, but deliberately not
   --wg-ink" slate: its own comment says full black reads as the loudest thing
   on an admin screen, and that reasoning applies to a chip too. No new colour
   token was added, and --wg-accent is not used here at all.

   ⚠️ Reusable on purpose (`.filter-chips`, not `.hosts-filters`) — hosts.php,
   albums.php and player.php are the callers (RV182). Do not restyle it for a
   single screen; extend it. The centring is RV19's flex mechanism
   (justify-content on a flex row), not a fourth centring idiom. */
.filter-chips {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: .4rem;
    margin: 0 0 1.5rem;
}

.filter-chips-label {
    margin-right: .25rem;
    font-size: .85rem;
    color: var(--wg-muted);
}

.filter-chip {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    min-height: var(--wg-tap); /* RV182 — was 2.2rem (35px), under the tap standard */
    padding: .3rem .8rem;
    font-size: .85rem;
    font-weight: 500;
    text-decoration: none;
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 999px;
}

.filter-chip:hover { background: var(--wg-panel); }

/* The tick is what tells a chip set apart from a button row at a glance, and
   it survives a greyscale print and a colour-blind reader — the fill alone
   does neither. It is decorative: <strong aria-current="page"> already says
   the same thing to a screen reader. */
.filter-chip[aria-current="page"] {
    color: var(--wg-paper);
    background: var(--wg-nav-head-bg);
    border-color: var(--wg-nav-head-bg);
    cursor: default;
}

.filter-chip[aria-current="page"]::before {
    content: "\2713";
    font-size: .8em;
}

/* RV88 §6a (2026-08-14) — the CHECKBOX member of the same family, and the
   reason it exists is that .qual-toggle was doing two jobs on two screens.
   RV79 measured the checked chip on albums.php at rgb(180, 35, 31) — the same
   --wg-accent red RV74 had just taken off hosts.php's filter row on Alex's
   "red as warning, I don't like it" — but the SAME class is the real
   qualification toggle on hosts.php?host=, where the red is the selection it
   is supposed to be. 🛑 Recolouring .qual-toggle would have changed a feature
   to fix a filter, so the class is split by JOB instead: filtering is the
   .filter-chips family, holding a qualification stays .qual-toggle.

   Two things the anchor chips above do NOT need:
   · cursor — .filter-chip[aria-current="page"] sets `default` because the
     chosen anchor is inert. A checkbox is never inert, so it is `pointer` in
     both states and the chosen one is still clickable (it un-filters).
   · the ::before tick — the native checkbox is visible inside the chip and IS
     the tick. Adding the glyph too would show two ticks side by side.
   ⚠️ The checked look is the family's own --wg-nav-head-bg fill, never
   --wg-accent: chosen and unchosen must differ only in fill (see above). */
.filter-chip--check {
    cursor: pointer;
}

.filter-chip--check:has(input:checked) {
    color: var(--wg-paper);
    background: var(--wg-nav-head-bg);
    border-color: var(--wg-nav-head-bg);
}

/* accent-color, not a restyled box: the box is a real checkbox and stays one.
   Left at its default it renders browser-blue against the slate fill and reads
   as a third colour; --wg-nav-head-bg fills the tick's box with the chip's own
   fill so only the white tick shows. */
.filter-chip--check input {
    margin: 0;
    accent-color: var(--wg-nav-head-bg);
}

/* RV1 — "+ Add an event" was `<a class="row-action">`: on a <button> that
   class rides on the element's own display/background/radius, but an <a>
   has none of that, so it rendered as plain text. Give it the same accent
   look as <button> directly, since a GET navigation can't be a <button>.
   Centred per the RV5 rule: width: fit-content + margin-inline: auto, never
   text-align: center — the same idiom .table-wrap uses. */
/* ⚠️ WGT_15382 — THE TOKENS MOVED WITH THE BARE `button` RULE, AND THEY HAD TO.
   This class exists to make an `<a>` look exactly like a `<button>`; leaving it
   on the accent would have made a NAVIGATION LINK the only red thing on the
   page. Both its users are plain navigation — venue-edit.php's *Add an event
   here* and night.php's *Change what this day uses →*. */
.add-event-button {
    display: block;
    width: fit-content;
    margin: 0 auto 1.5rem;
    padding: .7rem 1.4rem;
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    color: var(--wg-button-ink);
    background: var(--wg-button-bg);
    border-radius: 8px;
    cursor: pointer;
}

.add-event-button:hover { filter: brightness(1.08); }

/* --- Activities (RV20) — one card per activity, not a three-column table --
   Alex: "let's give each activity its own card." Long text is the point:
   the description and checklist get width instead of being squeezed into a
   table column. Same :last-child idiom as .kit-card/.av-card — whatever
   separates the cards has to suppress its own trailing margin, or the old
   table's last-row border line comes back in a new shape. */
/* RV76 (2026-08-14) — Alex: "Edit moves to each card's top right, and the card
   text centres." Every rule below is scoped to `.activity-card`, which is
   activities.php's own class and appears in no other Code/portal/*.php — so
   this centring cannot reach .detail-list, .data-table, a form field or any
   other screen's checklist. It is the batch's headings-and-prose ruling
   (2026-08-14 2:12pm): card title, description and checklist only. */
.activity-card {
    position: relative;
    margin: 0 0 .85rem;
    /* 🛑 SYMMETRIC, and that is the whole reason for the number. The Edit
       button is taken out of flow into the top right corner, so the card needs
       room for it; padding-RIGHT alone would shift the content box left and
       render "centred" text off the card's true centre by half a button.
       Measured 2026-08-14: the button is 74px wide, so 5.5rem (88px) clears it
       with a ~14px gap on both sides. */
    padding: .85rem 5.5rem;
    text-align: center;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.activity-card:last-child { margin-bottom: 0; }

.activity-card-title { margin: 0 0 .35rem; font-size: 1.05rem; }

/* The checklist centres with the rest of the card, but a centred <ol> whose
   markers hang out in the left padding reads as ragged — so the numbers come
   inside the text block. 🛑 Scoped to the card on purpose: RV19's sitewide
   "`.confirm-list` stays left" (see its comment block above) still holds
   everywhere else, including activity.php's own read-only preview. */
.activity-card .confirm-list {
    margin: .5rem 0 0;
    padding-left: 0;
    list-style-position: inside;
}

/* Out of flow, into the corner. .add-event-button's own block/auto-margin
   centring is sized for a standalone button under a heading; here the card's
   padding above reserves the room, so the margin goes entirely. */
.activity-card .add-event-button {
    position: absolute;
    top: .85rem;
    right: .85rem;
    margin: 0;
}

/* A field row that reads as one line on a desktop and stacks on a phone. */
.field-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1rem;
}

.field {
    flex: 1 1 12rem;
    min-width: 0;
}

/* AC58 — visual-only uppercase for a state-abbreviation input. The stored
   value is already uppercased server-side (hostValidateAddress()); this just
   stops the field looking different from what gets saved and from its own
   placeholder while the applicant is still typing. Never the only uppercase
   step — `autocapitalize="characters"` alongside it is what the phone
   keyboard reads, and neither touches what gets posted. */
.field input.wg-upper {
    text-transform: uppercase;
}

/* WGT_21809 — Town · State · ZIP on one row (👤 Graham, 2026-10-18: the
   order people are used to). Three `.field`s at their 12rem default do not fit
   a ~540px form column, so the ZIP wrapped onto a line of its own. State and
   ZIP are short values: State keeps a narrow fixed width, Town takes two
   shares of what is left and ZIP one. On a phone (measured at 390) Town and
   State share the first line and ZIP wraps to a full-width line of its own.
   Used by host-profile.php, hosts.php (office edit) and player.php (sign-up). */
.address-row .field-town  { flex: 2 1 10rem; }
.address-row .field-state { flex: 0 1 5rem; }
.address-row .field-zip   { flex: 1 1 7rem; }

/* PL3 (2026-08-27) — Alex, on player.php's radius control: "make the 25 only
   three digits wide. So that whole thing can be in one line instead of three
   separate lines."

   ⚠️ THE CAUSE IS THE BASE RULE, NOT `.field`. `label` is `display: block` and
   every text-entry input — `number` among them — is `display: block; width:
   100%; margin: 0 0 1rem`. That is three block boxes in a column, which is the
   three lines. So this class has to undo the display, the width AND the
   bottom margin; undoing only the width leaves the label still on its own line.

   🛑 SCOPED TO THIS CLASS, NEVER TO `.field input`. `.field` is shared across
   the whole portal, so a width there would reach every screen. It is also
   deliberately NOT keyed on the control's id — keying a rule on a section-slug
   id is a standing rule against in this project.

   ⚠️ `max="200"` is THREE digits, so the box is sized for three and not two.
   5rem leaves room for "200" at the 16px iOS-zoom minimum plus the number
   spinner Chrome draws inside the box; measured at 1440/1152/1024, the row
   does not wrap at any of them. `min-height: var(--wg-tap)` is inherited from
   the base rule and deliberately kept — this is still a 48px tap target.

   The row CENTRES rather than baseline-aligns, so the box and the words beside
   it sit level; `flex-wrap` is what lets it stack rather than clip if a
   narrower screen ever cannot fit it. */
.field-inline {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
}

.field-inline > label {
    margin: 0;
}

/* ⚠️ EVERY input in the row, not just the number one. PL5 put a text input
   (the ZIP) into `.field-inline` and it kept the base rule's `margin: 0 0 1rem`
   — measured: the row went 48px -> 64px tall and the box's centre sat 8px above
   its neighbours', because `align-items: center` centres a 64px item, margin
   included, against 48px ones. The margin reset belongs to the row, and the
   WIDTH is the only per-type part. */
.field-inline > input {
    display: inline-block;
    margin: 0;
}

.field-inline > input[type="number"] {
    width: 5rem;
}

/* RV25 — qualification toggles, grouped by qualifications.kind ('activity'
   vs 'capability') per Alex's "prettier toggles ... grouped together by
   type". A checkbox styled as a pill, `:has()` driving the checked look —
   the same technique RV19 already relies on, not a new mechanism. Still a
   native checkbox under the hood, so it stays one form posting the host's
   whole qualifications[] set (see the comment on $qualGroups below).
   🛑 RV88 §6a (2026-08-14) — IT HAS ONE CALLER NOW AND ONLY ONE JOB. It used
   to dress albums.php's two filter checkboxes as well, which is how a red
   "selected" look ended up meaning "filtered" on one screen and "this host
   holds this qualification" on the other; albums.php uses
   .filter-chip--check now. The accent red below is the SELECTION, and it is
   deliberately not the .filter-chips fill: do not "harmonise" the two, and do
   not reach for this class for a filter. */
.qual-group + .qual-group {
    margin-top: .75rem;
}

.qual-group h3 {
    margin: 0 0 .4rem;
    font-size: .95rem;
}

.qual-toggles {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
}

.qual-toggle {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .4rem .9rem;
    font-weight: 600;
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 999px;
    cursor: pointer;
}

.qual-toggle:has(input:checked) {
    color: var(--wg-accent-ink);
    background: var(--wg-accent);
    border-color: var(--wg-accent);
}

.qual-toggle input {
    margin: 0;
}

/* WGT_22841 (👤 Alex, 2026-10-23) — one host's qualifications as ONE vertical
   list, one per line, no group headings. Each pill keeps its own width
   (`flex-start`) rather than stretching to the column. */
.qual-toggles--stack {
    flex-direction: column;
    align-items: flex-start;
}

/* WGT_22841 — the grid's tick columns: "we want to center the check marks in
   those columns as well". The header and the cell centre together, so a tick
   sits under the middle of its short header. */
.qual-grid .qual-grid-col {
    text-align: center;
}

.qual-grid td.qual-grid-col label {
    display: inline-block;
    margin: 0;
}

.qual-grid td.qual-grid-col input {
    margin: 0;
    vertical-align: middle;
}

select {
    display: block;
    width: 100%;
    min-height: var(--wg-tap);
    margin: 0 0 1rem;
    padding: .6rem .75rem;
    font-size: 1rem;            /* 16px+ or iOS zooms the page on focus */
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

select:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: 2px;
}

/* LK13 — event-add.php's payment block packed a checkbox label and a text
   field into one half-width column of the row, so the checkbox label wrapped
   onto the note label beneath it. Stack all three controls one per line
   instead, same fix as .equipment-action's field-row below. */
.payment-row {
    display: block;
}

.payment-row .field {
    flex-basis: auto;
}

/* LK7 — equipment.php's filter form: .field-row defaults to align-items:
   stretch, so a bare .row-action sibling of a label+select .field stretches
   to the full column height instead of sitting beside the select. Align to
   the select's own bottom edge and match its height instead. */
.equipment-filter-row {
    align-items: flex-end;
}

.equipment-filter-row .row-action {
    min-height: var(--wg-tap);
    margin: 0 0 1rem;
}

/* RV145 — hosts.php's search row is the same shape: a label+input .field
   beside a bare .row-action Search button (and Clear link), so .field-row's
   default align-items: stretch padded the button out to the field's full
   label+input+margin height — about twice the input's own 48px box, which
   is what Alex saw. event.php's "Find a particular set" was checked and does
   NOT share this markup (its button sits in its own .button-row, not beside
   the .field), so it needed no change. Same fix as .equipment-filter-row
   above: align to the input's bottom edge and match its height, never
   shrink the input itself. */
.hosts-search-row,
.texts-filter-row {          /* texts.php's Channel + Show row, same shape (👤 Alex, 2026-09-18) */
    align-items: flex-end;
}

.hosts-search-row .row-action,
.texts-filter-row .row-action {
    min-height: var(--wg-tap);
    margin: 0 0 1rem;
}

/* THE SITEWIDE PAGE-HEAD ROW — a back link to the LEFT of the title, on every
   screen that has one. Alex, 2026-08-17, first for `hosts.php?host=` ("can we
   put all hosts to the left of Amy instead of below it?") and then, having seen
   it: "go through all the pages that have a similar link that goes back to
   something and put it on the left of the title like this one is."

   ⚠️ RENAMED FROM `.host-head-row`, WHICH SHIPPED ~90 MINUTES EARLIER on that
   one screen. It is emitted by `portalHeader()`'s `back` option now (nine
   screens), so a name saying `host` would be wrong on eight of them — and a
   screen-named class is exactly what `portal_classes.md` warns turns into a
   family nobody can reuse. **There is no `.host-head-row` left to fall back to.**

   🛑 A PREV/NEXT PAGER IS NOT A BACK LINK AND IS DELIBERATELY EXCLUDED —
   `published.php`'s *← Newer / Older →* and `tonight.php`'s date arrows. Both are
   PAIRS; hoisting the left half up beside the title strands the right half where
   the pair used to be, so the reader loses the one affordance that told them the
   two go together. **They keep their own row and must not be "finished" later.**

   🛑 WGT_18369 (2026-10-02) — TWO FLOATS AND A CENTRED TITLE, AND THE TITLE DROPS
   BELOW THE LINK WHEN IT DOES NOT FIT BESIDE IT. 👤 Alex, on his look of
   `WGT_18303` (tab 9, `checkin.php`): the venue name "should go below the back to
   schedule link so it can spread out across the whole line instead of being
   squished into the center column". It was a `1fr auto 1fr` grid until then, and a
   grid NEVER lets go: the title kept the middle track however little was left in
   it, so *P-Club — Friday, October 2* had 180px of a 576px column and ran to
   THREE lines beside an empty right-hand track.

   The shape now. The link floats left, a hidden mirror of it floats right, and the
   `<h1>` is an `inline-block` on a centred line between them. An inline-block is
   one unbreakable box as wide as its text (capped at the column), and a line that
   floats have shortened too far to hold its first box is moved DOWN past them —
   that is plain CSS 2.1 float behaviour, in every browser. So:
     · the title fits between the two → same line, dead centre, exactly as the
       grid drew it (the two floats are one width, so the middle of what is left
       IS the middle of the column);
     · it does not → the link keeps its place at the left, and the title takes the
       whole line below, centred, wrapping only if it is wider than the column.
   🛑 IT IS DECIDED BY THE RENDERED WIDTHS, NOT BY A COUNT. portalHeader()'s
   28-character test cannot know the column or the link: 26 characters beside a
   23-character link did not fit a 576px column, and 18 beside the same link fits
   an 896px one. Nothing that fitted on one line before this moved.

   🛑 THE MIRROR MUST COME BEFORE THE `<h1>` IN THE MARKUP (portal.php says why):
   a float after the title is placed after the title has already taken its line.
   ⚠️ `visibility: hidden`, NOT `display: none`, on the mirror — it has to take
   its width and paint nothing. Without it the title centres in what the link
   left and sits right of centre by half a link (measured on the grid: 49px on
   `question-set.php` at 1024).

   🛑 `.portal-main > h1 { text-align: center }` AND RV88's sitewide `.lede`
   centring BOTH STOP REACHING THESE, because the `>` combinator is scoped to
   `.portal-main`'s direct children and both elements are one level deeper. The
   row's own `text-align: center` is what centres the title's box on its line, and
   the `<h1>`'s own is what centres a title that wraps inside that box.

   🛑 `white-space: nowrap` ON THE LINK STAYS. On the grid a starved link track
   folded "← All events" onto THREE lines ("←" / "All" / "events") with every
   measured figure green. A float cannot be starved that way, but a link wider
   than a narrow column could still fold: a title is prose and wraps, a back link
   is a control and a folded control reads as broken.

   ⚠️ `margin-top: 1rem` ON THE TWO FLOATS IS THE BASELINE. The grid aligned the
   link's baseline to the title's (`align-items: baseline`), which put the link's
   box 16px below the row's top — measured before this change, and floats have no
   baseline alignment, so it is written out. It moves if the `h1` or `.lede` font
   size moves. `vertical-align: top` on the title keeps the line exactly the
   title's height (a baseline-aligned inline-block leaves descender room under it).
   UNDO: `git show fe3e2523:Code/assets/css/portal.css` holds the grid rules, and
   portal.php's mirror `<p>` goes back after the `<h1>`.

   ⚠️ `margin-bottom: .5rem` ON THE TWO FLOATS IS THE GAP ABOVE A DROPPED TITLE
   (👤 Alex, 2026-10-03, on his `WGT_18390` look of `checkin.php`: "a little more
   vertical gap"). Measured at 1440: gap 0 → 8px there. A title that stays BESIDE
   the link does not move, but its row is 3.6px taller (45.6 → 49.2 on
   `hosts.php?host=`), because the float is now taller than the title's line. */
.page-head-row {
    display: flow-root;
    margin: 0 0 1rem;
    text-align: center;
}

.page-head-row .lede {
    float: left;
    margin: 1rem .75rem .5rem 0;
    text-align: start;
    white-space: nowrap;
}

.page-head-row h1 {
    display: inline-block;
    max-width: 100%;
    margin: 0;
    text-align: center;
    vertical-align: top;
}

/* The hidden mirror of the link, on the right — see the block above. */
.page-head-row .page-head-spacer {
    float: right;
    margin: 1rem 0 .5rem .75rem;
    visibility: hidden;
}

/* THE STACKED LAYOUT, reached two ways — by VIEWPORT below 40rem, where nothing
   can share a line without wrapping mid-word, and by CONTENT via
   `.page-head-row--stacked`, which portalHeader() adds above 28 characters of
   title (its comment carries Alex's 2026-08-17 decision and the numbers). Both
   land on the same declarations on purpose: the link CENTRED on its own line and
   the title on the full line under it.
   ⚠️ This is not the same picture as a title the browser dropped (above): there
   the link stays at the LEFT, where it sits on every other screen. Nothing is
   floated here, so every float declaration above is undone by name. */
.page-head-row--stacked .lede {
    float: none;
    margin: 0 0 .25rem;
    text-align: center;
}

.page-head-row--stacked h1 {
    display: block;
}

@media (max-width: 40rem) {
    .page-head-row .lede {
        float: none;
        margin: 0 0 .25rem;
        text-align: center;
    }

    .page-head-row h1 {
        display: block;
    }

    /* 🛑 HERE `display: none` IS RIGHT, and it is the opposite call to the rule
       above for the opposite reason: nothing floats at this width, so the mirror
       has nothing to balance and would just be an invisible paragraph pushing
       the title down by a line. */
    .page-head-row .page-head-spacer {
        display: none;
    }
}

/* The bottom "View as <nickname>" button (hosts.php?host=) — `.button-row`
   already centres it (below); this only lifts it off the last accordion panel,
   which sits flush against it otherwise. */
.host-view-as-row {
    margin-top: 1.5rem;
}

/* RV19 — `justify-content: center`, not `text-align`, because this is
   already a flex row: the same "flex-centre a flex container, never
   text-align an inline-block" split RV5 draws below for .table-wrap,
   applied to the opposite case. Nothing in a .button-row is wide enough to
   overflow its column the way a data table can, so the RV1 warning against
   flex-centring overflowing content does not apply here. */
.button-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .75rem;
    align-items: center;
}

.button-row button { width: auto; min-width: 9rem; }

.button-quiet {
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
}

/* A link that must look like a button (👤 Alex, 2026-09-18: venue-edit.php's
   "Back to the venues"). The base `button` box, for an <a>: pair it with
   .button-quiet for the white secondary look. */
a.button-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 9rem;
    min-height: var(--wg-tap);
    padding: .7rem 1rem;
    font-size: 1rem;
    font-weight: 600;
    border-radius: 8px;
    text-decoration: none;
}

/* A destructive button, full-size — question-set.php's *Remove from the
   library*, venue-edit.php's *Close this venue*, event.php's *Delete event*.
   🛑 WGT_15382 (👤 Alex, 2026-09-22) REVERSED THE WEIGHTS HERE, AND THE OLD
   REASONING IS KEPT BECAUSE IT STILL HOLDS FOR THE PART IT WAS ABOUT.
   RV158 shipped this class PALE — `--wg-error-ink` on `--wg-error-bg` with a
   `#f5c6c2` border — on the ground that --wg-accent already means "selected"
   (RV88 §6) and could not also mean "destructive". That half stands: the token
   has not moved and must not. What was wrong was the OTHER half of the picture
   — the bare `button` rule was handing the same red to every safe button on the
   site, so the one genuinely destructive action was the PALEST thing on the
   screen and *Duplicate this event* was a full-width red bar. Alex's call is
   "red means danger only", so the red came off `button` and landed here.
   ✅ WHAT THIS IS NOT: it is NOT --wg-accent by another name. --wg-danger is its
   own token with its own meaning; the values match today and either may move.
   ⚠️ `border: 0` matches the base `button` box — a pale outline on a solid fill
   reads as a disabled control. */
.button-danger {
    color: var(--wg-danger-ink);
    background: var(--wg-danger);
    border: 0;
}

/* ⚠️ The shared `button:focus-visible` ring is `--wg-accent`, which is now this
   button's own fill — a red ring on red, saved only by the 2px offset. Give the
   danger button the slate ring instead, so the keyboard focus state is as loud
   here as it is everywhere else. */
.button-danger:focus-visible {
    outline-color: var(--wg-button-bg);
}

/* --- published.php scan log cards (LK15) ---------------------------------

   Alex: give the caption room to run horizontal instead of squeezing it into
   one narrow table column. Phone-first stack (photo, then meta, then
   caption, one under the other); from 36rem the photo moves to the left and
   spans both rows, with venue/date/time along the top beside it — the shape
   he described. .portal-main widens the same way .data-table--wide already
   does elsewhere, because a full-width caption is the whole point. */
.portal-main:has(.scan-log) {
    max-width: min(60rem, 100%);
}

.scan-log {
    display: flex;
    flex-direction: column;
    gap: .85rem;
    margin: 0 0 1.5rem;
}

.scan-card {
    display: grid;
    grid-template-columns: 1fr;
    gap: .6rem;
    padding: .85rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.scan-card-photo {
    display: block;
    width: 100%;
    max-width: 16rem;
    height: 10rem;
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid var(--wg-line);
}

/* WT14 — the action must never reflow onto its own line: a real
   publishedAt plus an albumLabel is long enough to push a flex .scan-card-
   action to the row below, moving the button relative to shorter cards. A
   fixed grid column pins it in place; venue/fields shrink and wrap their
   own text instead. */
.scan-card-top {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    /* stretch (the grid default), not start: .scan-card-action needs the
       full row height so it can bottom-anchor the button — see below. */
    gap: .35rem 1rem;
}

/* RV28 — Alex, stop 12: "confusing with different dates floating around
   that aren't really organized." Every field the card was already showing
   (event date, post date, host, album) now carries a visible label instead
   of being a bare .muted span, in the order he gave top to bottom. Venue
   name and the two fields that follow it stay in the wide left column;
   album moves into the action column so it sits directly above the button
   it explains, per his "above the Remove from Facebook button".

   🛑 RV112 (2026-08-15) — FLEX COLUMN → TWO-COLUMN GRID. Alex: event date,
   post date and host "should be aligned right, with their values aligned
   left beside them, so it reads as a column and lines up nicely". A flex
   column cannot do that: each row sized its own label, so the three pills
   shared a LEFT edge and their right edges came out ragged (measured 631 /
   667 / 676 at 1440, before). One grid owning all three rows is what makes
   the label edge and the value edge shared facts rather than per-row ones.
   ⚠️ This is NOT the .data-table exclusion. RV17 measured "cells stay left"
   on a .data-table with 193 real album rows; a scan card's label/value pair
   is not a table cell, so a right-aligned label column here is a scoped,
   deliberate exception — see portal_classes.md. Do not "fix" it.
   ⚠️ The venue name spans both columns: it is a heading for the pairs under
   it, not a value in the right-hand one. */
.scan-card-venue {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    /* row gap is RV28's .3rem unchanged; the column gap replaces the margin
       that used to sit on .scan-card-field-label — see below. */
    gap: .3rem .35rem;
    align-items: baseline;
}

.scan-card-venue > strong { grid-column: 1 / -1; }

/* RV112 — justify-self, NOT text-align. A grid item blockifies, so an
   inline-block pill would stretch to the whole auto-sized column and the
   three pills would come out equal-width with their text floating inside
   them. `end` keeps each pill its own natural width and puts its RIGHT edge
   on the column's end, which is the edge Alex is lining up. */
.scan-card-field-label { justify-self: end; }

/* RV112 — the value cell. min-width: 0 lets it shrink below its content on
   a card whose album badge has starved the left column (RV77 measured that
   squeeze at 1024), so the pair still lines up instead of forcing the card
   wider. */
.scan-card-field-values { min-width: 0; }

/* RV77 (2026-08-14) — Alex: "put the words event date and post date in this
   span as a pill label and not the date itself." 🛑 THE PILL CARRIES THE
   LABEL, NEVER THE VALUE. So this stopped being a look of its own (a plain
   bold muted word) and became a MODIFIER composed onto .scan-card-badge:
   `class="scan-card-badge scan-card-field-label"`. The pill look therefore
   still lives in exactly one place — portal.md's "reuse the shared class,
   never restyle one for a single screen" — and all this adds is the gap
   between the pill and the value that now sits beside it.
   ⚠️ The gap is NOT on .scan-card-badge itself: the album badge is the only
   thing in the right-aligned .scan-card-action column, and a right margin
   there would push it out of line with the button underneath it.
   🛑 RV112 (2026-08-15) MOVED THAT GAP ONTO THE GRID and left this block
   with nothing to declare, so the modifier is now carried entirely by
   .scan-card-venue's column-gap and justify-self rules above. The reason it
   must not live on .scan-card-badge is unchanged and is why the gap became
   a COLUMN gap rather than a margin. */

/* RV77 — a date is one token to a reader, so it must not break in half. The
   value used to live INSIDE the pill, where the pill's own box wrapped it;
   out in the running text it is bare inline content and 1024 (portal-main
   800px, and the album badge in the right column starving the left one to
   ~200px) broke it as "Aug 11," / "2026" — measured on the before harness,
   where the pill itself wrapped the same way, so this is a pre-existing
   break that moving the value merely relocated. The label now stays put and
   the whole date drops to the next line together.
   ⚠️ Deliberately NOT applied to the host name — a two-word name wrapping is
   normal, and nowrap there would push the column wider instead. */
.scan-card-field-value { white-space: nowrap; }

/* Event date and album are the two Alex asked for "on a coloured
   background" / "as ONE coloured span" — the same neutral info pill used
   elsewhere (RV5's gap panels, the view-as banner), never .wg-alert-* or
   .qual-toggle's accent, because neither carries a warning or a selection.

   🛑 RV88 §6b (2026-08-14) — `.pill` JOINS THIS RULE RATHER THAN GETTING ONE
   OF ITS OWN. RV79 found `class="pill"` on activities.php and activity.php
   with NO `.pill` selector in either stylesheet: the word appears 13 times in
   this file and every one is a comment about .qual-toggle or the Q3 flags,
   which is exactly why it read as covered. The "this night only" marker was
   rendering as plain unstyled text. Giving it its own block would have made a
   second copy of the same six declarations and broken the one rule this file
   keeps about pills — the look lives in exactly one place (portal.md, "reuse
   the shared class, never restyle one for a single screen"). One block, two
   selectors, so a change to the look still cannot drift between them.
   ⚠️ The @media cap below is scoped `.scan-card-action .scan-card-badge` and
   deliberately does NOT follow: it exists because a long album name sets the
   width of published.php's auto-sized action column, and "this night only" is
   a fixed two-word string in running text.
   ⚠️ NEITHER EMITTER FIRES ON LOCAL DEV — `activities WHERE venueEventId IS
   NOT NULL` is 0 rows, the same untested branch GV6 flagged — so this was
   proved on a harness copying the real markup out of both files
   (temporary/rv88/pill.html), never asserted from the source. */
.scan-card-badge,
.pill {
    display: inline-block;
    padding: .15rem .55rem;
    color: var(--wg-info-ink);
    background: var(--wg-info-bg);
    border: 1px solid var(--wg-info-line);
    border-radius: 999px;
}

/* Album labels run long (venue-chosen names, never shortened — WT14).
   Below 36rem venue/action sit side by side (the >=36rem query only moves
   the PHOTO, not this pair — see .scan-card-top above), so an unconstrained
   badge sets the auto-sized action column's width there, squeezing
   scan-card-venue and reflowing the whole card taller ONLY on cards that
   carry an album — measurably moving the button WT14 pinned (was 102px,
   RV28 harness). Capped to the button's own rough width and wrapped
   instead, matching the "never shortened" rule since wrapping isn't
   truncating. Scoped to the narrow case: at >=36rem the extra room means
   an unwrapped badge already leaves the button in the same place (RV28
   harness measured a 1px delta, unscoped), so it stays a single line. */
@media (max-width: 35.9375rem) {
    .scan-card-action .scan-card-badge {
        max-width: 9.5rem;
        overflow-wrap: anywhere;
        text-align: right;
    }
}

/* WT14's fixed-place guarantee, restated for the two-column layout: the
   left column's field list is the same four lines on every card (post
   date falls back to "not recorded", host is never absent), so its height
   no longer varies between card kinds.
   🛑 RV77 (2026-08-14) REVERSED THE VERTICAL HALF OF THAT, KNOWINGLY. Alex,
   looking at the rendered card: "album name and Remove from Facebook align
   TOP, not centre." `justify-content` was `flex-end`, which bottom-anchored
   the pair inside a stretched grid cell and left the album badge floating
   level with the third field instead of with the venue name. It is
   `flex-start` now, so the column starts where .scan-card-venue starts.
   ⚠️ WHAT THAT COSTS: it is now the button's TOP edge that lands identically
   between card kinds rather than its bottom, so on a card carrying an album
   the button sits ~34px lower than on one without. WT14's actual complaint
   — the action reflowing onto its own LINE — is unaffected: that is held by
   .scan-card-top's fixed grid column, not by this.
   ⚠️ `align-items: flex-end` is the HORIZONTAL half and is untouched; RV77
   was about vertical alignment only. */
.scan-card-action {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: flex-start;
    gap: .4rem;
    height: 100%;
}

.scan-card-caption { overflow-wrap: anywhere; }

@media (min-width: 36rem) {
    .scan-card {
        grid-template-columns: 9rem 1fr;
        grid-template-rows: auto auto;
        column-gap: 1rem;
        row-gap: .5rem;
        align-items: start;
    }

    .scan-card-photo {
        grid-column: 1;
        grid-row: 1 / 3;
        max-width: none;
        height: 100%;
        min-height: 8rem;
    }

    .scan-card-top { grid-column: 2; grid-row: 1; }
    .scan-card-caption { grid-column: 2; grid-row: 2; }
}

/* hosts.php's Sub Availability list (WT13's cards) went back to a plain
   .data-table under RV26, once RV5 removed the width cap that forced the
   card layout in the first place — see RV26 in completed/review.md. */

/* --- The night page (C1) — dim room, one hand, one big target ---------- */

.checkin-button {
    width: 100%;
    min-height: calc(var(--wg-tap) * 1.5);
    font-size: 1.25rem;
    font-weight: 700;
}

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

.night-item { border-top: 1px solid var(--wg-line); }
.night-item:first-child { border-top: 0; }

.night-item.is-today {
    background: var(--wg-ok-bg, rgba(46, 125, 50, .08));
}

/* RV195 — ONE ROW PER NIGHT: date left, venue middle, activity right (Alex,
   2026-08-29). It was `column`, so each night stacked into three lines.
   ⚠️ `flex-wrap: wrap` is deliberate, not a hedge — a host reads this on a
   phone in a dark bar, and four items on a 360px screen must fall onto a
   second line rather than squeeze the venue name to nothing. The row is single
   wherever there is room for it, which is every desktop and most phones. */
.night-link {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    align-items: baseline;
    gap: .15rem .75rem;
    padding: .75rem .5rem;
    min-height: var(--wg-tap);
    text-decoration: none;
    color: inherit;
}

/* 🛑 GRID, NOT FLEX, AND THE DIFFERENCE IS VISIBLE. Flex packs each span
   against the one before it, so with dates of different lengths ("Tuesday,
   September 1" vs "Thursday, September 24") the venues start at a different x
   on every row and the column Alex asked for is not a column at all. Grid
   tracks give all three a shared left edge. The optional check-in state takes
   the fourth track, so the activity stays in the third whether or not a row
   has one.
   ⚠️ `minmax(0, 1fr)` on the venue, not `1fr`: a grid item's default
   `min-width: auto` refuses to shrink below its content, so a long venue name
   would push the activity off the right rather than wrap. */

.night-link:hover .night-where { text-decoration: underline; }

/* The three columns. Only the venue flexes: the date and the activity are
   short and known, and letting the middle absorb the slack is what keeps the
   activity hard against the right edge without a `justify-content` rule that
   would also move the other two. */
/* 🛑 A MIN-WIDTH, AND IT IS WHAT MAKES THE VENUES A COLUMN. Each row is its
   OWN grid — the grid is on the <a>, not the list — so `auto` sizes column one
   to that row's own date and every venue starts at a different x. Measured:
   "Tuesday, September 1" and "Thursday, September 24" differ by ~2rem, which
   is exactly how ragged it looked. Giving the date a floor lines them all up
   without moving the grid onto the list, which would need `display: contents`
   on the anchor and would take the clickable row, its padding and its hover
   with it. */
.night-when  { min-width: 16rem; font-size: .95rem; color: var(--wg-muted); }
.night-item.is-today .night-when { color: inherit; font-size: 1.05rem; }
.night-where { flex: 1 1 auto; font-weight: 600; }
.night-what  { flex: 0 0 auto; font-size: .95rem; color: var(--wg-muted); }
.night-cta   { flex: 0 0 auto; font-weight: 600; }

/* 🛑 THE MEDIA QUERY MUST SIT AFTER THE THREE RULES ABOVE, NOT BESIDE
   `.night-link`. Media queries carry no extra specificity, so source order
   decides — written above them, its `min-width: 0` lost to the `16rem` every
   time and the phone layout silently did not happen. Measured at 390px: the
   venue stayed in the right-hand column and wrapped to three lines. */
@media (max-width: 30rem) {
    /* A phone in a dark bar. Four tracks at 360px squeezes the venue to a
       word per line, so below this the row stacks — the same content, and the
       single row is kept for every width that can actually hold one. */
    .night-link {
        grid-template-columns: 1fr auto;
    }
    .night-when  { min-width: 0; }
    .night-where { grid-column: 1 / -1; }
}

/* WGT_22011 — 👤 Alex, 2026-10-20, once WGT_22004 put the town beside the
   venue ("The Trap, Smithfield"): "the town is too far to the right and kind
   of squishes up against the activity … make the whole table a bit wider so
   there's equal space between the three columns."
   🛑 ONE SHARED GRID, BY SUBGRID. Each row used to be its own grid with a
   16rem floor on the date, so the slack all landed after the date and the
   venue ran into the activity. Now the list holds the tracks and every row's
   <li> and <a> take them as a subgrid: each column is as wide as its widest
   entry, they still line up row to row, and `space-between` makes the two gaps
   equal. The fourth track (Check in → / Checked in ✓) exists only when a row
   has one, or its empty track would take a third gap at the right edge.
   ⚠️ Scoped to #your-nights: `.night-list` is also event.php's night folds,
   and that later rule (`display: flex`) would win on source order otherwise.
   The phone stack below 30rem is untouched. */
/* WGT_22012: from 40rem, not 30 — between the two, Rob's four columns
   squeezed the date to 65px; the per-row grid above is kept there. */
@media (min-width: 40.0625rem) {
    #your-nights .night-list {
        display: grid;
        grid-template-columns: repeat(3, auto);
        justify-content: space-between;
        gap: 0 1.25rem;   /* the MINIMUM gap; space-between adds the rest.
                             The subgrids inherit it; a gap set on them
                             instead pads the tracks unevenly (measured) */
    }
    #your-nights .night-list:has(.night-link > :nth-child(4)) {
        grid-template-columns: repeat(4, auto);
    }
    #your-nights .night-item,
    #your-nights .night-link {
        display: grid;
        grid-template-columns: subgrid;
        grid-column: 1 / -1;
        column-gap: normal;   /* = the list's 2rem; `.night-link`'s own .75rem
                                 would be added to the tracks unevenly */
    }
    #your-nights .night-when { min-width: 0; }
}

/* …and the panel itself a little wider than the 40rem reading column, on a
   screen with room either side (below 64rem the column fills the window).
   ⚠️ The explicit `width` on the column is what lets the margins widen the
   panel — RV68's shrink-to-fit trap above `.portal-main:has(.event-detail)`.
   Without it, measured: the column shrank by the 6rem instead (603 → 506px)
   and the panel stayed the same width. */
@media (min-width: 64rem) {
    .portal-main:has(#your-nights) { width: min(40rem, 100%); }
    #your-nights { margin-inline: -4rem; }   /* WGT_22012: was -3rem */

    /* WGT_22012 — THE DATE ON ONE LINE, ON A WIDE SCREEN. Rob's list (a Check
       in column and "Uno's Bellingham, Bellingham") did not fit, and the grid
       shrank the date track until every date wrapped. 🛑 `max-content`, NOT
       `white-space: nowrap` alone: through the nested subgrids the date's
       min-content did not hold its track, so a nowrap date ran 20px INTO the
       venue (measured). The venue is the column that gives. Below 64rem the
       date may wrap again — there a one-line date would push the page. */
    #your-nights .night-list { grid-template-columns: max-content repeat(2, auto); }
    #your-nights .night-list:has(.night-link > :nth-child(4)) {
        grid-template-columns: max-content repeat(3, auto);
    }
    #your-nights .night-when { white-space: nowrap; }
}
/* WGT_22012 — and one step wider where the window has the room, so Rob's
   longest names ("Uno's Bellingham, Bellingham") stay on one line too. */
@media (min-width: 80rem) {
    #your-nights { margin-inline: -5rem; }
}

/* RV195 — `.night-spec` IS DELETED. It styled exactly one link, index.php's
   per-night "Read the spec sheet →", and 👤 Alex moved that off the board on
   2026-08-29: the spec sheet belongs on the individual event page, where
   checkin.php and night.php already show it as `.add-event-button`. With its
   one caller gone the rules had none. ⚠️ Deleted rather than left "in case" —
   VA11's own note said class names are an interface, and an interface nothing
   implements is a trap for the next person who finds it and assumes a caller. */

/* The night's one worklist (C2) — checklist steps plus the self-ticking
   items, all 48px+ targets for a thumb in a dark room. */

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

.task-item {
    border-top: 1px solid var(--wg-line);
}
.task-item:first-child { border-top: 0; }
.task-item form { margin: 0; }

/* 🛑 RV196 — THE MARK NO LONGER TAKES THIS PADDING, and that padding is what
   made the tick sit through the bottom of its circle (Alex, 2026-08-29, of the
   bonus list: "some of the check marks aren't spaced correctly so the check
   mark intersects the bottom of the circle").
   Measured: `.task-mark` is 1.7rem square with `border-radius: 50%`, and this
   rule added .7rem top and bottom — so the box rendered ~1.7 x 3.1rem. A 50%
   radius on a box that is not square is an ELLIPSE, and the glyph, positioned
   by `line-height` against the padded box rather than the visible ring, landed
   low against it. The label keeps the padding, so the row is exactly as tall
   as it was. */
.task-item > .task-label {
    display: inline-block;
    padding: .7rem 0;
}

.task-item > .task-mark {
    display: inline-block;
    padding: 0;
}

.task-toggle {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: 100%;
    min-height: var(--wg-tap);
    padding: .55rem .35rem;
    text-align: left;
    font-size: 1.05rem;
    color: var(--wg-ink);
    background: transparent;
    border: 0;
    border-radius: 0;
}

.task-toggle:hover { filter: none; background: var(--wg-panel); }

.task-mark {
    /* ⚠️ CENTRED BY FLEX, NOT BY `line-height`. A line box is positioned on a
       baseline, so it centres a glyph only when the box height and the line
       height happen to agree — they did not here, and any future change to the
       tick's font size would break it again. `box-sizing` keeps the 2px border
       inside the 1.7rem so the ring stays a circle. */
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: 1.7rem;
    height: 1.7rem;
    line-height: 1;
    margin-right: .6rem;
    text-align: center;
    border: 2px solid var(--wg-line);
    border-radius: 50%;
    font-weight: 700;
    color: #2e7d32;
}

.task-toggle .task-mark { margin-right: 0; }
.task-done .task-mark { border-color: #2e7d32; }

/* 👤 Alex, 2026-10-27 — the night page's job folds (`checkinFoldStart()`,
   checkin.php): a green check left of a job that is done, a red circle with a
   white X left of one the host can do now. The done mark is `.task-done
   .task-mark`'s green ring, so the two checks on the page read as one thing. */
.fold-mark {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: 1.7rem;
    height: 1.7rem;
    line-height: 1;
    border-radius: 50%;
    font-weight: 700;
}
.fold-mark--done { border: 2px solid #2e7d32; color: #2e7d32; }
.fold-mark--todo { background: var(--wg-danger); color: var(--wg-danger-ink); }

/* 👤 Alex, 2026-10-27 (WGT_23845) — Host performance → *Nights worked*: one table
   as wide as its panel, and each host's row a `<details>` that opens that host's
   nights under it. `width: 100%` overrides `.table-wrap`'s fit-content (its
   load-bearing `max-width: 100%` cap stays). The summary is laid out as the two
   columns the header names; the chevron is `.wg-section > summary::before`'s. */
.nw-totals-wrap { width: 100%; }
.nw-totals-table th:last-child { text-align: right; }
.nw-totals-table > tbody > tr > td { padding: 0; }
.nw-host > summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    min-height: var(--wg-tap);
    padding: .4rem .7rem;
    cursor: pointer;
    list-style: none;
}
.nw-host > summary::-webkit-details-marker { display: none; }
.nw-host > summary::before {
    content: '';
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid var(--wg-muted);
    border-bottom: 2px solid var(--wg-muted);
    transform: rotate(-45deg);
}
.nw-host[open] > summary::before { transform: rotate(45deg); }
.nw-host > summary:focus-visible { outline: 3px solid var(--wg-accent); outline-offset: -3px; }
.nw-host-name { flex: 1 1 auto; text-align: left; overflow-wrap: anywhere; }
.nw-host-count { flex: 0 0 auto; text-align: right; white-space: nowrap; }
.nw-host > .table-wrap { margin: 0 .7rem .75rem 1.8rem; }
.task-done .task-label { color: var(--wg-muted); }
.task-label { flex: 1 1 auto; }
.task-auto { display: flex; align-items: center; padding: 0 .35rem; }

/* 🛑 `WGT_15505` — THE GROUP HEADING HAS TO OUT-MEASURE ITS OWN STEPS, and it
   did not. Measured 2026-09-22: the row naming which activity a block of steps
   belongs to (`checkin.php`, drawn only when a shift bundles more than one
   activity) rendered at **17px against 17.85px steps** — smaller than its own
   children — with no margin and the same 1px hairline every other row carries.
   It read as one more step.
   ⚠️ The label is sized rather than the `<li>`: the `<strong>` inside inherits
   it, and sizing the row would move the `.task-mark` circle on every heading.
   ⚠️ A step's label takes 1.05rem from `.task-toggle` above, so this value has
   to CLEAR that, not match it. */
.task-group-heading {
    margin-top: .9rem;
    border-top: 2px solid var(--wg-line);
}
.task-group-heading > .task-label { font-size: 1.15rem; }

/* W4 — a hard 14rem floor on each of the two inputs (28rem+ before the
   button) is exactly what turned activities.php into the worst offender
   .table-wrap was hiding: 704px of table the column never had. flex-wrap
   plus a shrinkable basis lets the row give way and drop to a second line
   instead of forcing the whole table wider than the page. */
.rename-form { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.rename-form input[type="text"] { flex: 1 1 10rem; min-width: 0; }

/* WT7 — albums.php turned event-major; GV4's Album/Venue-night widths were
   measured for the old album-major table and do not apply to this one — a
   transposed table is a different table, re-measured from scratch. The
   Venue night column is now the narrower, fixed-text one; Album carries
   either the mapped text or the colspan="2" picker. */
.albums-table-event  { width: 28%; }
.albums-table-status { width: 30%; }

/* The attach form itself — now a search box ahead of the select, per WT7.
   Deliberately NOT .rename-form — that rule's flex-wrap: wrap is exactly the
   stacking behaviour being removed here. `.data-table .attach-form`, not
   bare `.attach-form`: `.data-table form { display: inline }` above outranks
   a single-class selector, so a bare rule here is silently overridden and
   the button never leaves the line. */
.data-table .attach-form { display: flex; flex-wrap: nowrap; gap: .5rem; align-items: center; }
.data-table .attach-form select { flex: 1 1 auto; min-width: 0; }
.data-table .attach-form input[type="text"] { flex: 0 1 9rem; min-width: 5rem; margin: 0; }

/* RV17 — the match list that REPLACES the <select> above while its search
   box carries text (see albums.php's own script comment for why the
   <select> stays in the DOM rather than being removed). Sits in the same
   flex slot the select occupies, so it takes the same flex rule. */
.data-table .attach-form .album-matches { flex: 1 1 auto; min-width: 0; }

/* WGT_19446 — the "Add another album" box (WGT_19350) puts a paragraph inside
   the same nowrap flex row as its search, select and button, so the opened
   box ran ~540px past its column and the Attach button sat off the panel
   (measured on Graham's host at 1680: wrap 1343, scroll 1894). Here only:
   wrap, give the paragraph the whole first line, let the select shrink from
   a 10rem basis so search · select · Attach share the next line. The main
   picker above is unchanged. */
.data-table .album-add-other .attach-form { flex-wrap: wrap; }
.data-table .album-add-other .attach-form > p { flex: 1 1 100%; margin: .25rem 0; }
.data-table .album-add-other .attach-form select { flex: 1 1 10rem; }

.album-matches {
    display: flex;
    flex-direction: column;
    max-height: 11rem;
    overflow-y: auto;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

/* WGT_19450 — `display: flex` above beats the `hidden` attribute, so an empty
   bordered sliver sat in every unmatched row's picker until the search box
   had text. The script removes `hidden` while it does. */
.album-matches[hidden] { display: none; }

.album-match-item {
    display: block;
    width: 100%;
    min-width: 0;
    padding: .4rem .6rem;
    font: inherit;
    text-align: left;
    color: var(--wg-ink);
    background: none;
    border: 0;
    border-bottom: 1px solid var(--wg-line);
    white-space: normal;   /* a <button>'s UA default is nowrap — without this
                               a long album name blows the flex row's width out
                               instead of wrapping */
    overflow-wrap: anywhere;
    cursor: pointer;
}

.album-match-item:last-child { border-bottom: 0; }

.album-match-item:hover,
.album-match-item:focus-visible {
    background: var(--wg-panel);
}

.album-match-empty {
    margin: 0;
    padding: .4rem .6rem;
    color: var(--wg-muted);
    font-size: .9rem;
}

/* Winner photos (C4) */

.photo-grid {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: .75rem;
}

.photo-cell img {
    display: block;
    width: 100%;
    height: 140px;
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid var(--wg-line);
}

.photo-cell form { margin: .25rem 0 0; text-align: center; }

.photo-cell form .link-button {
    min-height: var(--wg-tap); /* RV182 — was 29px (inherited .link-button), under the tap standard */
}

/* The caption (P2). Wraps rather than truncating — it is about to be public
   text on somebody's Page, so a host must be able to read all of it back. */
.photo-caption {
    margin: .35rem 0 0;
    font-size: .85rem;
    line-height: 1.35;
    color: var(--wg-muted);
    overflow-wrap: anywhere;
}

/* The album screen (P2) */

.photo-thumb {
    display: block;
    width: 72px;
    height: 72px;
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid var(--wg-line);
}

/* ⚠️ Each candidate album is a whole tappable line, not a bare radio: the label
   carries the photo count and the reason a suggestion is doubtful, and those are
   the parts that stop a wrong album being picked. */
.album-choice {
    display: inline-block;
    padding: .2rem 0;
    line-height: 1.4;
    cursor: pointer;
}

.album-choice input { margin-right: .4rem; }

.album-paste {
    width: 100%;
    max-width: 22rem;
    margin-top: .35rem;
    font-size: .85rem;
}

input[type="file"] {
    width: 100%;
    font-size: 16px;
    padding: .5rem 0;
}

/* WGT_20952 — 👤 Alex, 2026-10-13, walk tab 15 (a day's questions): "I don't
   like how the choose file option is off to the left. Can we center that? And
   put no file chosen under it … with a nice little bordered span around it with
   rounded edges." `.file-pick` is a centred column: the file's name in a rounded
   box, then the *Choose file* button (name ABOVE since Alex's second look, and
   the button on a light fill "to show it more as a button"). The real input is only hidden once the
   page's script has added `.is-enhanced`, so with no script the browser's own
   picker shows. It stays focusable: Tab reaches it, and the button shows the ring. */
.file-pick {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
    margin: 0 0 .5rem;
}

.file-pick.is-enhanced .file-pick-input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    opacity: 0;
    overflow: hidden;
    clip-path: inset(50%);
}

.file-pick-button {
    display: inline-block;
    margin: 0;
    padding: .55rem 1.2rem;
    border: 1px solid var(--wg-button-bg);
    border-radius: 8px;
    background: #e6ebf2;
    color: var(--wg-ink);
    font-weight: 600;
    cursor: pointer;
}

.file-pick-button:hover { background: #d8dfe9; }

.file-pick-input:focus-visible ~ .file-pick-button {
    outline: 2px solid var(--wg-button-bg);
    outline-offset: 2px;
}

.file-pick-name {
    display: inline-block;
    max-width: 100%;
    padding: .3rem .9rem;
    border: 1px solid var(--wg-line-mid);
    border-radius: 999px;
    background: var(--wg-paper);
    color: var(--wg-muted);
    font-size: .9rem;
    overflow-wrap: anywhere;
    text-align: center;
}

.file-pick-name.is-picked { color: var(--wg-ink); font-weight: 600; }

/* `display` above would beat the `hidden` attribute; the script un-hides both. */
.file-pick-button[hidden],
.file-pick-name[hidden] { display: none; }

/* The photo drop zone, previews and per-file captions (P6). `.photo-drop`
   wraps the real `input[type="file"]` rather than replacing it — with
   photo-upload.js absent this is just a bordered box around the ordinary
   file button, and `.is-dragover` is added/removed by that script only. */

.photo-drop {
    padding: 1rem;
    border: 2px dashed var(--wg-line-mid);
    border-radius: 8px;
    background: var(--wg-panel);
    text-align: center;
    transition: border-color .15s ease, background-color .15s ease;
}

.photo-drop.is-dragover {
    border-color: var(--wg-accent);
    background: var(--wg-paper);
}

.photo-drop input[type="file"] {
    margin: 0 0 .5rem;
}

.photo-drop-hint {
    margin: 0;
    font-size: .85rem;
}

.photo-picks {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
    display: grid;
    gap: .6rem;
}

.photo-pick {
    display: flex;
    align-items: flex-start;
    gap: .75rem;
    padding: .6rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

.photo-pick-thumb {
    flex: 0 0 auto;
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid var(--wg-line);
}

.photo-pick-info {
    flex: 1 1 auto;
    min-width: 0;
}

.photo-pick-name {
    margin: 0 0 .35rem;
    font-size: .85rem;
    color: var(--wg-muted);
    overflow-wrap: anywhere;
}

/* Beats the base `input[type="text"]` rule (equal specificity, later in the
   file wins) without `!important` — there are zero uses of it in this file. */
input.photo-pick-caption {
    margin: 0;
}

.photo-pick-remove {
    flex: 0 0 auto;
    width: auto;
    min-height: var(--wg-tap); /* RV182 — was 29px (inherited .link-button), under the tap standard */
}

/* WGT_24243 — the photo form's busy state, set by photo-upload.js on the first
   press of Upload. The button is disabled and reads "Uploading…"; the line under
   it says how many photos are on their way, with a small turning ring so a slow
   upload on bar wifi still looks alive. Without JS none of this exists. */
.is-uploading button[type="submit"]:disabled {
    opacity: .7;
    cursor: progress;
}

.photo-upload-status {
    margin: .5rem 0 0;
    text-align: center;
    font-size: .9rem;
    color: var(--wg-muted);
}

.photo-upload-status[hidden] { display: none; }

.photo-upload-status::before {
    content: "";
    display: inline-block;
    width: .85em;
    height: .85em;
    margin-right: .45em;
    vertical-align: -.1em;
    border: 2px solid var(--wg-line-mid);
    border-top-color: var(--wg-accent);
    border-radius: 50%;
    animation: wg-photo-upload-turn .8s linear infinite;
}

@keyframes wg-photo-upload-turn {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .photo-upload-status::before { animation: none; }
}

/* The tonight board (C6) */

.date-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1rem;
    margin: 0 0 1rem;
    padding: .5rem 0;
    border-bottom: 1px solid var(--wg-line);
}

/* An empty state directly under a form was touching its submit button —
   measured on player.php's merged Find-a-game panel, 2026-08-27: "No venues to
   show right now" sat 0px under the Search button, because `.button-row` sets
   no bottom margin and `.empty-state` sets no top one.

   ⚠️ THE SIBLING IS THE FORM, NOT THE BUTTON ROW, and getting that wrong is
   why the first attempt at this rule measured 0px and changed nothing: the
   `.button-row` is INSIDE the form, so `.button-row + .empty-state` never
   matches anything. Measure the gap, do not assume the selector bit.

   🛑 SCOPED TO THE ADJACENCY, not added to `.empty-state` itself. The class is
   used across a dozen screens where it already sits under prose and is spaced
   correctly; a top margin on the class would push all of those. Same idiom as
   SU2's `.auth-note + form` — the gap belongs to the pairing. Reach, counted:
   TWO callers, this panel and equipment.php:802, where the same gap applies. */
form + .empty-state {
    margin-top: 1rem;
}

/* --- PL2b (2026-08-27) — the player's search panel centres its own labels ---

   👤 Alex, on the merged Find-a-game panel: "Center the ZIP Code, What are you
   looking for, leave unboth unticked, how far counts as near."

   🛑 THIS IS A DELIBERATE REVERSAL OF RV19 AND RV146, NOT DRIFT, AND IT IS
   SCOPED SO IT STAYS ONE SCREEN'S DECISION. RV19 lists "form `.field`/
   `.field-row` labels and inputs" among the things left alone on purpose
   ("labels and facts-in-a-list read worse centred"), and RV146 REFUSED to
   widen `.field > p.muted` with a count — 35 field-scoped help paragraphs
   across NINE screens. Both still hold everywhere else. This is the same shape
   of exception as event.php's Style help: Alex named these four things and
   asked for them centred.

   🛑 KEYED ON `.player-find`, A CLASS, AND DELIBERATELY NOT ON `#find`. The
   panel carries that id, but keying a rule on a section-slug id is exactly
   RV107's trap — `portalSectionStart()` emits slugs as ids, so another screen
   can grow an `id="find"` and silently inherit this. The class exists only
   here and says what the rule is about.

   ⚠️ NOTHING HERE TOUCHES AN `<input>`. `text-align` on an input is forbidden
   (portal_centring.md records one made and removed inside a session) — the
   digits would read from the middle. The ZIP box and the radius box keep their
   own alignment; only the words around them move.

   ⚠️ The radius row is a FLEX row, so it centres with `justify-content`, not
   with `text-align` — text-align cannot reach a flex item. That is the same
   distinction `.button-row` already draws.

   ⚠️ WGT_18302 (2026-10-02) — THE LABEL AND HINT RULE THAT STOOD HERE IS GONE
   (`.player-find .field > label, .player-find .field > p.muted`): Alex made it
   the base rule for every portal form, so the "reversal" above is now simply
   the rule, and "both still hold everywhere else" is history. The flex row
   below is still this panel's own. */

.player-find .field-inline {
    justify-content: center;
}

/* 👤 Alex: the ZIP box "more like 6 characters wide instead of full width".
   The base rule makes every text input `display: block; width: 100%`, which on a
   40rem column is a 600px box holding five digits.

   👤 `text-align: center` ON THE INPUT IS DELIBERATE, AND IT REVERSES THIS
   FILE'S ONE NAMED PROHIBITION. Alex, 2026-08-27, asked for it after being shown
   the ban's actual reasoning and the alternative. 🛑 DO NOT "TIDY" IT AWAY BY
   CITING portal_centring.md — read its PL2b section first; the reversal is
   recorded there with the trade-off spelled out.

   ⚠️ THE BAN'S REASON IS REAL AND STILL APPLIES: a centred value re-centres
   character by character while you type (`event.php`'s radius box went 1 -> 15
   -> 150 and slid the digits under the cursor). A ZIP does the same, 0 -> 02 ->
   027 -> 0270 -> 02703. What makes it acceptable HERE and not there is that a
   ZIP is a FIXED five characters typed once, not a value nudged up and down —
   so the shifting stops after five keystrokes and never recurs. Alex was offered
   `:not(:focus)` centring, which removes the shifting entirely, and chose plain
   centring anyway.

   🛑 `margin-inline: auto` WAS HERE AND HAD TO GO WHEN PL5 PUT THE ZIP AND THE
   RADIUS ON ONE LINE. It centred the box correctly while it was a block on its
   own row, but AUTO MARGINS ON A FLEX ITEM ABSORB THE FREE SPACE — so inside
   `.field-inline` it ate everything `justify-content: center` had to
   distribute and shoved the ZIP box and the radius group to opposite ends of
   the row. Measured before removing it: row content offset 37px from centre.
   ⚠️ The row centres the box now, which is the correct owner of that job (the
   same rule this file states for `.button-row`): the ROW gets
   `justify-content`, the control gets nothing but its width.

   ⚠️ 7rem holds a five-digit ZIP comfortably. IT DOES NOT HOLD ZIP+4, and that
   is a KNOWN, ACCEPTED limit rather than an oversight: `maxlength` is 10 and
   both `playerNormaliseZip()` and `findResolveZipOrigin()` accept
   `/^(\d{5})(?:-\d{4})?$/`, so "02703-1234" is legal input and MEASURED as
   clipped (it scrolls inside the box) at this width. Nothing breaks — the box
   scrolls like any input, the value submits whole, and both functions keep only
   `$match[1]`, the five digits. Alex asked for roughly six characters; widening
   to ~10 to make the rarer form fully visible would undo that.

   🛑 KEYED ON THE INPUT'S OWN ID, and that is the `event.php` Style-field
   precedent rather than the `#activities` trap: `findZip` is an INPUT id, unique
   in the app (grepped: one occurrence), not a `portalSectionStart()` section
   slug that another screen could emit. ⚠️ Rule and anchor go together — renaming
   the input leaves this matching nothing. */
.player-find #findZip {
    width: 7rem;
    text-align: center;
}

/* WGT_19360 — 👤 Alex, 2026-10-07, walking the settings pages (WGT_19313):
   tab 3, Front Page Settings, "just center the text in the email and miles
   inputs"; tab 10, Host settings, "center the input on the miles of the venue
   under picking a host. Same for chase and unfinished checklist for three
   mornings … and same for all the inputs under the substitute requests tab."

   🛑 A NAMED REVERSAL, LIKE #findZip ABOVE, AND IT DOES NOT SPREAD. These boxes
   sit in `.setting-input`, a SHARED-LINE row (box + unit), which WGT_18302's
   base rule leaves alone on purpose: the row centres with `justify-content`
   (RV117) and the typed text keeps its start edge. ⚠️ The jitter the ban names
   (1 -> 15 -> 150 sliding under the cursor) is real on the four number boxes
   and was accepted, as for #findRadius. Every other settings input — Coverage
   warnings, The night bonus, Host expenses, the clock boxes — stays `start`;
   portal_centring.md's PL2b section lists these eight.

   KEYED ON EACH INPUT'S OWN ID: `settingFieldId()` makes `set-<group>-<key>`,
   one per setting, so none can appear on another screen. ⚠️ A renamed
   setting key leaves its line matching nothing. Checkboxes in the
   Substitute requests panel are not in the list — a tick has no typed text. */
#set-contact-notifyAddress,
#set-players-defaultRadiusMiles,
#set-assignment-zipCloseMiles,
#set-messaging-checklistReminderDays,
#set-callout-ringPercent,
#set-callout-emergencyMinutes,
#set-callout-notifyMobile,
#set-callout-unfilledLeadDays,
#set-callout-notifyWithinDays {   /* WGT_20952 — 👤 Alex, 2026-10-13, walk tab 1 */
    text-align: center;
}

/* PL6 (2026-08-27) — Alex, on the search form: "move the 'What are you looking
   for?' text down but have no space between it and 'leave both unticked...' so
   it looks like a separate area from the zip code / radius", "un-bold 'within'",
   and "center the text in the miles input and make it 3 characters wide".

   THE GROUPING IS DONE WITH ONE GAP MOVED, not with a rule or a box: the space
   that was sitting BETWEEN the heading and its own hint is taken out and put
   ABOVE the heading instead, so the two lines read as one block and the block
   reads as separate from the row above. `p` keeps the browser's 1em top margin
   (`.muted` sets only colour and size), which is the gap being removed. */
.player-find .field + .field {
    margin-top: 1.75rem;
}

.player-find .field > label + p.muted {
    margin-top: 0;
}

/* "within" is CONNECTIVE TEXT inside a control row, not a field heading — the
   base `label` rule makes every label 600, which made it read as a second
   heading competing with "ZIP code" above it. ⚠️ Scoped to a label that is a
   direct child of `.field-inline`; a label introducing a field still bolds. */
.player-find .field-inline > label {
    font-weight: 400;
}

/* 👤 "center the text in the miles input and make it 3 characters wide instead."
   🛑 SECOND REVERSAL OF THE SAME PROHIBITION, AND THIS ONE IS THE EXACT CONTROL
   IT WAS WRITTEN ABOUT. portal_centring.md bans `text-align` on an input because
   a value re-centres character by character while you type, and the example it
   gives is a RADIUS BOX: 1 -> 15 -> 150 shifting the digits under the cursor.
   That is this box. The ZIP reversal could argue a fixed five characters; this
   one cannot — the jitter is real and will be visible when the number is
   retyped. Alex asked for it knowing the ZIP case; recorded in
   portal_centring.md rather than applied quietly. ⚠️ Do not "restore" either
   one by citing the ban — read that file's PL2b and PL6 sections first. */
/* PL6 — each search result gets its own card, so a list of ten venues reads as
   ten things rather than one wall of text (Alex, 2026-08-27).

   ⚠️ Same visual tokens as `.activity-card` — `--wg-paper` on `--wg-line` at a
   10px radius — but deliberately NOT that class: its own comment says it
   "appears in no other Code/portal/*.php", and that is precisely what lets
   activities.php centre text inside it without reaching another screen. Sharing
   the look is fine; sharing the class would falsify that invariant.

   The list loses its markers because each row is now a box: a bullet hanging
   outside a bordered card reads as a stray dot. */
/* 👤 "move the search results a bit below the 'search' button so they're not
   immediately touching." The button row sets no bottom margin, so the first tab
   sat flush against it. ⚠️ On the tab strip, not on `.find-results` — the
   results are nested two levels down now and the gap belongs between the FORM
   and the thing under it. */
.player-find .find-days {
    margin-top: 1.5rem;
    text-align: left;
}

.player-find .find-day + .find-day {
    margin-top: 1rem;
}

.player-find .find-day-title {
    margin: 0 0 .4rem;
    /* RV182 — smaller than the sitewide h3 base rule (1.05rem) on purpose:
       this heading is deliberately quieter than a normal subheading. */
    font-size: .95rem;
    color: var(--wg-muted);
}

.player-find .find-results {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Each slot is ONE row — name, time, address — in its own little box. It WRAPS
   rather than truncating: the ask was to keep the full name and the full
   address, so on a narrow screen the address drops to a second line instead of
   being cut. */
/* 🛑 A GRID, NOT A FLEX ROW, AND THAT IS THE WHOLE ASK. "Like a normal table
   where the activity type takes up the same amount of space on each row" is a
   shared column track: every row's second column starts at the same x whatever
   its label says. Flex sizes each row to its own content, which is exactly the
   ragged indent Alex was describing.
   ⚠️ 8.5rem fits the longest label in the data — "General event" and
   "Bingo/Karaoke", both 13 characters, measured 2026-08-27 across every public
   weekly slot. A longer one wraps inside its own cell rather than pushing the
   second column, which is the property a fixed track buys. */
.player-find .find-result {
    display: grid;
    grid-template-columns: 8.5rem 1fr;
    gap: .1rem .8rem;
    /* 👤 "vertically center the activity name(s) in their sections."
       ⚠️ WAS `baseline`, which pinned the activity to the FIRST line of the row
       — so on any row whose venue half wrapped to two lines the label sat at the
       top against a two-line block, and a row with two stacked activities sat
       top against a one-line block. `center` centres each cell against the
       row's real height, so whichever side is taller, the shorter one centres
       on it. The venue half keeps its OWN internal baseline alignment
       (`.find-result-main`), which is what still lines the name, time and
       address up with each other. */
    align-items: center;
    margin: 0 0 .4rem;
    padding: .55rem .7rem;
    text-align: left;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

/* 👤 "If an activity has more than one, then vertically stack the activities on
   the left side instead of having them go left to right." One event can carry
   several — `eventActivitiesLabels()` — so the cell is a column, not a line. */
.player-find .find-result-kinds {
    display: flex;
    flex-direction: column;
    gap: .15rem;
}

.player-find .find-result-kind {
    font-size: .85rem;
    font-weight: 600;
    color: var(--wg-muted);
}

/* The venue half stays one line and wraps only when it must, which is what the
   fixed left column bought back in width. */
.player-find .find-result-main {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .1rem .6rem;
}

.player-find .find-result:last-child {
    margin-bottom: 0;
}

/* PL14 — the row is a SWITCH with a link beside it. The label takes the width;
   the arrow sits at the end and is a separate control.
   🛑 THE ARROW IS OUTSIDE THE `<label>` IN THE MARKUP, not merely floated here:
   a link inside a label gets activated by the label's own click handling in some
   browsers, so it would toggle the row on its way to the venue. */
.player-find .find-row {
    display: flex;
    align-items: stretch;
    gap: .35rem;
    margin: 0 0 .4rem;
}

.player-find .find-row > .find-result {
    flex: 1 1 auto;
    margin: 0;
}

.player-find .find-row-open {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    padding: 0 .7rem;
    font-size: 1.1rem;
    line-height: 1;
    /* the same de-styled-link idiom as `.entity-link` (RV16) */
    color: var(--wg-muted);
    text-decoration: none;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

.player-find .find-row-open:hover,
.player-find .find-row-open:focus-visible {
    color: var(--wg-ink);
    background: var(--wg-panel);
}

/* 🛑 GREEN / RED ON THE SEARCH ROW ITSELF, the same `:has(input:checked)` the
   digest picker used before the two lists merged — and the same reasoning:
   not-subscribed is the DEFAULT paint so a failed selector renders visibly
   wrong rather than silently unstyled. */
.player-find .find-result:has(input[type="checkbox"]) {
    color: var(--wg-error-ink);
    background: var(--wg-error-bg);
    border-color: #f5c6c2;
    cursor: pointer;
}

.player-find .find-result:has(input[type="checkbox"]:checked) {
    color: #1c4b2a;
    background: #eaf6ed;
    border-color: #b7dcc2;
}

/* ⚠️ Visually hidden, still focusable — `display: none` would drop every row out
   of the tab order, and the colour is the only other affordance. */
.player-find .find-result input[type="checkbox"] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.player-find .find-result:has(input:focus-visible) {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

/* 👤 "don't show the ugly html blue/purple links; just let the entire section
   link to the venue (in a new window) and have a nice hover effect."

   🛑 THE CARD IS THE LINK, so there is no blue text left to suppress —
   `color: inherit` on the box does the whole job. That is `.entity-link`'s and
   `a.row-action`'s idiom (RV16, and RV181 answering gate [60] "yes,
   portal-wide") applied to a box rather than a word.

   ⚠️ THE HOVER IS AN INSET SHADOW, NOT A BORDER CHANGE OR A DROP SHADOW.
   Thickening a real border reflows the row by a pixel on every hover; an inset
   shadow paints inside the box and cannot move anything — which is the reason
   RV110 already gives for using one on `.table-wrap`. A drop shadow would be a
   new idiom in this file, and the nine existing `box-shadow`s are all inset. */
.player-find .find-result-linked {
    color: inherit;
    text-decoration: none;
    transition: box-shadow .12s ease, background-color .12s ease;
}

.player-find .find-result-linked:hover {
    background: var(--wg-panel);
    box-shadow: inset 0 0 0 2px var(--wg-nav-head-bg);
}

/* ⚠️ Keyboard users get the same cue plus a real outline — the hover alone is
   not a focus indicator. Matches `.wg-section > summary:focus-visible`. */
.player-find .find-result-linked:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

.player-find .find-result-name {
    font-weight: 600;
}

.player-find .find-result-time {
    color: var(--wg-ink);
}

.player-find .find-result-address {
    font-size: .9rem;
    color: var(--wg-muted);
    /* 🛑 NO `flex-basis: 100%` and NO centring. Both were here and both were
       wrong once the rows became a table: the address sits inline and wraps only
       when it genuinely cannot fit, and it reads from the LEFT with the name and
       the time beside it (Alex: "left align the name and such of the
       business"). */
}

.player-find #findRadius {
    width: 3.75rem;
    /* ⚠️ THE PADDING HAS TO COME DOWN OR THREE CHARACTERS DO NOT FIT. The base
       input rule sets `padding: .6rem .75rem` — 24px of it horizontal — and
       Chrome draws a ~13px spinner inside the box on top of that. At 60px wide
       that left ~21px of text area for "200", which needs ~27px: MEASURED as
       clipped. Trimming the side padding buys back 13px and the widest legal
       value fits. The vertical padding is untouched, so the box still matches
       its neighbours' height and the 48px tap target. */
    padding-inline: .35rem;
    text-align: center;
}

.empty-state {
    margin: 0 0 1.5rem;
    padding: 1rem;
    color: var(--wg-muted);
    background: var(--wg-panel);
    border: 1px dashed var(--wg-line);
    border-radius: 10px;
}

.wg-alert-ok {
    color: #1c4b2a;
    background: #eaf6ed;
    border: 1px solid #b7dcc2;
}

/* --- The confirmation step (F14) -----------------------------------------
   ⚠️ Deliberately heavy. Saving time off opens real shifts, and a mistyped
   year doubles the blast radius — this must not look like a routine dialog.
   ------------------------------------------------------------------------ */

.confirm-box {
    margin: 0 0 1.5rem;
    padding: 1rem 1.1rem;
    color: var(--wg-warn-ink);
    background: var(--wg-warn-bg);
    border: 1px solid var(--wg-warn-line);
    border-left: 5px solid var(--wg-warn-line);
    border-radius: 10px;
}

.confirm-box h2 { margin: 0 0 .5rem; font-size: 1.1rem; }

.confirm-count {
    display: block;
    margin: .25rem 0 .75rem;
    font-size: 1.35rem;
    font-weight: 700;
}

.confirm-list {
    margin: 0 0 1rem;
    padding: 0 0 0 1.1rem;
    font-size: .95rem;
}

.confirm-list li { margin: 0 0 .2rem; }

/* 👤 Alex, 2026-10-14 (the WGT_21264 walk) — the bar's *Posters and logos*:
   each file centred, its name and a preview right above its Download button. */
.venue-downloads {
    margin: 0 0 1rem;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 1.5rem;
}

.venue-downloads li {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .35rem;
    text-align: center;
}

.venue-downloads strong { overflow-wrap: anywhere; }

.venue-downloads .button-row { justify-content: center; margin: .25rem 0 0; }

.venue-download-preview {
    display: block;
    max-width: 100%;
    max-height: 20rem;
    border: 1px solid var(--wg-line, #d9dde3);
    border-radius: 6px;
}

.venue-download-pdf {
    width: 16rem;
    height: 20rem;
}

/* --- Coverage banner (F7) ------------------------------------------------
   Admin only, and above the page heading on purpose: a venue with nobody
   turning up outranks whatever the admin came to this page to do.
   ------------------------------------------------------------------------ */

.gap-banner {
    margin: 0 0 1.5rem;
    padding: .85rem 1rem 1rem;
    color: var(--wg-warn-ink);
    background: var(--wg-warn-bg);
    border: 1px solid var(--wg-warn-line);
    border-left: 5px solid var(--wg-warn-line);
    border-radius: 10px;
}

.gap-banner-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .5rem 1rem;
    flex-wrap: wrap;
}

.gap-banner-title {
    margin: 0;
    font-size: 1.05rem;
}

.gap-list {
    margin: .75rem 0 0;
    padding: 0;
    list-style: none;
}

.gap-item {
    display: flex;
    align-items: flex-start;
    gap: .7rem;
    padding: .6rem 0;
    border-top: 1px solid var(--wg-warn-line);
}

.gap-count {
    flex: 0 0 auto;
    min-width: 2.1rem;
    padding: .15rem .35rem;
    font-size: .95rem;
    font-weight: 700;
    text-align: center;
    color: var(--wg-accent-ink);
    background: var(--wg-warn-ink);
    border-radius: 6px;
}

.gap-text   { flex: 1 1 auto; font-size: .95rem; }
.gap-detail { color: inherit; opacity: .85; }

/* W1 — every gap links to the screen that fixes it; the banner no longer
   dismisses, so there is nothing else in this section. */
.gap-link {
    display: inline-block;
    margin-top: .25rem;
    color: inherit;
    font-weight: 600;
    text-decoration: underline;
}

/* A check that cannot run yet. Visually distinct from a counted gap so it
   never reads as either a problem solved or a problem found. */
.gap-unknown             { border-top-color: var(--wg-info-line); }
.gap-unknown .gap-count  { background: var(--wg-info-ink); }
.gap-unknown .gap-text   { color: var(--wg-info-ink); }

/* ⚠️ W1 removed the coverage banner's OWN use of this (it no longer
   dismisses), but `mailAlertBanner()` (includes/mail.php) still renders it
   for its "log could not be read" state — do not remove. */
.gap-strip {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem 1rem;
    flex-wrap: wrap;
    margin: 0 0 1.25rem;
    padding: .4rem .75rem;
    font-size: .9rem;
    color: var(--wg-warn-ink);
    background: var(--wg-warn-bg);
    border: 1px solid var(--wg-warn-line);
    border-radius: 8px;
}

/* --- Venue cover lists (F12) ---------------------------------------------
   ⚠️ The tags are NOT the spreadsheet's colours. Green and yellow are how the
   data arrived, not what it means, and a green "priority 1" pill would invite
   Graham to reason about colours again when the whole point of F12 was to turn
   them into numbers.

   SB14 — 3 and 4 used to fold into one undifferentiated `pri-n` tag on the
   cover screens (event.php/night.php), so a level-3 host and a level-4 host
   rendered identically. Fixed by fill pattern, not a new hue: all four stay
   inside the same blue "info" family (never the red/amber already claimed by
   pri-none and .venue-flag elsewhere on this file) and step from filled, to
   faded, to outlined, to solid-dark as the level rises. */

.pri-tag {
    display: inline-block;
    padding: .1rem .5rem;
    font-size: .8rem;
    font-weight: 700;
    letter-spacing: .01em;
    white-space: nowrap;
    color: var(--wg-info-ink);
    background: var(--wg-info-bg);
    border: 1px solid var(--wg-info-line);
    border-radius: 999px;
}

.pri-2 { opacity: .85; }
.pri-3 {
    background: transparent;
    border-width: 2px;
}
.pri-4 {
    color: var(--wg-accent-ink);
    background: var(--wg-nav-head-bg);
    border-color: var(--wg-nav-head-bg);
}
.pri-n { color: var(--wg-muted); background: var(--wg-panel); border-color: var(--wg-line); }

/* A venue nobody can cover. Loud on purpose — it is the thing this screen
   exists to surface, and it is invisible until the night it matters. */
.pri-none {
    color: var(--wg-error-ink);
    background: var(--wg-error-bg);
    border-color: #f0b3ad;
}

/* F15's venue list: the coverage flags. Red is a night that fails without a
   human fixing something; amber is a night one dropout from trouble. Loud on
   purpose, and there is no way to dismiss one — the failure mode is a venue
   with no host on the night (SC12). */
.venue-flag {
    display: inline-block;
    padding: .1rem .5rem;
    font-size: .8rem;
    font-weight: 700;
    letter-spacing: .01em;
    white-space: nowrap;
    color: var(--wg-error-ink);
    background: var(--wg-error-bg);
    border: 1px solid #f0b3ad;
    border-radius: 999px;
}

.venue-flag-warn {
    color: var(--wg-warn-ink);
    background: var(--wg-warn-bg);
    border-color: var(--wg-warn-line);
}

/* Q3's "Ready". ⚠️ The base pill is RED because every flag before this one was
   a problem — a venue with no host, a night with no set. This is the first pill
   that reports something being FINE, so it needs the third colour rather than
   the absence of one: the same greens .wg-alert-ok uses, so a state does not
   read as two different kinds of good on one screen. */
.venue-flag-ok {
    color: #1c4b2a;
    background: #eaf6ed;
    border-color: #b7dcc2;
}

/* SB26 -- gate [51]'s second half. Same placement and shape as the `.muted`
   "(inactive -- will not be called)" note beside a listed host's name, but
   red: this host is NOT gone from the sub list, just not notifiable because
   `Can sub` is off. Text, not a pill -- .venue-flag/.pri-none are both loud
   badges meant to draw the eye across a whole row or table; this sits inline
   next to one name among several, so it borrows the .muted note's own shape
   and only swaps the ink. */
.not-notifiable {
    color: var(--wg-error-ink);
    font-size: .95rem;
    font-weight: 600;
}

/* RV16 — a real `<a>` styled to read as plain text until interacted with,
   not the browser's default blue underline (the "ugly HTML blue underline
   thing" Alex asked to avoid on a clickable name). `venues.php` uses this
   for the venue name and each night; `RV17` matches it exactly for
   `albums.php`'s event name so the portal does not grow two different
   clickable-name styles.

   🛑 WGT_15421 — WHAT THIS CLASS IS FOR, AND WHAT IT IS NOT FOR. It is for the
   NAME INSIDE A CLICKABLE TABLE ROW (a `tr.row-link`, handler row-link.js): the
   row is the tap target, so the name needs no colour cue and gets none. It is
   NOT for a link in prose — a link in a sentence is left alone and takes the
   base `:where(a)` blue above (--wg-link, #1552b8). Putting this class on a
   link in a paragraph makes it invisible until hover, which is exactly what
   Alex rejected on 2026-09-06. The whole rule:
   ../../../CLAUDE/specs/design_guide.md → "The link rule". */
.entity-link {
    color: inherit;
    text-decoration: none;
}

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

/* RV16 — venues.php's Logo column: the first real image that screen has
   ever loaded (VA3, always through venue-asset.php, never `logoPath`
   directly). Small on purpose — a 61-row page cannot afford a portrait. */
.venue-logo-thumb {
    display: block;
    width: 2rem;
    height: 2rem;
    object-fit: cover;
    border-radius: 4px;
    border: 1px solid var(--wg-line);
}

/* The per-row forms sit side by side rather than stacking, so a long list
   stays scannable. */
.data-table select {
    width: auto;
    min-width: 4rem;
    margin: 0 .25rem 0 0;
}

/* --- Settings (F16) ------------------------------------------------------ */

/* One setting is a label, its control, its explanation and its provenance —
   stacked, because the explanation is the part that makes the number mean
   anything and it must not be a tooltip. */
.setting {
    padding: 0 0 1rem;
    margin: 0 0 1rem;
    border-bottom: 1px solid var(--wg-line);
}

.setting:last-of-type { border-bottom: 0; }

.setting-label {
    display: block;
    margin: 0 0 .35rem;
    font-weight: 600;
    font-size: .95rem;
}

/* The unit sits beside the box rather than in the label: "20" means nothing
   without "% of the time still left" next to it while you are typing. */
.setting-input {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .6rem;
}

.setting-input input {
    width: auto;
    min-width: 7rem;
    margin: 0;
}

/* WGT_13723 — the reset above is `.setting-input input` and a <select> is not an
   <input>, so the picker fell through to the global `select { width: 100% }` at
   the top of this file and spanned the whole 1216px column. WGT_13714 held these
   four declarations in a `style` attribute on the element because portal.css was
   not its file; this rule is that attribute, moved.
   🛑 SPECIFICITY IS THE WHOLE RISK. An inline style beats every selector, so a
   class rule replacing one has to out-rank what it fell through to: `.setting-input
   select` is 0,1,1 against the bare `select`'s 0,0,1, and it also sits later in the
   file, so it wins on either count. Measured after the move: the picker is 278px at
   1440 / 1024 / 390, right edge 971 / 763 / 334 — WGT_13714's figures exactly. */
.setting-input select {
    width: auto;
    min-width: 12rem;
    max-width: 100%;
    margin: 0;
}

/* WGT_13723 — `width: auto` above leaves a text input at the browser's ~20-column
   default (203px border-box, measured), and an email address is longer than that:
   `set-contact-notifyAddress` and `set-scheduler-alertEmail` both spilled 10px of
   text out of the box at 1440, 1024 AND 390. Widening is the fix rather than
   `text-overflow: ellipsis`, because this is a field Graham EDITS — hiding the tail
   of an address he is checking is worse than the spill was.
   ⚠️ Scoped to `[type="email"]` on purpose: the number and time boxes beside it are
   two and five characters wide and must stay compact. `max-width: 100%` is what
   keeps 18rem (288px) inside the 357px row at 390. */
.setting-input input[type="email"] {
    min-width: 18rem;
    max-width: 100%;
}

/* WGT_24566 — a LIST of addresses (`'multiple'` email setting, Activity
   Settings). It holds several addresses, so it is wide: at the default width
   only the first address showed, cut mid-word. 0,2,1 out-ranks
   `.setting-input input` above. */
.setting-input input.setting-email-list {
    flex: 0 1 28rem;
    width: 28rem;
    max-width: 100%;
    /* WGT_24574 (👤 Alex, 2026-11-02): "center the text and the input". The
       row already centres the box (`justify-content: center` on a settings
       page); a capped width lets that show, and the typed text centres too. */
    text-align: center;
}

/* WGT_24575 — ONE address on Activity Settings (`finalQuestion.alertTo`). It is
   `type="email"`, so `.setting-input input[type="email"]` gives it the 18rem
   width; this centres the typed text to match the list box above it. */
.setting-input input.setting-email { text-align: center; }

.setting-unit { color: var(--wg-muted); font-size: .95rem; }

/* WGT_24573 — the Host guide link box at the bottom of Add a host: the address
   reads centred, like the rest of the page (Alex wants portal text centred). */
.host-guide-share input { text-align: center; }

/* RV88 (2026-08-14) — `margin-inline: auto` is RV78's capped-box trap again,
   and this class is where it bites twice. On settings.php the 46rem cap sits
   inside a 1216px column (`.data-table--wide` widens it), so measured at 1440
   these five paragraphs hung 191px LEFT of centre while the uncapped ones
   beside them read centred-ish — the same screen disagreeing with itself.
   On host-settings.php the identical cap is inside a 640px column where it
   does NOT bite (measured 0px), so the fix is latent there rather than visible;
   one rule covers both and the second cannot drift out of step with the first.
   ⚠️ The 46rem cap is untouched — only where the box sits changes — and the
   TEXT inside it is NOT centred: this is form field help, and RV19's exclusion
   of form labels and inputs stands sitewide. RV78 centred question-set.php's
   equivalents only "within this page's scope", and this is the page scale it
   was told not to go to. `margin: .5rem 0 0` had to become the longhand below
   or the shorthand would keep resetting the inline margins to 0. */
.setting-help {
    margin-block: .5rem 0;
    margin-inline: auto;
    font-size: .9rem;
    color: var(--wg-muted);
    max-width: 46rem;
}

.setting-meta { margin: .35rem 0 0; font-size: .85rem; }

/* A checkbox reads as one line, and the whole line is the tap target. */
.setting-check {
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    margin: 0;
    font-weight: 600;
    cursor: pointer;
}

.setting-check input[type="checkbox"] {
    width: 1.25rem;
    height: 1.25rem;
    margin: .1rem 0 0;
    flex: 0 0 auto;
}

.setting-check .setting-label { margin: 0; }

/* WGT_20952 — 👤 Alex, 2026-10-13, walking Host settings: "several of those are
   very close together … [they] should be in one section that's noticeably
   together by itself". Settings that belong together (`cluster` in the
   registry) sit in a white box inside the grey panel, titled by its legend.
   The mid-tone border is RV103's L2 step: one level in from the panel. */
.setting-cluster {
    margin: 0 0 1rem;
    padding: .5rem 1rem 0;
    border: 1px solid var(--wg-line-mid);
    border-radius: 8px;
    background: var(--wg-paper);
}

.setting-cluster > legend {
    margin-inline: auto;
    padding: 0 .6rem;
    font-weight: 700;
    font-size: 1rem;
}

.setting-cluster .setting:last-child { border-bottom: 0; }
/* The box's own border already separates it; the divider above it doubled up. */
.setting:has(+ .setting-cluster) { border-bottom: 0; }

/* Resets are deliberately quiet and separated from Save — a stray click here
   discards a value rather than storing one. */
.setting-resets {
    margin: 1rem 0 0;
    padding: .75rem 0 0;
    border-top: 1px dashed var(--wg-line);
}

.setting-resets form { display: inline; margin: 0; }

/* --- The weekly digest venue picker (AC19) ------------------------------- */

/* PL11 (2026-08-27) — the digest picker is one row per event, coloured by
   whether it is subscribed, grouped by night.

   👤 Alex: "the list under weekly digest is a huge amount of wasted space. Let's
   give one row per event with a green or red light background; green is
   subscribed, red is not."

   🛑 THE COLOUR IS `:has(input:checked)`, NOT JAVASCRIPT — the same technique
   `.filter-chip--check` already uses. The row is a real <label> around a real
   checkbox, so it toggles and repaints with scripting off. The two bulk buttons
   are the only scripted part, and they are a convenience.

   ⚠️ GREEN AND RED ARE THE ALERT FAMILY'S OWN PAIR, not new colours: the red is
   `--wg-error-bg`/`--wg-error-ink` (tokens), and the green is the literal trio
   `.wg-alert-ok` already carries. ⚠️ Those greens are NOT tokenised in this file
   — if they ever become tokens, this block and `.wg-alert-ok` move together. */
.digest-day + .digest-day { margin-top: 1.1rem; }

/* 🛑 OVERRIDES `.setting-check`'s flex-start/gap, which this row still carries
   so the bulk-mark script's selector keeps matching. Left aligned and compact —
   the point of the row is that it is one line. */
.digest-slot {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .15rem .55rem;
    margin: 0 0 .3rem;
    padding: .45rem .6rem;
    font-weight: 400;
    text-align: left;
    border: 1px solid;
    border-radius: 8px;
    /* NOT subscribed is the default paint; the :has() rule below is the
       exception. Stated this way round so an unchecked row can never render
       unstyled if the selector ever fails. */
    color: var(--wg-error-ink);
    background: var(--wg-error-bg);
    border-color: #f5c6c2;
    cursor: pointer;
}

.digest-slot:last-child { margin-bottom: 0; }

.digest-slot:has(input:checked) {
    color: #1c4b2a;
    background: #eaf6ed;
    border-color: #b7dcc2;
}

/* ⚠️ VISUALLY HIDDEN, STILL FOCUSABLE. `display: none` would take the row out
   of the tab order and strand a keyboard user, since the colour is the only
   other affordance. The row shows focus via `:has(input:focus-visible)`. */
.digest-slot input[type="checkbox"] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.digest-slot:has(input:focus-visible) {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

.digest-slot-venue { font-weight: 600; }

.digest-slot-meta { font-size: .9rem; opacity: .85; }

/* 👤 "move the 'save' button down 10 pixels at the bottom there." The last row
   and the button row had nothing between them. ⚠️ Keyed on `#digestPicks`, which
   is an ELEMENT id used by the bulk-mark script — not a `portalSectionStart()`
   section slug, so RV107's trap does not apply here. */
#digestPicks + .button-row {
    margin-top: .625rem;
}

/* PL12 — the auto-save status line. ⚠️ It sits BETWEEN the rows and the (now
   script-hidden) Save button, so the 10px above still separates the last row
   from whatever follows it. */
.digest-status {
    margin: .625rem 0 0;
    text-align: center;
}

.digest-status-error {
    color: var(--wg-error-ink);
    font-weight: 600;
}

.digest-venue-group { margin-top: 1.25rem; }

.digest-venue-head { margin-bottom: .35rem; }

/* --- The escalation preview (A8) ----------------------------------------- */

/* TB1: `.preview` moved from its own `<section class="panel">` to a `<div>`
   nested inside the messaging group's own section (settings.php) — a tab for
   it would otherwise jump into the middle of that panel. This is what marks
   it as a sub-part of that panel rather than a plain continuation of it. */
.preview {
    margin-top: 1.25rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--wg-line);
}

.preview h3 {
    /* RV182 — font-size dropped: matches the sitewide h3 base rule exactly. */
    margin: 0 0 .5rem;
}

/* The line where the priority order stops applying. It moves as the threshold
   changes, so it is drawn as a break in the table rather than as a footnote —
   the whole point is to see WHERE it falls, not to be told that it exists. */
.preview-divider td {
    padding: .5rem .6rem;
    font-size: .85rem;
    color: var(--wg-warn-ink);
    background: var(--wg-warn-bg);
    border-top: 2px solid var(--wg-warn-line);
    border-bottom: 1px solid var(--wg-warn-line);
    white-space: normal;
}

/* Rows below it are a different kind of night, not a worse-looking one. */
.data-table tr.is-emergency td { background: var(--wg-warn-bg); }

.preview .data-table th .muted { font-weight: 400; }

/* --- Tonight board — RETIRED with LK9's expand panel (RV114, 2026-08-15) ---
   `.tonight-row`, `.tonight-toggle`, `.tonight-panel` and `.tonight-options`
   stood here: LK9 made the row toggle a sibling <tr> of links beneath it
   ("instead of taking it to a whole nother page let's have it drop down a div
   below it"). Alex reversed that on the RV walk — the four links all went to
   night.php, so the row goes there directly. tonight.php now uses the SHARED
   `.data-table tr.row-link` above, whose hover is `--wg-panel`, identical to
   `.tonight-row`'s, so the table's appearance is unchanged.

   🛑 GREPPED TO ZERO BEFORE DELETING — tonight.php held the only markup using
   any of the four, and `.equipment-*` below is a separate copy of the same
   shape, not a user of these names. The lost `:focus-visible` outline needed
   no replacement: the row is no longer `tabindex="0"`, and what the keyboard
   reaches now is the venue name's real `.entity-link` anchor.
   UNDO: `git show` this file before RV114. */

/* 🛑 RV110 — the table had NO frame of its own, so it read as loose lines floating on the page
   ("the table just seems to run together"), while the row-panel below it — bordered cards, a
   bold title, a solid-red button — read heavier than the table that owns it ("more bolded and
   colored than the main table"). Read as ONE hierarchy problem, not two, per the task brief
   ("fix the relationship, not each element"): this table is now the OUTERMOST box the panel
   nests inside, so it takes RV103's three-tone scale (portal_sections.md → Nested panels
   lighten INWARD) at its darkest tier — REUSED, not a new token. The panel's own action-grid
   takes the scale's middle tier, one level in; `.equipment-action`'s card border was already
   the scale's lightest tier and needs no change.
   Scoped to THIS table only — `.table-wrap` and `.data-table th` are shared by every table
   site-wide (RV65: nothing goes global) — `:has(#equipmentTable)` and the ID selector below
   never reach another screen. No `border-radius` on the frame: the table sits flush inside
   `.table-wrap` with no padding of its own, so a rounded corner would clip the TABLE's square
   corners, not the frame's.
   🛑 `box-shadow: inset`, NOT `border` — MEASURED, NOT ASSUMED. `.table-wrap` is `width:
   fit-content`, sized off the TABLE's own intrinsic content width (RV5), so a real `border`
   joins that sizing loop: it measured 0px page overflow at 1024 before this rule and 2px after,
   the frame's own 2px pushing past what the wrap previously fit exactly. `box-shadow` paints
   the identical 1px line without adding to the box model, so it cannot re-open that overflow. */
.table-wrap:has(#equipmentTable) {
    box-shadow: inset 0 0 0 1px var(--wg-muted);
}

#equipmentTable thead th {
    border-bottom-color: var(--wg-muted);
}

/* The row-panel's action-grid becomes the scale's MIDDLE tier — one defined box nested inside
   the table's new frame, rather than `.equipment-action` cards floating loose in the tinted
   <td>. Scoped to `.equipment-panel .equipment-actions` only: the ITEM DETAIL page reuses
   `.equipment-actions` inside its own `<section class="panel">` (RV81, a different nesting)
   and is untouched by this selector. */
.equipment-panel .equipment-actions {
    border: 1px solid var(--wg-line-mid);
    border-radius: 8px;
    padding: 1rem;
}

/* --- Equipment list (LK8) — same interaction as the tonight board -------
   Alex picked the inline panel over a whole-row link to the item page: "it
   would have all the options of what you can do with it on that page
   directly, which I think I like even better." The item name goes from an
   <a> to plain text; clicking anywhere on the row toggles a `.equipment-panel`
   <tr> beneath it holding every action the item page offers, laid out flat —
   never a nested accordion. The class names shadowed `.tonight-*`, whose
   markup and JS shape (LK9) this reused rather than re-rolled.
   🛑 RV114 DELETED `.tonight-*` and this block is now the ONLY copy of that
   shape — do not read the deletion as a signal to follow. This row expands a
   panel in place; tonight.php's navigated, which is why only that one moved to
   the shared `.data-table tr.row-link`. */

.equipment-row { cursor: pointer; }

.equipment-row:hover td { background: var(--wg-panel); }

/* RV149 — Alex: hovering a row erased the table frame's left/right line for
   that row's band ("the black border goes away on the left and right...
   keep that left and right black border even though it does change color").
   `:hover td`'s background is a LATER paint layer than `.table-wrap`'s own
   `box-shadow: inset` frame (RV110), so it painted straight over the 1px
   line at the table's flush left/right edge. An inset box-shadow on the
   hovered end cells sits above their OWN background (same box, later paint
   step), so it redraws the line without touching the frame rule itself —
   same token, `--wg-muted`, per RV110's "do not disturb the scale". */
.equipment-row:hover td:first-child { box-shadow: inset 1px 0 0 var(--wg-muted); }
.equipment-row:hover td:last-child { box-shadow: inset -1px 0 0 var(--wg-muted); }

.equipment-row:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

.equipment-toggle {
    display: inline-block;
    width: .6em;
    margin-right: .35rem;
    color: var(--wg-muted);
    transition: transform .15s ease;
}

.equipment-toggle::before { content: '▸'; }

.equipment-row[aria-expanded="true"] .equipment-toggle {
    transform: rotate(90deg);
}

.equipment-panel td {
    padding: 1rem 1.25rem;
    background: var(--wg-panel);
    white-space: normal;
}

/* 🛑 RV108, 2026-08-15 — GRID, NOT FLEX, AND THE SWAP IS THE WHOLE POINT OF THE TASK.
   Alex: missing and retire "take an entire row when the others get a third of a column… then all
   four fit in a single row". Under `flex: 1 1 33.333%` a fourth action could not do that — its
   hypothetical size is a third of the row, so it wraps, and `flex-grow` then stretches the lone
   wrapped item to the FULL width. That stretch IS the complaint, and it would have survived the
   merge unchanged: an item that is out still has four actions after it.

   `repeat(auto-fit, minmax(15rem, 1fr))` fixes both halves at once:
     · the column count follows the room — measured on the real page, 4 at 1440, 3 at 1152, 2 at
       1024 (the panel's inner width is 1112 / 824 / 728 and a column floor is 15rem + a 1.5rem
       gap = 264px);
     · a wrapped action takes ONE COLUMN, never the whole row, so the shape Alex objected to
       cannot come back at a narrower width;
     · `auto-fit` COLLAPSES the empty tracks, so a container holding one action still renders it
       full width — which is what keeps the ITEM DETAIL page (640px column, one action) unmoved.
   ⚠️ `align-items: start` is deliberate and carried over from the flex rule: `stretch` would give
   every box in a row the tallest box's height. That is arguably tidier and it is RV110's call to
   make, not this task's. */
.equipment-actions {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1.5rem;
    align-items: start;
}

/* 🛑 AND `auto-fit` DOES NOT COLLAPSE THE EMPTY TRACKS IN THE ROW PANEL — MEASURED, NOT ASSUMED.
   The promise of auto-fit is that a container holding one item collapses the rest and the item
   fills; it holds on the item detail page, and it FAILS here, because `.equipment-full-link`
   spans `1 / -1` and so no track is ever empty. The first pass shipped without this rule and the
   two single-action branches — *It turned up* (status `lost`) and *Put it back in service*
   (status `retired`) — rendered 260px inside a 1112px panel instead of the 1112px they had under
   flex. Neither branch is anything RV108 was asked to touch. */
.equipment-actions > .equipment-action:only-of-type { grid-column: 1 / -1; }

/* RV150 — same collapse bug, a second shape: three actions (the no-holder branch — Issue it to a
   host / Record its condition / Report an issue) at a container wide enough for auto-fit to seat
   FOUR tracks (measured: the row panel's inner width crosses 1032px at viewport 1394px exactly,
   which 1440 clears) leave the 4th phantom track empty on the right — Alex: "it kind of squishes
   it all on the left." Below 1394px the natural capacity is already ≤3, so auto-fit already fills
   or wraps correctly and this rule does not fire (`:has()` scopes it to exactly-3-action
   containers only, never the 4-action or 1-action panels). */
@media (min-width: 87.125rem) {
    .equipment-panel .equipment-actions:has(> .equipment-action:nth-child(3)):not(:has(> .equipment-action:nth-child(4))) {
        grid-template-columns: repeat(3, 1fr);
    }
}

/* Each action is its own small form; several sit side by side on a desktop
   screen, so the panel reads left to right rather than as one tall column.
   Alex, 2026-08-13: "give each input and such its own line so it's more left
   to right instead of trying to do a two column thing."

   🛑 THE OVERLAP IN THAT SCREENSHOT WAS NOT THE FLEX LAYOUT — it was
   `.data-table select { width: auto }` above winning over the base
   `select { width: 100% }`. This panel is a <tr> inside `table.data-table`, so
   every select in it fell back to its INTRINSIC content width: measured on the
   real authenticated page, the condition select rendered 346px inside a 154px
   field and spilled 192px into the neighbouring form. ⚠️ Restacking the fields
   alone would NOT have fixed it — the width reset below is the half that does,
   and removing it puts the overlap straight back. */
/* ⚠️ RV108 — the flex basis and `min-width: 15rem` moved UP into the grid's `minmax()`, which is
   now the single place the column floor is written. `min-width: 0` stays because a grid item's
   default `min-width: auto` refuses to shrink below its content, and this panel holds the 45-char
   serial and the long free-text notes that W37 had to wrap. */
.equipment-action {
    min-width: 0;
    box-sizing: border-box;
    border: 1px solid var(--wg-line);
    border-radius: 8px;
    padding: .85rem 1rem;
}

/* 🛑 RV64's `.equipment-action-group` WAS RETIRED BY RV108, 2026-08-15, and its reasoning is kept
   here because the DECISION it recorded outlives the rule. RV64 was Alex asking to "combine the
   missing and retire into a single column… but then give the missing and retire its own little
   border even still" — one flex item holding two bordered forms stacked. RV108 is the same man
   revising the same call with the screen in front of him: he now wants ONE FORM, not one column,
   so there is no longer a group to style. **Do not restore it** — the two forms it held are gone,
   not merely re-laid-out, and `equipment.php` has no element carrying the class.

   🛑 RV81, 2026-08-14 — THE ITEM DETAIL PAGE DOES NOT TAKE THE LIST SCREEN'S COLUMN
   GRID, AND THAT IS A DECISION, NOT AN OVERSIGHT. Divergence by accident was
   the failure to avoid, so here is the reason, measured rather than argued:

     · the LIST screen's `.portal-main` is 1216px and each .equipment-action
       renders 355px, three columns at left 276 / 655 / 1033;
     · the ITEM DETAIL page's `.portal-main` is 640px — at 1440, at 1152 AND
       at 1024, it does not move — and its `.panel` is 576px inside that.

   ⚠️ SO THE ROW-EXPAND PANEL IS ROUGHLY TWICE AS WIDE AS THE DETAIL PAGE'S
   WHOLE READING COLUMN. RV81's own brief had it the other way round ("the
   detail page has a whole page of room where the panel is squeezed inside a
   <td colspan="7">"); that premise is false and the numbers above are why.
   `min-width: 15rem` means three columns need ≥ 720px + gaps, so on the
   detail page they cannot be three columns — they would wrap two-and-one,
   which is neither look. The grid is UNAVAILABLE there, not merely unwanted.

   ✅ What the detail page takes instead: one action per <section class=
   "panel">, which is already a bordered box with its own <h2> and its own
   explanatory prose — the row panel has room for none of that. "One look for
   one operation" is satisfied by `.panel` there and by `.equipment-action`
   here. ⚠️ RV81's "single exception" — the one section holding TWO actions —
   IS NO LONGER AN EXCEPTION: RV108 merged them into one *Report an issue*
   form, so every section on that page now holds exactly one action, and it
   reached the rule the exception was written around rather than breaking it.
   Reused, never restyled. */

/* One control per line. The shared `.field-row` pairs two fields side by side,
   which is what put a label beside a select in a 154px column; inside an action
   form the row is a plain block and each `.field` takes the full width. */
.equipment-action .field-row { display: block; }
.equipment-action .field { flex-basis: auto; }

.equipment-action select { width: 100%; }
.equipment-action select,
.equipment-action input[type="text"] { margin-bottom: .5rem; }
.equipment-action .button-row { margin-top: .25rem; }
.equipment-action .button-row button { min-width: 0; }

/* WGT_18965 (👤 Alex, 2026-10-04, walking host-equipment.php): the title is the card's section
   header, not one more input label, so it is centred on a tinted band that runs edge to edge
   across the card's top. The negative margins equal `.equipment-action`'s padding above, and
   the radius matches its border. The ITEM DETAIL page's action cards (equipment.php) share the
   class and take the same header. */
.equipment-action-title {
    margin: -.85rem -1rem .75rem;
    padding: .55rem 1rem;
    border-bottom: 1px solid var(--wg-line);
    border-radius: 8px 8px 0 0;
    background: var(--wg-line);
    font-weight: 600;
    text-align: center;
}

/* ⚠️ RV108 — `flex-basis: 100%` was how this spanned the row while `.equipment-actions` was a
   flex container. It is a grid now, so the span has to be said in grid terms or the link renders
   as one more column beside the actions.

   🛑 RV109, 2026-08-15 — THIS CLASS IS THE ROW NOW, NOT THE LINK. Alex: "Just make it more of a
   button and center it." The control inside it is `<a class="add-event-button">`, the SHARED class
   RV1 added for an <a> that has to look like a <button> (a GET navigation cannot be one), and it
   brings its own `width: fit-content; margin-inline: auto` — the block-control row of
   portal_sections.md's centring table. The wrapper exists for two measured reasons:
     · the grid span has to live on something panel-wide, and a fit-content button is not;
     · the separator rule above it would otherwise be BUTTON-wide rather than panel-wide.
   ⚠️ `font-weight: 600` moved off this rule because `.add-event-button` already carries it —
   leaving it here would style the row and read as if the wrapper had a look of its own.

   🛑 THE CENTRING IS MEASURED ON THE INK, NOT THE BOX. Before this change the link was a
   full-width `display: block` box, so a box-centre check scored 0px — trivially "centred" while
   the label plainly hugged the left. The real figure was the text rectangle: 536px of ink at the
   panel's left edge, 288px left of centre. `temporary/rv109/snap.php` prints both. */
.equipment-full-link {
    grid-column: 1 / -1;
    padding-top: .5rem;
    border-top: 1px solid var(--wg-line);
}

/* `.add-event-button`'s own `margin: 0 auto 1.5rem` is sized for a standalone button under a
   heading; inside the panel that trailing margin is dead space at the bottom of the row. Same
   contextual override `.activity-card .add-event-button` already makes — the shared class is
   reused, never restyled. */
.equipment-full-link .add-event-button { margin-bottom: 0; }

/* --- Equipment item history (WT6) — the note is free prose of any length,
   so a five-column table always gave it whatever the other four columns
   had left over. Same shape as .scan-card (published.php): a structured
   top line, then the note on its own row beneath, never a column. */

.equipment-history {
    display: flex;
    flex-direction: column;
    gap: .6rem;
}

.history-card {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    padding: .85rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.history-card-top {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .35rem 1.25rem;
}

.history-card-when { font-weight: 600; }
.history-card-what { flex: 1 1 14rem; }

.history-card-holder,
.history-card-condition { white-space: nowrap; }

.history-card-note { overflow-wrap: anywhere; }

/* --- My kit (E4) — a phone, one hand, a dim loud room -------------------- */

/* Cards rather than a table, deliberately. A table on a phone is a sideways
   scroll and a mis-tap, and a mis-tap on this page reports a loss. */
.kit-card {
    margin: 0 0 .85rem;
    padding: .85rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.kit-card:last-child { margin-bottom: 0; }

.kit-name {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 700;
}

.kit-meta {
    margin: .2rem 0 .6rem;
    font-size: .95rem;
    color: var(--wg-muted);
}

/* Actions fold away: one summary line each, so a card is short enough to read
   at a glance and nothing destructive sits under a stray thumb. */
.kit-more { margin: .35rem 0 0; }

/* 🛑 SLATE, NOT RED — WGT_15512 (👤 Alex, 2026-09-22, via WGT_15635). These
   were bold `--wg-accent` words with no fill and no border: red that was
   neither *selected* nor *destructive*, against WGT_15382's rule, and nothing
   saying they could be pressed. They now wear `.cover-reveal > summary`'s pill
   — ink text, `--wg-line` border, the same chevron — on `--wg-panel`, because
   `.kit-card` is already `--wg-paper` and a paper pill on it is white on
   white. Left-aligned, not centred: they stack under the item's own lines.
   ✅ expenses.php's `.exp-more` and `.pay-more` share this one selector list
   since WGT_15704 (👤 Alex, 2026-09-22, via WGT_15703) — their cards are
   `--wg-paper` too, so the same panel fill. Change the pill HERE, never in a
   copy under `.exp-*` / `.pay-*`: three copies is how two stayed red. */
.kit-more > summary,
.exp-more > summary,
.pay-more > summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: fit-content;
    max-width: 100%;
    min-height: var(--wg-tap);
    padding: 0 .9rem;
    font-size: 1rem;
    font-weight: 600;
    color: var(--wg-ink);
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
    cursor: pointer;
    list-style: none;
}

.kit-more > summary::-webkit-details-marker,
.exp-more > summary::-webkit-details-marker,
.pay-more > summary::-webkit-details-marker { display: none; }

.kit-more > summary::before,
.exp-more > summary::before,
.pay-more > summary::before {
    content: '';
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid var(--wg-muted);
    border-bottom: 2px solid var(--wg-muted);
    transform: rotate(-45deg);
}

.kit-more[open] > summary::before,
.exp-more[open] > summary::before,
.pay-more[open] > summary::before { transform: rotate(45deg); }

.kit-more > summary:focus-visible,
.exp-more > summary:focus-visible,
.pay-more > summary:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: 2px;
    border-radius: 8px;
}

.kit-more[open] > summary,
.exp-more[open] > summary,
.pay-more[open] > summary { margin-bottom: .4rem; }

.kit-more form { margin: 0 0 .25rem; }

.kit-more label {
    display: block;
    margin: 0 0 .25rem;
    font-size: .95rem;
    font-weight: 600;
}

/* Full-width targets inside a card: the whole row is the button, which is what
   works one-handed. Never below 16px text — iOS zooms the page on focus. */
.kit-button {
    width: 100%;
    min-height: var(--wg-tap);
    font-size: 1rem;
}

/* --- Reports (R1) — admin, desktop assumed (SC14) ------------------------- */

/* The date filter. A GET form, so no token and no PRG — the URL is the state. */
.rp-range {
    margin: 0 0 1rem;
    padding: .85rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.rp-range-row {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem;
    align-items: flex-end;
}

.rp-range .field { flex: 1 1 10rem; margin: 0; }
.rp-range-go { flex: 0 0 auto; }
.rp-range-go button { width: auto; min-width: 7rem; }
.rp-range p { margin: .6rem 0 0; }

/* WGT_22894 — Rob's page only: the Daily view's ONE row of inputs, Host ·
   Period · From · To · Show (👤 Alex, 2026-10-23: "put host above the every host
   drop down and then it can have the period from and to and then show"),
   centred, with *Show* level with the boxes (WGT_22891: "the show button is
   vertically lower than the select boxes"). 🛑 The base `select` / `input`
   rules carry `margin: 0 0 1rem` and the row aligns bottoms (`flex-end`), so
   every control here takes the SAME `height` and no margin. The fields stop
   growing (`flex: 0 1`) so `justify-content: center` has room to centre them.
   Reports' own `.rp-range` keeps its look. */
.rel-controls .rp-range-row { justify-content: center; }
.rel-controls .field { flex: 0 1 12rem; }
.rel-controls .rp-range-go { flex: 0 0 auto; }   /* it is a `.field` too: only as wide as Show */
.rel-controls .field > select,
.rel-controls .field > input,
.rel-controls .rp-range-go > button {
    height: var(--wg-tap);
    min-height: 0;
    margin: 0;
}
.rel-controls .rp-range-go > button { padding-block: 0; }

/* WGT_22891 — the page's two tabs read as the main switch, a step larger than
   the filter chips under them; the count on a filter chip is bold. */
.rel-view-tabs .filter-chip { font-size: 1rem; }
.rel-chip-count { font-weight: 700; }

/* WGT_22896 — *Host overview*: every header and value centred (👤 Alex,
   2026-10-23: "center the values and the column headers on the whole table"). */
.rel-host-stats-table th,
.rel-host-stats-table td { text-align: center; vertical-align: middle; }

/* WGT_22839 — the same panel's table: Venue holds more width so a bar's name
   wraps less, and *On time?* may wrap (only a line or two need its long
   "— 12 min after the start"), so it stops holding a wide nowrap column. */
.rel-nights-table .rel-venue { min-width: 12rem; }
.rel-nights-table td.rel-on-time { min-width: 7rem; }

/* What period this is. Loud enough that a screenshot carries it. */
.rp-period {
    margin: 0 0 1rem;
    font-size: 1.05rem;
}

/* --- The state vocabulary ------------------------------------------------ */

/* An absent number. NEVER a 0: the dash carries its reason in a title, and the
   same reason is spelled out in the legend for anyone not hovering. */
.rp-none {
    color: var(--wg-muted);
    font-weight: 700;
    cursor: help;
    border-bottom: 1px dotted var(--wg-line);
}

/* The summary list. Three colours, matching the coverage banner's meanings:
   warm for "somebody should look at this", blue for "not answerable yet",
   plain for a real count. Deliberately NOT red — an uncounted night is a gap,
   not an error, and permanent red is how a warning becomes furniture (SC12). */
.rp-states {
    list-style: none;
    margin: .75rem 0 0;
    padding: 0;
}

.rp-state {
    display: flex;
    gap: .7rem;
    align-items: flex-start;
    padding: .55rem 0;
    border-top: 1px solid var(--wg-line);
}

.rp-state:first-child { border-top: 0; }

.rp-state-count {
    flex: 0 0 auto;
    min-width: 2.4rem;
    padding: .1rem .4rem;
    font-weight: 700;
    text-align: center;
    border-radius: 6px;
    background: var(--wg-panel);
}

.rp-state-text { flex: 1 1 auto; font-size: .95rem; }

.rp-counted .rp-state-count { color: var(--wg-accent-ink); background: #2e7d32; }
.rp-warn    .rp-state-count { color: var(--wg-warn-ink);    background: var(--wg-warn-line); }
.rp-info    .rp-state-count { color: var(--wg-accent-ink);  background: var(--wg-info-ink); }

/* The legend folds away, but it is on the page rather than in a tooltip: the
   difference between "nobody counted" and "nobody came" is the one thing a
   reader must not get wrong.

   RV152 — Alex: "it's just red text that looks like plain red text without
   any link or anything." The red was already real (measured, not assumed —
   RV65 found the opposite on this same page's export links). What was
   missing was the chrome: grey box, chevron, header left with the arrow —
   the sitewide `.wg-section` disclosure shape, copied verbatim below rather
   than converting this to a `.wg-section` (no `<h2>` wrapper here, and it
   must stay a plain `<summary>`, never a `<button>`). */
.rp-legend {
    margin: 1rem 0;
    padding: 0 1rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.rp-legend[open] {
    padding-bottom: 1rem;
}

.rp-legend > summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    margin: 0 -1rem;
    padding: .75rem 1rem;
    min-height: var(--wg-tap);
    font-weight: 600;
    color: var(--wg-accent);
    cursor: pointer;
    list-style: none;
}

.rp-legend > summary::-webkit-details-marker {
    display: none;
}

.rp-legend > summary::before {
    content: '';
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid var(--wg-muted);
    border-bottom: 2px solid var(--wg-muted);
    transform: rotate(-45deg);
}

.rp-legend[open] > summary::before {
    transform: rotate(45deg);
}

.rp-legend > summary:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

/* WGT_14402 — the label column is `max-content` on `.detail-list` (portal.css
   :924), and a `max-content` track cannot compress. Inside a legend fold the
   labels are whole sentences, so at 360 the two tracks ran 6px past the
   viewport and pushed the PAGE sideways (attendance's legend, `?preset=all`).
   `minmax(0, max-content)` lets the label column give way and wrap; column 2
   stays a bare `1fr` on purpose — `minmax(0, 1fr)` collapses it to 0 and the
   figure overflows its own cell instead, which measured WORSE (26px at 390).
   Scoped to the legend rather than `.detail-list` itself: every other user of
   that class measured 0 at 390 and 360 on 2026-09-16 (`WGT_14302`). */
.rp-legend .detail-list {
    margin: .25rem 0 0;
    grid-template-columns: minmax(0, max-content) 1fr;
}

/* --- Figures ------------------------------------------------------------- */

/* RV151 — Alex: "Anything small like the average and total and lowest and
   highest teams for trivia and the similar area for bingo should be
   centered." The box was stretching to the report panel's full width (grid
   column 2 is `1fr`), so the figures sat hugging the LEFT of a wide panel
   with the rest of the row empty. Same idiom RV88 already used one screen
   up for the same underlying problem (`.portal-main:has(.report-sections)
   .lede`): shrink the box to its own content, then centre the box — never
   `text-align: center`, which would centre lines inside a box that is
   itself still off-centre. RV17's ruling stands: this centres the FIGURE
   BLOCK, not a `.data-table` cell, and nothing here touches `.detail-list`
   generally.
   `grid-template-columns` is repeated here rather than left to `.detail-list`
   alone — coverage.php's caller (coverageReportRenderHeadline()) uses a bare
   `<dl class="rp-headline">` with no `.detail-list`, so without its own the
   default UA stylesheet's block dt/dd layout would centre as an unwilling
   indented list. Reused, not forked, across all six callers.
   🛑 WGT_14402 — THE FIRST TRACK IS `minmax(0, max-content)`, NOT `max-content`,
   AND THAT IS LOAD-BEARING. A bare `max-content` track cannot compress, so the
   `max-width: 100%` above capped the BOX while the tracks ran past it: with one
   equipment item recorded, reports.php pushed the page 55px at 390 and 85px at
   360. Column 2 stays a bare `1fr` — `minmax(0, 1fr)` collapses it to zero and
   the figure then overflows its own cell, measured worse at 26px. Measured
   2026-09-16 07:56–07:58 CDT: 0 at 390 and 360 on all five report sections,
   and 1440 geometry identical when the old value is A/B'd back in the same
   page load (reports.php is data-dependent, so two separate runs cannot be
   compared — a peer's rows move the numbers). */
.rp-headline {
    display: grid;
    grid-template-columns: minmax(0, max-content) 1fr;
    gap: .35rem 1rem;
    width: max-content;
    max-width: 100%;
    margin: .5rem auto 1.25rem;
    padding: .75rem 0 0;
    border-top: 1px solid var(--wg-line);
}

.rp-headline dt { font-weight: 600; color: var(--wg-muted); }

.rp-headline dd { margin: 0 0 .5rem; }

/* A bar is drawn ONLY for a real number, so an empty cell can never read as a
   measured zero. A recorded 0 gets the stub below instead of nothing. */
.rp-bar {
    display: inline-block;
    width: 4.5rem;
    height: .55rem;
    margin-right: .45rem;
    vertical-align: baseline;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 3px;
    overflow: hidden;
}

.rp-bar-fill {
    display: block;
    height: 100%;
    background: var(--wg-accent);
}

.rp-bar-zero { width: .5rem; background: var(--wg-line); }

/* The empty state is the deliverable that matters most today — every report is
   empty. It reads as a statement, not as a failure. */
.rp-empty { border-left: 4px solid var(--wg-info-line); }
.rp-empty-lede { font-size: 1.05rem; }

.rp-empty .confirm-list { margin: .75rem 0; }

/* Report tables carry a lot of columns and this is an admin desktop screen, so
   they scroll sideways inside their own wrapper rather than squeezing. */
.data-table .rp-none { white-space: nowrap; }

/* Every venue gets a row, including the ones with no number (Alex, 2026-07-30 —
   nothing to click through beats a shorter table). A dashed row is therefore
   quieted rather than hidden: present, readable, and visibly not a figure. The
   dash itself carries the reason; this only stops 38 of them shouting as loudly
   as the two rows that mean something. */
.data-table tr.rp-row-none td { background: var(--wg-panel); }
.data-table tr.rp-row-none td:first-child { color: var(--wg-muted); }

/* R4's direction words. ⚠️ These are readings OF numbers, not reasons a number
   is missing — every absence still goes through .rp-none with a cell state
   behind it. Only "falling" is coloured: this report exists to surface that one,
   and colouring all five would make the table a rainbow nobody scans. */
.rp-trend-down   { color: var(--wg-warn-ink); font-weight: 700; }
.rp-trend-up     { color: #2e7d32; }
.rp-trend-steady { color: var(--wg-ink); }
.rp-trend-none   { color: var(--wg-muted); }

/* A whole dimension that has no source yet (R3's callout response). It reads as
   a statement about what is NOT measured — the unknown rule applied to a column
   rather than a cell — so it is marked like the empty state rather than like a
   warning. A report that quietly omitted it would read as complete. */
.rp-unmeasured { border-left: 4px solid var(--wg-info-line); }
.rp-unmeasured h2 { margin-top: 0; }

/* --- A choice a person makes with a thumb (A7) ---------------------------- */

/* ⚠️ A whole tappable LINE, not a bare radio — the same reasoning as
   `.album-choice`, generalised because A7 needs it on two screens. The label
   carries what the option actually does ("they may still be asked to cover"),
   and that sentence is the part that stops the wrong one being picked, so it
   has to sit inside the target rather than beside it.
   🛑 `var(--wg-tap)` is not decoration: this is read on a phone, one-handed, and
   a radio's own 13px box is a mis-tap (SC14). */
.choice {
    display: flex;
    align-items: center;
    gap: .5rem;
    min-height: var(--wg-tap);
    padding: .1rem .25rem;
    line-height: 1.35;
    cursor: pointer;
}

.choice input { flex: 0 0 auto; margin: 0; }

/* A fieldset of them reads as one control, not as a new section — no border, and
   the legend WEIGHTED like the labels above every other field on the form.
   ⚠️ Weighted, not sized — measured on event-add.php 2026-08-16 (RV138): every
   `legend` in the app renders at the inherited 17px (nothing in either
   stylesheet sets one), against `label`'s .95rem/15.2px. So a `.choice-set`
   legend is the only BOLD legend on a form and that is what separates it from a
   section heading. 🛑 Do not "finish the job" with a font-size here: three
   screens share this class (event-add.php, hosts.php, availability.php) and it
   must never be restyled for one of them. */
.choice-set {
    margin: .85rem 0 0;
    padding: 0;
    border: 0;
}

.choice-set > legend {
    padding: 0;
    margin: 0 0 .1rem;
    font-weight: 600;
}

/* --- Private / Hidden / Public, and its "?" (WGT_15523) -------------------- */

/* 🛑 THE "?" IS A CLICK-TO-OPEN EXPLANATION, AND THAT IS DELIBERATE HERE. The
   Settings block below says a setting's explanation "must not be a tooltip" —
   that rule is for the Settings screen's numbers and is NOT changed by this.
   👤 Alex, 2026-09-22, asked for THIS control's explanation behind a "?" that
   opens on a click or a tap, never on hover (Graham is often on a phone). Two
   rules for two different things: do not "fix" either one to match the other.
   ⚠️ A `<details>`, so it needs no JavaScript — built by
   `eventVisibilityChoice()` in Code/includes/oneoff.php, the one source for the
   markup and the words on event.php and event-add.php alike. */

/* A FLOATED legend is laid out as an ordinary float rather than on the fieldset's
   border, which is the only way to put the "?" on the same line as it. The line
   height is the tap height so the text and the "?" centre on one line. */
.visibility-choice > legend {
    float: left;
    margin: 0 .5rem 0 0;
    line-height: var(--wg-tap);
}

.visibility-choice .visibility-options {
    clear: left;
    display: flex;
    flex-wrap: wrap;
    gap: 0 1.5rem;
}

/* Each word is a whole tap target, the `.choice` reasoning (SC14). */
.visibility-options .setting-check {
    align-items: center;
    min-height: var(--wg-tap);
}

.help-reveal > summary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--wg-tap);
    min-height: var(--wg-tap);
    cursor: pointer;
    list-style: none;
}

.help-reveal > summary::-webkit-details-marker { display: none; }

.help-reveal > summary > [aria-hidden] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 50%;
    font-weight: 700;
    line-height: 1;
}

.help-reveal[open] > summary > [aria-hidden] {
    color: var(--wg-paper);
    background: var(--wg-ink);
}

.help-reveal > summary:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: 2px;
    border-radius: 50%;
}

.help-reveal-body {
    clear: left;
    margin: 0 0 .75rem;
    padding: .75rem 1rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

.help-reveal-body dl { margin: 0; }
.help-reveal-body dt { font-weight: 600; }
.help-reveal-body dd { margin: 0 0 .6rem; }
.help-reveal-body p { margin: .6rem 0 0; }

/* --- My availability (A7) — host-facing, phone-first ---------------------- */

/* Cards, for the same reason `.kit-card` is one: a table of date ranges on a
   phone is a sideways scroll, and every action on this page changes who is
   working a real night. */
.av-card {
    margin: 0 0 .85rem;
    padding: .85rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.av-card:last-child { margin-bottom: 0; }

/* A range that has already ended is history — R3 reads it, so it is still shown,
   but it must not compete with the one they are about to act on. */
.av-card.is-past { background: var(--wg-panel); color: var(--wg-muted); }

.av-dates {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 700;
}

.av-meta {
    margin: .2rem 0 .6rem;
    font-size: .95rem;
    color: var(--wg-muted);
}

.av-button {
    width: 100%;
    min-height: var(--wg-tap);
    font-size: 1rem;
}

/* WGT_22436's `.open-shift-busy` (the centred reason under a card) went with
   the cards in WGT_22456: the reason is `.pickup-busy`, in the row below. */

/* WGT_22456 — ONE LINE PER NIGHT on *Open shifts* and the board's *Nights you
   can pick up* (👤 Graham, 2026-10-22: compact, like a table row, not a card).
   Four cells: when · where · what (shift, distance, spec sheet) · the button or
   the reason. Both screens draw the same markup (`openCalloutRowText()`).
   📱 A PHONE FIRST: the three words stack on the left and the button stays on
   the right of the row, so a 360px line never squeezes the bar to a word.
   🖥 From 40rem the LIST holds the four tracks and every row takes them as a
   subgrid, so the columns line up row to row — `#your-nights`' lesson
   (WGT_22011): a grid on each row alone gives every row its own column x. */
.pickup-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.pickup-item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "when act" "where act" "what act";
    align-items: center;
    gap: .1rem .75rem;
    padding: .55rem .5rem;
    min-height: var(--wg-tap);
    border-top: 1px solid var(--wg-line);
}

.pickup-item:first-child { border-top: 0; }

.pickup-item.is-today {
    background: var(--wg-ok-bg, rgba(46, 125, 50, .08));
}

.pickup-when  { grid-area: when;  font-size: .95rem; color: var(--wg-muted); }
.pickup-item.is-today .pickup-when { color: inherit; }
.pickup-where { grid-area: where; font-weight: 600; }
.pickup-what  { grid-area: what;  font-size: .95rem; color: var(--wg-muted); }
.pickup-act   { grid-area: act;   justify-self: end; text-align: right; }

.pickup-act form { margin: 0; }

/* `button` is a full-width block portal-wide; a row's button is its own size. */
.pickup-act button {
    width: auto;
    padding: .45rem .9rem;
    font-size: .95rem;
    white-space: nowrap;
}

/* WGT_22436's reason, in the button's place: muted, one sentence per line. */
.pickup-busy {
    display: block;
    max-width: 9.5rem;
    font-size: .875rem;
    color: var(--wg-muted);
}

.pickup-busy.sentences > span { display: block; }

@media (min-width: 40.0625rem) {
    .pickup-list {
        display: grid;
        grid-template-columns: max-content minmax(0, 1fr) auto auto;
        column-gap: 1rem;
    }
    .pickup-item {
        grid-column: 1 / -1;
        grid-template-columns: subgrid;
        grid-template-areas: none;
    }
    .pickup-when,
    .pickup-where,
    .pickup-what,
    .pickup-act { grid-area: auto; }
    .pickup-busy { max-width: 15rem; }
}

/* WGT_22882 — 👤 Alex, 2026-10-23, on the board's *Nights you can pick up*:
   "make the take this shift button shorter so it doesn't artificially make
   the row taller. Also add an HR above the first row". Desktop only: on a
   phone the three lines stack and are taller than the button anyway, and
   the button keeps its full tap height there. */
#pick-up .pickup-item:first-child { border-top: 1px solid var(--wg-line); }
@media (min-width: 40.0625rem) {
    #pick-up .pickup-item { min-height: 0; }
    #pick-up .pickup-act button {
        min-height: 0;
        padding: .2rem .75rem;
        line-height: 1.3;
    }
}

/* …and the panel holding the list a little wider than the 40rem reading
   column on a wide screen, exactly as `#your-nights` is (WGT_22011/22012):
   measured at 1440 before this, every "Bar, Town" wrapped onto three lines in
   the 575px panel. ⚠️ The explicit `width` on the column is what lets the
   negative margins widen the panel (RV68's shrink-to-fit trap). */
@media (min-width: 64rem) {
    .portal-main:has(.pickup-list) { width: min(40rem, 100%); }
    :has(> .pickup-list) { margin-inline: -4rem; }
}
/* WGT_22842 — 👤 Alex, 2026-10-23: on the board the list is exactly as wide as
   *Your nights* below it, which goes one step wider here (WGT_22012). */
@media (min-width: 80rem) {
    .portal-main:has(#your-nights) :has(> .pickup-list) { margin-inline: -5rem; }
}

/* --- The work queue's deep link into a night (EV2) ------------------------
   A queue row links `?event=N&shift=S#night-<id>`, and the night it names has
   to be findable the instant the page settles — a fortnightly event's list runs
   to dozens of rows and "scrolled to roughly here" is not an answer.

   🛑 THE FLAG IS `:target`, WHICH MEANS THE FRAGMENT DOES ALL OF IT. A fragment
   is never sent to the server, so PHP cannot know which row was clicked and any
   class it wrote would flag the wrong one. This needs no round trip, no
   JavaScript, and it still works from a bookmark or a pasted link.

   ⚠️ It is a persistent band, not a fade. Graham arrives to work the night, then
   looks away at the rotation panel and back — a highlight that has timed out by
   then has cost him the place he came to.
   ------------------------------------------------------------------------ */

.data-table tr:target > td {
    background: var(--wg-info-bg);
    box-shadow: inset 4px 0 0 var(--wg-accent);
}

/* Which SHIFT the queue row was about, on a night running more than one.
   ⚠️ Only ever styled INSIDE `:target` — the marker is written on every row that
   holds that shift, and outside the targeted one it means nothing. */
.data-table tr:target .is-scoped-shift {
    font-weight: 700;
    box-shadow: inset 0 -2px 0 var(--wg-accent);
}

/* --- In-page sections (TB7, superseding TB1's `.wg-jump` strip) -----------

   portalSectionStart()/portalSectionEnd() wrap each panel in a
   `<details class="wg-section" name="wg-section">`; `name=` alone gives the
   exclusive-group behaviour, so there is no JavaScript here at all — the
   strip this replaces was the portal chrome's only script.

   🛑 `wg-` IS REQUIRED, NOT DECORATION. A Chrome extension's injected
   `.alert{opacity:0}` once made every portal message invisible (2026-08-07,
   see ../specs/portal.md) and the culprit was never identified. A generic
   name here (`.accordion`, `.section`, `.panel-toggle`) is exactly the shape
   of selector a future extension collides with by accident.

   ⚠️ `--wg-tap` on `.wg-section > summary` is the whole tap target now and it
   applies AT EVERY WIDTH — no `64rem` drop. The strip's drop was about a
   sideways-scrolling row that no longer exists; night.php is still the one
   screen a host thumbs in a dark, loud room, at every width it's viewed at.

   ⚠️ The marker is drawn by hand and the native one is hidden on both engines
   — Safari and Chrome disagree on the default disclosure triangle's box, so
   leaving it native means two different-looking portals.
   -------------------------------------------------------------------------- */

.wg-section {
    margin: 0 0 1rem;
    padding: 0 1rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.wg-section[open] {
    padding-bottom: 1rem;
}

.wg-section > summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    /* Pulled back out to the box edges so the whole bar is one tap target,
       not just the text inside it. */
    margin: 0 -1rem;
    padding: .75rem 1rem;
    min-height: var(--wg-tap);
    cursor: pointer;
    list-style: none;
}

/* 🛑 WGT_18965 (👤 Alex, 2026-10-04): ONE bar centres — equipment.php's lone *Add an item*, by
   its own marker class. Every other accordion bar stays LEFT by his 2026-08-15 ruling
   (CLAUDE/specs/portal_centring.md); he chose this narrow line over centring all 22 screens. */
.wg-section--centred > summary {
    justify-content: center;
}

.wg-section > summary::-webkit-details-marker {
    display: none;
}

.wg-section > summary::before {
    content: '';
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid var(--wg-muted);
    border-bottom: 2px solid var(--wg-muted);
    transform: rotate(-45deg);
}

.wg-section[open] > summary::before {
    transform: rotate(45deg);
}

.wg-section > summary h2 {
    margin: 0;
}

.wg-section > summary:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

.wg-section > summary + * {
    margin-top: .25rem;
}

/* --- RV65 — reports.php's accordion is FULL WIDTH, not the 40rem prose
   measure (reversing WT15's flat layout back to LK1's accordion, now that
   RV24 moved every long table off the page and into the CSV). Same
   marker-class :has() idiom as .data-table--wide and .field--wide above —
   portal.php's <main class="portal-main"> is outside this task's grant too.

   ⚠️ `width`, NOT `max-width` alone, same reasoning as .field--wide: a report
   section has no wide table left inside it to pull a fit-content grid item
   out to full width on its own (RV24 moved those into the CSV), so without an
   explicit `width` .portal-main would stay shrink-wrapped near its own
   content's natural size regardless of viewport. An explicit `width` bypasses
   that shrink-to-fit computation outright. */
.portal-main:has(.report-sections) {
    width: 100%;
    max-width: 100%;
}

/* Prose stays at reading measure even though the column just got wide — same
   exception .field--wide takes for its ledes below (RV5's rule: a report
   takes the width its content needs, but a paragraph must never need to
   scroll sideways to read). Covers the page's own intro lede and every
   per-section blurb (both are .lede) plus the two standalone .muted
   paragraphs bookending the report loop — not the inline `.muted` captions
   inside `.rp-export-list`'s <li>s, which are text, not block prose, and
   max-width has no effect on an inline element anyway.

   🛑 `margin-inline: auto` IS RV88's HALF, 2026-08-14, and it is not cosmetic
   tidying — it is RV78's trap at page scale and it was the worst instance on
   the site. A `max-width` with no auto side margin leaves the box hugging the
   LEFT of a column RV65 had just widened to 1216px, so measured at 1440 the
   page lede and ~50 report paragraphs sat 256px LEFT of `.portal-main`'s
   centre under a title centred across the full width (48px at 1024).
   ⚠️ `text-align: center` alone would have made it WORSE, not better: it would
   have centred each line inside a box that is itself 256px off-centre. The box
   moves first; RV88's sitewide rule at the foot of this file then centres the
   text in it. Both are needed, exactly as RV78 found on question-set.php.
   ⚠️ The 40rem cap itself is untouched — it is the reading measure, and RV5's
   rule that a paragraph must never need sideways scrolling depends on it. */
.portal-main:has(.report-sections) .lede,
.portal-main:has(.report-sections) p.muted {
    max-width: 40rem;
    margin-inline: auto;
}

/* --- RV65 — the export links stop reading as red warnings ------------------
   See reportExportPanel()'s docblock in reporting.php for the full record,
   including the measurement that found them NOT actually --wg-accent red
   before this shipped. Scoped to this one panel (`.rp-export-list`), not
   every link on the portal — the task's own instruction was to propose the
   sitewide recolour rather than pick it silently, so it stays out of scope
   here. Ink, not accent and not the browser default: a deliberate colour
   reading as "a link", never as an error or a call to action. */
.rp-export-list a {
    color: var(--wg-ink);
    font-weight: 600;
    text-decoration: underline;
}

.rp-export-list a:hover,
.rp-export-list a:focus-visible {
    color: var(--wg-muted);
}

/* --- W14 — checklist.php's step list: display + drag-to-reorder -----------

   Steps come out of the accordion and off the horizontal-scroll table (the
   walk's themes 1 and 2) into a plain list — `.night-list`'s border-top-
   divider shape, not `.data-table`'s. Drag needs no library: native HTML5
   dragstart/dragover/drop, persisted by checklist.js's fetch() call (the
   app's only AJAX endpoint — see checklist.php's `reorder` action). The
   handle is a real <button>, so ArrowUp/ArrowDown on it is the keyboard-
   accessible fallback the walk asked for; drag alone is mouse/touch only.

   ⚠️ NOT `.confirm-list` — that stays on activities.php's read-only preview
   inside a table cell. This is an editable list with a drag handle, an Edit
   toggle and a Remove form on every row; unifying the two would mean either
   dragging a preview that cannot be reordered or losing the buttons a
   preview never needed. */

.step-list {
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
}

.step-row {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .5rem 0;
    border-top: 1px solid var(--wg-line);
}

.step-row:first-child { border-top: 0; }

.step-row--dragging {
    opacity: .4;
}

.step-handle {
    flex: 0 0 auto;
    width: var(--wg-tap);
    min-height: var(--wg-tap);
    padding: 0;
    font-size: 1.1rem;
    line-height: 1;
    color: var(--wg-muted);
    background: transparent;
    border: 1px solid var(--wg-line);
    cursor: grab;
}

.step-handle:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: 2px;
}

.step-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: break-word;
}

.step-actions {
    flex: 0 0 auto;
    display: flex;
    gap: .4rem;
}

.step-rename-form {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    gap: .4rem;
    margin: 0;
}

.step-rename-form input[type="text"] {
    flex: 1 1 auto;
    margin: 0;
    min-height: var(--wg-tap);
}

.step-rename-form button {
    width: auto;
}

/* Belt-and-braces over the `hidden` attribute: `.step-actions`/
   `.step-rename-form` set their own `display`, which is the same
   specificity as the UA stylesheet's `[hidden] { display: none }` — without
   this, source order alone decides which one wins. */
.step-actions[hidden],
.step-rename-form[hidden] {
    display: none;
}

/* GV3 — a day-of-week (or other repeating) group heading inside a
   `.data-table`, replacing `event.php`'s retired `.event-calendar`. No
   existing table groups rows under a repeating sub-heading, so this is new
   rather than reused; built from the same tokens `.rp-row-none` already uses
   for a quieted table row, not a new visual language. */
.data-table tr.table-group-row td {
    background: var(--wg-panel);
    font-size: .8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--wg-muted);
}

/* RV12 — one block per shift inside the merged Shifts section on event.php;
   a rule between blocks is what used to be four separate <details> chrome
   doing that job. */
.shift-block {
    padding: 1rem 0;
    border-top: 1px solid var(--wg-line);
}
.shift-block:first-of-type { border-top: none; padding-top: 0; }

/* 🛑 WGT_15649 (👤 Alex, 2026-09-22) — event.php's Shifts section is ONE CARD
   PER SHIFT, with a host line per rota week inside it. This REVERSES RV98
   (2026-08-15), which drew a `.shift-week` region per rota week and listed
   every shift in each; only the host differs by week, so the work, *Retire*
   and the empty-shift alert were printed twice. UNDO: git history.

   The card keeps RV98's look, moved from the week onto the shift: it inverts
   against its surroundings rather than inventing a tone — `#shifts` is a
   `.panel`, so it is already `--wg-panel` grey, and a card on `--wg-paper`
   lifts out of it the same way `.scan-card`'s grey lifts off a white page.
   The border follows RV72's inside-out rule — the darker `--wg-muted` on the
   box. ⚠️ `.shift-block`'s own top rule and `:first-of-type` reset are
   overridden here, because each card is now its own box.
   ⚠️ A one-off (no rotation) gets no `.shift-box` and renders as before. */
.shift-block.shift-box,
.shift-block.shift-box:first-of-type {
    margin: 0 0 1rem;
    padding: 1rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-muted);
    border-radius: 10px;
}

/* One week's host on the card's summary row — *Week 1 Rob* — kept together
   so the label never wraps away from the name it labels. */
.shift-row-week {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .1rem .35rem;
}

/* Inside the open card, one Host block per rota week, each labelled. A rule
   between the two weeks so week 2's radios never read as week 1's. */
.shift-host-week + .shift-host-week {
    border-top: 1px dashed var(--wg-line);
    margin-top: .4rem;
}
.shift-host-week-label {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .1rem .4rem;
    margin: .5rem .6rem .3rem;
}

/* RV98 — who is on this shift on this week: the whole reason the split
   exists, so it leads the block. Wraps rather than truncates; an inactive
   host or a "needs a host" tag has to stay beside the name it qualifies.
   🛑 RV130 centres the box the same way as the `h3` above — `margin-inline:
   auto` on a `width: fit-content` flex container. `justify-content` is left
   untouched (default `flex-start`), which is what keeps the label, name and
   Edit link reading left-to-right inside the now-narrower box.

   🛑🛑 RV133 (2026-08-16) — `.shift-who` IS NOW `.shift-row`, AND THIS IS
   RV130's RULE RE-POINTED, NOT DELETED. Alex asked for one row per shift —
   "the host name on the left, activities in the middle, and a button on the
   right" — and the line RV130 dressed ("Who is on it: Norma · Edit") is that
   row's left and right cells. So the box-centring it carried moves onto the
   element that replaced it, in the same shape RV130 measured on all seven of
   its targets: `width: fit-content` + `max-width: 100%` + `margin-inline:
   auto`, and NO `text-align` anywhere. `justify-content` stays at its default
   `flex-start`, so the three cells still read left-to-right inside the box.

   ⚠️ `fit-content` IS ALSO THE ANSWER TO WHAT ALEX WAS COMPLAINING ABOUT —
   "this feels like there's a lot of empty space right now". A full-width row
   would throw the button out to the column edge and put the empty space back
   in the middle of it; this row is as wide as what is in it and centres in the
   shift's card (`.shift-box`, one per shift since WGT_15649). 🛑 A `0` from a centring probe means nothing on its own
   here (RV109/RV130) — quote the box's WIDTH beside it. */
.shift-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .35rem 1.5rem;
    margin: .25rem 0 .5rem;
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
}

/* Each cell is its own wrapping line so a long activity list or an "(inactive)"
   qualifier stays beside what it belongs to rather than breaking the row. */
.shift-row-who,
.shift-row-work,
.shift-row-act {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .1rem .4rem;
}

.shift-row-name { font-weight: 700; }

.shift-who-label { font-weight: 600; color: var(--wg-muted); }

/* ===========================================================================
   WGT_15357 (👤 Alex, 2026-09-22) — THE WEEK CARD IS ONE LINE, AND EVERYTHING
   THAT EDITS IT OPENS INSIDE THAT CARD.

   🛑 WHAT THIS REPLACES, AND WHY. The host form used to render ONCE, at the
   foot of `#shifts`, below every week card and below *Add a shift* — so the
   form that edits week 1 sat nowhere near week 1. Alex, at the screen on
   Graham's host: *"I really dislike this layout where the change host form
   goes below all the weeks, even though it's talking about week one. That's
   very visually confusing and fragmented."*

   ⚠️ `.shift-row` above is now a `<summary>`, so the whole line is the
   control: `display: flex` suppresses the native marker, and the chevron is
   `.shift-change-toggle::before` below. The box-centring RV130/RV133 measured
   (`width: fit-content` + `margin-inline: auto`, NO `text-align`) is
   untouched — this is that same row wearing a different element. */

/* 🛑 WGT_15357 — a label a screen reader needs and the eye does not. Two of the
   panel's controls lost their visible label when the row collapsed to one line
   (the add-an-activity select, whose own placeholder option says it, and the
   host picker, whose radio directly above it does). ⚠️ THE LABEL IS NOT
   DELETED — a `<select>` with no label is a control a screen reader announces
   as nothing at all. The shape is the one `.pw-rules-status` already uses in
   this file; this is the first version of it that is reusable. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.shift-card { margin: 0; }
.shift-card > summary { cursor: pointer; list-style: none; }
.shift-card > summary::-webkit-details-marker { display: none; }
.shift-card > summary:focus-visible {
    outline: 2px solid var(--wg-accent);
    outline-offset: 3px;
    border-radius: 6px;
}

/* The right-hand cell's affordance. It borrows `.cover-reveal > summary`'s own
   tokens rather than inventing a look, so the two disclosures on this panel
   match; it is a `<span>` because the pressable element is the whole row. */
.shift-change-toggle {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .25rem .65rem;
    border: 1px solid var(--wg-line);
    border-radius: 6px;
    background: var(--wg-paper);
    font-weight: 700;
    white-space: nowrap;
}
.shift-change-toggle::before {
    content: '';
    width: .45rem;
    height: .45rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform .12s ease;
}
.shift-card[open] > summary .shift-change-toggle::before { transform: rotate(45deg); }
.shift-card > summary:hover .shift-change-toggle { border-color: var(--wg-muted); }

/* The opened half. Full card width on purpose — the ROW centres (RV130), the
   edit surface does not, because its two columns have to line up with each
   other rather than with the row above them. */
.shift-change {
    border-top: 1px solid var(--wg-line);
    margin-top: .7rem;
}

/* 👤 Alex, 2026-09-22: *"make the activity and host labels more noticeable and
   maybe you do have some background highlighting on that row"*. The card is
   `--wg-paper` white, so the bar is the panel grey it sits on — the same
   inside-out inversion the shift card (`.shift-box`) itself uses. */
.shift-change-bar {
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 6px;
    padding: .3rem .6rem;
    margin: .8rem 0 .4rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    font-size: .8rem;
    color: var(--wg-ink);
}

/* One activity per line, its button hard right. 👤 Alex: *"each activity can
   have its own row with the activity and the takeoff button being separate
   elements on that row … aligned horizontally to the right of those columns
   with the activity names and the activity drop-down list being aligned left"*.
   🛑 `flex-wrap` IS LOAD-BEARING AT PHONE WIDTH — without it the add row's
   button hangs past 390 and pushes the page (measured on the mock before it
   was added). */
.shift-change-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .5rem .6rem;
    padding: .4rem .6rem;
    border-bottom: 1px solid var(--wg-line);
}
.shift-change-row:last-child { border-bottom: none; }
.shift-change-left { flex: 1 1 auto; min-width: 0; }
/* 🛑 `width: auto` UNDOES THE SITEWIDE FULL-WIDTH BUTTON. Without it the
   button grows to fill the row and `justify-content` has nothing left to
   push — measured: *Take off this shift* spanned the whole card. */
.shift-change-row button { flex: 0 0 auto; width: auto; margin-left: auto; }
/* 🛑 `flex-basis: 0`, NOT `auto`. A `<select>`'s auto basis is its WIDEST
   option — *"Custom — invent something for this shift only"* — so with `auto`
   the row wrapped at 1440 and the Add button dropped to its own line on a
   desktop. Measured on the built page before this was changed.
   🛑 AND `min-width` IS THE OTHER HALF. With `min-width: 0` the select shrank
   to *"Add an a…"* at 390 instead of letting the button wrap under it —
   measured on the phone shot. 11rem is what makes the row break rather than
   squash, and the button then takes its own line. */
.shift-change-row select { flex: 1 1 14rem; width: auto; min-width: 11rem; max-width: 22rem; }
.shift-change-row .field { margin: 0; }
.shift-change form { margin: 0; }
.shift-change-body { padding: 0 .6rem; }

/* The host picker's own reveal — filters AND dropdown, shown only once
   *Replace the host* is picked. 👤 Alex, 2026-09-22: *"it should only show the
   choose a host drop-down and those filters if they click replace the host
   first … and … show the filters directly above the select drop-down and
   below the words replace the host"*. ⚠️ THIS REVERSES the row's own earlier
   instruction that the filters sit above all three choices; his later call
   wins and the row records the reversal. */
.host-reveal { margin: .35rem 0 .6rem 1.6rem; }
.host-reveal[hidden] { display: none; }
.host-pick { margin: 0; max-width: min(22rem, 100%); }

/* "Narrow the list" as real buttons. 👤 Alex, 2026-09-22: *"turn them into
   buttons with pretty check marks and background shading when you click it"*,
   then *"make the button a solid color instead of having the checkbox itself
   … we want to suggest that you can click anywhere on that rounded span"*.
   ⚠️ They are still LINKS — the filter is a server-side `?only=` round trip,
   not JavaScript — so the pill is what changed, never the mechanism. */
.pick-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem;
    margin: .2rem 0 .45rem;
}
.pick-filters-label { font-weight: 600; color: var(--wg-muted); font-size: .9rem; }
.pick-filter {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .32rem .85rem;
    border: 1px solid var(--wg-line);
    border-radius: 999px;
    background: var(--wg-paper);
    color: var(--wg-ink);
    text-decoration: none;
    font-size: .9rem;
    line-height: 1.3;
}
.pick-filter:hover { border-color: var(--wg-muted); background: var(--wg-panel); }
.pick-filter:focus-visible { outline: 2px solid var(--wg-accent); outline-offset: 2px; }
.pick-filter .tick { display: none; font-size: .85rem; font-weight: 700; line-height: 1; }
.pick-filter .count { color: var(--wg-muted); font-variant-numeric: tabular-nums; }
.pick-filter.is-on {
    background: var(--wg-accent);
    border-color: var(--wg-accent);
    color: var(--wg-accent-ink);
    font-weight: 700;
}
.pick-filter.is-on:hover { background: var(--wg-accent); }
.pick-filter.is-on .tick { display: inline; }
.pick-filter.is-on .count { color: rgba(255, 255, 255, .8); }

/* RV12 — the "who could cover this shift" reveal, per shift block. Same
   fold-away chrome as `.kit-more` (venues.php's "Manage this event…"),
   duplicated rather than extending that selector so venues.php's own rule
   is untouched.

   🛑 RV98 — IT READS AS A CONTROL NOW, not as a line of text. Alex,
   2026-08-15: "the show who could cover this shift and add a shift just looks
   strange being plain text." It was `display: flex` with no border and no
   fill, measured `border=0px none bg=rgba(0,0,0,0)` — bold red words with the
   native marker already suppressed by the flex display, so there was nothing
   left saying it could be pressed.
   ⚠️ It borrows `.button-quiet`'s own tokens (`--wg-paper` on `--wg-line`)
   rather than a new look, and `width: fit-content` is what makes it a button
   rather than a full-width accordion bar — this is NOT the sitewide
   `.wg-section` accordion and must not start reading as one. The chevron is
   `.wg-section > summary::before` copied verbatim so the two disclosure
   affordances match. */
.cover-reveal { margin: .5rem 0 0; }

.cover-reveal > summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    width: fit-content;
    max-width: 100%;
    /* RV130 (2026-08-16) — Alex: centre both reveals ("Show who could cover
       this shift" and "Add a shift"). The pill was already `width:
       fit-content`; it just had no side margin, so it sat at the block start
       of whatever it is in (a shift card or the bare `#shifts` panel).
       `margin-inline: auto` is the only change — the `display: flex` row
       inside stays `justify-content: flex-start` by default, so the chevron
       and text keep reading left-to-right within the pill exactly as they
       do today; only the pill's position on the page moves. */
    margin-inline: auto;
    min-height: var(--wg-tap);
    padding: 0 .9rem;
    font-size: 1rem;
    font-weight: 600;
    /* 🛑 BLACK, NOT RED — Alex, 2026-08-17: "the *Change this shift* and *Add a
       shift* buttons should have black text, not red." Measured before the change:
       rgb(180, 35, 31), which is --wg-accent.
       ⚠️ HE NAMED TWO PILLS AND THIS RULE CHANGES ALL THREE, deliberately — *Show
       who could cover this shift* is the same `.cover-reveal > summary` control in
       the same look, and one red pill among three identical black ones reads as a
       different kind of thing rather than as a third instance of the same one. It
       also lands where RV74, RV88 §6 and RV158 already arrived independently:
       --wg-accent means *selected* and *alarm* on this site, so it cannot also mean
       *ordinary disclosure control*. The border and the chevron are untouched. */
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
    cursor: pointer;
    list-style: none;
}

.cover-reveal > summary::-webkit-details-marker { display: none; }

.cover-reveal > summary::before {
    content: '';
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid var(--wg-muted);
    border-bottom: 2px solid var(--wg-muted);
    transform: rotate(-45deg);
}

.cover-reveal[open] > summary::before { transform: rotate(45deg); }

.cover-reveal > summary:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: 2px;
    border-radius: 8px;
}

.cover-reveal[open] > summary { margin-bottom: .6rem; }

/* The pill inverts against whatever it is sitting on, the same one idea as
   `.shift-box` above: white on the grey `.panel` (the *Why an empty checklist
   still pays* reveal, which is outside the cards), grey on a white shift card.
   ⚠️ *Add a shift* is its own card since WGT_15652, so it takes the grey.
   Without this an in-card pill is white on white and only its border does any
   work. ⚠️ Was `.shift-week …` until WGT_15649 turned the week regions into
   one card per shift. */
.shift-box .cover-reveal > summary { background: var(--wg-panel); }

/* --- WGT_14860 — the weekly-announcement rows are a LIST, not 26 pills ----

   👤 Alex, 2026-09-22 (the `WGT_15016` reading walk, decision 2): *"The 26
   weekly-announcement rows are restyled to left-aligned full-width rows."*

   🛑 THE DEFECT WAS THE RAGGED LEFT EDGE, NOT THE PILL ITSELF. `WGT_14654`
   built this panel with no `portal.css` grant, so it reused `.cover-reveal` —
   the shipped `width: fit-content; margin-inline: auto` pill from the shifts
   screen (RV12/RV130). One centred pill is a control. Twenty-six of them, each
   a different width because the date text varies ("Sep 21 to Sep 27" is not
   "Nov 30 to Dec 6"), is a zig-zag: no two left edges line up, so the eye has
   to re-find the start of every row down a list whose whole job is to be
   scanned. Every other list in this portal is a left-aligned full-width row.

   ⚠️ IT IS STILL A CONTROL, and that is why this is not simply "remove the
   class". The chevron, the tap target, the pointer and the focus ring are all
   kept verbatim from `.cover-reveal` — only the BOX changes: full width instead
   of fit-content, hairline separators instead of a pill border, and a tighter
   vertical rhythm (the pill carried `margin: .5rem 0 0` per row plus .9rem of
   side padding, which on 26 rows is a lot of nothing).

   🛑 `.wg-week` IS WORN ONLY BY portal/weekly-announcements.php's ANNOUNCEMENT
   ROWS (on settings.php until WGT_19221 moved the panel). The holiday days on
   settings.php are `.holiday-row` since WGT_15660 (block below) — the
   same list idea, their own hook, because a hook worn by two different looks is
   not a hook.
   ⚠️ This is NOT the sitewide `.wg-section` accordion — these rows carry no
   `name`, so several open at once, which is what writing a fortnight ahead
   needs. The chevron matches `.wg-section > summary::before` on purpose, the
   same way `.cover-reveal`'s does. */
.wg-week {
    margin: 0;
    border-top: 1px solid var(--wg-line);
}

/* The stack closes with a line so it reads as one block rather than as a run of
   rules that stops mid-air. `:last-of-type` counts `<details>` siblings, so the
   last past-announcement row carries it when that list is present and the last
   upcoming row when it is not. */
.wg-week:last-of-type { border-bottom: 1px solid var(--wg-line); }

.wg-week > summary {
    display: flex;
    align-items: center;
    gap: .6rem;
    /* The two halves of the fix. `width: 100%` puts every left edge on the same
       x; `.45rem` of vertical padding against `.cover-reveal`'s pill box is the
       tighter rhythm. `--wg-tap` still sets the floor, so the row is a full
       48px tap target at every width — the rule `.wg-section > summary` states
       and `portal_sections.md` records. */
    width: 100%;
    min-height: var(--wg-tap);
    padding: .45rem 0;
    color: var(--wg-ink);
    cursor: pointer;
    list-style: none;
}

/* ⚠️ Safari and Chrome disagree on the native triangle's box, so it is hidden
   on both engines and drawn by hand below — the same two-engine reason stated
   at `.wg-section` above. */
.wg-week > summary::-webkit-details-marker { display: none; }

.wg-week > summary::before {
    content: '';
    flex: 0 0 auto;
    width: .5rem;
    height: .5rem;
    border-right: 2px solid var(--wg-muted);
    border-bottom: 2px solid var(--wg-muted);
    transform: rotate(-45deg);
}

.wg-week[open] > summary::before { transform: rotate(45deg); }

.wg-week > summary:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

/* An open row needs air under its form, but a closed one must not: 26 closed
   rows are the normal state of this panel and every one of them would pay it. */
.wg-week[open] { padding-bottom: .9rem; }

.wg-week > summary + * { margin-top: .25rem; }

/* --- WGT_15660 — Settings → Holidays: one line per day ----------------------

   👤 Alex, 2026-09-22, on the old seven cards: *"extremely sparse and expanded
   on a bigger screen … For a small phone, one per line is probably good, but
   for this it's definitely overkill."*

   ONE LINE PER HOLIDAY: name · next date · what happens · Change. On a wide
   screen the four sit on one line; on a phone they wrap inside the row. The
   whole line is the <summary>, so a tap anywhere on it opens the form.
   ⚠️ The row look is `.wg-week`'s (hairlines, full width, 48px tap floor)
   written again under its own hook — see the 🛑 in that block. */
.holiday-list { margin: .5rem 0 1rem; }

.holiday-row {
    margin: 0;
    border-top: 1px solid var(--wg-line);
}

.holiday-row:last-child { border-bottom: 1px solid var(--wg-line); }

.holiday-row > summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .15rem 1rem;
    width: 100%;
    min-height: var(--wg-tap);
    padding: .45rem 0;
    color: var(--wg-ink);
    cursor: pointer;
    list-style: none;
}

.holiday-row > summary::-webkit-details-marker { display: none; }

.holiday-row > summary:focus-visible {
    outline: 3px solid var(--wg-accent);
    outline-offset: -3px;
}

/* Name and date are fixed-width columns on a wide screen, so the seven dates
   line up down the list; the state takes what is left. */
.holiday-row-name  { flex: 0 0 11rem; font-weight: 600; }
.holiday-row-date  { flex: 0 0 8.5rem; white-space: nowrap; }
.holiday-row-state { flex: 1 1 18rem; min-width: 0; }

/* It looks like `.button-quiet`, but it is a label on the summary, not a button:
   the summary is the control. `aria-hidden` in the markup for the same reason. */
.holiday-row-change {
    flex: 0 0 auto;
    margin-left: auto;
    padding: .3rem .8rem;
    font-size: .9rem;
    font-weight: 600;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 8px;
}

.holiday-row[open] > summary .holiday-row-change { visibility: hidden; }

.holiday-row[open] { padding-bottom: 1rem; }

/* The open form. Fields sit side by side and each box is only as wide as it
   needs; on a phone they stack, one per line (the 40rem block below). */
.holiday-fields {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0 1.5rem;
}

/* ⚠️ CENTRED, NOT LEFT, AND ON PURPOSE. The brief said "labels left over their
   box unless design_guide.md says otherwise" — and it does: its router sends
   centring to portal_centring.md, where `RV117` records 👤 Alex asking for
   EVERYTHING on the Settings screens centred, form labels included (the
   `.portal-main:has(.setting) .field > label` rule further down). So each label
   is centred over its OWN box, and the row of boxes is centred in the panel. */
.holiday-fields { justify-content: center; }
.holiday-message textarea { margin-inline: auto; }

.holiday-fields > .field { flex: 0 1 auto; }
.holiday-fields input[type="text"] { width: 20rem; max-width: 100%; }
.holiday-fields input[type="date"] { width: 11rem; max-width: 100%; }

/* `.choice-set`'s margin is authored for the END of a form (portal_controls.md);
   here it sits beside the fields, so its top lines up with their labels. */
.holiday-switch { margin: 0 0 1rem; }
.holiday-switch .setting-check { font-weight: 400; margin: .35rem 0 0; }

/* The two radios line up under their legend. `RV117`'s settings-page rule
   centres every `.setting-check` (`.portal-main:has(.setting) .setting-check`,
   further down), which left the shorter "We work this day" indented under the
   longer one — two options of one choice must share a left edge. The extra
   type selector outweighs that rule; nothing else on the page is touched. */
.holiday-fields fieldset.holiday-switch .setting-check { justify-content: flex-start; }
.holiday-switch input[type="radio"] { width: 1.1rem; height: 1.1rem; margin: .15rem 0 0; flex: 0 0 auto; }

.holiday-message textarea { max-width: 40rem; }

.holiday-nights form,
.holiday-past form { display: inline; }

.holiday-delete { margin: 1rem 0 0; }

.holiday-past { margin: 0 0 1rem; }
.holiday-past > summary { cursor: pointer; color: var(--wg-muted); }

@media (max-width: 40rem) {
    .holiday-row-name,
    .holiday-row-date { flex-basis: auto; }
    .holiday-fields { display: block; }
    .holiday-fields input[type="text"],
    .holiday-fields input[type="date"] { width: 100%; }
}

/* --- The substitutes filters, 2026-08-17 --------------------------------

   Alex, walking event.php: "for the substitutes area, center the two only show
   text boxes", and make the radius typeable.

   🛑 `justify-content`, NEVER `text-align` — the same rule this file states at
   `.shift-activities-list`. `.setting-check` is `display: flex` and it already
   spans the full column, so its BOX was centred before this change and measured
   `offset-from-centre=0` while its content sat hard left at x=561. `text-align`
   cannot move a flex item; only the main-axis alignment can. */
.cover-reveal .setting-check { justify-content: center; }

/* --- /portal/night.php — WGT_14229 (Alex's keeps from WGT_13519) -----------

   🛑 EVERY RULE BUT THE LAST IS FENCED TO ONE SCREEN. `night.php` marks its first
   panel `.night-page` (portal.php builds <main> and takes no class — the same
   :has() route `.data-table--wide`, `.field--wide` and `.event-detail` use above),
   and `.cover-table` exists only in night.php's sub-hunt tables. Nothing here can
   reach markup that does not ask for it.

   Candidate 2 — the column. 40rem left 45% of a 1440 window empty on a page that
   scrolls. `width`, not `max-width` alone, for the shrink-to-fit reason WT9 records
   at `.field--wide`. 60rem is that rule's own ceiling, reused rather than a new one.
   Below 64rem the shell is not a grid and `100%` wins, so a phone is unchanged. */
.portal-main:has(.night-page) {
    width: min(60rem, 100%);
    max-width: min(60rem, 100%);
}

/* Candidate 5 — WENT GLOBAL 2026-09-16 and the scoped rule that stood here is
   deleted. It was `.portal-main:has(.night-page) .table-wrap { max-width: 100% }`,
   shipped by `WGT_14229` because a `.table-wrap` is `width: fit-content`, so on a
   phone its table's min-content pushed it 6–85px past its panel instead of
   scrolling inside it.
   `WGT_14302` then measured every portal screen at 390 and 360 and found the same
   defect on THIRTEEN more — this rule is why `night.php` alone was clean. 👤 Alex
   made it global on 2026-09-16, so the declaration now lives on `.table-wrap`
   itself (search `THIS CAP IS LOAD-BEARING`) and this scoped copy would only be a
   duplicate. 🛑 Do not re-add it here. */

/* Candidate 3 — Priority 1 and Priority 2 are one list in two tiers, but each
   auto-sized to its own names, so their columns sat 17px apart. A fixed layout
   over a shared <colgroup> gives every tier identical columns.
   ⚠️ From 40rem up only: a fixed layout never lets a `nowrap` cell widen its
   column, so on a phone a long name would paint over its neighbour. Below 40rem
   the tables keep auto layout and scroll inside their wrap (candidate 5).
   ⚠️ The action column is sized for "Assign to <shift>" on a two-shift night, not
   for the bare "Assign" — measure that case before narrowing it. */
@media (min-width: 40rem) {
    .cover-reveal .table-wrap:has(.cover-table) { width: 100%; }
    .cover-table { table-layout: fixed; }
    .cover-table .cover-col-pri { width: 3.5rem; }
    .cover-table .cover-col-host { width: 30%; }
    .cover-table .cover-col-act { width: 11rem; }
}

/* Candidate 12 — header tap targets on a phone. 🛑 NOT FENCED: the brand link and
   a page's back link are portalHeader()'s, on every portal screen, and a 26px brand
   is as small on hosts.php as on night.php. Phone-only by construction (the
   file's own 40rem breakpoint), so no desktop header moves; measured on four
   other screens at 1440/1152/1024/360 before and after (WGT_14229's record).
   `inline-flex` keeps the link inline — centred by its line's `text-align` — while
   giving it a box that `min-height` can reach; 44px is the tap floor the row named
   (`--wg-tap` is 48 and would push every header down a further 4px). */
@media (max-width: 40rem) {
    .portal-brand,
    .page-head-row .lede a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }
}

/* The radius line is three things on one row — checkbox+words, the box, then
   "miles (N)" — so it wraps as a unit on a narrow screen instead of orphaning
   the number. ⚠️ `cursor: default` undoes `.setting-check`'s pointer: this one
   is a `<div>`, not a `<label>`, because a number input nested in a label
   forwards its clicks to that label's checkbox (see event.php's own note). The
   inner `.cover-near-toggle` is the label, and it keeps the pointer. */
.cover-near {
    flex-wrap: wrap;
    align-items: center;
    cursor: default;
}

.cover-near-toggle {
    display: flex;
    align-items: center;
    gap: .6rem;
    cursor: pointer;
}

/* `input[type="number"]` is `display: block; width: 100%; margin: 0 0 1rem`
   sitewide — all three have to go or the box takes the whole line and pushes
   "miles (N)" under it.

   ⚠️ `min-height` HAS TO GO TOO, and it was measured: left on `--wg-tap` (48px)
   the box rendered 88×48 in a 64px-tall line and read as a form field dropped
   into a sentence. `2.2rem` is `.row-action`'s own inline min-height — the
   established size on this site for a control that sits inside a line of text
   rather than owning one — and 16px type still holds, so iOS does not zoom on
   focus (see the base rule's note).

   🛑 `input.cover-miles`, NOT `.cover-miles` — AND THAT IS THE WHOLE RULE
   WORKING. The base selector is `input[type="number"]` (0,1,1); a bare class is
   (0,1,0) and LOSES, so `width` and `min-height` stayed on the sitewide values
   while `max-width` (which the base does not set) applied — the box measured 88×48,
   then 70×48, looking as though the min-height line had been ignored. Adding the
   element name ties the specificity and this rule wins on source order.

   🛑 NO `text-align` — IT WAS HERE FOR ~20 MINUTES AND IT IS THE ONE FORBIDDEN
   MOVE IN `portal_centring.md`: *"THE BOX MOVES, THE TYPED TEXT NEVER DOES … a
   value that re-centres character by character while you type is unusable."* A
   two-digit radius is exactly that case — 1 → 15 → 150 shifts the digits under the
   cursor on every keystroke. The digits read from the left like every other typed
   value on the site. ⚠️ **Centring the box's CONTENTS is never how this line gets
   centred** — the row is a flex container and `justify-content` on
   `.cover-reveal .setting-check` is what does it, three rules up. */
input.cover-miles {
    display: inline-block;
    width: auto;
    min-width: 3.6rem;
    max-width: 4.4rem;
    min-height: 2.2rem;
    margin: 0;
    padding: .2rem .45rem;
}

/* --- One row of actions per row, 2026-08-17 -----------------------------

   Alex: the two *Add at N* buttons should sit side by side "instead of making
   twice the vertical space". 🛑 THE CAUSE IS `button { display: block }`, NOT a
   missing width: `.row-action` overrides only `width` (to `auto`), which leaves
   a block-level shrink-to-fit box that still takes a line of its own — the same
   trap RV107 documents for the centred `#retire` button. Measured at 1440 before
   the change: *Add at 1* at y=3293 and *Add at 2* at y=3328, 35px apart.

   ⚠️ IT COVERS *Move* / *Remove* TOO, and that is intended rather than scope
   creep: they are the identical two-buttons-in-one-cell case three rows up the
   same table, stacking by the same 35px, and fixing one pair would leave the
   table disagreeing with itself. The select goes inline for the same reason —
   it is part of *Move*'s own control.

   🛑 SCOPED TO `.cover-reveal`. `.row-action` is worn by buttons on hosts.php,
   equipment.php and questions.php that are laid out by their own flex rows; a
   sitewide `display: inline-flex` would reach all of them. */
.cover-reveal .data-table td .row-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    vertical-align: middle;
}

.cover-reveal .data-table td .row-action + .row-action { margin-left: .35rem; }

.cover-reveal .data-table td select {
    display: inline-block;
    width: auto;
    min-width: 3.5rem;
    margin: 0 .35rem 0 0;
    padding: .3rem .4rem;
    vertical-align: middle;
}

/* RV134's `.sub-tabs` / `.sub-tab` / `.sub-panel` (one Substitutes tab per
   shift) were REMOVED by WGT_15652 (👤 Alex, 2026-09-22): each shift's cover
   list sits inside that shift's own card now, so there is nothing to switch
   between. UNDO: git history. */

/* RV130 (2026-08-16) — the remaining two boxes from Alex's walk of `#about`
   and `#shifts`: the venue's address (and, admin-only, the *Edit venue
   details* link below it) and the admin-only *Add something else to this
   shift* form. Same rule as above: `width: fit-content` + `margin-inline:
   auto`, no `text-align` anywhere here, so wrapped lines keep reading from
   the box's own left edge.
   🛑 `#address > p:not(.muted)` is deliberately scoped past the `.muted`
   geocode-status paragraphs in the same `<div>` (`event.php`'s "Located —
   looked up …" / "Waiting to be looked up" lines) — Alex asked to centre the
   address and the edit link, not the lookup status, and both of those are
   the only two `<p>` in `#address` that ever render without `.muted`. */
#address > p:not(.muted) {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
}

/* `.work-add-heading` is `.shift-block`'s "Add something else to this shift"
   only — its sibling "Retire this shift" `h5` is not part of this task and
   is untouched. `.work-add-label` is the one field label named in the walk
   ("What else this person does"); the form's other two labels (the ad-hoc
   name and checklist-step fields) are unnamed and stay left as they render
   today. */
.work-add-heading {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
}

.work-add-label {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
}

/* --- RV75 — albums.php centres its headings and prose (2026-08-14) ---------

   Alex asked for "the Facebook album section and the every-event section"
   centred, and was then asked whether that reached the table cells:
   🛑 HEADINGS AND PROSE ONLY (2026-08-14 2:12pm). RV17 had already re-tested
   the data-table half BY MEASUREMENT on this exact screen — 193 albums, names
   4–50 characters — and centred nothing; that exclusion stands and the cells
   below are untouched.

   PAGE-SCOPED, deliberately. Two of the three things centred here are
   sitewide shapes RV19 left alone on purpose:
   · a panel `<h2>` — RV19's #retire note says the heading is "deliberately
     excluded, matching every other panel heading";
   · multi-line `.lede` and `.muted` prose — RV19 kept it left because the
     class is reused for genuine descriptions, and a long paragraph is harder
     to read centred.
   ⚠️ The batch ruling KNOWINGLY overrules the second for this screen (and for
   published.php and question-set.php). So neither may go sitewide from here:
   the anchor is `.albums-lede`, a marker class on albums.php's own lede, and
   `:has()` scopes every rule to the one page — the same idiom as
   `.data-table--wide`, `.field--wide` and RV65's `.report-sections`, and for
   the same reason: portal.php's <main class="portal-main"> is outside this
   task's file grant.

   `p:not(.wg-alert)` is the one exclusion. `.wg-alert` is shared error/ok
   chrome rendered identically on every screen; centring its text here alone
   would make albums.php's alerts differ from the rest of the portal, which is
   drift for RV79 to catalogue rather than a thing Alex asked for. The two
   filter chips are left too — they are controls, not prose.

   ⚠️ SUBSUMED BY RV88's SITEWIDE RULE at the foot of this file (2026-08-14) and
   KEPT ANYWAY, deliberately. It is now redundant — every selector below is a
   strict subset of the sitewide one, proved by re-rendering this screen as a
   control after the change — but `.albums-lede` is a marker class in
   albums.php's markup, which is outside RV88's file grant. Deleting the rule
   without the marker would leave a class with no CSS rule anywhere, which is
   the exact defect RV79 §6b raised about `class="pill"`. 🛑 So the rule and its
   marker are removed TOGETHER or not at all; do not tidy one half. */
.albums-lede,
.portal-main:has(.albums-lede) > .panel > h2,
.portal-main:has(.albums-lede) > .panel > p:not(.wg-alert) {
    text-align: center;
}

/* --- RV77 — published.php centres its headings and prose (2026-08-14) ------

   Alex asked for the page lede, the "these have already published" text and
   the needs-attention panel and its whole div to centre. Deliberately the
   SAME SHAPE as RV75 above rather than a second mechanism: one rule, keyed
   off a marker class on this page's own lede, `:has()` scoping it to the one
   screen, headings and prose only.

   🛑 THE MARKER IS THE LEDE, NOT `.scan-log`. `.scan-log` is page-unique too
   (grep -rln: published.php alone) and would have needed no `:has()` at all —
   RV76's shortcut. It is wrong here: the scan log only renders when the feed
   has entries, so on an empty or error feed — the one screen state that is
   nothing BUT prose — the centring would silently vanish. The lede renders
   in every branch.

   🛑 HEADINGS AND PROSE ONLY (Alex, 2026-08-14 2:12pm). The data-table cells
   in the two attention panels stay left, per RV17's measured exclusion; so
   does .scan-card-caption, which is a published caption reproduced verbatim
   and is long-form content by any reading of the batch rule. Neither is a
   direct child of `.panel`, so neither is reachable by the selector below —
   that is not luck, it is why the selector is `> .panel >` and not `.panel`.

   `p:not(.wg-alert)` is the one exclusion, exactly as RV75: the four "this
   could not be read, so it is unknown — not empty" paragraphs are shared
   error chrome rendered identically across the portal, and centring them
   here alone would make published.php's alerts differ from every other
   screen's. The three top-of-page .wg-alert DIVs are outside .panel and are
   untouched for the same reason.

   ⚠️ The table inside each attention panel needs nothing: `.table-wrap` is
   already `width: fit-content; margin-inline: auto` (RV5), so the table
   block is centred in the panel while its cells stay left. Do not add
   `text-align: center` to reach it — that is the exact split RV5 draws.

   ⚠️ SUBSUMED BY RV88's SITEWIDE RULE (2026-08-14) and KEPT for the same reason
   RV75's is, above: `.published-lede` is a marker in published.php's markup,
   outside RV88's grant, and a marker with no rule is RV79 §6b's defect.
   🛑 Rule and marker go together or not at all. */
.published-lede,
.portal-main:has(.published-lede) > .panel > h2,
.portal-main:has(.published-lede) > .panel > p:not(.wg-alert) {
    text-align: center;
}

/* --- RV78 — question-set.php centres everything outside the set's box -------

   Alex, in his own words: "don't center the text inside of the set text area
   but center all the text outside of that." So the textarea's CONTENT stays
   left — it is long-form ENTRY, the general rule for this whole batch — and
   everything around it centres, the box's own label and caption included.

   PAGE-SCOPED off `.field--wide`, and NO marker class was added. RV76's
   shortcut applies: `.field--wide` is already page-unique — `grep -rn` finds
   the class used in question-set.php alone (published.php and albums.php name
   it only in comments, citing the idiom) — and portal.css:414/425 ALREADY
   scope off it, so this reuses that anchor rather than minting a second one.
   It renders in every branch of the page (new set and edit alike), which is
   the property RV77 required of a marker and `.scan-log` lacked.

   🛑 TWO RULES, NOT ONE, AND THE FIRST IS THE ONE A SCREENSHOT MISSES.
   portal.css:425-429 caps `.lede`, `.field:not(.field--wide)` and the box's
   `.muted` at `max-width: 40rem` inside a `.portal-main` this page widens to
   60rem (WT9). A max-width with no auto margin leaves that box hugging the
   LEFT, so `text-align: center` alone centres the text within a box that is
   itself off-centre. MEASURED before the change, admin session, real
   stylesheet, 1440: the lede's box centre sat 128px left of .portal-main's,
   the title field's 111px left. Centring the text inside them would have
   landed the result ~131px and ~115px left of the page's middle — visibly
   wrong, and RV76's "off by half a button reads as a bug rather than a
   choice" failure at three times the size. `margin-inline: auto` centres the
   BOX; the second rule then centres the text in it. Both are needed.
   ⚠️ The 40rem cap itself is NOT touched — it is the reading measure, and
   WT9/RV19 both want it. Only where the box sits changes.

   ✅ It also fixes the bottom back-link, which the sitewide
   `.lede:has(> a:only-child)` rule already centred: its text was centred
   inside that same left-hugging 640px box, so "← Back to the questions"
   rendered 128px left of centre on this screen and nowhere else.

   What is deliberately NOT centred:
   · the textarea's content — the line Alex drew.
   · `.data-table` cells in the history table — RV17's measured exclusion,
     upheld unchanged by RV75 and RV77.
   · the Remove button in `#remove`. It is a CONTROL, not prose, and RV75 left
     albums.php's filter chips for that reason. `.button-row` is already
     centred sitewide by RV19, so the Save button needs nothing here.
   ⚠️ Centring a form INPUT here reversed part of RV19's list for this one page.
   WGT_18302 (2026-10-02) made that the base rule, so the five label / input /
   hint selectors that stood in the second rule below are gone. The
   `margin-inline` rule is still needed: the base rule centres text, not boxes. */
.portal-main:has(.field--wide) > .lede,
.portal-main:has(.field--wide) .field:not(.field--wide),
.portal-main:has(.field--wide) .field--wide .muted {
    margin-inline: auto;
}

/* WGT_22094 — question-week.php's day view put its lede inside a `.panel`
   (👤 Alex, 2026-10-21: each part in its own box). The 40rem cap above still
   reaches it (a descendant rule); this centres the capped box in the panel. */
.portal-main:has(.field--wide) > .panel > .lede {
    margin-inline: auto;
}

/* WGT_22103 — question-week.php's DAY view takes the wide column on EVERY
   day. A past day draws no paste box, so no `.field--wide`, and the column
   fell back to the 40rem reading measure: the nights table wrapped every cell
   onto two lines (👤 Alex, 2026-10-21: "make the sections a bit wider so the
   email doesn't drop down to two lines"). `.question-day-panel` is on the
   *This day's questions* panel, drawn on every day. `width`, not `max-width`
   alone — WT9's shrink-to-fit reason above.
   🛑 Prose keeps its 40rem measure (RV5: never widen a paragraph with the
   column) — the ledes and every paragraph straight inside a panel, centred. */
.portal-main:has(.question-day-panel) {
    width: min(60rem, 100%);
    max-width: min(60rem, 100%);
}

.portal-main:has(.question-day-panel) > .panel > .lede,
.portal-main:has(.question-day-panel) > .panel > p:not(.wg-alert) {
    max-width: 40rem;
    margin-inline: auto;
}

/* WGT_22103 — the day's nights table, ONE LINE PER NIGHT at a desktop width.
   Below 64rem its cells wrap as before and the table scrolls in `.table-wrap`.
   The send button carries the email state; its date is the smaller words.
   The *questions changed* note is the one second line a row may have. */
/* WGT_22111 — 👤 Alex, walking it: "center all the labels on top of each column". */
.data-table.question-day-nights th { text-align: center; }

.question-day-nights .send-when {
    font-size: .85em;
    font-weight: 400;
    opacity: .8;
}

@media (min-width: 64rem) {
    .data-table.question-day-nights td.wrap { white-space: nowrap; }
    .data-table.question-day-nights td .send-note {
        margin: .25rem 0 0;
        white-space: normal;
    }
}

.portal-main:has(.field--wide) > .lede,
.portal-main:has(.field--wide) > .panel > h2,
.portal-main:has(.field--wide) > .panel > p:not(.wg-alert) {
    text-align: center;
}

/* --- RV88 — headings and prose centre SITEWIDE (2026-08-14) ----------------

   👤 Alex, 2026-08-14 8:40pm, answering RV79's §1: "one rule naming headings
   and prose plus a short opt-out list for long-form entry (checklists,
   question sets, note fields)." Four screens had by then shipped the SAME
   page-scoped rule four times (RV75 albums, RV77 published, RV78 question-set,
   and RV76's card variant) and RV79's survey found ~20 of the 24 remaining
   screens wanting it. A rule written twenty-four times is a sitewide rule with
   extra steps, and — the reason he was given for preferring it — "which
   screens got the marker" is invisible in a screenshot, so the codebase drifts
   silently. It had already drifted that way once by accident: see RV19's
   `.lede:has(> a:only-child)` note near the top of this file.

   🛑 THIS IS NOT THE SITEWIDE CENTRING RV19 REFUSED, and the difference is the
   whole reason it is safe. RV19 rejected `.portal-main { text-align: center }`,
   which CASCADES into `.data-table`, `.detail-list`, form labels and the rota
   grid. This selector cascades into nothing: it names heading and paragraph
   elements at a known depth under three container idioms and reaches no table,
   no list, no label and no input. RV17's measured table exclusion — re-tested
   on albums.php's 193 real album names and centring nothing — STANDS, and
   `.data-table th/td { text-align: left }` above defends it explicitly anyway.

   🛑 IT KNOWINGLY REVERSES RV75's LEAK TEST. RV75 rendered activities.php — a
   `.panel` screen with an `<h2>` and prose and no marker — and proved it did
   NOT pick the rule up. Under one sitewide rule it now does, deliberately.
   ⚠️ THE TEST INVERTS RATHER THAN DISAPPEARING: what has to be proved now is
   that the OPT-OUTS stayed `start`. That is what the second rule below is for,
   and temporary/rv88/measure-after-*.txt is where it was proved.

   THE THREE CONTAINER IDIOMS (RV79 §3) are why this is not one line. `.panel`
   is a direct child of `.portal-main` on eleven screens; index.php, venues,
   equipment, night, hosts?host= and event.php use `<details class="wg-section">`
   instead; reports.php nests its sections inside `.report-sections` and
   event.php inside `.event-detail`. Miss one and that screen silently keeps
   its old look, which is the drift this rule exists to stop.

   🛑 THE `>` COMBINATOR IS LOAD-BEARING, NOT TIDINESS — it is what keeps the
   rule out of long-form entry and out of forms. checklist.php's steps are
   `.step-list > li > span`, question-set.php's set is a `<textarea>`, and
   event-add.php's shift box (FINISHED at RV70/RV72 — do not re-open it) nests
   three levels deep inside a `<form>`. None is a direct child of a container
   named here, so none is reached. RV75 recorded the same property: "that is
   not luck, it is why the selector is `> .panel >` and not `.panel`."

   🛑 WHAT IS DELIBERATELY NOT IN IT, each measured on 2026-08-14:
   · `.wg-section > summary h2` — the accordion bar's own heading. 🛑 SETTLED BY
     ALEX 2026-08-15 AND IT IS A SITEWIDE RULE, NOT A PER-SCREEN CALL: the
     header row you click to open an accordion keeps its arrow at the left and
     its text left-aligned. Centring is for things OUTSIDE the accordion and for
     the CONTENT inside it — never the `<summary>`. Do NOT centre one screen's
     bars to match its page; that is the reversal this note forbids, on all
     seven screens with the pattern (index, reports, venues, equipment, night,
     hosts?host=, event). The measurement behind it: `text-align` here is a NO-OP
     because the summary is `display: flex` with a chevron `::before` pinned at
     its left edge, so the h2 is a flex item sized to its text (index.php's are
     224–452px wide inside a 569px column); centring needs `justify-content` on
     the summary, which carries the CHEVRON into the middle with the text. The
     spacer variant — heading centred, chevron pinned — is REJECTED, not
     deferred: markup on all seven screens for an alignment nobody asked for.
     No declaration was ever added; left is what has always been live. It is why
     index.php centres its prose and not its seven headings today.
   · form field help — `.setting-help`, `.field > p.muted` and friends. RV19
     left form labels and inputs alone sitewide; RV78 centred question-set's
     "only within this page's scope" and said it must not go sitewide from
     there. The `>` scoping above already excludes them; this note is so the
     next reader knows it was a decision and not an oversight.
   · `.wg-alert` — shared error/ok chrome rendered identically on every screen,
     excluded here exactly as RV75 and RV77 excluded it.
   · controls — `.button-row` is centred sitewide by RV19 already, and the
     filter chips stay put. ⚠️ index.php's "Fix this →" and "See who →" ARE
     bare `<p><a>` controls and DO centre under this rule; that is consistent
     with `.button-row` rather than an exception to it. */
.portal-main > .lede,
.portal-main > h2,
.portal-main > h3,
.portal-main > p:not(.wg-alert),
.portal-main > .panel > .lede,
.portal-main > .panel > h2,
.portal-main > .panel > h3,
.portal-main > .panel > p:not(.wg-alert),
.portal-main > .wg-section > .lede,
.portal-main > .wg-section > h3,
.portal-main > .wg-section > p:not(.wg-alert),
.portal-main > .report-sections > .wg-section > .lede,
.portal-main > .report-sections > .wg-section > h3,
.portal-main > .report-sections > .wg-section > p:not(.wg-alert),
.portal-main > .report-sections > .wg-section > .panel > h2,
.portal-main > .report-sections > .wg-section > .panel > h3,
.portal-main > .report-sections > .wg-section > .panel > p:not(.wg-alert),
.portal-main > .event-detail > .lede,
.portal-main > .event-detail > .panel > h2,
.portal-main > .event-detail > .panel > h3,
.portal-main > .event-detail > .panel > p:not(.wg-alert),
.portal-main > .event-detail > .wg-section > h3,
.portal-main > .event-detail > .wg-section > p:not(.wg-alert),
/* RV190 — `.confirm-box`'s own heading and prose join the ONE rule (Alex,
   2026-08-29, of the merge box: "center this text"). 🛑 EXTENDING THIS
   SELECTOR IS THE SANCTIONED MOVE AND A PAGE RULE IS NOT: RV88 settled that a
   rule written per screen is a sitewide rule with extra steps, and the box is a
   shared component with callers on more than one screen.
   🛑 `> h3` AND `> p` ONLY, NEVER `.confirm-box { text-align: center }` —
   `.confirm-list` is on this file's deliberately-left-aligned list, and a box
   rule would centre it silently the first time a confirm box carried one. The
   `>` combinator is the safety here exactly as it is above.
   WGT_21631 — `> h2` and `> .confirm-count` join it (👤 Alex, 2026-10-17, of
   the host's *Come off this night?* step: "center the text headers"). Same
   `>` guard: `.confirm-list` stays left. */
.portal-main > .confirm-box > h2,
.portal-main > .confirm-box > h3,
.portal-main > .confirm-box > p:not(.wg-alert),
.portal-main > .confirm-box > .confirm-count {
    text-align: center;
}

/* The opt-OUT list Alex asked for in the same answer — long-form ENTRY stays
   left. 🛑 IT IS A GUARD, NOT A REPAIR, and saying so honestly matters: the
   `>` scoping above means the rule never reaches any of these today, measured
   on checklist.php, question-set.php and every screen carrying a textarea.
   What it buys is that a future screen putting a step body or a note field
   inside a `.panel` as a direct child cannot silently pick centring up. The
   line Alex drew, in his own words: "don't center the text inside of the set
   text area but center all the text outside of that."
   ⚠️ It must stay AFTER the rule above — same specificity, so source order is
   what decides, and moving it up would silently disarm it.
   ⚠️ The third thing Alex named — "note fields" — is NOT a class here. Every
   note field in the portal is a `<textarea>`, named below by element, and a
   `.note-field` selector was written and then removed rather than shipped: a
   rule matching no markup is the mirror of RV79 §6b's `class="pill"`, and it
   reads to the next editor as though such a class exists. */
.portal-main .step-list,
.portal-main .step-list p,
.portal-main .step-list .step-text,
.portal-main textarea {
    text-align: start;
}

/* RV146 (2026-08-17) — host-add.php's optional-fields divider centres, and it
   is ONE PARAGRAPH ON ONE SCREEN. Alex: "Alter 'The rest is optional — for a
   full record ready before they arrive.' to say 'The rest is optional and can
   be filled in by the host instead.' but center that text."

   🛑 WHY THIS IS NOT ONE MORE SELECTOR ON RV88's SITEWIDE RULE. That rule
   reaches a `<p>` that is a direct child of `.portal-main`, `> .panel`,
   `> .wg-section` and the two nested idioms. This paragraph sits INSIDE the
   `<form>` — `.panel > form > p` — because it is a divider between the
   required email and the optional fields and cannot leave the form without
   leaving its place in the reading order. Widening the sitewide rule to
   `form > p` would centre field help portal-wide, which is exactly the set
   RV19 refused and RV88 re-affirmed as excluded; RV117's comment above names
   host-add.php among the screens that widening would wrongly reach.

   ✅ NO MARKER CLASS WAS MINTED — RV88 forbids that and it was not needed.
   `id="add-host"` is this screen's own section id, written in host-add.php's
   markup, and grepped across the whole of Code/ it appears on that one line.
   🛑 It is NOT a portalSectionStart() slug, so RV107's "a bare section id
   reaches every screen registering that slug" trap does not apply — nothing
   registers `add-host` as a section.

   ⚠️ Scoped to the form's DIRECT `p.muted` children on purpose. The screen's
   other two muted paragraphs are untouched by it: the panel blurb above the
   form is already centred by the sitewide rule (it IS a `> .panel > p`), and
   any future `.field > p.muted` field help is a grandchild, so it stays left
   with every other screen's. */
.portal-main > #add-host > form > p.muted {
    text-align: center;
}

/* WGT_19303 (2026-10-06) — who-gets-alerts.php centres the headings and prose
   INSIDE its one form. 👤 Alex: "let's center all the text in the middle area.
   Some of it's left right now." Its *Each message on its own* and *Messages
   from the website* sub-headings and their paragraphs sit inside the <form>
   (the two tables are ONE form — the page's save branch says why), so the
   sitewide rule above cannot reach them: RV146's case exactly, and RV146's
   shape — keyed on the form's own id, `#alerts-form`, written once in that
   page's markup, never a portalSectionStart() slug.
   ⚠️ DIRECT children only: the `.data-table` cells stay left (RV17), and the
   `.setting` block above the tables is already centred by RV117's rule. */
.portal-main #alerts-form > h3,
.portal-main #alerts-form > p:not(.wg-alert) {
    text-align: center;
}

/* WGT_19303 — Alex: "break that up into smaller sentences on their own lines."
   ON THEIR OWN LINES, fit or not, so the `.sentences` spans go block here, the
   same override `.venue-page-centred` and `.vis-help-notes` carry. */
#alerts-intro.sentences > span { display: block; }

/* WGT_19303 — each "also send to" box is a drop-down of the admin and manager
   logins, with an email box under it that shows only for *Someone else…*.
   0,2,1 on the email box so it out-ranks `.setting-input input[type="email"]`'s
   18rem minimum, which would push a table cell wider than the drop-down. */
.alert-pick {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .4rem;
}

.alert-pick > select,
.alert-pick > input[type="email"] {
    width: 18rem;
    min-width: 0;
    max-width: 100%;
    margin: 0;
}

.alert-pick > input[hidden] { display: none; }

/* RV117 (2026-08-15) — A FOURTH CENTRING SHAPE, and it is the settings screens
   ONLY: settings.php and host-settings.php centre their FORM CONTROLS as well
   as their prose. Alex: "centre all of the input form elements and basically
   literally everything on the page ... other pages might be a little
   different, but settings will just look better with everything centered."

   🛑 WHY THIS IS NOT ONE MORE SELECTOR ON THE SITEWIDE RULE ABOVE. RV88 says
   an uncentred screen means the sitewide rule is missing that screen's
   container idiom. Measured before this shipped, that is NOT the case here:
   both screens are `<section class="panel">` direct children (their
   portalSectionNav() is called with accordion=false), so every h1, .lede,
   panel h2 and panel blurb on them already read `text-align: center` with
   TEXTMID 0. Nothing is missing from the sitewide rule. What Alex asked for
   on these two is exactly the set RV19 refused sitewide and RV88 re-affirmed
   as excluded — form LABELS, INPUTS and FIELD HELP. Widening the sitewide rule
   to reach those would centre every form in the portal, including
   event-add.php's shift boxes (finished at RV70/RV72, explicitly not to be
   re-opened), host-add.php, venue-add.php and checklist.php. That is a
   reversal of a settled decision, not an extension of it. So the rule is
   page-scoped, and this comment is the "say why" RV117 asked for.

   🛑 NO MARKER CLASS WAS MINTED — RV88 forbids that and it was not needed.
   `class="setting"` is F16's existing settings-row wrapper: it carries real
   layout (padding, border-bottom above) and, grepped across the whole of
   Code/, it appears on settings.php and host-settings.php and NOWHERE else.
   The `-` classes are not it: `.setting-check` and `.setting-label` are reused
   on event.php, event-add.php and signup.php, which is why the scope hangs off
   the BARE class and why event-add.php is this task's leak test.
   ⚠️ THE ONE RISK, stated rather than hidden: a third screen that adopts
   `class="setting"` picks this up silently. It is the same class of risk RV88
   objected to, bounded here to one class with two callers instead of a
   free-floating marker anyone can paste. If a third screen ever wants the
   settings row WITHOUT the centring, that is the moment to split them.

   🛑 THE BOX MOVES, THE TYPED TEXT DOES NOT. There is no `text-align: center`
   on an input, select or textarea anywhere below, and adding one is the
   forbidden move: a value that re-centres itself character by character while
   you type is unusable, and RV117's own brief says a centred input means
   centred ON THE PAGE. `.setting-input` and `.setting-check` are already
   `display: flex`, so `justify-content` is what moves them — measured before,
   the number boxes sat 492–503px left of centre on settings.php and 215px left
   on host-settings.php, with `text-align` powerless over them exactly as
   RV107 found for a block-level <button>.
   (The create-account form this once excused was removed by WGT_19300, 2026-10-06.)
   ⚠️ `.wg-alert`, `.confirm-list` and `.data-table` cells are deliberately
   absent — shared chrome and RV17's measured table exclusion, both unchanged
   on these screens as on every other. */
.portal-main:has(.setting) .setting-label,
.portal-main:has(.setting) .setting-help,
.portal-main:has(.setting) .setting-meta,
.portal-main:has(.setting) .preview h3,
.portal-main:has(.setting) .preview > p {
    text-align: center;
}

.portal-main:has(.setting) .setting-input,
.portal-main:has(.setting) .setting-check {
    justify-content: center;
}

/* RV139 — venue-edit.php, Alex: "on the venue logo and the location spec
   sheet areas just center all the text including the replace a logo and the
   choose file and replace the spec sheet and remove logo and remove spec
   sheet." `text-align` inherits, so setting it once on the section reaches
   the muted copy, the preview image (inline, centres inside its own `<p>`),
   the field label and the file input's own text — all without a second
   selector per element.

   🛑 RV165 — rekeyed off `.venue-asset-section` (a hand-wrapped `<div>` in
   venue-edit.php around its `portalSectionStart('logo')` section),
   never bare `#logo`/`#spec` id selectors. RV139 used the ids because they
   were provably unique to this one page at the time and `portal.php` (which
   emits them) was out of its grant; `portal.css` is now in this task's grant
   so the documented escape applies — a class this file owns, not an id
   `portal.php` owns, so a second page ever registering `#logo`/`#spec` can
   no longer collide with this rule.

   This is a deliberate exception to the sitewide rule a few hundred lines up
   that leaves form `.field`/`.field-row` labels and inputs uncentred
   ("numbers, labels and facts-in-a-list read worse centred") — Alex asked
   for these two specific sections by name, not for the site's forms in
   general, and nothing else on this page changes.

   ✅ WGT_17222 (👤 Alex, 2026-09-27: *"Also center the photo selection and text
   parts"*) — the venue page's *Photos* section is wrapped in the same class, so
   its two *Choose a photo* uploads centre exactly as the logo's does. */
.venue-asset-section {
    text-align: center;
}

/* `.button-row` already centres the Replace/Upload submit buttons sitewide
   (RV19) via flex, not text-align — untouched here. `.link-button` (Remove
   logo / Remove spec sheet) sits alone in its own form, outside any
   `.button-row`, and inherits `display: block; width: auto` from the base
   `button` rule — a block box hugs the left edge regardless of a parent's
   text-align (RV107 measured this exact failure on activity.php). Making it
   inline-block is what lets the inherited centring reach it. */
.venue-asset-section .link-button {
    display: inline-block;
}

/* The sitewide file-input rule forces `width: 100%`, which — being an exact
   fill of the container — leaves nothing for text-align to centre. Shrink it
   to content width here only, so the "Choose File" control itself sits in
   the middle of the section like everything else in it. */
.venue-asset-section input[type="file"] {
    width: auto;
}

/* --- My expenses (PY5) — host-facing, phone-first, and the only money on the
   portal ---------------------------------------------------------------------

   Cards rather than a table, for the third time and the same reason `.kit-card`
   and `.av-card` are: a table on a phone is a sideways scroll and a mis-tap.
   🛑 Kept as its own `.exp-*` block rather than reusing `.kit-*` — those classes
   are named for the equipment screen, and a rule called `.kit-meta` styling a
   dollar figure is the kind of thing that gets "tidied" into a shared class and
   takes one of the two screens with it.

   ⚠️ The amount is the biggest thing on the card ON PURPOSE. Every other host
   screen leads with a name or a date; this one leads with money, because a host
   scanning the list is checking a total rather than looking something up. */

.exp-card {
    margin: 0 0 .85rem;
    padding: .85rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.exp-card:last-child { margin-bottom: 0; }

.exp-amount {
    display: flex;
    flex-wrap: wrap;
    gap: 0 .5rem;
    align-items: baseline;
    margin: 0;
    font-size: 1.25rem;
    font-weight: 700;
}

/* The category rides beside the figure rather than under it — one line saved on
   every card, on a screen that is mostly cards. */
.exp-kind {
    font-size: .95rem;
    font-weight: 600;
    color: var(--wg-muted);
}

.exp-meta {
    margin: .2rem 0 .6rem;
    font-size: .95rem;
    color: var(--wg-muted);
}

/* Actions fold away, one summary line each — `.kit-more`'s shape, so the two
   host screens behave identically under a thumb. */
.exp-more { margin: .35rem 0 0; }

/* The summary pill is `.kit-more > summary`'s own selector list (search
   `WGT_15704`) — no copy here, so the two screens cannot drift apart again. */

.exp-more form { margin: 0 0 .25rem; }

.exp-button {
    width: 100%;
    min-height: var(--wg-tap);
    font-size: 1rem;
}

/* The period picker. A GET form, so no token and no PRG — the URL is the state,
   which is also what makes the download link below shareable. */
.exp-range {
    margin: 0 0 1rem;
    padding: .85rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

/* The per-category tally. A grid row rather than a table: label and figure on
   one line, the count spanning underneath.
   ⚠️ Empty categories are rendered too (the model returns every one of them), so
   this has to look deliberate at $0.00 rather than look like a bug. */
.exp-totals {
    margin: 0 0 1rem;
    padding: .35rem .85rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.exp-total-line {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0 .75rem;
    margin: 0;
    padding: .5rem 0;
    border-bottom: 1px solid var(--wg-line);
}

.exp-total-line:last-child { border-bottom: 0; }

.exp-total-label { font-weight: 600; }

.exp-total-value {
    font-weight: 700;
    /* Tabular figures so a column of dollar amounts lines up on the decimal —
       which is the whole reason a host scans this block. */
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.exp-total-note {
    grid-column: 1 / -1;
    font-size: .9rem;
    color: var(--wg-muted);
}

.exp-total-line.is-total {
    margin-top: .25rem;
    border-top: 2px solid var(--wg-line);
    border-bottom: 0;
    font-size: 1.1rem;
}

/* The download. A full-width tap target rather than a text link: it is used on a
   laptop at tax time and on a phone the rest of the year, and 48px is the floor
   either way (SC14). Ink and not accent — RV65's finding on the reports exports,
   that a red export link reads as a warning rather than as a thing to click. */
.exp-download-line { margin: 0 0 1rem; }

.exp-download {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--wg-tap);
    padding: 0 .85rem;
    color: var(--wg-ink);
    font-weight: 600;
    text-align: center;
    text-decoration: underline;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.exp-download:hover,
.exp-download:focus-visible { color: var(--wg-muted); }

/* The add/amend field set. ⚠️ The explicit `[hidden]` rule is not belt-and-braces
   — the inline script at the foot of the screen sets the attribute, and a
   `display` declaration from any descendant selector beats the UA sheet's
   `[hidden]`, which is how a "hidden" field comes back on one screen only.
   🛑 WITHOUT THE SCRIPT NOTHING IS HIDDEN AND THE FORM STILL WORKS — every field
   and every help line shows at once and the server decides which it needs. */
.exp-fields > .field { margin: 0 0 .85rem; }

.exp-fields [hidden] { display: none; }

/* --- Bar payments (PY7) — the income half, the third panel on the same screen
   ----------------------------------------------------------------------------

   🛑 ITS OWN BLOCK, NOT `.exp-*` REUSED — the same call `PY5` took against
   `.kit-*`, and it matters more here than it did there. The two panels sit on
   ONE page, so a shared `.exp-card` would make them look like one list of one
   kind of money; they are a deduction and an income, they are never added
   together, and looking different is part of saying so
   (`CLAUDE/specs/payments.md` §1).

   ⚠️ The card is a NIGHT, not a payment. Most of them start with nothing written
   against them, so the empty state has to look like a question waiting rather
   than like a row that failed to load — `.pay-amount.is-blank` is that, and it
   is deliberately quiet rather than red: a host who has not got round to a night
   has done nothing wrong. */

.pay-card {
    margin: 0 0 .85rem;
    padding: .85rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.pay-card:last-child { margin-bottom: 0; }

.pay-amount {
    display: flex;
    flex-wrap: wrap;
    gap: 0 .5rem;
    align-items: baseline;
    margin: 0;
    font-size: 1.25rem;
    font-weight: 700;
}

/* Not paid yet: a state, not a figure. Same size as the money it stands in for,
   so a column of cards scans as one list rather than two. 🛑 NEVER a dollar
   amount — nothing in this app knows what a bar pays (`payments.md` §2). */
.pay-amount.is-open { color: var(--wg-accent); }

/* Nothing written down. Muted rather than accented: it is the number the panel
   exists to shrink, but it is a gap in the host's own paperwork and not an
   error, and colouring it like one makes the list feel like a telling-off. */
.pay-amount.is-blank {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--wg-muted);
}

/* The method rides beside the figure, exactly as the category does on an
   expense card — one line saved on every card, on a panel that is mostly cards. */
.pay-kind {
    font-size: .95rem;
    font-weight: 600;
    color: var(--wg-muted);
}

.pay-meta {
    margin: .2rem 0 .6rem;
    font-size: .95rem;
    color: var(--wg-muted);
}

.pay-more { margin: .35rem 0 0; }

/* Summary pill: shared with `.kit-more > summary` (search `WGT_15704`). */

.pay-more form { margin: 0 0 .25rem; }

.pay-button {
    width: 100%;
    min-height: var(--wg-tap);
    font-size: 1rem;
}

/* The tally. ⚠️ Two of its three lines carry a COUNT OF NIGHTS in the slot the
   expense tally puts money in, which is why the value column is not forced to
   `nowrap` the way `.exp-total-value` is: "3 nights" may wrap on a narrow phone
   and a wrapped count is readable, where a wrapped dollar figure is not. */
.pay-totals {
    margin: 0 0 1rem;
    padding: .35rem .85rem;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
}

.pay-total-line {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0 .75rem;
    margin: 0;
    padding: .5rem 0;
    border-bottom: 1px solid var(--wg-line);
}

.pay-total-line:last-child { border-bottom: 0; }

.pay-total-label { font-weight: 600; }

.pay-total-value {
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.pay-total-note {
    grid-column: 1 / -1;
    font-size: .9rem;
    color: var(--wg-muted);
}

.pay-total-line.is-total {
    margin-top: .25rem;
    border-top: 2px solid var(--wg-line);
    border-bottom: 0;
    font-size: 1.1rem;
}

/* 🛑 The explicit `[hidden]` rule is the same trap `.exp-fields` carries: the
   inline script sets the attribute, and a `display` declaration from any
   descendant selector beats the UA sheet's `[hidden]`.
   ⚠️ WITHOUT THE SCRIPT NOTHING IS HIDDEN AND THE FORM STILL WORKS — every field
   and both help lines show at once, and `paymentValidate()` blanks an unpaid
   row's amount, date and method whatever was posted with them. */
.pay-fields > .field { margin: 0 0 .85rem; }

.pay-fields [hidden] { display: none; }

/* --- Invoicing (PY3) -----------------------------------------------------

   Two rules, and deliberately no more: `/portal/invoices.php` is built on the
   generic `.panel` / `.table-wrap` / `.data-table` / `.detail-list` /
   `.button-row` set, so a third money screen does not arrive with a third
   private stylesheet the way `.tonight-*` and `.equipment-row` once did.

   🛑 A DOLLAR FIGURE IS RIGHT-ALIGNED AND TABULAR, and that is not decoration:
   the point of a column of money is that the digits line up, and `.data-table
   td` is `text-align: left` for every other cell. `.pay-total-value` reached the
   same conclusion for the payment tally's grid — this is that rule for a table
   CELL rather than a grid cell, and neither should be "harmonised" into the
   other: one is a total, this is a column. */
.data-table td.inv-money {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* Which row of the list the panel below it is showing. ⚠️ The SAME token
   `.data-table tr.row-link:hover` uses, so the open row reads as the one being
   pointed at rather than as a new colour meaning a new thing. */
.data-table tbody tr.is-open td { background: var(--wg-panel); }

/* RV184 (2026-08-29) — hosts.php's Host applications queue: a <select> and its
   Confirm button sitting INSIDE table cells. Alex: "make the style of the
   dropdown of existing hosts different so it's the same text height as the rest
   of the row instead of being two or three times taller".

   The base `select` rule is built for a form field on its own line — width
   100%, `min-height: var(--wg-tap)` (48px), .6rem padding and a 1rem bottom
   margin. In a .data-table cell whose text is .95rem that is roughly three
   times the height of the row it sits in, and the margin pushes the row taller
   still. This class undoes exactly those four properties and nothing else.

   🛑 `font-size` DELIBERATELY STAYS AT 1rem — it is NOT part of the height
   problem, and the base rule's comment says why it cannot move: iOS zooms the
   whole page when a control under 16px takes focus. The row's own text is
   .95rem, so the select's glyphs stay a hair larger than the cell's; that is
   the trade the tap-zoom note already decided and this class does not reopen
   it. Height is what was asked for and height is what changed.

   ⚠️ A CLASS, NOT `#duplicates select` — a rule keyed on a section-slug id
   breaks the moment the section is renamed or reused, and this file already
   carries that lesson elsewhere. */
.app-select {
    display: inline-block;
    width: auto;
    max-width: 22rem;
    min-height: 0;
    margin: 0;
    padding: .2rem 1.6rem .2rem .5rem;
    border-radius: 6px;
}

/* RV184 — the Confirmed / Unconfirmed badge sits BESIDE the address, not under
   it (Alex: "there is plenty of space on a laptop-sized screen still"). The gap
   is the only thing this rule adds; .pri-tag carries no margin of its own
   because everywhere else it is the sole occupant of its cell. */
.data-table td.app-email .pri-tag {
    margin-left: .5rem;
}

/* The Confirm button is in its own last column so it lands at the right edge of
   the table (Alex, same note). It is tied to the form in the previous cell by
   `form="app-<id>"`; there is no styling relationship between them, which is
   the whole reason the split is safe. */
.data-table td.app-confirm {
    text-align: right;
}

/* Check-in's headcount panel (2026-09-13). 👤 Alex, on the phone: centre
   "15 players recorded.", the field label and the input.

   ⚠️ A DELIBERATE EXCEPTION, scoped to ONE field — the `.player-find` shape
   (portal_centring.md). RV19 leaves form labels and inputs left, and RV88's
   sitewide rule excludes `.wg-alert p`; both still hold everywhere else.

   🛑 KEYED ON THE INPUT'S OWN ID `#headcountValue`, NOT ON `#headcount`.
   `headcount` is a portalSectionStart() slug — RV107's trap — and until this
   change the input shared that id with its own section.

   👤 THE TYPED DIGITS ARE CENTRED TOO — Alex asked again after seeing the box
   centred with the number at its left edge (2026-09-13). That is the THIRD
   exception to portal_centring.md's ban on centring typed text, and that file
   said a third should retire the ban rather than add to it. It is retired
   there. */
/* ⚠️ WGT_18302 — the LABEL half of this rule (`.field:has(> #headcountValue) >
   label`, and the class twin below) is gone: the base rule centres every field
   label. The saved message and the box's width are still this panel's own. */
:is(.panel, .wg-section):has(#headcountValue) > .wg-alert p { text-align: center; }

/* 👤 Alex dislikes a line wrapping one or two words by themselves; at 390px the
   label left "the bar" alone. `balance` evens the two lines out, and a browser
   without it simply wraps as before. */
.field:has(> #headcountValue) > label { text-wrap: balance; }

.field > #headcountValue {
    display: block;
    max-width: 8rem;
    margin-inline: auto;
    text-align: center;
}

/* 🛑 WGT_15205 — A SHIFT BUNDLING TWO ACTIVITIES DRAWS ONE BOX PER ACTIVITY, so
   those inputs are `headcountValue-<activityId>` and every `#headcountValue`
   rule above stops matching them. `.headcount-input` is carried by EVERY
   headcount input, the single one included, so one box and two cannot drift
   apart.

   ⚠️ THE ID RULES ABOVE ARE LEFT EXACTLY AS THEY ARE, deliberately. The
   single-box panel is the one 👤 Alex measured on his phone (2026-09-13), and
   re-keying it on the class would be a change to the screen every night on every
   box actually renders — measured 2026-09-21: 0 of 77 live shifts bundle more
   than one activity. These rules are additive and say the same thing. */
:is(.panel, .wg-section):has(.headcount-input) > .wg-alert p { text-align: center; }
.field:has(> .headcount-input) > label { text-wrap: balance; }

.field > .headcount-input {
    display: block;
    max-width: 8rem;
    margin-inline: auto;
    text-align: center;
}

/* WGT_14488 (👤 Alex, 2026-09-17: "the buttons stack on each other and look
   ugly") — host-profile.php's Cell number panel. Verify and Send another code
   are one form's two submit buttons on ONE .button-row: Verify is the accent
   primary, Send another code borrows .button-quiet, and the row's own
   flex-wrap puts them side by side wherever two 9rem buttons fit (390px
   included) and wraps rather than overlaps where they do not.
   *Keep <number>* in change mode is a link that must read as the quiet button
   beside Save and text me a code, so it takes the base button's box. */
.cell-number-panel .button-row a.button-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 9rem;
    min-height: var(--wg-tap);
    padding: .7rem 1rem;
    font-size: 1rem;
    font-weight: 600;
    border-radius: 8px;
    text-decoration: none;
}
/* *Wrong number? Change it* — a quiet text action under the panel's controls,
   centred like the .button-row above it, with a full tap target. */
.cell-number-panel .cell-change-row {
    margin-top: .5rem;
    text-align: center;
}
.cell-number-panel .cell-change-row .link-button {
    display: inline-block;
    min-height: var(--wg-tap);
}
/* The panel is a focus target after an in-place swap (host-profile.js), so it
   gets no focus ring of its own — the same call as .venue-owner-box. */
.cell-number-panel:focus,
#contactDetails:focus,
#notifications:focus { outline: none; }

/* 👤 Alex, 2026-09-17: centre everything in host-profile.php's Photo panel
   (the label, the file picker and its note) and the Email me / Text me
   checkboxes in Notifications, to match the centred panels around them. */
.host-photo-panel .field { text-align: center; }
.host-photo-panel .field label { display: block; }
.host-photo-panel input[type="file"] { display: block; width: auto; max-width: 100%; margin-inline: auto; }
.notify-choices .setting-check { justify-content: center; }
.notify-choices .setting-check + .setting-check { margin-top: .5rem; }
/* 👤 Alex, 2026-09-17: in host-profile.php's Cell number panel, centre the
   notices ("We sent a code…", "Your cell number is verified.", the refusals),
   the "Your code" label and the code typed into its box. */
.cell-number-panel .wg-alert { text-align: center; }
.cell-number-panel .field label { display: block; text-align: center; }
.cell-number-panel #phoneCode { text-align: center; }

/* 👤 Alex, 2026-09-17 — THE SENTENCE-BREAK RULE. A two-sentence line must break
   BETWEEN its sentences, never in the middle of the second one: "Nothing on your
   schedule in the coming weeks. / If that doesn't look right, tell Graham."
   Each sentence is its own <span>, and an inline-block is an unbreakable unit to
   the line breaker until it has to be broken, so the space between the spans is
   the first break offered. 🛑 It is NOT a <br>: at a width where both sentences
   fit, they stay on one line, and a single very long sentence still wraps inside
   its own span rather than running off the screen.
   ⚠️ The same rule and class live in assets/css/style.css for the public pages.
   The rule is written up in CLAUDE/specs/design_guide.md. */
.sentences > span { display: inline-block; }

/* --- The archive tray at the foot of hosts.php (👤 Alex, 2026-09-17) -------

   A quiet drawer, not a call to action: it is the last thing on an admin
   screen and it must not out-shout the roster above it. So it borrows the
   `.panel` tokens rather than any accent — the fill is --wg-panel, the rule
   --wg-line, and the only colour anywhere is the ink the page already uses.

   🛑 It sits BELOW the roster, so it needs a top rule of its own; without one
   a panel-coloured box floats against a white page with nothing to say where
   the list ended. The rule is the box's own border, not an <hr>.
   ⚠️ The glyph is an inline SVG using `currentColor`, so it cannot drift from
   the text beside it in either state. */
.host-archive {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .9rem 1.5rem;
    margin: 2.25rem 0 1.5rem;
    padding: 1rem 1.15rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 10px;
    text-align: left;   /* the sitewide centring rule would break the two-column read */
}

/* The open state earns ONE difference and no more: a left edge in the mid
   tone, so "you are looking at the archive" is visible without a second
   colour entering the page. */
.host-archive.is-open { border-left: 3px solid var(--wg-line-mid); }

.host-archive-what { display: flex; align-items: flex-start; gap: .75rem; min-width: 0; }
.host-archive-glyph { flex: none; margin-top: .15rem; color: var(--wg-muted); }
.host-archive-words { min-width: 0; }

.host-archive-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0 0 .15rem;
    font-size: 1rem;
    line-height: 1.3;
}

/* The count is a fact, so it reads as one: a small slab in the page's own
   paper, tabular so 3 and 13 sit on the same edge. */
.host-archive-count {
    font-variant-numeric: tabular-nums;
    font-size: .85rem;
    font-weight: 600;
    color: var(--wg-muted);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 999px;
    padding: .05rem .55rem;
}

.host-archive-note { margin: 0; color: var(--wg-muted); font-size: .92rem; }

/* The button keeps `.row-action .button-quiet` — this only stops it stretching
   and gives the hover somewhere to go. The lift is 1px: an admin footer does
   not need a flourish, it needs to look pressable. */
.host-archive-go {
    flex: none;
    display: inline-block;
    transition: border-color .15s ease, transform .15s ease;
}
.host-archive-go:hover,
.host-archive-go:focus-visible { border-color: var(--wg-line-mid); transform: translateY(-1px); }

@media (prefers-reduced-motion: reduce) {
    .host-archive-go { transition: none; }
    .host-archive-go:hover, .host-archive-go:focus-visible { transform: none; }
}

/* Phone: the tray stacks and the button goes full width, the same shape every
   other action on this screen takes at that width. */
@media (max-width: 560px) {
    .host-archive { flex-direction: column; align-items: stretch; }
    .host-archive-go { width: 100%; text-align: center; }
}

/* WGT_14581 (2026-09-18) — the click-to-open suggestion list assets/js/combobox.js
   builds around an `<input list … data-combobox>` (Add a host's roster match).
   The wrapper takes over the input's bottom margin so the list can sit flush
   under the box (top: 100%) instead of 1rem below it. */
.combobox {
    position: relative;
    margin: 0 0 1rem;
}
/* WGT_18965 — combobox.js wraps the input, so the label-then-input centring rule in the Forms
   block no longer reaches it outside a `.field`; centre it here like every one-line control. */
.combobox > input { margin-bottom: 0; text-align: center; }
.combobox-list {
    position: absolute;
    z-index: 20;
    top: 100%;
    left: 0;
    right: 0;
    max-height: 16rem;
    overflow-y: auto;
    margin: 2px 0 0;
    padding: .25rem 0;
    list-style: none;
    background: var(--wg-paper);
    border: 1px solid var(--wg-line-mid);
    border-radius: 8px;
    box-shadow: 0 6px 18px rgba(22, 24, 29, .12);
}
.combobox-list[hidden] { display: none; }
.combobox-option {
    min-height: var(--wg-tap);
    display: flex;
    align-items: center;
    padding: .4rem .75rem;
    cursor: pointer;
    color: var(--wg-ink);
}
.combobox-option.is-active { background: var(--wg-info-bg); color: var(--wg-info-ink); }

/* WGT_14591 — Upcoming Nights (`/portal/upcoming-nights.php`): one small table
   per day. At desktop width every day's table takes the panel's full width with
   FIXED column widths, so Time / Event / Venue / Host line up down the whole
   page instead of each day's table sizing itself to its own contents.
   ⚠️ Desktop only. Below 40rem (this file's phone breakpoint) the tables keep
   `.table-wrap`'s ordinary fit-content sizing and scroll inside their panel —
   measured 2026-09-18 at 390: page overflow 0. Scoped by `:has(.upcoming-nights-day)`,
   a class on that page and nowhere else. 🛑 NOT by a wrapper <div>: one was
   tried, and it took the lede and headings out of the `.portal-main > …`
   centring rule above, so the page stopped matching every other screen. */
.upcoming-nights-day { margin: 1.25rem 0 .5rem; }
@media (min-width: 40.01rem) {
    .portal-main:has(.upcoming-nights-day) .table-wrap { width: 100%; }
    .portal-main:has(.upcoming-nights-day) .data-table { width: 100%; table-layout: fixed; }
    .portal-main:has(.upcoming-nights-day) .data-table th:nth-child(1) { width: 18%; }
    .portal-main:has(.upcoming-nights-day) .data-table th:nth-child(2) { width: 24%; }
    .portal-main:has(.upcoming-nights-day) .data-table th:nth-child(3) { width: 30%; }
    .portal-main:has(.upcoming-nights-day) .data-table th:nth-child(4) { width: 28%; }
    .portal-main:has(.upcoming-nights-day) .data-table td { white-space: normal; }
}

/* --- WGT_15727 — event.php's night folds (*Upcoming dates* / *Past dates*) ----
   One night = one `.night-fold`: a closed one is an `<a>` to its own URL, the
   one named in `&night=` is a `<details open>` holding its `.night-line`s
   (`nightPanelLine()`, Code/includes/night_panels.php). Built from the mock
   `temporary/WGT_15655/mock.html`; the `.kit-more` pill inside a line is the
   existing one, unchanged.
   🛑 NO JAVASCRIPT: opening another night is a page load (proposal.md §3d).
   ⚠️ A fold is flagged when it is the `:target` OR holds the old serial-id
   anchor that is (`#night-<id>`, the work queue's link) — the same persistent
   band `.data-table tr:target` drew on the table this replaced. */
.night-list { display: flex; flex-direction: column; gap: .5rem; margin: .5rem 0 1rem; }
.night-fold {
    display: block;
    border: 1px solid var(--wg-line);
    border-radius: 8px;
    background: var(--wg-paper);
    color: var(--wg-ink);
}
.night-fold-link,
.night-fold > summary,
.night-fold--skip {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .9rem;
    padding: .6rem .8rem;
    min-height: var(--wg-tap);
    box-sizing: border-box;
}
.night-fold-link { text-decoration: none; }
.night-fold-link:hover { background: var(--wg-panel); }
.night-fold-link:focus-visible,
.night-fold > summary:focus-visible { outline: 3px solid var(--wg-accent); outline-offset: 2px; }
.night-fold > summary { cursor: pointer; list-style: none; }
.night-fold > summary::-webkit-details-marker { display: none; }
.night-fold-link::before,
.night-fold > summary::before {
    content: '';
    flex: 0 0 auto;
    align-self: center;
    width: .45rem;
    height: .45rem;
    border-right: 2px solid var(--wg-muted);
    border-bottom: 2px solid var(--wg-muted);
    transform: rotate(-45deg);
}
.night-fold[open] > summary::before { transform: rotate(45deg); }
.night-fold[open] > summary { border-bottom: 1px solid var(--wg-line); }
.night-fold--skip { color: var(--wg-muted); background: var(--wg-panel); }
.night-fold.is-past { background: var(--wg-panel); }
.night-fold[open] { background: var(--wg-paper); }
.night-fold:target,
.night-fold:has(.night-anchor:target) { box-shadow: inset 4px 0 0 var(--wg-accent); background: var(--wg-info-bg); }
/* 🛑 WGT_15873 (👤 Alex, 2026-09-23) — A FOLD SHUT BY HAND GOES BACK TO WHITE.
   *"when I close it, the background is still blue instead of reverting back to
   the normal white."* The fragment stays in the URL after the `<summary>` is
   pressed, so `:target` keeps matching a fold that is no longer open. The flag
   is the OPEN night's, so a `<details>` fold without `[open]` drops it. ⚠️ Only
   `details` — a closed fold that is an `<a>` (an old bookmark landing on it) is
   not "shut by hand" and keeps its flag. A shut past night keeps its grey. */
details.night-fold:not([open]):target,
details.night-fold:not([open]):has(.night-anchor:target) { box-shadow: none; background: var(--wg-paper); }
details.night-fold.is-past:not([open]):target,
details.night-fold.is-past:not([open]):has(.night-anchor:target) { background: var(--wg-panel); }
.night-anchor { scroll-margin-top: 1rem; }
.night-date { font-weight: 700; }
.night-who { margin-left: auto; text-align: right; }
.night-pill {
    font-size: .8rem;
    font-weight: 700;
    padding: .05rem .45rem;
    border-radius: 999px;
    border: 1px solid var(--wg-line);
    white-space: nowrap;
}
.night-pill--warn { background: var(--wg-error-bg); border-color: #e6a39b; color: var(--wg-error-ink); }

.night-body { padding: .4rem .8rem .8rem; }
.night-body > .wg-alert { margin: .4rem 0; }
.night-line {
    display: grid;
    grid-template-columns: 7.5rem minmax(0, 1fr);
    gap: .3rem .8rem;
    align-items: start;
    padding: .5rem 0;
    border-bottom: 1px dashed var(--wg-line);
}
.night-line:last-child { border-bottom: 0; }
.night-line > .nl-label { font-weight: 700; color: var(--wg-muted); margin: 0; padding-top: .35rem; font-size: 1rem; }
/* WGT_24611 — a shift's activities under *Host 1* on a date fold. */
.night-line > .nl-label .nl-sub { display: block; font-weight: 400; font-size: .875rem; }
.nl-body { min-width: 0; }
.nl-body > p { margin: 0 0 .3rem; }
.nl-body > p:last-child { margin-bottom: 0; }
.nl-body .kit-more { margin: .3rem 0 0; }
.nl-body .kit-more > p { margin: .3rem 0; }
.nl-inline { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: 0 0 .3rem; }
.nl-inline form { margin: 0; }
.night-line select,
.night-line input[type=date],
.night-line input[type=time],
.night-line input[type=text] { width: auto; min-width: 0; max-width: 100%; margin: 0; }
/* WGT_23844 (👤 Alex, 2026-10-27) — a `.field-centred` select inside a night line
   (*Change who worked it…*) keeps its centring: the `margin: 0` above outranks
   `.field-centred > select`'s `margin-inline: auto` by coming later in the file. */
.night-line .field-centred > select { margin-inline: auto; }
.night-line select.nl-wide { min-width: 12rem; flex: 1 1 12rem; }
.night-line .button-row { margin: .3rem 0 0; }
.night-line .data-table { margin: .3rem 0; }
@media (max-width: 40rem) {
    .night-line { grid-template-columns: minmax(0, 1fr); gap: .25rem; }
    .night-line > .nl-label { padding-top: 0; }
    .night-line select.nl-wide { min-width: 0; width: 100%; }
    .night-who { margin-left: 0; text-align: left; }
}

/* WGT_21617 — 👤 Alex, walk WGT_21559 tabs 7 and 8 (2026-10-17), on *Record for the
   host*: "put a tooltip pop-up after record for the host for that initial paragraph
   of explanation. And put another tooltip next to photos for the host … Then just
   center everything, like the caption input and so forth. Same for tab 8."
   `nightOfficeTip()` in night_panels.php draws both "?"s; `.office-entry` wraps the
   block's body, so every rule below reaches this block and nothing else.

   `.nl-label--tip` is a night-line label with a "?" after it. The bubble hangs
   from the LABEL, not the line — the line is the whole block, so `top: 100%` of it
   would open the bubble under the last form. The label column is 7.5rem, too
   narrow for `.tip-body`'s `100%`, so on a wide screen the bubble opens rightward
   from the label's left edge; in one column it centres under the label. */
.nl-label--tip { position: relative; }
.nl-label--tip > .tip { vertical-align: middle; }
.nl-label--tip .tip-body { left: 0; transform: none; width: min(22rem, calc(100vw - 3rem)); }

/* WGT_22241 — the Helpers fold's "?" beside *Add a helper…* (👤 Alex, 2026-10-21).
   `.tip` itself is `static`, so with no positioned ancestor the bubble opened
   against <body>, ~450px below the "?" (measured on Graham's host). The bubble
   hangs from the whole add-a-helper line, as `.tick-with-tip` does. */
.helpers-add { position: relative; }

/* The two headings with a "?": the heading and the "?" on one centred line, the
   bubble placed against that whole line (as `.tick-with-tip`), so at 390 it can
   never hang off the edge. */
.office-entry-head {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: .25rem;
    margin: .5rem 0 .3rem;
}
.office-entry-head > h3,
.office-entry-head > p { margin: 0; }

/* Centre everything in the block: the host lines, the check-in button row, the
   count boxes and their labels, the "entered by" lines, *For <host>*, *Whose
   photos*, the drop box, the caption, the upload button, the count line and the
   flash. The base rule already centres a `.field`'s label and hint and every
   `.button-row`; these are the parts it does not reach. `.night-line`'s
   `width: auto; margin: 0` leaves a select or text box hugging the left, so
   `margin-inline: auto` moves the BOX — its typed text centres by the base rule. */
.office-entry { text-align: center; }
.office-entry .nl-inline { justify-content: center; }
.office-entry .field > select,
.office-entry .field > input[type=text] { margin-inline: auto; }
.office-entry .photo-drop input[type=file] { width: auto; max-width: 100%; }

/* WGT_21633 — 👤 Alex, walk WGT_21618 tabs 4 and 6 (2026-10-17): "let's put the save
   button immediately right of the input. Just so it's not so many single element
   lines stacked on top of one another." Each count box and the caption box carry
   their button on their own line, the pair centred as one group; it wraps rather
   than overflows on a narrow screen.
   `.office-count` is the count form's `.field`: the label and the "Entered by" line
   take a whole line each, so the box and its button share the line between.
   `.office-pair` holds the caption `.field` and the upload button — the button sits
   outside the field because `photo-upload.js` hides `#sharedCaptionField` once
   photos are picked; the button then centres alone. */
.office-entry .office-count {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: .5rem;
    margin-bottom: 1rem;
}
.office-entry .office-count > label,
.office-entry .office-count > p { flex-basis: 100%; margin-block: 0; }
.office-entry .office-count > .headcount-input { margin: 0; }
.office-entry .office-count > button { width: auto; margin: 0; }
.office-entry .office-pair {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-end;
    gap: .5rem;
}
.office-entry .office-pair > .field { flex: 1 1 11rem; max-width: 20rem; min-width: 0; margin-bottom: 0; }
.office-entry .office-pair > .field > input[type=text] { width: 100%; margin-bottom: 0; }
.office-entry .office-pair > button { width: auto; margin: 0; }
@media (max-width: 40rem) {
    .night-line > .nl-label--tip:has(~ .nl-body > .office-entry) { text-align: center; }
    .nl-label--tip .tip-body { left: 50%; transform: translateX(-50%); width: min(24rem, 100%); }
}

/* ===========================================================================
   🛑🛑 WGT_15866 (👤 Alex, 2026-09-23) — ONE PRESSABLE LINE PER AREA INSIDE A
   SHIFT CARD. *"for each shift, show week one and week two and the activities
   on separate lines. And then clicking any of those opens up just the edit for
   that particular area."*

   🛑 `.shift-area-bar` DELIBERATELY REUSES `.shift-change-bar`'s CHROME — the
   grey `--wg-panel` fill, the 1px `--wg-line` border, the 6px radius, the
   uppercase 700 label. Those bars were 👤 Alex's own ask on 2026-09-22
   (*"make the activity and host labels more noticeable and maybe you do have
   some background highlighting on that row"*), so the look he approved is
   kept and only the behaviour changes: the label now presses.
   ⚠️ `.shift-change-bar` itself is STILL USED — *Retire this shift* keeps it —
   so do not delete that rule when tidying.

   🛑 THE CHEVRON IS `.shift-change-toggle::before`'s, one nesting level in. Two
   different chevrons on one card read as two different kinds of control.
   =========================================================================== */
.shift-area { margin: 0; }
.shift-area + .shift-area { margin-top: .4rem; }
.shift-area > summary {
    cursor: pointer;
    list-style: none;
}
.shift-area > summary::-webkit-details-marker { display: none; }
.shift-area > summary:focus-visible {
    outline: 2px solid var(--wg-accent);
    outline-offset: 3px;
    border-radius: 6px;
}
.shift-area-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .3rem .6rem;
    background: var(--wg-panel);
    border: 1px solid var(--wg-line);
    border-radius: 6px;
    padding: .35rem .6rem;
    margin: .8rem 0 0;
}
.shift-area > summary.shift-area-bar:hover { border-color: var(--wg-muted); }
/* The label — the same uppercase treatment `.shift-change-bar` carries. */
.shift-area-name {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    font-size: .8rem;
    color: var(--wg-ink);
}
/* 🛑 `min-width: 0` IS LOAD-BEARING AT PHONE WIDTH. Without it a long activity
   list ("Trivia, Bingo, Karaoke") sets the line's minimum width to its own and
   pushes the page sideways — the RV5-reversal bug WGT_14302 measured fourteen
   of. With it the value wraps under the label instead. */
.shift-area-value {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
}
/* The chevron, hard right, borrowed from `.shift-change-toggle::before`. */
.shift-area-bar::after {
    content: '';
    flex: 0 0 auto;
    margin-left: auto;
    width: .45rem;
    height: .45rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform .12s ease;
}
.shift-area[open] > summary.shift-area-bar::after { transform: rotate(45deg); }
.shift-area-body { padding: .2rem 0 .4rem; }
/* A one-off has no weeks, so it gets a muted sentence where the week lines
   would be rather than an empty gap. */
.shift-area-none { margin: .8rem 0 0; }

/* ===========================================================================
   🛑 WGT_15869 (👤 Alex, 2026-09-23) — THE PILL TOGGLE. *"make the bonus a pill
   toggle on/off instead of a button"*.

   One `<form>`, two submit `<button>`s named `value`, drawn as one capsule. The
   PRESSED segment is the CURRENT STATE, not the thing pressing would do — that
   is the whole reason the button it replaces was wrong.

   🛑 `--wg-button-bg` FOR ON, `--wg-muted` FOR OFF, AND NEITHER IS
   `--wg-danger`. Switching a bonus off is a setting, not a danger — RV88 §6 and
   WGT_15382 both pin red to "this cannot be undone from here", and this can.
   ⚠️ Both fills are set against `--wg-button-ink` white: 10.2:1 and 7.0:1.

   🛑 THE UNPRESSED SEGMENT MUST NOT LOOK DISABLED. It is the control Graham
   presses to CHANGE the setting, so it keeps full contrast ink on the panel
   grey; only the fill tells the two apart.
   =========================================================================== */
/* 🛑 `form.pill-toggle`, NOT `.pill-toggle` — `.wg-flag-row form { display: inline }`
   (portal.css:818) is (0,1,1) and would otherwise win, which leaves the two
   segments STACKED in a tall blob. Measured before this element qualifier was
   added: the form computed `display: block`. ⚠️ Keep the element qualifier on
   every `.pill-toggle` rule that sets a layout property. */
form.pill-toggle {
    display: inline-flex;
    margin: 0;
    border: 1px solid var(--wg-line);
    border-radius: 999px;
    background: var(--wg-panel);
    padding: 2px;
    gap: 2px;
}
/* 🛑 `width: auto` UNDOES THE SITEWIDE FULL-WIDTH BUTTON — without it each
   segment fills the row and the capsule is two stacked bars. Same fix
   `.shift-change-row button` carries, and for the same reason. */
form.pill-toggle .pill-seg {
    width: auto;
    margin: 0;
    border: none;
    border-radius: 999px;
    padding: .3rem .9rem;
    font-weight: 700;
    font-size: .9rem;
    line-height: 1.3;
    background: transparent;
    color: var(--wg-ink);
    cursor: pointer;
    white-space: nowrap;
}
form.pill-toggle .pill-seg:hover { background: var(--wg-line); }
form.pill-toggle .pill-seg.is-on {
    background: var(--wg-button-bg);
    color: var(--wg-button-ink);
}
form.pill-toggle .pill-seg.is-off {
    background: var(--wg-muted);
    color: var(--wg-button-ink);
}
form.pill-toggle .pill-seg.is-on:hover  { background: var(--wg-button-bg); }
form.pill-toggle .pill-seg.is-off:hover { background: var(--wg-muted); }
form.pill-toggle .pill-seg:focus-visible {
    outline: 2px solid var(--wg-accent);
    outline-offset: 2px;
}

/* ===========================================================================
   🛑 WGT_15869 (👤 Alex, 2026-09-23) — A SHIFT CARD HAS NO SUMMARY ROW.
   The three `.shift-area` lines render directly, always visible. What follows
   is only what that removal needs; `.shift-row*` and `.shift-change-toggle`
   above are STILL LIVE — *Add a shift* is still a `.shift-card` with a
   `.shift-row` summary — so none of those rules may be deleted.
   =========================================================================== */

/* The shift's name, when there is more than one shift or this one is labelled.
   It was the summary row's first cell; it is a heading over the lines now. */
/* 🛑 WGT_15871 — CENTRED, on every card (👤 Alex: *"center those labels"*). */
.shift-block-name {
    margin: 0 0 .2rem;
    font-weight: 700;
    text-align: center;
}

/* 🛑 `.shift-change` CARRIES A `border-top` AND A `margin-top` THAT EXIST TO
   SEPARATE IT FROM THE SUMMARY ROW ABOVE IT. With no row there, that line
   floats at the top of the card with nothing above it. This modifier removes
   both and nothing else — the class itself stays, because
   `.shift-change form`, `.shift-change-row select` and friends are scoped to
   it and every one of them is still needed. */
.shift-change-flat {
    border-top: none;
    margin-top: 0;
}
/* The first area line sets its own top margin; with no row above it, that
   margin is the card's inner padding and is not wanted. */
.shift-change-flat > .shift-area:first-child > summary { margin-top: 0; }

/* ---------------------------------------------------------------------------
   THE SUBSTITUTE READ-OUT, on each week's line. READ-ONLY — every control is
   in the *Substitute Priorities* section. 👤 Alex: *"the shift section would
   just show those four priorities or however many have someone in them along
   with the distance and the note if a particular host is already busy on that
   particular week."*

   🛑 A LIST, NOT A `<table>`. A four-column table here reintroduces exactly the
   `.table-wrap` inner scroller `WGT_15357` removed from this panel — measured
   at 130px at 390. A wrapping flex row costs nothing at any width.
   --------------------------------------------------------------------------- */
.subs-readout {
    list-style: none;
    margin: 0;
    padding: 0;
}
.subs-readout-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .5rem;
    padding: .3rem .1rem;
    border-bottom: 1px solid var(--wg-line);
}
.subs-readout-row:last-child { border-bottom: none; }
/* 🛑 `min-width: 0` IS LOAD-BEARING AT PHONE WIDTH — a long name would
   otherwise set the row's minimum width and push the page sideways, which is
   the defect WGT_14302 measured fourteen of on this portal. */
.subs-readout-name { flex: 1 1 8rem; min-width: 0; overflow-wrap: anywhere; }
.subs-readout-miles { flex: 0 0 auto; margin-left: auto; color: var(--wg-muted); }
/* 🛑 WARN AMBER, NOT DANGER RED. Being booked elsewhere that week is something
   Graham weighs, not something forbidden — he may still ring them. Red here
   would break WGT_15382's "red means this cannot be undone from here". */
.subs-readout-busy {
    flex: 0 0 auto;
    background: var(--wg-warn-bg);
    color: var(--wg-warn-ink);
    border-radius: 999px;
    padding: .1rem .5rem;
    font-size: .85rem;
    font-weight: 600;
    white-space: nowrap;
}

/* One shift's block inside the *Substitute Priorities* section. */
.subs-shift + .subs-shift {
    margin-top: 1.2rem;
    padding-top: 1rem;
    border-top: 1px solid var(--wg-line);
}
.subs-shift-name { margin: 0 0 .4rem; }

/* 🛑 WGT_15871 (👤 Alex, 2026-09-23) — THE PER-SHIFT BONUS LINE'S BODY IS CENTRED,
   text and toggle both: *"Center the inner text and toggle inside of the bonus
   area"*. `.wg-flag-row` is a wrapping flex row, so `justify-content` is what
   centres the pill and the *Back to the default* button as one group. */
.shift-bonus-body { text-align: center; }
.shift-bonus-body .wg-flag-row { justify-content: center; }

/* 🛑 WGT_15871 — EVERY WARNING IN *Substitute Priorities* IS CENTRED: the
   phone-number sentence, the *Nobody can sub* / *Hand-pick* / *Can sub* alerts and
   the empty states. *"center 'No host has a phone number on record…' and whatever
   other warning text may be shown there."* Scoped to `.shift-subs`, which only
   that section emits — never to the `#substitutes` slug (RV107's trap). */
.shift-subs > p.muted,
.shift-subs .wg-alert p,
.shift-subs .empty-state { text-align: center; }

/* ===========================================================================
   🛑 WGT_15871 (👤 Alex, 2026-09-23) — THE *Replace the host* PICKER: priority 1
   and 2 as a clickable list, then *Everyone else* as the filtered dropdown.
   The row reuses `.subs-readout-*` (name, busy pill, miles hard right) so it
   reads exactly like the list it came from.
   =========================================================================== */
.pri-pick-heading {
    margin: .6rem 0 .2rem;
    font-weight: 700;
    font-size: .8rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--wg-muted);
}
.pri-pick { list-style: none; margin: 0; padding: 0; }
.pri-pick-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .2rem .5rem;
    padding: .35rem .5rem;
    border: 1px solid transparent;
    border-bottom-color: var(--wg-line);
    border-radius: 6px;
    cursor: pointer;
    font-weight: 400;
}
.pri-pick-row:hover { background: var(--wg-panel); }
/* 🛑 THE CHOSEN ROW MUST SHOW IT — the radio dot alone is too small to read
   as the answer on a phone. */
.pri-pick-row:has(input:checked) {
    background: var(--wg-panel);
    border-color: var(--wg-button-bg);
}
.pri-pick-row input[type=radio] { margin: 0; width: auto; }

/* 👤 Alex, 2026-09-23: *"Add a bit more vertical spacing above 'replace the
   host' and 'review the change' button"*. Scoped to the rota form inside a
   shift's week line, so no other `.field` on the page moves. */
.shift-area-body .shift-change-body > form > .field:first-of-type { margin-top: .9rem; }
.shift-area-body .shift-change-body > form > .button-row { margin-top: 1.1rem; }
/* ⚠️ WGT_15871 — a SMALLER name basis inside the picker. At 8rem a name plus an
   *on Grill on Main this week* pill no longer fitted a 560px card, so the miles
   dropped to a line of their own (measured on the shot). 3rem lets the name give
   way first; `overflow-wrap` still breaks a long one rather than push the page. */
.pri-pick-row .subs-readout-name { flex-basis: 3rem; }
/* WGT_16030 — the *also on* note is a line of its own under the name, inside
   the row (render_rota.php). As a name child it was squeezed to 56px at 390. */
.pri-pick-also { flex-basis: 100%; }

/* 🛑 WGT_15874 (👤 Alex, 2026-09-23) — *Private / Hidden / Public* at the top of the
   event page, CENTRED: *"move the setting … to the very top setting right under
   'read the spec sheet'. And center it."* `text-align` centres the choice's label
   and sentences; the radio group is inline content so it follows. */
.visibility-top {
    text-align: center;
    margin: .8rem auto 0;
    max-width: 34rem;
}
.visibility-top .button-row { justify-content: center; }

/* 🛑 WGT_15874 (👤 Alex, 2026-09-23) — *Add a shift* FLOATS ON ITS OWN WHILE SHUT.
   *"Get rid of the white toggle around this and just have the button be floating
   by itself."* The white card is `.shift-block.shift-box`'s; while the `<details>`
   is shut the card's fill, border and padding go, leaving only the button on the
   grey panel. ⚠️ OPEN, THE CARD COMES BACK — the form inside it needs a surface,
   and a form floating on the panel would read as part of the shift above it.
   `:has()` is already relied on elsewhere in this file (`.pri-pick-row`). */
#add-shift.shift-box:not(:has(> details[open])) {
    background: transparent;
    border-color: transparent;
    padding: 0;
}
/* ⚠️ WGT_15894 — the flex-row centring that stood here (WGT_15874) is gone: the
   title now sits inside the "?"'s own summary (`titled`), so nothing floats. */

/* ===========================================================================
   🛑 WGT_15888 (👤 Alex, 2026-09-23) — *Private / Hidden / Public* AS A
   SEGMENTED TOGGLE. *"make this look more like a pretty toggle or something as
   the three radio buttons just look old and cheap."*

   🛑 THE RADIOS ARE KEPT, ONLY HIDDEN VISUALLY — never `display: none`, which
   takes them out of the keyboard order and away from screen readers. Each
   `<label>` is a segment; the CHECKED one is filled (`:has(input:checked)`), the
   focused one gets the accent ring (`:has(input:focus-visible)`).
   ⚠️ Scoped to `.visibility-top`: event-add.php renders the same
   `eventVisibilityChoice()` markup and keeps its plain radios.
   ⚠️ Same capsule as the bonus `.pill-toggle` (WGT_15869), so the page has one
   kind of on/off-style control, not two.
   =========================================================================== */
.visibility-top .visibility-choice .visibility-options {
    flex-basis: auto;
    display: inline-flex;
    flex-wrap: nowrap;
    gap: 2px;
    margin: .4rem auto 0;
    padding: 2px;
    border: 1px solid var(--wg-line);
    border-radius: 999px;
    background: var(--wg-panel);
}
.visibility-top .visibility-options .setting-check {
    position: relative;
    display: inline-flex;
    align-items: center;
    min-height: 0;
    margin: 0;
    padding: .35rem 1rem;
    border-radius: 999px;
    cursor: pointer;
    font-weight: 700;
    white-space: nowrap;
}
.visibility-top .visibility-options .setting-check:hover { background: var(--wg-line); }
.visibility-top .visibility-options .setting-check input {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}
.visibility-top .visibility-options .setting-check:has(input:checked) {
    background: var(--wg-button-bg);
    color: var(--wg-button-ink);
}
.visibility-top .visibility-options .setting-check:has(input:focus-visible) {
    outline: 2px solid var(--wg-accent);
    outline-offset: 2px;
}
/* With the Save button gone, the toggle sat tight on the Shift / Week grid below. */
.visibility-top { margin-bottom: 1rem; }

/* ===========================================================================
   🛑 WGT_15894 (👤 Alex, 2026-09-23) — THE TOP-OF-PAGE *Who can see this event*.
   =========================================================================== */

/* 1. THE "?" STAYS PUT. The title is inside the summary (`titled`), so the title
   and the "?" are one centred line whether the help is open or shut, and the help
   opens as a block beneath them. */
.help-reveal--titled > summary {
    display: flex;
    justify-content: center;
    gap: .5rem;
    width: 100%;
    min-width: 0;
}
.help-reveal-title { font-weight: 600; }
.help-reveal--titled > .help-reveal-body { margin: .3rem 0 .6rem; text-align: left; }
.visibility-choice--titled { margin-top: .6rem; }
/* 🛑 WGT_18368 (👤 Alex, 2026-10-02, on /portal/event-add.php) — *"This section
   should be centered."* The titled head above is one centred line already; this
   centres the ONE row of three radios under it, as a group.
   ⚠️ A horizontal row, so it centres. A stacked column of ticks keeps its left
   edge (WGT_18302) and nothing here reaches one.
   ⚠️ event.php is not touched: its fieldset is `--inline`, never `--titled`. */
.visibility-choice--titled > .visibility-options { justify-content: center; }

/* 2. THE HELP, FORMATTED — every place the choice renders. A card per choice with
   its name as a tag and the first sentence as the lead; the notes as a list. */
.vis-help-options {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: .5rem;
}
.vis-help-option {
    display: grid;
    grid-template-columns: 5rem minmax(0, 1fr);
    gap: .6rem;
    align-items: start;
    padding: .55rem .6rem;
    background: var(--wg-panel);
    border-radius: 6px;
}
.vis-help-option > p { margin: 0; }
.vis-help-tag {
    justify-self: start;
    padding: .15rem .6rem;
    border-radius: 999px;
    background: var(--wg-button-bg);
    color: var(--wg-button-ink);
    font-weight: 700;
    font-size: .85rem;
}
.vis-help-lead { font-weight: 600; }
.vis-help-heading {
    margin: .9rem 0 .3rem;
    font-weight: 700;
    font-size: .8rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--wg-muted);
}
.vis-help-notes { margin: 0; padding-left: 1.2rem; }
.vis-help-notes > li { margin: 0 0 .45rem; }
.vis-help-notes > li:last-child { margin-bottom: 0; }
/* ⚠️ The shared `.help-reveal-body p { margin-top }` would push each card's text
   down off its tag; the card sets its own spacing. */
.help-reveal-body .vis-help-option > p { margin: 0; }

/* 3. ONLY THE CURRENT CHOICE, UNTIL PRESSED (`collapsed`). The pill is the same
   dark fill as the selected segment it opens onto, with a chevron. */
.visibility-picker { text-align: center; }
.visibility-picker > summary { list-style: none; cursor: pointer; display: inline-block; }
.visibility-picker > summary::-webkit-details-marker { display: none; }
.visibility-current-word {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    padding: .35rem 1.1rem;
    border-radius: 999px;
    background: var(--wg-button-bg);
    color: var(--wg-button-ink);
    font-weight: 700;
}
.visibility-current-word::after {
    content: '';
    width: .4rem;
    height: .4rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translateY(-2px);
    transition: transform .12s ease;
}
.visibility-picker[open] .visibility-current-word::after { transform: rotate(-135deg) translateY(-2px); }
.visibility-picker > summary:focus-visible { outline: 2px solid var(--wg-accent); outline-offset: 3px; border-radius: 999px; }
.visibility-picker > .visibility-options { margin-top: .5rem; }
.help-reveal-body .vis-help-notes p { margin: 0; }
/* ⚠️ `.sentences > span` is `inline-block` sitewide, and a list bullet aligns with
   the BASELINE of the first line box — which for an inline-block is its LAST line.
   So the bullet sat halfway down each note (measured). As plain blocks the
   sentences still break between them, and the bullet sits on the first line. */
.vis-help-notes .sentences > span { display: block; }

/* ===========================================================================
   🛑 WGT_15896 (👤 Alex, 2026-09-23) — *Once a month*, a line on each shift card.
   One rule per row: what it does on the left, its two buttons hard right, the
   same row shape as the Activities line (`.shift-change-row`). A switched-off
   rule is dimmed, not hidden, so it can be switched back on.
   ⚠️ A list, not the old `.data-table` — that table was a four-column inner
   scroller at phone width, the thing `WGT_15357` removed from this panel.
   =========================================================================== */
.monthly-rules { list-style: none; margin: .6rem 0; padding: 0; }
.monthly-rule {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .4rem .6rem;
    padding: .45rem .1rem;
    border-bottom: 1px solid var(--wg-line);
}
.monthly-rule:last-child { border-bottom: none; }
.monthly-rule.is-off .monthly-rule-what { opacity: .65; }
.monthly-rule-what { flex: 1 1 12rem; min-width: 0; }
.monthly-rule-act { display: inline-flex; flex-wrap: wrap; gap: .4rem; }
.monthly-rule-act form { margin: 0; }
.monthly-rule-act button { width: auto; }
.monthly-add { margin-top: .6rem; }
.shift-monthly-body > p.muted:first-child { margin-top: .4rem; }

/* 🛑 WGT_15905 — *Custom night activities label* in *This event*, under the
   visibility toggle, with its own Save. Centred like the field itself. */
.label-top { margin: 0 auto 1rem; max-width: 34rem; }
.label-top .button-row { justify-content: center; margin-top: .4rem; }

/* ===========================================================================
   🛑 WGT_15905 (👤 Alex, 2026-09-23) — *Who can see this event* ON ONE LINE:
   title · "?" · current-choice pill. The help and the three-way toggle open
   BELOW the line, each shown by its own hidden checkbox (`:checked ~`), in the
   flow — see `eventVisibilityChoice()`'s `inline` mode for why not `<details>`.
   =========================================================================== */
.visibility-choice--inline { text-align: center; }
.vis-toggle {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    margin: 0;
    pointer-events: none;
}
.vis-line {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: .4rem .6rem;
}
/* The "?" — the same round button the `<details>` version draws. */
.vis-help-btn { cursor: pointer; display: inline-flex; }
.vis-help-btn > [aria-hidden] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    color: var(--wg-ink);
    background: var(--wg-paper);
    border: 1px solid var(--wg-line);
    border-radius: 50%;
    font-weight: 700;
    line-height: 1;
}
.vis-toggle--help:checked ~ .vis-pick-row .vis-help-btn > [aria-hidden] {
    color: var(--wg-paper);
    background: var(--wg-ink);
}
.visibility-current { cursor: pointer; display: inline-flex; }
.vis-toggle--pick:checked ~ .vis-line .visibility-current-word::after { transform: rotate(-135deg) translateY(-2px); }
/* Keyboard focus lands on the hidden checkbox; draw its ring on its label. */
.vis-toggle--help:focus-visible ~ .vis-pick-row .vis-help-btn > [aria-hidden],
.vis-toggle--pick:focus-visible ~ .vis-line .visibility-current-word {
    outline: 2px solid var(--wg-accent);
    outline-offset: 2px;
}
/* The two bodies, shut until their box is ticked. */
/* ⚠️ Prefixed `.visibility-top` ON PURPOSE: `.visibility-top .visibility-choice
   .visibility-options` (the segmented pill, WGT_15888) is (0,3,0) and sets
   `display: inline-flex`, so an unprefixed hide lost to it and the three choices
   showed permanently (measured). */
.visibility-top .visibility-choice--inline > .help-reveal-body,
.visibility-top .visibility-choice--inline > .vis-pick-row { display: none; }
.visibility-top .vis-toggle--help:checked ~ .help-reveal-body { display: block; margin: .6rem 0 .2rem; text-align: left; }
.visibility-top .vis-toggle--pick:checked ~ .vis-pick-row { display: grid; }
/* WGT_15905 — the label's input and *Save label* on one centred line. */
.label-line {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: .5rem;
}
.label-line > input { flex: 0 1 15rem; max-width: 15rem; margin: 0; }
.label-line > button { width: auto; margin: 0; }

/* 🛑 WGT_15906 (👤 Alex, 2026-09-23) — THE OPENED ROW: an empty track, the centred
   Private | Hidden | Public toggle, and the "?" a little to its right. Two equal
   `1fr` side tracks are what keep the toggle centred on the line above it while the
   "?" takes space on one side only; `minmax(0, …)` lets both shrink on a phone
   rather than push the page. */
.vis-pick-row {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: .5rem;
    margin-top: .5rem;
}
.visibility-top .vis-pick-row > .visibility-options { margin: 0; }
.vis-pick-row > .vis-help-btn { justify-self: start; }

/* 🛑 WGT_15906 — the title CENTRED ON THE PILL, not on its own text baseline:
   *"make sure the label who can see this event is vertically centered within the
   choice on the right."* Both are flex items centred on the cross axis, and the
   title's line box is taken to 1 so its glyphs, not its leading, set its centre. */
.vis-line { align-items: center; }
.vis-line > .help-reveal-title { line-height: 1; display: inline-flex; align-items: center; }
.vis-line > .visibility-current { align-self: center; }

/* WGT_17222 — venue-edit.php's *Good to know*, *About* and *For hosts* are three
   accordion sections inside ONE `<form class="venue-page-form">`, so a save never
   drops another section's typed words. The form sits between `.portal-main` and
   the sections, so the sitewide centring rule (`.portal-main > .wg-section > p`)
   cannot reach their visibility lines; this is that rule, one level deeper.
   ⚠️ It replaced `.venue-page-layer` (WGT_14579's two fieldsets, now the
   sections themselves) and `.venue-page-waiting` (the list that moved to the
   portal home as its `venue-pages` panel). */
.portal-main > .venue-page-form > .wg-section > p:not(.wg-alert) { text-align: center; }
tr.is-changed td { font-weight: 700; }
/* 👤 Alex, 2026-10-06: event.php's *Edit date, days and times* — centre
   everything in it except its own header. The header is the section's
   `<summary>`, OUTSIDE `#schedule`, so this one wrapper reaches the body only:
   the scope question and its radios, *Every other week* and *First night*.
   `.setting-check` is flex, so `justify-content`, never `text-align`. */
#schedule { text-align: center; }
#schedule .setting-check { justify-content: center; }

/* WGT_19302 (👤 Alex, 2026-10-06) — website-this-week.php's *Already used*
   bonus words: a word/date table, centred, the newest five showing until the
   *Show all* button opens it. ⚠️ `.is-folded` is added by the page's script,
   so with JavaScript off every row shows and no row is ever unreachable. */
.bonus-used { text-align: center; }
.bonus-used .bonus-used-label { margin-bottom: .4rem; }
.bonus-used .table-wrap { margin: 0 auto .75rem; }
.bonus-used.is-folded tr.is-extra { display: none; }
.bonus-used .button-row { margin-bottom: 1rem; }
/* ⚠️ The base `button { display: block }` beats the `hidden` attribute, so a
   button left hidden for JavaScript-off would still show without this. */
.bonus-used .bonus-used-toggle[hidden] { display: none; }

/* WGT_22461 — team.php's compact switch list (moved here from the page's own
   <style> block). Columns fill the panel; each line is the tick and its "?".
   ⚠️ `.tip` is `position: static`, so the bubble hangs from the nearest
   POSITIONED ancestor — `.cap-item` is that, or it would open against <body>
   (WGT_22227). */
.cap-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    gap: 0 1.5rem;
    margin: 0 0 1rem;
}
.cap-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: .15rem;
    border-bottom: 1px solid var(--wg-line);
}
.cap-item .setting-check { align-items: center; }
.cap-item .setting-check input[type="checkbox"] { margin: 0; }
.cap-item .setting-label { font-weight: 600; }
