/* App components — HAND-OWNED (module 2.0's map; built by 2.1–2.13).
 *
 * Two kinds of rule live here, and only here:
 *   COPY   — classes the design bundle DEFINES that the extractor never carried into
 *            components.css. Copied verbatim from the named design source.
 *   AUTHOR — classes the design genuinely lacks (docs/class-map.md). Tokens only.
 *
 * A separate file so re-running the extractor cannot clobber it. components.css remains
 * extractor-owned; never hand-edit that one.
 */

/* ══ COPY — design-defined, extractor-omitted ═══════════════════════════════════════════ */

/* region-label / nowrap — design v5, STAGE 0.
   Both are universal but the extractor emitted them per screen: `.region-label` only into
   page-team.css (team-branding.html is the one frame that draws async region states) and
   `.nowrap` into all fourteen page sheets but never the shared one. Every page has loading
   regions and unbreakable cells, so both belong in a sheet every page loads.
   `.region-label` is the design's own text beside a spinner; its sibling `.statechip` is NOT
   copied — that chip exists so a reviewer can tell three simultaneously-drawn states apart,
   and the product only ever renders one. */
.region-label { font-size: var(--fs-sm); color: var(--ink-quiet) }
.nowrap { white-space: nowrap }

/* modal sizes + the <dialog> host — design v5 (admin.html and eight other screens).
   The extractor emits `.modal-sm|md|lg` into per-page sheets only, and modals are not a
   per-page component. The DIALOG rules are ours: the design draws its modals on an
   `.overlay-stage`, which is the review page's way of floating a panel and has no product
   meaning, so the product uses a native <dialog> and `showModal()` — top layer, backdrop,
   inert background, focus trap, Escape, and focus returned to the opener, none of it
   hand-rolled. The UA's default dialog chrome (border, padding, white background, centred
   auto margins) is reset so the design's `.modal` is the only surface. */
.modal-sm { max-width: var(--modal-sm) }
.modal-md { max-width: var(--modal-md) }
.modal-lg { max-width: var(--modal-lg) }
dialog {
  border: 0; padding: 0; background: transparent; color: inherit;
  max-width: min(92vw, var(--modal-lg)); max-height: 88vh; overflow: visible;
}
dialog .modal { max-height: 88vh }
dialog::backdrop { background: rgb(8 19 31 / 0.5) }

/* An error inside a `.form-grid` must SPAN it. A grid places every child in a column, so a
   `.field-error` dropped at the end of a form-grid was laid out as the next FIELD - the refusal
   about the pixel's name rendered a card's width to the right of the name box, level with it and
   reading as an unrelated caption (owner's screenshot, 2026-07-30). Measured across the console
   the moment it was reported: 24 stranded errors on nine screens.
   `grid-column: 1/-1` is the design's own `.form-span`; this applies it by position rather than
   asking eleven call sites to remember the class, and it holds for errors added later. */
.form-grid > .field-error { grid-column: 1 / -1 }

/* hastip — the positioning context for the design's `.tip-bubble`, which is absolutely
   positioned against its trigger. The bundle gets this for free from its `.tip-demo` review
   container; the product needs a real one. `display: inline-flex` so wrapping a button does not
   change how the row lays out around it. */
.hastip { position: relative; display: inline-flex }
/* The bubble is revealed by hover OR focus. Focus is the half that matters: the whole point of
   `aria-disabled` over `disabled` is that a keyboard user can reach the control and be told why
   it is unavailable. */
.hastip .tip-bubble { opacity: 0; visibility: hidden; transition: opacity .12s ease }
.hastip:hover .tip-bubble,
.hastip:focus-within .tip-bubble { opacity: 1; visibility: visible }
@media (prefers-reduced-motion: reduce) { .hastip .tip-bubble { transition: none } }

/* w-NN — the skeleton width utilities, design v5. They pair with `.skel` (which IS in the
   shared sheet) but the extractor emitted them per page, so a skeleton built by the SHARED
   loader in app.js had nothing to size it on any screen whose own sheet lacked them. Values
   verbatim; the set is the design's own eight. */
.w-30 { width: 30% } .w-40 { width: 40% } .w-44 { width: 44% } .w-50 { width: 50% }
.w-60 { width: 60% } .w-70 { width: 70% } .w-80 { width: 80% } .w-100 { width: 100% }

/* livestrip-wrap — the design writes this as `<span style="flex-basis:100%;height:0">`, an
   inline style that forces the nickname chips onto their own line of the flex strip. Inline
   styles are review-document shorthand; the product gives it a name so the markup carries no
   layout. Values verbatim. */
.livestrip-wrap { flex-basis: 100%; height: 0 }

/* field-req — design v5: the required marker beside a field's label. Taken now because the
   create modal's one field is required and the design draws it; module 10.10 rolls it out
   across every form. */
.field-req { color: var(--danger-solid); margin-left: 2px }

/* field-row — design v5 auth.html:186 (component layer; absent from components.html, so the
   extractor never carried it). The label/forgot-link line above a field. */
.field-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3) }

/* 51.9 — TWO FIELDS SIDE BY SIDE MUST START THEIR INPUTS AT THE SAME HEIGHT, and before this they
   did not. A field whose label sits alone renders an 11px `.field-label`; one that also carries a
   character counter renders a 20px `.field-row` around the same label — so on the pixel settings
   card "Public key" began its input 9px above "Site hostname", which the owner saw immediately as
   "these elements are not seems aligned".
   ⚠ SCOPED TO `.form-grid`, WHERE FIELDS SHARE A ROW. In a stacked form nothing is beside anything
   and a uniform label height would only add space; this is about columns lining up.
   ⚠ AND IT IS `min-height`, NOT `height`: a label that wraps to two lines on a narrow column must
   still be allowed to grow, and pinning the height would clip it. The value is the natural height
   of the taller of the two — a label row carrying a counter — so nothing moves on the fields that
   already had one. */
.form-grid > .field > .field-label,
.form-grid > .field > .field-row {
  min-height: 20px;
}

.form-grid > .field > .field-label {
  display: flex;
  align-items: center;
}

/* row-action — design v5 auth.html:371-377 ("carried over from the shared screen layer"),
   with its two drawn modifiers. */
.row-action {
  appearance: none; background: none; border: 0; padding: 0;
  font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--accent-text); cursor: pointer; white-space: nowrap; text-decoration: none;
}
.row-action:hover { text-decoration: underline }
.row-action:disabled { color: var(--disabled-ink); cursor: not-allowed; text-decoration: none }
.row-action-danger { color: var(--danger-ink) }
.row-action-quiet { color: var(--ink-body) }

/* tab-as-link — design v5 (components layer via auth.html:284-286): tabs are LINKS with
   aria-current="page", and .tab carries text-decoration:none. The extracted components.css
   snapshot predates both (it styles only [aria-selected], the button form), so anchor tabs
   rendered underlined with no active state on pages using link tabs (caught by 4.2's
   screenshots on /visitor). */
a.tab { text-decoration: none }
.tab[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent-fill) }

/* (`.btn.btn-quiet` lived here until module 10.S. It was MINE - the design has six button
   variants and that was not one of them - and Phase 9 had already had to give it a surface
   because a control with no edge reads as body text. All 70 uses across 13 pages are now
   `btn-secondary`, which is what it had become anyway, and the "do it now" ones are the design's
   `btn-tinted`: "Tinted is the one that was missing - the shipped app uses variant='light' 94
   times against 3 uses of the outlined secondary, so the most common button in the product
   previously had no design." Deleted rather than kept: a rule no markup can reach is
   indistinguishable from a rule that works. */


/* (`.btn.rail-signout` lived here until Stage 0. The design's rail is icon-only, so sign-out is
   now a plain `button.rail-item` sharing the anchors' geometry and states — shell.css. Deleted
   rather than kept: a rule no markup can reach is indistinguishable from a rule that works.) */

/* cell-sub — the second line of a two-line table cell. The extracted rule sets font, colour
   and truncation but NOT display, so it only stacked when its sibling happened to be a block
   (.cell-main). After an inline sibling — a .cell-link, which is what a drill-through row uses
   — path and title collapsed onto one line and rendered as "/pricingPricing" (owner's review).
   Declared here rather than in the page sheets: those are extractor output. */
.cell-sub { display: block }

/* meter-row: the track is the flexible part, the caption sits beside it. The track is CAPPED —
   left to grow it filled a 1140px card and stranded its own caption a thousand pixels away
   from the bar it describes (owner's visual review). */
.meter-row .meter-track { flex: 0 1 360px; min-width: 0 }

/* plancard — the plan grid is drawn as three cards; with one purchasable plan the single card
   stretched the full card width and spread its three feature bullets across a 1140px row. */
.plancard { max-width: 420px }

/* The preview iframe resolves width:100% against the card, so the inset margin above pushed its
   MARGIN box 32px past the card — invisible to a rect check (rects exclude margins) and caught
   by breakpoint-gate as 15px of horizontal scroll on /content-edit at 1024. Take the inset out
   of the width instead. */
.card > .previewframe { width: calc(100% - 2 * var(--pad-card)) }

/* (Card insets are handled by the module-4.2 convention further down this file — the
   owner's 2026-07-31 review found the CONTAINER children it never listed, and they are
   added to that same list rather than given a competing padding rule.) */

/* cell-link — design overview.html */
.cell-link { color: var(--accent-text); text-decoration: none; font-weight: var(--fw-semibold) }
.cell-link:hover { text-decoration: underline }
.cell-link:visited { color: var(--accent-9) }

/* chip — design visitors.html */
.chip {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-xs); font-weight: var(--fw-medium);
  color: var(--accent-text-on-tint); background: var(--accent-tint);
  border: 0; border-radius: var(--r-full); padding: 6px 10px;
  cursor: pointer; white-space: nowrap; text-decoration: none;
}
.chip:hover { background: var(--accent-border) }


/* crumbs — design automations.html; the SEPARATOR and the current-page leaf were missed when
   this was copied and only surfaced at module 10.1, when /pixel became the first screen to draw
   a full trail. Both are verbatim from pixels.html. `.crumbs a` is ours: the shared `a` rule
   underlines every anchor, and a breadcrumb trail of underlined links is noise. */
.crumbs { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); font-weight: var(--fw-medium) }
.crumbs a { text-decoration: none }
.crumbs-sep { color: var(--hairline-strong) }
.crumbs-here { color: var(--ink-quiet); white-space: nowrap }

/* danger-confirm — design team-branding.html (the inline confirm is the product convention) */
.danger-confirm { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; font-size: var(--fs-xs); color: var(--danger-ink) }

/* kpi-label — design automations.html */
.kpi-label { font-size: var(--fs-micro); letter-spacing: var(--tr-micro); text-transform: uppercase; font-weight: var(--fw-semibold); color: var(--ink-quiet) }

/* linkbtn-danger — design pixels.html (screen ADDITION list, item 24) */
.linkbtn-danger { color: var(--danger-ink) }

/* ── rename TARGETS the extractor also missed ── */

/* preview — design automations.html */
.preview { border-top: 1px solid var(--hairline-soft); padding-top: 11px; display: flex; flex-direction: column; gap: var(--sp-3) }

/* editor-grid — design automations.html */
.editor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: var(--sp-4); align-items: start }

/* formline — design admin.html */
.formline { margin: var(--sp-2) 0 0; font-family: var(--font-mono); font-size: var(--fs-micro); line-height: 1.65; color: var(--ink-quiet); word-break: break-word }
.formline b { color: var(--ink-body); font-weight: var(--fw-semibold) }

/* kpi-val — design automations.html, where it is an inline stat. The overview's KPI STRIP is a
   different component in the design (.kpi-v: mono, tabular, --fs-xl) and the strip is the only
   place .kpi-val is used, so it takes the strip's scale — a 15px number in a 92px tile read as
   under-set beside its own label (owner's visual review, 2026-07-31). tabular-nums so the five
   figures line up column-to-column as they change. */
.kpi-val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: var(--fs-xl); line-height: var(--lh-xl); font-weight: var(--fw-semibold); color: var(--ink) }

/* kpis — design overview.html (item 6, the KPI strip) */
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); gap: var(--sp-3) }

/* swatch — design admin.html */
.swatch { width: 14px; height: 14px; border-radius: var(--r-xs); border: 1px solid var(--hairline-strong); display: inline-block; vertical-align: -2px; flex-shrink: 0 }

/* swatchrow — design content.html */
.swatchrow { display: flex; gap: var(--sp-3); align-items: center }

/* table-wrap-scroll — design admin.html */
.table-wrap-scroll { overflow-x: auto; overflow-y: hidden }

/* ══ AUTHOR — no design equivalent (docs/class-map.md names each) ═══════════════════════ */

.card-centred { max-width: 520px; margin: 10vh auto 0; text-align: center }
.card-danger { border-color: var(--danger-solid) }
.card-meta { font-size: var(--fs-xs); color: var(--ink-quiet) }

/* 203.6 — a card head that carries a control AND a note on its right. `.card-head` is already
   `space-between`, so a third child would have spread all three across the width and left the
   toggle floating in the middle; this keeps the pair together at the end. */
.card-headtools { display: flex; align-items: center; gap: var(--sp-3); }
.card-headtools .seg button { padding: 5px 10px; font-size: var(--fs-xs); }

/* the look of design cell-2line > strong.cell-truncate, without restructuring six pages */
.cell-main { display: block; font-weight: var(--fw-semibold); color: var(--ink) }
.cell-wrap { white-space: normal; overflow-wrap: anywhere }

.checkbox { width: 16px; height: 16px; accent-color: var(--accent-text) }
.code { font-family: var(--font-mono); font-size: var(--fs-xs) }
.colorpair { display: flex; gap: var(--sp-3); align-items: center }
.condlist { display: flex; flex-direction: column; gap: var(--sp-3) }
.danger-row { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); flex-wrap: wrap }

.editor-fields { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0 }

.factgrid { display: grid; grid-template-columns: max-content 1fr; gap: var(--sp-2) var(--sp-5); margin: 0 }
.factgrid dt { color: var(--ink-quiet); font-size: var(--fs-xs) }
.factgrid dd { margin: 0; font-size: var(--fs-sm) }

.filterbar-actions { display: flex; gap: var(--sp-3); align-items: end; flex-wrap: wrap }

/* row-actions — the action cell of a table row. Adjacent link-buttons with only whitespace
   between them read as one label ("Rename Sessions"); a real gap and a wrap rule keep them
   distinct at any column width (owner's visual review, 2026-07-31). */
.row-actions { display: flex; gap: var(--sp-4); align-items: center; flex-wrap: wrap }
.form-actions { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap }

/* formrow — a one-line form: label+field pairs and their submit, on one wrapping row.
   Four forms (billing redeem, pixel domain claim, content test-email, the events search) were
   marked .formline, which is the design's MONOSPACE MICRO-TEXT class for showing a wire format,
   not a layout: they inherited mono/10px and no flex, so the controls stacked and the submit
   fell under the input (owner's visual review, 2026-07-31). Fields grow, the button does not. */
.formrow { display: flex; gap: var(--sp-3); align-items: flex-end; flex-wrap: wrap }
.formrow > .field, .formrow > .field-inline { flex: 1 1 220px; min-width: 0 }
.formrow > .btn { flex: 0 0 auto }
.input-color { width: 48px; height: 32px; padding: 2px }
.input-sm { padding: var(--sp-1) var(--sp-3); font-size: var(--fs-sm) }
.kpi-unavailable { color: var(--ink-quiet); font-size: var(--fs-sm) }
.meter-row { display: flex; gap: var(--sp-4); align-items: center }

/* a danger modifier ON a state panel — banner-danger is a banner, structurally different */
.panel-error { border-color: var(--danger-solid); color: var(--danger-ink) }

.panel-wrap { display: flex; justify-content: center; padding: var(--sp-6) }
.pixel-actions { display: flex; gap: var(--sp-3); flex-wrap: wrap; margin: var(--sp-4) 0 }
.pixel-card { display: flex; flex-direction: column; gap: var(--sp-3) }
.plancard-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-4) }
.previewframe { width: 100%; min-height: 320px; border: 1px solid var(--hairline); border-radius: var(--r-md); background: var(--surface-sunken) }
.range-form { display: flex; gap: var(--sp-3); align-items: end; flex-wrap: wrap }
.screen-head-actions { display: flex; gap: var(--sp-3); align-items: center }
.screen-head-main { display: flex; flex-direction: column; gap: var(--sp-1) }
.snippet-wrap { margin-top: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-3) }

/* ── AUTHOR rules rehomed from extractor-owned page sheets (module 4.3) ─────────────────────
   These were hand-APPENDED to page-*.css during Phase 1, before this file existed as the
   hand-owned home — so the first re-run of the extractor silently dropped all of them. The
   extractor's outputs must stay regenerable; hand-authored rules live here. */

/* module 1.13 — the expandable detail row under an event (visitors + visitor pages) */
.event-detail td { padding: var(--sp-4) var(--sp-5) }

/* module 1.20 — an asset tile that inserts instead of managing (content editor) */
.assetpick { display: block; padding: 0; border: 0; background: none; cursor: pointer; width: 100% }
.assetpick img { width: 100%; display: block; border-radius: var(--r-sm, 4px) }

/* module 1.14 — the is / is-not toggle paired with each value filter (visitors rail) */
.op-pair { display: inline-flex; gap: var(--sp-2); align-items: center }
.select-op { width: auto; min-width: 0 }

/* avatar — AUTHOR (module 6.7): the profile photo preview. Tokens only. */
.avatar { width: 40px; height: 40px; border-radius: var(--r-full); object-fit: cover; border: 1px solid var(--hairline) }

/* ── consolidation targets (also defined per-page today; copies fold in as pages are touched) ── */
/* The screen-head set, verbatim from the design. All four are shared now rather than "also
   defined per-page": Stage 0 moved the page title out of the shell head, so EVERY console page
   draws `screen-head > div > h1.screen-title + p.screen-sub`, and pages that carry controls put
   them in `headtools`. The extractor emits these into each screen's own sheet, which meant
   `.headtools` on /ai and `.screen-sub` on /pixel had no rule at all — caught by coverage-gate,
   which is the check that exists precisely because a class with no rule is invisible, not an
   error. */
.screen-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-5); flex-wrap: wrap }
.screen-title { margin: 0; font-size: var(--fs-xl); line-height: var(--lh-xl); letter-spacing: var(--tr-xl); font-weight: var(--fw-semibold); color: var(--ink) }
.screen-sub { margin: 4px 0 0; font-size: var(--fs-sm); line-height: 1.55; color: var(--ink-quiet); max-width: 76ch }
.headtools { display: flex; gap: var(--sp-2); align-items: flex-end; flex-wrap: wrap }

/* ══ 39.1 — one row, one control height ═══════════════════════════════════════════════════════
   The owner, 2026-08-24: "the Refresh, View Visitors and live now buttons are not as same as the
   date range input in size only."

   ⚠ MEASURED ON THE RUNNING SCREEN BEFORE TOUCHING ANYTHING, because adding up padding tokens is
   how this repo has been wrong before. The dashboard toolbar had THREE heights, not two: the
   website picker 30px, the date control 33px, and the three buttons 27px. They shared a bottom
   edge — `.headtools` is `align-items: flex-end` — which is why only the tops looked ragged.

   ⚠ A DECLARED NUMBER, BECAUSE ALL THREE WERE ACCIDENTS OF FONT METRICS. 30, 33 and 27 are what
   fell out of `padding + line-height` for three different fonts and line-heights (`.pp-button` and
   `.dr-button` inherit `line-height: normal`; `.btn-sm` sets `--lh-flat`). Nothing made them equal
   and nothing was going to. `--h-control` makes the equality a rule instead of a coincidence, and
   `dashboard-controls` asserts the five rects match rather than asserting this class is present —
   a class name is exactly what keeps passing when someone changes what it does.

   ⚠ AND THE SIZE MODIFIERS DO NOT COVER THIS CASE. The design has `btn-sm`, `btn` and `btn-lg` and
   nothing that is the height of a form control; the generated tokens carry no control height. That
   was grepped for before this was written — naming a class by reasoning about what the design
   "would" call it has cost this repo five wrong names in one session. */
:root { --h-control: 34px; }
.pp-button, .dr-button { min-height: var(--h-control); }
/* ⚠ 46.9's HOST SELECT IS A SIXTH CONTROL ON THIS BAR, and it arrived 2.8px taller than the five
   39.1 levelled. `.input, .select` is `padding: 9px 11px` on a 16.8px line box, which comes to
   36.8 — a real <select> sizes itself from its text, which is exactly the "accident of font
   metrics" --h-control exists to stop. The owner spotted the family of this (the caret sitting
   differently) before any check did; the equal-height check could not see it, because it names
   five controls and a sixth had been added to the toolbar.
   Scoped to `.headtools` so a select in a form elsewhere keeps the standard control size. */
.headtools .select {
  height: var(--h-control); min-height: var(--h-control);
  padding-top: 0; padding-bottom: 0;
}
/* Buttons only — `.headtools .field` is a label stacked over a control and is taller by design. */
.headtools .btn { min-height: var(--h-control); }

/* ⚠ 39.4 — AND EQUAL WIDTHS, BECAUSE "BIGGER" WAS NOT ABOUT THE HEIGHT. After 39.1 made all five
   controls 34px the owner still read the CTA as bigger than its neighbours, and measuring said why:
   identical boxes — same padding, same border, no shadow — but 87.2px wide against 61.5 and 66.8,
   because "View visitors" is a longer word than "Refresh". A filled button that is also half again
   as wide as the outline buttons beside it reads as a different size, and it was.

   ⚠ EQUAL COLUMNS, NOT A `min-width` IN PIXELS. A hard number is a guess that stops being true the
   moment a label is reworded or translated — and these labels have been reworded once already. An
   auto-width grid whose columns are all `1fr` sizes every column to the WIDEST content, so the set
   stays matched whatever the words become. */
.headtools .row-actions {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; align-items: center;
}
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--sp-4) }
/* Module 4.2 (the first LOOK at a screenshot): pages put fields straight inside .card with no
   .card-body, so everything ran edge-to-edge against the card border. The design's card
   convention pads bodies with --pad-card; give the field-shaped DIRECT children the same
   inset (tables and card-head/-foot keep their designed full-bleed). */
