/* ============================================================================
   event.clinic core component styles. Run 0 step 2, D-245.
   ----------------------------------------------------------------------------
   Badge, Button, Chip and Drawer are ported from design/event-clinic-foundation.html
   in the discovery workspace. The data table (.dt) is BUILT here: the
   foundation renders lists with CSS grids and has no table component, so this
   is a new system component, token-styled to match, per the design brief's
   build-once rule. Tokens only, no raw hex.
   ============================================================================ */

/* ---------- Button ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
  border-radius: var(--radius); font-size: var(--text-sm); font-weight: 550;
  border: 1px solid var(--line-strong); background: var(--surface-2);
  color: var(--ink-700); white-space: nowrap;
}
.btn:hover { border-color: var(--ink-300); }
.btn svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2; }
.btn-primary { background: var(--accent-600); border-color: var(--accent-600); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-700); border-color: var(--accent-700); }
.btn-sm { padding: 4px 9px; font-size: var(--text-xs); }

.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn:disabled:hover { border-color: var(--line-strong); background: var(--surface-2); }
.btn-primary:disabled:hover { background: var(--accent-600); border-color: var(--accent-600); }

/* ---------- Form controls. BUILT here, not ported: the foundation is a
   read-oriented prototype and defines no input. Token-styled so a field sits
   beside .btn without looking foreign. ---------- */
.input {
  width: 100%; padding: 10px 12px; border-radius: var(--radius);
  /* --surface-2 is the token designated "raised and inputs". An earlier draft
     used --surface-0, the APP BACKGROUND, which rendered a grey slab on the
     white drawer that read as a filled block rather than a control. */
  background: var(--surface-2);
  /* --ink-300 rather than --line-strong on purpose: a divider line and a field
     outline are different jobs, and the lighter divider tone disappears against
     a white drawer, which is exactly how the field became hard to see. */
  border: 1px solid var(--ink-300);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / .07);
  color: var(--ink-900); font-size: var(--text-md); font-family: var(--font-ui);
}
.input::placeholder { color: var(--ink-300); }
.input:hover:not(:disabled) { border-color: var(--ink-500); }
.input:focus {
  outline: none; border-color: var(--accent-600);
  box-shadow: 0 0 0 3px var(--accent-100), inset 0 1px 2px rgb(0 0 0 / .04);
}
.input:disabled {
  /* `background-color` AND NOT THE `background` SHORTHAND. The shorthand resets
     background-image to none, and this rule is (0,2,0) against select.input's
     (0,1,1), so it won and silently erased the select caret on every disabled
     select. Measured in the browser 2026-07-29: backgroundImage computed to
     `none` on the disabled select and to the data URI on every other one. */
  background-color: var(--surface-sunken); color: var(--ink-500);
  border-color: var(--line-strong); box-shadow: none; cursor: not-allowed;
}
.input-label {
  display: block; font-size: var(--text-xs); color: var(--ink-400); font-weight: 600; margin-bottom: var(--sp-2);
}
.input-hint { font-size: var(--text-xs); color: var(--ink-400); margin-top: var(--sp-2); }

/* A LONG-FORM QUESTION, NOT A FIELD NAME. W40.
   `.input-label` was 11px uppercase with letter-spacing until E7 (2026-10-02)
   made labels sentence case at 12px. Still a short label, right for
   "Amount" and wrong for "How many days, and is there a show floor?". The
   wizard asks fourteen full sentences and the first draft wore the field label
   for all of them: the intake rendered as fourteen lines of shouting, found by
   opening the screen rather than by any test.
   `.page label:not([class])` applies the same treatment, so a bare label is not
   an escape and this class is how a question opts out.
   THE MEASURE IS CAPPED for the same reason: a question set in a 1200px card
   runs past the eye's return sweep, and the control below it is capped with it
   so the two read as one block. */
.input-question {
  display: block; font-size: var(--text-md); color: var(--ink-700);
  font-weight: 600; line-height: 1.45; margin-bottom: var(--sp-2);
  max-width: 68ch; text-transform: none; letter-spacing: normal;
}
.field-wide { max-width: 68ch; }

/* ONE PROPOSED ACTION: a checkbox, a sentence, and two or three badges. W40.
   `.check` alone is a flex row, so at 375px the badges claimed their width
   first and squeezed the sentence into a one-word-per-line column: "send each
   / segment / a / suggested / schedule". No overflow, no failing test, visible
   only by looking — the same shape as the glossary's `dt` gap opening a hole
   inside a highlighted word.
   The sentence gets a MINIMUM measure rather than flex-grow, and the
   difference is visible. `flex: 1 1 24ch` fixed the crush and immediately
   introduced the opposite defect at 1280px: a short action stretched to fill
   the row and threw its badges against the far edge of the card while a long
   one kept them adjacent, so no two lines aligned. `flex: 0 1 auto` with a
   floor takes only the width the words need, and the floor is what makes the
   badges wrap BELOW rather than crush when the row runs out.
   `min(24ch, 100%)` so the floor cannot itself overflow a narrow card. */
.wiz-line { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.wiz-line > input[type="checkbox"] { flex: none; }
.wiz-line-text { flex: 0 1 auto; min-width: min(24ch, 100%); line-height: 1.5; }

/* Summary strip above a list. Built for the many-line quote case, where a
   supplier pricing dozens of items needs to see progress without counting rows. */
.quote-summary {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
  margin: var(--sp-4) 0; padding: var(--sp-3) var(--sp-4);
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: var(--radius); font-size: var(--text-sm); color: var(--ink-500);
}
.quote-summary b { color: var(--ink-900); font-weight: 600; }

/* A content card on the internal surface, the .page counterpart of
   .portal-card. Used by transactional internal screens such as grant re-open. */
.page-card {
  margin-top: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--line);
  border-radius: var(--radius-lg); background: var(--surface-1);
}
/* A CARD THAT BELONGS TO THE BLOCK ABOVE IT RATHER THAN FOLLOWING IT. The
   --sp-4 above is the gap between the cards that make up a PAGE, and the
   Academy knowledge check is not one of those: it sits inside a lesson, under
   that lesson's practical work, and the full step reads as the next section
   starting rather than as the lesson continuing. The modifier is the `is-`
   form `.zone-tile.is-nested` and `.alert-card.is-acked` already use, and it
   replaces `style={{ marginTop: "var(--sp-2)" }}` at the one call site that
   had reached the same conclusion inline. */
.page-card.is-nested { margin-top: var(--sp-2); }

/* Quote line item, list cell and drawer detail. `description` is one text
   column, so the first line is the heading and the rest is the detail. */
.quote-work { display: block; max-width: 46ch; }
.quote-work-title { display: block; color: var(--ink-900); }
.quote-work-more {
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden; margin-top: 2px;
  font-size: var(--text-xs); color: var(--ink-400);
}
.quote-detail-text {
  margin: 0; font-size: var(--text-sm); line-height: 1.65; color: var(--ink-700);
  /* pre-wrap so a multi-line specification keeps the line breaks the author
     typed, rather than collapsing into one paragraph. */
  white-space: pre-wrap;
}
.quote-detail-empty { color: var(--ink-400); font-style: italic; }
.quote-ref { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-500); }

/* The live line total on a structured quote, amount x time x unit price. */
.quote-calc {
  margin-top: var(--sp-3); padding: var(--sp-3);
  background: var(--surface-sunken); border-radius: var(--radius);
  font-size: var(--text-sm); color: var(--ink-500);
}
.quote-calc b { color: var(--ink-900); font-weight: 600; font-size: var(--text-md); }

/* ---------- Badge, the one status vocabulary ---------- */
.badge {
  display: inline-flex; align-items: center; gap: 5px; font-size: var(--text-xs);
  font-weight: 600; padding: 2px 8px; border-radius: 20px; line-height: 1.5; white-space: nowrap;
}
.badge .bdot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
/* ★ W436. A BADGE CARRYING A NAME MAY WRAP. `.badge` is nowrap so a status never
   breaks in two, but a performer's name in the programme's 13rem On stage column
   is not a status: unwrapped it ran 219px past its cell and over the Room
   column. Opt-in through Badge's `wrap`, and the dot keeps to the first line. */
.badge-wrap { white-space: normal; overflow-wrap: anywhere; max-width: 100%; align-items: flex-start; text-align: left; }
.badge-wrap .bdot { margin-top: 0.45em; }
.b-ok { background: var(--ok-100); color: var(--ok-600); }            .b-ok .bdot { background: var(--ok-600); }
.b-warn { background: var(--warn-100); color: var(--warn-700); }      .b-warn .bdot { background: var(--warn-700); }
.b-err { background: var(--err-100); color: var(--err-600); }         .b-err .bdot { background: var(--err-600); }
.b-info { background: var(--info-100); color: var(--info-600); }      .b-info .bdot { background: var(--info-600); }
.b-neutral { background: var(--neutral-100); color: var(--neutral-600); } .b-neutral .bdot { background: var(--neutral-600); }
.b-mag { background: var(--mag-100); color: var(--mag-600); }         .b-mag .bdot { background: var(--mag-600); }
.b-accent { background: var(--accent-100); color: var(--accent-700); }      .b-accent .bdot { background: var(--accent-700); }

/* ---------- Chip ---------- */
.chip {
  font-size: var(--text-xs); padding: 1px 9px; border: 1px solid var(--line);
  border-radius: 20px; color: var(--ink-500); background: var(--surface-1); cursor: pointer;
}
.chip:hover { border-color: var(--accent-500); color: var(--accent-700); background: var(--accent-050); }

/* ---------- Drawer ---------- */
.drawer-overlay {
  position: fixed; inset: 0; background: rgba(28, 33, 31, .28); z-index: 99;
}
.drawer {
  position: fixed; top: 0; right: -480px; width: 460px; max-width: 94vw;
  /* 100vh THEN 100dvh, in that order and both kept. On a mobile browser 100vh
     is the height WITH the address bar retracted, so a 100vh panel is taller
     than what you can see and its footer sits below the fold. `dvh` tracks the
     live viewport. The vh line stays as the fallback for anything that does
     not know dvh, which drops the second declaration and keeps the first. */
  height: 100vh;
  height: 100dvh;
  background: var(--surface-1); border-left: 1px solid var(--line); box-shadow: var(--shadow-md);
  z-index: 100; transition: right .25s cubic-bezier(.2, .8, .2, 1); display: flex; flex-direction: column;
}
.drawer.show { right: 0; }
/* ONE GUTTER FOR THE WHOLE DRAWER, sp-5. Head, body and foot all inset by the
   same amount, so the footer's action lines up with the fields above it rather
   than sitting on its own margin. It was sp-4 horizontally and the drawer is
   flush against the viewport's right edge, so a right-aligned primary button
   ended up 16px from the edge of the browser window and read as stuck to it.

   `flex-shrink: 0` ON HEAD AND FOOT, and neither had it. `.drawer` is a flex
   column, the default shrink is 1, and only `.drawer-body` carrying `flex: 1`
   was keeping them at their natural height. Nothing guaranteed that, so a tall
   enough body could compress the action bar. */
.drawer-head {
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--line);
  display: flex; gap: var(--sp-3); align-items: flex-start; flex-shrink: 0;
}
.drawer-title { font-size: var(--text-lg); font-weight: 640; letter-spacing: -.015em; color: var(--ink-900); line-height: 1.3; }
.drawer-sub { margin-top: 6px; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.drawer-close { margin-left: auto; flex-shrink: 0; }
.drawer-body { flex: 1; overflow-y: auto; padding: var(--sp-5); }
.drawer-foot {
  padding: var(--sp-5); border-top: 1px solid var(--line);
  display: flex; gap: var(--sp-2); justify-content: flex-end;
  background: var(--surface-1); flex-shrink: 0;
  /* EVEN PADDING, sp-5 on all four sides, matching the head and the body.
     An earlier version made the bottom sp-6 on the theory that the window edge
     needs more air than an internal divider. THAT WAS COMPENSATING FOR A BUG
     RATHER THAN A LOOK: the panel was rendering 16px low and its last 16px hung
     below the viewport, so the bottom padding was being CLIPPED and no amount
     of it would ever have shown. See the out-of-flow rule at the foot of this
     file. With the panel back on the viewport the even bar is correct. The
     safe-area floor stays, because that is a real inset and not a taste. */
  padding-bottom: max(var(--sp-5), env(safe-area-inset-bottom));
}
.drawer-section {
  font-size: var(--text-xs);
  color: var(--ink-400); font-weight: 600; margin: var(--sp-5) 0 var(--sp-2);
}
/* ★★ W348. `.field-grid` IS A dt/dd LIST AND `.form-grid` IS A FORM, and until
   today they were one class doing both jobs. The comment beside `.fact-grid`
   below already said what this is -- "a two-column dt/dd list built for the
   drawer" -- and yet SIX screens were using it to lay out `<Field>` controls,
   which put the first control in a 132px column and clipped the second at
   375px. Found by opening the phone door on a phone-sized viewport.

   It is the same defect `.panel` had: one name, two layouts, invisible because
   each looked right in the place its author was looking. Five screens really do
   use dt/dd and keep this rule unchanged.

   ★ `min(100%, 240px)` RATHER THAN A BARE 240px. `minmax(240px, 1fr)` OVERFLOWS
   below 240px -- the track cannot shrink past its own minimum -- which on a
   320px phone with the rail showing is exactly where the form ends up. */
.field-grid { display: grid; grid-template-columns: 132px 1fr; gap: 9px 12px; font-size: var(--text-sm); margin: 0; }
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: var(--sp-3) var(--sp-4);
  align-items: start;
}
/* ★★★ THE OWNER, 2026-09-12: "Input fields are not aligned in one line, so
   this should be fixed." THEY WERE NOT, AND `align-items: start` WAS NOT THE
   PROBLEM -- it was already here, which is what made this look impossible.
   Measured in the browser on the mini budget's actuals form: the three fields
   sat at marginTop 0, 16px and 16px, so the FIRST one rendered 16px higher
   than its neighbours and every label in the row disagreed about the baseline.

   `.field + .field` above adds 16px so that fields STACKED in a column get
   their rhythm. In a grid the `gap` already does that job, so the stacking
   margin is pure leakage -- and it lands on every child except the first,
   which is precisely why the row goes crooked rather than merely low.

   `.field-row` has carried this exact cancellation since it was written; the
   grid was simply never given one. This is the same family as the note at the
   foot of this file, where `.stack > :only-child > * + *` reached into a
   grid's second column: A RHYTHM UTILITY MUST NEVER REACH AN ELEMENT WHOSE
   SPACING IS ALREADY SOMEBODY ELSE'S JOB. */
.form-grid > .field + .field { margin-top: 0; }
.field-grid dt { color: var(--ink-400); }
.field-grid dd { color: var(--ink-900); font-weight: 500; margin: 0; }

/* ---------- Fold: a collapsible section. 2026-08-06 ----------

   THE SKIN FOR NATIVE `<details>`. See `components/Fold.tsx` for why the
   element was chosen over a state toggle. Everything here is either killing
   browser chrome that ignores the tokens, or giving the summary the same
   typographic voice as `.drawer-section` so a folded section and a plain
   heading read as the same rank.

   `.drawer-section` STAYS AND IS NOT DEPRECATED. A two-field group does not
   earn a disclosure control, and wrapping one is worse than leaving it open.
   The fold is for sections a reader would otherwise have to scroll past. */
.fold {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-1);
  /* The summary carries a tinted background to its own edges, and without this
     the tint squares off the two top corners the radius just rounded. */
  overflow: hidden;
}
.fold > summary {
  /* TWO RESETS, NOT ONE, AND BOTH ARE LOAD-BEARING. `list-style: none` removes
     the marker in Firefox and Chrome; Safari draws it from a proprietary
     pseudo-element that ignores `list-style` entirely. Ship only the first and
     the disclosure triangle survives on Safari, beside the chevron drawn
     below. */
  list-style: none;
  cursor: pointer;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-sunken);
  /* Same voice as `.drawer-section`, one step darker. The heading is now a
     control, and a control that reads dimmer than the text it governs looks
     disabled. */
  font-size: var(--text-xs);
  font-weight: 600; color: var(--ink-500);
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary:hover { color: var(--ink-900); }
.fold > summary:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }
/* THE TITLE SHRINKS TOO, AND IT DID NOT. `flex-shrink: 0` here meant the note
   absorbed every pixel of a narrow summary on its own, so at 375px
   "CULTURAL AND DIETARY RESTRICTIONS" kept its full width and its summary
   rendered as the single letter "N". Both items now shrink at the flex
   default, which is PROPORTIONAL TO CONTENT WIDTH, so the long title gives up
   more than the short value beside it and each keeps an ellipsis rather than a
   truncation. `min-width: 0` is what actually permits it: the default `auto`
   refuses to shrink below the content. */
.fold-title {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* A FLOOR, NOT A WIDTH, AND `min()` IS WHAT MAKES IT SAFE. Weighting the
     shrink was not enough on its own: against a 51-character email address
     even a 3:1 ratio left "Contact" rendering as "CONT…". Fourteen characters
     is enough for every heading in the app that is one or two short words, and
     `min(100%, 14ch)` means a container narrower than that still wins, so the
     floor can never push the row wider than the drawer. */
  min-width: min(100%, 14ch);
}
/* THE SUMMARY LINE IS THE POINT OF THE COMPONENT. It is the only thing a
   reader sees while the section is closed, so it gets the readable size and
   the natural case that the heading beside it deliberately gives up. */
.fold-note {
  margin-left: auto; display: flex; align-items: center; gap: var(--sp-2);
  justify-content: flex-end;
  font-size: var(--text-sm); text-transform: none; letter-spacing: 0;
  font-weight: 500; color: var(--ink-500);
  /* ONE LINE, ALWAYS, AND THE OVERFLOW IS CUT RATHER THAN WRAPPED. A summary
     that wraps makes its row taller than its neighbours, and a stack of eight
     folds whose rows are different heights stops reading as a list. The two
     `min-width: 0` are what actually let a flex item shrink below its content:
     the default is `auto`, which refuses, and without them the ellipsis never
     appears no matter what `overflow` says. */
  flex-wrap: nowrap; overflow: hidden; min-width: 0;
  /* THE VALUE GIVES WAY BEFORE THE NAME DOES. Plain proportional shrinking
     divides the shortfall by content width, so a 51-character email address
     beside the word "Contact" took most of the room and the HEADING rendered
     as "CON…". A heading that cannot be read is a row nobody can navigate by,
     while a truncated address still shows its first half and opens in one
     click. Three is enough to invert the ratio at 375px without starving a
     short value like "None" on a long heading. */
  flex-shrink: 3;
}
.fold-note > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A badge shrinks to nothing before the text does otherwise, and a truncated
   "Life-threatening" is worse than a truncated email address. */
.fold-note > .badge { flex-shrink: 0; }
/* THE CHEVRON IS DRAWN, NOT AN IMAGE. `<summary>` is a normal element and can
   carry a pseudo-element, unlike the `<select>` two hundred lines below, so
   this one follows `currentColor` and needs no per-theme hex. */
.fold > summary::after {
  content: ""; flex-shrink: 0; width: 7px; height: 7px; margin-left: var(--sp-1);
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s cubic-bezier(.2, .8, .2, 1);
}
.fold[open] > summary::after { transform: rotate(-135deg); }
.fold[open] > summary { border-bottom: 1px solid var(--line); }
.fold-body { padding: var(--sp-4); }
/* Same rhythm as `.drawer-body`, for the same reason: the global reset leaves
   stacked blocks flush against each other. */
.fold-body > * + * { margin-top: var(--sp-4); }
/* `alert` TINTS THE SUMMARY ROW, and this is a correction. 2026-08-06.

   IT WAS A 3px LEFT BORDER and the design review was right to flag it. `.fold`
   is a rounded card, 7px, with `overflow: hidden`, so a left border curves into
   both corners and renders as a rounded tab stuck to the side — which is a
   generated-UI tell rather than a considered mark.

   THE REASON I ORIGINALLY REJECTED A TINT WAS WRONG. The note said "a tinted
   panel competes with the notices and badges inside it", and that is true of
   tinting the PANEL. Tinting only the summary is not that: the body keeps
   `--surface-1` and its contents keep the whole contrast range to themselves.

   It is also a stronger signal than the line was. D-491 still holds because
   the caller states the reason in `note` — "Deletes this guest" — so the
   colour is emphasis and never the only carrier. */
.fold-alert > summary { background: var(--err-100); color: var(--err-600); }
.fold-alert > summary:hover { color: var(--err-600); }
.fold-alert > summary .fold-note { color: var(--err-600); }

/* ---------- Option grid and the chosen list. 2026-08-06 ----------

   A LONG SET OF CHECKBOXES IS NOT A LIST, IT IS A MAP, and it was being drawn
   as a list. Measured on the guest panel: fourteen allergens and thirty-four
   cultural restrictions across eleven traditions, one per line, each with a
   severity `<select>` appearing beside it once ticked. About two thousand
   pixels of drawer for two fields, and the reader could not see what they had
   already chosen without scrolling back through what they had not.

   THE FIX IS TO SPLIT THE TWO QUESTIONS THE CONTROL WAS ASKING AT ONCE.
   "Which ones apply" is a scan over many candidates and wants width, so it
   becomes a grid of plain checkboxes. "How severe is each" only concerns the
   handful already chosen, so it moves into `.pick-chosen` above the grid where
   the whole answer is visible at once. Neither question interrupts the other.

   `auto-fill` AND NOT A FIXED COLUMN COUNT. The drawer is 460px wide on a
   desktop and 94vw on a phone, and the same grid has to hold both. 168px is
   the narrowest a checkbox plus "Cereals containing gluten" reads at without
   wrapping mid-phrase, so a 360px viewport gets one column and stays legible
   rather than getting two cramped ones. */
.optgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: var(--sp-2) var(--sp-4);
}
/* THE GRID OWNS THE GAPS NOW. `.check + .check` further up this file gives a
   run of checkboxes a top margin, which is right in a column and wrong in a
   grid: it lands on every cell except the first of each row and ripples the
   baselines. Doubled for specificity, same reason as `.drawer.drawer`. */
.optgrid .check + .check, .optgrid .check { margin-top: 0; display: flex; }
.opt-group {
  grid-column: 1 / -1;
  font-size: var(--text-xs);
  font-weight: 600; color: var(--ink-400);
  padding-top: var(--sp-2); border-top: 1px solid var(--line);
}
/* The first group heading sits directly under the search box and needs no rule
   to separate it from what is above. */
.opt-group:first-child { padding-top: 0; border-top: 0; }

/* THE CHOSEN LIST. One row per selection: what it is, how severe, and the way
   out. It carries a sunken well so it reads as a summary of the grid below
   rather than as more of the same. */
.pick-chosen {
  background: var(--surface-sunken); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--sp-2) var(--sp-3);
}
.pick-row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) 0; font-size: var(--text-sm); color: var(--ink-900);
  flex-wrap: wrap;
}
.pick-row + .pick-row { border-top: 1px solid var(--line); }
/* THE ONE SEVERITY THAT IS NOT A PREFERENCE. The select in the same row spells
   it out in words, so this edge is emphasis rather than the only signal. */
.pick-severe { border-left: 3px solid var(--err-600); padding-left: var(--sp-2); }
.pick-name { flex: 1 1 40%; min-width: 0; font-weight: 500; }
/* The severity control is secondary to the name it qualifies, so it takes the
   small control height rather than the full one. */
.pick-row .input { width: auto; flex: 0 1 auto; height: 28px; padding-top: 0; padding-bottom: 0; font-size: var(--text-sm); }
.pick-guide { flex: 1 1 100%; font-size: var(--text-xs); color: var(--ink-500); line-height: 1.45; }
/* A REMOVE CONTROL IS A BUTTON, NOT A GLYPH IN A SPAN. It has to be tabbable
   and it has to say what it removes, which `aria-label` at the call site does.
   The visible mark stays a multiplication sign because the row beside it
   already names the thing. */
/* 24px AND NOT 22, WHICH IS THE SIZE THIS WANTED TO BE. WCAG 2.2 AA 2.5.8
   sets 24 by 24 CSS pixels as the floor for a pointer target, and D-491 binds
   this app to AA. There is a spacing exemption for a target inside a 24px
   circle that touches nothing else, and relying on it here would be a bet on
   the row never gaining a second control. */
.pick-x {
  flex-shrink: 0; width: 24px; height: 24px; line-height: 1;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-1); color: var(--ink-500); cursor: pointer;
  font-size: var(--text-md);
}
.pick-x:hover:not(:disabled) { border-color: var(--err-600); color: var(--err-600); background: var(--err-100); }
.pick-x:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 1px; }
.pick-x:disabled { cursor: not-allowed; opacity: .5; }

/* ---------- Event area counts in a table cell. Owner, 2026-08-06 ----------

   NINE NUMBERS IN ONE CELL, NOT NINE COLUMNS. The owner asked for the other
   event characteristics beside the Spaces count. Nine numeric columns would
   push the event name and its dates off a laptop screen, and D-615's column
   chooser, which is what would make nine columns survivable, does not exist
   yet. A strip inside one cell costs one column and reads at a glance.

   `tabular-nums` SO THE NUMBERS LINE UP DOWN THE LIST. Without it a 1 is
   narrower than a 7 and nine little columns visibly wander row to row. */
/* ★★ W410. CENTRE, NOT BASELINE, AND THAT IS THE WHOLE LAYOUT CHANGE. These
   items were a digit beside an uppercase word, which share a baseline. They are
   now a digit beside an SVG, which has none -- an icon aligned on `baseline`
   sits on it like a letter and floats above the number it belongs to. */
