/* 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) }

/* 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) }

/* 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 }
.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.) */
.card > [data-state] > :last-child:not(.table-wrap-scroll):not(.kpi) {
  margin-bottom: var(--pad-card);
}

/* 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);
}

/* ── 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. */
[data-theme="dark"] .btn-danger { background: #D03A46; }
[data-theme="dark"] .btn-danger:hover:not(:disabled) { background: #B82D38; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn-danger { background: #D03A46; }
  :root:not([data-theme="light"]) .btn-danger:hover:not(:disabled) { background: #B82D38; }
}

/* ── .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;
}

/* ── 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 TEMPLATE TILE'S LIVE FRAME (design E7). The extracted sheet defines .tplcard,
   .tplcard-thumb (a 70px box) and .tplcard-name; what it cannot define is the <iframe> we put
   inside the thumb, because the design frame renders that slot as descriptive text rather than
   a real frame. These are the two rules that make an iframe behave as the box's content, keyed
   to the DESIGN'S OWN class - not a new one. I reached for `.tplframe` first; it exists nowhere,
   which would have been the fifth invented class on this project. */
.tplcard { flex: 0 0 150px; }
/* ⚠ 70px, NOT 100%. The thumb is a grid container whose row is auto-sized, and a grid item's
   percentage height needs a DEFINITE area to resolve against - so `height:100%` was dropped
   and the iframe kept its 150px intrinsic default inside a 70px box, clipped by
   .tplcard's overflow so it merely looked wrong rather than broken. Measured at 150 twice
   before this line, including once after adding align-self:stretch, which lands and still
   does not make the percentage resolve. 70 is the design's own number: "a live 70px frame
   of the template's HTML". */
.tplcard-thumb iframe { width: 100%; height: 70px; border: 0; display: block;
  pointer-events: none; }

/* ⚠ 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;
}