.card > .form-grid, .card > form.form-grid { padding: var(--pad-card) }
.card > .field, .card > .switch-row, .card > .form-actions, .card > .meter-row,
.card > .field-hint, .card > .banner, .card > p, .card > .btn, .card > .chips,
.card > .card-meta, .card > .settings-nums, .card > dl, .card > ul, .card > ol,
.card > [data-state] > .field, .card > [data-state] > .switch-row,
.card > [data-state] > .form-actions, .card > [data-state] > .banner,
.card > [data-state] > .field-hint, .card > [data-state] > p,
.card > [data-state] > .form-grid, .card > [data-state] > .settings-nums,
/* 2026-07-31, owner's visual review: 4.2 listed the FIELD-shaped children and missed the
   CONTAINER-shaped ones, so eight screens still had content flush against the border —
   measured at 1px from the card edge. Same convention (margin, not padding, so a child with
   its own border keeps its own inner spacing), same value. */
.card > .kpis, .card > .stack, .card > .condlist, .card > .plangrid, .card > .formline,
.card > .panel-actions, .card > .danger-row, .card > .pixel-actions, .card > .factgrid,
.card > .fieldset, .card > fieldset, .card > .previewframe, .card > .assetgrid,
.card > .formrow, .card > .settings-grid, .card > [data-mode-panel],
/* [data-list-bar] — admin's per-tab creation mount, found in the Stage-0 screenshots: its
   "New account" button sat exactly on the card's left border. The gate walked past it because
   an empty wrapper is recursed into and the BUTTON inside has its own padding, which the check
   counted as an inset. A control's padding is inside the control; it is not a gap from the
   card. The gate is tightened for that below. */
.card > [data-list-bar],
/* descendant, not child: the custom-domain card's three state panels sit one level deeper,
   inside [data-state="ready"], and data-dom exists nowhere else. */
.card [data-dom],
.card > [data-state] > .stack, .card > [data-state] > .condlist,
.card > [data-state] > .plangrid, .card > [data-state] > .formline,
.card > [data-state] > .panel-actions, .card > [data-state] > .danger-row,
.card > [data-state] > .pixel-actions, .card > [data-state] > .factgrid,
.card > [data-state] > .fieldset, .card > [data-state] > fieldset,
.card > [data-state] > .assetgrid, .card > [data-state] > .formrow,
.card > [data-state] > .meter-row, .card > [data-state] > .table-wrap-scroll ~ .form-actions {
  margin-inline: var(--pad-card);
}
.card > .field, .card > .switch-row, .card > .form-actions, .card > .banner, .card > .chips,
.card > .settings-nums,
/* 2026-07-31: .meter-row was insetting horizontally but not vertically, so a card whose only
   content is a meter (the team seat card) collapsed to a 34px strip with the bar hugging both
   edges — owner's visual review. */
.card > .meter-row, .card > [data-state] > .meter-row,
.card > [data-state] > .field, .card > [data-state] > .switch-row,
.card > [data-state] > .form-actions, .card > [data-state] > .banner {
  margin-block: var(--sp-4) 0;
}
.card > :last-child:not(.card-foot):not(.card-head):not(.table-wrap-scroll):not(.form-grid):not([data-state]),
/* :not(.kpi) — the KPI strip is BOTH the state wrapper and a grid (.kpis[data-state]), so this
   rule was matching its last TILE and giving it margin-bottom:16px. A margin on a grid item
   eats into the stretched row height, so the fifth tile rendered 76px against its siblings' 92
   and the strip had a ragged bottom edge. Measured: row 91.5px − sp-5 16px = 75.5 → 76.
   (Reported as "intermittent" in the first review pass; it was not. The second measurement ran
   on a pixel with no data, where the strip is hidden and the empty panel shows instead —
   nothing to measure, not nothing wrong.) */
/* ⚠ :not(.card-foot) — AND THE RULE DIRECTLY ABOVE ALREADY KNEW THIS (46.5). Its
   `.card > :last-child` twin excludes `.card-foot` explicitly; this `[data-state]` variant did
   not, so every card whose foot sits inside a state wrapper — which on the dashboard is all of
   them — carried 16px of margin BELOW a strip that is meant to sit flush against the card's
   bottom edge and owns the bottom border-radius. Measured: 17px of dead space under the note,
   card border included. Production bug report #7: "there is white padding at the end of each card
   that should not be there because the last portion is the notes". A card foot brings its own
   `--pad-row` and never wants an outer margin; the two rules now say the same thing. */
.card > [data-state] > :last-child:not(.table-wrap-scroll):not(.kpi):not(.card-foot) {
  margin-bottom: var(--pad-card);
}

/* 217.61 (Changes 03 C61) — a notice inside a card is followed by a gap. The rules above inset a banner from the card's
   sides and give it room ABOVE, but nothing set the room BELOW, so Billing → Plans' lifetime notice sat directly on the
   plan cards (and any banner directly above a grid or table did the same). A card is block flow, so this collapses
   with a following sibling's own top margin rather than adding to it. `card-padding` measures it. */
.card > .banner:not([hidden]), .card > [data-state] > .banner:not([hidden]) { margin-bottom: var(--sp-4); }

/* A character counter that has hit its ceiling. Not in the design, which draws the counter in
   one state only - added because `maxlength` stops the typing SILENTLY: without a signal the
   reader's next keystroke simply does nothing and the field looks broken rather than full.
   Deliberately fires only AT the limit; a counter that changes colour at 80% is one people
   learn to ignore by the time it matters. */
.field-count.is-warning {
  color: var(--warning-ink);
  font-weight: var(--fw-semibold);
}
/* 217.28 — a list past its ceiling, which the server refuses on save. */
.field-row > .field-count.is-danger { color: var(--danger-ink); }

/* ── the image library, promoted to the shared layer (module 10.6d) ───────────────────
   The design's library is ONE surface "opened from any image field" and shared by everything on
   the account — creatives on every pixel, your photo, the company logo and both brand logos. It
   lived on content.html alone, so `assetgrid`/`assettile` were extracted only into
   page-content.css and no other page could draw it.
   These are copies of the extractor's rules, not edits to them: extractor output is never
   hand-edited, and page-content.css keeps its own identical copy (it loads after this sheet, so
   the two agree by construction rather than by anyone remembering). */
.assetgrid { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.assettile {
  position: relative; display: flex; flex-direction: column; gap: var(--sp-2);
  width: 132px; padding: var(--sp-2); border: 1px solid var(--hairline);
  border-radius: var(--r-md); background: var(--surface); cursor: pointer;
}
.assettile input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.assettile.is-selected { border-color: var(--accent-hairline); background: var(--accent-wash); }
.assettile:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
.assettile-img {
  width: 100%; height: 72px; object-fit: contain;
  border-radius: var(--r-sm); background: var(--surface-sunken);
}
.assettile-img.is-broken { display: grid; place-items: center; color: var(--ink-quiet); font-size: var(--fs-lg); }
.assettile-meta { display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-xs); color: var(--ink-body); min-width: 0; }

/* ── the six-card settings shell (module 10.15) ─────────────────────────────────
   `.span-all` was extracted only into page-pixels.css — the LIST page — because the design's
   frame that uses it lives in that file. The detail screen needs it for two of its six cards
   (Sending email is ~3x the height of the others and would strand a column; Danger zone is last
   so a destructive control is never adjacent to a save), so it belongs in the shared layer. */
.span-all { grid-column: 1 / -1; }

/* The note under the header that says which cards the one save covers. */
.screen-note {
  margin: 0 0 var(--sp-5);
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--ink-quiet);
}

/* An input that takes the room, with a button that takes what it needs (module 10.15).
   `.row-actions` is the wrong tool: it right-aligns a row of BUTTONS, and a full-width input
   leaves it nothing to align, so the button wrapped underneath. Used by the public-key + Copy
   pair, which the design draws side by side. */
.inputrow { display: flex; gap: var(--sp-2); align-items: center; }
.inputrow > .input { flex: 1 1 auto; min-width: 0; }
.inputrow > .btn { flex: 0 0 auto; }

/* ── two classes the extractor put in twelve page sheets but not the thirteenth ───────────
   `stack-sm` is in 12 page sheets and `row-actions-start` in 8 — they are shared components in
   everything but location, and `page-pixel.css` happens not to have them because the design's
   pixel frames never used them. Promoted rather than copied a thirteenth time. The page sheets
   keep their identical copies; they load after this one, so the two agree by construction. */
.stack-sm { display: flex; flex-direction: column; gap: var(--sp-2); }
.row-actions-start { justify-content: flex-start; }

/* ── a card that is a GRID ITEM carries no section margin (module 10.15) ───────────────
   components.css has `section { margin-top: 44px }`, which is right for sections stacked down a
   page and wrong for one that is a grid item: the grid's own `gap` already owns that spacing, so
   the two ADD. On the pixel screen that put 116px between the note and the first card, and 44px
   of dead space inside every row of the grid. Extractor output is never hand-edited, so the
   correction is scoped here to the containers that lay cards out as a grid. */
.settings-grid > section,
.plangrid > section,
.assetgrid > section { margin-top: 0; }

/* ── dark-theme .btn-danger: a WCAG correction to the design's own palette ──────────────────
   The design ships --danger-solid:#E04B57 in dark and .btn-danger{color:#fff}. That pair is
   3.95:1 — under AA's 4.5:1 for text below 18.66px, and this is the label on a DESTRUCTIVE
   button. Worse, the dark palette inverts the two reds: --danger-ink (#F58B95) is LIGHTER than
   --danger-solid there, so .btn-danger:hover{background:var(--danger-ink)} *lightens* on hover
   and lands at 2.33:1 — the button gets harder to read exactly as you reach for it. In light
   theme the same tokens are the right way round (ink #B02631 is darker than solid #C6303B), so
   this is a dark-only defect and the fix is dark-only too.

   Scoped to .btn-danger rather than to the token: --danger-solid also paints .field-req, meter
   fills and .input.is-error borders, where the lighter red is genuinely more legible on a dark
   surface. Redefining the token would fix one pair and degrade three.

   #D03A46 (4.80:1) sits between the design's two reds — it keeps the dark theme's brighter red
   rather than falling back to the light value, and clears AA with headroom. Hover #B82D38
   (6.05:1) restores the convention the light theme already follows: hover goes darker. */


/* ── .row-msg — promoted to the shared layer (module 10.23) ─────────────────────────────────
   The design declares it once, in the pixels-list frame, and the extractor therefore emitted it
   into page-pixels.css only. Module 10.19 used it for the Off row's failure message (list) and
   10.23 for the duplicate-trait-key warning (pixel detail) — two pages, two different page
   sheets, so on /pixel it resolved to nothing and the warning rendered as unstyled text.
   Values are the design's own, verbatim. Page sheets load after this one, so the list page keeps
   using its identical copy and nothing changes there. */
.row-msg {
  display: block; font-size: var(--fs-xs); line-height: 1.45;
  color: var(--danger-ink); padding-top: var(--sp-1);
}

/* ── .card-foot ───────────────────────────────────────────────────────────────────
   NOTHING IS DEFINED HERE, DELIBERATELY — and the note stays because I got this wrong once.
   The shared `components.css` copy of .card-foot stops at line-height and omits
   display:flex / align-items / justify-content:space-between / gap / flex-wrap. That looks like
   an extractor gap, and I added the five declarations here on that reading.
   It is not a gap. EVERY page-*.css carries the complete rule, and page sheets load AFTER this
   one, so every console screen already had the flex half. Proved by removing this block and
   re-running card-padding-gate: still 7/7, "flex/space-between, 960px apart".
   A hand rule justified by a premise that turns out to be false is worse than no rule — it reads
   as a fix and silently duplicates the sheet it claims to repair. If a screen ever appears with no
   page sheet, that screen needs the rule, not this file. */

/* ── a <fieldset> used as a .field ────────────────────────────────────────────────────────────
   The design groups radios in `<fieldset class="field" style="border:0;margin:0;padding:0">` —
   the reset is inline in every frame that does it, so there is no class to copy. `.fieldset`
   exists but is emitted into page-ai.css and page-automation.css only, so on any other screen it
   resolves to nothing; using it here would have been a real class in the wrong sheet.
   This is the design's own inline reset, once, in the shared hand layer. */
fieldset.field {
  border: 0; margin: 0; padding: 0; min-inline-size: 0;
}

/* ── the radio row (213.1) ─────────────────────────────────────────────────────────────────────
   ⚠ PROMOTED, NOT INVENTED — the `.steprow` treatment below. The design's `.radio` is emitted into
   page-ai.css, page-automation.css and page-admin.css only, so on Channels (which has no page sheet)
   it resolved to nothing and its two radios would have drawn as the browser's own. Values verbatim. */
.radio {
  display:inline-flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-sm);color:var(--ink-body);cursor:pointer
}
.radio input {
  appearance:none;width:15px;height:15px;margin:0;border:1px solid var(--hairline-strong);border-radius:50%;background:var(--surface);cursor:pointer;display:grid;place-items:center;flex-shrink:0
}
.radio input:checked { border-color:var(--accent-fill) }
.radio input:checked::after { content:"";width:7px;height:7px;border-radius:50%;background:var(--accent-fill) }

/* ── a loading skeleton inside a .panel ───────────────────────────────────────────────────────
   ⚠ THIS IS THE `visitors(+80px)` INTERMITTENT, and it took four wrong guesses to find.
   `.panel` is the design's centred empty/loading box: `display:flex; flex-direction:column;
   align-items:center`. With `align-items:center` a flex item is sized to its CONTENT, not
   stretched to the container — so the skeleton table-wrap that app.js (module 10.S) puts inside
   the loading panel took the table's full 1056px (the sum of the visitors colgroup) instead of the
   ~847px the ready state gets, and the whole content column scrolled sideways.
   It only ever showed while a region was LOADING, which is why it appeared in a busy full run and
   never standalone, and why `breakpoints` passed 8/8 every time I chased it.
   Measured: content overflow 80 → 0 with the grid held in its loading state at 1024px. */
.panel > .table-wrap-scroll {
  align-self: stretch; min-width: 0; width: 100%;
}

/* ── the journey step row (design B4) ─────────────────────────────────────────────────────────
   ⚠ PROMOTED, NOT INVENTED. B4 is drawn inside the AUTOMATIONS design document, so the extractor
   emitted these into page-automation.css and page-automations.css only — and the journey editor is
   its own route (/journey → page-journey.css), where none of them resolve. The editor had been
   built from `.condrow` instead, which is the CONDITION row of the automation editor: a real class
   in the wrong component, the species that no gate can see and that this project keeps shipping.
   Values are the design's own, verbatim. Same treatment as `.row-msg`. */
.steprow {
  display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap;
}
.steprow-ord {
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--ink-quiet);
  width: 14px; flex-shrink: 0;
}
.steprow-move { display: flex; flex-direction: column; gap: 1px; flex-shrink: 0 }
/* ⚠ ONE ROW PER STEP, which is the whole point of the component. `.input, .select, .textarea`
   are `width: 100%`, so inside a `flex-wrap: wrap` row every one of them claims a full line and
   the step renders as five stacked controls - the owner's screenshot. The design gives the
   CONDITION row exactly this treatment (`.condrow .select { flex: 1 1 210px; min-width: 150px }`,
   `.condrow .key { flex: 0 0 116px }`) and simply has no equivalent for `.steprow`; these are its
   own numbers, applied to the row that needs them. min-width:0 on the row is what lets the select
   shrink instead of pushing the siblings out. */
.steprow { min-width: 0 }
.steprow .select { flex: 1 1 210px; min-width: 150px }
.steprow .key { flex: 0 0 116px; width: 116px }
.btn-nudge {
  appearance: none; background: var(--surface); border: 1px solid var(--hairline);
  color: var(--ink-body); border-radius: var(--r-xs); width: 20px; height: 15px;
  display: grid; place-items: center; cursor: pointer; padding: 0;
}
.btn-nudge:hover:not(:disabled) { border-color: var(--accent-hairline) }
.btn-nudge:disabled { color: var(--disabled-ink); cursor: not-allowed; background: var(--disabled-fill) }

/* ── the event-detail screen (design C5) ──────────────────────────────────────────────────────
   ⚠ PROMOTED, because /event is a page the extractor never saw. C5 is drawn inside the AUTOMATIONS
   design document, so its classes were emitted into page-automations.css — and build.mjs links
   `page-<filename>.css`, so a new event.html would load nothing. Rather than hand-author a file
   named like extractor output (which is what page-*.css is), the handful of components C5 needs
   live here, in the layer that is hand-written by design and loaded by every page.
   Values are the design's own, verbatim. Same treatment as .steprow and .row-msg. */
.kpi { display: flex; gap: var(--sp-5); flex-wrap: wrap }
.kpi-item { display: flex; flex-direction: column; gap: 2px }
.cell-2line { display: flex; flex-direction: column; gap: 2px; min-width: 0 }
.cell-badges { display: flex; gap: var(--sp-1); flex-wrap: wrap; align-items: center }
/* One series, no axis: a shape, not a chart. The readable numbers are the KPIs above it. */
.spark { display: block; width: 100%; height: 44px }
.spark-area { fill: var(--chart-1); opacity: var(--chart-area-opacity) }
.spark-line { fill: none; stroke: var(--chart-1); stroke-width: 1.6 }

/* ── shared table column widths (design: the automations document's token block) ───────────────
   ⚠ PROMOTED for the same reason as .kpi and .spark above: /event has no extracted page sheet, so
   the column classes C5's colgroup uses resolve to nothing there. Every value is a token that is
   already global — only the four rules that read them were page-scoped.
   Caught by verify's own "undefined CSS classes" measurement, which is exactly what it is for:
   the first version of event.html borrowed col-visitor / col-loc / col-device from the VISITORS
   sheet, and they would have silently done nothing. */
.c-flex { width: auto; min-width: 132px }
.c-mini { width: var(--col-mini) }
.c-flag { width: var(--col-flag) }
.c-when { width: var(--col-when) }

/* ⚠ THE PREVIEW FRAME FILLS ITS STAGE. `.previewframe` carries its own 320px height from when
   it was a direct child of a card; inside `.stage-body` (220px, or 380px for email) that makes it
   50px TALLER THAN ITS BOX, and because .stage-body is `align-items:center` it overflows equally
   top and bottom — printing the creative straight over the caption above it. Measured: stage
   220px, frame 320px, frame top 50px above the stage top.
   The stage's own background is the staging (scrim / grey), so the frame must not paint over it:
   `colorScheme:normal` keeps a sandboxed document from forcing an opaque canvas. */
/* ⚠ min-height: 0 IS THE LOAD-BEARING DECLARATION. `.previewframe` above sets
   `min-height: 320px`, and min-height BEATS max-height and height in the cascade - so
   `height:100%` and `max-height:100%` were both simply ignored and the frame stayed 320px
   in a 220px box. Measured three times before I read the rule I was fighting rather than
   the rule I had written. */
.stage-body .previewframe { width: 100%; height: 100%; min-height: 0; max-height: 100%;
  border: 0; border-radius: 0; display: block; background: transparent; color-scheme: normal; }

/* ⚠ THE IMAGE TILE'S PICTURE (design H1). `.imgtile-box` is a 72px `place-items:center` grid
   and the frame renders a placeholder glyph in it; ours renders the real upload, which needs its
   own sizing. An EXPLICIT height, not 100%: a centred grid item's percentage height has no
   definite area to resolve against - the same rule that silently dropped `height:100%` on the
   template tile's frame and left it 150px inside a 70px box.
   `.imgtile` is a <button> here because it is selectable; the frame's <div> needs no reset. */
.imgtile { appearance: none; padding: 0; text-align: left; cursor: pointer; font: inherit;
  color: inherit; }
.imgtile-box img { max-width: 100%; height: 64px; object-fit: contain; display: block; }
.imgtile[aria-selected="true"] { border-color: var(--accent-hairline);
  box-shadow: 0 0 0 1px var(--accent-hairline); }

/* ⚠ THE ENLARGED STAGE KEEPS THE STAGING AND DROPS THE HEIGHT. The channel classes are copied
   from the inline stage so the scrim / grey / alignment are identical - but `.stage-body` carries
   a fixed 220px (380px for email), so a copied class made the "enlarged" view exactly as tall as
   the small one. Measured: 550x188 next to 354x188, wider and no taller, which is not enlarging.
   Keyed to the data attribute, not a new class. */
[data-big-stage].stage-body { height: min(62vh, 620px); }

/* ⚠ `.codeblock` is defined in page-content.css and page-content-edit.css only, so /admin —
   which renders job payloads and sent broadcast bodies as source — loaded no rule for it at all.
   I grepped `dist/assets/*.css`, found two hits, and concluded it existed; the coverage gate is
   what noticed that neither hit was a stylesheet this page loads. Same rule, defined once here
   where every page can see it. */
.codeblock {
  font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.55;
  background: var(--neutral-bg); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: var(--r-md);
  padding: var(--sp-3); margin: 0;
  white-space: pre-wrap; word-break: break-word;
  max-height: 40vh; overflow: auto;
}

/* L6's logo slot shows the real image once one is set. The design only ever draws the empty
   placeholder, so it has no rule for the filled state — and a 34px grid cell with an unsized
   <img> in it renders the logo at its natural size and blows the slot open. Element selector
   rather than a new class: there is no design name to match, and inventing one is how
   `fieldset-hint` and `planrow` happened. */
.logo-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: var(--r-sm);
}

/* ⚠ THE SECTION RHYTHM MUST NOT BE PAID TWICE (owner's spacing review, 2026-08-07).
   `section { margin-top: 44px }` in the extracted sheet is what separates stacked cards across the
   console. On /billing the state container is itself a <section>, so once its children became
   sections too — which is what gives them their gaps — the first child added a second 44px on top
   of the wrapper's, and the screen opened with a 100px void under its title where every other
   screen has 56px.
   Scoped to the first child of a state container, because that is exactly where the two rhythms
   meet. The rule is general on purpose: any screen that nests sections inside a state region gets
   the same correction rather than discovering it separately. */