.ev-counts { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); align-items: center; }
.ev-count { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.ev-count b { font-size: var(--text-sm); font-weight: 640; color: var(--ink-900); font-variant-numeric: tabular-nums; }

/* W410. The icon that replaced the word, sized and stroked exactly like
   `.nav-icon svg` in shell.css -- because it IS the navigation's icon for that
   module, and a reader who has learned it in one place should meet the same
   shape in the other. One vocabulary, two surfaces. */
.ev-count-icon {
  width: 15px; height: 15px; display: grid; place-items: center;
  flex-shrink: 0; color: var(--ink-400);
}
.ev-count-icon svg {
  width: 14px; height: 14px;
  stroke: currentColor; fill: none; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
/* AN EMPTY AREA IS SAID QUIETLY, NOT IN AN ALARM COLOUR. Zero agenda items on
   an event six months out is the normal state of a healthy record, and this
   review has already had to undo three cases of a warning tone used for
   exactly that. The number dims; nothing turns red. The event dashboard's own
   tiles redden a zero, and that is a different surface with a different job:
   there you have chosen one event and are being asked what to do next. */
.ev-count.is-zero b { color: var(--ink-400); font-weight: 500; }
/* W410. The icon dims WITH the number. Left at full strength beside a
   dimmed zero it would be the loudest thing in a row of nine. */
.ev-count.is-zero .ev-count-icon { color: var(--line-strong); }

/* W332. THE DOOR'S VERDICT LINE, and the one place in this product where the
   type scale is deliberately broken. Everything else here is 13.5px because
   these are data-dense screens read at a desk; this is read at arm's length,
   in the dark, by somebody who is also watching a face and a queue. A name at
   body size is a name nobody reads in time.

   `--text-xl` and not a raw px, so it still moves with the scale. */
.door-verdict {
  font-size: var(--text-xl); font-weight: 640; line-height: 1.2;
  margin: var(--sp-1) 0 var(--sp-2);
}

/* ---------- Notice, a token-styled banner for states ---------- */
.notice { padding: var(--sp-3) var(--sp-4); border-radius: var(--radius); font-size: var(--text-sm); }
/* THE TITLE ONLY, and the child combinator is the fix rather than a nicety.
   B-198, 2026-08-14. This was `.notice strong`, a descendant selector, so it
   caught EVERY <strong> a caller put in the body and turned each one into a
   block: an emphasised phrase mid-sentence broke onto its own line and the
   sentence read as three fragments. Six callers already do this. The title is
   a DIRECT child of .notice, so scoping it costs one character and fixes
   every one of them. */
.notice > strong { display: block; font-weight: 640; }
.notice-body { margin-top: 3px; }
.n-info { background: var(--info-100); color: var(--info-600); }
.n-warn { background: var(--warn-100); color: var(--warn-700); }
.n-err { background: var(--err-100); color: var(--err-600); }
.n-ok { background: var(--ok-100); color: var(--ok-600); }
.n-neutral { background: var(--neutral-100); color: var(--neutral-600); }
/* W940 (B-617). A hint inside a notice takes the notice's own ink. --ink-400 on a
   tinted notice measured 4.34:1 on .n-err (Showbook › Készültség, axe
   color-contrast, serious); the notice colours are the ones chosen to pass. */
.notice-body .input-hint { color: inherit; }

/* ---------- Data table, built here ---------- */
/* ---------- THE TABLE IS GENERATED NOW. Only the deltas live here. ----------
 *
 * `.dt`, `.dt th`, `.dt td`, `.dt-wrap` and the row states come from
 * `components.generated.css`, which carries the FOUNDATION'S OWN declarations
 * under the app's selector. Anything restated here would win by load order and
 * put the drift straight back, so this block holds only what the app
 * deliberately does differently, each with its reason.
 *
 * WHAT THE APP WAS MISSING BEFORE THE GENERATOR, measured 2026-07-29:
 * `.dt-wrap` did not exist at all, so every table in the app rendered BARE
 * where the foundation wraps it in a rounded, bordered, shadowed card. `.num`
 * did not exist either, so money columns had neither tabular numerals nor
 * right alignment, in a module whose columns are mostly money. The header also
 * differed on size, weight, letter spacing, padding, background and border.
 * ---------------------------------------------------------------------------- */

/* WIDE TABLES MUST BE REACHABLE ON A PHONE. The foundation's `.tbl-wrap` sets
   `overflow: hidden`, which clips the corners to the radius and is right for a
   one-screen design reference. In the app it hides data.
   MEASURED at 375px on 2026-07-29: a realistic actuals table is 1459px wide in
   a 373px wrap, so 1086px of it is clipped, and with `hidden` there is NO
   scrollbar and NO touch drag, so a user cannot reach any of it. The content is
   still reachable programmatically, which is exactly the kind of "technically
   present" that D-491's operability requirement rules out.
   `overflow-x: auto` was verified to keep BOTH the sticky header and the
   clipped corners, and leaves `overflow-y` hidden, so this is the smallest
   possible delta from the foundation. */
.dt-wrap { overflow-x: auto; }

/* ★ A TABLE WHOSE COLUMNS WERE GIVEN WIDTHS HONOURS THEM. `table-layout: auto`
   treats a <col> width as a suggestion and will still stretch a column to fit
   its longest cell, which is the very thing being fixed: one section's long
   titles would widen its Item column and the section below it would keep its
   own. `fixed` makes the declared widths binding, so every table on a screen
   shares one geometry.

   It also stops the browser measuring every cell before it can paint, which on
   the October 23 agenda is 139 rows across several tables. */
.dt-fixed { table-layout: fixed; }
/* With fixed layout a long unbroken title would otherwise overflow its cell
   rather than wrap. */
.dt-fixed td, .dt-fixed th { overflow-wrap: anywhere; }
/* The drag handle column is furniture and takes the width it has always had. */
.dt-fixed .dt-grip-col { width: 34px; }

/* STICKY HEADER. The app's tables are long and scroll inside a page; the
   foundation's single reference table does not. Only the positioning is
   restated, so the TYPOGRAPHY still comes from the generated `.dt th` and
   cannot drift from it. `background` is required or rows show through. */
.dt thead th { position: sticky; top: 0; z-index: 1; }

/* NOT EVERY ROW IS CLICKABLE HERE. The foundation makes every `tbody tr` a
   pointer because every row in a design reference opens something. In the app
   a table is often just data, and a pointer cursor on an inert row is a
   promise the UI does not keep. The generated rule's `transition` survives,
   because only `cursor` is overridden. */
.dt tbody tr { cursor: default; }
.dt tbody tr.clickable { cursor: pointer; }

/* ---------------------------------------------------------------------------
   W284. EACH ROW READS AS A BLOCK, AND A TALL CELL NO LONGER FLOATS.

   Owner, 2026-09-04, looking at the agenda importer: "the imported lines in
   the table are not aligned perfectly... the sample data is not clearly
   separated when it's not hovered. I need more separated and more visible
   blocks and representations of each line of data."

   Both complaints are about the same thing and neither is about the importer.
   The generated `.dt` gives a row a hairline bottom border and a hover tint,
   which is right for a short list of one-line rows and not enough for dense
   data: the importer's mapping table has cells three lines deep beside cells
   one line deep, so where a row starts and stops is genuinely hard to see.

   TWO CHANGES, AND THE ORDER OF THE RULES IS LOAD BEARING.

   `vertical-align: top`. The generated rule centres a cell, so a one-line
   value sits halfway down a three-line neighbour and every column starts at a
   different height. Centring is right when rows are uniform; it is exactly
   what makes a ragged table look broken. Top is the ordinary answer for data.

   A ZEBRA STRIPE, in `--surface-sunken`. No new token: `tokens.local.css` may
   not declare one, and design parity refuses it if it does.

   ★ AND HOVER MOVES TO `--accent-050`, WHICH IS NOT COSMETIC. The generated
   hover is `--surface-sunken` -- the same value the stripe now uses -- so
   striping alone would have made hover INVISIBLE on every second row. A
   feedback state that works on half the rows is worse than none, because the
   half where it fails looks like a dead row. Both tokens carry a dark value,
   checked before they were used.

   The `:hover` rule follows the `:nth-child` rule because they have the same
   specificity and source order is what decides. Moving it above would restore
   the invisible-hover bug silently.
   --------------------------------------------------------------------------- */
.dt td { vertical-align: top; }
.dt tbody tr:nth-child(even) { background: var(--surface-sunken); }
.dt tbody tr:hover { background: var(--accent-050); }

/* A grouping row already paints its own background on the CELL, so it outranks
   the stripe by specificity and keeps reading as a heading rather than as one
   more striped line. Restated here only so the interaction is written down. */
.dt tr.l0 td { background: var(--surface-sunken); }

/* W233. THE FOCUS RING FOR A CLICKABLE ROW.
   `.clickable` carried `cursor: pointer` and NOTHING ELSE, measured
   2026-08-29, so once the row became focusable there was no way to see where
   focus was. A tab stop nobody can locate is not operable.

   `:focus-visible` rather than `:focus`, so a mouse click does not leave a
   ring behind on a row the user has already opened.

   THE OFFSET IS NEGATIVE BECAUSE `.dt` IS `border-collapse: collapse`.
   The ring is drawn INSIDE the row's own box, which is what `.cal-row` in
   this same file already does for the only other focusable row in the app.
   Drawn outside, it would be clipped by `.dt-wrap`'s `overflow-x: auto` on
   the first and last columns of a wide table. */
.dt tbody tr.clickable:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }

.dt-empty { padding: var(--sp-6); text-align: center; color: var(--ink-400); font-size: var(--text-sm); }
/* E10: a string empty text renders EmptyState, which carries its own padding. */
.dt-empty > .empty-state { padding: var(--sp-2) 0; }

/* ---------- W13. Facts, section headings and inline links ----------
   The design foundation shows an event's summary as a grid of labelled facts
   above the detail. `.field-grid` already exists but is a two-column dt/dd list
   built for the drawer, which wraps badly at eight facts across a full page
   width. This is the page-width variant: it wraps by available space rather
   than forcing one pair per row. */
.fact-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--sp-3) var(--sp-4); margin: var(--sp-4) 0 var(--sp-5);
  padding: var(--sp-4); background: var(--surface-sunken);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.fact { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.fact-label {
  font-size: var(--text-xs); color: var(--ink-400); font-weight: 600;
}
.fact-value { font-size: var(--text-sm); color: var(--ink-900); font-weight: 500; }

.section-title {
  font-size: var(--text-md); font-weight: 640; color: var(--ink-900);
  margin: var(--sp-5) 0 var(--sp-2);
}

/* ★ A CARD WHOSE HEADING SHARES ITS LINE WITH ACTIONS. Added 2026-09-05 on
   the owner's report: "a szövegdobozok kezdőpontja nem ugyanabban a
   magasságban kezdődik."

   MEASURED, not guessed. The agenda card's top row is `align-items: flex-start`
   with the heading block on the left and the buttons on the right, so both
   boxes DO start at the container's top. What did not was the heading itself:
   `.section-title` carries `margin: var(--sp-5) 0 var(--sp-2)`, and that 20px
   pushed the title down while the buttons stayed put. Measured on the agenda:
   the button box began 20px above the heading box and their text 16px apart,
   which is exactly the offset visible in the report.

   SCOPED, NOT FIXED ON `.section-title`. That class appears 47 times across 29
   files and its leading margin is right in most of them -- it is what separates
   a section heading from whatever sits above it. Only a heading that OPENS a
   row has nothing above it to be separated from.

   The row also replaces an inline style object, which the design system asks
   screens not to carry. */
.card-titlerow {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
}
.card-titlerow .section-title { margin-top: 0; }

/* A HEADING THAT OPENS A SUBSECTION INSIDE A CARD, one step down from the
   card's own title. A MODIFIER, worn as `class="section-title subsection-title"`,
   so the weight and colour stay in one place and only the two things that
   differ are restated -- the same shape as `.line-row-2` beside `.line-row`.

   A TOKEN, NOT A RAW `1rem`. The call site this replaces wrote
   `fontSize: "1rem"`, which is how a screen leaves the scale without anybody
   noticing -- and in this case leaving the scale is exactly what went wrong.

   ★★★ AND THE TOKEN IS `--text-sm`, BECAUSE A SUBSECTION IS SMALLER THAN THE
   SECTION IT IS INSIDE. The first version of this rule took `--text-lg` on the
   reasoning that it matched the 16px the inline rule had set. It did match it,
   and that was the defect rather than the requirement: `.section-title` is
   `--text-md`, 13.5px, so a 16px child rendered the h3 LARGER than the h2 above
   it. Measured on the running screen before this line was changed -- h2
   "Ki képzett ebben a munkaterületben" at 13.5px, h3 "Amit ez a munkaterület
   előír" at 16px -- and the owner reported it.

   --text-sm at weight 640 is not a new opinion: `.owner-editor h4` is exactly
   that, and it is this stylesheet's existing answer for a heading one level
   under a section title. Smaller than the body text beneath it is deliberate
   and is what the weight is for; `.input-label` goes further still.

   AND A SMALLER TOP MARGIN, which is the other half of "subsection".
   `.section-title` carries --sp-5, the gap that separates one section of a page
   from the next. A subsection is still inside its section, so --sp-3 keeps it
   attached to what it subdivides. The bottom margin is inherited unchanged. */
.subsection-title { font-size: var(--text-sm); margin-top: var(--sp-3); }

.link { color: var(--accent-600); text-decoration: none; font-weight: 520; }
.link:hover { text-decoration: underline; }

/* One level of visual nesting for a child space. Deliberately not a tree
   widget: the rows arrive ordered and one indent is enough to read the shape
   without the list pretending to be something it is not. */
.tree-indent { color: var(--ink-300); margin-right: 2px; }
.muted { color: var(--ink-400); font-size: var(--text-xs); }

/* ---------- Segmented control ----------
   Ported from the foundation's .seg for the theme preference, 2026-07-26.

   A SEGMENT GROUP RATHER THAN A SWITCH, because the preference has THREE
   values and the third one is not a middle position: "system" means keep
   following the machine, which a two-state control cannot say at all.

   Transitions are literal rather than var(--t-fast), which is a foundation
   token this app does not define. Same substitution as .foot-collapse. */
.seg {
  display: inline-flex; background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  overflow: hidden;
}
.seg-btn {
  border: 0; border-right: 1px solid var(--line); background: transparent;
  padding: 7px 16px;
  font-size: var(--text-sm); font-weight: 550; color: var(--ink-500);
  cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
.seg-btn:last-child { border-right: 0; }
.seg-btn:hover:not(:disabled) { background: var(--surface-sunken); color: var(--ink-900); }
.seg-btn.on, .seg-btn.on:hover { background: var(--accent-600); color: var(--on-accent); }
.seg-btn:disabled { cursor: not-allowed; opacity: .6; }
/* The group clips its children, so a ring on a segment would be cut off by the
   overflow. Inset instead, which stays inside the button's own box. */
.seg-btn:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--accent-600);
}
.seg-btn.on:focus-visible { box-shadow: inset 0 0 0 2px var(--surface-2); }

/* ============================================================================
   FORM CONTROLS THAT WERE NEVER BUILT. Added 2026-07-29.
   ----------------------------------------------------------------------------
   `.input` was built in Run 0 for text fields and NOTHING ELSE WAS. Measured
   on 2026-07-29 across src/screens and src/system: 52 `<select>`, 2
   `<textarea>`, 19 checkboxes and 1 file input carried no class at all, and
   `appearance: none` appeared EXACTLY ONCE in the whole codebase, on the
   footer unit picker in shell.css. Everything else rendered the BROWSER'S OWN
   chrome: OS-drawn, unthemed, ignoring every token in tokens.css.

   THESE ATTACH TO `.input` RATHER THAN INTRODUCING NEW CLASS NAMES. The
   screens that were built correctly already write `<select className="input">`,
   so element-qualified rules fix those for free and leave one name to teach
   instead of four. Only the properties that genuinely differ are set, so a
   select still sits beside a text field without looking foreign.
   ============================================================================ */

/* ---------- Select ----------
   THE CARET IS A BACKGROUND IMAGE AND THAT COSTS A HEX, which this file's
   header otherwise forbids. The alternative is the footer's approach, a
   wrapper element with a drawn ::after, because a `<select>` is a replaced
   element and cannot carry a pseudo-element of its own. That would mean a
   wrapper span at 52 call sites for a caret, so the trade was taken the other
   way and the colour is duplicated per theme instead.

   THE TWO HEXES BELOW ARE `--ink-400` IN EACH THEME AND MUST TRACK IT. A data
   URI cannot interpolate a CSS variable, so this is the one coupling in the
   file that a token change will not carry automatically. */
select.input {
  appearance: none; -webkit-appearance: none;
  cursor: pointer;
  /* Room for the caret, so a long option never runs underneath it. sp-8 and
     not sp-6: the caret sits at sp-3 from the right and is 10px wide, so sp-6
     left only 2px of clearance and a long option read as touching it. */
  padding-right: var(--sp-8);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23968d86' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--sp-3) center;
}
[data-theme="dark"] select.input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23727b86' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
/* A disabled select must not keep the pointer cursor its base rule sets. */
select.input:disabled { cursor: not-allowed; }

/* ---------- Textarea ----------
   Height is the only real difference. `resize: vertical` rather than `both`,
   because horizontal resize breaks out of the drawer's fixed width. */
textarea.input {
  min-height: 84px;
  padding: var(--sp-3);
  line-height: 1.55;
  resize: vertical;
}

/* ---------- Checkbox and radio ----------
   THE CLASS GOES ON THE LABEL, not the control. Every checkbox in this app is
   already written as `<label><input type="checkbox"> text</label>`, so styling
   the child by descendant means the 19 call sites gain one class on the
   wrapper instead of two on two elements. */
.check {
  display: inline-flex; align-items: flex-start; gap: var(--sp-2);
  cursor: pointer; font-size: var(--text-sm); color: var(--ink-700);
  line-height: 1.45;
}
.check input[type="checkbox"],
.check input[type="radio"] {
  appearance: none; -webkit-appearance: none;
  flex-shrink: 0;
  width: 16px; height: 16px; margin: 0;
  /* Optical alignment against a 1.45 line-height first line. */
  margin-top: 2px;
  display: grid; place-content: center;
  background: var(--surface-2);
  border: 1px solid var(--ink-300);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / .07);
  cursor: pointer;
  transition: background .14s ease, border-color .14s ease;
}
.check input[type="checkbox"] { border-radius: var(--radius-sm); }
.check input[type="radio"] { border-radius: 50%; }
.check input:hover:not(:disabled) { border-color: var(--ink-500); }
.check input:checked {
  background: var(--accent-600); border-color: var(--accent-600); box-shadow: none;
}
/* The tick is drawn rather than imported, so it needs no second asset and no
   hex. Same reasoning as the footer caret. */
.check input[type="checkbox"]:checked::after {
  content: ""; width: 4px; height: 8px;
  border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg) translate(-1px, -1px);
}
.check input[type="radio"]:checked::after {
  content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff;
}
.check input:focus-visible {
  outline: none; border-color: var(--accent-600);
  box-shadow: 0 0 0 3px var(--accent-100);
}
.check input:disabled {
  background: var(--surface-sunken); border-color: var(--line-strong);
  box-shadow: none; cursor: not-allowed;
}
.check input:disabled:checked { background: var(--ink-300); border-color: var(--ink-300); }
/* A disabled control must carry the state on its LABEL too, or the text beside
   a greyed box still reads as available. */
.check:has(input:disabled) { color: var(--ink-400); cursor: not-allowed; }

/* ---------- File input ----------
   The button inside a file input is OS chrome that `appearance: none` does not
   reach. `::file-selector-button` is the only handle on it, so the control
   keeps the field skin and the button inside it is restyled to match `.btn`. */
input[type="file"].input {
  padding: var(--sp-2);
  cursor: pointer;
  line-height: 1.6;
}
input[type="file"].input::file-selector-button {
  margin-right: var(--sp-3);
  padding: 5px 11px;
  font-family: var(--font-ui); font-size: var(--text-sm); font-weight: 550;
  color: var(--ink-700); background: var(--surface-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  cursor: pointer;
}
input[type="file"].input::file-selector-button:hover { border-color: var(--ink-300); }

/* ---------- Field, the label + control + hint unit ----------
   The vertical rhythm that hand-set margins kept getting wrong. A field is
   always label, then control, then optional hint or error, and the spacing
   between them belongs here rather than at each call site. */
.field { display: flex; flex-direction: column; gap: var(--sp-2); }
.field + .field { margin-top: var(--sp-4); }
.field-error { font-size: var(--text-xs); color: var(--err-600); }
.field-req { color: var(--err-600); margin-left: 2px; }

/* A row of fields that share a line on desktop and stack on narrow screens.
   auto-fit rather than a fixed count, so it needs no per-screen breakpoint. */
.field-row {
  display: grid; gap: var(--sp-3) var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin-top: var(--sp-4);
}
.field-row .field + .field { margin-top: 0; }
/* UX Phase 6: a field after a row of fields keeps the field rhythm, and a row
   whose labels wrap to different heights lines its controls up at the bottom. */
.field-row + .field { margin-top: var(--sp-4); }
.field-row { align-items: end; }

/* A CONTROL WITH ITS ACTION ATTACHED: a link box and the button that reads it.
   Added 2026-09-05, on the owner's report that the import screen's "Read the
   link" button "does not look like a button".

   IT DID NOT, AND FOR TWO REASONS THAT COMPOUNDED.

   1. `.btn` and `.input` wear nearly the same skin. Both take
      `background: var(--surface-2)` and a 1px border -- deliberately, so a
      field sits beside a button without looking foreign (see the note above
      `.input`). Side by side and equally wide, that similarity stops being
      harmony and becomes a second text box.

   2. The markup reached for `.field-row`, whose track is
      `repeat(auto-fit, minmax(180px, 1fr))`. That is a container OF fields
      sharing a line, so it stretched the button to a full 1fr -- half the row,
      the same width as the input beside it.

   So this is the pattern `.field-row` is not: ONE control that flexes, and one
   action sized to itself. It is used as the CHILD of a `<Field>`, where
   `.field`'s own `gap: var(--sp-2)` does the spacing -- hence no `margin-top`
   here, unlike `.field-row`, which is a sibling container and carries its own.
   Using `.field-row` inside a field was adding 16px on top of that gap.

   ★ `min-width` ON THE BUTTON RATHER THAN `width`, AND IT IS NOT TIMIDITY.
   The label changes while the fetch runs -- "Read the link" becomes "Reading…"
   -- and a button sized to its text would shrink under the pointer at the
   moment it was clicked. A floor holds it still. A hard `width` would do that
   too and would clip the first translation longer than the English: Hungarian
   already renders "Link beolvasása". */
.field-attach { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }
/* `width: auto` overrides `.input`'s `width: 100%`, which inside a flex row
   fights the basis rather than filling it. */
.field-attach > .input { flex: 1 1 220px; width: auto; }
.field-attach > .btn { flex: 0 0 auto; min-width: 10rem; justify-content: center; }

/* ★ ONCE IT WRAPS, THE ACTION TAKES THE WHOLE LINE. Owner, 2026-09-05: "on the
   mobile view the fixed width button looks off to me" -- and it did. A 160px
   button sitting under a full-width field leaves a wide empty gap to its right,
   which reads as a control that failed to lay out rather than one sized on
   purpose. The floor that keeps it steady beside a field is exactly what
   strands it beneath one.
   640px is this stylesheet's existing narrow breakpoint, not a new one. */
@media (max-width: 640px) {
  .field-attach > .btn { flex: 1 1 100%; }
}

/* A NUMBER AND THE UNIT IT IS COUNTED IN, side by side.
   The owner, 2026-09-12: "I think it's necessary to make the units visible
   when we are asking for time and amount data, to clarify what units we mean
   and how the answer is requested."

   ★ THE UNIT IS TEXT AND NOT A SECOND CONTROL, which is the whole distinction
   from `DimensionFields`, where the unit is a `<select>` because the writer
   chooses it. Here the line already HAS its units -- `amount_unit` and
   `time_unit` on `app.budget_line` -- and an actual is recorded in the line's
   own unit or it is not comparable. Rendering it as a control would invite
   somebody to change it and would silently make the plan and the actual
   incommensurable. So it is a fact shown beside the box, not a field.

   Sized to itself, so the number keeps the room: a 3ch unit like `day` must
   not take half the column off a `set`. */
.field-unit { display: flex; align-items: center; gap: var(--sp-2); }
.field-unit > .input { flex: 1 1 auto; width: auto; min-width: 0; }
.field-unit-suffix {
  flex: 0 0 auto; white-space: nowrap;
  font-size: var(--text-sm); color: var(--ink-500);
}

/* A toolbar above a list: filters on the left, actions on the right. Replaces
   the hand-built flex rows with inline gaps that drifted between screens. */
.toolbar {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--sp-3); margin: var(--sp-4) 0;
}
.toolbar > .field { margin-top: 0; }
.toolbar-spacer { margin-left: auto; }

/* A button that NAVIGATES rather than submits. `.link` alone is not enough on
   a <button>: the element keeps its user-agent background, border and centred
   text, so a text link rendered as a button showed up as a grey chip. The
   reset is scoped to button.link so an <a class="link"> is untouched. */
button.link {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0;
  font: inherit; text-align: left; cursor: pointer;
}

/* ---------- Row ----------
   `.row` WAS USED TWENTY TIMES AND DEFINED NOWHERE. Measured 2026-07-29 across
   src/screens and src/system: GuestsScreen and PerformersScreen, the two
   newest screens, between them wrote `className="row"` twenty times against no
   rule at all. Eighteen carried an inline style that did the real work, and
   the class was decoration. TWO CARRIED NOTHING, so they laid out as plain
   blocks while reading as rows in the source.

   The inline values were all the same handful, `gap: 8` with an occasional
   `gap: 4` and an `alignItems` of `flex-end` or `center`, so the rule below is
   the common case and the two modifiers are the rest. The toolbar shape,
   `gap: 8` plus `align-items: flex-end` plus `margin: 16px 0`, appeared four
   times and is `.toolbar` above rather than a third modifier here. */
/* ★★ ALL THREE ARE ROWS. `.row-end` and `.row-tight` were written as
   MODIFIERS -- one adjusting alignment, the other the gap -- and each assumed
   `.row` was beside it. Measured 2026-09-07: it usually is not. 32 call sites
   carry `row-tight` alone and 18 carry `row-end` alone, against 6 and 13 that
   pair them properly.

   Alone they did nothing at all: `gap` and `align-items` are inert on a box
   that is not a flex container, so fifty places on this product were laying
   their children out as inline boxes on a TEXT BASELINE. Mostly that passed
   unnoticed, because JSX separators put a space where the gap should have been
   and most rows hold controls of one height.

   It stopped passing on the event screen, where a row holds two labelled
   buttons and one icon-only button. An inline-flex box's baseline is its first
   line of text, and a button containing only an SVG has no text -- so its
   baseline is its bottom edge and it sat visibly higher than its neighbours.
   The owner: "the three buttons are not aligned."

   A class called `row-tight` is a row. Widening the selector fixes all fifty
   at once and leaves `row row-tight` behaving exactly as before, since the
   same declarations simply apply twice. The two modifiers keep their overrides
   BELOW, so the cascade still gives each its own alignment and gap. */
.row, .row-end, .row-tight { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* Controls of different heights on one line align on their BASELINE, which for
   a label-above-field pair means the fields line up rather than the labels. */
.row-end { align-items: flex-end; }
.row-tight { gap: var(--sp-1); }

/* ---------- THE ACTIONS THAT CLOSE A BLOCK ----------
   `.row` says the buttons sit on one line and says NOTHING about the space
   above them, because the global reset means nothing has any. Written out, a
   row of actions flush against the text it acts on reads as part of that text:
   the Scenario Lab's four decision buttons sat directly against the inject that
   prompted them, and the knowledge check's Submit sat against the last option
   of the last question.

   ONE STEP, AND NOT THE `.toolbar` STEP. `.toolbar` is the same shape ABOVE a
   list and carries --sp-4 on both sides, because it separates two things that
   are not related. These actions BELONG to the block they close, so they take
   the smaller step and nothing below.

   A MODIFIER, worn as `class="row action-row"`: five call sites across the
   three Academy panels each wrote the same `marginTop` inline, and two of them
   wrote --sp-1 rather than --sp-2 for the identical relationship. One step is
   the point of having a step. */
.action-row { margin-top: var(--sp-2); }

/* ---------- Vertical rhythm ----------
   tokens.css opens with `* { margin: 0 }`, so NOTHING in this app carries a
   default margin and every gap has to be asked for. Until 2026-07-29 there was
   no utility to ask with, and the screens built latest asked for nothing: on
   /inventory?tab=catalogue the heading, the intro paragraph, the search field,
   the button and the table all sat flush against one another with no space at
   all.

   PUT ON THE PAGE WRAPPER AND NOT ON `.page` ITSELF, deliberately. Eighteen
   screens already space their own blocks and read correctly, and blanket
   rhythm on `.page` would add a second margin to every one of them. A screen
   opts in, so nothing that already looks right can regress. */
.stack > * + * { margin-top: var(--sp-4); }
.stack-2 > * + * { margin-top: var(--sp-2); }
.stack-6 > * + * { margin-top: var(--sp-6); }

/* ---------- A label that WRAPS its control ----------
   101 of them across seven screens, written `<label>Caption <input/></label>`
   with no class at all. A label is INLINE by default, so the caption and the
   control render on one line with no gap, and the caption gets the control's
   type rather than a label's. That is the shape reported on the catalogue tab.

   Scoped to `.page` and to labels carrying NO class, so `.check` and
   `.input-label` are untouched. Fixing this here rather than by rewriting 101
   call sites keeps the change to one rule and cannot restructure a text node
   by accident. */
.page label:not([class]) {
  display: block;
  font-size: var(--text-xs);
  color: var(--ink-400); font-weight: 600;
}
/* The control inside must not inherit the caption's small caps. `.input`
   already sets its own font-size, colour and family; these two are the ones
   that would otherwise cascade in. */
.page label:not([class]) > .input { margin-top: var(--sp-2); }
.input { text-transform: none; letter-spacing: normal; }
/* Two labelled controls in a row need air between them, which the label's own
   display:block does not provide. */
.page label:not([class]) + label:not([class]) { margin-top: var(--sp-4); }

/* ============================================================================
   THE FIFTEEN PHANTOM CLASSES. Added 2026-07-29.
   ----------------------------------------------------------------------------
   Every name below was already written in the markup and defined in NO
   stylesheet. A phantom class is worse than no class: it reads as intentional
   in the source, survives review because the JSX looks styled, and renders as
   nothing at all. Three separate spacing reports on /inventory, /files and
   /venues traced back here.

   The definitions match how each name is ALREADY used, read off the call
   sites, rather than a new opinion about how those screens should look. */

/* A screen body that renders one root element. Same rhythm as `.stack`, under
   the name three screens already use for it. */
.screen > * + * { margin-top: var(--sp-4); }
/* When a body renders ONE root and the page wrapper's `> * + *` therefore
   matches nothing, the rhythm belongs to that root's children. */
.stack > :only-child > * + * { margin-top: var(--sp-4); }

/* Title on the left, primary action on the right. */
.screen-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
}
/* A page header block: back link, title, intro. Tighter than the page rhythm
   because these three lines belong together. */