[data-state] > section:first-child,
[data-state] > .card:first-child {
  margin-top: 0;
}

/* ⚠ AND THE 12px HALF OF THE RHYTHM HAS TO REACH THEM TOO. Section spacing on this console is
   composed of two parts: `section { margin-top: 44px }` from the extracted sheet, and
   `.content-inner`'s own `gap: var(--sp-4)` — 44 + 12 = the 56px every screen shows between cards.
   Billing is the one screen whose page sections are nested a level deeper, inside a state
   container, so they were only ever getting the 44 and sat 12px tighter than the rest of the
   product. Same two properties as `.content-inner`, so the two compose identically instead of the
   total being restated as a number. */
[data-state-region="billing"] > [data-state="ready"] {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* ⚠ ...EXCEPT INSIDE A LABEL, WHERE IT MEANS THE OPPOSITE (owner's review, 2026-08-07).
   The rule above is right for a table cell, where `.cell-sub` is the SECOND LINE. The design also
   uses the same class inside a `<label>` for a quiet inline qualifier — L6's SMTP fields name the
   JSON key each one maps to ("Host  host"), which matters there because the seven controls are one
   whole-blob field and the key is what an operator needs when the blob is what actually travels.
   Rendered as a block, every one of those labels grew a second line and the form read as though
   each field were labelled twice.
   The design's own sheet computes `inline` here; this restores that without touching the table
   case the block rule exists for. */
.field-label .cell-sub,
.readonly-label .cell-sub {
  display: inline;
}

/* ══ the bump composer's preview (27.9) ═════════════════════════════════════════════════════
   The shared .previewframe is 320px tall, which is right for the editor's sticky column beside
   three cards of fields. Inside a modal it pushes the Send button off the bottom of a laptop
   screen, so the composer gets a shorter frame — the same document, less of it at once.
   In app-components.css, NOT a page sheet: the composer is built in app.js and opens on
   /visitors, /visitor and anywhere else that calls Gump.bumpComposer, so a page-scoped rule
   would style it on one screen and not the next. */
.bump-previewframe { min-height: 0; height: 190px; }
/* 223.5 (Changes 08 C5) — an onsite message is shown on a desktop-sized page (1280×800) scaled down to the column's width,
   like the Messages gallery's picture, so it is never drawn at full size in a small frame and cut off. `--stage-s` is the
   column's width over 1280, set by app.js when the preview is drawn. */
.bump-stage[data-staged] { position: relative; overflow: hidden; aspect-ratio: 1280 / 800;
  border: 1px solid var(--hairline); border-radius: var(--r-md); background: var(--surface-sunken); }
.bump-stage[data-staged] > .bump-previewframe { position: absolute; left: 0; top: 0; width: 1280px; height: 800px;
  border: 0; border-radius: 0; transform: scale(var(--stage-s, .34)); transform-origin: 0 0; }

/* ══ the website picker with icons (32.2) ═══════════════════════════════════════════════════
   Drawn beside the real <select>, which stays in the form and stays the source of truth. These
   rules describe a control the browser no longer draws for us, so they have to cover the states
   a native select had for free: hover, focus and the open list. */
.pp-wrap { position: relative; display: inline-block; min-width: 200px; }

.pp-button {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: 6px 10px; border: 1px solid var(--hairline); border-radius: var(--r-md);
  background: var(--surface); color: var(--ink-body); font-family: var(--font-sans);
  font-size: var(--fs-sm); cursor: pointer; text-align: left;
}

.pp-button:hover { border-color: var(--accent-border); }
.pp-label { display: flex; align-items: center; gap: var(--sp-2); flex: 1; min-width: 0; }
.pp-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ⚠ `line-height: 1` IS WHAT KEEPS THE GLYPH OUT OF THE CONTROL'S HEIGHT (46.3). Enlarging this
   caret from 10px to 13px grew its line box to 17px - taller than the 15px label beside it - so it
   became the tallest child and pushed `.dr-button` to 35px against every other control's 34px.
   `dashboard-controls` went red on it, which is 39.1 doing exactly what it was written for: those
   five heights were "accidents of font metrics" and --h-control made the equality a rule. A
   decorative glyph must not be allowed to set a control's height. `.vd-exp-caret` already carried
   this line; the three that did not are the three that broke. */
/* ── 46.3 (corrected) · ONE dropdown glyph ─────────────────────────────────────────────────────
   The owner: "i have asked you to change all input elements gliphs not just Host."
   They were right, and the first pass is why. A native <select> draws its caret as two gradient
   half-squares in the control's own ink; these three drew a text "▾" in `--ink-quiet` at a font
   size. So the Host select painted a 14x7 near-black triangle while the Pixel and Date range
   controls beside it painted an 11x13 grey arrow — and 46.3's gate passed both, because it
   asserted each family cleared a FLOOR. A floor cannot see whether two controls match.
   ⚠ SAME TECHNIQUE, NOT MERELY THE SAME SIZE. These carry the select's own two-gradient triangle
   rather than a font glyph, so shape, size and ink come from one description and there is nothing
   left to drift. `font-size: 0` collapses the "▾" the markup still carries — it is `aria-hidden`
   decoration, and the alternative was editing three JS call sites plus the visitors expander,
   which swaps ▸/▾ and is a DISCLOSURE control rather than a dropdown.
   ⚠ AND AN EXPLICIT TOKEN, NOT `currentColor`. The three containers resolve three different inks
   (`.pp-button` --ink-body, `.dr-button` --ink, `.select` --ink), which is how they came to differ
   in the first place. --ink-body is the middle of the three: darker than the grey the owner called
   a dot, lighter than the near-black triangle they asked to bring down. */
.pp-caret, .dr-caret {
  flex: 0 0 auto; width: 12px; height: 6px; font-size: 0; line-height: 0;
  background-image: var(--caret-image);
  background-position: center;
  background-size: 12px 6px;
  background-repeat: no-repeat;
}

/* 16px is the size a favicon is authored at; scaling one up only shows its pixels. */
.pp-icon { width: 16px; height: 16px; flex: 0 0 16px; border-radius: 3px; object-fit: contain; }

.pp-initial {
  display: grid; place-items: center; background: var(--neutral-bg); color: var(--ink-quiet);
  font-size: 10px; font-weight: var(--fw-semibold);
}

/* ⚠ THE LIST IS ABSOLUTE AND ELEVATED. It sits inside a header that other content follows, so
   without a stacking context above its siblings it would open BEHIND the KPI cards. */
/* ⚠ 215.7 — AS WIDE AS ITS LONGEST NAME, NEVER NARROWER THAN THE BUTTON. It was pinned to the button's
   width (`left: 0; right: 0`), so a short current name cut every longer one to "reading…" (owner's
   change brief, 07). Capped so a very long name still ellipsises inside the window. */
.pp-list {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 40;
  min-width: 100%; width: max-content; max-width: min(420px, calc(100vw - 32px));
  max-height: 280px; overflow-y: auto;
  border: 1px solid var(--hairline); border-radius: var(--r-md);
  background: var(--surface); box-shadow: 0 8px 24px rgba(0,0,0,.12);
}

.pp-option {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 8px 10px; cursor: pointer; font-size: var(--fs-sm);
}

.pp-option:hover, .pp-option:focus { background: var(--surface-sunken); outline: none; }
.pp-option[aria-selected="true"] { font-weight: var(--fw-semibold); }

/* The real <select> behind the icon picker: visually gone, still in the form, still focusable and
   still what a screen reader and an autofill both see. See app.js for why this is not `vis-hidden`. */
select.pp-native {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  padding: 0; border: 0;
}

/* ── 34.1 · the expandable KPI chart ───────────────────────────────────────────────────────────
   ⚠ NEW CLASS NAMES, AND THAT IS DELIBERATE RATHER THAN LAZY. The rule on this project is to grep
   the extracted design sheets before naming anything, because four times in one session I invented
   a name the design already had. Done here: the only chart vocabulary the design carries is
   `.spark` / `.spark-area` / `.spark-line`, and that is a 44px-tall decorative sparkline with no
   axes, no cursor and no readout. The design deleted its large chart blocks, so there is no name
   to borrow for this one — these are `chart-*` to sit obviously beside `spark-*` without
   pretending to be it.
   Every colour is a design token, so the chart themes with everything else. */
.chart { display: block; width: 100%; touch-action: pan-y; }
/* No :focus-visible rule here on purpose - components.css already gives every focusable
   element the same 2px outline, and a second copy is a thing that drifts. */
.chart-area { fill: var(--chart-1); opacity: var(--chart-area-opacity); }
/* 2px, and it draws as 2px: one SVG unit is one CSS pixel here (see app.js). */
.chart-line { fill: none; stroke: var(--chart-1); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart-grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart-cross { stroke: var(--chart-1); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0; }
.chart-dot { fill: var(--chart-1); stroke: var(--surface); stroke-width: 2; opacity: 0; }
.chart-tick { fill: var(--ink-quiet); font-size: var(--fs-xs); font-family: var(--font-sans); }

/* The readout is TEXT, not a floating bubble: it is the same sentence for a pointer and for the
   arrow keys, it cannot be clipped by the card, and it does not vanish when you look away. */
.chart-readout {
  margin: var(--sp-2) 0 0; min-height: 1.2em;
  font-size: var(--fs-sm); color: var(--ink-body); font-variant-numeric: tabular-nums;
}

/* The chart the strip selects into, directly under it.
   ⚠ 203.7 — NO SURFACE, NO BORDER, NO SIDE PADDING. HANDOFF §3 phase 2 step 3: the chart is
   "aligned flush with the content column on both sides", and TESTS 2.4 measures its edges against
   the content column to ±1px. A bordered, `--sp-4`-padded card put the plot 17px inside the column
   on each side. Vertical padding stays at zero for the same reason the horizontal did — the
   caption row is the chart's own head and wants the strip's rhythm, not a card's inset. */
.kpi-open {
  margin-top: var(--sp-4);
  padding: 0;
  background: none; border: 0; border-radius: 0;
}
.kpi-open-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  margin-bottom: var(--sp-2);
}
.kpi-open-title { font-size: var(--fs-md); font-weight: var(--fw-medium); color: var(--ink); }
.kpi-open-sub { font-size: var(--fs-xs); color: var(--ink-quiet); }

/* A KPI tile is a button now (point 8: "on clicking any of these graphs"). It has to keep looking
   exactly like the card it was, so the button's own chrome is reset rather than restyled. */
button.kpi {
  appearance: none; background: var(--surface); font: inherit; text-align: left;
  cursor: pointer; width: 100%;
  /* the chevron below is positioned against this */
  position: relative;
}
button.kpi:hover { border-color: var(--hairline-strong); }

/* ── 203.7 · THE SELECTED ONE CARRIES THE RULE ────────────────────────────────────────────────
   SPEC-SCREENS §1: "Clicking a figure redraws the chart; the selected one carries the rule."
   ⚠ A 2px BOTTOM RULE, AND THE SAME 2px IS ON EVERY TILE IN THE HAIRLINE COLOUR. Giving only the
   selected tile a border-bottom would make it 2px taller than its five neighbours and shunt the
   whole strip down when you click along it — the row would visibly twitch on every selection.
   Reserved on all six, coloured on one.
   ⚠ AND `aria-selected`, NOT `aria-expanded`. The old rule keyed off the disclosure state these
   tiles no longer have; left alone it would have matched nothing and the selected tile would have
   looked exactly like the other five, which is the failure mode where the feature "works" and the
   screen cannot say which series it is drawing. */
button.kpi { border-bottom: 2px solid var(--hairline); }

/* ── 203.8 · ONE LINE, AND ONE LINE MEANS CLIPPED RATHER THAN WRAPPED ─────────────────────────
   HANDOFF §3 phase 2 step 1: "Each has a one-line subtext — the original wrapped to two lines and
   wasted vertical space." Measured before changing anything: all six wrapped to exactly two lines
   at 1280px, which is 16px of dead space under every tile in the strip.
   ⚠ A SHORTER SENTENCE WOULD NOT HAVE FIXED IT. The strip is `repeat(auto-fit, minmax(178px,1fr))`
   — at 1280 that is a ~180px tile, and "↑ 12.4% from 288" fits while "— nothing in 11 Jul – 09
   Aug" does not, so any copy short enough to guarantee one line would have to drop the dates.
   Clipping is the prototype's own answer (its hint span is nowrap + hidden + ellipsis).
   ⚠ AND EVERY SUB-LINE CARRIES A `title` (set in dashboard.html), because an ellipsis with nothing
   behind it deletes the fact rather than abbreviating it. */
/* ⚠⚠ THE ELLIPSIS BELOW DOES NOTHING WITHOUT THIS LINE, and it did nothing for a whole module.
   `.kpi` is a column flex container inside a `minmax(0, 1fr)` track. Its children stretch to the
   button's width, but a child whose min-content width is larger — which is exactly what
   `white-space: nowrap` makes a long subtext — overflows the button instead of being constrained
   by it. Measured: a 95-character subtext left the tile 224px wide and the subtext 451px, spilling
   over its neighbours. `overflow: hidden` clipped nothing because there was no overflow relative
   to the element's own box, and `text-overflow: ellipsis` needs that overflow to have anything to
   replace. So SPEC-SCREENS §1's "one-line subtext" was enforced in appearance only, against the
   short copy the strip happens to ship with; the first long one would have broken the row.
   ⚠ `min-width: 0` ALONE IS NOT ENOUGH — measured too: it takes the subtext from 451px to 383px
   and still leaves it wider than its tile. The cap has to be `max-width: 100%`. */
.kpis .kpi > * { min-width: 0; max-width: 100%; }
.kpis .kpi-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ⚠ AND THE RESERVED SECOND LINE COMES BACK OFF, WHICH IS THE HALF THAT ACTUALLY SAVES THE SPACE.
   `page-dashboard.css` carries `.kpi > .kpi-sub { min-height: calc(2 * 1.45em) }` from 39.2 — the
   owner's own request, 2026-08-24: "the small chart in KPI boxes is going down when the KPI
   description coming in two line so reserve the gap of two lines so in small screen the graphs
   will be aligned." With the line above, a description CANNOT reach two lines any more, so that
   reserve is exactly the wasted vertical space HANDOFF §3 phase 2 step 1 is asking to remove.
   ⚠ THE OWNER'S INVARIANT SURVIVES, AND IT IS NOT THIS DECLARATION THAT HOLDS IT. 39.2's own note
   says so: "the second is the one that holds at every width" — `.kpi > .kpi-spark { margin-top:
   auto }` pushes every sparkline to the bottom of an equal-height grid row, which aligns them for
   ANY number of lines. `dashboard-kpi-align` measures that offset at four widths and still does.
   ⚠ OVERRIDDEN HERE, NOT EDITED THERE. `page-dashboard.css` is generated (ground rule 2), and it
   loads AFTER this file — so the override needs to win on specificity rather than order:
   `.kpis .kpi > .kpi-sub` is (0,3,0) against that rule's (0,2,0). */
.kpis .kpi > .kpi-sub { min-height: 1.45em; }
/* ⚠ `--color-accent`, THE REDESIGN'S LOGO BLUE, NOT `--accent-fill`. HANDOFF §2.1 names
   #0b8fd3 for "FILLS AND BORDERS ONLY" and a 2px rule is a border. `--accent-fill` still resolves
   to the product's teal `#0ca678` — Phase 0 added the redesign ramp without rebinding the old one,
   deliberately, so anything drawn in the new colour has to ask for it by name. */
button.kpi[aria-selected="true"] { border-color: var(--hairline-strong); border-bottom-color: var(--color-accent); }
/* ⚠ THE CHEVRON IS GONE (`.kpi-more` removed from the markup), so its rules go with it rather
   than sitting here matching nothing. It promised a disclosure; a tab does not disclose. */

/* ── two table classes PROMOTED out of the page sheets (34.2) ──────────────────────────────────
   ⚠ SAME REASON AS `.kpi` AND `.spark` ABOVE, and the same failure that caught them: a page with
   no extracted sheet of its own resolves page-scoped classes to nothing. `.is-nested` lives only
   in page-dashboard.css and page-pixels.css, and `.table-min` only in the twelve page sheets —
   so /report, which has no design frame and therefore no page-report.css, referenced both and
   they meant nothing there. Caught by verify's "undefined CSS classes" measurement, which is
   exactly what it is for.
   Copied verbatim from page-dashboard.css rather than reinvented: every value is a token or a
   keyword, and a second, subtly different definition of a shared class is worse than none. */
.is-nested { border: 0; border-radius: 0; }
.table-min { min-width: min-content; }

/* ── 32.4 · the date range popover ─────────────────────────────────────────────────────────────
   ⚠ NEW NAMES, CHECKED FIRST. The design has `.drawer*` and nothing else in this territory — no
   popover, no calendar, no day grid — because the frame it drew for this control was in an image
   the text export dropped. `dr-*` is the prefix; every value below is a design token, so it
   themes with the rest of the console and needs no dark-mode block of its own.

   ⚠ THE BUTTON MUST NOT CHANGE SIZE. That is the whole complaint being fixed: the previous
   version grew a 417px From/To block inside a flex row, pushing four controls onto four different
   baselines and the header to 115px. This one is a fixed trigger with everything layered above
   the page. */
.dr { position: relative; display: inline-flex; }

.dr-button {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--ink);
  background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r-md);
  /* 46.3 (second correction) - 10px to match `.pp-button`, so the two popover dropdowns inset
     their carets identically. At 11px this one sat a pixel further in than the pixel picker
     beside it. Height is held by --h-control (39.1), so the padding does not move it. */
  padding: 8px 10px; cursor: pointer; white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease);
}
.dr-button:hover { border-color: var(--hairline-strong); }
.dr-button[aria-expanded="true"] { border-color: var(--accent-border); }
.dr-label { font-variant-numeric: tabular-nums; }


/* Anchored to the trigger and layered over the page — nothing below it reflows. Right-aligned
   because this control sits at the right end of the header and a left-aligned panel would hang
   off the edge of the viewport. */
.dr-pop {
  /* 215.3 — directly under the button, as the reference draws it (was 6px below). */
  position: absolute; top: calc(100% + 2px); right: 0; z-index: 60;
  background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r-lg);
  box-shadow: var(--shadow-overlay); padding: var(--sp-4); min-width: 460px;
}
/* 215.3 — presets on the left behind a rule, then two months side by side (owner's reference, change brief item f). */
.dr-body { display: flex; gap: var(--sp-4); }
.dr-body > .dr-presets { padding-right: var(--sp-4); border-right: 1px solid var(--hairline-soft); }
.dr-cals { display: flex; gap: var(--sp-5); }

.dr-cal { width: 238px; }
/* the empty end of a month's head, the width of the arrow on the other month, so both names centre */
.dr-cal-pad { width: 28px; flex: 0 0 auto; }
.dr-cal-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}
.dr-month { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--ink); }
.dr-dow, .dr-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dr-dow span {
  text-align: center; font-size: var(--fs-micro); color: var(--ink-quiet);
  padding-bottom: 4px; font-weight: var(--fw-semibold);
}

.dr-day {
  appearance: none; background: none; border: 0; border-radius: var(--r-sm);
  font: inherit; font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
  color: var(--ink-body); padding: 6px 0; cursor: pointer; text-align: center;
}
.dr-day:hover:not(:disabled) { background: var(--accent-wash); }
.dr-day.is-outside { color: var(--ink-faint); }
.dr-day:disabled { color: var(--disabled-ink); cursor: not-allowed; }
/* Today is marked but not selected — they are different facts and the reference shows both. */
.dr-day.is-today { font-weight: var(--fw-semibold); box-shadow: inset 0 0 0 1px var(--hairline-strong); }
/* ⚠ `--accent-tint` + `--accent-text-on-tint` is a PAIR the palette defines for this exact
   situation. The first version used `--accent-hover` — a solid button's hover green — and left
   the text at `--ink-body`, so a selected range read as green blocks with the dates missing. */
.dr-day.is-in { background: var(--accent-tint); color: var(--accent-text-on-tint); border-radius: 0; }
/* The pair `.btn-primary` uses — a solid accent surface and the ink designed to sit on it.
   `--ink-on-inverse` was close enough to look fine and is not the token for this. */
.dr-day.is-start, .dr-day.is-end {
  background: var(--accent-action); color: var(--accent-on-fill); font-weight: var(--fw-semibold);
}

.dr-presets { display: flex; flex-direction: column; gap: 2px; min-width: 150px; }
.dr-preset {
  appearance: none; background: none; border: 0; border-radius: var(--r-sm);
  font: inherit; font-size: var(--fs-sm); color: var(--ink-body);
  text-align: left; padding: 7px 10px; cursor: pointer; white-space: nowrap;
}
.dr-preset:hover { background: var(--accent-wash); }
/* 215.3 — the chosen preset is a solid bar, as the reference draws it: the same pair a selected day's ends use. */
.dr-preset[aria-pressed="true"] { background: var(--accent-action); color: var(--accent-on-fill);
  font-weight: var(--fw-semibold); }