.page-head > * + * { margin-top: var(--sp-2); }
.page-head { margin-bottom: var(--sp-5); }

/* ---------- THE INTRO SPACES ITSELF. Owner, 2026-08-06 ----------

   MEASURED, not a taste call. `.page-intro` had `margin-top` and no
   `margin-bottom`, and the space below it came entirely from `.page-head`'s
   own `margin-bottom`. 13 screens wrap their header in `.page-head` and get
   20px. THE OTHER 19 DO NOT, and their intro paragraph sat flush against the
   table beneath it: measured 0px on `/events`, and the same on Projects,
   Contacts, Companies, the Dashboard, Agenda, Settings and thirteen more.

   The rule went on `.page-intro` rather than on the 19 screens, because a
   screen forgetting a wrapper class is exactly the kind of thing that happened
   19 times and will happen again. The reset inside `.page-head` keeps the 13
   correct screens at 20px rather than making them 20 + 20: the head is
   already the thing spacing itself, and two margins would collapse to the
   larger only if nothing sat between them, which `.page-head`'s own children
   do not guarantee. */
.page-intro { margin-bottom: var(--sp-5); }
.page-head .page-intro { margin-bottom: 0; }

/* Secondary line inside a table cell. The cell's own colour is `--ink-700`, so
   this has to sit below it to read as subordinate. */
.dt-sub { display: block; font-size: var(--text-xs); color: var(--ink-400); }

/* A row of figures above a budget. */
.totals {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5);
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-sunken); border: 1px solid var(--line);
  border-radius: var(--radius); font-size: var(--text-sm); color: var(--ink-500);
}
.totals strong { color: var(--ink-900); font-weight: 600; }

/* An in-page destructive confirmation. There is no window.confirm in this
   application, so this block IS the confirmation and it has to look like one. */
.danger-zone {
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-4); border: 1px solid var(--err-600);
  border-radius: var(--radius); background: var(--err-100); color: var(--err-600);
}
.danger-zone p { margin: 0; }

/* --- THE DELETE CONTROL. ---------------------------------------------------
   The owner, 2026-09-12: "the delete section should be just a red waste bin
   with a pop-up or drop-down box. It should not take this much space on the
   screen, and this applies to all the other drawers with delete buttons,
   sections and fields."

   ★★★ AT REST THIS IS ONE 28px ICON. The block above, with its heading, its
   note and its explanatory paragraph, was being rendered PERMANENTLY on every
   drawer that could delete anything -- a standing red slab for an action taken
   once in a record's life. The consequence text still has to be said, because
   several of these deletions take other rows with them, but it is said WHEN
   SOMEBODY REACHES FOR THE BIN and not before.

   ★★ AN INLINE DISCLOSURE RATHER THAN A FLOATING POPOVER, and that is measured
   rather than preferred. `.drawer-body` is `overflow-y: auto`, and CSS computes
   the other axis of a scroll container to `auto` as well, so an absolutely
   positioned panel hanging off a bin near the foot of a drawer would be CLIPPED
   by the very container it sits in -- and these controls sit at the end of the
   body, which is exactly where the clipping is worst. An inline strip cannot be
   clipped, needs no measuring JavaScript, and stays out of the z-index order
   that already runs 100 for the drawer, 102 for the wide one and 120 for the
   date popup. It is also honest about what it is, which is the same argument
   `.moremenu` makes for not claiming `role="menu"`.

   ★ THE CONFIRM REUSES `.danger-zone` rather than restating its colours, so
   there is one red panel in this stylesheet and not two that can drift apart. */
.delzone { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
/* Red AT REST here, which `.icon-btn-danger` deliberately is not: that class
   tints on hover because a row of table actions would otherwise read as a row
   of alarms. A lone bin is the alarm, and the owner asked for a red one. */
.delzone > .icon-btn { color: var(--err-600); }
.delzone > .icon-btn:hover:not(:disabled) {
  background: var(--err-100); border-color: var(--err-600);
}
/* A measure, not a width: the consequence sentences here run to two lines and
   a full-drawer-wide red band is the slab this control exists to remove. */
.delzone-confirm { max-width: 52ch; }

/* --- THE ARCHIVE CONTROL. W499, D-1131. -------------------------------------
   The delete control's LAYOUT, and deliberately NOT its colour. An archive is
   reversible -- it writes three columns and a restore clears exactly those --
   so it must not wear the one red panel this stylesheet keeps for acts that
   cannot be undone. DeleteControl's header records the survey that found only
   14 of 44 red blocks were deletions; a red archive would make it 31 of 45.
   Inline, not a popover, for the clipping reason `.delzone` gives above. Every
   colour is a token that already has a dark value. */
.archzone { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
.archzone-confirm {
  display: flex; flex-direction: column; gap: var(--sp-3); width: 100%; max-width: 52ch;
  padding: var(--sp-4); border: 1px solid var(--line-strong);
  border-radius: var(--radius); background: var(--surface-sunken); color: var(--ink-900);
}
.archzone-confirm p { margin: 0; }

/* The budget line owner sub-editor and its rows. */
.owner-editor > * + * { margin-top: var(--sp-3); }
.owner-editor h4 { font-size: var(--text-sm); font-weight: 640; color: var(--ink-900); }
.owner-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--line);
}
.owner-row:last-child { border-bottom: 0; }

/* A content card and its header. `.panel-card` is the venue screen's own name
   for what `.page-card` already is, so it is defined as the same thing rather
   than renamed at the call sites. */
.panel-card {
  margin-top: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--line);
  border-radius: var(--radius-lg); background: var(--surface-1);
}
.card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-3);
}
.card-title { font-size: var(--text-lg); font-weight: 640; color: var(--ink-900); }

/* A definition list rendered as a two column key/value grid. */
.kv {
  display: grid; grid-template-columns: minmax(120px, max-content) 1fr;
  gap: var(--sp-2) var(--sp-4); margin: 0; font-size: var(--text-sm);
}
.kv dt { color: var(--ink-400); }
.kv dd { color: var(--ink-900); font-weight: 500; margin: 0; }

/* THE SAME LIST READ AS A STACK, and the difference is the length of the value.
   `.kv` puts the term BESIDE it, which is right for "Status: confirmed" and
   wrong for an Academy competency whose value is a full sentence saying what
   good looks like: the grid's first column is `minmax(120px, max-content)`, so
   at 375px that sentence would be left about a third of a 231px card. Here the
   term leads and the line explaining it sits directly under it.

   THE `dd` RULE IS NOT COSMETIC. The description carries `.input-hint`, which
   brings a `margin-top: var(--sp-2)` meant for a hint under a FIELD. Under its
   own term that margin reads as a gap between two halves of one thought, so it
   is removed here rather than at each call site -- which is where it was, as
   `style={{ margin: 0 }}` on every row. `.deflist dd` outranks `.input-hint`
   on specificity, so this holds wherever the two meet. */
.deflist > div + div { margin-top: var(--sp-1); }
.deflist dd { margin-top: 0; }

/* The venue space tree. One indent level is drawn by an inline padding-left at
   the call site, so this supplies only the row shape. */
.tree-block + .tree-block { margin-top: var(--sp-4); }
.tree-row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding-top: var(--sp-2); padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--line); font-size: var(--text-sm);
}

/* The open event's name in the context bar. It is the one item there that can
   be arbitrarily long, so it is the one that truncates. */
.context-bar .cb-name { min-width: 0; }
.context-bar .cb-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- Fieldset and legend ----------
   NEITHER WAS EVER STYLED, and the global `* { margin: 0; padding: 0 }` reset
   in tokens.css does NOT touch `border`. Every browser applies
   `border: 2px groove` to a fieldset, so the Production and Work permit groups
   in the asset drawer rendered as inset grooved boxes: the "windows" reported
   on 2026-07-29. Five fieldsets across two screens were affected. */
fieldset {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  margin: 0; min-width: 0;
}
legend {
  padding: 0 var(--sp-2);
  font-size: var(--text-xs);
  color: var(--ink-400); font-weight: 600;
}

/* ---------- Consecutive checkboxes are a LIST ----------
   `.check` is inline-flex so a single toggle can sit inline beside other
   controls. That is right for one and wrong for four: the production group's
   four toggles flowed onto one line because nothing made them stack. A check
   that FOLLOWS another check is part of a list and goes block, and every check
   inside a fieldset does the same. The first of a run keeps inline-flex, and a
   block sibling after it starts a new line anyway. */
.check + .check { display: flex; margin-top: var(--sp-2); }
fieldset .check { display: flex; }
fieldset .check + .check { margin-top: var(--sp-2); }

/* ---------- Drawer body rhythm ----------
   `.drawer-body` sets padding and scrolling and NOTHING about the space
   between the blocks inside it. With the global margin reset that means a
   drawer's fields, notes and buttons all sat flush, and a `.btn` following a
   paragraph read as sitting on top of the text. Same rhythm as `.stack`, on
   the surface that needed it most. */
.drawer-body > * + * { margin-top: var(--sp-4); }

/* A label that wraps its control behaves the same inside a drawer as on a
   page. The earlier rule was scoped to `.page` alone, which left every drawer
   field rendering its caption inline against its input. */
.drawer-body label:not([class]) {
  display: block;
  font-size: var(--text-xs);
  color: var(--ink-400); font-weight: 600;
}
.drawer-body label:not([class]) > .input { margin-top: var(--sp-2); }

/* A notice that carries actions. The in-page delete confirmations put their
   buttons straight after the sentence, and with the global margin reset they
   sat against the last word. There is no window.confirm in this application,
   so these blocks ARE the confirmation and the choice has to be legible. */
.notice .btn { margin-top: var(--sp-3); }
.notice .btn + .btn { margin-left: var(--sp-2); }

/* ---------- OUT OF FLOW TAKES NO RHYTHM ----------
   `margin-top` ON A `position: fixed` ELEMENT WITH `top: 0` STILL MOVES IT.
   It is not ignored the way `width` or `float` would be: the margin offsets
   the box from the position `top` resolved to.

   `Drawer` renders its overlay and its panel as SIBLINGS inside the screen, so
   on any screen whose wrapper carries `.stack` both of them were picking up
   `margin-top: var(--sp-4)` from the rhythm rule and rendering 16px LOW.
   Measured 2026-07-29, and it produced two separate-looking bugs from one
   cause:

     the overlay started at y=16, leaving a 16px strip across the top of the
     page ungreyed, which is the "top couple of pixels are not covered" report

     the panel started at y=16 and is 100dvh tall, so its last 16px hung BELOW
     the viewport and the footer's bottom padding was simply cut off, which is
     the "the button sits at the very bottom" report

   Introduced by this file's own `.stack` rule. A rhythm utility must never
   reach an element that is not in the flow it is spacing.

   THE CLASS IS DOUBLED TO WIN ON SPECIFICITY, not for emphasis. `.stack > * + *`
   is (0,2,0), so a plain `.drawer` at (0,1,0) would lose. `.drawer.drawer` is
   (0,2,0) and sits later in the file, which beats it without `!important`. */
.drawer.drawer, .drawer-overlay.drawer-overlay { margin: 0; }

/* Motion is a preference, not a decoration. */
@media (prefers-reduced-motion: reduce) {
  /* The fold's chevron still turns, it simply arrives there. Removing the
     transition and not the transform: the direction it points is state, not
     decoration, and dropping it would leave the control saying nothing. */
  .fold > summary::after { transition: none; }
}

/* THE SECTION RULE. A label with a line running to the edge, which is the
   foundation's own `section-label` idea rendered with the tokens this app has. */
.dash-rule {
  display: flex; align-items: center; gap: var(--sp-3);
  font-size: var(--text-xs);
  font-weight: 700; color: var(--ink-500); margin: var(--sp-6) 0 var(--sp-3);
}
.dash-rule::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.ev-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--sp-4); }
/* A BUTTON, NOT A DIV. Every card here navigates, so it is keyboard reachable
   and announces itself as an action. D-491. */
.ev-card {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-4); border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--surface-1); text-align: left; cursor: pointer; min-height: 150px;
  font: inherit; color: inherit; width: 100%;
}
.ev-card:hover { border-color: var(--accent-500); }
.ev-card:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.ev-card-name { font-size: var(--text-md); font-weight: 650; color: var(--ink-900); }
.ev-card-where { font-size: var(--text-sm); color: var(--ink-500); }
.ev-card-foot { display: flex; align-items: flex-end; gap: var(--sp-3); margin-top: auto; }
/* THE DAY BLOCK. Big number over the month, which is what makes the card
   scannable by date in the reference. */
.ev-day { line-height: 1; text-align: center; min-width: 44px; }
.ev-day b { display: block; font-size: var(--text-xl); font-weight: 700; color: var(--ink-900); }
.ev-day span { display: block; font-size: var(--text-xs); color: var(--ink-500); font-weight: 600; }
.ev-card-dates { font-size: var(--text-sm); color: var(--ink-500); }
/* THE ACTION TILES, dashed so they read as "add" rather than as a record. */
.ev-action {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-2); padding: var(--sp-4); min-height: 150px;
  border: 1px dashed var(--line-strong); border-radius: var(--radius-lg);
  background: var(--surface-sunken); cursor: pointer; font: inherit;
  color: var(--ink-700); font-weight: 600; width: 100%;
}
.ev-action:hover { border-color: var(--accent-500); color: var(--accent-600); }
.ev-action:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }

/* The compact counts strip. Numbers big enough to read at a glance, which
   `.fact-value` at --text-sm is deliberately not. */
.dash-stats { display: flex; flex-wrap: wrap; gap: var(--sp-5); }
.dash-stat b { display: block; font-size: var(--text-xl); font-weight: 700; color: var(--ink-900); line-height: 1.1; }
.dash-stat span { font-size: var(--text-xs); color: var(--ink-500); font-weight: 600; }

.ev-area-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: var(--sp-3); }
/* A BUTTON, because every tile navigates INTO that area carrying the event id.
   That is the half of the left-menu complaint a picker alone did not fix. */
.ev-area {
  display: flex; flex-direction: column; gap: 2px; padding: var(--sp-3);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1);
  text-align: left; cursor: pointer; font: inherit; width: 100%;
}
.ev-area:hover { border-color: var(--accent-500); }
.ev-area:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.ev-area b { font-size: var(--text-xl); font-weight: 700; color: var(--ink-900); line-height: 1.1; }
.ev-area span { font-size: var(--text-xs); color: var(--ink-500); font-weight: 600; }
.ev-area em { font-size: var(--text-xs); font-style: normal; }

/* ---------- The group dashboard's area tiles. 2026-07-30 ----------
   The same tile as the event dashboard's, plus the surface's own mark. The
   icon is what the collapsed rail shows for that surface, so the tile and the
   menu row a reader arrives from carry the same picture. */
.grp-area { gap: var(--sp-2); }
.grp-area-icon { display: block; color: var(--ink-500); }
.grp-area-icon svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 1.7; }
.grp-area:hover .grp-area-icon { color: var(--accent-600); }

/* ---------- The venue map box. 2026-07-30 ----------
   THE ADDRESS SITS ABOVE THE MAP AND IS ALWAYS RENDERED. An embedded map is a
   picture of a place; the address is the thing you can copy, read down a phone
   or paste elsewhere. The map never replaces it. */
.venue-map { display: flex; flex-direction: column; gap: var(--sp-2); }
.venue-map-addr { font-size: var(--text-sm); color: var(--ink-700); }
.venue-map-frame {
  width: 100%; border: 1px solid var(--line); border-radius: var(--radius);
  display: block; background: var(--surface-sunken);
}
/* The two absences, one shape. A missing key and a missing address need
   different fixes, so they carry different copy, but both are a quiet well
   rather than an error: neither is a fault. */
.venue-map-empty {
  display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-start;
  padding: var(--sp-4); border: 1px dashed var(--line-strong);
  border-radius: var(--radius); background: var(--surface-sunken);
  font-size: var(--text-sm);
}

/* ---------- Google Places suggestions under the venue search. 2026-07-30 ----
   NOT AN OVERLAY. A floating dropdown over the results would cover the very
   rows the reader is comparing the suggestion against, and it would need focus
   trapping and outside-click handling to be usable by keyboard. This sits IN
   the flow, pushing the list down, which costs a little movement and needs
   none of that machinery. */
.places-box {
  display: flex; flex-direction: column; gap: 1px;
  margin-top: var(--sp-2); padding: var(--sp-2);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-1);
}
.places-hit {
  display: flex; flex-direction: column; gap: 1px; width: 100%;
  padding: var(--sp-2) var(--sp-3); border: 0; border-radius: var(--radius-sm);
  background: transparent; text-align: left; font: inherit; color: var(--ink-900);
  cursor: pointer;
}
.places-hit:hover { background: var(--surface-sunken); }
.places-hit:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }
.places-hit span { font-size: var(--text-sm); }
.places-note { font-size: var(--text-xs); padding: var(--sp-1) var(--sp-3); }

/* ==========================================================================
   W315. THE GROUP HEADING IN A MERGED SUGGESTION LIST.

   ★ IT IS NOT `.places-note`, AND THAT DISTINCTION IS THE POINT. The merged
   list holds two kinds of answer -- venues this tenant already maintains, and
   map references from Google -- and the FIRST draft titled both groups with
   `.places-note muted`, the same class the Google attribution footnote uses.
   A heading and a footnote then rendered identically, so a reader scrolling
   past the boundary had nothing telling them which half they were in, and
   picking a Google row for a building already in the directory -- the exact
   duplication this merge exists to prevent -- stayed just as easy as before.

   A heading has to look like a heading for the grouping to do any work.
   ========================================================================== */
.places-group {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-500);
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
}
/* Not above the first group: the box's own top padding is the space there,
   and a rule hanging under nothing reads as a stray line. */
.places-group + *, .places-group:first-child { border-top: 0; }
.places-group:not(:first-child) {
  margin-top: var(--sp-1);
  border-top: 1px solid var(--line);
  padding-top: var(--sp-3);
}

/* ---------- Owner display rules, 2026-07-30 ----------

   RULE 1. A ZERO COUNT READS AS ATTENTION, NOT AS DATA. Owner: "when there is
   a zero number, for example zero budget or zero agenda, let's make it red,
   showing that there's an error on that."

   IT IS NOT COLOUR ALONE, which D-491 forbids and which a red number on its own
   would be. The tile also carries the word "none yet" under it, so the state
   survives a reader who cannot separate the two hues, a greyscale print and a
   screen reader. The colour is the glance; the word is the fact. */
.ev-area.is-zero b { color: var(--err-600); }
.dash-stat.is-zero b { color: var(--err-600); }

/* RULE 3. SUNDAY IS RED, as a traditional calendar marks it. Applied to the
   column heading AND to the day number, because the heading alone would leave
   the days themselves unmarked once the reader has scrolled past it.

   IT IS NOT COLOUR ALONE EITHER. The heading carries an abbreviation a reader
   already knows the position of, and every date rendered outside the grid now
   spells the weekday in words, which is the other half of the owner's rule. */
/* On the PAGE, `--err-600` clears AA in both themes: 5.06 light, 5.28 dark. */
.mini-cal-grid th.is-sunday,
.mini-cal-grid td.is-sunday span.mini-cal-day { color: var(--err-600); }

/* ---------- The document surface. W31.5 ----------------------------------
   Owner 2026-07-31: the documents module "doesn't look like a document yet".

   IT DID NOT, AND THE REASON WAS STRUCTURAL RATHER THAN COSMETIC. A document
   was a `.dt` data table of clause rows: name, state, decided. That is the
   right shape for a LIST of documents and the wrong one for a document, which
   is a titled page with numbered prose. This block is the page.

   BUILT HERE like `.dt` was, for the same reason: the foundation renders lists
   and forms and has no document component, so this is a new system component,
   token-styled to match. Tokens only, no raw hex.

   IT IS NOT A SIMULATED SHEET OF A4. No drop shadow, no fixed 210mm width, no
   paper texture. Those make a screen pretend to be a printout and they fight
   the real one: the browser already paginates properly at print time, and a
   hard millimetre width would letterbox the document on a phone. The screen
   gets a readable measure; the printer gets the page. */
.doc-page {
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--sp-8);
  max-width: 76ch;                 /* a reading measure, not a paper size */
}
.doc-title {
  font-size: var(--text-xl); font-weight: 650; color: var(--ink-900);
  line-height: 1.25; margin-bottom: var(--sp-2);
}
.doc-meta {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4);
  padding-bottom: var(--sp-4); border-bottom: 1px solid var(--line);
}
.doc-meta-item { font-size: var(--text-xs); color: var(--ink-500); }
.doc-meta-item b { color: var(--ink-700); font-weight: 550; }

.doc-clause { margin-top: var(--sp-6); }
.doc-clause-head {
  display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-2);
}
.doc-clause-no {
  font-variant-numeric: tabular-nums; font-weight: 650;
  color: var(--ink-500); font-size: var(--text-md);
}
.doc-clause-name {
  font-size: var(--text-lg); font-weight: 600; color: var(--ink-900); line-height: 1.3;
}
.doc-body { color: var(--ink-700); font-size: var(--text-md); line-height: 1.65; }
.doc-p { margin-top: var(--sp-3); }
.doc-p:first-child { margin-top: 0; }
.doc-h {
  margin-top: var(--sp-4); font-size: var(--text-md);
  font-weight: 650; color: var(--ink-900);
}
.doc-list { margin-top: var(--sp-3); padding-left: var(--sp-5); }
.doc-list li { margin-top: var(--sp-1); }
.doc-body code {
  font-family: var(--font-mono); font-size: var(--text-sm);
  background: var(--surface-sunken); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 0 4px;
}
.doc-empty {
  margin-top: var(--sp-6); padding: var(--sp-6);
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  color: var(--ink-500); font-size: var(--text-sm); text-align: center;
}

/* The formatting bar above the clause editor. */
.doc-toolbar {
  display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-2);
}
.doc-toolbar .btn { font-family: var(--font-mono); }
.doc-editor { font-family: var(--font-mono); line-height: 1.6; }

/* ---------- Print. This is the PDF export ---------------------------------
   `exportPdf()` sets the title and calls window.print(). Everything that is
   not the document is hidden here rather than in JavaScript, so Cmd-P produces
   the same clean page as the button does.

   VISIBILITY AND NOT DISPLAY. Setting `display: none` on ancestors would
   remove the document with them, because it is a descendant of the shell.
   `visibility: hidden` on everything and `visible` on the page and its
   children is the one combination that hides an ancestor while keeping a
   descendant on the page. */
/* ---------------------------------------------------------------------------
   W602. THE SHOWBOOK, E-451 SECTION 9.
   Its design rule is that a caller scans the current line, the next line and
   the immediate risk in SECONDS, so the type is small, the rows are tight and
   the segment bands are the only heavy thing on the page.
   --------------------------------------------------------------------------- */