.dr-foot {
  display: flex; align-items: flex-end; gap: var(--sp-3);
  margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--hairline-soft);
}
.dr-field { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.dr-foot .btn { flex: 0 0 auto; }

/* The real <select> and the From/To pair stay in the document — they are what works without
   scripting and what the page reads on Apply — and stop taking up room. Element-agnostic here
   because it is applied to both a <select> and a <div>, but still two selectors deep so a page
   sheet's bare `.select` cannot outrank it (the trap that hung the pixel picker 391px off-screen). */
.dr-native, .dr-native.select, .dr-native.field-inline {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  white-space: nowrap; padding: 0; border: 0; margin: 0;
}

@media (max-width: 700px) {
  /* One column, and anchored to the right edge of the screen rather than the button. */
  .dr-pop { min-width: 0; width: min(92vw, 460px); }
  .dr-body { flex-direction: column; }
  .dr-body > .dr-presets { padding-right: 0; border-right: 0; }
  .dr-cals { flex-direction: column; }
  .dr-cal { width: 100%; }
}

/* ── 34.7 · the sparkline on a KPI card, and the chart's hover card ────────────────────────────
   Point 8's reference draws both: a shape under every number, and a small card at the cursor
   carrying the date and the value. */
.kpi-spark { display: block; margin-top: var(--sp-2); height: 34px; }
.kpi-spark .chart { height: 34px; }

/* The tooltip. Positioned from script against the chart's own box, so `.chart-host` is the
   containing block — without the relative position it would anchor to the page and sit in the
   wrong place on any scrolled screen. */
.chart-host { position: relative; }
.chart-tip {
  position: absolute; z-index: 20; pointer-events: none;
  background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r-md);
  box-shadow: var(--shadow-raised); padding: var(--sp-2) var(--sp-3);
  display: flex; flex-direction: column; gap: 3px; white-space: nowrap;
}
.chart-tip-when { font-size: var(--fs-xs); color: var(--ink-quiet); }
.chart-tip-row { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); }
.chart-tip-dot {
  width: 8px; height: 8px; border-radius: var(--r-full); background: var(--chart-1); flex: 0 0 auto;
}
.chart-tip-label { color: var(--ink-body); }
.chart-tip-value { color: var(--ink); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }

/* ── 203.14 · the globe and ring styles are GONE with their components ───────────────────────
   SPEC-SCREENS §1: "Removed, and they must not come back: the globe · the three progress rings ·
   Top IPs." The note that used to sit here said the `.donut-*` component stayed "for now" because
   removing a shared component inside a dashboard PR is a separate, reviewable change — and named
   the module that would do it. This is that module.
   ⚠ WHAT WENT: `--globe-sea` / `--globe-land` and the `.globe-*` rules; `.donut`, `.donut-slice`,
   `.donut-host`, `.donut-tip` and the `:has()` pair that dimmed the unhovered slices. `.donutrow`
   went at 203.11 with the row it laid out. Nothing else in the console referenced any of them —
   checked against every page's markup, not only against the dashboard's.
   ⚠ `.meter` AND ITS BANDS ARE NOT PART OF THIS and are still used by /billing. The rings and the
   meter are different components that both draw a proportion; deleting the wrong one would take
   the quota bar off the Billing screen. */

/* ── 203.9 · Message performance three quarters, This month one ───────────────────────────────
   The prototype's own `3fr 1fr`. ⚠ `align-items: start`, NOT `stretch` like `.listrow`: those two
   cards hold ranked lists of the same shape and a ragged bottom edge reads as a bug, while these
   two hold a table of unknown length beside four fixed figures — stretching the short one leaves a
   quarter-width card of white space, which is 46.5's complaint in the other direction.
   ⚠ AND IT COLLAPSES TO ONE COLUMN AT THE SAME 1200px `.listrow` DOES. A 1/4 column of a 1024px
   viewport is 230px, which is narrower than "of 20,000 on the Growth plan". */
.perfrow { display: grid; grid-template-columns: 3fr 1fr; gap: var(--sp-4); align-items: start; }
/* ⚠ THE ROW'S CARDS MUST NOT KEEP THE GLOBAL `section { margin-top: 44px }`, and this row was
   missing the reset every other row on the page already has. `components.css` gives every
   <section> a 44px top margin; `page-dashboard.css` cancels it with
   `.listrow > *, .donutrow > *, .panelgrid > *, .hstack-2 > * { margin-top: 0 }` — a list `.perfrow`
   was never added to, because `.perfrow` did not exist when it was written. The result was 44px of
   dead space above Message performance and This month, so the band gap there read 72px against 28
   everywhere else. Nothing looks broken; the column just loses its rhythm at the bottom, which is
   the fault 39.5 exists to catch and which dashboard-shell-gate did catch.
   ⚠ IT IS FIXED HERE, NOT THERE. `page-dashboard.css` is GENERATED (ground rule 2) and a hand edit
   would be destroyed by the next `extract-css.py` run. `.perfrow > *` is (0,1,0) against
   `section`'s (0,0,1), so it wins on specificity and does not depend on sheet order. */
.perfrow > * { margin-top: 0; }
@media (max-width: 1200px) { .perfrow { grid-template-columns: minmax(0, 1fr); } }

.monthcol { display: flex; flex-direction: column; padding: var(--pad-row); gap: var(--sp-3); }
/* ⚠ THE PLAN STRIP'S OWN BLOCK (§1 structure 7), WHICH 203.9 USED AND NEVER DEFINED. The markup
   carried `class="monthquota"` and no stylesheet had a rule for it — the "GREP THE CLASS BEFORE
   YOU WRITE IT" trap CLAUDE.md records four instances of, found here by coverage-gate's undefined
   -class measurement rather than by looking, which is exactly the backstop that note describes.
   It is not a naked wrapper: the strip is a statement about the ACCOUNT sitting above four figures
   about the WEBSITE, and the rule under it is what says so. */
.monthquota { padding-bottom: var(--sp-3); border-bottom: 1px solid var(--hairline); }
.monthquota-top, .monthstat-top {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2);
}
.monthquota-k, .monthstat-k { font-size: var(--fs-xs); color: var(--ink-quiet); }
.monthquota-pct { font-size: var(--fs-xs); color: var(--ink-quiet); font-variant-numeric: tabular-nums; }
.monthquota-v {
  display: block; font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--fs-lg); line-height: var(--lh-lg); font-weight: var(--fw-semibold); color: var(--ink);
}
/* 3px, the prototype's height. The fill is `--color-accent`: HANDOFF §2.1 puts the logo blue on
   fills, and this one carries no text. */
.monthquota-track {
  display: block; height: 3px; margin-top: 5px; background: var(--hairline); border-radius: var(--r-full);
}
.monthquota-fill { display: block; height: 100%; background: var(--color-accent); border-radius: var(--r-full); }
.monthquota-of { display: block; font-size: var(--fs-xs); color: var(--ink-quiet); margin-top: 3px; }
.monthstat { padding-top: 7px; border-top: 1px solid var(--hairline-soft); }
.monthstat + .monthstat { margin-top: var(--sp-2); }
.monthstat-v {
  font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; text-align: right;
  color: var(--ink); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* "Not yet" is a state, not a figure — it takes the quiet ink so it never reads as a measurement. */
.monthstat-v.is-none { color: var(--ink-quiet); font-weight: var(--fw-regular); }
.monthstat-sub { display: block; font-size: var(--fs-micro); color: var(--ink-quiet); }

/* ── 35.1–35.3 · the ranked lists (feedback point 11) ──────────────────────────────────────────
   Icon, label over its page, a line chart, the count — and a mark when the row is pinned. */
/* ⚠ `--pad-row` IS A TWO-VALUE SHORTHAND (12px 16px), NOT A LENGTH - AND THIS IS THE FOURTH
   PLACE IT HAS BITTEN. Written as `padding: var(--sp-2) var(--pad-row) var(--sp-3)` it expanded to
   FOUR values, `6px 12px 16px 8px`: top 6, right 12, bottom SIXTEEN, and a LEFT OF EIGHT - the
   `--sp-3` that was meant to be the bottom landed on the left instead. Measured on the running
   screen, not inferred. The owner in production bug report #6: "the left inside padding is missing
   and it's negligible". It was 8px, against 47px on the other side.
   The same trap is written up at `.drawer-dialog .drawer-head` below, in page-report.css and in
   page-visitors.css. Three notes about it already existed and this rule sat between two of them.

   ⚠ AND THE 47px CAME FROM THREE PLACES, WHICH IS WHY ONLY MEASURING FOUND IT: 12px of container
   padding, 15px of scrollbar gutter reserved on the right ONLY, and the 12px pin slot plus its 8px
   gap sitting outside the number. The pin moved off the trailing edge (see rankList), and
   `both-edges` makes the browser reserve the gutter symmetrically - which is a RULE about the two
   sides being equal, not a hand-computed 15px that would rot on a platform with a different
   scrollbar.

   ⚠ AND 46.4 MADE THE TWO SIDES EQUAL AT **4px**, WHICH IS ONLY HALF OF WHAT THE OWNER ASKED FOR —
   `ranked-cards-gate` has been red on its own module's second sentence since the day 46.4 shipped
   (de8c465, 2026-08-30), and the phase-0 baseline of this redesign recorded it red before a line
   of redesign code existed: "smallest 4px (was 8)". The report is one sentence with two clauses,
   "It should be equivalent AND A LITTLE LARGER", and `--sp-1` satisfied the first by shrinking the
   larger side down to below the value the owner had just called negligible. `--sp-4` (12px) is the
   card's own horizontal step and clears the 8px floor the gate encodes. Fixed at 203/204 rather
   than left as an inherited red, because "it was already red" is how a red becomes furniture. */
.ranklist { padding: var(--sp-2) var(--sp-4) var(--sp-4); }
.ranks { overflow-y: auto; scrollbar-gutter: stable both-edges; }
/* ⚠ EVERY COLUMN IS A FIXED WIDTH EXCEPT THE LABEL. The count column was `auto`, so a row reading
   "10,952" made it wider than a row reading "1" — which pushed the BAR left by the difference and
   left every row with a different gap between bar and number. The owner spotted it: "the distance
   is not look same". A fixed count column and a fixed slot for the pin make the geometry identical
   on every row, which is what the reference shows. */
.rank {
  /* 46.4 - the pin slot moved from the trailing column to between the label and the bar, so the
     COUNT is the end of the row. As the last column its 12px plus the row gap sat outside the
     number and made the right inset 20px wider than the left before anything else was counted. */
  display: grid; grid-template-columns: 22px minmax(0, 1fr) 12px 70px 58px;
  align-items: center; gap: var(--sp-3);
  padding: 6px 0; border-bottom: 1px solid var(--hairline-soft);
}
.rank:last-child { border-bottom: 0; }
.rank-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; color: inherit; text-decoration: none; }
.rank-label {
  font-size: var(--fs-sm); color: var(--ink-body);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
a.rank-main:hover .rank-label { color: var(--accent-text); text-decoration: underline; }
.rank-sub {
  font-size: var(--fs-micro); color: var(--ink-quiet);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ⚠ `display: block` STATED. `.geo-track` is the same shape and omits it, and because a <span>
   is inline its height is ignored — every country bar in this product has measured 0px tall since
   the day it was written. Same trap, one card over. */
.rank-bar {
  display: block; width: 70px; height: 7px; border-radius: 4px;
  background: var(--hairline-soft); overflow: hidden;
}
.rank-fill { display: block; height: 100%; border-radius: 4px; background: var(--accent-fill); }
.rank-n {
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--ink);
  font-variant-numeric: tabular-nums; text-align: right;
}
.rank-pin { color: var(--warning-ink); font-size: var(--fs-xs); text-align: center; }

/* ── N01 · the share beside the figure (redesign phase 2, module 203.6) ──────────────────────
   ⚠ A SIXTH COLUMN, ADDED ONLY WHEN THERE ARE SHARES TO PUT IN IT. `rankList` sets `.has-share`
   from whether the window total is a positive number, so a list with no denominator keeps the
   five-column geometry rather than showing an empty 46px gutter where a percentage should be —
   N01 is explicit that a zero total renders NO percentage, and an empty column is a percentage
   that failed to load.
   ⚠ AND THE COLUMN IS FIXED-WIDTH FOR THE REASON THE COUNT COLUMN IS. "4.2%" and "41.9%" are
   different widths; auto would move the bar and the number on every row, which is the uneven
   geometry the owner already reported once ("the distance is not look same"). */
.ranklist.has-share .rank { grid-template-columns: 22px minmax(0, 1fr) 12px 70px 58px 46px; }
.rank-pct {
  font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--ink-quiet);
  font-variant-numeric: tabular-nums; text-align: right;
}
/* The slot exists on every row so the pinned one does not sit wider than its neighbours.
   ⚠ THE OLD `.rank > .rank-n + :not(.rank-pin) { display: none }` WENT WITH THE REORDER (46.4).
   It guarded the trailing position the pin used to hold; with the count last there is nothing
   after `.rank-n` for it to match, so it was inert rather than protective. Removed rather than
   left in place looking like a rule that still does something. */

/* ⚠ A COLOUR PER ACTIVITY TYPE, AND NEVER THE ONLY SIGNAL. Every row says in words what it is;
   the colour is there to make the kinds separable at a glance, which is what point 11 asks for.
   Each is an existing semantic family rather than a new hue — the palette already distinguishes
   these six, and inventing more would be six colours nobody chose. */
.rank-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: var(--r-sm); flex: 0 0 auto;
}
.rank-icon.is-click   { color: var(--info-ink);    background: var(--info-bg); }
.rank-icon.is-change  { color: var(--accent-text); background: var(--accent-tint); }
.rank-icon.is-pagein  { color: var(--success-ink); background: var(--success-bg); }
.rank-icon.is-pageout { color: var(--danger-ink);  background: var(--danger-bg); }
.rank-icon.is-tabaway { color: var(--warning-ink); background: var(--warning-bg); }
.rank-icon.is-tabback { color: var(--success-ink); background: var(--success-bg); }
.rank-icon.is-scroll  { color: var(--neutral-ink); background: var(--neutral-bg); }
.rank-icon.is-custom  { color: var(--neutral-ink); background: var(--neutral-bg); }

/* The three lists point 11 puts in one row. */
/* ⚠ `align-items: stretch`, NOT `start` (46.5). Production bug report #7: "the three cards are
   not vertically aligned evenly". Measured on the running screen before changing anything - Top
   pages ended at y=1481 while the other two ended at y=1778, a 297px ragged edge - because `start`
   sizes each card to its own content and the lists hold different numbers of rows.
   Stretching the track is only half of it: the card also has to lay its own children out, or a
   taller card just leaves white space under its list instead of under its footnote. `.listrow
   .card` below is that half - the ranked list takes the slack and the note strip is pushed to the
   bottom edge, which is the owner's second sentence: "there is white padding at the end of each
   card that should not be there because the last portion is the notes". */
/* ⚠ TWO COLUMNS, NOT THREE (203.4). Top IPs was the third card and the redesign removes it —
   "Removed, and they must not come back: the globe · the three progress rings · Top IPs". Leaving
   the track at three would have put two cards in a three-column row, which is 34.4's own failure:
   the column count must DIVIDE the card count or the row ends in a hole. `dashboard-charts-gate`
   measured exactly that — "1 empty slot(s) across 2 grid(s)" — before this line changed. */
.listrow { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4); align-items: stretch; }
.listrow > .card { display: flex; flex-direction: column; }
/* ⚠ `:not([hidden])` IS LOAD-BEARING. `[data-state="ready"]` ships WITH the `hidden` attribute
   and is unhidden when the region loads - and `display: flex` BEATS `[hidden] { display: none }`,
   so without this guard the ready panel is laid out from first paint and every card shows its
   empty list under its own spinner. Same trap as `.drawer` over `dialog:not([open])`, which cost
   this repo a screen where the closed drawer silently ate every click. */
.listrow > .card > [data-state="ready"]:not([hidden]) {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
}
.listrow > .card .ranklist { flex: 1 1 auto; min-height: 0; }
.listrow > .card .card-foot { margin-top: auto; }

/* ── 46.5 · the footnote, two lines, on all three cards ────────────────────────────────────────
   The owner offered the choice and took it: "we could have two notes: one about showing the top
   and the second as a disclaimer. These should either be presented in two lines for all or one
   line for all."
   ⚠ EACH LINE IS ITS OWN BOX rather than one wrapped sentence. A single string that happens to
   wrap to two lines on this viewport is one line on a wider one, and the three cards go ragged
   again the moment somebody resizes - which is the fault this module exists to remove. Two
   elements occupy two rows at every width.
   ⚠ AND THE CARD FOOT STOPS BEING A ROW. `.card-foot` is `display: flex` with
   `justify-content: space-between` (page-dashboard.css), which laid the two lines out SIDE BY
   SIDE at opposite ends of the strip. The ranked cards' foot is a stacked note, not a toolbar. */
.listrow > .card .card-foot { display: block; }
.listrow > .card [data-pages-foot],
.listrow > .card [data-events-foot],
.listrow > .card [data-ips-foot] {
  display: flex; flex-direction: column; gap: 2px;
}
.foot-line { display: block; }
@media (max-width: 1200px) { .listrow { grid-template-columns: minmax(0, 1fr); } }

/* ══ 38.18 — a place, with a real flag ════════════════════════════════════════════════════════
   The owner, 2026-08-23: "The location should display the city along with the country name and a
   flag." 38.12 delivered the name and left the flag owed, on a claim that turned out to be wrong
   twice over — see the module note. What was true is the platform problem, and it is worth stating
   because it is the whole reason a font file exists in this repo:

   ⚠ WINDOWS SHIPS NO REGIONAL-INDICATOR GLYPHS. `🇮🇳` is not one character, it is the pair
   U+1F1EE U+1F1F3, and a font that has no glyph for the pair renders the two letters it is built
   from. Segoe UI Emoji deliberately omits flags, so Chrome and Edge on Windows draw "IN" — which
   is exactly the "country digits" the owner asked us to stop showing. Measured on this box before
   and after: without the font the glyph is a MONOCHROME letterform; with it, a colour glyph.

   ⚠ THE `unicode-range` IS DEFENSIVE, AND I FIRST WROTE THE OPPOSITE HERE. The comment said it
   was load-bearing — that without it "every letter would come from a file that has none of them" —
   and the gate had an assertion agreeing. Then the range was DELETED and the assertion stayed
   green, because font fallback is per-GLYPH: a face with no 'A' never supplies one, range or no
   range. Since this family is named only by `.flag`, its absence is undetectable by rendering.
   It is kept because it is correct and free, and because it is the only thing that would stop a
   future selector putting a flags-only font in front of body text — but the honest statement is
   that nothing on screen can prove it is here. The gate now asserts the DECLARATION, via the
   parsed CSSOM, and says in its own comment that this is what it is.

   ⚠ `swap`, NOT `block`, deliberately. `block` would avoid the letters-then-flag flash, but a font
   that fails to arrive would then leave the flags INVISIBLE rather than degraded — silently, since
   a missing font is not an error. `swap` degrades to the letters we shipped yesterday. The build
   already proves the file exists (`build.mjs` resolves every `url()` into dist), so the flash is
   one paint on a first load and nothing after it. */
@font-face {
  font-family: "Twemoji Country Flags";
  font-style: normal; font-weight: 400; font-display: swap;
  unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067,
    U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
  src: url("/fonts/9f04f14429/twemoji-country-flags.woff2") format("woff2");
}

/* Shared by the visitors grid, the drawer and the single-visitor page — which is why these three
   rules live here and not in `page-visitors.css`. visitor.html showed bare country CODES until
   38.18; a component in one page's sheet is how that happened. */