.showbook { font-size: 0.85rem; width: 100%; }
.showbook th, .showbook td {
  vertical-align: top;
  padding: 0.3rem 0.45rem;
  line-height: 1.25;
}
.showbook thead th {
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.showbook-segment th {
  background: var(--surface-2, #f1f2f4);
  font-size: 0.8rem;
  letter-spacing: 0.05em;
  padding-top: 0.6rem;
}

@media print {
  /* ★★ THE SAME REPAIR W323 MADE TO THE AGENDA SHEET, and the same defect: the
     rules below originally pulled the page to the origin with `position:
     absolute; top: 0; left: 0`, and an absolutely positioned box whose
     ancestors are all static resolves against the initial containing block --
     the PAGE BOX, not the page content box. So `@page { margin }` was stepped
     over and the document printed to the paper's edge.

     Found by generating an actual PDF with headless Chrome and looking at it,
     which is the only way this class of bug is ever found: every gate in this
     project was green, and the screen looked right. Fixed here as well as on
     the agenda because it is one root cause with two call sites, and leaving
     the older one wrong would mean the next person to copy these rules copies
     the bug a third time. */
  /* ★★ `.demo-banner` IS DELIBERATELY NOT NAMED HERE. It was, from W324 until
     W368, and that put the string back into the always-shipped stylesheet --
     which is the ONE thing `src/system/demo-banner.css` exists to prevent, as
     its own header says: CSS is not tree-shaken, so an unreferenced class in
     THIS file ships in full. `verify-demo-bridge-dev-only.sh` had been red on
     every push since, and the print-hide now lives in that dev-only file. */
  .rail, .panel, .topbar { display: none !important; }
  .app, .main, .content, .page {
    display: block !important;
    height: auto !important; max-height: none !important;
    overflow: visible !important;
    margin: 0 !important; padding: 0 !important;
    background: #fff !important;
  }
  /* A body background PROPAGATES TO THE CANVAS in print, and body carries a
     grey. Without this every part of the paper the white page did not cover
     came out grey. */
  html, body { background: #fff !important; }

  body * { visibility: hidden; }
  .doc-page, .doc-page * { visibility: visible; }
  .doc-page {
    position: static;
    width: auto; max-width: none;
    border: 0; border-radius: 0; padding: 0; margin: 0;
    background: #fff; color: #000;
  }
  /* Ink, not tokens: a themed page must not print white-on-white when the
     reader happens to be in dark mode. */
  .doc-page .doc-title, .doc-page .doc-clause-name, .doc-page .doc-h { color: #000; }
  .doc-page .doc-body, .doc-page .doc-meta-item { color: #111; }
  .doc-page .doc-meta { border-bottom: 1px solid #999; }
  /* W602. A SHOWBOOK ROW IS ONE MOMENT AND MUST NOT SPLIT ACROSS A PAGE: half
     a moment at the foot of a page is the one thing a caller cannot read in
     seconds. The header repeats on every sheet, because a column nobody can
     name is a column nobody trusts. */
  .showbook tr { break-inside: avoid-page; page-break-inside: avoid; }
  .showbook thead { display: table-header-group; }
  .showbook-segment th { break-after: avoid-page; page-break-after: avoid;
                         background: #eee !important; color: #000 !important; }
  .showbook th, .showbook td { border-bottom: 1px solid #ccc; color: #000; }

  /* A clause should not be split from its own heading. */
  .doc-clause { break-inside: avoid-page; page-break-inside: avoid; }
  .doc-clause-head { break-after: avoid-page; page-break-after: avoid; }
  .doc-empty, .doc-toolbar { display: none; }
  @page { margin: 18mm; }
}

/* --- W101. THE MINI BUDGET LINE. E-447. -----------------------------------
   Bubble's budget row is a name, a strip of value-over-label cells, and a
   money figure on the right. The cells reuse `.fact` rather than inventing a
   second value-over-label pattern; only the container is new, because
   `.fact-grid` carries its own page margins and a sunken fill that would
   fight inside a compact row. */
.mb-line {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-3); background: var(--surface-1);
  border: 1px solid var(--line); border-radius: var(--radius);
}
/* A losing competing quote. Dimmed, never hidden, and the "Not counted" badge
   beside it carries the meaning so this is not state by colour alone. D-491. */
.mb-line-off { opacity: .62; border-style: dashed; }
.mb-line-head {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  align-items: flex-start; justify-content: space-between;
}
.mb-line-name { display: flex; flex-direction: column; gap: 2px; min-width: min(100%, 16ch); }
.mb-line-title { font-size: var(--text-md); font-weight: 600; color: var(--ink-900); }
.mb-line-cells {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: var(--sp-2) var(--sp-4);
  padding-top: var(--sp-2); border-top: 1px solid var(--line);
}
.mb-line-foot {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3);
  align-items: baseline; justify-content: space-between;
}
.mb-line-total {
  font-size: var(--text-lg); font-weight: 640; color: var(--ink-900);
  font-variant-numeric: tabular-nums;
}

/* --- W103. THE AGENDA'S GAP AND OVERLAP BANDS. -----------------------------
   Owner 2026-08-06: a gap between two sessions must be visible rather than
   arithmetic the reader does in their head.

   THE COLOUR IS NEVER THE ONLY SIGNAL. D-491. Each band spells out its own
   length and its own kind, so the distinction survives a monochrome print,
   a colour-blind reader and a screenshot in a chat. */
.dt-band > td { padding: 0 !important; background: transparent; }
.ag-band {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-1) var(--sp-3);
}
.ag-band-rule { flex: 1; height: 2px; border-radius: 2px; }
.ag-band-text {
  font-size: var(--text-xs); font-weight: 600;
  white-space: nowrap;
}
/* A gap is a QUESTION: empty air a producer may have meant. */
.ag-band-gap .ag-band-rule { background: var(--warn-700); opacity: .45; }
.ag-band-gap .ag-band-text { color: var(--warn-700); }
/* An overlap is a FAULT: two sessions booked on top of each other. */
.ag-band-overlap .ag-band-rule { background: var(--err-600); }
.ag-band-overlap .ag-band-text { color: var(--err-600); }
/* Out of order is a fault of the LIST rather than of the programme: the two
   sessions may be perfectly fine and simply shown in the wrong sequence.
   Dashed, so it reads as a sorting problem rather than a clash. */
.ag-band-out-of-order .ag-band-rule {
  background: none; height: 0;
  border-top: 2px dashed var(--mag-600); border-radius: 0;
}
.ag-band-out-of-order .ag-band-text { color: var(--mag-600); }

/* --- W104. ICON-ONLY ACTIONS. ---------------------------------------------
   The PADDING is the target, not the glyph. WCAG 2.2 AA 2.5.8 wants 24x24 and
   D-491 binds this project to it, so the box is sized here and the icon inside
   it stays small enough to read as a mark rather than a picture. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-width: 28px; min-height: 28px; padding: 4px;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-500); cursor: pointer;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.icon-btn svg {
  width: 18px; height: 18px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The play triangle is a solid mark; a stroked one reads as an outline arrow. */
.icon-btn svg path[d^="M8.4 5.6"] { fill: currentColor; }
.icon-btn-text { font-size: var(--text-sm); font-weight: 550; }
.icon-btn:hover:not(:disabled) {
  background: var(--surface-sunken); color: var(--ink-900); border-color: var(--line);
}
.icon-btn-danger:hover:not(:disabled) {
  background: var(--err-100); color: var(--err-600); border-color: var(--err-600);
}
.icon-btn:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 1px; }
.icon-btn:disabled { opacity: .45; cursor: not-allowed; }
/* The actions cell. Right-aligned, as asked, and it does not wrap: three marks
   that reflow onto two lines look like a broken row rather than a tidy one. */
.row-actions { display: inline-flex; gap: var(--sp-1); justify-content: flex-end; }

/* --- W105. THE CALENDAR PICKER. -------------------------------------------
   The native popup is drawn by the browser outside the document, so an OK
   button means owning the calendar. This is that calendar. */
.datefield { position: relative; display: flex; flex-direction: column; gap: 4px; }
.datefield-row { display: flex; gap: var(--sp-2); align-items: stretch; }
.datefield-row .input { flex: 1; min-width: 0; }
.datefield-open {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 34px; padding: 0 var(--sp-2);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface-2); color: var(--ink-500); cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
.datefield-open svg {
  width: 17px; height: 17px; fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round;
}
.datefield-open:hover:not(:disabled) { background: var(--surface-sunken); color: var(--ink-900); }
.datefield-open:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 1px; }
.datefield-open:disabled { opacity: .5; cursor: not-allowed; }

.datepop {
  /* FIXED AND PORTALLED TO THE BODY. Absolutely positioned in place it was
     clipped by the `<details>` Fold's `overflow: hidden` and the OK button
     became unclickable, measured 2026-08-06. The coordinates come from the
     trigger's rect; see DateField.tsx.

     Z-INDEX 120, ABOVE THE DRAWER'S 100 AND ITS OVERLAY'S 99.
     The ladder in this app runs: shell rail 20, menus 40, topbar 90, demo
     banner 95, drawer overlay 99, drawer 100. A date field's most common home
     is INSIDE a drawer, so a popup launched from one has to clear it. At 60 it
     was portalled correctly and still unclickable, because the drawer painted
     over it: elementFromPoint over the OK button returned div.drawer-body.
     Anything launched FROM a drawer belongs above the drawer. */
  position: fixed; z-index: 120;
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-3); width: max-content;
  background: var(--surface-1); border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-md);
}
.datepop-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.datepop-head .icon-btn svg { stroke-width: 1.8; }
.datepop-month { font-size: var(--text-md); font-weight: 620; color: var(--ink-900); }
.datepop-weekdays, .datepop-grid {
  display: grid; grid-template-columns: repeat(7, 34px); gap: 2px;
}
.datepop-weekdays span {
  text-align: center; font-size: var(--text-xs); font-weight: 600;
  color: var(--ink-400);
}
/* ONE TAB STOP for the whole grid; the arrows move inside it. */
.datepop-grid:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 3px; border-radius: var(--radius-sm); }
.datepop-cell {
  display: flex; align-items: center; justify-content: center;
  height: 32px; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-900);
  font-size: var(--text-sm); cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.datepop-blank { cursor: default; }
.datepop-cell:hover:not(:disabled):not(.datepop-blank) { background: var(--surface-sunken); }
.datepop-cell.on { background: var(--accent-600); color: var(--on-accent); font-weight: 640; }
/* Today is RINGED, never merely tinted: the chosen day is already the filled
   one, and two fills competing is how a reader loses which is which. */
.datepop-cell.today:not(.on) { border-color: var(--accent-500); }
/* A day outside the window. D-491: it is also `disabled`, so the state reaches
   a screen reader rather than living only in the opacity. */
.datepop-cell:disabled { opacity: .3; cursor: not-allowed; text-decoration: line-through; }
.datepop-time { display: flex; align-items: center; gap: var(--sp-2); }
.datepop-time label { font-size: var(--text-xs); font-weight: 600; color: var(--ink-400); }
 /* WIDE ENOUGH FOR TWO DIGITS PLUS THE INPUT'S OWN PADDING. Owner
    2026-08-06: "when the minutes value is 0, the input field is not wide
    enough because it's covering the second 0." Centred, tabular, and sized in
    `ch` so it holds at any font size rather than at the one it was measured
    at. */
.datepop-part {
  width: 4.5ch; min-width: 4.5ch; text-align: center;
  font-variant-numeric: tabular-nums; padding-left: 4px; padding-right: 4px;
}
.datepop-foot { display: flex; justify-content: flex-end; gap: var(--sp-2); padding-top: var(--sp-1); }

/* --- Visually hidden, still announced. ------------------------------------
   A column of drag handles needs a header for the table's own structure, and
   that header must not print a word above six dots. This is the standard clip
   technique rather than `display:none` or `visibility:hidden`, both of which
   remove the text from the accessibility tree as well as from the page, which
   is the opposite of what is wanted. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* --- W106. DRAG TO REORDER. -----------------------------------------------
   The grip is a real button so the same control works with a keyboard, which
   HTML5 drag alone never does. D-491. */
.dt-grip-head { width: 34px; }
.dt-grip { width: 34px; padding-right: 0 !important; }
.grip-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-300);
  cursor: grab; touch-action: none;
}
.grip-btn:active { cursor: grabbing; }
.grip-btn svg { width: 16px; height: 16px; fill: currentColor; stroke: none; }
.grip-btn:hover { color: var(--ink-700); background: var(--surface-sunken); border-color: var(--line); }
.grip-btn:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 1px; color: var(--ink-900); }
/* The row in flight. Faded rather than hidden, so the reader keeps track of
   which one they are carrying. */
tr.dt-dragging > td { opacity: .4; }
/* Where it would land. A LINE rather than a fill: a filled row reads as
   selected, and the drop target is a position between rows, not a row. */
tr.dt-dropzone > td { box-shadow: inset 0 2px 0 0 var(--accent-600); }

/* --- W108. THE SECOND-LEVEL DRAWER. ---------------------------------------
   Wider, and stacked OVER the first rather than replacing it, so the record
   you opened it from is still behind you when it closes.

   The z-indexes sit one rung above the first drawer's 99/100 and one below the
   date popup's 120, which is the order these three actually appear in: a
   picker opened from the wide drawer must still clear it. */
/* ★★ 1240 AND NOT 1080, AND THE NUMBER WAS MEASURED RATHER THAN CHOSEN.
   The owner, 2026-09-12: "the drawer is not wide enough to accommodate the
   details of the budget line item and also the actual data box." Measured in
   the browser at a 1440 viewport: `.mb-sheet` wanted 1139px of track and was
   given 1005px inside the 1080px panel, so it was short by 134 and showed the
   horizontal scrollbar in the owner's screenshot. The twelve columns sum to
   exactly 1139 -- Line 188, Qty 88, Time 83, Unit price 96, VAT 44, Markup 70,
   two discounts at 87 and 91, Status 94, Total 118, Payable 116, actions 64 --
   and the panel loses 74px to its own padding, so the floor is 1213.

   ★ THE SCROLL IS KEPT UNDERNEATH, DELIBERATELY. Below about a 1330 viewport
   `max-width` takes over and the sheet cannot fit however wide this says, so
   `.mb-sheet`'s own `overflow-x: auto` is still the honest answer there. This
   removes the scrollbar on a normal desktop; it does not pretend to remove it
   on a small laptop, and widening past the viewport to fake that would just
   push the panel off screen. */
.drawer-wide {
  width: 1240px; max-width: 96vw;
  right: -1240px;
  z-index: 102;
}
.drawer-wide.show { right: 0; }
.drawer-overlay-2 { z-index: 101; }
/* A spreadsheet needs its own scroll rather than making the whole panel
   scroll sideways. */
.drawer-wide .drawer-body { overflow: auto; }

/* --- W108. THE MINI BUDGET AS A SPREADSHEET. ------------------------------
   Owner 2026-08-06: "people's minds are connected to Excel sheets. When we
   are talking about budget, we should visualize ... some sort of Excel sheet
   or table kind of representation."

   ONE BREAKPOINT SWITCHES BETWEEN TWO RENDERINGS OF THE SAME DATA. Eleven
   columns is a spreadsheet on a wide drawer and an unreadable horizontal
   scroll on a phone, so below 760px the stacked cards come back. Neither is a
   fallback for the other; each is right at its own width. */
.mb-sheet { display: none; overflow-x: auto; }
.mb-cards { display: flex; flex-direction: column; gap: var(--sp-2); }
@media (min-width: 760px) {
  .mb-sheet { display: block; }
  .mb-cards { display: none; }
}
.mb-table { font-size: var(--text-sm); }
.mb-table th, .mb-table td { white-space: nowrap; }
.mb-table td:first-child { white-space: normal; min-width: 22ch; }
.mb-table .dt-sub { display: block; }
/* The money column is the one the eye goes to, so it is the only bold one. */
.mb-total-cell { font-weight: 640; color: var(--ink-900); }
/* A losing competing quote. Dimmed AND struck, never colour alone. D-491. */
.mb-row-off > td { opacity: .55; }
.mb-row-off .mb-line-title { text-decoration: line-through; }

/* --- W110. THE TOAST. -----------------------------------------------------
   Fixed to a corner because a save closes the drawer and returns the reader to
   a list: the confirmation has to find them rather than wait to be found.
   Above everything, including the wide drawer and the date popup, because it
   is the newest thing that happened. */
.toast {
  position: fixed; z-index: 200; right: var(--sp-5); bottom: var(--sp-5);
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); max-width: min(420px, calc(100vw - var(--sp-6)));
  background: var(--ok-600); color: #fff;
  border-radius: var(--radius); box-shadow: var(--shadow-md);
  font-size: var(--text-sm); font-weight: 550;
  animation: toast-in .18s cubic-bezier(.2, .8, .2, 1);
}
.toast-leaving { opacity: 0; transform: translateY(6px); transition: opacity .2s ease, transform .2s ease; }
.toast-tick { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.toast-close {
  border: 0; background: transparent; color: inherit; cursor: pointer;
  font-size: 18px; line-height: 1; padding: 0 2px; opacity: .8;
}
.toast-close:hover { opacity: 1; }
.toast-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; border-radius: 3px; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* A reader who has asked for less motion gets the toast without the slide. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast-leaving { transition: opacity .2s ease; transform: none; }
}

/* --- W112. THE DAY GRID. --------------------------------------------------
   Owner 2026-08-06: the agenda on a calendar day view, zoomable and
   scrollable. The geometry is in system/agendaDay.ts; this only draws it. */
.daygrid-scroll {
  /* THE GRID SCROLLS, NOT THE PAGE. At the deepest zoom a day is 4,800px tall
     and scrolling the window to read an afternoon would carry the toolbar off
     the top with it. */
  max-height: 70vh; overflow-y: auto; overflow-x: hidden;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-1);
}
.daygrid {
  --daygrid-gutter: 56px;
  /* The share of the width the blocks may use, after the hour gutter. */
  --daygrid-track: 0.88;
  position: relative;
}
.daygrid-hour {
  position: absolute; left: 0; right: 0;
  border-top: 1px solid var(--line);
}
.daygrid-hour:first-child { border-top: 0; }
.daygrid-hour-label {
  position: absolute; left: var(--sp-2); top: 2px;
  font-size: var(--text-xs); color: var(--ink-400);
  font-variant-numeric: tabular-nums;
}
.daygrid-block {
  position: absolute; overflow: hidden;
  display: flex; flex-direction: column; gap: 1px;
  padding: 3px var(--sp-2); text-align: left;
  border: 1px solid var(--accent-500); border-left: 3px solid var(--accent-600);
  border-radius: var(--radius-sm);
  background: var(--accent-050); color: var(--ink-900);
  cursor: pointer;
}
.daygrid-block:hover { background: var(--accent-100); }
.daygrid-block:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 1px; }
.daygrid-label { font-size: var(--text-sm); font-weight: 600; line-height: 1.2; }
.daygrid-sub { font-size: var(--text-xs); color: var(--ink-500); }
/* Tone is carried by the left bar AND stays legible without it. D-491. */
.daygrid-live { border-color: var(--ok-600); background: var(--ok-100); }
.daygrid-draft { border-color: var(--line-strong); border-left-color: var(--ink-300); background: var(--surface-2); }

/* --- W113. DRAGGING AND RESIZING ON THE GRID. -----------------------------
   The block is a div and the open control is a button inside it: a draggable
   button swallows its own click, and losing the keyboard path to open a
   session to gain a mouse gesture is not a trade worth making. */
.daygrid-block { padding: 0; cursor: grab; touch-action: none; }
.daygrid-block:hover { background: var(--accent-100); }
.daygrid-dragging { cursor: grabbing; box-shadow: var(--shadow-md); z-index: 2; opacity: .92; }
.daygrid-open {
  display: flex; flex-direction: column; gap: 1px;
  width: 100%; height: 100%; overflow: hidden;
  padding: 3px var(--sp-2); border: 0; background: transparent;
  color: inherit; text-align: left; cursor: pointer; font: inherit;
}
.daygrid-open:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }
.daygrid-time {
  font-size: var(--text-xs); font-weight: 640; color: var(--accent-700);
  font-variant-numeric: tabular-nums;
}
.daygrid-people { font-size: var(--text-xs); color: var(--ink-700); font-style: italic; }
/* THE RESIZE HANDLE IS 8px TALL AND SITS INSIDE THE BLOCK'S LOWER EDGE.
   Any thinner and it cannot be hit; any taller and it eats the text of a
   short session. */
/* W115. 12px TALL AND ALWAYS VISIBLE. Owner 2026-08-06: "the grab effect
   handle is very weak ... it's difficult to catch it." At 8px with a grip that
   only appeared on hover it had to be found before it could be seen. */
.daygrid-handle {
  position: absolute; left: 0; right: 0; height: 12px;
  cursor: ns-resize; touch-action: none;
}
.daygrid-handle-top { top: 0; }
.daygrid-handle-bottom { bottom: 0; }
/* W118. HIDDEN UNTIL THE BLOCK IS HOVERED. Owner 2026-08-06: "the pull tabs
   on the sessions should show up only when the actual box is hovered.
   Otherwise, they are just adding too much clutter." Seven grips on a busy
   day read as decoration; one under the pointer reads as an affordance.

   THE HANDLE ITSELF STAYS PRESENT AND HITTABLE, only its grip is hidden. A
   handle that appeared on hover would have to be found before it could be
   used, which is the complaint W115 fixed.

   ON TOUCH THERE IS NO HOVER AND SO NO GRIP, which is why resizing is not the
   only way to change a session's times: the drawer's Starts, Duration and Ends
   fields do the same job and are the path a phone actually takes. */
.daygrid-handle::after {
  content: ""; position: absolute; left: 50%;
  width: 30px; height: 3px; margin-left: -15px; border-radius: 3px;
  background: var(--accent-600); opacity: 0;
  transition: opacity .12s ease, height .12s ease;
}
.daygrid-handle-top::after { top: 2px; }
.daygrid-handle-bottom::after { bottom: 2px; }
.daygrid-block:hover .daygrid-handle::after,
.daygrid-block:focus-within .daygrid-handle::after { opacity: .6; }
.daygrid-block:hover .daygrid-handle:hover::after { opacity: 1; height: 4px; }
/* A block being dragged keeps its grips, or the one under the cursor vanishes
   the moment the pointer leaves the block's original bounds. */
.daygrid-dragging .daygrid-handle::after { opacity: 1; }
/* A block too short to hold two handles keeps only the bottom one, or the two
   would overlap and neither could be hit. */
.daygrid-block { min-height: 24px; }
.daygrid-live .daygrid-time { color: var(--ok-600); }
.daygrid-draft .daygrid-time { color: var(--ink-500); }

/* --- W116. THE HEADING AND ITS PRIMARY ACTION ON ONE LINE. ----------------
   Owner 2026-08-06: the create action belongs beside the heading rather than
   stacked under the intro, where it pushed the content down a row.

   IT WRAPS RATHER THAN SQUEEZING. Below the breakpoint the button drops under
   the heading at full width, which is the right answer on a phone and is what
   `flex-wrap` gives without a media query. */
.page-titlerow {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-3) var(--sp-4); flex-wrap: wrap;
  margin-bottom: var(--sp-5);
}
.page-titlerow > :first-child { min-width: min(100%, 22ch); }
.page-titlerow .page-intro { margin-bottom: 0; }

/* --- W126, D-634. SEARCH AND CREATE, BOTH TOP RIGHT, SEARCH FIRST. --------
   Owner 2026-08-06. W116 put ONE button beside the heading on ONE screen.
   This is the same idea generalised to the whole app and given a search field
   in front of the button, and it is driven from `PageHead.tsx` so the rule
   lives in one place rather than in 31 screens.

   THE HEADING CELL TAKES THE SLACK. `flex: 1 1 22ch` on the left and nothing
   on the right means a long intro paragraph absorbs the spare width and the
   tools keep their natural size, instead of a short title leaving the search
   field stretched across half a screen. */
.page-titlecell { flex: 1 1 22ch; min-width: min(100%, 22ch); }

/* The title and its count share a baseline. The count is not a heading and
   must not read as one, so it is smaller and dimmer rather than bold. */
.page-titleline { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
.page-titlemeta { font-size: var(--text-sm); color: var(--ink-400); }

/* THE ACTIONS. `align-items: center` and not `flex-end`, so a button lines up
   against the heading rather than hanging off the bottom of a wrapped row.

   `margin-left: auto` keeps them right even when the heading cell is narrow
   enough that `space-between` has nothing left to distribute. */
.page-headtools {
  display: flex; align-items: center; gap: var(--sp-2);
  flex-wrap: wrap; justify-content: flex-end; margin-left: auto;
}
.page-headtools .btn { flex: 0 0 auto; }

/* --- D-638. SEARCH ON ITS OWN LINE, AT THE FOOT OF THE HEADER. ------------
   Owner 2026-08-07, replacing the shared-line arrangement D-634 first asked
   for. THE REASON IS A MEASUREMENT, and the honest version of it: sharing the
   line left the field 166px at 1280, an inner width of 140px, against a 139px
   placeholder. It FIT, by one pixel. That is not a margin, it is a coincidence
   that survives no longer placeholder, no font fallback and no typed term.
   Here the field is 420px, an inner width of 394px.

   CAPPED, NOT FULL WIDTH. Stretched across a 994px column a search box reads
   as a form field inviting a sentence rather than a filter. 420px is what
   Contacts and Companies had each chosen by hand before this component
   existed.

   The heading row above gives up most of its bottom margin (the -tight
   modifier) so the two rows read as ONE header block. Without that they read
   as a heading and then an unrelated control floating above the table. */
.page-titlerow-tight { margin-bottom: var(--sp-3); }
.page-headsearch { max-width: 420px; margin-bottom: var(--sp-5); }

/* The day grid's hint is the only non-control in its toolbar, so it is the
   part that gives way first when the row runs out of room. */
.daygrid-hint { flex: 1 1 auto; min-width: 0; }

/* --- W117. THE DAY HEADER, AND BLOCKS THAT DROP LINES. --------------------
   Owner 2026-08-06: the date "is displayed in a button format, which is
   confusing ... maybe a top header for the day". The arrows stay buttons
   because they DO something; the date itself is a heading because it does not. */
.daygrid-dayhead {
  display: flex; align-items: center; gap: var(--sp-2);
  padding-bottom: var(--sp-2); border-bottom: 1px solid var(--line);
}
.daygrid-daytitle {
  font-size: var(--text-lg); font-weight: 620; color: var(--ink-900);
  margin: 0 var(--sp-2) 0 0;
}
.daygrid-dayhead .icon-btn svg { stroke-width: 1.8; }

/* One line, when that is all there is room for: the time, then the title.
   Both matter; a row of anonymous timestamps would be useless. */
.daygrid-oneline {
  display: flex; align-items: baseline; gap: var(--sp-2);
  min-width: 0; white-space: nowrap;
}
.daygrid-oneline .daygrid-label {
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.daygrid-oneline .daygrid-time { flex: none; }
/* W119. NO LINE MAY SHRINK, AND THAT IS THE WHOLE FIX.
   Measured 2026-08-06: `.daygrid-label` carried `overflow: hidden` and nothing
   else did, so when the four lines needed 72px in a 66px pane it was the only
   child flex could compress. It went from 15px to 9px and the title clipped
   through the middle of its glyphs while its neighbours sat untouched.
   `flex: none` makes every line keep its height, so a block that cannot hold a
   line DROPS it through blockDensity rather than amputating one. */
.daygrid-open > * { flex: none; }
.daygrid-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.daygrid-sub, .daygrid-people { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- W121. THE TAG PICKER. ------------------------------------------------
   Replaces the native `<datalist>`, whose popup is drawn by the browser and
   anchored by it, and which inside a `position: fixed` drawer landed in the
   middle of the screen. Owner reported it 2026-08-06. This menu is IN THE
   DOCUMENT, inside the field's own stacking context, so it cannot. */
.tagpick { position: relative; display: flex; flex-direction: column; gap: var(--sp-2); }
.tagpick-chosen { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.tagpick-tag {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 3px var(--sp-2); border-radius: 999px;
  background: var(--accent-100); color: var(--accent-700);
  font-size: var(--text-sm); font-weight: 550;
}
/* A value that is not on the list is MARKED rather than refused, so nobody
   wonders whether their own wording saved. */
.tagpick-own { background: var(--surface-2); color: var(--ink-700); border: 1px dashed var(--line-strong); }
.tagpick-tag em { font-style: normal; font-size: var(--text-xs); opacity: .7; }
.tagpick-tag button {
  border: 0; background: transparent; color: inherit; cursor: pointer;
  font-size: 15px; line-height: 1; padding: 0 1px; opacity: .7;
  /* WCAG 2.5.8 wants 24px of target, and a × in a chip is the smallest thing
     on this screen. */
  min-width: 20px; min-height: 20px;
}
.tagpick-tag button:hover { opacity: 1; }
.tagpick-tag button:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 1px; border-radius: 3px; }
.tagpick-menu {
  position: absolute; z-index: 30; top: 100%; left: 0; right: 0;
  margin-top: 4px; max-height: 240px; overflow-y: auto;
  background: var(--surface-1); border: 1px solid var(--line-strong);
  border-radius: var(--radius); box-shadow: var(--shadow-md);
}
.tagpick-opt {
  display: flex; align-items: baseline; gap: var(--sp-2);
  width: 100%; padding: 6px var(--sp-3); border: 0; background: transparent;
  text-align: left; cursor: pointer; font: inherit; color: var(--ink-900);
}
.tagpick-opt:hover { background: var(--surface-sunken); }
.tagpick-opt:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }
.tagpick-opt em { font-style: normal; font-size: var(--text-xs); color: var(--ink-400); }
.tagpick-add { font-weight: 600; color: var(--accent-700); border-bottom: 1px solid var(--line); }
.tagpick-empty { display: block; padding: 6px var(--sp-3); font-size: var(--text-sm); color: var(--ink-400); }

/* --- W124. THE PERFORMER'S PHOTO. -----------------------------------------
   Owner 2026-08-06: "right next to their name in a 3x4 classical photo ID
   box". 3x4 is the ID-photo ratio, so the box is `aspect-ratio: 3 / 4` and the
   image is `object-fit: cover` inside it. Cover rather than contain because a
   passport-style crop is what the request describes; contain would letterbox a
   landscape shot inside an upright frame and read as a mistake.

   `flex: none` ON THE BOX, and it is load bearing. Trap 4 of the agenda work:
   in a flex row the default shrink is 1, so a long performer name would
   compress the photo instead of wrapping itself. The box is a fixed 96px and
   the name field takes what is left. */
.perf-photo { display: flex; flex-direction: column; gap: var(--sp-2); flex: none; }
.perf-photo-box {
  width: 96px; aspect-ratio: 3 / 4; flex: none;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface-sunken); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.perf-photo-box img { width: 100%; height: 100%; object-fit: cover; display: block; }
.perf-photo-empty { font-size: var(--text-xs); color: var(--ink-400); text-align: center; }
/* The two buttons stack under a 96px box rather than sitting side by side,
   which is narrower than either of them. */
.perf-photo-actions { display: flex; flex-direction: column; gap: var(--sp-1); }
/* The row the box and the name field share. `align-items: flex-start` so the
   box hangs from the top rather than centring against a field half its height. */
.perf-identity { display: flex; gap: var(--sp-4); align-items: flex-start; }
.perf-identity > label { flex: 1; min-width: 0; }
.perf-photo-count { font-size: var(--text-xs); color: var(--ink-400); max-width: 96px; line-height: 1.35; }

/* ============================================================================
   THE ACCESS MATRIX. W143, D-659.
   ----------------------------------------------------------------------------
   A dense grid of roles against capabilities, recreating the Bubble screen the
   owner supplied as the spec. It is NOT `.dt`: that table is for records a
   person reads a row at a time, and this one is read by scanning a column.

   `.dt-wrap` IS REUSED for the horizontal scroll rather than a second wrapper.
   Twelve role columns overflow at 1280px and every one of them must stay
   reachable, and that class already solved it for the data tables.
   ============================================================================ */
.am-table { border-collapse: separate; border-spacing: 0; font-size: var(--text-sm); }

/* THE FIRST COLUMN STAYS PUT. A tick eight columns right means nothing once
   the capability it belongs to has scrolled off the left edge, which is the
   whole failure mode of a wide matrix. */
.am-table th[scope="row"], .am-corner {
  position: sticky; left: 0; z-index: 2;
  background: var(--surface-1);
  text-align: right; white-space: nowrap;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  font-weight: 400; color: var(--ink-700);
}
.am-table thead th { position: sticky; top: 0; z-index: 3; background: var(--surface-1); }
.am-corner { z-index: 4; }

.am-grouphead {
  background: var(--surface-2); color: var(--ink-700);
  font-size: var(--text-xs);
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--line);
}
/* The role name reads vertically, because twelve horizontal names is a header
   as tall as the table. Kept to one line and given a title on the cell rather
   than truncated, so a long role name is never silently cut. */
.am-rolehead { padding: var(--sp-2) var(--sp-1); border-bottom: 1px solid var(--line); vertical-align: bottom; }
.am-rolename {
  writing-mode: vertical-rl; transform: rotate(180deg);
  white-space: nowrap; font-weight: 500; color: var(--ink-700);
  font-size: var(--text-xs); max-height: 11rem;
}

/* NOT sticky on the CELL. It spans every column, so it is as wide as the
   table and pinning it pins a box whose label has already scrolled off. The
   label carries the stickiness instead. */
.am-section th {
  background: var(--surface-sunken); color: var(--ink-900);
  text-align: left; font-weight: 600;
  font-size: var(--text-xs);
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--line-strong); border-bottom: 1px solid var(--line);
}
.am-cap .am-action { text-transform: none; }
/* A flag rather than a second column, because `per record` and `money` qualify
   what a tick MEANS and belong beside the name they qualify. */