.place { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.place-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 40.3 — the grid's two-line form: the flag beside a city stacked over its country, which is the
   shape Device and Current page already use and the one the owner's reference draws. */
.place-stack { display: flex; flex-direction: column; min-width: 0; }
.place-stack .cell-main,
.place-stack .cell-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The flag sits against the first line rather than floating in the middle of two.
   217.12 (Changes 03 C12) — and it is CENTRED on that line, not pinned to its top. Pinned (`margin-top: 1px`), a flag
   13.6px tall rode 2px high against a 19.5–20.3px city line on the page, in the drawer and in the grid alike. The
   flag's box is given the city line's own height (`1lh` read on `.place`, which has the city's font) and its glyph
   is centred inside it, so it follows the line at every size. `--place-line` is registered so `1lh` is resolved on
   `.place` and inherited as a length, rather than re-read on the flag's own one-em line. */
@property --place-line { syntax: '<length>'; inherits: true; initial-value: 0px; }
.place:has(.place-stack) { align-items: flex-start; --place-line: 1lh; }
.place:has(.place-stack) .flag { display: inline-flex; align-items: center; height: max(1em, var(--place-line)); }
.flag {
  flex: 0 0 auto; line-height: 1; font-size: 1.05em;
  /* The system faces stay in the stack after ours: macOS, iOS and Android all draw flags natively
     and will never fetch the file, and if it somehow fails to load they still show a real flag. */
  font-family: "Twemoji Country Flags", "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
}
/* ⚠ THE FALLBACK IS STILL A CODE, AND STILL NEEDED. A code that no country claims — "QX", or the
   provider's own "ZZ" — has no flag glyph in any font, so it would render as its own two letters
   with nothing to say that is what happened. This badge says it deliberately. */
.cc-chip {
  flex: 0 0 auto; font-family: var(--font-mono); font-size: 9px; letter-spacing: .04em;
  line-height: 1; padding: 3px 4px; border-radius: 3px;
  background: var(--neutral-bg); color: var(--ink-quiet);
}


/* ⚠ 41.4b — DOUBLE WIDTH, AND IT TAKES TWO RULES, NOT ONE. The owner asked for twice the width
   (2026-08-24). `.modal-md` is 520px so the target is ~1040 — but the DIALOG element is capped at
   `min(92vw, var(--modal-lg))`, i.e. 620px, and a dialog's cap governs the modal inside it. Widening
   `.modal` alone changes nothing on screen, which is exactly the kind of edit that looks done and
   is not. Both are raised, scoped to this dialog so the other modals keep the design's sizes.
   ⚠ AND `min(92vw, …)` IS KEPT so a 1040px modal does not overflow a 1000px window.
   ⚠⚠ `width`, NOT JUST `max-width`, AND MEASURING IS THE ONLY REASON THAT IS HERE. A `<dialog>`
   shrink-wraps its content: raising the CAP alone let it shrink to its natural 367px — NARROWER
   than the 520 it had, because the old cap was also what was holding it open. The screen would
   have looked "changed" and been wrong in the opposite direction. */
[data-bug-modal] { width: min(92vw, 1040px); max-width: min(92vw, 1040px); }
[data-bug-modal] .modal { width: 100%; max-width: 1040px; }

/* ══ 41.4 — the Report Bug modal ══════════════════════════════════════════════════════════════
   ⚠ THE DROP ZONE IS FOCUSABLE AND SAYS WHAT TO PRESS. Paste is the path the owner asked for and
   it is invisible by nature — there is no control to click — so the zone carries the instruction
   rather than leaving Ctrl+V to be guessed. */
.bugshot {
  display: flex; align-items: center; justify-content: center; text-align: center;
  min-height: 132px; padding: var(--sp-3);
  border: 1px dashed var(--hairline-strong); border-radius: var(--r-md);
  background: var(--surface-sunken); color: var(--ink-quiet); font-size: var(--fs-xs);
}
.bugshot.is-over { border-color: var(--accent-border); background: var(--accent-tint); }
.bugshot:focus-visible { outline: 2px solid var(--accent-action); outline-offset: 2px; }
.bugshot-img {
  max-width: 100%; max-height: 260px; border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid var(--hairline);
}
.bugshot kbd {
  font-family: var(--font-mono); font-size: var(--fs-micro); padding: 1px 4px;
  border: 1px solid var(--hairline-strong); border-bottom-width: 2px; border-radius: 3px;
  background: var(--surface); color: var(--ink-body);
}

/* The context block is collapsed by default: it is there to be checkable, not to be read every
   time. `dt`/`dd` rather than a table — it is a list of pairs, not a grid. */
.bugctx > summary { font-size: var(--fs-xs); color: var(--ink-quiet); cursor: pointer; }
.bugctx-list {
  display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px var(--sp-3);
  margin: var(--sp-2) 0 0; font-size: var(--fs-micro);
}
.bugctx-list dt { color: var(--ink-quiet); font-family: var(--font-mono); }
.bugctx-list dd {
  margin: 0; color: var(--ink-body); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Pushes the "See reported bugs" link away from the two actions, so a navigation and a save are
   not mistaken for a pair of buttons. */
.modal-foot-gap { flex: 1 1 auto; }

/* ══ 41.5 — the reported bugs list and drawer ══════════════════════════════════════════════════ */
/* Only two of the four needed a width of their own; `c-flex` and `c-when` are the design's. */
.col-bug-who    { width: 220px; }
.col-bug-status { width: 130px; }

/* ══ 48.1 — the bump send history ═════════════════════════════════════════════════ */
/* Three of the five need a width of their own; `c-when` and `c-flex` are the design's, and the
   offer is the one column that should take whatever is left. */
.col-send-who { width: 200px; }
.col-send-how { width: 190px; }
/* ⚠ WIDE ENOUGH FOR THE LONGEST BADGE THIS COLUMN CAN HOLD, MEASURED. "Delivered, not shown"
   renders at 142px, and 160px minus 2×16px of cell padding left it 128px — so the badge
   overflowed its own cell and the words were clipped against the table edge. The gate asserts the
   RELATIONSHIP (no badge wider than its cell's content box) rather than this number, so a reworded
   outcome cannot quietly reintroduce it. */
.col-send-out { width: 180px; }

/* ══ 205.16 — Deliveries: SPEC-SCREENS §12's grid ══════════════════════════════════════════════ */
/* ⚠⚠ THESE FOUR NUMBERS ARE THE ONE THING IN THIS MODULE THAT NO DOCUMENT SETTLES, so they are
   variables and the owner can change the screen from this block alone.
   HANDOFF.md §3 item 11 and TESTS 4.26 both read "the four middle columns fixed at 84/64/64/76;
   Message and Outcome split the remainder". The grid has FIVE columns, and with Message and
   Outcome both flexible only THREE remain to fix — the sentence cannot be satisfied as written.
   `Deliveries.dc.html` does something different again: it fixes the two ENDS (120px … 132px) and
   flexes the three middles.
   ⚠⚠ THE QUADRUPLE WAS BUILT FIRST AND MEASURED, AND IT DOES NOT FIT ITS OWN CONTENT. At 1440px
   with When 84 · Who 64 · Why 64 · Outcome 76: FOUR OF THE FIVE CELLS CLIPPED — "Today 07:35"
   overflowed 84px, "WittyTiger82" overflowed 64, "By hand" overflowed 64, the badge "Dismissed"
   overflowed 76 — while Message, the least informative cell on the row ("Ad-hoc message · Popup"),
   took 834px, more than half the table. Those four numbers fit a table with four NARROW middle
   columns; the dashboard's Message performance row (Message · Channel · Shown · Clicked ·
   Dismissed · Outcome) is exactly that shape and this one is not.
   So the built widths are the prototype's, which is the design artefact the sentence summarises,
   and they are variables: to try the quadruple again, change these four values and nothing else.
   The gate asserts the RELATIONSHIP — 36px rows, nothing clipped, Message not swallowing the
   table — rather than these numbers, the same choice 48.1 made above for the badge column. */
:root {
  --col-dl-when: 120px;   /* HANDOFF/TESTS say 84 */
  /* ⚠ 22%/24% BEFORE THE MERGE, AND THE SIXTH COLUMN IS WHY THEY MOVED. 55.4's 210px comes out
     of the flexible one, and at 1440px that left Message at 230px against Why's 308 — the design's
     one stated relationship, "Message takes the remainder", inverted. Measured, not guessed. */
  --col-dl-who:  17%;     /* HANDOFF/TESTS say 64px */
  --col-dl-why:  18%;     /* HANDOFF/TESTS say 64px */
  --col-dl-out:  132px;   /* HANDOFF/TESTS say 76 and call it flexible */
}
.col-dl-when { width: var(--col-dl-when); }
.col-dl-who  { width: var(--col-dl-who); }
.col-dl-why  { width: var(--col-dl-why); }
.col-dl-out  { width: var(--col-dl-out); }
/* ⚠ QA E019 — AT THE SUPPORTED 1024px THE MESSAGE COLUMN WAS 2px WIDE. A `<col>` ignores `min-width`, so `.c-flex`'s
   132px never applied: 462px of fixed columns plus 35% of a 714px table left Message nothing, its heading drawn over
   "Who saw it". Below 1280px the other five give way in pixels instead — Message keeps 146px at 1024 and grows from
   there; at 1280 and up the widths above are unchanged. */
@media (max-width: 1279px) {
  :root { --col-dl-when: 96px; --col-dl-who: 120px; --col-dl-why: 120px; --col-dl-out: 112px; }
}

/* ⚠ 36px IS THE ROW, NOT THE CELL. HANDOFF and SPEC-SCREENS §12 both say "one 36px line per row";
   with the shared table's own vertical padding the height is set here on the cells so the rule
   holds whatever the row contains, and `line-height` keeps a two-line cell from silently making
   it 52. Everything that does not fit is elided rather than wrapped — a wrapped cell is how one
   long message name makes every row on the screen a different height. */
.data tr[data-send-row] > td {
  height: 36px;
  padding-top: 0; padding-bottom: 0;
  line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ⚠⚠ `.cell-seg` IS NOT `.cell-sub`, AND THE DIFFERENCE IS THE WHOLE OF `design-consistency`'s
   "a two-line cell stacks" RULE. `.cell-sub` means the SECOND LINE of a two-line cell — the gate
   walks every `.cell-sub` on 25 screens and fails any that shares a line with the text above it.
   A 36px row has no second line, so this cell is not a two-line cell at all, and calling its parts
   `.cell-sub` claimed a shape it does not have. 112 cells reported it within the hour.
   `.cell-seg` is an inline SEGMENT of a one-line cell: message · channel · host. */
.data tr[data-send-row] > td > .cell-main,
.data tr[data-send-row] > td > .cell-seg,
.data tr[data-send-row] > td > .cell-link {
  display: inline; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cell-seg { color: var(--ink-quiet); font-size: var(--fs-sm); }
.data tr[data-send-row] > td > .cell-seg:not(:first-child)::before,
.cause > .cell-seg:not(:first-child)::before {
  content: " · "; color: var(--ink-quiet);
}

/* SPEC-SCREENS §12: "Every row names its cause with a glyph." The glyph is decorative — the cause
   is spelled beside it, because an icon alone is a cause only its author can read. */
.cause { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.cause-glyph { display: inline-flex; color: var(--ink-quiet); flex: 0 0 auto; }
.cause > .cell-main, .cause > .cell-seg, .cause > .cell-link {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ⚠ `.ed-eyebrow` MOVED HERE FROM `page-automations.css` at 205.16 — a second drawer now carries
   one, and a page sheet does not reach another page. Same rule as `.headbar` and `.drawer-dialog`
   itself: shared the moment a pattern gets a second caller. The name is kept so the event panel's
   markup and its gates are untouched; the third copy of these five declarations, `.secret-eyebrow`,
   is left where it is because it is still used on exactly one screen. */
.ed-eyebrow {
  display: block; font-size: var(--fs-micro); letter-spacing: var(--tr-micro);
  text-transform: uppercase; color: var(--ink-quiet); font-weight: var(--fw-semibold);
  margin-bottom: 4px;
}

/* A row opens its delivery (N20), so it has to look like it does. */
.data tr[data-send-row] { cursor: pointer; }

/* ── N20 · the delivery detail panel ──────────────────────────────────────────────────────── */
/* ⚠ 680px, WHICH IS SPEC-SCREENS §0's WIDTH FOR A DETAIL PANEL ("680px for detail; 820-1000px for
   a visitor"), and the same one the event panel uses. Without an attribute rule of its own the
   drawer inherits `--drawer-w: 82vw` from the bare `.drawer-dialog` rule below and opens at
   1181px on a 1440px screen — a sheet that wide stops being a panel beside the list and becomes a
   second screen on top of it. Set here rather than in a page sheet because there is no
   `page-sends.css`, and inventing one for a single declaration is how a sheet-per-page becomes a
   sheet-per-rule. */
.drawer-dialog[data-dl-drawer] { --drawer-w: min(680px, 100vw); }
.dl-sec { margin-top: var(--sp-5); }
.dl-sec > .card-title { margin: 0 0 var(--sp-3); }
.dl-trouble { margin-bottom: var(--sp-4); }

/* 207.9 — a refusal is NOT trouble, and the tone is the whole argument. `.panel-error` is
   reserved for the two outcomes SPEC-SCREENS §12 groups as Trouble ("the two anybody comes here
   hunting for"); a message a house rule held back is the product doing exactly what the account
   asked. Turning every working frequency cap into a page of red alarms is how a customer learns
   to switch the cap off. */
.panel-held { border-color: var(--warning-border); background: var(--warning-bg); color: var(--warning-ink) }

/* ⚠ THE TIME COLUMN IS FIXED AND TABULAR. The whole point of this list is the ORDER of things
   that happened seconds apart, and proportional digits make two stamps of the same length look
   different widths, which is exactly the comparison a reader is trying to make. */
.dl-time { list-style: none; margin: 0; padding: 0; }
.dl-step {
  display: grid; grid-template-columns: 112px minmax(0, 1fr);
  gap: 2px var(--sp-3); align-items: baseline;
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--hairline);
}
.dl-step:last-child { border-bottom: 0; }
.dl-step-when {
  font-variant-numeric: tabular-nums; font-size: var(--fs-sm); color: var(--ink-quiet);
  grid-row: span 2;
}
.dl-step-what { font-weight: var(--fw-semibold); color: var(--ink); }
.dl-step-why { font-size: var(--fs-sm); color: var(--ink-quiet); }

/* Tall enough to show a popup at rest without scrolling it, which is what "exactly what they
   saw" promises; the frame is the gallery's own renderer, so the placement is real. */
.dl-frame {
  width: 100%; height: 320px; border: 1px solid var(--hairline);
  border-radius: var(--r-sm); background: var(--surface); display: block;
}
.dl-acts { margin-top: var(--sp-5); }
/* QA E001 — a popup or banner is drawn on a desktop-sized page, where the visitor saw it, and the page is scaled down to
   the drawer (Gump.fitStage) — the way "Their page" below shows the whole window. */
.dl-stage { min-width: 0; }
.dl-stage.is-staged { position: relative; overflow: hidden; border: 1px solid var(--hairline); border-radius: var(--r-sm); }
.dl-stage.is-staged > .dl-frame { width: 1280px; height: 800px; border: 0; border-radius: 0; transform-origin: 0 0; }
/* 217.26 — the picture of the visitor's page: its own aspect, never cropped, the frame's hairline. */
.dl-shot {
  display: block; width: 100%; height: auto; border: 1px solid var(--hairline);
  border-radius: var(--r-sm); background: var(--surface);
}

/* 55.4 — WHAT THE VISITOR TYPED INTO THE BUMP'S OWN FORM. */
/* ⚠ NARROWER THAN THE OFFER COLUMN ON PURPOSE. This cell is empty on the great majority of
   rows — most offers carry no form — so giving it the room its fullest case wants would spend
   that width on whitespace across every screen. Long values ellipsis and carry a `title`. */
.col-send-got { width: 210px; }
/* ⚠ QA E038 — THE NARROW WIDTH MUST COME AFTER THE BASE RULE. E019 put it in the media block above, where this later
   rule of the same specificity beat it: at 1024 the other four columns gave way and this one kept 210px, so the table
   was 790px in a 724px box and Outcome sat off-screen. */
@media (max-width: 1279px) { .col-send-got { width: 120px; } }
.got { display: flex; flex-direction: column; gap: 1px; min-width: 0 }
/* `.cell-main` supplies the weight and ink; this only has to stop a long address from pushing
   the column wider than its own colgroup width, which a table cell will happily let it do. */
/* The address is the one line in this cell somebody scans for, so it keeps the ink the others
   give up; the label beside it is what stops it reading as a duplicate of the Visitor column. */
/* ⚠ AND IT WRAPS RATHER THAN TRUNCATING, unlike every other value here. An e-mail ellipsised at
   this width reads "sends-mtsg1pnz@useg…" — which is exactly the half that is the same for
   everybody, so two different customers render identically and the column stops distinguishing
   the thing it exists to show. A hover title is not an answer for a list somebody is scanning.
   The Visitor column beside it already wraps for the same reason. */
.got-email {
  color: var(--ink); font-weight: var(--fw-semibold);
  white-space: normal; overflow-wrap: anywhere;
}
/* ⚠⚠ ON THE DELIVERIES GRID THIS BLOCK LIES DOWN, AND THAT IS THE MERGE OF TWO OWNER REQUESTS.
   55.4 stacks the captured fields, which is right for a row with no height rule; SPEC-SCREENS §12
   and HANDOFF §3 item 11 give this grid one — "one 36px line per row" — and a stack inside it is
   a cell clipped after its first line. So the same markup lies down here: label and value inline,
   fields separated, the whole thing ellipsised inside the 210px its colgroup gives it, with the
   full list in the cell's `title` and, untruncated, in the delivery panel one click away.
   ⚠ SCOPED TO THE ROW, so the panel's copy of `.got` keeps the column layout it was written for. */
.data tr[data-send-row] > td > .got { display: flex; flex-direction: row; gap: var(--sp-2); }
.data tr[data-send-row] > td > .got > .got-field { min-width: 0; }
.data tr[data-send-row] > td > .got > .got-field:not(:first-child)::before {
  content: "· "; color: var(--ink-quiet);
}
.data tr[data-send-row] > td > .got .got-val {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ⚠ `display: flex` OVERRIDES `.cell-sub`'s `display: block`, and both are wanted: the block
   keeps each field on its own line, the flex keeps the label and the value on ONE line with the
   label unshrinkable. Written after `.cell-sub` in this same sheet, so it wins on order without
   a specificity bump nobody would be able to explain later. */
.got-field { display: flex; gap: var(--sp-2); min-width: 0 }
.got-key { flex: none; color: var(--ink-quiet); opacity: .8 }
.got-val { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink) }

/* The site picker sits above the rest and is separated from them, because it is not a filter in
   the same sense: every other control is meaningless until it is answered. */
.sendfilter-site { padding: 14px 14px 12px; border-bottom: 1px solid var(--hairline) }

.sendfilters { display: flex; flex-direction: column; gap: var(--sp-4); padding: 14px }

.sendfilters-foot { display: flex; gap: var(--sp-2); align-items: center }

/* ⚠ 48.8 — the row a reader followed a link to reach. Ringed AND tinted: on a list where every
   row looks alike, a tint alone is hard to find, and on a filtered list of one visitor's sends the
   ring is what says "this one" rather than "these". */
.send-here { background: var(--accent-wash); box-shadow: inset 3px 0 0 var(--accent-base) }

/* ⚠ PROMOTED, NOT COPIED A THIRD TIME. This trio was written out byte-for-byte in
   page-visitors.css AND page-visitor.css, and 48.1 is the third screen to draw a pager. Nothing
   uses components.css's older button-based `.pager` — grep says the class appears in exactly two
   pages, both of which drew this one — so the shared layer is where it belongs. The declarations
   are unchanged from the copies they replace, so the visitors grid's pager is untouched. */
.pager {
  display:flex;gap:var(--sp-2);align-items:center;justify-content:space-between;flex-wrap:wrap;width:100%
}

.pagerbtns {
  display:flex;gap:var(--sp-2);align-items:center
}

.pagerbtns .btn:disabled {
  background:var(--disabled-fill);color:var(--disabled-ink);border-color:var(--disabled-border)
}
/* The whole row opens the drawer (38.14's pattern), so the whole row says so. */
[data-bug-rows] tr { cursor: pointer; }
[data-bug-rows] tr:hover { background: var(--neutral-bg); }
[data-bug-rows] tr:focus-visible { outline: 2px solid var(--accent-action); outline-offset: -2px; }

/* ⚠ `pre-wrap`, NOT `pre`. A description is prose with newlines in it: `pre` would refuse to wrap
   and give the drawer a horizontal scrollbar the moment someone pastes a stack trace. */
.bd-text { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; line-height: 1.55; }
/* ⚠ 41.10 — THE SCREENSHOT FITS WITHOUT SCROLLING. The owner: "i dont want to scroll to view the
   image". `max-width: 100%` alone bounds the WIDTH, so a tall screenshot — which is most of them,
   since a screen capture is taller than this 40vw panel is wide — pushed the description and the
   context below the fold. A viewport-relative cap bounds the other axis, and `object-fit: contain`
   keeps the aspect ratio while it does. Clicking enlarges it, so nothing is lost by fitting. */
.bd-shot { margin: 0; }
.bd-shot img {
  display: block; width: 100%; max-height: 46vh; object-fit: contain;
  border: 1px solid var(--hairline); border-radius: var(--r-md);
  cursor: zoom-in; background: var(--surface-sunken);
}

/* ⚠ 41.10b — THE IMAGE TAKES WHATEVER IS LEFT, rather than a fixed slice of the viewport. `46vh`
   was measured at 423px against a body that needed 673 for 608 available — still 65px of scroll,
   because a viewport fraction cannot know how long the description or the context list is. Making
   the figure the flexible child means the screenshot shrinks to fit whatever the text leaves, and
   `overflow: hidden` on the body states the promise the owner asked for: no scrolling to see it.
   ⚠ A FLOOR, THOUGH. Below ~140px a screenshot is a smudge; past that the description scrolls
   instead, which is the right thing to give up — the text can be read a line at a time, an image
   cannot. */
/* ⚠ `auto`, NOT `hidden`. Hiding the overflow made the promise true by CUTTING things off: with a
   long description the last line was sliced in half, which looks like a rendering fault rather than
   a choice. The image flexing is what keeps the scrollbar away in the ordinary case; if a report is
   genuinely longer than the panel, scrolling is the honest answer. */
[data-bug-drawer] .drawer-body { display: flex; flex-direction: column; overflow: auto; }
[data-bug-drawer] .bd-shot { flex: 1 1 auto; min-height: 140px; display: flex; flex-direction: column; }
[data-bug-drawer] .bd-shot img { flex: 1 1 auto; height: auto; min-height: 0; max-height: none; }
/* The description takes the height it needs — it is usually two lines, and capping it to a
   percentage clipped a line mid-stroke for no gain. */
[data-bug-drawer] .bd-text { flex: 0 0 auto; }
[data-bug-drawer] .bugctx { flex: 0 0 auto; }

/* The enlarged view: as big as the window allows, on a scrim, with the report still open behind. */
.shotbox {
  width: 96vw; max-width: 96vw; height: 94vh; max-height: 94vh;
  padding: 0; border: 0; background: transparent; overflow: visible;
}
.shotbox::backdrop { background: rgb(15 23 42 / 72%); }
.shotbox img {
  display: block; width: 100%; height: 100%; object-fit: contain;
  cursor: zoom-out; border-radius: var(--r-md);
}
.shotbox-x {
  position: absolute; top: 8px; right: 8px; z-index: 1;
  background: var(--surface); border-radius: var(--r-full);
}

/* ── 41.9 · the bug drawer's own shape ──────────────────────────────────────────────────────── */
/* Narrower than the visitors drawer: a description, one screenshot and eight context rows. */
/* ⚠ `.drawer-dialog[data-bug-drawer]`, NOT `[data-bug-drawer]` ALONE. The shared rule that declares
   `--drawer-w: 82vw` has the SAME specificity and sits later in this sheet, so the bare attribute
   selector lost on source order and the bug drawer opened at 82vw — 1366px for a paragraph and one
   screenshot. Measured before and after. */
/* 41.9b — the owner asked for 40% of the screen (2026-08-24). A floor keeps it usable on a small
   window, where 40% of 900px would be 360px for a screenshot. */
.drawer-dialog[data-bug-drawer] { --drawer-w: max(40vw, 460px); }
/* ⚠ THE FOOT WAS CENTRED AND STACKED, because `.stack` is a vertical rhythm helper and the shared
   `.drawer-foot` is `justify-content: flex-end`. Together they produced a column of centred
   controls filling a third of the panel. The queue controls are a form: labels left, actions left,
   in the order you work them. */
[data-bug-drawer] .drawer-foot {
  display: block; padding: var(--sp-4) var(--pad-row);
}
[data-bug-drawer] .drawer-foot .field { margin-bottom: var(--sp-3); }
[data-bug-drawer] .drawer-foot .field-label { display: block; margin-bottom: 4px; }
[data-bug-drawer] .drawer-foot .row-actions { justify-content: flex-start; gap: var(--sp-2); }
/* Status is a short list; it does not need the width of the panel. */
[data-bug-drawer] .drawer-foot .select { max-width: 220px; }
[data-bug-drawer] .drawer-body { gap: var(--sp-4); padding: var(--sp-4) var(--pad-row); }


/* ══ 41.9 — a drawer's POSITION is shared, because two screens now use it ═════════════════════
   A bare <dialog> is centred by the user agent and capped by the `dialog` rule above at
   `--modal-lg`. `.drawer` (components.css) styles the panel; THIS is what puts it against the
   right edge at full height — and it was sitting in `page-visitors.css`, where only one screen
   could reach it, so the bug-reports drawer rendered as a 620px box in the middle of the window.
   ⚠ THE WIDTH IS A VARIABLE, because the two drawers are not the same job. The visitors drawer
   carries a grid of facts and an activity feed and wants 82vw; a bug report is a paragraph and one
   screenshot. Each screen sets `--drawer-w`; this rule owns the geometry. */
.drawer-dialog {
  --drawer-w: 82vw;
  width: var(--drawer-w); max-width: var(--drawer-w); height: 100vh; max-height: 100vh;
  margin: 0 0 0 auto; padding: 0; border: 0;
}
/* ⚠ WITHOUT THIS THE CLOSED DRAWER SITS OVER THE PAGE, INVISIBLY EATING EVERY CLICK. `.drawer` sets
   `display: flex`, which beats the browser's own `dialog:not([open]) { display: none }`. */
.drawer-dialog:not([open]) { display: none; }

/* ══ 42.15 — the block after a page's heading sits close to it ══════════════════════════════
   The owner, on the visitor page: "reduce the gap as per the dashboard has."

   ⚠ MEASURED, AND IT WAS EXACTLY DOUBLE. `section { margin-top: 44px }` (components.css, the
   extracted sheet) plus `.content-inner`'s own 12px gap is the 56px this console puts BETWEEN
   stacked cards — right there, wrong immediately under a page title, where the heading and the
   first card are one unit. The dashboard has carried a fix since 39.x, but it lives in
   `page-dashboard.css` and nowhere else, so every other screen kept the 56.
   Visitor page: crumbs ended at 91, the first section began at 147 — 56px.
   Dashboard: head ended at 121, the first section began at 149 — 28px.

   ⚠ SCOPED TO "AFTER THE HEADING", NOT APPLIED GLOBALLY. The dashboard's rule is `* + *` — every
   top-level block on that screen sits at 16. Promoting THAT to the shared sheet would retune the
   vertical rhythm of all fifteen screens at once, which is a design change nobody asked for and
   which several spacing gates measure. This says only that the first block after the page's own
   heading row belongs to it — true of `.screen-head` and of `.crumbs` alike, and it leaves the
   spacing between the cards below completely alone.

   ⚠ THE DASHBOARD IS UNAFFECTED: its own rule already sets the same 16px on the same element, so
   the two agree rather than fight, and `page-dashboard.css` is loaded after this sheet anyway. */
/* ⚠ `~ section:first-of-type`, NOT `+ *`, AND THE FIRST VERSION OF THIS WAS `+ *` AND SILENTLY
   MISSED. The visitor page keeps a CLOSED `<dialog>` between its crumbs and its first section, so
   the adjacency selector styled the dialog — display:none, so nothing moved — and the section it
   was aimed at kept its 44px. Measured: still 147px after the "fix". This is the SECOND time a
   hidden sibling has broken an adjacency rule on this console; 39.x had `.app-banners`, a
   display:none div sitting first inside every `.content-inner`, and the lesson was written down
   and then not applied here.
   Naming the target by WHAT IT IS survives anything invisible being parked in front of it. */
/* 51.1 — AND `.screen-head`, WHICH IS THE OTHER THING A CRUMBED PAGE PUTS FIRST. Measured on
   2026-09-04: /visitor left 28px under its crumb trail and /pixel and /report left 12px, because
   those two follow their crumbs with a `<div class="screen-head">` and this rule named `section`.
   Four of the six crumbed pages matched it; two never had. The comment above warns against
   matching by position instead of by what a thing IS, and then matched on a tag name — the same
   mistake one line down. */
/* 51.8 — ONE RHYTHM FOR THE WHOLE CONSOLE. Measured across twelve screens on 2026-09-04 it was
   three: /pixels and /pixel 12px, nine screens 28px, and /automations, /content and /admin 56px.
   Two rules made those numbers, and both are neutralised here rather than page by page — four
   stylesheets had already grown their own override, which is how a shared rhythm becomes a
   per-page opinion.
     · `section { margin-top: 44px }` in components.css, out of the design bundle, gave the 56.
     · this rule's own `--sp-5` on the element after the head gave the 28.
   `.content-inner` is a flex column with a 12px gap, so with both at zero every screen leaves that
   gap and nothing else — the owner's "standardise on 12px everywhere".
   ⚠ THE CRUMB GAP IS NOT PART OF THIS AND KEEPS ITS STEP. `.crumbs + .screen-head` is the trail
   sitting above a title, which 51.1 standardised at 28 across the six crumbed pages after the
   owner reported it as inconsistent. Folding it into this change would undo that one. */
.content-inner > section,
.content-inner > .screen-head + * {
  margin-top: 0;
}

/* THE CRUMB TRAIL'S OWN STEP, which 51.1 standardised at --sp-5 across the six crumbed pages after
   the owner reported it as inconsistent. It applies whether the trail leads to a title (/pixel,
   /report) or straight to content (/visitor, /automation, /event, /journey) — dropping the second
   case while making 51.8's change is what put /visitor back out of step at 12 against everyone
   else's 28, caught by settings-layout-gate. */
.content-inner > .crumbs + .screen-head,
.content-inner > .crumbs ~ section:first-of-type {
  margin-top: var(--sp-5);
}

/* ⚠ ...BUT NOT WHEN A HEADING SITS BETWEEN THEM. Then the section is content under a heading, and
   content under a heading is 12px everywhere (51.8) — the rule above would otherwise put /pixel and
   /report back to 28 and undo the owner's request on exactly the two pages it came from.
   Specificity is why this is written with `:first-of-type` it does not need: the selector above is
   (0,3,1) because `:first-of-type` counts as a class, so a plain `.screen-head ~ section` at
   (0,2,1) would lose however late it is declared. */
.content-inner > .screen-head ~ section:first-of-type {
  margin-top: 0;
}


/* 42.14 — an icon beside a two-line cell, for the ranked tables on the visitor page. `.cell-main`
   and `.cell-sub` are blocks, so they need a wrapper of their own once something sits next to
   them. ⚠ GREPPED FIRST: the design has `.cell-main`, `.cell-sub`, `.cell-stack` and `.cell-wrap`
   and nothing that pairs a cell with an icon. */
.cell-flex { display: flex; align-items: flex-start; gap: var(--sp-3); min-width: 0; }
.cell-flex > span:last-child { min-width: 0; }


/* ══ 42.12 — the tab strip, with room to read ═══════════════════════════════════════════════
   The owner: "the tabs are well separated, there is a lot of padding, and each underline has
   enough space underneath so that each tab is equally visible and properly aligned."

   `.tabs`/`.tab` (components.css, design-generated — never hand-edited, so this is an override)
   sets `gap: var(--sp-5)` and `padding: 12px 2px`. Two pixels of horizontal padding means the
   whole gap between two tabs is the flex gap, and the selected tab's underline is exactly as wide
   as its text — so the rule under "Activities" and the rule under "Associated Emails" are wildly
   different lengths and the strip reads unevenly. Padding on the tab itself widens the underline
   with the tab, which is what makes them look like equals.

   ⚠ THE SPACE UNDER THE RULE IS THE STRIP'S, NOT THE TAB'S. A tab's bottom padding sits ABOVE its
   own border-bottom, so adding it there moves the underline down and leaves it touching the panel
   just the same. The breathing room has to come from the strip below the border. */
.drawer-dialog .tabs {
  gap: var(--sp-3);
  padding: 0;
  margin-bottom: var(--sp-5);
}

.drawer-dialog .tab {
  padding: 12px var(--sp-5);
}


/* ══ 42.9 — a thin drawer head, so the facts column gets the height ═════════════════════════
   The owner, looking at the finished drawer: "reduce the space from the header i need it thin so
   the left side details can visible more."

   ⚠ THE HEAD WAS 93px TO SAY TWO SHORT THINGS. `.drawer-head` (components.css, design-generated —
   never hand-edited, so this is an override and not a change to it) pads 15px and stacks the
   title over `.modal-sub`. For this drawer those two lines are a nickname and an email: both
   short, and stacking them spent a whole line of height plus the gap to say something that fits
   on one. Measured at 900px: 12 of 15 facts visible, the column cut off after First seen.

   ⚠ BOTH DRAWERS, BECAUSE IT IS ONE COMPONENT. `/bugs` has the identical head — a title over a
   `.modal-sub` — and 41.9 is the module that exists because `.drawer-dialog` was kept in one
   screen's stylesheet while two screens used it. Scoping this to the visitor drawer would be the
   same mistake with the geometry the other way round. */
/* ⚠ `--pad-row` IS NOT A LENGTH — it is `12px 16px`, a two-value shorthand (tokens.css:88). Written
   as `padding: 6px var(--pad-row)` it expanded to `6px 12px 16px`: top 6, sides 12, and a bottom of
   SIXTEEN. The head measured 55px when its tallest child is 32, and the extra sat under the
   controls, so the whole row read as sitting high in its own bar. A token whose name reads like a
   single value is exactly the kind that gets used as one — measured, not assumed.
   18px matches what `.drawer-head` used before this override, so the horizontal rhythm is
   unchanged and only the vertical shrinks. */
.drawer-dialog .drawer-head {
  padding: 6px 18px;
  align-items: center;
}

/* ⚠ AND THE TITLE CARRIES `margin-bottom: 6px`, WHICH IS WHAT PUT IT OFF-CENTRE. The boxes lined
   up — wrapper and buttons both centred at 22 — but the wrapper was 26px of which 6 was margin
   below the text, so the TEXT centred at 19 while the buttons centred at 22. Three pixels: enough
   for the owner to see it and hard to find by reading, because `align-items: center` was already
   there and doing its job on the boxes it was given. */
.drawer-dialog .drawer-head .card-title { margin: 0; }

/* Title and subtitle on ONE line. `baseline` rather than `center` so the small grey text sits on
   the title's own baseline instead of floating half a line above it; `wrap` so a long email on a
   narrow drawer goes under rather than squeezing the title to nothing. */
.drawer-dialog .drawer-head > div:first-child {
  display: flex; align-items: baseline; gap: var(--sp-3);
  min-width: 0; flex-wrap: wrap;
}

/* `.modal-sub` carries a top margin for the stacked case it was written for. */
.drawer-dialog .drawer-head .modal-sub { margin: 0; }

.drawer-dialog::backdrop { background: rgb(15 23 42 / 38%); }

/* ══ the head bar (38.2; shared at 43.14) ══════════════════════════════════
   The `@headbar` slot hoists a page's own title and controls into `.shell-head` beside the
   brandmark. It began on /visitors and lived in that page's sheet; the template library is the
   second caller, and a page sheet is loaded only on its own page — so these are shared now. */
.headbar { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.headbar-div { width: 1px; height: 18px; background: var(--hairline); margin: 0 var(--sp-2); flex: 0 0 auto; }
/* ⚠ THE PAGE'S ONE h1, RELOCATED - NOT A SECOND ONE. design-consistency asserts exactly one
   visible `.screen-title` and exactly one h1 per screen; it carries both here instead of in the
   content, so both counts are unchanged. The size comes down because a 28px heading in a 52px
   bar is not a heading, it is the bar. */
.headbar-title {
  margin: 0; font-size: var(--fs-md); font-weight: var(--fw-semibold);
  letter-spacing: var(--tr-lg); line-height: 1.2; white-space: nowrap;
}
.headbar-link {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 9px; border: 0; border-radius: var(--r-sm); background: none;
  font: inherit; font-size: var(--fs-sm); color: var(--ink-quiet);
  text-decoration: none; cursor: pointer; white-space: nowrap;
}
.headbar-link:hover { background: var(--neutral-bg); color: var(--ink); }
.headbar-link .rail-icon { width: 14px; height: 14px; }


/* == 49.7 - the two tiles a host filter cannot reach ============================== */
/* WARN: IN THE HEADER ROW, SO IT COSTS NO HEIGHT. The sentence this replaced took a whole line
   on a screen whose stated problem is not having enough vertical room (owner, 2026-09-02). */
/* WARN: INSIDE .kpi-k, NOT BESIDE IT. The tile is a flex COLUMN, so a sibling span became its own
   row - it pushed the value down and made these two tiles taller than the other four, which is a
   worse version of the line it replaced. Inside the label it is inline text. */
/* WARN: ABSOLUTE, BESIDE THE CHEVRON. Three placements were wrong before this one:
   a sentence above the strip (took a whole line, which the owner asked to remove);
   a sibling in normal flow (became its own row and made these two tiles taller);
   and inside .kpi-k (polluted the label, so `overview-screen` read "Attention timewhole
   site" and went red on the design's five keys). Out of the flow costs no height AND
   leaves the label text alone. button.kpi is already position:relative for .kpi-more. */
.kpi-wholesite {
  position: absolute; top: 8px; right: 32px;
  font-size: var(--fs-micro); pointer-events: none;
}


/* == 49.4 - the range panel's own footnotes ====================================== */
/* WARN: IN THE PANEL, NOT ABOVE THE TOOLBAR. These three sentences stood permanently above the
   controls and cost three lines on a screen short of vertical room (owner, 2026-09-02). Inside a
   popover that opens on demand they cost nothing and are read at the moment they matter. */
.dr-note { border-top: 1px solid var(--hairline); padding: var(--sp-3); display: grid; gap: var(--sp-2); }
.dr-note p { margin: 0; }

/* ── the visitors grid has no frame (redesign §3 Phase 3a step 1) ──────────────────────────────
   "No frame around the grid. The header hairline and row rules carry it."

   ⚠ A SEPARATE CLASS RATHER THAN AN OVERRIDE ON `.card`. `.card` is in components.css, which is
   GENERATED from the design's own sheet and must never be hand-edited — and nineteen other screens
   still want the frame it draws. This carries everything `.card` gives except the border, the
   radius and the shadow, so the grid keeps its surface and stops being a box. */
.gridframeless {
  background: var(--surface);
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
/* The card head's own bottom hairline stays: it is the "header hairline" the step names, and it is
   what separates the title row from the column bar now that no border does. */
.gridframeless > .card-head { border-bottom: 1px solid var(--hairline); }

/* ── the visitors grid's two new columns (redesign §2, phase 204) ──────────────────────────────
   The seven-column grid merged Time spent and the event count into ENGAGEMENT, and Bump into
   MESSAGES. Both are new `<col>` classes and both need a width, or the table's auto layout gives
   the two-line cells whatever is left over and the row heights go ragged.

   ⚠ HAND-OWNED, NOT `page-visitors.css`. That sheet is GENERATED by `dotnet/tools/extract-css.py`
   and is never hand-edited — so these live here and must win on their own, which they do: nothing
   in the generated sheets defines either class.
   ⚠ AND THE WIDTHS ARE THE ONES THEY REPLACE, not new guesses. Engagement takes `.col-onsite`'s
   104px (the same duration, now with a count under it); Messages takes the old status column's
   space. Both were measured at 1280px, which is the width HANDOFF's acceptance names. */
/* ⚠ 92 AND 100, NOT 104 AND 112, AND THE NUMBERS WERE MEASURED RATHER THAN CHOSEN. Taking the
   retired columns' widths verbatim left the table 24px wider than its 928px wrapper at 1280px —
   which is the horizontal scroll step 1 of this phase exists to remove, arriving 24px at a time.
   Both cells are `nowrap` two-liners whose longest strings are a duration ("1h 2m") over a count
   ("1 event") and a badge over "none yet"; neither needs more. */
.col-engage { width: 92px; }
.col-msgs   { width: 100px; }

/* 215.4 — the row's hover Send button (27.4) is gone: a send starts in the drawer. Its rules went with it. */

/* ── the visitors filter rail: capped, with its own scroll (redesign §3 Phase 3a step 4) ───────
   "Filter rail: sticky under the frozen head, capped to viewport height, its own scroll with
   `overscroll-behavior: contain`."

   ⚠ THERE IS NO RULE HERE FOR THE FIRST TWO, AND WRITING ONE IS WHAT BROKE THE RAIL. The pane is
   NOT inside `.content`: `_layout.html` puts `{{PANE}}` as a sibling of it, both direct flex
   children of `div.shell-main` — which is `flex: 1; min-height: 0` under a shell that never
   scrolls. So the pane is ALREADY the full height of the flex line (measured: 848px in an 848px
   row at a 900px viewport), and `page-visitors.css` already spends that height correctly —
   `.pane.filterpane { display: flex; flex-direction: column; min-height: 0 }` over
   `.filterpane-scroll { flex: 1; overflow-y: auto; min-height: 0 }`. Capped and self-scrolling,
   since 38.2.
     · `position: sticky` is inert on a child of a container that does not scroll. Nothing to
       stick to; the pane does not move because it is not in the scroller.
     · `max-height: calc(100dvh - …)` is measured against the VIEWPORT, which is 52px taller than
       the flex line the pane occupies — so it can only ever be looser than the cap already in
       force, never tighter.
     · and `align-self: flex-start`, which I added with them, ACTIVELY REMOVED the cap: it opts the
       pane out of `stretch`, so it sizes to its content instead of to the line and the scroller's
       `flex: 1` resolves to exactly the content height. `scrollHeight === clientHeight`, a
       scroller that never scrolls, from a rule written to make it scroll.
   The requirement was already met by the layout; what it needed was to be left alone. Recorded at
   this length because the three rules read like the requirement quoted above almost word for word,
   and the next person to compare that sentence against this file will reach for them again.

   ⚠ `overscroll-behavior: contain` IS THE ONE THING THE STEP NAMES THAT WAS ACTUALLY MISSING, and
   it is kept: scrolling past the end of the filter list must not hand the gesture to whatever is
   behind it. It is cheap and it is stated in the spec; it stays whether or not today's shell
   happens to give the chain anywhere to go. */
.filterpane-scroll { overscroll-behavior: contain; }

/* ── N04 · the filter glyph on a filtered column header (redesign §3 Phase 3a step 7) ──────────
   "Marks which columns a filter is acting on." It sits beside the sort glyph, and it has to read
   as a state marker on a header rather than as a control: no box, no hover, nothing to click. The
   header is not interactive on this grid — the sort is fixed — so anything that looked pressable
   would be a promise the screen cannot keep.
   ⚠ `currentColumn`-coloured rather than accent-coloured, at the header's own weight. An accent
   funnel beside a muted arrow reads as the louder of the two, and the arrow is the one carrying
   the sort — the fact the reader needs more often. */
.th-filtered {
  vertical-align: -1px;
  margin-left: 2px;
  color: var(--ink-quiet);
  flex: none;
}

/* ── the active-filter chips above the visitors grid (redesign §3 Phase 3a step 7) ─────────────
   ⚠ NEW CLASS NAMES, AND `.chip` WAS CHECKED FIRST. This codebase already has a `.chip` family —
   `.chips`, `.chip-tag`, `.chip-x`, `.chip-input` — and it belongs to the automations webhook
   editor: a bordered box of monospace header names, in `page-automation.css`, which /visitors does
   not load. Reusing the name would have meant a class that styles nothing here and collides
   everywhere else. (`grep -c "\.NAME[{ ,:]" dotnet/web/dist/assets/*.css` before writing a class —
   CLAUDE.md's rule, and the reason this row is not called `.chips`.)
   The prototype draws them as accent-tinted pills, 12px, 6px apart, 14px clear of the grid. */
.chiprow {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  margin-bottom: var(--sp-3);
}
.filterchip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-xs); line-height: 1.4;
  padding: 4px 10px; border-radius: var(--r-full);
  background: var(--accent-tint); color: var(--accent-text-on-tint);
  text-decoration: none; border: 0;
}
/* The shared `a` rule underlines every anchor and paints it accent-coloured; these are pills, and
   an underline through a filled pill reads as a mistake. Hover earns the underline back so the
   chip still says it is a link. */
.filterchip:hover { text-decoration: underline; }
.filterchip-x { font-size: var(--fs-sm); line-height: 1; opacity: .7; }
.filterchip-clear {
  font-size: var(--fs-xs); margin-left: var(--sp-1);
}

/* ── N03 · the bulk action bar (module 204.7) ──────────────────────────────────────────────────
   ⚠ `.bulkbar` AND `.bulkbar-count` ARE THE DESIGN'S OWN AND ARE NOT REDEFINED HERE — they are in
   the extracted `components.css` (accent wash, a bottom hairline, space-between, wrapping). This
   file adds only the two things the design's own bar has no markup for: a group for the actions,
   and the page-scope sentence.
   ⚠ `flex-basis: 100%` RATHER THAN A SECOND CONTAINER. `.bulkbar` already wraps, so a full-basis
   child takes its own line without another nesting level fighting the design's `space-between` —
   and the sentence stays inside the accent wash, which is what makes it read as part of the
   selection rather than as a note about the grid. */
.bulkbar-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.bulkbar-note { flex-basis: 100%; margin: 0; }

/* ⚠ THE CHECKBOX COLUMN IS SIZED BY ITS `<col>`, NOT BY ITS CELLS. Every other column in this
   grid is, the table is `table-layout: fixed`, and a cell-sized one would change width between the
   loading skeleton (which clones the colgroup) and the loaded grid — the seven columns beside it
   would shift sideways as the rows arrived. */
.col-check { width: 34px; }
/* Centred both ways so the box lines up with the header's, and `line-height: 0` because a label
   wrapping a 15px input still inherits the row's 1.5 line box and would make the checkbox cell
   taller than the two-line cells beside it.
   ⚠ AND ITS OWN PADDING, BECAUSE THE ROW'S IS 16px A SIDE. Thirty-two of this column's thirty-four
   pixels were padding and the 15px box was overflowing a 2px content box — visible only as a
   checkbox sitting a little left of its header's. A control column is not a text column and does
   not want a text column's gutters. */
.col-check-h, .col-check-cell {
  text-align: center; line-height: 0; padding-left: var(--sp-4); padding-right: var(--sp-1);
}
.col-check-cell .check, .col-check-h .check { gap: 0; }

/* ⚠ THE CHECKBOX COLUMN HAS TO BE PAID FOR, AND THIS IS WHERE THE 34px COMES FROM. The grid's
   width budget at 1280 is EXACT: 204.2 dropped nine columns to seven precisely to stop the
   sideways scroll CLAUDE.md lists as an open owner question, and it landed the table at 928px
   inside a 928px wrapper. Adding a column overflowed by exactly its own width, and both
   `visitors-layout` ("the row highlight reaches both content edges") and `row-bump` ("Send bump is
   reachable at scrollLeft 0") went red on the same 34px.
   ⚠ CURRENT PAGE IS THE COLUMN THAT GIVES IT UP, and its own note in the generated sheet is the
   argument: `width:auto` "so it yields to the visitor column rather than pushing the numeric
   columns off a narrow screen", and "the URL already truncates with its full value on hover". The
   min-width is only what stops it yielding further; 128px still shows a readable prefix, and it is
   the one cell here with no natural length. The alternative was the visitor column's 208px, which
   exists so a nickname AND an email both fit — shrinking that would truncate identities to buy a
   checkbox.
   ⚠ HIGHER SPECIFICITY DELIBERATELY: `page-visitors.css` loads AFTER this file, so a bare
   `.col-page` here would lose. Scoped to the grid's own wrapper rather than made `!important`.
   ⚠ AND THE OVERRIDE LIVES HERE RATHER THAN IN THAT FILE FOR A SECOND REASON: `page-*.css` still
   carries the extractor's "Generated; do not hand-edit" header, the extractor is gone, and both
   agents edit those files — so it is a shared file that collides. This one is hand-owned and my
   log names it. (The 204.7 note said "GENERATED" as the reason; that was only half of it.) */
.table-wrap-scroll .col-page { min-width: 128px; }


/* ══ redesign PHASE 3b · the visitor panel ═════════════════════════════════════════════════════
   TESTS 3.21/3.22: "1000px panel opens" and "shrink the viewport to 1280px with the panel open —
   the panel holds 1000px (`flex: none`), it does not shrink".

   ⚠ A LENGTH, NOT A PERCENTAGE, AND THAT IS THE WHOLE OF 3.22. `--drawer-w: 82vw` measured 1312px
   at 1600 and 1050 at 1280 — it tracked the viewport, which is exactly the shrinking the redesign
   is ruling out. HANDOFF §"Right-hand panels" gives the number: "820–1000px (visitor). Panels use
   `flex: none` so they hold their width instead of shrinking with the viewport."
   ⚠ `min(…, 100vw)` KEEPS IT HONEST BELOW 1000px. A flat 1000px would overflow a phone and put the
   close button off screen; the cap only ever binds where the panel could not fit anyway, so at
   1280 — the viewport 3.22 names — it is 1000px exactly.
   ⚠ HIGHER SPECIFICITY THAN `page-visitors.css`'s `.drawer-dialog { --drawer-w: 82vw }`, which
   loads after this file. The attribute is the panel's own hook, so it cannot drift onto the bug
   drawer, which sets its own width one rule up. */
.drawer-dialog[data-visitor-drawer] { --drawer-w: min(1000px, 100vw); }

/* The eight tabs must sit on ONE line at that width (TESTS 3.23). They are `.tabs`/`.tab` from the
   design; all this adds is the rule that the strip does not wrap — a second row of tabs inside a
   panel reads as two groups of tabs. `overflow-x: auto` rather than a squeeze: if a translation
   ever makes eight labels genuinely too wide, scrolling is honest and shrinking the text is not. */
.drawer-dialog[data-visitor-drawer] .tabs {
  flex-wrap: nowrap; overflow-x: auto; scrollbar-width: thin;
  /* 216.5 — the Windows screenshot showed an UP/DOWN bar on this strip: `overflow-x: auto` makes the other axis
     auto as well, and the tab's underline overhangs by a pixel. The strip only ever scrolls sideways. */
  overflow-y: hidden;
}
.drawer-dialog[data-visitor-drawer] .tab { white-space: nowrap; }

/* ══ 216.8 — TWO SCROLLS, NOT ONE (Changes 02, phase 2) ════════════════════════════════════════════════════════════
   "The left-hand side … should have its own scroll. The tab side should also have its own scroll. Moreover, make the
   tabs sticky on top … make the visitor name sticky on the left-hand side." The body stops scrolling; the two columns
   each fill its height and scroll on their own, and neither hands its scroll to the other or to the page behind
   (`overscroll-behavior`). The tab strip and the Visitor fact stick to the top of their own column. */
.drawer-dialog[data-visitor-drawer] .drawer-body {
  overflow: hidden; display: flex; flex-direction: column; padding-bottom: 0;
}
.drawer-dialog[data-visitor-drawer] .vd-grid { flex: 1; min-height: 0; align-items: stretch; }
.drawer-dialog[data-visitor-drawer] :is(.vd-side, .vd-main) {
  overflow-y: auto; min-height: 0; overscroll-behavior: contain;
}
/* ⚠ THE COLUMNS' TOP PADDING MOVES INTO THE PINNED ITEMS. A sticky box stops at the scroller's padding edge, so with
   the columns' 8px / 16px top padding the tabs and the name pinned that far down and the rows scrolled past showed
   through the strip above them. Padded from inside instead, they sit flush and cover it. */
.drawer-dialog[data-visitor-drawer] .vd-main { padding-top: 0; padding-bottom: var(--sp-5); }
.drawer-dialog[data-visitor-drawer] .vd-main > .tabs {
  position: sticky; top: 0; z-index: 2; background: var(--surface); padding-top: 8px;
}
.drawer-dialog[data-visitor-drawer] .vd-side { padding-top: 0; }
.drawer-dialog[data-visitor-drawer] .vd-facts > .vd-fact:first-child {
  position: sticky; top: 0; z-index: 1; background: var(--surface); padding-top: 25px;
}

/* ── the Journeys tab's step boxes (3b step 9; TESTS 3.34-3.36) ────────────────────────────────
   "Step boxes with timestamps, no arrows-only diagrams, no step numbers, no DONE/NEXT labels."
   ⚠ NO COUNTER, NO ARROW, NO STATUS WORD — the three things the redesign names are the three
   things this does not draw. A completed step is told by its TIME; that is the point of the tab. */
.vd-journey { display: flex; flex-direction: column; gap: var(--sp-2); }
.vd-journey-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.vd-steps { display: flex; flex-direction: column; gap: var(--sp-2); }
.vd-step {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  border: 1px solid var(--hairline); border-radius: var(--r-md);
  padding: 9px 12px; background: var(--surface);
}
.vd-step.is-done { border-color: var(--accent-border); background: var(--accent-wash); }
.vd-step-name { font-size: var(--fs-sm); color: var(--ink-body); min-width: 0; }
.vd-step-when {
  font-size: var(--fs-xs); color: var(--ink); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* `Not yet` is muted, not hidden — "the gap is the useful part". */
.vd-step-when.is-none { color: var(--ink-quiet); }
.vd-step-back { margin-left: var(--sp-2); }


/* ── N06 · the event detail line (`selector · text`) ──────────────────────────────────────────
   ⚠ MIDDLE TRUNCATION WITH THE WHOLE STRING STILL IN THE DOM. The head is allowed to ellipsise
   and the tail is not, so what the browser drops comes out of the middle — and because both
   halves are real text rather than a JS-shortened copy, selecting the line and copying it still
   yields every character. `min-width: 0` on the head is what lets a flex child shrink below its
   content; without it the line pushes the panel sideways instead, which is acceptance point 4
   ("a 200-character selector does not introduce horizontal scroll"). */
.vd-ev-detail {
  display: flex; align-items: baseline; min-width: 0;
  font: var(--font-mono, 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace);
  color: var(--ink-3); margin-top: 2px;
}
.vd-ev-detail .mid-head {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 0 1 auto;
}
.vd-ev-detail .mid-tail { flex: none; white-space: nowrap }


/* ── N07 · the pin is a HOVER affordance on the event row ─────────────────────────────────────
   SPEC-NEW-FEATURES N07: "the pin toggles on hover, which is what the Pinned-only filter reads."
   Two controls printed permanently beside every event in a fifty-event feed are fifty pairs of
   buttons competing with the thing the operator came to read.
   ⚠ `:focus-within` IS NOT OPTIONAL HERE, AND IT IS THE HALF THAT GETS FORGOTTEN. A control
   revealed only by `:hover` cannot be reached by keyboard at all — it is not merely awkward, it
   is unreachable — so the row also opens when anything inside it takes focus. Both the gate's
   hover leg and its focus leg exist because either rule alone ships a broken screen.
   ⚠ AND IT IS `opacity`, WHICH THE FIRST VERSION GOT BACKWARDS. `visibility: hidden` was the
   obvious choice — it hides the control AND takes it out of the tab order — and the second of
   those is exactly the bug: a pin removed from the tab order cannot be reached by keyboard AT
   ALL, so `:focus-within` can never fire from the button itself and the rule that was supposed to
   rescue keyboard users is unreachable code. `opacity: 0` keeps the button focusable, so Tab lands
   on it, `:focus-within` fires, and it appears — which is the only sequence in which a keyboard
   user ever sees it. It also holds the row's height steady, which `display: none` would not.
   ⚠ THE COST IS THAT AN INVISIBLE BUTTON IS STILL CLICKABLE, and that is acceptable here: a mouse
   pointer over the button is a pointer over the row, which has already revealed it.
   ⚠ AND "IS IT ON SHOW" MUST THEN BE MEASURED AS OPACITY, not as `isVisible()` — an opacity-0
   element reports visible to every layout-based check there is. The gate reads the computed value
   for exactly this reason; reading visibility instead is the proxy-for-a-property mistake this
   repo keeps making, and it passed on an always-visible pin when it was written that way. */
.vd-ev .row-actions { opacity: 0; }
.vd-ev:hover .row-actions,
.vd-ev:focus-within .row-actions { opacity: 1; }


/* ── TESTS 3.31 · the Activity tab's two filters, each stating N of M ─────────────────────────
   Side by side, wrapping on a narrow panel. The count sits with its own label rather than in a
   line of its own, because "3 of 14" beside `Pinned only` is a fact ABOUT that checkbox and a
   separate line would make it a fact about the tab. */
.vd-filters { display: flex; gap: var(--sp-4); flex-wrap: wrap; align-items: center; }
.vd-filter-n {
  font-variant-numeric: tabular-nums; color: var(--ink-quiet); font-size: var(--fs-xs);
}

/* ── N08 · the dictation control (module 204.12) ──────────────────────────────────────────────
   A mic beside the box, a hint line that changes while it is recording, and — only while it is —
   a pulsing ring and a seven-bar waveform.

   ⚠ THE RING AND THE BARS EXIST ONLY WHILE RECORDING, IN THE DOM AND NOT MERELY IN OPACITY.
   Drawn always and hidden with `visibility`, they would be a claim that something is being heard
   whenever a stylesheet failed to load, and a gate could not tell the two states apart at all. */
.dictate {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-2);
}
.dictate .field-hint { margin: 0; min-width: 0; }
/* 215.9 — the language dictation listens for, beside the mic. As wide as its longest name. */
.dictate .dictate-lang { width: auto; flex: none; max-width: 14rem; }

.mic {
  position: relative; flex: none; width: 30px; height: 30px; padding: 0;
  border: 1px solid var(--hairline); border-radius: 999px; background: var(--surface);
  color: var(--ink-body); display: inline-grid; place-items: center; cursor: pointer;
}
.mic:hover { background: var(--neutral-bg); color: var(--ink); }
.mic.is-live {
  background: var(--accent-action); border-color: var(--accent-action); color: var(--accent-on-fill);
}

/* The ring pulses OUTWARD from the button and never moves it: it is a positioned overlay with no
   layout of its own, so a note composer does not jump 6px the moment recording starts. */
.mic-ring {
  position: absolute; inset: -4px; border-radius: 999px;
  border: 2px solid var(--accent-action); pointer-events: none;
  animation: mic-pulse 1.4s ease-out infinite;
}
@keyframes mic-pulse {
  0%   { transform: scale(0.86); opacity: 0.9; }
  70%  { transform: scale(1.22); opacity: 0; }
  100% { transform: scale(1.22); opacity: 0; }
}

.wave { display: inline-flex; align-items: center; gap: 2px; height: 18px; flex: none; }
.wave > i {
  display: block; width: 2px; height: 4px; border-radius: 1px; background: var(--accent-action);
  animation: mic-wave 0.9s ease-in-out infinite;
}
@keyframes mic-wave {
  0%, 100% { height: 4px; }
  50%      { height: 16px; }
}

/* ── TESTS 3.41 · the anonymous visitor's Identities tab ──────────────────────────────────────
   A real empty state, not a grey line: a title, what it costs, the two ways in, and the line of
   code that makes it happen. */
.empty-title { margin: 0; font-weight: 600; color: var(--ink); }
.empty-ways {
  margin: 0; padding-left: var(--sp-5); color: var(--ink-body);
  font-size: var(--fs-xs); line-height: 1.6;
}
.empty-ways li + li { margin-top: var(--sp-2); }

/* The snippet and its Copy sit on one line and the snippet takes the slack — a code line that
   wrapped mid-string would be copied by hand wrongly, which is the failure this button exists to
   prevent. */
.snippet-row { display: flex; align-items: center; gap: var(--sp-3); }
.snippet-row > code {
  flex: 1 1 auto; min-width: 0; overflow-x: auto; white-space: nowrap;
  background: var(--neutral-bg); border: 1px solid var(--hairline);
  border-radius: var(--r-md); padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-xs); color: var(--ink);
}


/* ══ 204.16 — the visitor's activity feed, moved here from `page-visitors.css` ════════════════
   TESTS 3.44 asks the visitor PAGE for the same sections as the panel, and `Gump.activityFeed`
   is the one renderer both surfaces mount. `visitor.html` loads `page-visitor.css`, not
   `page-visitors.css`, so a shared control whose styles live in one page's sheet renders
   unstyled on the other — the same reason 38.18 moved `.place`, `.flag` and `.cc-chip` here. */

/* One entry in the Activities feed. */
.vd-act {
  padding: var(--sp-4) 0; border-bottom: 1px solid var(--hairline);
  display: flex; justify-content: space-between; gap: var(--sp-4);
}

/* ⚠ THE ROW'S BODY TAKES THE WIDTH, AND UNTIL 42.4 NOTHING NOTICED IT DID NOT. `.vd-act` is a
   flex row whose body holds the title, the disclosure AND the events panel — and with no `flex` on
   it that child sized to its own content, so the expanded event rows were as wide as their longest
   label instead of as wide as the panel. Invisible while an event row was one line of text;
   obvious the moment 42.4 gave it a right-hand column of controls, which then sat in the middle of
   the drawer with empty space beyond it.

   ⚠ AND THIS WAS `> div:first-child` UNTIL 42.11 PUT AN ICON IN FRONT OF IT. The body stopped
   being the first child, so it lost its `flex` — and `.vd-act` is `justify-content: space-between`,
   which then threw the three children to the left, middle and right of the panel with two lakes of
   whitespace between them. Selecting the body by WHAT IT IS NOT (the fixed-width time column)
   survives anything else being prepended to the row. */
.vd-act > .rank-icon { flex: 0 0 auto }
.vd-act > div:not(.vd-act-when) { flex: 1 1 auto; min-width: 0 }

.vd-act:last-child { border-bottom: 0; }
/* QA E002 — a message row opens that message's details: it looks like something that does. */
.vd-act-open { cursor: pointer; }
.vd-act-open:hover { background: var(--surface-sunken); }

.vd-act-url {
  font-size: var(--fs-xs); color: var(--ink-quiet); word-break: break-all;
}

.vd-act-when {
  font-size: var(--fs-xs); color: var(--ink-quiet); white-space: nowrap; text-align: right;
  flex: 0 0 auto;
}

/* 42.10 — the exact timestamp under the relative one. Lighter again, because "5 mins ago" is the
   answer and the date qualifies it — the same weight relationship 42.1 gave the facts column. */
.vd-act-when-exact { display: block; margin-top: 2px; opacity: .75; }

/* The visit count sits with the page it counts, on its own line under the address, so the row
   reads title → where → how many. `inline-flex` so the chip hugs its text instead of stretching
   to the width of the URL above it. */
.vd-visits { display: inline-flex; margin-top: var(--sp-2); }
/* 215.9 — Top pages: the visits and the time spent, side by side under the address (brief 09). */
.vd-page-stats { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.vd-page-stats .vd-visits { margin-top: 0; }

/* ══ the Activities accordion (27.7) ════════════════════════════════════════════════════════
   "3 events captured" was a badge - a count with nothing behind it. It is now a disclosure
   button, so it looks like the control it always should have been: pointer, caret, and a
   focus ring, because it is reachable by keyboard like any other button.
   The disabled state is real: a visit with zero events has nothing to open, and a toggle that
   opens onto "nothing here" is worse than one that says it is empty by being unavailable. */
.vd-exp {
  appearance: none; display: inline-flex; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-2); padding: 2px 8px; border: 1px solid var(--hairline);
  border-radius: var(--r-full); background: var(--surface-sunken);
  font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--ink-body); cursor: pointer;
}

.vd-exp:hover:not(:disabled) { border-color: var(--accent-border); color: var(--accent-text); }
.vd-exp:disabled { cursor: default; color: var(--ink-quiet); opacity: 0.7; }
.vd-exp-caret { font-size: 13px; line-height: 1; }

/* The events themselves, indented under the visit they belong to so the nesting is visible
   without a second border - the panel is already inside a bordered .vd-act.
   ⚠ TESTS 3.25 — PAST EIGHT EVENTS THE LIST SCROLLS INSIDE THE VISIT, at 328px. Without it a
   visit with forty events pushes every LATER visit off the screen, and the Activity tab stops
   being a feed you can scan: "the next visit is still on screen" is the whole point of the number.
   A short visit is shorter than 328px and is unaffected. */
.vd-evs {
  margin-top: var(--sp-3); padding-left: var(--sp-4);
  display: grid; gap: var(--sp-3);
  max-height: 328px; overflow-y: auto;
}
/* 215.8 — "Show more" lifts the cap for one visit; "Show less" puts it back (owner's change brief, 08). */
.vd-evs.is-expanded { max-height: none; overflow: visible; }
.vd-evs-more { display: block; margin: var(--sp-2) 0 0 var(--sp-4); }

/* ⚠ TESTS 3.26 — THE CONNECTOR RUNS BEHIND THE ICONS, NOT THROUGH THEM. It used to be a
   `border-left` on this container: one unbroken rule down the far left, beside the icons rather
   than joining them, which is a margin and not a timeline.
   ⚠ AND IT IS DRAWN PER ROW, NOT ONCE ON THE CONTAINER, because this container SCROLLS now. An
   absolutely-positioned line on a scroll container is sized to the visible box and stays put while
   the content moves, so it would end in mid-air after one wheel notch. Each row carrying its own
   segment scrolls with the row, which is the only version that survives 3.25.
   The `box-shadow` ring is what makes it stop AT each icon: a halo in the panel's own background
   colour, painted over the line, so the line reads as passing behind the icon rather than through
   it. Nothing is hidden and no segment is missing — the icon simply occludes it. */
.vd-ev { position: relative; }
.vd-ev::before {
  content: ''; position: absolute; left: 10px; width: 2px; background: var(--hairline);
  top: calc(var(--sp-3) * -1); bottom: calc(var(--sp-3) * -1); z-index: 0;
}
.vd-ev:first-child::before { top: 11px; }
.vd-ev:last-child::before { bottom: auto; height: 11px; }
.vd-ev > * { position: relative; z-index: 1; }
.vd-ev .rank-icon { box-shadow: 0 0 0 3px var(--surface); }

/* ⚠ 42.4 — THREE TRACKS, AND THE FIRST IS THE ICON. It was `1fr auto auto` — body, time, controls
   — which left the name barely half the panel on a narrow drawer. The time moved under the name
   (point 1: "an icon, a page, and a time displayed below it"), so the column it occupied is now
   the icon's, and the body gets the width back. */
.vd-ev {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-3);
  align-items: start;
}

/* The controls ride in the name row (see the note in vdEventRow), so they must not stretch it. */
.vd-ev-name .row-actions { margin-left: var(--sp-2); flex: 0 0 auto }

/* The exact time, under the name (215.8 took the page off — the visit above already names it). Same size and colour as `.vd-act-url` beside it —
   they are the same kind of line, and two greys would read as a distinction that is not there. */
.vd-ev-meta {
  font-size: var(--fs-xs); color: var(--ink-quiet); margin-top: 2px; overflow-wrap: anywhere;
}

.vd-ev-name { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.vd-ev-main { min-width: 0; }

/* 215.8 — the rename editor that lived inside the event's row is gone: the owner's change brief (08)
   asked for the rename in a pop-up, and a second click had opened a second editor for the same name. */

/* The 29.5 filter sits above the feed it filters, reading as one line: a checkbox and its words.
   `.field-hint` gives it the muted weight of a control that qualifies what is below rather than
   commanding it. */
.vd-onlyev { display: flex; align-items: center; gap: var(--sp-2); cursor: pointer; }

/* ══ 205.5 · the message gallery — SHARED, because it is now drawn on two screens ══════════════
   COPY — design content.html, verbatim from `page-content.css` (the extracted sheet), which is
   generated and must not be hand-edited. SPEC-SCREENS §8 tab 2 puts the gallery inside the
   Campaigns area, and /automations does not load /content's sheet — so the block moves DOWN a
   layer rather than being duplicated into a second page sheet. page-content.css still defines
   these and still loads after this file, so /content is byte-for-byte unchanged; when route
   folding (TESTS 4.1) retires that screen its copy goes with it and this one is already the home.
   ⚠ `.vchip` is deliberately NOT copied: the gallery card has no version chip. The design puts a
   channel tag in that slot, and `v3` beside a name means nothing to somebody choosing a picture. */
.grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: var(--sp-4);
}
.ccard {
  background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r-lg);
  overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow-card);
}
.thumb {
  height: 130px; background: #f7f7f7; position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  border-bottom: 1px solid var(--hairline-soft);
}
.thumb-frame {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
}
.thumb-scrim { background: rgba(10, 37, 64, .35); }
.ccard-body {
  padding: var(--pad-card); display: flex; flex-direction: column; gap: var(--sp-2); flex: 1;
}
.ccard-name {
  font-size: var(--fs-md); font-weight: var(--fw-semibold); color: var(--ink);
  display: flex; align-items: baseline; gap: var(--sp-2); justify-content: space-between;
}
.ccard-stat { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--ink-body); }
.ccard-stat-quiet { font-family: var(--font-sans); font-size: var(--fs-xs); color: var(--ink-quiet); }
.ccard-foot {
  padding: 10px var(--pad-card); border-top: 1px solid var(--hairline-soft);
  background: var(--surface-sunken); display: flex; gap: var(--sp-4); align-items: center;
  flex-wrap: wrap;
}

/* COPY — design admin.html (`page-admin.css`), which styles `.seg .segbtn` for BUTTONS. The
   gallery's segments are LINKS, because each one is its own URL: `?tab=messages&ch=banner` is
   shareable, survives the trip out to the editor and back, and needs no handler. The rule is the
   admin sheet's own, with the two things an anchor needs that a button does not — the shared `a`
   rule underlines every anchor and paints it accent-coloured, and a segmented control with an
   underline through it reads as a mistake. */
.seg a.segbtn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  background: var(--surface); border: 0; border-right: 1px solid var(--hairline);
  font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--ink-body); padding: 8px 11px; text-decoration: none; white-space: nowrap;
}
.seg a.segbtn:last-child { border-right: 0; }
.seg a.segbtn:hover { text-decoration: none; background: var(--surface-sunken); }
.seg a.segbtn[aria-current="true"] { background: var(--accent-tint); color: var(--accent-text-on-tint); }

/* ══ 205.6 · the image library dialog — SHARED, for the same reason as the gallery above ═══════
   COPY — design content.html, verbatim from the generated `page-content.css`. SPEC-SCREENS §8's
   Dialogs list settles the shape outright — "A **dialog**, reachable from the Messages tab and
   from inside the editor — no separate page" — and /automations does not load /content's sheet.
   `.imgtile` and `.imgtile-box` already have their AUTHOR half in this file (the picture sizing,
   and the button reset the frame's <div> does not need); what was missing is the frame's own
   geometry, which lived one layer up. */