.am-flag {
  margin-left: var(--sp-2); padding: 0 var(--sp-1);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-size: var(--text-xs); color: var(--ink-500);
}
.am-table td {
  text-align: center; padding: var(--sp-1) var(--sp-2);
  border-bottom: 1px solid var(--line);
}
/* Inherited from the template this role was cloned from, rather than granted
   here. Underlined rather than tinted, because the tint is already carrying
   the state and a second colour on the same mark would be unreadable. */
.am-inherited { border-bottom: 1px dotted var(--ink-400); }

/* THE MARK CARRIES A GLYPH, NOT ONLY A COLOUR. D-491, WCAG 2.2 AA. A grid of a
   thousand coloured dots is the easiest place in this app to fail contrast and
   colour-blindness at once, so every state is legible with the colour removed. */
.am-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.25rem; height: 1.25rem; border-radius: 50%;
  font-size: var(--text-sm); line-height: 1;
}
.am-edit { background: var(--accent-100); color: var(--accent-700); }
.am-view { background: var(--ok-100);   color: var(--ok-600); }
.am-deny { background: var(--err-100);  color: var(--err-600); }
.am-none { background: transparent;     color: var(--ink-300); border: 1px solid var(--line); }
/* B-198. THE SELECTED VERSION CHIP in the form builder.
   `.chip` is a static label, so a chip that is also a CONTROL needs the button
   chrome reset and a focus ring, the same treatment `.am-cell-btn` gets one
   block down and for the same reason: it is an action, not a destination.

   THE SELECTED STATE IS NOT COLOUR ALONE. D-491. The chip's own text already
   carries the version and its draft/published word; this adds weight and a
   border so the selection survives a greyscale render. */
button.chip {
  appearance: none; font: inherit; cursor: pointer;
}
button.chip:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.fb-chip-on {
  border-color: var(--accent-600);
  background: var(--accent-100);
  color: var(--accent-700);
  font-weight: 600;
}

/* W155, D-685. A CELL THAT IS A CONTROL. The button resets its own chrome
   entirely and carries only the focus ring and the hit area, because the state
   is the `.am-mark` inside it and a button border around a coloured circle
   reads as a second, meaningless state.

   `.dash-tile-shuffle` is the precedent for resetting button chrome here; the
   settings tiles are the counter-example, and they are links because they are
   destinations. This is an action, so it is a real button and gets Enter,
   Space and a focus ring for free rather than faking them. */
.am-cell-btn {
  appearance: none; background: none; border: 0; padding: 0;
  font: inherit; color: inherit; cursor: pointer;
  display: inline-flex; border-radius: 50%;
}
.am-cell-btn:hover .am-mark { outline: 1px solid var(--line-strong); }
.am-cell-btn:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.am-cell-btn:disabled { cursor: progress; opacity: 0.5; }

.am-legend { display: inline-flex; align-items: center; gap: var(--sp-2); color: var(--ink-500); font-size: var(--text-xs); }
.am-section-label { position: sticky; left: var(--sp-3); display: inline-block; }

/* ============================================================================
   SETTINGS LAUNCHER TILES. W143.2.
   ----------------------------------------------------------------------------
   Three staff surfaces that were three sentences of prose, which read as
   footnotes rather than as places you can go.

   NOT `.dash-tile`. That is the dashboard's SIX-COLUMN grid with span
   modifiers, sized for a greeting and a glossary, and reusing it here would
   drag a layout system these three squares do not need and cannot fill.

   THEY ARE LINKS, NOT BUTTONS, so they get focus, Enter and the browser's own
   middle-click and open-in-new-tab for free. `.dash-tile-shuffle` had to reset
   button chrome and fake keyboard support precisely because it is a control
   rather than a destination; these are destinations.
   ============================================================================ */
.tile-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 172px));
  gap: var(--sp-3); margin: var(--sp-3) 0 var(--sp-5);
}
.tile {
  aspect-ratio: 1; min-width: 0;
  display: flex; flex-direction: column; justify-content: flex-end; gap: var(--sp-1);
  padding: var(--sp-3);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--surface-1); color: var(--ink-900); text-decoration: none;
}
/* The affordance. A square with text in it does not say "go here" on its own,
   so the border takes the accent and the surface lifts on hover AND on focus,
   never on hover alone: D-491 binds this screen to keyboard parity. */
.tile:hover { border-color: var(--accent-600); background: var(--surface-2); }
.tile:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; border-color: var(--accent-600); }
.tile-name { font-weight: 600; font-size: var(--text-md); line-height: 1.2; }
.tile-hint { font-size: var(--text-xs); color: var(--ink-500); line-height: 1.35; }

/* ===========================================================================
   B-200. THE TABLE BECOMES CARDS BELOW 640px.
   Owner decision 2026-08-15.

   THE PROBLEM IT FIXES, measured rather than assumed. At 360px `.dt-wrap`
   scrolls horizontally, which is right for a budget table and wrong for a
   table whose last column is the only thing the screen exists to do. On
   My Approvals the Approve and Reject buttons sat entirely off-screen, so
   the primary action of the most external-facing screen in the app was
   unreachable on a phone.

   ---------------------------------------------------------------------------
   IT IS CSS ONLY, AND THAT IS THE WHOLE REASON IT IS SAFE.

   The obvious implementation is a second rendering in `Table.tsx`, cards
   below the breakpoint and a table above. That is two renderings to keep in
   agreement, and they diverge the first time somebody adds a column to one
   of them. Here the DOM is identical at every width: the same `<table>`, the
   same cells, the same accessible content. Only `display` changes.

   The one thing the component contributes is `data-label` on each cell,
   because the header row is gone and a value with no name is a mystery.

   WHY `role` IS NOT REWRITTEN. Setting `display: block` on table elements
   does drop their implicit table semantics in some engines. The alternative
   is pinning `role="table"`/`row`/`cell` back on, which restores a grid
   reading experience that is WORSE on a phone than a list of labelled
   values: a screen reader user swiping through a 6-column grid on a 360px
   viewport is doing the same work as the sighted user scrolling sideways.
   The labels are real text in the reading order, in the right order, which
   is what actually helps here.
   =========================================================================== */
@media (max-width: 640px) {
  /* The card is the ROW now, so the wrapper stops being a card and stops
     being a scroll container. Leaving `overflow: hidden` here would clip the
     rounded corners of the row cards against a border that no longer frames
     anything. */
  .dt-wrap {
    background: none; border: 0; border-radius: 0;
    box-shadow: none; overflow: visible;
  }

  .dt, .dt tbody, .dt tr, .dt td { display: block; width: auto; }

  /* NOT `display: none`. The header text is the source of every `data-label`
     below, and hiding it this way keeps it out of the visual flow without
     removing it from the accessibility tree in engines that still expose it. */
  .dt thead {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }

  .dt tbody tr {
    background: var(--surface-1);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: var(--sp-2);
    margin-bottom: var(--sp-2);
  }
  .dt tbody tr:last-child { margin-bottom: 0; }

  /* STACKED, NOT TWO COLUMNS, AND THAT WAS MEASURED RATHER THAN CHOSEN.
     The first version put the label in column one and the value in column
     two. A cell renders whatever a screen's `render` returns, which is very
     often more than one node — a title plus a sub-line, a badge plus a
     count — and every node after the first became its own grid item and
     wrapped under the LABEL. "Poster artwork sign-off" sat beside its label
     while "creative, on a task" dropped to a full-width line below it,
     visually detached from the title it describes.

     Stacking makes the number of children irrelevant, which is the property
     this needs: the component cannot know what a screen will put in a cell.
     It also gives a long title the full card width instead of two thirds. */
  .dt td {
    padding: var(--sp-1) 0;
    border: 0;
  }

  .dt td[data-label]::before {
    content: attr(data-label);
    display: block;
    margin-bottom: 2px;
    color: var(--ink-400);
    font-size: var(--text-xs);
  }

  /* RIGHT ALIGNMENT IS A COLUMN CONCEPT AND THERE ARE NO COLUMNS HERE. It
     has to be undone on the DESCENDANT too, not only on the cell: screens
     put `<span class="num">` inside a cell that is itself `.num`, and
     resetting only the cell left the value hard against the right edge of
     the card, nowhere near the label naming it. Tabular numerals stay,
     because those are about the digits and not about the column. */
  .dt td.num, .dt td .num { text-align: left; }

  /* The reorder grip keeps its own row and its label gutter is empty. */
  .dt td.dt-grip { grid-template-columns: minmax(0, 1fr); }
}

/* ===========================================================================
   W173, D-708. THE CROWD MODULE'S TWO SCREENS.
   =========================================================================== */

/* THE CLICKER. Big because it is used one handed, in the dark, wearing
   gloves, by somebody whose eyes are on a crowd rather than on a screen.
   `touch-action: manipulation` removes the 300ms double-tap delay, which
   matters when the gesture is repeated a few hundred times an hour. */
.clicker { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.clicker-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-1);
  min-height: 168px; padding: var(--sp-4);
  border: 1px solid var(--line-strong); border-radius: var(--radius-lg);
  background: var(--surface-1); color: var(--ink-900);
  cursor: pointer; touch-action: manipulation; user-select: none;
}
.clicker-btn:active { transform: scale(.98); background: var(--surface-2); }
.clicker-sign { font-size: 2.5rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.clicker-label { font-size: var(--text-sm); color: var(--ink-400); }
/* The two directions are distinguished by a border rather than by fill, so
   the pair still reads at a glance in bright sun where a tinted background
   washes out. */
.clicker-in  { border-color: var(--accent-600); }
.clicker-out { border-color: var(--ink-300); }

@media (prefers-reduced-motion: reduce) {
  .clicker-btn:active { transform: none; }
}

/* THE ZONE TILES. */
.zone-grid {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.zone-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.zone-figure { font-size: 2rem; font-weight: 650; margin: var(--sp-2) 0 0; }
/* A zone nobody has counted does NOT get the big-number treatment: the point
   is that there is no number, and setting "Not counted" at 2rem would give an
   absence the visual weight of a measurement. */
.zone-unknown { font-size: var(--text-sm); font-weight: 500; color: var(--ink-400); }
/* Nesting is shown, because "the arena is inside the fan zone" is why one
   tile's figure is contained in another's and a reader who cannot see the
   containment will read the two as double counting. */
.zone-tile.is-nested { border-left: 3px solid var(--line-strong); }

/* ---------- Expanded detail, nested under the row that opened it ----------
   The same device as `.zone-tile.is-nested` directly above, and the same 3px
   left rule the system already uses in six places: `.pick-severe`,
   `.alert-card`, the two calendar day states and the warning block. It says
   "this belongs to the thing above it" and nothing more, so it takes `--line`,
   the quietest border token, and never a brand or status colour.

   A design hook reads a 3px left border as an accent stripe, which is the
   right instinct in general and wrong here: an accent stripe carries meaning
   by its COLOUR, and this one is the neutral rule. Kept deliberately, and kept
   at 3px, because matching six existing uses is the point of having a system.

   It exists as a class because AcademyScreen had it inline, along with eleven
   other inline styles, and an inline rule is one nobody can change in one
   place. */
.detail-nest {
  margin-top: var(--sp-2);
  padding-left: var(--sp-3);
  border-left: 3px solid var(--line);
}
/* The heading inside it steps down from the section title it sits under, and
   since 2026-09-23 it actually does. `--text-lg`, not a raw 1rem, was right
   about using a token and wrong about which one: --text-lg is 16px and
   `.section-title` is --text-md at 13.5px, so the sentence above this rule
   said "steps down" while the rule stepped UP. A lesson heading rendered
   larger than "A tananyag", the card title two levels above it.

   Measured before the change: h3 "The Showcaller Role" at 16px under h2
   "A tananyag" at 13.5px. Same token as `.subsection-title` and the same
   precedent, `.owner-editor h4`. The indent and the 3px rule already say this
   block is nested; the type no longer has to contradict them. */
.detail-nest .section-title { font-size: var(--text-sm); }

/* W177. AN OPEN ALERT. Not acknowledged is the loud state, because somebody
   was told and nobody has answered; acknowledged recedes rather than
   disappearing, since the record is the point and it has to stay readable. */
.alert-card { border-left: 3px solid var(--danger, #c0392b); }
.alert-card.is-acked { border-left-color: var(--line-strong); opacity: .85; }

/* ===========================================================================
   W179. THE CROWD REPORT'S FOUR CHARTS.

   NO CHARTING LIBRARY. Four charts is not enough to earn a dependency that
   would arrive with its own colour system and its own typography. These use
   the foundation's tokens because they have no opinions of their own.
   =========================================================================== */

.report-controls {
  display: grid; gap: var(--sp-2) var(--sp-3);
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: center;
}
@media (max-width: 640px) {
  /* One column on a phone: a label beside a date input at 360px leaves the
     input too narrow for the native picker to be usable. */
  .report-controls { grid-template-columns: minmax(0, 1fr); }
}

/* THE OCCUPANCY CURVE. `preserveAspectRatio: none` lets it stretch to the
   card, which is right for a time series: the shape carries the meaning and
   the exact aspect does not. `vector-effect` keeps the stroke one pixel
   despite that stretch, without which the line thins and thickens with the
   viewport. */
.chart { width: 100%; height: 200px; display: block; }
.chart-line {
  fill: none; stroke: var(--accent-600); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.chart-capacity {
  stroke: var(--ink-300); stroke-width: 1; stroke-dasharray: 4 4;
  vector-effect: non-scaling-stroke;
}
/* W713. Where each day starts, and the day under it. */
.chart-day { stroke: var(--ink-300); stroke-opacity: 0.5; stroke-width: 1; vector-effect: non-scaling-stroke; }
.chart-axis { position: relative; height: 1.25rem; margin-top: var(--sp-1); }
.chart-axis span {
  position: absolute; top: 0; font-size: var(--text-xs); color: var(--ink-400);
  white-space: nowrap; padding-left: 2px;
}

/* THE HEATMAP. A table, because it IS one: gates down, hours across, and a
   screen reader gets row and column headers rather than a wall of divs. */
.heat-wrap { overflow-x: auto; }
.heat { border-collapse: collapse; }
.heat th { font-size: var(--text-xs); color: var(--ink-400); font-weight: 500; padding: 2px 4px; }
.heat th[scope="row"] { text-align: left; white-space: nowrap; padding-right: var(--sp-2); }
.heat th.heat-day { text-align: left; color: var(--ink-700); border-left: 1px solid var(--ink-300); }
.heat td { padding: 1px; }
.heat-cell {
  display: block; width: 14px; height: 14px; border-radius: 2px;
  background: var(--accent-600);
}

/* THE PASSAGE SHARE. Bars rather than a pie: the figure exists to be
   compared across gates, and a length compares well where an angle does not. */
.share-row {
  display: grid; grid-template-columns: minmax(80px, 140px) minmax(0, 1fr) max-content;
  gap: var(--sp-2); align-items: center;
}
.share-name { font-size: var(--text-sm); }
.share-bar {
  display: block; height: 12px; border-radius: 6px;
  background: var(--surface-2); overflow: hidden;
}
.share-fill { display: block; height: 100%; background: var(--accent-600); }
.share-pct { font-size: var(--text-sm); white-space: nowrap; }

/* ===========================================================================
   W180. THE FLOOR PLAN.

   Positions are PERCENTAGES of the surface, which is why the pins need no
   knowledge of the rendered size and the whole thing is responsive for free.
   =========================================================================== */
.plan-surface {
  position: relative; display: block; width: 100%;
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  overflow: hidden; background: var(--surface-2);
}
.plan-surface.is-placing { cursor: crosshair; outline: 2px solid var(--accent-600); }
.plan-img { display: block; width: 100%; height: auto; }

.plan-pin {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 4px 8px; border-radius: var(--radius-md);
  /* Opaque rather than translucent: a counter that a busy patch of the map
     shows through is a counter nobody can read at a glance, which is the one
     thing this view exists to provide. */
  background: var(--surface-1); border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-sm); white-space: nowrap;
}
.plan-pin-name { font-size: var(--text-xs); font-weight: 640; }
.plan-pin-nums { font-size: var(--text-xs); color: var(--ink-400); }
.plan-pin-x {
  position: absolute; top: -8px; right: -8px;
  width: 18px; height: 18px; line-height: 1; padding: 0;
  border-radius: 50%; border: 1px solid var(--line-strong);
  background: var(--surface-1); color: var(--ink-400); cursor: pointer;
}
.plan-chips { flex-wrap: wrap; justify-content: flex-start; }

/* W181. The approver picker. A scrolling list rather than a long page: a
   tenant with sixty members would otherwise push the submit button off the
   bottom of the drawer, and the control that raises the approval has to stay
   reachable while you are choosing who is on it. */
.approver-list { max-height: 280px; overflow-y: auto; gap: var(--sp-1); }
.approver-row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1); border-radius: var(--radius-md); cursor: pointer;
}
.approver-row:hover { background: var(--surface-2); }
.approver-row span { display: inline-flex; align-items: center; gap: var(--sp-1); }

/* W182. The held-mail queue. */
.quarantine-tabs { justify-content: flex-start; }
.notice-inline {
  padding: var(--sp-2); border-radius: var(--radius-md);
  background: var(--surface-2); font-size: var(--text-sm);
}
/* PRE, and the body is rendered as TEXT rather than markup. This is
   unauthenticated content from a stranger, so the review queue must not be the
   one place in the app where an outsider can inject anything they like.
   `pre-wrap` keeps the sender's line breaks without letting a long line push
   the card wider than the screen. */
.mail-body {
  margin: 0; padding: var(--sp-2);
  border: 1px solid var(--line); border-radius: var(--radius-md);
  background: var(--surface-2);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs); line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
  max-height: 240px; overflow-y: auto;
}

/* W243. A MESSAGE INSIDE A THREAD, AND IT IS NOT `.mail-body`.
   `.mail-body` is a monospace review box for RAW mail and for secrets, at
   --text-xs inside a scroll well. A message in a conversation is PROSE: it is
   read, not inspected, so it takes the body ramp and a comfortable line height.

   `pre-wrap` IS THE WHOLE FIX. The body was rendered in a bare <p>, whose
   default `white-space: normal` collapses every newline into one space. The
   first real inbound email measured 5137 characters carrying 110 newlines, and
   every one of them was thrown away at paint time while the text sat intact in
   the database. Nothing was lost; nothing was displayed.

   `overflow-wrap: anywhere` because mail carries long URLs, and `pre-wrap`
   alone lets a single unbroken one push the card wider than the screen.

   STILL RENDERED AS TEXT AND NEVER AS MARKUP, which is the same rule
   `.mail-body` states above and it applies with more force here, not less: the
   quarantine queue is reviewed by somebody expecting a stranger, while a thread
   is where a colleague is not. The HTML part of an inbound email is
   deliberately never stored (`inbound-email.ts` takes RawTextBody), so there is
   no markup to render even if somebody wanted to. */
.msg-body {
  margin: 0;
  font-size: var(--text-md);
  line-height: 1.65;
  color: var(--ink-700);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* W183. Client quotes. */
.row-between { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.line-row, .line-row-2 { padding: var(--sp-1) 0; border-bottom: 1px solid var(--line); }
.line-row:last-child, .line-row-2:last-child { border-bottom: 0; }
/* A ROW THAT OPENS. An Academy curriculum module carries an expandable detail
   panel, so it is a block with a heading rather than a single line, and it
   needs the taller step. Named for its TOKEN, like `.stack-2` beside `.stack`,
   and the modifier stays BELOW so the cascade gives it its own padding while
   the border and the last-child rule above stay shared. */
.line-row-2 { padding: var(--sp-2) 0; }

/* ★ A NAME AND THE EVIDENCE RECORDED AGAINST IT. The Scenario Lab debrief
   lists all eleven competencies and offers a box beside each, and the rows only
   read as a list if the boxes start at the same place. Worn as
   `class="row evidence-row"`, so `.row` supplies the flex, the gap and the wrap
   and this supplies only what makes a COLUMN out of the repetition.

   A COLUMN, NOT A FLOOR, AND MEASURED BEFORE BEING CLAIMED. The call site wrote
   `minWidth: 180` on the name and `maxWidth: 380` on the box. Measured on the
   running lab at 1280px: all eleven names render at exactly 180px, which is the
   floor itself -- every one of them is SHORTER than it, so the boxes did line
   up and there was no visible defect to fix here. That is worth writing down
   rather than implying otherwise.

   What `min-width` is, though, is a floor a flex item's CONTENT can push past,
   so the alignment held only because no name reached it. A twelfth competency,
   or a Hungarian translation longer than its English name -- `academy_competency`
   holds these as data, and this screen runs in Hungarian -- would take the width
   it needed and shove its own box right while the other ten stayed put.
   `flex: 0 0 24ch` cannot do that: the name gets exactly the column and a long
   one wraps inside it. So this is a latent case closed, not a bug repaired.

   MEASURES IN `ch`, NOT PX, because both numbers are counts of characters and
   this stylesheet already sizes text that way -- 68ch on `.field-wide`, 46ch on
   `.quote-work`, 52ch on `.delzone-confirm`. 46ch here is `.quote-work`'s
   measure, reused rather than a fourth opinion about how wide a sentence is. */
.evidence-row > :first-child { flex: 0 0 24ch; }
.evidence-row > .input { flex: 1 1 24ch; width: auto; max-width: 46ch; }
/* Three narrow numeric fields on one row above the breakpoint, stacked below:
   a quantity input at 100px on a phone is unusable, and this is the form
   somebody fills in twenty times while building a quote. */
/* ★ AUTO-FIT, NOT A FIXED THREE. W641.2 added a fourth field, Discount, and a
   hard `repeat(3, ...)` dropped it onto a row of its own with two empty cells
   beside it. The drawer this renders in is about 300px wide, so auto-fit at 150px gives
   a 2x2 block there and a single row in a wide container, without the count
   being written down anywhere. One sole user of this class, ClientQuotesScreen. */
.quote-line-fields { display: grid; gap: var(--sp-2);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
@media (max-width: 640px) { .quote-line-fields { grid-template-columns: minmax(0, 1fr); } }


/* ============================================================================
   W260. THE EVENT BRAND BAR, AND THE MICROSITE THAT FIRST USES IT.

   Owner 2026-09-02, as a standing rule rather than one screen's decoration:
   every event editing surface should carry the EVENT'S logo, and the
   ORGANISING COMPANY'S logo should be able to appear beside it.

   ★ THE ABSENT LOGO IS A NAMED SLOT AND NOT A HOLE, which is the only real
   design decision in this block. A header that renders nothing when a logo is
   missing looks finished, so nobody ever uploads one. The slot carries the
   initials on a tinted tile with a DASHED border -- dashed because a solid one
   would read as a deliberate monogram mark rather than as somewhere a file
   belongs. It is the same device `.thumb-add` uses for an empty image well.
   ============================================================================ */
.brandbar {
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-3) 0; border-bottom: 1px solid var(--line);
  margin-bottom: var(--sp-4);
}
.brandmark { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
/* `margin-left: auto` rather than `justify-content: space-between` on the bar:
   with one child the organiser block is absent entirely, and space-between
   would then leave the event mark stranded mid-row on a wide screen. */
.brandmark-end { margin-left: auto; }
.brand-lines { min-width: 0; }
.brand-lines-end { text-align: right; }
.brand-title {
  font-size: var(--text-md); font-weight: 650; color: var(--ink-900);
  letter-spacing: -.01em;
}
.brand-sub { font-size: var(--text-xs); color: var(--ink-400); }
.brand-sub-strong { color: var(--ink-500); font-weight: 600; }

.brand-logo-slot {
  width: 34px; height: 34px; flex: none; border-radius: var(--radius-sm);
  display: grid; place-items: center;
  font-size: var(--text-min); font-weight: 700; letter-spacing: .02em;
  background: var(--accent-100); color: var(--accent-700);
  border: 1px dashed var(--accent-500);
}
/* `object-fit: contain` and NOT cover. A logo cropped to fill a square is a
   logo somebody will complain about, and an organisation's mark is the one
   image on the page that must not be reframed. */
.brand-logo-img {
  width: 34px; height: 34px; flex: none; border-radius: var(--radius-sm);
  object-fit: contain; background: var(--surface-1);
  border: 1px solid var(--line);
}

/* ---------- The microsite's own frame. ----------
   It is NOT the two-level app shell: a microsite serves ONE event, so a module
   rail and an event switcher would both be furniture with nothing behind them. */
.micro-tabs {
  display: flex; gap: var(--sp-1); border-bottom: 1px solid var(--line);
  margin-bottom: var(--sp-5); flex-wrap: wrap;
}
.micro-tab {
  appearance: none; border: 0; background: none; padding: 8px 12px;
  font-size: var(--text-sm); font-weight: 550; color: var(--ink-500);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  /* Rounded at the top only, like `.tab`: the hover wash below needs a tab
     shape to sit in, or it paints a hard rectangle against a rounded strip. */
  border-radius: var(--radius) var(--radius) 0 0;
  transition: color .13s ease, background .13s ease;
}
/* ★ THE SAME STATES THE APP'S OWN TAB HAS. Owner, 2026-09-05: "a felső menü
   aktív eleme csak árnyékolást kap. Ez így nem felel meg a teljes weboldal
   vizuális szabványának és stílusának."

   `.tab` in shell.css -- the tab strip everywhere else in the app -- carries
   FOUR states. `.micro-tab` carried two, and the two it was missing are the
   two that were being reported:

     `.tab.on` adds `font-weight: 640`. Without it the selected microsite tab
     differed from its neighbours only in hue, which is a weaker signal than
     the app uses anywhere else -- and colour alone is also what D-491's
     accessibility rule tells this project not to lean on.

     `.tab:focus-visible` draws the accent outline. `.micro-tab` had NO focus
     rule at all, so a tab that had just been clicked wore the BROWSER'S
     default ring: a grey box around the label, appearing only on the active
     tab, looking like a shading nobody designed. That is the box in the
     report.

   Hover gains the same `--surface-sunken` wash `.tab:hover` uses, so the strip
   behaves identically to every other tab strip in the product.

   NOT the sliding `.tab-ink` underline: that is drawn and animated by
   `Tabs.tsx`, which this shell does not use. A static 2px border in the same
   `--accent-600` is the same mark without the machinery. */
.micro-tab:hover { color: var(--ink-900); background: var(--surface-sunken); }
.micro-tab:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }
.micro-tab[aria-selected="true"] {
  color: var(--accent-700); font-weight: 640; border-bottom-color: var(--accent-600);
}

/* Two columns: the list of things, and the one being worked on. Collapses at
   980 rather than at a breakpoint token because that is where the editor form's
   own paired fields stop fitting beside a 280px list. */
.micro-split {
  display: grid; grid-template-columns: minmax(260px, 360px) 1fr;
  gap: var(--sp-4); align-items: start;
}
@media (max-width: 980px) { .micro-split { grid-template-columns: 1fr; } }

/* ★ THE TWO COLUMNS BEGIN ON THE SAME LINE. Owner, 2026-09-05: "zavar, hogy a
   két szövegdoboz nem egy vonalban kezdődik."

   `.micro-split` already sets `align-items: start`, so the two grid items DID
   share a top edge -- measured, both at 150.9. What did not was what a person
   actually sees: `.page-card` carries `margin-top: var(--sp-4)` for rhythm
   between stacked cards, and the detail column's FIRST card took it, so the
   right-hand box drew 16px below the list beside it, which has no such margin.

   A first child needs no rhythm above it. Scoped to this grid rather than
   fixed on `.page-card` itself, which forty screens rely on for exactly the
   spacing it is doing everywhere else. */
.micro-split > * > .page-card:first-child { margin-top: 0; }

.micro-list {
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--surface-1); overflow: hidden; box-shadow: var(--shadow-sm);
}
.micro-list-head {
  padding: 10px 12px; border-bottom: 1px solid var(--line);
  font-size: var(--text-min);
  color: var(--ink-400); font-weight: 700;
  display: flex; justify-content: space-between; gap: var(--sp-2);
}
/* B-352. "+ Új" is a padding-free button.link in a 12 px head, 23.7 px wide:
   under WCAG 2.5.8's 24 px target. Same repair as .home-sec-head > .link,
   in both directions. */
.micro-list-head > .link { display: inline-flex; align-items: center; justify-content: center; min-height: 24px; min-width: 24px; }
.micro-item {
  display: block; width: 100%; text-align: left; appearance: none;
  border: 0; border-bottom: 1px solid var(--line); background: none;
  padding: 10px 12px; transition: background var(--t-fast); color: inherit;
}
.micro-item:last-child { border-bottom: 0; }
.micro-item:hover { background: var(--surface-sunken); }
/* The same 3px left accent `.pick-severe`, `.alert-card` and `.zone-tile` use.
   `inset` box-shadow rather than a border, so the row does not shift by 3px
   when it becomes the selected one. */
.micro-item[aria-current="true"] {
  background: var(--accent-050); box-shadow: inset 3px 0 0 var(--accent-600);
}
.micro-item-top {
  display: flex; align-items: center; gap: var(--sp-2); justify-content: space-between;
}
.micro-item-name { font-size: var(--text-sm); font-weight: 600; color: var(--ink-900); }
.micro-item-sub {
  display: block; font-size: var(--text-xs); color: var(--ink-400);
  margin-top: 2px; font-variant-numeric: tabular-nums;
}

/* THE INTERNAL FIELD, MARKED SO THE WRITER CAN SEE WHAT THE READER CANNOT.
   `agenda.view_internal` already keeps `internal_notes` away from the website
   builder. This is the other half: the person TYPING needs to know which box
   is the private one, and a label alone is not enough when it sits in a column
   of eight identical boxes. */
.micro-internal {
  border-left: 3px solid var(--warn-700); border-radius: var(--radius-sm);
  background: var(--warn-100); padding: var(--sp-3);
}
.micro-internal .input-label { color: var(--warn-700); }

/* ---------- The handover text. ----------
   MONOSPACE IS AN ASSERTION, NOT A STYLE. The whole promise to the website
   builder is that this is unformatted text with line breaks and nothing else.
   Set in the UI face it would look like a paragraph somebody had styled, and
   the reader would reasonably wonder what else travels with it. In a mono face
   it reads as what it is: characters. */
.micro-plain {
  font-family: var(--font-mono); font-size: var(--text-sm); line-height: 1.6;
  color: var(--ink-700); background: var(--surface-sunken);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4); white-space: pre-wrap; overflow-x: auto;
  margin: 0;
}
.micro-plain-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2); margin-bottom: var(--sp-2); flex-wrap: wrap;
}
.micro-export {
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  background: var(--surface-1); overflow: hidden; box-shadow: var(--shadow-sm);
}
.micro-export-head {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--line);
}
.micro-export-body {
  padding: var(--sp-4); display: grid; grid-template-columns: 1fr 260px;
  gap: var(--sp-4); align-items: start;
}
@media (max-width: 980px) { .micro-export-body { grid-template-columns: 1fr; } }