.quota { display: flex; flex-direction: column; gap: var(--sp-2); }
.quota-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  font-size: var(--fs-xs); color: var(--ink-quiet);
}
.imggrid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: var(--sp-3);
}
.imgtile {
  border: 1px solid var(--hairline); border-radius: var(--r-md); overflow: hidden;
  background: var(--surface-sunken); display: flex; flex-direction: column;
}
.imgtile-box {
  height: 72px; display: grid; place-items: center; color: var(--ink-quiet);
  border-bottom: 1px solid var(--hairline-soft);
}
.imgtile-name {
  padding: 6px 7px; font-family: var(--font-mono); font-size: var(--fs-micro);
  color: var(--ink-quiet); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* AUTHOR — the drop zone. SPEC-SCREENS §8's dialog copy is "Drop images here, or choose files",
   which the /content version never offered: it had a bare <input type=file> and nothing to drop
   onto. The zone IS the label for that input, so the keyboard path is unchanged and the drop is
   an addition rather than a replacement. */
.dropzone {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding: var(--sp-4); text-align: center; cursor: pointer;
  border: 1px dashed var(--hairline-strong); border-radius: var(--r-md);
  background: var(--surface-sunken); color: var(--ink-body); font-size: var(--fs-xs);
}
.dropzone:hover { border-color: var(--accent-border); }
.dropzone.is-over { border-color: var(--accent-fill); background: var(--accent-tint); }
.dropzone input[type="file"] { position: absolute; opacity: 0; width: 1px; height: 1px; }
.dropzone-lead { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--ink); }

/* ══ 205.8 — a FACTS BAND, promoted because a second screen now draws one ═════════════════════
   COPY of `page-visitor.css`'s `.facts / .fact / .fact-k / .fact-v / .fact-null`, byte-for-byte,
   moved here rather than duplicated under a new name. `page-visitor.css` is GENERATED from the
   design's own document and says "do not hand-edit" at the top, so the block cannot be cut out of
   it; it is also loaded on exactly one page, which is why the event detail panel on /automations
   could not reach it. That sheet still wins on /visitor — it loads after this one and declares the
   identical rules — so nothing about that screen changes.
   Same move, and the same reason, as 41.9's `.drawer-dialog`: a component kept in one screen's
   stylesheet while two screens use it. */
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-4) }
.fact { display: flex; flex-direction: column; gap: 2px; min-width: 0 }
.fact-k {
  font-size: var(--fs-micro); letter-spacing: var(--tr-micro); text-transform: uppercase;
  color: var(--ink-quiet); font-weight: var(--fw-semibold);
}
.fact-v { font-size: var(--fs-sm); color: var(--ink); overflow: hidden; text-overflow: ellipsis }
.fact-null { color: var(--ink-quiet) }

/* ══ 205.15 · two page-scoped patterns that now have a second caller ════════════════════════ */
/* Same move, and the same reason, as `.headbar` above: a page sheet is loaded only on its own
   page, so a class defined in one and used in another is simply undefined there — the journey
   editor's name input rendered at body size and its two lists carried browser bullets, which is
   what `coverage`'s "undefined CSS classes" measurement found. Lifted rather than copied: two
   editors whose heads are separately defined are two heads that drift. */
/* 218.7 (Changes 04 C7) — the campaign and journey editors' title is plain text repeating the name field, which is the
   form's first field. Was `.ced-name`/`.ced-nameinput`, the name typed into the title itself. */
.ced-title { margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 320px; }
.ced-namefield { min-width: 0; }
.ced-namefield > .input { max-width: 520px; }
.hr-list { margin: var(--sp-3) 0 0; padding: 0; list-style: none; max-width: 74ch; }
/* 213.19 — a failure on our side, in a region's error panel: the reference and its three companions, selectable whole. */
.crash-facts { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: var(--sp-1) var(--sp-3); }
.crash-facts dt { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--ink-body); }
/* 213.31 — offline and maintenance: a card over the page, never a scrim, so the page behind stays readable. */
.trouble-overlay { position: fixed; left: 50%; bottom: var(--sp-5); transform: translateX(-50%); z-index: 900; width: min(460px, calc(100vw - 32px)); display: grid; gap: var(--sp-2); padding: var(--sp-4); background: var(--surface); color: var(--ink); border: 1px solid var(--hairline-strong); border-radius: var(--r-lg); box-shadow: var(--shadow-overlay); }
.trouble-overlay p { margin: 0; }
.trouble-overlay-title { font-size: var(--fs-md); font-weight: var(--fw-semibold); color: var(--ink); }
.crash-facts dd { margin: 0; overflow-wrap: anywhere; font-size: var(--fs-sm); color: var(--ink); }

/* ══ 215.9 · THE VISITOR PANEL, SHARED ════════════════════════════════════════════════════════
   Moved from page-visitors.css: the full visitor page mounts the same panel (Gump.visitorDetail) and
   the owner asked for the two to be exactly the same, so its rules can no longer belong to one page. */

/* Facts on the left, tabs and their panel on the right, as the legacy screen had it. The facts
   column is fixed so the tab content does not reflow as values change length.
   ⚠ 204.9b — 300px LEFT THE EIGHT-TAB STRIP 35px SHORT AND IT SCROLLED. The panel is 1000px, so
   the content column got 1000 - 36 (body padding) - 300 - 24 (gap) = 640px for a strip that needs
   675.5. `row-bump` measures `scrollWidth <= clientWidth` and found it; my own tab legs asked for
   `one row of tabs`, which `flex-wrap: nowrap` guarantees whether the tabs fit or not — a proxy,
   not the property, and it read green over a clipped `Notes`. 240px gives the strip 700px, ~24px
   of headroom, and matches SPEC-SCREENS' own words for this column: "label in a NARROW left
   column, value and sub-line stacked". */
.vd-grid {
  display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: var(--sp-6); align-items: start;
}

.vd-facts {
  display: flex; flex-direction: column;
  border: 1px solid var(--hairline); border-radius: var(--r-lg); padding: var(--sp-4);
  background: var(--surface-sunken);
}

/* ⚠ 42.1 — THE FACTS ARE SEPARATED BY A RULE, NOT BY A GAP. The reference divides them, and a
   column of twelve facts held apart only by whitespace makes a two-line fact ("Navi Mumbai" over
   "India") look like two one-line facts. The line is what says where one ends. */
.vd-fact {
  padding: var(--sp-3) 0; border-bottom: 1px solid var(--hairline);
}

.vd-fact:first-child { padding-top: 0 }

.vd-fact:last-child { padding-bottom: 0; border-bottom: 0 }

/* One fact: a quiet label over its value, which is the shape the legacy panel used. */
.vd-fact-k {
  display: block; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-quiet);
  margin-bottom: 2px;
}

/* ⚠ 42.1 — THE VALUE CARRIES THE WEIGHT AND THE SUB-LINE DOES NOT. The owner's point 2 —
   "notice the usage of bold and regular font fields" — is about which of two stacked lines is the
   answer. Rendered at one weight, as this was, the reader has to work that out every time. */
.vd-fact-v {
  display: block; font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--ink);
}

.vd-fact-sub {
  display: block; font-size: var(--fs-xs); font-weight: var(--fw-regular);
  color: var(--ink-quiet); margin-top: 2px; overflow-wrap: anywhere;
}

/* 217.15 (Changes 03 C15) — the Message fact's "?": the button and its explanation, which shows on hover, on focus,
   and pinned open by a click or a tap (`is-open`); Esc or a click elsewhere puts it away (`is-dismissed` outranks the
   hover and focus that would otherwise keep it up). It spans the facts column, under the button, so it is never cut
   by the column's edge, and it is drawn over the facts below it. */
.vd-bump-act { display: inline-flex; align-items: center; gap: var(--sp-2); }
.vd-fact:has(.vd-help) { position: relative; }
.vd-help.hastip { position: static; }
.vd-help-btn {
  appearance: none; display: inline-grid; place-items: center; width: 18px; height: 18px; padding: 0;
  border: 1px solid var(--hairline-strong); border-radius: 50%; background: var(--surface);
  font: 600 11px/1 var(--font-body); color: var(--ink-quiet); cursor: help;
}
.vd-help-btn:hover, .vd-help.is-open .vd-help-btn { color: var(--accent-text); border-color: var(--accent-text); }
.vd-help .vd-help-tip {
  top: calc(100% + 2px); bottom: auto; left: 0; right: 0; transform: none; width: auto; max-width: none; z-index: 5;
  pointer-events: none; /* it explains; it never takes a click meant for the fact under it ("View on map") */
}
.vd-help .vd-help-tip::after { display: none; }
.vd-help.is-open .tip-bubble { opacity: 1; visibility: visible; }
.vd-help.is-dismissed .tip-bubble { opacity: 0; visibility: hidden; }

/* 217.14 (Changes 03 C14) — "View on map" under the place, and the pop-up's map. */
.vd-map-link { font-size: var(--fs-xs); margin-top: 2px; }
.vd-map-coords { display: block; font-size: var(--fs-xs); color: var(--ink-quiet); font-variant-numeric: tabular-nums; }
.modal:has(.vd-map-frame) { width: min(720px, calc(100vw - 32px)); max-width: none; }
.vd-map-frame { position: relative; width: 100%; aspect-ratio: 16 / 10; background: var(--surface-sunken); }
.vd-map-frame > iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* 42.13 — the rename editor, inside the Visitor fact rather than in a third stacked dialog. */
.vd-rename { margin-top: var(--sp-3); display: grid; gap: var(--sp-2); }

.vd-rename .row-actions { justify-content: flex-start; }

/* The fact's own control, against the fact it acts on (42.2). `align-items: center` rather than
   baseline: a two-line value would otherwise drag the button up against its first line. */
.vd-fact-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
}

.vd-fact-body { min-width: 0; flex: 1 1 auto }

.vd-fact-row > .btn { flex: 0 0 auto }

/* The delivery's outcome, beside its name rather than under it — it is one word and the sub-line
   below already carries the channel and the time. */
.vd-fact-badge { margin-left: var(--sp-2); vertical-align: middle }
/* 215.9 — a value with a status badge beside it (Live now, Signed out): one line, the badge outside the value. */
.vd-fact-v-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); min-width: 0 }
.vd-fact-v-row > .vd-fact-badge { margin-left: 0 }

/* ⚠ `placeCell` PUTS ITS SECOND LINE IN `.cell-sub`, WHICH HAS NO COLOUR OR WEIGHT OF ITS OWN
   (`app-components.css` defines it as `display: block` and nothing else). Inside a value that is
   now semibold it would inherit the bold and read as a second answer, so the country line is
   given the same treatment as any other sub-line here. */
.vd-fact-v .cell-sub {
  font-size: var(--fs-xs); font-weight: var(--fw-regular); color: var(--ink-quiet);
}

.vd-main {
  min-width: 0;                     /* or a long URL in the feed widens the grid instead of wrapping */
}

/* 42.12 gave the tab strip a bottom margin, so this would otherwise be twice the gap it was. */
.vd-panel {
  padding-top: 0;
}

/* ══ the drawer's AI tabs (29.1) ════════════════════════════════════════════════════════════
   An answer is prose, not a row: it wraps, it can be long, and it needs reading width. The
   activity rows above use a three-column grid because they are scannable facts; these are not,
   so they get a block with the head above the body rather than beside it. */
.vd-ai {
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--hairline);
}

.vd-ai:last-child { border-bottom: 0; }

.vd-ai-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-3); margin-bottom: var(--sp-2);
}

/* pre-wrap, because a model's answer arrives as lines and paragraphs and collapsing them turns a
   list of five buttons into one run-on sentence. It is INSERTED AS TEXT, never as html — this is
   third-party output rendered inside the dashboard. */
.vd-ai-body {
  white-space: pre-wrap; font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-body);
  max-width: 78ch;
  /* 215.9 — a pasted URL or order id is one unbroken word; it wraps rather than pushing the panel
     sideways (the visitor page's notes did this; the page now draws the drawer's). */
  overflow-wrap: anywhere;
}

/* A note is the operator's own writing; the accent edge is what tells it from a model answer at a
   glance in the shared feed. */
.vd-ai-note { border-left: 2px solid var(--accent-border); padding-left: var(--sp-3); }

/* ══ two-line facts in the drawer (29.2, superseded by 42.1) ════════════════════════════════
   29.2 stacked "Nothing in the last hour" over "Total 3 min" by reaching for `.cell-sub` inside
   the value — which rendered as one run-on string until this rule made it a block. 42.1 gives
   every fact a real sub-line (`.vd-fact-sub`), so the only `.cell-sub` left inside a value is the
   one `placeCell` writes, and it is styled with the rest of the facts above. This rule is kept
   for the block/margin it still supplies to that one case. */
.vd-fact-v .cell-sub { display: block; margin-top: 2px; }

/* The side column: the facts card, then the tag editor beneath it. One grid child, so the body
   stays the two columns it was designed as. */
.vd-side { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

/* Carded like the facts above it, so the side column reads as two blocks rather than as a card
   with some loose controls under it. */
/* 216.7 — the tag editor now sits INSIDE the facts list, under Visitor, so it is divided like a fact. */
.vd-facts > .vd-tags { padding: 9px 0 12px; border-bottom: 1px solid var(--hairline); }
/* 216.9 — "the copy button after the session code should have more margin at the bottom". */
.vd-fact:has(> .vd-fact-row [data-vd-copy-code]) { padding-bottom: 18px; }
.vd-tags {
  display: flex; flex-direction: column; gap: 6px;
  border: 1px solid var(--hairline); border-radius: var(--r-lg); padding: var(--sp-4);
  background: var(--surface-sunken);
}

.tagpick { display: flex; flex-wrap: wrap; gap: 5px; }

.tagchip {
  border: 1px solid var(--hairline); background: var(--surface); border-radius: 999px;
  padding: 3px 10px; font: inherit; font-size: var(--fs-xs); color: var(--ink-body); cursor: pointer;
}

.tagchip:hover { border-color: var(--hairline-strong); color: var(--ink); }

.tagchip.is-on {
  background: var(--accent-tint); border-color: var(--accent-tint);
  color: var(--accent-text-on-tint); font-weight: var(--fw-semibold);
}

.tagchip:disabled { opacity: .55; cursor: default; }

.tagnew { display: flex; gap: 6px; align-items: center; }

.tagnew .input { flex: 1 1 auto; min-width: 0; font-size: var(--fs-xs); padding: 6px 8px; }

/* The tag chips wrap like the drawer's; the count rides inside the pill. */
.tagpick { max-height: 168px; overflow-y: auto; }

.tagchip-n {
  margin-left: 6px; font-variant-numeric: tabular-nums; font-weight: var(--fw-regular);
  color: var(--ink-quiet);
}

.tagchip.is-on .tagchip-n { color: inherit; opacity: .75; }

/* ── TESTS 3.40 · why a message was sent ──────────────────────────────────────────────────────
   Under the channel and status badges, in the body column so it wraps with the row rather than
   pushing the timestamp off the right edge. Quieter than the title and darker than the badges:
   it is the sentence somebody reads second, after seeing what was sent. */
.vd-act-why {
  margin-top: var(--sp-2); color: var(--ink-body); font-size: var(--fs-xs); line-height: 1.5;
}

.vd-act-why strong { color: var(--ink); font-weight: 600; }

/* ══ 217.24 (Changes 03 C24) · the open list of every dropdown ══════════════════════════════════════════════════
   Moved here from page-visitors.css, unchanged: `combobox` draws this list for every select in the console now (in
   place, over the select itself), not only for the Visitors filters. */
.combo-pop {
  position: fixed; z-index: 60; background: var(--surface);
  border: 1px solid var(--hairline); border-radius: var(--r-md);
  box-shadow: 0 8px 24px rgba(10, 37, 64, .12);
  padding: 6px; display: flex; flex-direction: column; gap: 6px; max-width: 92vw;
}
.combo-search { width: 100%; font-size: var(--fs-sm); }
.combo-list { list-style: none; margin: 0; padding: 0; max-height: 260px; overflow-y: auto; }
/* 52.1 — a flex row, so the count can be pushed to the right edge. It was a plain block relying on
   `text-overflow: ellipsis` for long names; the ellipsis moves onto the NAME span below so a long
   country still truncates and the number is never the thing that gets cut. */
.combo-opt {
  padding: 7px 9px; border-radius: var(--r-sm); cursor: pointer;
  font-size: var(--fs-sm); color: var(--ink-body);
  display: flex; align-items: baseline; gap: var(--sp-3);
  overflow: hidden; white-space: nowrap;
}

/* 52.1 — the visit count beside a country or a city: "India   2,431". Muted, so the name stays the
   thing being read, and tabular so a column of numbers lines up as a column. `margin-left: auto`
   rather than a grid column, because only two of the eight filters carry counts at all. */
.combo-count {
  margin-left: auto; padding-left: var(--sp-4); color: var(--ink-quiet);
  font-size: var(--fs-xs); font-variant-numeric: tabular-nums; flex: none;
}
/* 215.5 — a two-line option: the value, then what it belongs to (an event's page, a path's title).
   Each line ellipsises on its own, so a long title never pushes the value out of sight. */
.combo-two { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.combo-main, .combo-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo-sub { font-size: var(--fs-xs); color: var(--ink-quiet); font-weight: var(--fw-regular); }
.combo-opt.is-active { background: var(--accent-tint); color: var(--accent-text-on-tint); }
.combo-opt[aria-selected="true"] { font-weight: var(--fw-semibold); color: var(--ink); }
/* The truncation escape hatch reads as an action, not as a value that exists. */
.combo-opt.is-custom { font-style: italic; color: var(--ink-quiet); }
.combo-empty { padding: 8px 9px; font-size: var(--fs-xs); color: var(--ink-quiet); }
/* 216.17 — a wide list (URL path, Events) shows every address and title whole: its lines wrap instead of ending in an
   ellipsis, and it is taller, since three times the width is still a list somebody reads down. */
.combo-pop.is-wide .combo-list { max-height: min(420px, 60vh); }
/* the popover is capped at the room left in the window (app.js `place`); the list is what gives way */
.combo-pop { overflow: hidden; }
.combo-pop .combo-list { min-height: 0; }
.combo-pop.is-wide .combo-opt { white-space: normal; }
.combo-pop.is-wide :is(.combo-main, .combo-sub) { white-space: normal; overflow: visible; overflow-wrap: anywhere; }


/* 217.46 — the eyebrow on the webhook secret shown once (app.js showWebhookSecret: Website settings and the campaign
   editor both open it, so it lives in a shared sheet). */
.secret-eyebrow {
  display: block; margin-bottom: 6px;
  font-size: var(--fs-micro); letter-spacing: var(--tr-micro); text-transform: uppercase;
  font-weight: var(--fw-semibold); color: var(--warning-ink);
}

/* 217.56 (Changes 03 C56) — the line a form's own save writes under its button (Gump.formDone). Empty, it takes no room. */
.form-done { margin: 0; font-size: 13px; color: var(--success-ink); }
.form-done:empty { display: none; }

/* 218.12 (Changes 04 C12) — a picture that could not load: a plain panel naming what it was, never the browser's
   broken-image icon. It carries the image's own classes, so it takes the image's place and size. */
.img-missing { display: flex; align-items: center; justify-content: center; min-width: 1.5em; min-height: 1.5em;
  background: var(--surface-sunken); color: var(--ink-quiet); font-size: var(--fs-xs); line-height: 1.3;
  text-align: center; overflow: hidden; object-fit: initial; }
.img-missing-text { padding: var(--sp-2); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3;
  -webkit-box-orient: vertical; }