/* The approver strip. `auto-fit` with a floor, so two approvers sit side by
   side on a wide card and stack on a phone without a breakpoint. */
.micro-approvers {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-3);
}
.micro-approver {
  display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-1);
}
.micro-approver-mine { border-color: var(--accent-500); box-shadow: 0 0 0 3px var(--accent-100); }
.micro-avatar {
  width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
  background: var(--accent-600); color: var(--on-accent); font-size: var(--text-min); font-weight: 700; flex: none;
}

/* W260. The two logos in the event context bar. Deliberately SMALL: the bar is
   a 52px strip carrying an event name, a status and a way out, and a logo that
   competes with the name is a logo in the wrong place. `contain`, never
   `cover` -- an organisation's mark is the one image on the page that must not
   be reframed. */
.cb-name .brand-logo-img, .cb-name .brand-logo-slot { width: 20px; height: 20px; font-size: var(--text-min); }
.cb-org-logo {
  width: 16px; height: 16px; object-fit: contain; border-radius: 3px;
  margin-right: 5px; vertical-align: -3px;
}

/* W277. The approver picker: a checkbox per person, on every surface that
   asks who approves. Replaces three <select multiple> boxes. */
.approver-picker { border: 0; padding: 0; margin: 0; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.approver-picker > legend { padding: 0; margin-bottom: var(--sp-1); }
.approver-picker:disabled { opacity: .6; }

/* ---------- W299. The programme by room, side by side ----------

   The owner, 2026-09-05: "szükségem lenne egy helyszínek, pontosabban termek
   és event spaces szerinti bontásra... egymás mellé állítva is látni kellene,
   hogy a különböző spacekben, színpadokon milyen programok történtek
   időrendben, párhuzamosan."

   ★ IT SCROLLS IN BOTH DIRECTIONS AND THE HEADINGS DO NOT. Twelve rooms is
   wider than any screen and a deep zoom is taller than one. A column heading
   that scrolls away is a column nobody can name, which on this grid is the
   only thing that tells you which programme you are reading; the hour axis is
   the same fact for the other direction.

   ★ THE BLOCKS REUSE `.daygrid-block`. Same object, same skin -- a session
   drawn one way on the day calendar and another way here would read as two
   different kinds of thing. Only the frame around them is new. */
.roomgrid-scroll {
  max-height: 72vh; overflow: auto;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-1);
}
.roomgrid {
  --roomgrid-axis: 56px;
  --roomgrid-colw: 180px;
  min-width: calc(var(--roomgrid-axis) + var(--roomgrid-cols) * var(--roomgrid-colw));
}
.roomgrid-head {
  display: grid;
  grid-template-columns: var(--roomgrid-axis) repeat(var(--roomgrid-cols), 1fr);
  position: sticky; top: 0; z-index: 2;
  background: var(--surface-2); border-bottom: 1px solid var(--line-strong);
}
.roomgrid-axis-head { border-right: 1px solid var(--line); }
.roomgrid-colhead {
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--text-xs); font-weight: 640;
  color: var(--ink-700); border-right: 1px solid var(--line);
  /* A long room name truncates rather than making its column wide enough to
     push the twelfth off the screen. The full name is on the title attribute. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.roomgrid-colhead:last-child { border-right: 0; }
.roomgrid-body {
  display: grid;
  grid-template-columns: var(--roomgrid-axis) repeat(var(--roomgrid-cols), 1fr);
  position: relative;
}
.roomgrid-axis {
  position: relative; border-right: 1px solid var(--line);
  position: sticky; left: 0; z-index: 1; background: var(--surface-1);
}
.roomgrid-hour { position: absolute; left: 0; right: 0; }
.roomgrid-hour-label {
  position: absolute; left: var(--sp-2); top: 2px;
  font-size: var(--text-xs); color: var(--ink-400);
  font-variant-numeric: tabular-nums;
}
.roomgrid-col { position: relative; border-right: 1px solid var(--line); }
.roomgrid-col:last-child { border-right: 0; }
/* The hour rules are drawn per column rather than across the body, so a column
   that scrolls under the sticky axis does not paint its rules over it. */
.roomgrid-rule {
  position: absolute; left: 0; right: 0; height: 0;
  border-top: 1px solid var(--line);
}
.roomgrid-col .daygrid-block { margin: 0 2px; }

/* W308. SORTABLE COLUMN HEADERS.
   The header becomes a button so it is reachable by keyboard and announces
   itself; it inherits the header's own type so the table does not suddenly
   grow a second typeface. The chevron pair is faint until the column is the
   one in force, which is what tells a reader that the OTHER columns can be
   sorted too -- a single arrow on one column only says where you are. */
.dt-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}
.dt-sort:hover { color: var(--accent-600); }
.dt-sort:focus-visible {
  outline: 2px solid var(--accent-500);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.dt-sort-mark {
  width: 8px;
  height: 11px;
  flex: none;
  fill: var(--ink-400);
  opacity: 0.55;
}
.dt-sort:hover .dt-sort-mark { opacity: 0.9; }
/* The active column: the arrow that is in force is solid, its opposite is
   faded rather than removed, so the direction is unambiguous at a glance and
   the mark keeps the same footprint whichever way it points.

   ★★ IT USED TO SAY "DESCENDING ONLY" HERE, AND THAT WAS WRONG. The argument
   was that "most recent first is what somebody asking for last-modified means,
   and a second click would ask oldest first, which nobody wants" -- sound
   about the `updated` column, and quietly applied to the other three without
   looking at them. `order by e.name` on the server is A-Z, so the name column
   was ascending while this rule drew a descending arrow over it. Both
   directions now render, and which one shows is the one the data is in. */
.dt-sort-mark.on { fill: var(--accent-600); opacity: 1; }
.dt-sort-mark.on.asc path:last-child { opacity: 0.25; }
.dt-sort-mark.on.desc path:first-child { opacity: 0.25; }

/* ==========================================================================
   W313. THE ADD CONTROL: AN ICON THAT SAYS WHAT IT IS ON HOVER.

   The owner, 2026-09-07: "a felület túlzsúfoltságának elkerülése érdekében a
   szöveges gomb helyett egy plusz ikon gombként történő használata elegendő
   lehet... Hover animációval esetleg előhúzhatjuk azt, hogy ez egy új
   rendezvény hozzáadása."

   ★ THE LABEL IS ALWAYS IN THE DOM, never injected on hover. It is clipped by
   a max-width that opens on hover and on focus -- so a screen reader reads it,
   a keyboard reaches it, and `prefers-reduced-motion` gets it without the
   slide. A tooltip built from a title attribute would give the mouse a label
   and everyone else nothing.

   ★ AND IT IS NOT AN ICON-ONLY BUTTON. A bare plus with no accessible name is
   the commonest icon-button defect; the name is the label itself.
   ========================================================================== */
.btn-add {
  display: inline-flex;
  align-items: center;
  gap: 0;
  overflow: hidden;
}
.btn-add svg { flex: none; }
.btn-add .btn-add-label {
  display: inline-block;
  max-width: 0;
  opacity: 0;
  white-space: nowrap;
  overflow: hidden;
  /* ★ IF IT EVER DOES CLIP, IT SAYS SO. Without this an over-long label is
     chopped mid-word with no sign anything is missing, which is the exact
     complaint that started this round. */
  text-overflow: ellipsis;
  transition: max-width 220ms ease, opacity 160ms ease, margin-left 220ms ease;
  margin-left: 0;
}
.btn-add:hover .btn-add-label,
.btn-add:focus-visible .btn-add-label {
  /* ★★ 44ch IS A CEILING, NOT A WIDTH, and the first draft got that wrong.
     It said 18ch, and the first real label -- "Uj rendezveny ebbe a
     projektbe", 30 characters, 162px -- opened to 140px and lost its last
     four. max-width caps an inline-block that is already sized by its own
     nowrap content, so a ceiling no label reaches costs nothing in layout:
     the button still stops at the text. The only visible effect of raising
     it is that short labels finish travelling before the 220ms is up.
     Hungarian and German action labels run long; 18ch was a guess taken
     from English. */
  max-width: 44ch;
  opacity: 1;
  margin-left: var(--sp-1);
}
/* Reduced motion still reveals the label -- it just arrives without travel. */
@media (prefers-reduced-motion: reduce) {
  .btn-add .btn-add-label { transition: none; }
}

/* ==========================================================================
   W320. THE TIP ICON AND ITS POPOVER.

   ★ THE POPOVER IS NOT PORTALLED, and that is a deliberate difference from
   DateField's calendar. The calendar had to be portalled because it is opened
   from inside a `<details>` Fold carrying `overflow: hidden`, which clipped it
   so badly that the OK button was not clickable. A tip sits beside a row in a
   table or a heading, and a table cell does not clip its overflow; portalling
   it would mean tracking scroll position for a box that follows its row
   perfectly well on its own.

   If a tip is ever placed inside a Fold, this is the rule it will break, and
   the fix is the one DateField already documents.
   ========================================================================== */
/* ★ A CONTROL AND ITS TIP ON ONE LINE. `.row-tight` sets a gap and NOTHING
   else -- it inherits its display from `.row` and does not stand alone -- so
   using it here left the icon flowing UNDER a full-width select. Measured in
   the browser 2026-09-07. This pairing needs its own two lines. */
.field-with-tip { display: flex; align-items: center; gap: var(--sp-2); }
.field-with-tip > .input { flex: 1 1 auto; min-width: 0; }

.tip { position: relative; display: inline-flex; vertical-align: middle; flex: none; }
.tip-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--ink-400);
  cursor: pointer;
}
.tip-btn:hover { color: var(--accent-600); background: var(--accent-050); }
.tip-btn:focus-visible { outline: 2px solid var(--accent-500); outline-offset: 1px; }
.tip-btn[aria-expanded="true"] { color: var(--accent-600); background: var(--accent-100); }

/* ★★ ANCHORED RIGHT BY DEFAULT, and that is not a preference. Measured in the
   browser 2026-09-07: a tip sits at the RIGHT-HAND END of the control it
   annotates, and a control in this app is usually inside a drawer whose right
   edge is a few pixels away. Opening leftward from that point put a 360px box
   mostly outside the drawer, clipped to an unreadable strip. Opening rightward
   from the icon means growing back INTO the form, which is where the space is.
   `.tip-pop-left` is the escape hatch for a tip placed at the left of a wide
   page. */
.tip-pop {
  position: absolute; top: calc(100% + var(--sp-1)); right: 0; z-index: 40;
  width: min(340px, calc(100vw - var(--sp-6)));
  max-height: 60vh; overflow-y: auto;
  padding: var(--sp-3);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-1); box-shadow: var(--shadow-md);
  text-align: left; font-weight: 400;
}
.tip-pop.tip-pop-left { right: auto; left: 0; }

.tip-item + .tip-item {
  margin-top: var(--sp-3); padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}
.tip-title {
  font-size: var(--text-sm); font-weight: 600; color: var(--ink-900);
  margin-bottom: var(--sp-1);
  /* The heading sits inside a table header in some placements, which sets
     uppercase and letter-spacing on everything under it. */
  text-transform: none; letter-spacing: normal;
}
.tip-text {
  font-size: var(--text-sm); color: var(--ink-700); line-height: 1.5;
}
.tip-text + .tip-text { margin-top: var(--sp-2); }
.tip-source {
  margin-top: var(--sp-2);
  font-size: var(--text-xs); color: var(--ink-400);
}

/* ==========================================================================
   W322. THE OVERFLOW MENU.

   ★ THE TRIGGER IS THE ONLY ICON-ONLY BUTTON IN THIS SYSTEM THAT NEEDS NO
   HOVER LABEL. W313's `.btn-add` carries its words clipped at max-width 0 and
   opens them on hover, because a plus alone does not say WHAT is being added.
   Three dots say the only thing they ever say -- "there is more here" -- and
   the answer is one press away in words. Its aria-label carries the name for
   anyone who cannot see the dots.
   ========================================================================== */
.moremenu { position: relative; display: inline-flex; }
.moremenu-btn { padding-left: var(--sp-2); padding-right: var(--sp-2); }
.moremenu-btn[aria-expanded="true"] {
  background: var(--accent-100); color: var(--accent-700); border-color: var(--accent-500);
}

/* Right-anchored for the reason .tip-pop is: this control sits at the END of a
   toolbar, so the space to grow into is back towards the page, not off it. */
.moremenu-pop {
  position: absolute; top: calc(100% + var(--sp-1)); right: 0; z-index: 40;
  min-width: 240px; max-width: min(340px, calc(100vw - var(--sp-6)));
  padding: var(--sp-1);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-1); box-shadow: var(--shadow-md);
}
.moremenu-item {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: var(--sp-2) var(--sp-2);
  border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-900);
  font: inherit; font-size: var(--text-sm); text-align: left; text-decoration: none;
  white-space: nowrap; cursor: pointer;
}
.moremenu-item:hover { background: var(--surface-sunken); color: var(--ink-900); }
.moremenu-item:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }
.moremenu-item-danger { color: var(--err-600); }
.moremenu-item-danger:hover { background: var(--err-100); color: var(--err-600); }
.moremenu-icon {
  display: inline-flex; flex: none; width: 14px; height: 14px; color: var(--ink-400);
}
.moremenu-item:hover .moremenu-icon { color: inherit; }

/* ==========================================================================
   W323. THE COMBINED PROJECT PROGRAMME, ON SCREEN AND ON PAPER.

   ★ THE PRINT RULES REUSE W31.5's MECHANISM EXACTLY: `visibility: hidden` on
   everything and `visible` on the sheet and its children. `display: none` on
   the ancestors would take the sheet with them, because the sheet is a
   descendant of the app shell. That combination is the only one that hides an
   ancestor while keeping a descendant.
   ========================================================================== */
.pa-tools { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-4); }

/* The on-screen card. `.print-sheet` is the hook the paged rules use; this is
   what it looks like before anybody prints it. */
.print-sheet {
  margin-top: var(--sp-5);
  padding: var(--sp-6);
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.pa-head { margin-bottom: var(--sp-5); padding-bottom: var(--sp-3); border-bottom: 2px solid var(--ink-900); }
.pa-title { font-size: var(--text-xl); font-weight: 700; color: var(--ink-900); }
.pa-sub { margin-top: var(--sp-1); font-size: var(--text-sm); color: var(--ink-500); }

.pa-day + .pa-day { margin-top: var(--sp-6); }
.pa-day-title {
  font-size: var(--text-lg); font-weight: 600; color: var(--ink-900);
  margin-bottom: var(--sp-2);
  padding-bottom: var(--sp-1); border-bottom: 1px solid var(--line);
}
.pa-table { width: 100%; }
/* Tabular numerals so a column of clock times lines up digit under digit --
   the same reason the money columns carry `.num`. */
.pa-col-time {
  width: 7.5rem; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ★ SET APART, NOT SET ASIDE. This section is most of the programme on a real
   project, so it is marked as a different KIND of content rather than as a
   lesser one: a sunken well says "these have no time yet", where dimming them
   would say "these matter less". They are the work still to do. */
.pa-unscheduled {
  padding: var(--sp-4);
  background: var(--surface-sunken);
  border-radius: var(--radius);
}
.pa-unsched-group + .pa-unsched-group { margin-top: var(--sp-3); }
.pa-unsched-event { font-size: var(--text-sm); font-weight: 600; color: var(--ink-700); }
.pa-unsched-list { margin: var(--sp-1) 0 0 var(--sp-5); color: var(--ink-700); font-size: var(--text-sm); }
.pa-unsched-list li { margin-top: 2px; }

.pa-pick-name { font-weight: 500; }
.pa-pick { display: block; }

.pa-foot {
  margin-top: var(--sp-6); padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
  font-size: var(--text-xs); color: var(--ink-400);
}

@media print {
  /* ★★ THE SHELL IS REMOVED FROM THE FLOW, NOT JUST FROM VIEW, and that is the
     fix for two defects a real PDF showed that no amount of reading would
     have.

     The first draft copied W31.5's document-export rules: hide everything with
     `visibility: hidden`, show the sheet, and pull it to the page origin with
     `position: absolute; top: 0; left: 0`. Printed, that produced a page with
     NO MARGIN -- because an absolutely positioned box whose ancestors are all
     static resolves against the initial containing block, which in paged media
     is the PAGE BOX and not the page CONTENT box, so `@page { margin }` is
     simply stepped over. The margin exists and the box ignores it.

     Removing the chrome from the flow instead lets the sheet be an ordinary
     block that starts at the top of the text area, which means the page margin
     applies -- and applies on EVERY page rather than only the first, which
     absolute positioning could never have done.

     `visibility: hidden` stays for everything unnamed, because the shell is
     not the only thing on the page and a rule that has to enumerate every
     sibling is a rule that goes stale. */
  /* ★★ `.demo-banner` IS DELIBERATELY NOT NAMED HERE. It was, from W324 until
     W368, and that put the string back into the always-shipped stylesheet --
     which is the ONE thing `src/system/demo-banner.css` exists to prevent, as
     its own header says: CSS is not tree-shaken, so an unreferenced class in
     THIS file ships in full. `verify-demo-bridge-dev-only.sh` had been red on
     every push since, and the print-hide now lives in that dev-only file. */
  .rail, .panel, .topbar { display: none !important; }
  .app, .main, .content, .page {
    display: block !important;
    height: auto !important; max-height: none !important;
    overflow: visible !important;
    margin: 0 !important; padding: 0 !important;
    background: #fff !important;
  }

  /* ★ AND THE PAGE CANVAS IS WHITE. `body` carries `background:
     var(--surface-0)`, a grey, and a body background PROPAGATES TO THE CANVAS
     in print -- so every part of every sheet of paper the white sheet did not
     cover came out grey. In the first PDF that was a solid grey slab below the
     footer, which is ink nobody asked for and the clearest sign the sheet was
     floating over a page rather than being it. */
  html, body { background: #fff !important; }

  body * { visibility: hidden; }
  .print-sheet, .print-sheet * { visibility: visible; }
  /* ★★ `.print-sheet` AND NOT `.pa-sheet`, so there is ONE set of paged rules
     for every printable document in the product. The margin defect fixed on
     2026-09-07 existed in TWO places at once -- the agenda sheet and the
     document exporter -- because the second was a copy of the first. A third
     copy was one screen away. */
  .print-sheet {
    position: static;
    width: auto; max-width: none;
    margin: 0; padding: 0; border: 0; border-radius: 0;
    background: #fff; color: #000;
  }
  /* Ink, not tokens: a themed page must not print white-on-white for a reader
     who happens to be in dark mode. */
  .print-sheet .pa-title, .print-sheet .pa-day-title, .print-sheet .pa-unsched-event,
  .print-sheet strong, .print-sheet td, .print-sheet th { color: #000; }
  .print-sheet .pa-sub, .print-sheet .muted, .print-sheet .pa-foot { color: #444; }
  .print-sheet .pa-head { border-bottom: 2px solid #000; }
  .print-sheet .pa-day-title { border-bottom: 1px solid #999; }
  .print-sheet .dt td, .print-sheet .dt th { border-color: #ccc; }

  /* ★ A DAY IS NOT SPLIT FROM ITS OWN HEADING, and a row is not split down
     the middle. Left to itself a browser will happily put "Saturday 14
     November" at the foot of one page and its first three items on the next,
     which is the single thing that makes a printed running order unusable. */
  .pa-day-title { break-after: avoid-page; page-break-after: avoid; }
  .print-sheet tr { break-inside: avoid-page; page-break-inside: avoid; }
  .pa-unsched-group { break-inside: avoid-page; page-break-inside: avoid; }
  /* The header repeats on every page, so page four still says which programme
     it belongs to. */
  .print-sheet thead { display: table-header-group; }

  /* The well is a screen affordance; on paper a grey box wastes ink and adds
     nothing a heading does not already say. */
  .pa-unscheduled { background: transparent; padding: 0; }
  /* ★ .page-titlerow, NOT .page-head. The first draft named a class PageHead
     does not render -- it renders .page-titlerow -- so the rule matched
     nothing. Harmless here only because the visibility rule above had already
     hidden it; a selector that matches nothing is still the phantom-class
     failure this project has shipped sixteen of. */
  .pa-tools, .page-titlerow, .fold, .check { display: none !important; }
  @page { margin: 16mm; }
}

/* ===========================================================================
   W348. THE PHONE DOOR: A CAMERA, AND A VERDICT THAT FILLS THE SCREEN.
   ===========================================================================
   ★★ A MARSHAL READS THIS AT ARM'S LENGTH, IN SUNLIGHT, WITH A QUEUE. That is
   the whole brief and it is why the verdict is not a Notice: a banner at the
   top of a page is read by somebody sitting down. This covers everything.

   ★ 100dvh WITH A 100vh FALLBACK, IN THAT ORDER. Mobile Safari's toolbar makes
   100vh taller than the visible area, so the bottom of a 100vh panel sits under
   the browser chrome -- which is exactly where the verdict's own text would go.
   `dvh` fixes it and is ignored by older browsers, which then get the 100vh
   line above it rather than nothing.

   ★ AND THE COLOUR IS NOT THE ONLY SIGNAL. Each state carries a glyph and a
   word, because D-491 binds this screen like every other and a red/green door
   is unusable to a colour-blind steward -- who is precisely the person the
   accreditation module already buys a symbol for. */
.scan-stage {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  background: #000;
}
.scan-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* The preview is a mirror only for a selfie camera; the rear camera must not
     be flipped or a person reading a printed code sees it backwards. */
  transform: none;
}
/* The reticle. Not decoration: it tells somebody WHERE to hold the code, which
   is the difference between a scan that takes a second and one that takes ten. */
.scan-reticle {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(68vw, 68vh, 320px); aspect-ratio: 1;
  border: 3px solid rgba(255, 255, 255, .9);
  border-radius: var(--radius-lg);
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .45);
  pointer-events: none;
}
.scan-hint {
  position: absolute; left: 0; right: 0; bottom: calc(env(safe-area-inset-bottom, 0px) + var(--sp-6));
  text-align: center; color: #fff; font-size: var(--text-md);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
  padding: 0 var(--sp-4);
}
.scan-bar {
  position: absolute; left: 0; right: 0; top: 0;
  padding: calc(env(safe-area-inset-top, 0px) + var(--sp-3)) var(--sp-4) var(--sp-3);
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  color: #fff; background: linear-gradient(rgba(0,0,0,.6), rgba(0,0,0,0));
  font-size: var(--text-sm);
}
.scan-bar .btn { background: rgba(255,255,255,.15); color: #fff; border-color: rgba(255,255,255,.35); }

/* W352. THE SAME STAGE, SHRUNK INTO A CARD. The crew portal opens the camera
   inside a page that keeps scrolling around it, so the door's 100dvh would
   swallow the card holding it. Everything else -- the video's object-fit, the
   reticle, the verdict -- is unchanged, which is why this is a modifier and
   not a second set of classes: a fix to the reticle lands in both places. */
.scan-stage.scan-inline {
  height: auto;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius);
}
.scan-stage.scan-inline .scan-reticle {
  width: min(60%, 200px);
  /* A 100vmax spread would black out the whole page around a card this size. */
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .30);
}

/* ---- THE VERDICT ------------------------------------------------------- */
.scan-verdict {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: var(--sp-4);
  padding: var(--sp-6);
  text-align: center;
  color: #fff;
  /* ★ TAPPING ANYWHERE DISMISSES. A marshal has one hand and gloves on. */
  cursor: pointer;
}
.scan-verdict.v-ok   { background: var(--ok-600); }
.scan-verdict.v-warn { background: var(--warn-700); }
.scan-verdict.v-err  { background: var(--err-600); }
.scan-glyph { font-size: clamp(64px, 22vmin, 140px); line-height: 1; font-weight: 700; }
.scan-headline {
  font-size: clamp(24px, 7vmin, 44px); font-weight: 700; line-height: 1.1;
  letter-spacing: -0.01em;
}
.scan-who { font-size: clamp(16px, 4.5vmin, 24px); opacity: .95; }
.scan-why { font-size: clamp(15px, 4vmin, 20px); max-width: 30ch; opacity: .95; }
.scan-code {
  font-family: var(--font-mono); font-size: var(--text-xs);
  opacity: .7; word-break: break-all; max-width: 34ch;
}
.scan-dismiss { font-size: var(--text-sm); opacity: .8; }

/* A phone in landscape, and a small phone in portrait, both lose height fast.
   The glyph is what goes: the words are the part somebody has to read. */
@media (max-height: 420px) {
  .scan-glyph { font-size: clamp(40px, 14vmin, 64px); }
  .scan-verdict { gap: var(--sp-2); }
}

/* ===========================================================================
   W617. THE PRESENTER DISPLAY. A confidence monitor beside a stage.

   ★★ DARK IN BOTH THEMES, ON PURPOSE. A white screen at the foot of a stage
   lights the presenter's face from below and shows on camera. So the page's
   root carries `data-theme="dark"`, which scopes the dark palette to this
   subtree alone, and every colour below is still a token. The one raw value
   is the black: a projector and an OLED panel show #000 as no light at all,
   and the darkest surface token is not that.

   The signal bands put BLACK text on the bright dark-theme status colours,
   which is 6.8:1 on the red and better on the rest.
   =========================================================================== */
.stage-display {
  position: fixed; inset: 0;
  display: flex; flex-direction: column;
  background: #000; color: var(--ink-900);
  font-family: var(--font-ui);
  overflow: hidden;
}
.stage-top {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-4); padding: var(--sp-4) var(--sp-6);
  font-size: clamp(14px, 2.6vmin, 30px); color: var(--ink-500);
}
.stage-main {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-3); padding: 0 var(--sp-6); text-align: center;
}
.stage-moment { font-size: clamp(18px, 5vmin, 60px); font-weight: 600; line-height: 1.15; max-width: 92vw; }
.stage-clock {
  font-size: clamp(72px, 30vmin, 420px); font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}
.stage-clock.t-amber { color: var(--warn-700); }
.stage-clock.t-red   { color: var(--err-600); }
.stage-caption { font-size: clamp(14px, 3.2vmin, 38px); color: var(--ink-500); }
.stage-signal {
  padding: var(--sp-4) var(--sp-6); text-align: center; color: #000;
  font-size: clamp(28px, 8vmin, 120px); font-weight: 700; line-height: 1.1;
  overflow-wrap: anywhere;
}
.stage-signal.s-white { background: var(--ink-900); }
.stage-signal.s-green { background: var(--ok-600); }
.stage-signal.s-amber { background: var(--warn-700); }
.stage-signal.s-red   { background: var(--err-600); }
.stage-warn { color: var(--warn-700); }
/* The same swatches on an ordinary screen, where the live composer shows
   which colour a signal will be before it is sent. */
.signal-swatch {
  display: inline-block; width: 12px; height: 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong); vertical-align: middle;
}
.signal-swatch.s-white { background: var(--surface-1); }
.signal-swatch.s-green { background: var(--ok-600); }
.signal-swatch.s-amber { background: var(--warn-700); }
.signal-swatch.s-red   { background: var(--err-600); }

/* ===========================================================================
   W619. THE TELEPROMPTER, inside the presenter display's dark frame.

   The text scrolls inside a frame that does not, so the reading mark stays
   put while the words pass it. Both the pad above the words and the mark sit
   at the same fraction of the frame, which is where the first line starts and
   where the eye rests. Mirroring flips the words and not the frame, because
   beam-splitter glass reverses what it reflects and the operator's own screen
   must still read the right way round in the top bar.
   =========================================================================== */
.prompter-frame { position: relative; flex: 1; min-height: 0; display: flex; }
.prompter-text {
  flex: 1; min-height: 0; overflow: hidden;
  padding: 0 var(--sp-8);
  font-size: calc(var(--prompter-size, 6) * 1vw);
  font-weight: 600; line-height: 1.35;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.prompter-mirrored { transform: scaleX(-1); }
/* Half a line short of the mark, so the first line is read THROUGH its middle
   rather than hung beneath it. */
.prompter-pad { height: calc(38vh - 0.675em); }
.prompter-mark {
  position: absolute; left: 0; top: 38vh;
  width: var(--sp-4); height: 3px; background: var(--warn-700);
}

/* ============================================================================
   W634, D-1243. THE PRACTICE NOTE.

   Shown in dead time: in the shell's loading slot while a screen arrives, and
   quietly on the dashboard. It must read as a deliberate aside rather than as
   a notice, because nothing has gone wrong and nothing is being asked of the
   reader. Hence the sunken surface and the accent rule rather than `.notice`,
   whose whole vocabulary is "something happened".

   NO FIXED HEIGHT AND NO ABSOLUTE POSITION. It appears where the word
   "Loading..." used to, inside the content flow, so a screen arriving under it
   replaces it without anything jumping.
   ========================================================================= */
.practice-note {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2);
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-sunken);
  border-left: 2px solid var(--accent-500);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--ink-700);
  line-height: 1.5;
}
.practice-note-body { font-weight: 550; color: var(--ink-900); }
/* The credit is present and quiet. It is not a link: a note is two seconds of
   reading and sending somebody away from a page they are waiting for is the
   opposite of the point. */
.practice-note-source {
  font-size: var(--text-xs);
  color: var(--ink-400);
}
.practice-note-source::before { content: "— "; }

/* ===========================================================================
   W678, D-1298. THE TIME SCRUBBER.
   A range input is one of the controls that ignores CSS entirely until
   `appearance: none`, which is trap 6 of the design system: it renders as raw
   browser chrome and reads as an unstyled bug on a screen where nothing else
   is. Both the track and the thumb need their own rules, and the thumb needs
   them twice because WebKit and Firefox name the pseudo-element differently
   and a selector list containing one they do not know is DISCARDED WHOLE.
   =========================================================================== */
.scrub {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 22px; margin: 0;
  background: transparent; cursor: pointer;
}
.scrub:disabled { cursor: not-allowed; opacity: .55; }

.scrub::-webkit-slider-runnable-track {
  height: 6px; border-radius: 999px;
  background: var(--surface-sunken); border: 1px solid var(--line);
}
.scrub::-moz-range-track {
  height: 6px; border-radius: 999px;
  background: var(--surface-sunken); border: 1px solid var(--line);
}

.scrub::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; margin-top: -6px;
  border-radius: 50%; background: var(--accent-600);
  border: 2px solid var(--surface-1); box-shadow: var(--shadow-sm);
}
.scrub::-moz-range-thumb {
  width: 16px; height: 16px;
  border-radius: 50%; background: var(--accent-600);
  border: 2px solid var(--surface-1); box-shadow: var(--shadow-sm);
}

/* D-491. The focus ring goes on the CONTROL, not on the thumb: a ring drawn
   on a pseudo-element is clipped by the track on at least one engine. */
.scrub:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }

/* The instant being asked about, beside the slider. Tabular figures so the
   number does not jump about as the scrubber moves. */
.scrub-at {
  font-family: var(--font-mono); font-size: var(--text-sm);
  font-variant-numeric: tabular-nums; color: var(--ink-700);
  white-space: nowrap;
}

/* ============================================================================
   E7, THE TYPE FLOOR AND THE DENSITY PREFERENCE. UX study 2026-10-02, Phase 1.

   Body text is 14 px (tokens.local.css), table text 13.5 px, and nothing
   renders below 12 px (--text-min). Labels are sentence case: the uppercase
   rules were removed where they were declared. The foundation's 10 px
   uppercase table header and 9.5 px row number are rewritten by the generator
   since UX Phase 3 (DECLARATION_REWRITES in gen-design-system.mjs), so no
   override is needed here any more.

   Row height follows ONE user preference, Kényelmes (44 px) or Tömör (36 px),
   set in Beállítások and written as data-density on <html>. --row-h comes
   from the generator. A td height works as a minimum, so a wrapped cell still
   grows.
   ============================================================================ */
.dt { font-size: var(--text-table); }
.dt tbody td { height: var(--row-h); }
[data-density="compact"] .dt td { padding-top: var(--sp-1); padding-bottom: var(--sp-1); }

/* ----------------------------------------------------------------------------
   Beállítások, the preference cards. E1: SettingsScreen's inline styles moved
   here, 2026-10-02. One card is at most 640 px wide, each preference is a row
   divided from the one above by a line.
   ---------------------------------------------------------------------------- */
.pref-card { max-width: 640px; margin-bottom: var(--sp-4); }
.pref-card > .drawer-section:first-child { margin-top: 0; }
.pref-notice { margin: var(--sp-3) 0; }
.pref-row {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--sp-3); padding: var(--sp-3) 0; margin-top: var(--sp-3);
  border-top: 1px solid var(--line);
}
.pref-row + .pref-row { margin-top: 0; }
.pref-row .input-hint { margin-top: var(--sp-1); }
.pref-name { font-weight: 600; }
.pref-section { padding-top: var(--sp-3); margin-top: var(--sp-3); border-top: 1px solid var(--line); }
.pref-lead { margin-bottom: var(--sp-2); }
.pref-foot { margin-top: var(--sp-4); }
.pref-intro { margin-bottom: var(--sp-3); }
.pref-select { max-width: 320px; }

/* ============================================================================
   E10, THE STATES KIT. UX study 2026-10-02. system/components/States.tsx.
   ============================================================================ */
.empty-state {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--sp-2); padding: var(--sp-8) var(--sp-4);
}
.empty-state-icon svg {
  width: 32px; height: 32px; stroke: var(--ink-400); fill: none; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.empty-state-title { font-size: var(--text-body); font-weight: 600; color: var(--ink-900); }
.empty-state-cue { font-size: var(--text-sm); color: var(--ink-500); max-width: 52ch; }
.empty-state-action { margin-top: var(--sp-2); }

.skeleton { display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-4) 0; }
.skeleton-line {
  display: block; height: var(--sp-3); border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--surface-sunken), var(--line), var(--surface-sunken));
  background-size: 200% 100%; animation: skeleton-sweep 1.4s var(--ease) infinite;
}
.skeleton-line:nth-child(3n) { width: 70%; }
.skeleton-line:nth-child(3n + 1) { width: 90%; }
@keyframes skeleton-sweep { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .skeleton-line { animation: none; } }

.error-state .btn { margin-top: var(--sp-3); }

.inline-notice { font-size: var(--text-sm); margin-top: var(--sp-1); }
.in-info { color: var(--info-600); }
.in-warn { color: var(--warn-700); }
.in-err { color: var(--err-600); }
.in-ok { color: var(--ok-600); }
.in-neutral { color: var(--neutral-600); }

.undo-banner {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4); border-radius: var(--radius);
  background: var(--toast-bg); color: var(--toast-fg); font-size: var(--text-sm);
}
.undo-banner > span { flex: 1; }
.undo-banner-close {
  background: none; border: 0; color: inherit; font-size: var(--text-lg);
  min-width: 24px; min-height: 24px; line-height: 1;
}

/* ============================================================================
   E1, LAYOUT UTILITIES. UX study 2026-10-02, Phase 1.

   The screens carried about 600 inline style={{…}} objects. The static ones on
   plain elements were moved here mechanically: each utility is exactly one
   declaration the inline style had, so nothing moves on screen.

   !important IS DELIBERATE. An inline style beats every stylesheet rule, and a
   utility replacing it must keep that precedence, or a component rule such as
   `.notice .btn { margin-top }` would silently win and the layout would shift.
   Order matters for the same reason: shorthands come before the longhands
   that refine them, as they did inside one inline object.

   Use a component class for anything new. These exist so the move was exact,
   not as a vocabulary to grow. The max-width steps are the pixel values the
   screens used, kept as they were until a width scale exists.
   ============================================================================ */
.u-flex { display: flex !important; }
.u-inline-flex { display: inline-flex !important; }
.u-grid { display: grid !important; }
.u-block { display: block !important; }
.u-contents { display: contents !important; }
.u-col { flex-direction: column !important; }
.u-wrap { flex-wrap: wrap !important; }
.u-flex-1 { flex: 1 !important; }
.u-minw-0 { min-width: 0 !important; }
.u-ai-center { align-items: center !important; }
.u-ai-baseline { align-items: baseline !important; }
.u-ai-end { align-items: flex-end !important; }
.u-ai-start { align-items: flex-start !important; }
.u-jc-between { justify-content: space-between !important; }
.u-jc-end { justify-content: flex-end !important; }
.u-jc-center { justify-content: center !important; }
.u-gap-1 { gap: var(--sp-1) !important; }
.u-gap-2 { gap: var(--sp-2) !important; }
.u-gap-3 { gap: var(--sp-3) !important; }
.u-gap-4 { gap: var(--sp-4) !important; }
.u-gap-5 { gap: var(--sp-5) !important; }
.u-m-0 { margin: 0 !important; }
.u-my-3 { margin: var(--sp-3) 0 !important; }
.u-my-4 { margin: var(--sp-4) 0 !important; }
.u-my-5 { margin: var(--sp-5) 0 !important; }
.u-mt-0 { margin-top: 0 !important; }
.u-mt-1 { margin-top: var(--sp-1) !important; }
.u-mt-2 { margin-top: var(--sp-2) !important; }
.u-mt-3 { margin-top: var(--sp-3) !important; }
.u-mt-4 { margin-top: var(--sp-4) !important; }
.u-mt-5 { margin-top: var(--sp-5) !important; }
.u-mb-0 { margin-bottom: 0 !important; }
.u-mb-1 { margin-bottom: var(--sp-1) !important; }
.u-mb-2 { margin-bottom: var(--sp-2) !important; }
.u-mb-3 { margin-bottom: var(--sp-3) !important; }
.u-mb-4 { margin-bottom: var(--sp-4) !important; }
.u-mb-6 { margin-bottom: var(--sp-6) !important; }
.u-ml-auto { margin-left: auto !important; }
.u-p-4 { padding: var(--sp-4) !important; }
.u-py-1 { padding: var(--sp-1) 0 !important; }
.u-py-2 { padding: var(--sp-2) 0 !important; }
.u-bb { border-bottom: 1px solid var(--line) !important; }
.u-w-full { width: 100% !important; }
.u-maxw-110 { max-width: 110px !important; }
.u-maxw-140 { max-width: 140px !important; }
.u-maxw-160 { max-width: 160px !important; }
.u-maxw-180 { max-width: 180px !important; }
.u-maxw-200 { max-width: 200px !important; }
.u-maxw-220 { max-width: 220px !important; }
.u-maxw-260 { max-width: 260px !important; }
.u-maxw-320 { max-width: 320px !important; }
.u-maxw-560 { max-width: 560px !important; }
.u-maxw-640 { max-width: 640px !important; }
.u-ta-left { text-align: left !important; }
.u-va-top { vertical-align: top !important; }
.u-fw-600 { font-weight: 600 !important; }
.u-fs-xs { font-size: var(--text-xs) !important; }
.u-fs-sm { font-size: var(--text-sm) !important; }
.u-fs-lg { font-size: var(--text-lg) !important; }
.u-mono { font-family: var(--font-mono) !important; }
.u-pre-wrap { white-space: pre-wrap !important; }
.u-break-all { word-break: break-all !important; }
.u-c-ink-400 { color: var(--ink-400) !important; }
.u-c-ok-600 { color: var(--ok-600) !important; }
.u-c-warn-700 { color: var(--warn-700) !important; }

/* UX Phase 2. Moved out of AgendaScreen's inline styles. */
.clash-list { margin: 0; padding-left: var(--sp-5); }
.agenda-card { margin-bottom: var(--sp-5); }
.u-dim { color: var(--ink-500); }
.u-js-start { justify-self: start; }
.u-mr-auto { margin-right: auto; }
.u-minw-56 { min-width: calc(var(--sp-8) + var(--sp-6)); }
.input-num { width: 9ch; }

/* ============================================================================
   UX Phase 3, E6. THE SHARED DATA TABLE (DataTable.tsx). Toolbar of at most
   five controls, filter chips only while a filter is on, a bulk bar only while
   rows are selected, and a wrapper that scrolls inside the page so the sticky
   header stays in view. Token values only.
   ============================================================================ */
.dt-shell { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: var(--sp-4); }
.dt-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.dt-search { flex: 1 1 240px; max-width: 420px; }
.dt-view { display: inline-flex; align-items: center; gap: var(--sp-1); }
.dt-view .input { width: auto; min-width: 160px; }
.dt-filters {
  display: flex; flex-wrap: wrap; gap: var(--sp-3); padding: var(--sp-3);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1);
}
.dt-filter { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 180px; }
.dt-chips, .dt-bulk, .dt-naming { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.dt-chip {
  display: inline-flex; align-items: center; gap: var(--sp-2); min-height: 28px;
  padding: 0 var(--sp-3); border: 1px solid var(--accent-500); border-radius: var(--radius-lg);
  background: var(--accent-050); color: var(--ink-900); font: inherit; font-size: var(--text-sm); cursor: pointer;
}
.dt-chip:hover { background: var(--accent-100); }
.dt-chip:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.dt-count { color: var(--ink-500); font-size: var(--text-sm); }
.dt-bulk {
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--accent-500);
  border-radius: var(--radius); background: var(--accent-050);
}
.dt-naming .input-label { flex-basis: 100%; }
.dt-naming .input { max-width: 320px; }
.dt-wrap.dt-scroll { max-height: var(--table-max-h); overflow: auto; scroll-padding-top: var(--row-h); }
.dt-check { width: 24px; height: 24px; margin: 0; accent-color: var(--accent-600); cursor: pointer; }
.dt-own { display: inline-flex; align-items: center; }
.dt-actions { display: inline-flex; align-items: center; justify-content: flex-end; gap: var(--sp-1); }
.dt-date { white-space: nowrap; }
.moremenu-pop-up { top: auto; bottom: calc(100% + var(--sp-1)); }
.colchoose { position: relative; display: inline-flex; }
.colchoose-pop {
  position: absolute; top: calc(100% + var(--sp-1)); right: 0; z-index: 40;
  display: flex; flex-direction: column; gap: var(--sp-1); min-width: 220px; padding: var(--sp-2);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); box-shadow: var(--shadow-md);
}
.colchoose-item { display: flex; align-items: center; gap: var(--sp-2); min-height: 28px; font-size: var(--text-sm); }
@media (max-width: 640px) {
  /* The card layout: rows are cards, the wrapper is no box and no scroller. */
  .dt-wrap.dt-scroll { max-height: none; overflow: visible; }
  .dt-search { max-width: none; }
  /* A blank cell is blank in the card too: no label over nothing. And a card
     line is as tall as its content; the row height is for the table form. */
  .dt td:empty { display: none; }
  .dt tbody td { height: auto; }
}

/* ============================================================================
   UX Phase 3, E8. FORMS (Form.tsx, Field). One column at most --form-w wide,
   sections as headings with whitespace, "(nem kötelező)" in the label, one
   line of help with "Mit jelent?", Mégse then the primary in the footer.
   ============================================================================ */
.form-col { max-width: var(--form-w); }
.form-section + .form-section { margin-top: var(--sp-6); }
.form-section-title { font-size: var(--text-md); font-weight: 650; color: var(--ink-900); margin: 0 0 var(--sp-3); }
.form-section > .field + .field, .form-section > .field + .notice, .form-section > .notice + .field { margin-top: var(--sp-4); }
.field-opt { color: var(--ink-500); font-weight: 400; }
/* UX Phase 6: a fold that became a heading keeps its summary beside the title. */
.form-section-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2) var(--sp-3); margin: 0 0 var(--sp-3); }
.form-section-head > .form-section-title { margin: 0; }
/* A column of checkboxes under a heading: each its own line, the first included. */
.form-section > .check { display: flex; }
.form-section-note { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); color: var(--ink-500); font-size: var(--text-sm); min-width: 0; }
.field-more {
  min-height: 24px; padding: 0; border: 0; background: none; font: inherit;
  color: var(--accent-600); text-decoration: underline; cursor: pointer;
}
.field-more:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.form-footer { display: flex; flex-direction: column; gap: var(--sp-2); flex: 1; }
.form-footer-buttons { display: flex; justify-content: flex-end; gap: var(--sp-2); }
.form-footer-sticky {
  position: sticky; bottom: 0; z-index: 2; margin-top: var(--sp-6);
  padding: var(--sp-3) 0; border-top: 1px solid var(--line); background: var(--surface-0);
}
/* Rendezvények: the "shared with you" badge beside an event's name. */
.ev-shared { margin-left: var(--sp-2); }
/* UX Phase 3: an opened drawer takes focus as a container (tabIndex -1) so the
   next Tab reaches its first field; the container itself is not a control and
   draws no ring. Its controls keep theirs. */
.drawer:focus { outline: none; }
/* Helyszín: a floor and its spaces sit one step in from their building. */
.venue-tree-nest { padding-left: var(--sp-4); }

/* ============================================================================
   UX Phase 4, E4. KEZDŐLAP, attention first (Dashboard.tsx). Row 1 lists what
   needs the reader today, row 2 is the next 14 days beside a light month,
   row 3 is four figures that are links. The dark rail calendar is retired.
   ============================================================================ */
.home-sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.home-sec-head .section-title { margin: 0; }
.home-sec-head .btn-sm { min-width: 32px; min-height: 32px; }
.home-sec-head > .link { display: inline-flex; align-items: center; min-height: 24px; }
.home-attn { margin-bottom: var(--sp-5); }
/* W820, MR-25. The first-run checklist above row 1, for a new workspace. */
.firstrun { margin-bottom: var(--sp-5); }
.firstrun-list { margin: var(--sp-3) 0 0; padding-left: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-2); font-size: var(--text-body); }
.firstrun-list > li { min-height: 24px; }
.firstrun-list > li.is-done { color: var(--ink-500); }
.firstrun-list > li.is-done > span { margin-right: var(--sp-2); }
.attn-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); }
.attn-list > li + li { border-top: 1px solid var(--line); }
.attn-item {
  display: grid; grid-template-columns: 24px minmax(0, 1fr) auto auto; align-items: center; gap: var(--sp-3);
  min-height: var(--row-h); padding: var(--sp-2) var(--sp-4);
  color: var(--ink-900); text-decoration: none; font-size: var(--text-body);
}
.attn-item:hover { background: var(--surface-sunken); }
.attn-item:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }
.attn-icon { display: inline-flex; width: 20px; height: 20px; color: var(--ink-500); }
.attn-icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.attn-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.attn-event { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-500); font-size: var(--text-sm); max-width: 32ch; }
.attn-due { white-space: nowrap; color: var(--ink-500); font-size: var(--text-sm); }
/* Red is for late, and never by colour alone: the due label says it in words. */
.attn-item.is-late .attn-icon, .attn-item.is-late .attn-due { color: var(--err-600); }
@media (max-width: 640px) {
  .attn-item { grid-template-columns: 24px minmax(0, 1fr); }
  .attn-event, .attn-due { grid-column: 2; max-width: none; }
}

.home-row { display: grid; grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); gap: var(--sp-4); align-items: start; margin-bottom: var(--sp-5); }
@media (max-width: 1100px) { .home-row { grid-template-columns: minmax(0, 1fr); } }
.home-row > .page-card { margin: 0; }
.nowrap { white-space: nowrap; }

.mini-cal-grid { width: 100%; border-collapse: collapse; table-layout: fixed; text-align: center; }
.mini-cal-grid th { font-size: var(--text-xs); font-weight: 600; color: var(--ink-500); padding: var(--sp-1) 0; text-transform: none; }
.mini-cal-grid td { padding: 2px 0; }
.mini-cal-day {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%; font-size: var(--text-sm); color: var(--ink-700);
  border: 0; background: none; font-family: inherit; font-variant-numeric: tabular-nums;
}
button.mini-cal-day { cursor: pointer; font-weight: 650; color: var(--accent-700); background: var(--accent-050); box-shadow: inset 0 -2px 0 var(--accent-500); }
button.mini-cal-day:hover { background: var(--accent-100); }
button.mini-cal-day:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 1px; }
.mini-cal-grid td.is-out .mini-cal-day { color: var(--ink-400); }
.mini-cal-grid td.is-today .mini-cal-day { box-shadow: inset 0 0 0 2px var(--accent-600); color: var(--accent-700); font-weight: 700; }

.home-kpis { list-style: none; margin: 0 0 var(--sp-5); padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-3); }
@media (max-width: 640px) { .home-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.home-kpi {
  display: flex; flex-direction: column; gap: 2px; min-height: 72px; padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1);
  color: var(--ink-700); text-decoration: none; font-size: var(--text-sm);
}
.home-kpi b { font-size: var(--text-xl); font-weight: 700; color: var(--ink-900); }
.home-kpi:hover { border-color: var(--accent-500); }
.home-kpi:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.home-folds { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: var(--sp-5); }
.home-know { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-5); font-size: var(--text-body); }
.home-know p + p { margin-top: var(--sp-1); }
.home-holidays { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }

/* ============================================================================
   UX Phase 4, E5. RENDEZVÉNY KEZDŐLAP AS A COCKPIT (EventDashboard.tsx). The
   facts strip fills an empty fact inline, six area tiles sit in the main
   column, conversations and history in the right one.
   ============================================================================ */
.fact-set {
  min-height: 24px; padding: 0; border: 0; background: none; font: inherit; font-weight: 600;
  color: var(--accent-600); text-decoration: underline; cursor: pointer; text-align: left;
}
.fact-set:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.cockpit { display: grid; grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr); gap: var(--sp-5); align-items: start; }
@media (max-width: 1100px) { .cockpit { grid-template-columns: minmax(0, 1fr); } }
.cockpit .section-title:first-child { margin-top: 0; }
.area-tiles { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); }
@media (max-width: 640px) { .area-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.area-tile {
  display: flex; flex-direction: column; gap: 2px; min-height: 96px; padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1);
  color: var(--ink-700); text-decoration: none;
}
.area-tile:hover { border-color: var(--accent-500); }
.area-tile:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
.area-tile-name { font-size: var(--text-sm); font-weight: 600; color: var(--ink-700); }
.area-tile b { font-size: var(--text-xl); font-weight: 700; color: var(--ink-900); }
.area-tile-next { font-size: var(--text-sm); color: var(--ink-500); }
/* A zero is grey and invites, it never alarms (E5). */
.area-tile.is-zero { background: var(--surface-sunken); }
.area-tile.is-zero b { color: var(--ink-500); font-weight: 500; }

/* ============================================================================
   UX Phase 4, E12. RENDEZVÉNYNAPI MÓD (AppShell, Kapu, Kártyakapu, Validáló
   pult, Vezérlőközpont). Full screen with one bar, 48 px targets, one aligned
   control row, a 56 px scan field and a large result panel; the counts and
   last scans sit to the right from 768 px and below on a phone.
   ============================================================================ */
.app.eventday { display: flex; flex-direction: column; grid-template-columns: none; }
.eventday-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); flex-shrink: 0;
  min-height: 64px; padding: var(--sp-2) var(--sp-4);
  background: var(--sidebar-bg); color: var(--sidebar-fg); font-size: var(--text-body);
}
.eventday-mode { font-weight: 700; }
.eventday-event { flex: 1; color: var(--sidebar-fg); opacity: .85; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.eventday-net { display: inline-flex; align-items: center; gap: var(--sp-2); font-weight: 600; }
.eventday-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--ok-600); }
.eventday-net.is-off .eventday-dot { background: var(--err-600); }
.eventday-exit { min-height: var(--target-day); }
@media (max-width: 640px) {
  /* The exit button names the mode, so the label gives its room to the event. */
  .eventday-mode { display: none; }
  .eventday-bar { flex-wrap: nowrap; }
}
.eventday-offline {
  margin: 0; padding: var(--sp-3) var(--sp-4); flex-shrink: 0;
  background: var(--err-100); color: var(--ink-900); border-left: 4px solid var(--err-600);
  font-size: var(--text-body); font-weight: 600;
}
.eventday-main { flex: 1; overflow-y: auto; }
.eventday .page { max-width: none; }
.eventday .btn, .eventday .input, .eventday .seg-btn, .eventday .check { min-height: var(--target-day); font-size: var(--text-body); }
.eventday .dt tbody td { height: var(--target-day); }

.day-layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); gap: var(--sp-5); align-items: start; }
@media (max-width: 767px) { .day-layout { grid-template-columns: minmax(0, 1fr); } }
.day-main { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.day-controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: var(--sp-3); align-items: end; margin-bottom: var(--sp-4); }
.day-main > .day-controls { margin-bottom: 0; }
.day-controls .input { width: 100%; min-height: var(--target-day); }
.day-toggle { display: flex; width: 100%; }
.day-toggle .seg-btn { flex: 1; min-height: var(--target-day); font-size: var(--text-lg); font-weight: 700; }
.day-scan, .eventday .day-scan { min-height: var(--scan-h); font-size: var(--text-lg); }
.day-result {
  display: flex; flex-direction: column; justify-content: center; gap: var(--sp-2);
  min-height: 168px; padding: var(--sp-5) var(--sp-6); border-radius: var(--radius-lg);
  border: 2px dashed var(--line-strong); font-size: var(--text-lg);
}
.day-result.is-ok, .day-result.is-err { border: 0; color: var(--surface-1); }
.day-result.is-ok { background: var(--ok-600); }
.day-result.is-err { background: var(--err-600); }
.day-result-head { font-size: var(--text-verdict); font-weight: 800; line-height: 1.1; }
.day-result .door-verdict { margin: 0; }
.day-result code { color: inherit; font-size: var(--text-body); }
.day-result-idle { color: var(--ink-500); text-align: center; }
.day-side { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.day-side .section-title { margin: 0; }
.day-counts {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3);
  padding: var(--sp-4); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1);
}
.day-counts .fact-value { font-size: var(--text-xl); font-weight: 700; }
.day-scans { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); }
.day-scans li {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--sp-2) var(--sp-3); align-items: center;
  min-height: var(--target-day); padding: var(--sp-2) var(--sp-3); font-size: var(--text-body);
}
.day-scans li + li { border-top: 1px solid var(--line); }
.day-scan-who { min-width: 0; overflow-wrap: anywhere; }
.day-scan-who .dt-sub { display: block; }

/* W791 (S20), D-1484. The field camera and the photographs taken for a record. */
.field-camera { display: block; width: 100%; max-height: 60vh; object-fit: cover; border-radius: var(--radius); background: var(--surface-sunken); }
.field-thumbs { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--sp-2); }
.field-thumbs li { display: flex; flex-direction: column; gap: var(--sp-1); }
.field-thumbs img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius-sm); border: 1px solid var(--line); }

/* ==========================================================================
   UX Phase 5 (remaining screens, 2026-10-03). Classes that replaced the last
   inline styles. Token values only.
   ========================================================================== */
/* A tree drawn by depth: one step per level, capped at eight. Was an inline
   padding computed from the depth on SpaceDetail and VenueDetailScreen. */
.indent-0 { padding-left: 0; }
.indent-1 { padding-left: var(--sp-5); }
.indent-2 { padding-left: calc(2 * var(--sp-5)); }
.indent-3 { padding-left: calc(3 * var(--sp-5)); }
.indent-4 { padding-left: calc(4 * var(--sp-5)); }
.indent-5 { padding-left: calc(5 * var(--sp-5)); }
.indent-6 { padding-left: calc(6 * var(--sp-5)); }
.indent-7 { padding-left: calc(7 * var(--sp-5)); }
.indent-8 { padding-left: calc(8 * var(--sp-5)); }
/* The venue's building, floor and space tree: 12 px plus 16 px a level. */
.tree-depth-0 { padding-left: var(--sp-3); }
.tree-depth-1 { padding-left: calc(var(--sp-3) + var(--sp-4)); }
.tree-depth-2 { padding-left: calc(var(--sp-3) + 2 * var(--sp-4)); }
.tree-depth-3 { padding-left: calc(var(--sp-3) + 3 * var(--sp-4)); }
.tree-depth-4 { padding-left: calc(var(--sp-3) + 4 * var(--sp-4)); }
.tree-depth-5 { padding-left: calc(var(--sp-3) + 5 * var(--sp-4)); }
.tree-depth-6 { padding-left: calc(var(--sp-3) + 6 * var(--sp-4)); }
/* CheckedForm: one column with the field rhythm (E8). */
.checked-form > * + * { margin-top: var(--sp-4); }
.checked-form > .field-row .field + .field { margin-top: 0; }
/* A sub-block inside a drawer panel, set off by a dashed rule (Performers' move). */
.sub-block-dashed { margin-top: var(--sp-2); padding-top: var(--sp-2); border-top: 1px dashed var(--line); }
/* E8 in a toolbar: a CheckedForm laid out as one row keeps the row's own gaps. */
.checked-form.toolbar, .checked-form.row { max-width: none; }
/* UX Phase 6: a checked form laid out as a wide panel (a grid of fields), not one column. */
.checked-form.form-wide { max-width: none; }
.checked-form.toolbar > * + *, .checked-form.row > * + * { margin-top: 0; }
.checked-form.toolbar .field { margin: 0; }
/* A bullet list inside a notice or panel, indented by one step. */
.list-indent { margin: 0; padding-inline-start: var(--sp-4); }
.list-indent-top { margin: var(--sp-1) 0 0; padding-inline-start: var(--sp-4); }
/* Showbook studio's cue matrix: moments down, departments across. */
.cue-matrix-wrap { overflow-x: auto; }
.cue-matrix { min-width: 640px; }
.cue-matrix th.cm-moment { min-width: 200px; text-align: left; }
.cue-matrix th.cm-dept { min-width: 180px; text-align: left; }
.cue-matrix th.cm-seg { text-align: left; padding-top: var(--sp-3); }
/* A colour swatch beside a name; the colour itself is the record's own. */
.swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-inline-end: var(--sp-1); background: var(--ink-300); vertical-align: middle; }
.swatch-lg { width: 12px; height: 12px; border-radius: 3px; margin-inline-end: 0; }
.u-maxw-240 { max-width: 240px !important; }
.u-maxw-340 { max-width: 340px !important; }
.u-maxw-360 { max-width: 360px !important; }
.u-maxw-420 { max-width: 420px !important; }
/* A short list that scrolls inside its box rather than pushing the form down. */
.scroll-box { max-height: 260px; overflow-y: auto; }
/* Showbook live: the operator's screen reads from a distance. */
.live-now-title { font-size: calc(var(--text-xl) * 1.25); margin: var(--sp-1) 0; }
.live-hold { width: 100%; font-size: var(--text-xl); padding: var(--sp-3); }
.live-go { font-size: var(--text-xl); padding: var(--sp-2) var(--sp-4); }
.live-quiet { color: var(--warn-700); }
/* A cue row: its department's colour on the left edge (the colour is the record's). */
.live-cue { padding: var(--sp-2) 0 var(--sp-2) var(--sp-2); border-bottom: 1px solid var(--line); border-left: 4px solid var(--line); }
.live-cue-later { opacity: 0.72; }
.live-cue-next .live-cue-text { font-size: var(--text-xl); }
.live-cue-text { font-size: var(--text-lg); }
/* A native colour picker beside its hex field. */
.input-colour { width: calc(var(--sp-8) + var(--sp-4)); padding: calc(var(--sp-1) / 2); }
.micro-avatar.is-approved { background: var(--ok-600); }
.micro-avatar.is-rejected { background: var(--err-600); }
.micro-avatar.is-file { background: var(--neutral-600); }
/* Flex children that grow from a set basis, and a row-wide break inside a wrapping row. */
.u-grow-200 { flex: 1 1 200px; }
.u-grow-220 { flex: 1 1 220px; }
.u-grow-240 { flex: 1 1 240px; min-width: 0; }
.u-grow-260 { flex: 1 1 260px; }
.u-grow-160 { flex: 1 1 160px; min-width: 0; }
.u-row-break { flex-basis: 100%; margin-top: var(--sp-2); }
.u-bt-pad { border-top: 1px solid var(--line); padding-top: var(--sp-3); }
/* A production stage strip: the current stage marked, the rest quiet. */
.chip.stage-chip { cursor: default; opacity: 0.7; }
.chip.stage-chip.is-current { opacity: 1; border-color: var(--accent-600); color: var(--accent-700); background: var(--accent-050); }
.checked-form.u-flex > * + *, .checked-form.u-grid > * + * { margin-top: 0; }
.checked-form.toolbar, .checked-form.row { align-items: flex-end; }
/* A done task reads struck through. */
.is-done { text-decoration: line-through; }
/* The checklist's Kanban board: lanes side by side, scrolling sideways. */
.kanban { display: flex; gap: var(--sp-4); overflow-x: auto; }
.kanban-lane { min-width: 256px; }
/* A fieldset used only to disable a group of controls: no frame of its own. */
.fieldset-plain { border: 0; padding: 0; margin: 0; min-width: 0; }
.u-fw-400 { font-weight: 400 !important; }
.u-shrink-0 { flex-shrink: 0 !important; }
.dash-stats.dash-stats-spaced { margin: var(--sp-2) 0 var(--sp-4); }
.u-inline-ai { display: inline-flex; align-items: center; gap: var(--sp-2); }
/* A thumbnail bounded in both directions, its aspect ratio kept. */
.thumb-contain { display: block; width: auto; height: auto; object-fit: contain; max-width: 72px; max-height: 72px; }
.thumb-contain.thumb-56 { max-width: 56px; max-height: 56px; }
.thumb-contain.thumb-40 { max-width: 40px; max-height: 40px; }
.svg-fluid { display: block; width: 100%; height: auto; }
.u-mono { font-family: var(--font-mono); }
.u-break-word { word-break: break-word; }
.u-inline-gap { display: inline-flex; gap: var(--sp-2); align-items: center; }
.u-inline-wrap { display: inline-flex; gap: var(--sp-1); flex-wrap: wrap; }
.u-maxw-44ch { max-width: 44ch; display: inline-block; }
.u-maxw-62ch { max-width: 62ch; display: block; }
.u-maxw-120 { max-width: 120px !important; }
.u-ml-2 { margin-left: var(--sp-2) !important; }
.u-pb-2 { padding-bottom: var(--sp-2) !important; }
/* Numbering: one section per document type. */
.numbering-group { margin-bottom: var(--sp-8); }
.numbering-group > h2 { font-size: var(--text-lg); margin-bottom: var(--sp-2); }
/* A preference row: label left, cells right, a rule above. */
.pref-row { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-3); padding: var(--sp-3) 0; border-top: 1px solid var(--line); }
/* Venue share grant card. */
.panel-card.card-accent { border-color: var(--accent-500); }
.share-facets-title { font-weight: 620; font-size: var(--text-sm); margin-bottom: var(--sp-1); }
.share-facets { display: flex; flex-direction: column; gap: var(--sp-1); }
.share-facet { display: flex; gap: var(--sp-2); align-items: flex-start; }
.share-facet input { margin-top: 3px; }
.share-inline { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-sm); }
.share-stack { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--text-sm); }
.share-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.checked-form.form-grid > * + *, .checked-form.row-tight > * + * { margin-top: 0; }
/* A whole card as one button (EventGate's event list). */
.card-button { width: 100%; text-align: left; cursor: pointer; }
/* The error boundary's detail block. */
.error-detail, .code-block { white-space: pre-wrap; overflow-wrap: anywhere; font: var(--text-sm)/1.5 var(--font-mono); background: var(--surface-sunken); color: var(--ink-700); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: var(--sp-3); margin: 0; }
.u-minw-6rem { min-width: 6rem; }
.year-label { min-width: 3.2em; text-align: center; }
.scroll-box-sm { max-height: 14rem; overflow-y: auto; }
.u-ml-1 { margin-left: var(--sp-1) !important; }
/* A button that reads as a link inside a table cell (Numbering's issued count
   opens its ledger). Was a phantom class: the button rendered as native chrome
   at 17 px. 24 px is the WCAG 2.5.8 floor. */
.linklike { appearance: none; background: none; border: 0; padding: 0 var(--sp-1); min-height: 24px; min-width: 24px; font: inherit; font-weight: 520; color: var(--accent-600); text-decoration: underline; cursor: pointer; border-radius: var(--radius-sm); }
.linklike:focus-visible { outline: 2px solid var(--accent-600); outline-offset: 2px; }
main.content:focus-visible { outline: 2px solid var(--accent-600); outline-offset: -2px; }

/* ============================================================================
   UX Phase 6 (2026-10-03). THE EXTERNAL SURFACES: the client portal, the
   public programme and the presenter display, on the same tokens as the app.
   ============================================================================ */
/* A portal card's name is a heading of its own, not a second page title. */
.portal-card-title { margin: 0; font-size: var(--text-lg); font-weight: 650; color: var(--ink-900); }
/* A card that holds a table edge to edge. */
.portal-card-flush { padding: 0; overflow: hidden; }
/* The public programme: one moment per row, its time in a fixed column. */
.prog-row {
  display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: baseline;
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--line);
}
/* A moment that is over reads quieter, by colour (ink-500 keeps 4.5:1), not by opacity. */
.prog-row.is-done-row { color: var(--ink-500); }
.prog-time { min-width: calc(var(--sp-8) + var(--sp-6)); font-variant-numeric: tabular-nums; }
.prog-was { text-decoration: line-through; }

/* UX Phase 6, controls under 24 px (WCAG 2.2, 2.5.8). Hit areas grow to 24 px
   where the look does not have to change: a labelled checkbox's label (the
   label is part of its target), the table's sort buttons, text-styled buttons
   and the form builder's version chips. The 16 px box itself is the
   foundation's and stays. */
.check { min-height: 24px; }
.dt-sort { min-height: 24px; }
button.link { min-height: 24px; }
button.chip { min-height: 24px; }

/* B-347 gate, 2026-10-04 (D-1851). A link standing on its own in a row of
   controls is a target, not a word in a sentence, so it gets WCAG 2.5.8's
   24 px like button.link. Found on the sample event's "Mintarendezvény
   megnyitása" (20 px); same repair as .home-sec-head > .link, made general. */
:is(.row, .row-end, .row-tight, .toolbar) > a.link { display: inline-flex; align-items: center; min-height: 24px; }
/* The venue's "Megnyitás a Google Maps-ben" sat in a column flex, stretched to
   the full card width at 20 px tall. It is as wide as its words now, and 24 px. */
:is(.venue-map, .venue-map-empty) > a.link { align-self: flex-start; display: inline-flex; align-items: center; min-height: 24px; }

/* W980. The Finances ticketing chart: a plain SVG that scales with its card. */
.fin-chart { display: block; width: 100%; height: auto; max-height: 280px; }

/* =========================================================================
   W1010, D-2022. THE PERFORMANCE PAGE (the public shop), after the owner's
   Royal Albert Hall reference: the show and its date across the top, the
   quantity at the right, the hall's picture beside the prices. One column
   under 900px, the picture above the prices.
   ========================================================================= */
.shop-wide { max-width: 1180px; margin: 0 auto; padding: var(--sp-4) var(--sp-4) var(--sp-8); }
.shop-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--sp-5); }
.shop-head { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--sp-5); flex-wrap: wrap; margin-bottom: var(--sp-5); }
.shop-title { font-size: clamp(28px, 4vw, 44px); line-height: 1.05; letter-spacing: -.02em; margin: 0; }
.shop-date { max-width: 360px; font-size: var(--text-lg); }
.shop-when { color: var(--ink-700); font-size: var(--text-lg); }
.shop-qty { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.shop-stepper { display: inline-flex; align-items: center; gap: var(--sp-2); }
.shop-stepper button { width: 40px; height: 40px; border: 0; background: transparent; font-size: 24px; color: var(--ink-900); cursor: pointer; border-radius: var(--radius); }
.shop-stepper button:hover:not([disabled]) { background: var(--surface-2); }
.shop-stepper button[disabled] { color: var(--ink-300); cursor: default; }
.shop-stepper output { min-width: 40px; height: 44px; display: inline-grid; place-items: center; background: var(--ink-900); color: var(--surface-1);
                       border-radius: var(--radius-sm); font-weight: 700; font-size: var(--text-lg); }
.shop-qty-label { color: var(--ink-700); }
.shop-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: var(--sp-6); align-items: start; }
.shop-hero img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius-lg); display: block; }
.shop-hero-blank { aspect-ratio: 4 / 3; border-radius: var(--radius-lg); background: linear-gradient(135deg, var(--accent-600), var(--accent-700));
                   color: #fff; display: grid; place-items: center; font-size: var(--text-xl); font-weight: 700; padding: var(--sp-5); text-align: center; }
.shop-about { margin-top: var(--sp-3); color: var(--ink-700); }
.shop-panel { min-width: 0; }
/* The selected tab's pill slides between options, easing in and out. One
   pseudo-element, sized to one equal column and moved by its own width, so the
   markup stays three plain buttons. :has() reads which tab is selected and how
   many there are (up to three). */
.shop-tabs { position: relative; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; background: var(--ink-900); border-radius: 999px; padding: 3px; }
.shop-tabs::before { content: ""; position: absolute; top: 3px; bottom: 3px; left: 3px; width: calc(100% - 6px); border-radius: 999px; background: var(--surface-1);
                     transition: translate .32s cubic-bezier(.65, 0, .35, 1), width .32s cubic-bezier(.65, 0, .35, 1); }
.shop-tabs:has(> .shop-tab:nth-child(2):last-child)::before { width: calc((100% - 6px) / 2); }
.shop-tabs:has(> .shop-tab:nth-child(3):last-child)::before { width: calc((100% - 6px) / 3); }
.shop-tabs:has(> .shop-tab:nth-child(2)[aria-selected="true"])::before { translate: 100% 0; }
.shop-tabs:has(> .shop-tab:nth-child(3)[aria-selected="true"])::before { translate: 200% 0; }
.shop-tab { position: relative; z-index: 1; border: 0; background: transparent; color: var(--surface-1); font-weight: 700; padding: 10px var(--sp-3); border-radius: 999px; cursor: pointer; font-size: var(--text-md);
            transition: color .32s cubic-bezier(.65, 0, .35, 1); }
.shop-tab[aria-selected="true"] { color: var(--ink-900); }
@media (prefers-reduced-motion: reduce) {
  .shop-tabs::before, .shop-tab { transition: none; }
}
.shop-tab:focus-visible { outline: 2px solid var(--accent-500); outline-offset: 2px; }
.shop-info { display: flex; gap: var(--sp-3); justify-content: space-between; align-items: flex-start; color: var(--ink-700); }
.shop-select-all { border: 0; background: transparent; color: var(--ink-900); font-weight: 600; white-space: nowrap; cursor: pointer; font-size: var(--text-md); }
.shop-select-all[aria-pressed="true"] { color: var(--accent-600); }
.shop-none { font-weight: 700; font-size: var(--text-lg); color: var(--ink-900); }
.shop-rows { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
.shop-rows > li { border-bottom: 1px solid var(--line); }
.shop-row { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: var(--sp-3); padding: var(--sp-4) var(--sp-2);
            background: transparent; border: 0; text-align: left; cursor: pointer; color: var(--ink-900); font: inherit; border-radius: var(--radius); }
.shop-row:hover:not([disabled]) { background: var(--surface-2); }
.shop-row[aria-pressed="true"] { background: var(--accent-050); box-shadow: inset 3px 0 0 var(--accent-600); }
.shop-row[disabled] { cursor: default; color: var(--ink-400); }
.shop-row:focus-visible { outline: 2px solid var(--accent-500); outline-offset: 2px; }
.shop-row-name { display: flex; flex-direction: column; gap: 2px; }
.shop-price { border: 1px solid var(--line-strong); border-radius: var(--radius-sm); padding: 6px var(--sp-3); background: var(--surface-1); white-space: nowrap; font-weight: 600; }
.shop-row[aria-pressed="true"] .shop-price { border-color: var(--accent-600); color: var(--accent-700); }
.shop-line { display: flex; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) 0; }
/* W1013. The order view: the event, then a card per ticket with its code. */
.order-head { margin-bottom: var(--sp-4); }
.order-head p { color: var(--ink-700); }
.order-count { font-size: var(--text-lg); }
.order-list { list-style: none; display: grid; gap: var(--sp-3); }
.order-tickets { list-style: none; display: grid; gap: var(--sp-3); }
.order-ticket { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: var(--sp-4); align-items: start; padding: var(--sp-3); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); }
.order-qr { width: 112px; height: 112px; background: #fff; border-radius: var(--radius-sm); }
.order-seat { display: flex; gap: var(--sp-4); }
.order-seat dt { font-size: var(--text-xs); color: var(--ink-500); }
.order-seat dd { font-weight: 600; }
@media (max-width: 420px) { .order-ticket { grid-template-columns: 1fr; } }
/* W1016. A resale listing and the Sell tickets form, inside the order's Tickets tab. */
.resale-card { padding: var(--sp-3) var(--sp-4); border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); }
/* W1019. The seller's resold tickets, a card per ticket, as the reference's order page. */
.resale-head { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-3); }
.resale-grid { list-style: none; display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--sp-3); }
.resale-ticket { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); overflow: hidden; box-shadow: var(--shadow-sm); }
.resale-ticket-band { background: var(--mag-600); color: var(--on-accent); text-align: center; font-weight: 700; font-size: var(--text-sm); padding: var(--sp-1) var(--sp-3); }
.resale-ticket-body { display: grid; gap: var(--sp-3); padding: var(--sp-4); text-align: center; flex: 1; }
.resale-ticket-kind { color: var(--ink-700); }
.resale-ticket-place { display: flex; justify-content: space-between; gap: var(--sp-3); text-align: left; }
.resale-ticket-place dt { font-size: var(--text-xs); color: var(--ink-500); }
.resale-ticket-place dd { font-size: var(--text-xl); font-weight: 700; color: var(--ink-900); }
.resale-ticket-mark { width: 24px; height: 24px; margin: 0 auto; border-radius: 50%; display: grid; place-items: center; font-weight: 700; color: var(--on-accent); background: var(--ok-600); }
.resale-ticket-mark.is-listed { background: var(--accent-600); }
.resale-ticket-mark.is-withdrawn { background: var(--neutral-600); }
.resale-ticket-foot { border-top: 1px solid var(--line); padding: var(--sp-3) var(--sp-4); }
/* W1019. Resale and transfer, when this order cannot use them, and why. */
.shop-unavailable { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-2); }
.shop-help-mark { display: inline-grid; place-items: center; width: 20px; height: 20px; margin-left: var(--sp-2); border-radius: var(--radius-sm); background: var(--accent-600); color: var(--on-accent); font-weight: 700; font-size: var(--text-xs); }
/* W1017. The price bands on the map at step two, as the reference's legend. */
.shop-legend { list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); font-size: var(--text-sm); color: var(--ink-700); }
@media (max-width: 900px) {
  .shop-grid { grid-template-columns: 1fr; }
  .shop-head { align-items: stretch; }
  .shop-qty { justify-content: space-between; width: 100%; }
}

/* ---------------------------------------------------------------------------
   W1022, HR-3. TEAM AVAILABILITY'S MONTH: one row per person, one column per
   day. Away is the accent, waiting for a decision is hatched in the same hue,
   a public holiday or bridge day is the warning tint, a weekend is sunken.
   Colour is never alone: every cell carries its state as text for assistive
   technology, and the legend names each fill.
   ------------------------------------------------------------------------- */
.avail-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-1); }
.avail { border-collapse: collapse; font-size: var(--text-xs); }
.avail th, .avail td { border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); min-width: 24px; height: 30px; padding: 0; text-align: center; color: var(--ink-500); font-weight: 500; }
.avail .avail-person { position: sticky; left: 0; z-index: 1; min-width: 160px; padding: 0 var(--sp-2); text-align: left; background: var(--surface-1); color: var(--ink-900); font-size: var(--text-sm); white-space: nowrap; }
.avail .is-weekend { background: var(--surface-sunken); }
.avail .is-holiday { background: var(--warn-100); }
.avail .is-away { background: var(--accent-500); }
.avail .is-pending { background: repeating-linear-gradient(135deg, var(--accent-100) 0 4px, var(--surface-1) 4px 8px); }
.avail .is-half-start { background: linear-gradient(90deg, transparent 50%, var(--accent-500) 50%); }
.avail .is-half-end { background: linear-gradient(90deg, var(--accent-500) 50%, transparent 50%); }
.avail .is-today { box-shadow: inset 0 -3px 0 var(--accent-600); }
.avail-legend { display: flex; flex-wrap: wrap; gap: var(--sp-4); font-size: var(--text-xs); color: var(--ink-700); }
.avail-legend span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.avail-legend i { display: inline-block; width: 14px; height: 14px; border: 1px solid var(--line); border-radius: var(--radius-sm); }
.avail-legend .is-away-key { background: var(--accent-500); }
.avail-legend .is-pending-key { background: repeating-linear-gradient(135deg, var(--accent-100) 0 4px, var(--surface-1) 4px 8px); }
.avail-legend .is-holiday-key { background: var(--warn-100); }
.avail-legend .is-weekend-key { background: var(--surface-sunken); }
/* W1029. Blackouts: a band of their own, hatched in the warning ink. The week view gives each day room for its name. */
.avail .is-blackout { background: repeating-linear-gradient(45deg, var(--warn-100) 0 5px, var(--surface-1) 5px 10px); box-shadow: inset 0 0 0 1px var(--warn-700); }
.avail-legend .is-blackout-key { background: repeating-linear-gradient(45deg, var(--warn-100) 0 3px, var(--surface-1) 3px 6px); border-color: var(--warn-700); }
.avail.is-week th, .avail.is-week td { min-width: 88px; height: 40px; }
