/*
 * HPM CMS Admin — Main Stylesheet
 * Bootstrap 5.3 utilities + HPM Design System BEM components
 */

/* ============================================
   HPMKT Design Tokens (CSS Custom Properties)
   Canonical source: docs/design-system/tokens.md + docs/brand-guidelines.md
   (2025 HPMKT Brand Guide v20). Brand guide wins over Figma. Never hardcode a
   hex/size/weight — reference a token. Legacy --op-* / --wa-* names are retained
   below as back-compat aliases -> canonical tokens (retire incrementally).
   ============================================ */

/* Roobert — the sole HPMKT brand typeface. Files in app/assets/fonts/roobert/.
   Primary set Light 300 / Regular 400 / Bold 700 / Regular Italic 400i;
   Medium/SemiBold/Heavy load on demand. font-display: swap. */
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-Light-e2fbd49f.woff2") format("woff2"), url("/assets/roobert/Roobert-Light-f82ca18a.woff") format("woff"); font-weight: 300; font-style: normal; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-LightItalic-2974da7c.woff2") format("woff2"), url("/assets/roobert/Roobert-LightItalic-092aebe3.woff") format("woff"); font-weight: 300; font-style: italic; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-Regular-1e97cd8f.woff2") format("woff2"), url("/assets/roobert/Roobert-Regular-65afbc92.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-RegularItalic-85f140e8.woff2") format("woff2"), url("/assets/roobert/Roobert-RegularItalic-08b0fe10.woff") format("woff"); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-Medium-d7a3a988.woff2") format("woff2"), url("/assets/roobert/Roobert-Medium-bbfca743.woff") format("woff"); font-weight: 500; font-style: normal; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-MediumItalic-eba46d86.woff2") format("woff2"), url("/assets/roobert/Roobert-MediumItalic-9e576cca.woff") format("woff"); font-weight: 500; font-style: italic; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-SemiBold-4ab563d2.woff2") format("woff2"), url("/assets/roobert/Roobert-SemiBold-f4b6d0eb.woff") format("woff"); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-SemiBoldItalic-8bbc348e.woff2") format("woff2"), url("/assets/roobert/Roobert-SemiBoldItalic-70381b67.woff") format("woff"); font-weight: 600; font-style: italic; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-Bold-12623075.woff2") format("woff2"), url("/assets/roobert/Roobert-Bold-19ea49a0.woff") format("woff"); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-BoldItalic-19f7997b.woff2") format("woff2"), url("/assets/roobert/Roobert-BoldItalic-6b7f07c2.woff") format("woff"); font-weight: 700; font-style: italic; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-Heavy-2a1803c2.woff2") format("woff2"), url("/assets/roobert/Roobert-Heavy-6aa8f4b3.woff") format("woff"); font-weight: 800; font-style: normal; font-display: swap; }
@font-face { font-family: "Roobert"; src: url("/assets/roobert/Roobert-HeavyItalic-af30b1ca.woff2") format("woff2"), url("/assets/roobert/Roobert-HeavyItalic-f27ab1d6.woff") format("woff"); font-weight: 800; font-style: italic; font-display: swap; }

:root {
  /* ---- Primary palette (brand guide §2) ---- */
  --color-white:      #ffffff;
  --color-light-grey: #eeefef;
  --color-soft-black: #171717;
  --color-cool-grey:  #a8a8a8;
  --color-passion:    #f15b5d;

  /* ---- Passion tints (20% step) ---- */
  --color-passion-80: #f47c7d;
  --color-passion-60: #f79d9e;
  --color-passion-40: #f9bdbe;
  --color-passion-20: #fcdedf;

  /* ---- Soft Black tints ---- */
  --color-soft-black-80: #454545;
  --color-soft-black-60: #747474;
  --color-soft-black-40: #a2a2a2;
  --color-soft-black-20: #d1d1d1;

  /* ---- Semantic ---- */
  --color-text:           #171717;
  --color-text-secondary: #454545;   /* WCAG AA ~9.7:1 */
  --color-text-muted:     #747474;   /* WCAG AA ~4.6:1 */
  --color-text-switch-inactive: #6d6d6d; /* HPMR-887 reopen: Figma inactive segment label, 5.3:1 on white */
  --color-bg:             #ffffff;
  --color-bg-alt:         #eeefef;
  --color-accent:         #f15b5d;
  --color-accent-hover:   #f47c7d;
  --color-border:         #d1d1d1;
  --color-border-strong:  #a8a8a8;
  --color-focus:          #171717;

  /* ---- Functional (not brand-governed). cms-admin app palette: kept vivid for
     legibility — intentionally differs from tokens.md's marketing-tool values
     (esp. info stays blue, not grey). Both --color-* and legacy --op-* names use these. ---- */
  --color-success: #16A34A;
  --color-warning: #F59E0B;
  --color-danger:  #DC2626;
  --color-error:   #DC2626;
  --color-info:    #1D4ED8;

  /* ---- Typography (Roobert; brand guide §3) ---- */
  --font-family-base:    "Roobert", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-family-heading: "Roobert", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-weight-light: 300;
  --font-weight-regular: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;
  --font-weight-heavy: 800;
  --font-size-base: 1rem;
  --font-size-sm:   0.875rem;
  --font-size-xs:   0.75rem;
  --font-size-lg:   1.375rem;
  --line-height-base: 1.4;
  --line-height-heading: 1.2;
  --h1-size: 2.5rem;   --h1-weight: 300;
  --h2-size: 2rem;     --h2-weight: 300;
  --h3-size: 1.5rem;   --h3-weight: 700;
  --h4-size: 1.125rem; --h4-weight: 300;
  --h5-size: 0.875rem; --h5-weight: 300;
  --h6-size: 0.875rem; --h6-weight: 700;
  --tracking-wide: 0.12em;

  /* ---- Spacing / radius / elevation / layout ---- */
  --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; --space-4: 1rem;
  --space-5: 1.25rem; --space-6: 1.5rem; --space-8: 2rem; --space-10: 2.5rem;
  --space-12: 3rem;   --space-16: 4rem;
  --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-pill: 999px;
  /* Portal reskin (2026-07-01): larger card radii + resolved text/hover tokens
     not covered by the base scale. Used by the exhibitor-portal shell +
     components (spec 2026-07-01-exhibitor-portal-reskin-{spec,css}.md). */
  --radius-xl:  16px;   /* portal cards / form card / empty-state card / modal card */
  --radius-2xl: 32px;   /* main-panel left corners */
  --font-size-md: 1.125rem;                 /* 18px neutral body (account name, toast body, subheading) */
  --color-warning-text:  #B45309;           /* AA amber text (already inline in .badge--warning) */
  --color-passion-text:  #9B2C2E;           /* THE Passion for text. 7.52:1 on white, 6.52:1 on #EEEFEF,
                                               5.96:1 on passion-20 — all AA. Raw Passion #F15B5D is 3.28:1
                                               on white and is decorative-only (HPMR-598). */
  --color-success-text:  #166534;           /* AA dark-green for chip labels on success-fill (~6.5:1) */

  /* HPMR-605 — text colours for the BARE `.text-*` utilities. These differ from
     the tinted-component tokens above because a bare utility can land on any
     surface, and the worst realistic one is NOT white: `.portal-content` (the
     main admin panel) is --color-bg-alt #EEEFEF, so anything measured only
     against #FFFFFF is measured against a surface half this admin does not use.
     Every value here clears 4.5:1 on BOTH #FFFFFF and #EEEFEF.
       #B91C1C  6.47:1 white / 5.62:1 #EEEFEF   (kept clearly distinct from
                --color-passion-text #9B2C2E so danger != brand)
       #1D4ED8  6.70:1 / 5.82:1                 (same hue as --color-info)
       #92400E  7.09:1 / 6.15:1
       #585C5E  6.76:1 / 5.87:1
     --color-warning-text-strong exists because --color-warning-text #B45309 is
     4.36:1 on #EEEFEF — it was calibrated for the amber CHIP fill (#FEF3C7,
     4.51:1) and has no headroom left for a bare panel. The chip token is
     deliberately left alone so no shipped component repaints.
     --color-text-muted-strong is the exact composite Bootstrap's
     `rgba(33,37,41,.75)` already renders on white, pinned to a solid value: it
     is what `.text-muted` looks like TODAY, made deterministic. It is darker
     than --color-text-muted #747474 (4.06:1 on #EEEFEF — fails) on purpose. */
  --color-danger-text:         #B91C1C;
  --color-info-text:           #1D4ED8;
  --color-warning-text-strong: #92400E;
  --color-text-muted-strong:   #585C5E;
  /* HPMR-885 — the page-header (i) glyph: Figma greys/500, node 115-4335.
     Non-text glyph (WCAG 1.4.11, >= 3:1) on the #EEEFEF panel: 4.49:1
     (5.17:1 on white). Pinned by spec/views/shared/muted_token_surface_guard_spec.rb. */
  --color-page-header-info:    #6D6D6D;

  /* HPMR-610 — THE TWO-SURFACE RULE, generalised.
     -------------------------------------------------------------------------
     --color-text-muted #747474 is 4.67:1 on #FFFFFF and 4.06:1 on #EEEFEF.
     It was calibrated against white, and on white it is correct. It is NOT
     being changed here: inside a white card it is the right muted grey, and
     darkening it globally would repaint ~70 call sites that never had a defect.

     What changed is the RULE for choosing between the two. A token is only
     "AA" against a named surface, and this admin has THREE surfaces a muted
     label routinely lands on:
       #FFFFFF  cards, tables, modals, the sidebar, the topbar
       #EEEFEF  `.portal-content` (the main panel) AND every `--color-bg-alt`
                fill: chips, neutral badges/pills, list-table headers, tinted
                metric tiles, input adornments, sticky sub-headers
       a tint   `--color-*-fill` and `--color-passion-20/-40` (chips, callouts,
                status pills, the two upload dropzones)
     Measuring against #FFFFFF alone answers the wrong question on two of the
     three. This has now changed the conclusion three times: HPMR-598 (link
     3.28:1 white / 2.60:1 on its callout), HPMR-604 (.text-success 4.53:1
     white / 3.93:1 panel), HPMR-610 (this).
     And the surface to measure is the worst state the element REACHES, not its
     resting one: the dropzone hints go #FCDEDF -> #F9BDBE on hover/dragover,
     where even --color-text-muted-strong falls to 4.20:1 and only
     --color-text-secondary #454545 (5.96:1) clears AA.

     SO: --color-text-muted is for text PROVABLY always on white.
         --color-text-muted-strong (6.76:1 white / 5.87:1 #EEEFEF) is for
         everything else — anything on the panel, anything on a --color-bg-alt
         fill, and any bare/shared utility that can land on either, because a
         class with no fixed parent must be measured against the WORST surface
         the app actually paints, not the most flattering one.
     Decorative icon glyphs on #EEEFEF stay on --color-text-muted: 4.06:1
     clears the 3:1 that WCAG 1.4.11 asks of non-text content. */
  --wa-color-warning-fill-hover: #FDE68A;   /* amber-200 — quiet warning-button hover */
  --color-danger-fill-hover: #FECACA;       /* red-200 — quiet destructive-button hover */
  --wa-color-danger-fill-hover: var(--color-danger-fill-hover);
  --shadow-sm: 0 1px 2px rgba(23,23,23,0.06);
  --shadow-md: 0 2px 8px rgba(23,23,23,0.08);
  --container-max: 1280px;
  --sidebar-width: 316px; --sidebar-width-collapsed: 98px;
  --control-height: 48px;
  /* Single source for the modal body's inset. `.modal__footer--inset` cancels
     it with a negative margin so the footer divider bleeds to the card edge;
     without one token the two silently drift and the divider stops reaching. */
  --modal-body-padding: 32px;

  /* ---- Admin reskin Phase 2 (HPMR-536 / S1) — two tokens the shared
     foundation layer needs and the scale did not previously carry. ---- */

  /* Monospace stack for identifiers, URLs, coordinates and JSON evidence.
     Retires ~12 inline `font-family: ui-monospace,…` attributes across the SEO
     + Transportation views (`.list-table__cell--mono`, `.code-block`). */
  --font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
                      "Liberation Mono", monospace;

  /* Three-state scheduled-phase colour scale (spec §5.2 / S14 (b)): every
     content-block phase resolves to exactly one of Ended / Active now /
     Upcoming, and the same scale drives the Gantt bars, the legend dots and the
     per-phase row chips. Named semantically so the Gantt CSS never reaches for a
     functional token directly. Figma's #368AD9 / #4CBF78 / #D98236 are off-brand
     literals and map onto the shipped functional tokens (spec §3.5). */
  --color-phase-ended:    var(--color-info);
  --color-phase-active:   var(--color-success);
  --color-phase-upcoming: var(--color-warning);

  /* HPMR-642 — the in-progress neighborhood boundary outline, before the ring
     is closed. Sampled from Figma 715:75279's vertex handles (#8B5CF6).
     Deliberately NOT a brand colour: "unfinished shape" must not read as "this
     is the neighborhood's colour", which is exactly what the closed state then
     switches to. It is a non-text mark on the map canvas, so WCAG 1.4.11's 3:1
     applies rather than 1.4.3's 4.5:1; it measures ~4.0:1 against the light OSM
     tile layer. Consumed by neighborhood_boundary_map_controller.js via
     cssToken() and by the vertex-marker rules. */
  --neighborhood-draw-color: #8B5CF6;

  /* Canonical names for the five pale status fills. These VALUES already ship —
     they were only reachable through the legacy `--wa-color-*-fill-quiet`
     aliases, so every new tint had to reach for a `--wa-*` name, which
     .claude/rules/design.md forbids. Promoted here (canonical layer) with the
     identical values; the `--wa-*` names below now alias THESE, reversing the
     dependency to point legacy -> canonical like every other alias in this file.
     Zero resolved-value change; nothing repaints. */
  --color-success-fill: #DCFCE7;
  --color-warning-fill: #FEF3C7;
  --color-danger-fill:  #FEE2E2;
  --color-info-fill:    #DBEAFE;
  --color-brand-fill:   var(--color-passion-20);
  --color-neutral-fill: var(--color-bg-alt);

  /* HPMR-643 — the one-step-darker BORDER that goes with each tint. Albina's
     detection panel (Figma 691:108895 / 691:113435) outlines its fill rather
     than floating it, and her values are green/200 and yellow/200 from the
     Portals library. They are surfaces, not text, so her exact values stand;
     they are promoted to tokens here because the alternative is a hex literal
     in a rule, which .claude/rules/design.md forbids and a build guard
     enforces. Reuse them for any other outlined status tint. */
  --color-success-border: #CEF2DB;
  --color-warning-border: #FFF5D9;

  /* ============================================================
     Legacy --op-* / --wa-* aliases -> canonical tokens (back-compat).
     Existing views/CSS reference these; they now inherit the HPMKT
     brand values + Roobert. Retire incrementally toward --color-*.
     ============================================================ */
  --op-color-primary:        var(--color-accent);
  --op-color-primary-hover:  var(--color-accent-hover);
  --op-color-accent-light:   var(--color-passion-20);
  --op-color-text:           var(--color-text);
  --op-color-bg:             var(--color-bg);
  --op-color-text-secondary: var(--color-text-muted);  /* WCAG upgrade: #A8A8A8 -> #747474 */
  --op-color-text-muted:     var(--color-text-muted);
  --op-color-bg-alt:         var(--color-bg-alt);
  --op-color-border:         var(--color-border);
  --op-color-success:        var(--color-success);
  --op-color-warning:        var(--color-warning);
  --op-color-danger:         var(--color-danger);
  --op-color-info:           var(--color-info);

  /* Font family now Roobert (was Inter) */
  --op-font-family: var(--font-family-base);

  /* Admin font sizes — kept at existing admin scale (denser than marketing site) */
  --op-font-size-h1: 2rem;
  --op-font-size-h2: 1.5rem;
  --op-font-size-h3: 1.25rem;
  --op-font-size-body: 1rem;
  --op-font-size-label: 0.875rem;
  --op-font-size-small: 0.875rem;
  --op-font-size-micro: 0.75rem;
  --op-font-size-button: 1rem;
  --op-font-size-button-sm: 0.875rem;

  /* Admin radii/spacing — unchanged */
  --op-radius-sm: 6px;
  --op-radius-md: 8px;
  --op-radius-lg: 12px;
  --op-space-card-padding: 24px;
  --op-space-section-gap: 48px;

  /* --wa-* surface/text aliases */
  --wa-color-surface-default: var(--color-bg);
  --wa-color-surface-lowered: var(--color-bg-alt);
  --wa-color-surface-border:  var(--color-border);
  --wa-color-text-normal:     var(--color-text);
  --wa-color-text-quiet:      var(--color-text-muted);  /* WCAG upgrade: #A8A8A8 -> #747474 */
  --wa-color-text-link:       var(--color-accent);

  /* --wa-* status fills (light backgrounds) + on-quiet (icon/text).
     Now alias the canonical --color-*-fill tokens promoted above (same values). */
  --wa-color-success-fill-quiet: var(--color-success-fill);
  --wa-color-warning-fill-quiet: var(--color-warning-fill);
  --wa-color-danger-fill-quiet:  var(--color-danger-fill);
  --wa-color-brand-fill-quiet:   var(--color-brand-fill);
  --wa-color-neutral-fill-quiet: var(--color-neutral-fill);
  --wa-color-info-fill-quiet:    var(--color-info-fill);
  --wa-color-brand-on-quiet:     var(--color-passion-80);
  --wa-color-success-on-quiet:   var(--color-success);
  --wa-color-warning-on-quiet:   var(--color-warning);
  --wa-color-danger-on-quiet:    var(--color-danger);
  --wa-color-neutral-on-quiet:   var(--color-text-muted);
}

/* ============================================
   Base Styles
   ============================================ */
* { box-sizing: border-box; }

body {
  font-family: var(--op-font-family);
  color: var(--op-color-text);
  background: var(--wa-color-surface-lowered);
  margin: 0;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--op-color-primary); text-decoration: none; }
a:hover { color: var(--op-color-primary-hover); }

/* ============================================
   Admin Layout
   ============================================ */
.admin-layout {
  display: flex;
  min-height: 100vh;
}

.admin-sidebar {
  width: 260px;
  background: var(--op-color-text);
  color: #fff;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 100;
  overflow-y: auto;
}

.admin-sidebar__logo {
  padding: 20px 24px;
  border-bottom: 1px solid rgba(255,255,255,0.1);
  font-size: var(--op-font-size-label);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--op-color-primary);
}

.admin-sidebar__nav {
  padding: 16px 0;
  flex: 1;
}

.admin-sidebar__section {
  padding: 0 16px;
  margin-bottom: 8px;
}

.admin-sidebar__section-title {
  font-size: var(--op-font-size-micro);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgba(255,255,255,0.4);
  padding: 8px 8px 4px;
}

.admin-sidebar__link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: var(--op-radius-sm);
  color: rgba(255,255,255,0.7);
  font-size: var(--op-font-size-label);
  font-weight: 400;
  transition: all 0.15s ease;
}

.admin-sidebar__link:hover {
  background: rgba(255,255,255,0.08);
  color: #fff;
}

.admin-sidebar__link--active {
  background: var(--op-color-primary);
  color: #fff;
  font-weight: 600;
}

.admin-sidebar__link--active:hover {
  background: var(--op-color-primary-hover);
}

.admin-sidebar__link i {
  width: 20px;
  text-align: center;
  font-size: 14px;
}

.admin-sidebar__footer {
  padding: 16px 24px;
  border-top: 1px solid rgba(255,255,255,0.1);
  font-size: var(--op-font-size-micro);
  color: rgba(255,255,255,0.4);
}

.admin-main {
  flex: 1;
  margin-left: 260px;
  display: flex;
  flex-direction: column;
}

.admin-topbar {
  background: var(--op-color-bg);
  border-bottom: 1px solid var(--op-color-border);
  padding: 12px 32px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: sticky;
  top: 0;
  z-index: 50;
}

.admin-topbar__breadcrumbs {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--op-font-size-label);
}

.admin-topbar__breadcrumbs a {
  color: var(--op-color-text-secondary);
  opacity: 0.7;
}

.admin-topbar__breadcrumbs span.separator { color: var(--op-color-text-secondary); }

.admin-topbar__breadcrumbs .current {
  color: var(--op-color-primary);
  font-weight: 700;
}

.admin-topbar__actions {
  display: flex;
  align-items: center;
  gap: 16px;
}

.admin-topbar__user {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--op-font-size-label);
  color: var(--op-color-text);
}

.admin-content {
  padding: 32px;
  flex: 1;
}

/* ============================================
   Page Header
   ============================================ */
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
}

.page-header__title {
  font-size: var(--op-font-size-h1);
  font-weight: 700;
  margin: 0;
}

/* HPMR-885 — title + (i) description tooltip on one line.
   HPMR-885 reopen (Eugene, Jira 61243): BASELINE-aligned, not centred — the
   bottom of the (i) circle sits on the title's text baseline. The trigger's
   icon box has no text baseline of its own (see
   .info-tooltip__trigger--page-header), so flex synthesizes the trigger's
   baseline from its bottom edge, i.e. the circle's bottom. */
.page-header__heading {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}
/* HPMR-879 slot: see the shared breadcrumb-slot rule next to
   .detail-page__breadcrumb (one box for both). */

/* HPMR-610 — the page header sits ABOVE the cards, directly on the panel
   (--color-bg-alt #EEEFEF), never inside a white card. Strong token. */
.page-header__subtitle {
  font-size: var(--op-font-size-label);
  color: var(--color-text-muted-strong);
  margin: 4px 0 0;
}

.page-header__actions {
  display: flex;
  gap: 12px;
  align-items: center;
}

/* ============================================
   Buttons (HPM Design System)
   ============================================ */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: var(--op-radius-sm);
  font-family: var(--op-font-family);
  font-size: var(--op-font-size-button);
  font-weight: 600;
  border: 1px solid var(--op-color-border);
  background: var(--op-color-bg);
  color: var(--op-color-text-secondary);
  cursor: pointer;
  transition: all 0.15s ease;
  text-decoration: none;
  line-height: 1.4;
}

.button:hover { border-color: var(--op-color-text-secondary); color: var(--op-color-text); }

.button--brand-primary {
  background: var(--op-color-primary);
  color: #fff;
  border-color: var(--op-color-primary);
}
.button--brand-primary:hover { background: var(--op-color-primary-hover); border-color: var(--op-color-primary-hover); color: #fff; }

.button--brand-secondary {
  background: var(--op-color-accent-light);
  color: var(--op-color-primary);
  border-color: var(--op-color-accent-light);
}
.button--brand-secondary:hover { background: #FAD0D1; color: var(--op-color-primary); }

.button--success-green {
  background: var(--wa-color-success-fill-quiet);
  color: var(--op-color-success);
  border-color: var(--wa-color-success-fill-quiet);
}
.button--success-green:hover { background: #C6F6D5; }

.button--info-blue {
  background: var(--wa-color-info-fill-quiet);
  color: var(--op-color-info);
  border-color: var(--wa-color-info-fill-quiet);
}
.button--info-blue:hover { background: #BFDBFE; }

.button--destructive {
  background: var(--op-color-danger);
  color: #fff;
  border-color: var(--op-color-danger);
}
.button--destructive:hover { background: #B91C1C; border-color: #B91C1C; color: #fff; }

/* HPMR-610 — Cancel paints its label ON #EEEFEF (its own fill), so the muted
   token measures 4.06:1 here, not the 4.67:1 it gets on white. Ghost has NO
   fill at all, so it inherits whichever surface it is dropped onto — and it is
   used in panel-level toolbars as well as inside cards. Both take the strong
   token (5.87:1 on #EEEFEF, 6.76:1 on white). */
.button--cancel {
  background: var(--wa-color-surface-lowered);
  color: var(--color-text-muted-strong);
  border-color: var(--wa-color-surface-lowered);
}

.button--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--color-text-muted-strong);
}
.button--ghost:hover { background: var(--wa-color-surface-lowered); color: var(--op-color-text); }

.button--small {
  padding: 6px 10px;
  font-size: var(--op-font-size-button-sm);
}

.button[disabled], .button.disabled {
  background: var(--wa-color-surface-lowered);
  color: var(--op-color-border);
  border-color: var(--wa-color-surface-lowered);
  cursor: not-allowed;
  pointer-events: none;
}

/* --- Portal reskin (2026-07-01) — quiet (tinted) row-action + confirm
   buttons. Spec §3.1. ADD. --- */
.button--warning-quiet {
  background: var(--wa-color-warning-fill-quiet); /* #FEF3C7 */
  color: var(--color-warning-text);               /* AA amber (raw #ff9f26 fails) */
  border-color: var(--wa-color-warning-fill-quiet);
}
.button--warning-quiet:hover {
  background: var(--wa-color-warning-fill-hover);  /* amber-200, ~8% darker */
  border-color: var(--wa-color-warning-fill-hover);
  color: var(--color-warning-text);
}
.button--warning-quiet:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* AA FIX (HPMR-647, flagged): this button shipped `--color-danger` #DC2626 on
   its own #FEE2E2 tint, which measures **3.95:1** — an AA failure for the 14px
   semibold label it carries, and the exact defect design.md records for
   `.badge--danger` on the same pair ("auditing one component family and leaving
   its siblings is how this class of defect keeps coming back"). Two shipped
   comments already CLAIM this class carries the AA-safe token
   (neighborhoods/_neighborhood_row and buildings/_form both say so); the CSS
   did not. `--color-danger-text` #B91C1C on #FEE2E2 is **5.30:1**.
   App-wide and AA-positive: the callers are the venue row deletes, the
   unsaved-changes Discard, and HPMR-647's Dismiss. */
.button--destructive-quiet {
  background: var(--wa-color-danger-fill-quiet);   /* #FEE2E2 */
  color: var(--color-danger-text);                 /* #B91C1C — 5.30:1 (was #DC2626, 3.95:1) */
  border-color: var(--wa-color-danger-fill-quiet);
}
.button--destructive-quiet:hover {
  background: var(--wa-color-danger-fill-hover);    /* red-200, ~8% darker */
  border-color: var(--wa-color-danger-fill-hover);
  color: var(--color-danger-text);
}
.button--destructive-quiet:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* --- OVERRIDE (GLOBAL, flagged): Figma button radius is 8px; base was 6px
   (--op-radius-sm). --radius-md is the design default — visually trivial
   (6->8px), design-consistent (admin app included). --- */
.button { border-radius: var(--radius-md); }

/* --- OVERRIDE (GLOBAL, flagged, AA fix): white-on-Passion must be Bold >=14px.
   Base .button is 600 (semibold); bump brand-primary (Add/Save/Log Out) to 700
   so the white label clears WCAG AA on #F15B5D. App-wide, AA-positive. --- */
.button--brand-primary { font-weight: var(--font-weight-bold); }

/* ============================================
   Form Fields (HPM Design System)
   ============================================ */
.form-field { margin-bottom: 0; }

.form-field__label {
  display: block;
  font-size: var(--op-font-size-label);
  font-weight: 500;
  color: var(--op-color-text);
  margin-bottom: 4px;
}

/* Required-field indicator (decorative asterisk inside a form-field__label).
   aria-hidden on the markup; the input's aria-required carries the semantics. */
.form-field__required {
  color: var(--op-color-danger);
  margin-left: 2px;
}

.form-field__input-wrapper {
  position: relative;
}

.form-field__input,
.form-field__select,
.form-field__textarea {
  width: 100%;
  height: 43px;
  padding: 10px 16px;
  border: 1px solid var(--op-color-border);
  border-radius: var(--op-radius-sm);
  font-family: var(--op-font-family);
  font-size: var(--op-font-size-body);
  color: var(--op-color-text);
  background: var(--op-color-bg);
  transition: border-color 0.15s, box-shadow 0.15s;
}

.form-field__textarea {
  height: auto;
  min-height: 100px;
  resize: vertical;
}

/* HPMR-627 — chevron stroke repainted #A8A8A8 -> #747474.

   MEASURED: 68 selects across admin + exhibitor. #A8A8A8 on the select's own
   #FFFFFF background is 2.38:1; WCAG 1.4.11 asks 3:1 of "visual information
   required to identify user interface components", and with `appearance: none`
   stripping the native arrow this chevron is the ONLY thing distinguishing a
   select from a text input. #747474 is 4.67:1.

   #A8A8A8 is Cool Grey, which .claude/rules/design.md lists as decorative /
   borders only precisely because it is 2.38:1 — it had simply been pasted into
   a data URI where no token audit or grep for `--color-` would ever look at it.

   HARDCODED HEX — DOCUMENTED EXCEPTION, NOT AN OVERSIGHT. CSS custom
   properties cannot be interpolated into a `url()` string, so `var(...)` is
   not expressible here; the alternative is a mask-image + background-color
   rewrite, which changes the paint model for 68 controls and is not worth the
   regression risk for a colour change. %23747474 MUST stay equal to
   --color-text-muted. spec/views/shared/form_field_contrast_guard_spec.rb
   fails the build if the two drift.

   HPMR-974 — `select.list-toolbar__control` shares this rule. Filter-toolbar
   selects carried only `.list-toolbar__control` (padding 0 16px, no
   `appearance`), so Chromium painted its NATIVE menulist arrow flush against
   the right border, while the Active Market picker (`.form-field__select`)
   drew this chevron 16px in. Joining the selector list — rather than copying
   the data URI — makes every toolbar select draw the SAME chevron at the SAME
   `right 16px` inset, with the same 40px right padding (16px inset + 12px
   glyph + 12px clearance) so selected text never runs under the arrow.
   `select.list-toolbar__control` is (0,1,1), so it beats the (0,1,0)
   `.list-toolbar__control { padding; background }` shorthand at any source
   order; height/left padding/font stay the toolbar's own (44px, --space-4).
   spec/system/hpmr974_select_arrow_inset_spec.rb measures it in Chrome. */
.form-field__select,
select.list-toolbar__control {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23747474' stroke-width='1.5' fill='none'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-right: 40px;
}

/* HPMR-457 — left-icon input variant (.claude/rules/design-components.md
   "With left icon"; design.md: 44px left padding when an icon is present).
   Referenced by 14 views but previously had no backing rules — the icon
   rendered inline above the input and the text overlapped it. The wrapper
   already carries position: relative (above). No view uses the
   --with-icon-right variant, so only the left rules ship. */
.form-field__icon {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--op-color-text-secondary);
  pointer-events: none;
}

.form-field__input--with-icon-left .form-field__input,
.form-field__input--with-icon-left .form-field__select {
  padding-left: 44px;
}

.form-field__input:focus,
.form-field__select:focus,
.form-field__textarea:focus {
  outline: none;
  border-color: var(--op-color-primary);
  box-shadow: 0 0 0 2px rgba(241,91,93,0.3);
}

/* HPMR-627 — placeholder repainted from `--op-color-text-secondary at
   opacity: 0.6` to a full-opacity `--color-text-muted`.

   MEASURED IN CHROMIUM (hpmr627_formfield_family_sweep.js), 231 real rendered
   placeholders across 30 admin + exhibitor screens:

     #747474 @ 0.6 composites to #ACACAC on #FFFFFF   2.28:1  FAIL   x231
     #747474 @ 1.0                    on #FFFFFF      4.67:1  PASS

   The opacity was the whole defect and it is invisible in the source: the
   declared token is the AA-safe #747474 that HPMR-610 chose, and
   getComputedStyle().color still reports #747474 — the 0.6 lives on a separate
   property and is only applied at paint. Any audit that reads `color:` and
   stops (or greps the token) scores this as passing. It has to be composited
   over the surface to be seen at all, which is why 231 failing instances
   survived HPMR-598/604/605/610 and 620 untouched, three lines above the
   helper-text those tickets did repaint.

   ONE SURFACE, NOT TWO — and provably, not by assumption. Placeholder text is
   painted on the INPUT'S OWN background, never on an ancestor, and the input
   sets `background: var(--op-color-bg)` (#FFFFFF) itself six rules above. The
   only rule in this file that repaints it is
   `.form-field__input-wrapper--adorned .form-field__input { background:
   transparent }`, whose wrapper is `--color-bg` — still #FFFFFF. The sweep
   confirms it empirically: 231 of 231 instances resolved to #FFFFFF, including
   every field inside the #EEEFEF block-edit panel. So `--color-text-muted`
   (correct on white, 4.67:1) is right here and `--color-text-muted-strong` is
   not needed — and is actively wrong, because it is the exact colour of
   .form-field__helper-text below and would flatten placeholder and helper text
   into the same visual weight.

   `opacity: 1` IS LOAD-BEARING — do not delete it as redundant. Firefox
   applies `opacity: 0.54` to ::placeholder in its UA stylesheet; merely
   dropping the 0.6 would hand Firefox 4.67:1 -> ~2.6:1 and reintroduce the bug
   on one engine only. If a future input is ever painted --color-bg-alt, its
   placeholder must move to --color-text-muted-strong (4.06:1 fails there). */
.form-field__input::placeholder,
.form-field__textarea::placeholder {
  color: var(--color-text-muted);
  opacity: 1;
}

/* HPMR-620 — repainted from --op-color-text-secondary (an alias of
   --color-text-muted #747474) to --color-text-muted-strong #585C5E.

   MEASURED IN CHROMIUM, not reasoned about. A sweep of every
   .form-field__helper-text on 18 admin screens (hpmr620_helper_surface_sweep.js)
   found 115 instances resolving to exactly TWO backgrounds:

     #747474 on #FFFFFF  4.67:1  PASS   x60  white cards, modals, .section-card
     #747474 on #EEEFEF  4.06:1  FAIL   x55  the .portal-content panel itself,
                                              and the inline background on
                                              #block-edit-<id> (_block.html.erb)

   It is 14px/400 — not large text — so the bar is 4.5:1 and 4.06:1 fails 1.4.3.
   #585C5E is 6.76:1 / 5.87:1 and clears both.

   THE PRIOR PASS ON THIS TICKET MEASURED ONLY THE ADD-BLOCK PANEL, a white
   .section-card, and concluded 4.67:1 passed. Editing an EXISTING block — the
   common path, and the only path for a block already on a page — renders inside
   a panel _block.html.erb fills with --color-bg-alt INLINE. Same class, same
   token, different answer: the two-surface rule in .claude/rules/design.md.

   REPAINTING THE RULE, NOT THE TOKEN. --color-text-muted #747474 is correct
   inside white cards and is used well outside this class; darkening its value
   would be a visual regression dressed as a fix.

   THE STRONG VARIANT IS THE RIGHT ONE FOR A BARE SHARED UTILITY. This class
   appears in 55 view files with no fixed parent, so it must be measured against
   the worst surface the admin paints under it. The sweep confirms that worst
   surface is #EEEFEF and NOT a passion tint: in _upload_form and _field_media
   the helper text is a SIBLING of the passion-20 dropzone, never a descendant,
   so it never reaches the --color-passion-40 #F9BDBE hover state where even
   #585C5E drops to 4.20:1 and only --color-text-secondary would clear AA. If a
   helper text is ever moved INSIDE a dropzone, that call site needs
   --color-text-secondary, not this. */
.form-field__helper-text {
  font-size: var(--op-font-size-small);
  color: var(--color-text-muted-strong);
  margin: 4px 0 0;
}

.form-field__error-text {
  font-size: var(--op-font-size-small);
  color: var(--op-color-danger);
  margin: 4px 0 0;
}

/* HPMR-785 — helper text + live "N / 1500" character counter under a
   textarea (Company Profile description, portal + staff). The counter uses the
   helper text's muted-strong colour; the over-limit state uses
   --color-danger-text (#B91C1C), which clears AA on white AND on #EEEFEF, so
   it is safe whichever surface the form sits on. */
.form-field__footer {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-3);
}

.form-field__footer .form-field__helper-text { flex: 1 1 auto; }

.char-count {
  flex: 0 0 auto;
  font-size: var(--op-font-size-small);
  color: var(--color-text-muted-strong);
  margin: 4px 0 0;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.char-count--over {
  color: var(--color-danger-text);
  font-weight: 600;
}

/* HPMR-758 leftover: upload form used `.form-field__error` (block alert) while
   the shipped token was `__error-text`. Alias so the class-definition guard
   and the Media Library validation banner both resolve. */
.form-field__error {
  font-size: var(--op-font-size-small);
  color: var(--op-color-danger);
  margin: 4px 0 0;
}

.form-field--error .form-field__input,
.form-field--error .form-field__select,
.form-field--error .form-field__textarea {
  border-color: var(--op-color-danger);
  box-shadow: 0 0 0 2px rgba(220,38,38,0.3);
}

/* ============================================
   Tables (HPM Design System)
   ============================================ */
.list-container {
  background: var(--op-color-bg);
  border: 1px solid var(--op-color-border);
  border-radius: var(--op-radius-md);
  overflow: hidden;
}

.list-container__table {
  width: 100%;
  border-collapse: collapse;
}

.table__th {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  background: var(--wa-color-surface-lowered);
  font-size: var(--op-font-size-label);
  font-weight: 600;
  color: var(--op-color-text);
  text-align: left;
  border-bottom: 1px solid var(--op-color-border);
}

.table__th-icon { opacity: 0.5; font-size: 12px; }

.table__tbody-tr {
  border-bottom: 1px solid var(--op-color-border);
  transition: background 0.1s;
}

.table__tbody-tr:last-child { border-bottom: none; }
.table__tbody-tr:hover { background: rgba(241,91,93,0.03); }

.table__tbody-tr td {
  padding: 12px 16px;
  font-size: var(--op-font-size-label);
  vertical-align: middle;
}

/* ============================================
   Portal table tweaks (reskin 2026-07-01, spec §3.3) — all descendant-scoped
   under .portal-content so the admin app's tables render unchanged.
   ============================================ */
/* White table card (Figma 47:2638: header + rows on a clean white rounded card,
   NOT the grey panel showing through). Shared by Contacts/Brands/Videos/Social/
   Logos/Press Files; horizontal scroll on tablet instead of crushing Email. */
.contacts-table {
  width: 100%;
  overflow-x: auto;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.contacts-table .list-container__table { min-width: 720px; }

/* If a portal view still wraps a table in .list-container, strip its chrome. */
.portal-content .list-container {
  border: 0;
  border-radius: 0;
  background: transparent;
  overflow: visible;
}

/* Lighter Figma header cell: Regular 14px muted (base is 600 / --color-text),
   on WHITE (base .table__th uses grey --wa-color-surface-lowered). */
.portal-content .table__th {
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);                 /* 14px */
  background: var(--color-bg);
}

/* Tighter Figma data cell: 8/12 (base is 12/16), ellipsis overflow. */
.portal-content .table__tbody-tr td {
  padding: var(--space-2) var(--space-3);         /* 8 / 12 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Actions column fixed 182px; other columns flex equally. */
.contacts-table th:last-child,
.contacts-table td:last-child { width: 182px; }
.table__actions { display: inline-flex; gap: var(--space-2); }

/* Bulk-select checkbox column (buildings list — HPMR-793). Events still use
   inline width:40px on th/td; this BEM slot matches that width/alignment so
   the select-all / row checkboxes sit in a narrow centered column. */
.table__check {
  width: 40px;
  text-align: center;
  white-space: nowrap;
  vertical-align: middle;
}

/* Sort affordance — shown only on sortable headers (e.g. Type). */
.table__th-sort {
  margin-left: auto;
  font-size: var(--font-size-xs);                 /* 12px */
  opacity: 0.5;
  color: var(--color-text-muted);
}
.table__th-sort--active { opacity: 1; color: var(--color-accent); }

/* ============================================
   Cards
   ============================================ */
.card {
  background: var(--op-color-bg);
  border: 1px solid var(--op-color-border);
  border-radius: var(--op-radius-md);
  overflow: hidden;
}

.card__header {
  padding: 20px 24px;
  border-bottom: 1px solid var(--op-color-border);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.card__title {
  font-size: var(--op-font-size-h3);
  font-weight: 600;
  margin: 0;
}

.card__body { padding: 24px; }

.card__footer {
  padding: 16px 24px;
  border-top: 1px solid var(--op-color-border);
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}

/* ============================================
   Status Badges
   ============================================ */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: var(--op-radius-sm);
  font-size: var(--op-font-size-micro);
  font-weight: 600;
  line-height: 1.6;
}

/* HPMR-610 — the `.badge--*` family was the last one still painting the RAW
   functional hue on its own pale tint. `.chip--*` and `.status-pill--*` were
   both corrected to the darkened `--color-*-text` tokens in earlier passes;
   these three were their untouched siblings, and the same third surface (the
   tint, not white, not the panel) is what they had never been measured on:
     success #16A34A on #DCFCE7  3.00:1 FAIL -> #166534 6.49:1
     danger  #DC2626 on #FEE2E2  3.95:1 FAIL -> #9B2C2E 6.15:1
     brand   #F15B5D on #FCDEDF  2.60:1 FAIL -> #9B2C2E 5.96:1
   These are the exact tokens `.status-pill--danger` / `.chip--success` /
   `.chip--new` already use, so the badge family now matches them. Warning was
   already correct at 4.51:1 — only its hardcoded hex is tokenised here. */
.badge--success { background: var(--wa-color-success-fill-quiet); color: var(--color-success-text); }
.badge--warning { background: var(--wa-color-warning-fill-quiet); color: var(--color-warning-text); }
.badge--danger { background: var(--wa-color-danger-fill-quiet); color: var(--color-passion-text); }
.badge--brand { background: var(--wa-color-brand-fill-quiet); color: var(--color-passion-text); }
/* HPMR-610 — the neutral badge's "quiet fill" IS --color-bg-alt #EEEFEF, so its
   label was 4.06:1, not the 4.67:1 the token gets on white. */
.badge--neutral { background: var(--wa-color-neutral-fill-quiet); color: var(--color-text-muted-strong); }
.badge--info { background: var(--wa-color-info-fill-quiet); color: var(--op-color-info); }

/* ============================================
   Chips — contact-type pills (pill + leading icon). Portal reskin 2026-07-01,
   spec §3.4. Distinct from the square .badge* family. Labels are Bold 700 and
   the primary/billing text colors are darkened to reach WCAG AA >=4.5:1 on
   their pale tints (a11y refinement); the leading icon keeps the brighter
   brand hue (decorative, meaning carried by icon + text, never color alone).
   ============================================ */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);                            /* 4px */
  padding: var(--space-2) var(--space-3);         /* 8 / 12 */
  border-radius: var(--radius-pill);
  font-size: var(--font-size-sm);                 /* 14px */
  font-weight: var(--font-weight-bold);           /* a11y: bold label */
  line-height: 1;
  white-space: nowrap;
}
.chip i, .chip svg { font-size: 14px; }            /* chip leading icon ~14px (svg: FA Pro kit) */

.chip--primary   { background: var(--color-passion-20);           color: var(--color-passion-text); }  /* star   ~5.9:1 */
.chip--primary   i, .chip--primary   svg { color: var(--color-accent); }
.chip--secondary { background: var(--wa-color-warning-fill-quiet); color: var(--color-warning-text); }  /* user   ~4.5:1 */
.chip--marketing { background: var(--wa-color-info-fill-quiet);   color: var(--color-info); }           /* users  ~5.5:1 */
.chip--billing   { background: var(--wa-color-success-fill-quiet); color: var(--color-success-text); }  /* invoice ~6.5:1 */
.chip--billing   i, .chip--billing   svg { color: var(--color-success); }

/* ============================================
   Reservation detail (HPMR-201 S1) — definition-list summary + leg cards
   on the staff reservation show page. Token-only; no hardcoded colors.
   ============================================ */
.reservation-detail {
  margin: 0;
}
.reservation-detail dt {
  font-size: 12px;
  font-weight: 600;
  color: var(--op-color-text-secondary);
  margin-top: 12px;
}
.reservation-detail dt:first-child {
  margin-top: 0;
}
.reservation-detail dd {
  font-size: 14px;
  color: var(--op-color-text);
  margin: 2px 0 0;
}
.reservation-leg {
  border: 1px solid var(--op-color-border);
  border-radius: 4px;
  padding: 12px 16px;
}
.reservation-leg__title {
  font-size: 13px;
  font-weight: 700;
  color: var(--op-color-text);
  margin-bottom: 6px;
}

/* ============================================
   Flash Messages
   ============================================ */
.flash {
  padding: 12px 16px;
  border-radius: var(--op-radius-sm);
  margin-bottom: 16px;
  font-size: var(--op-font-size-label);
  display: flex;
  align-items: center;
  gap: 8px;
  border-left: 3px solid;
}

.flash--notice { background: var(--wa-color-success-fill-quiet); border-color: var(--op-color-success); color: #15803D; }
.flash--alert { background: var(--wa-color-danger-fill-quiet); border-color: var(--op-color-danger); color: #B91C1C; }
.flash--warning { background: var(--wa-color-warning-fill-quiet); border-color: #B45309; color: #B45309; }

/* ============================================
   Toasts (portal reskin 2026-07-01, spec §3.6) — fixed bottom-right,
   solid-fill. Auto-dismiss via the `portal-toast` Stimulus controller.
   NAMESPACED `.portal-toast*` (NOT `.toast`) to avoid Bootstrap 5.3's own
   `.toast` component, whose `.toast:not(.show){display:none}` (higher
   specificity, loaded first) would hide these. The .flash* rules above stay
   for the admin shell.
   ============================================ */
.portal-toast-region {
  position: fixed;
  right: var(--space-8);
  bottom: var(--space-8);
  /* HPMR-1046: above the elevated sticky footer (.detail-footer--elevated,
     1060) so its message is never hidden by it; dropped UNDER the modal layer
     (.modal 1055) whenever a modal is open — rule below. */
  z-index: 1065;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 475px;
  max-width: calc(100vw - 2 * var(--space-4));    /* never overflow */
  pointer-events: none;                           /* region ignores clicks; toasts opt back in */
}

/* HPMR-1046: while ANY modal is open — root-level (.modal 1055) or a confirm
   modal nested inside the elevated footer's 1060 stacking context — the toast
   sits under it, so it can never cover a modal's Save/Cancel. Modals open via
   `.modal--open` (portal-modal / confirm-modal) or inline `display: flex`
   (modal controller). */
body:has(.modal.modal--open, .modal[style*="display: flex"], .modal[style*="display:flex"]) .portal-toast-region {
  z-index: 1050;
}

/* HPMR-1046: a sticky .detail-footer (Events edit with inline z-index 1060,
   .detail-footer--elevated on shuttle schedules / SEO recs, plain sticky
   footers) sits at the viewport bottom. Lift the toast region's POSITION
   clear of it so the toast never covers the footer's actions. z-index is
   unchanged. 88px covers the footer bar (16px padding x2 + a ~44px button
   row). The non-sticky .detail-footer--card variant is excluded. */
body:has(.detail-footer:not(.detail-footer--card)) .portal-toast-region {
  bottom: calc(88px + var(--space-4));
}

.portal-toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--space-5);                            /* 20px */
  padding: var(--space-2) var(--space-4);         /* 8 / 16 */
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  border-top: 2px solid rgba(255, 255, 255, 0.4); /* translucent highlight */
  color: var(--color-white);
  opacity: 1;
  transform: translateX(0);
  transition: opacity 0.25s ease, transform 0.25s ease;
}

.portal-toast--success { background: var(--color-success); }   /* white bold >=18px passes AA */
.portal-toast--error   { background: var(--color-danger); }    /* white passes AA */
/* Amber fails white (~1.9:1) -> dark text/icon (a11y correction). */
.portal-toast--warning {
  background: var(--color-warning);
  color: var(--color-text);
  border-top-color: rgba(23, 23, 23, 0.15);
}

.portal-toast__icon {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.2);           /* reads on any fill */
  font-size: 16px;
  color: inherit;
}
.portal-toast--warning .portal-toast__icon { background: rgba(23, 23, 23, 0.12); }

.portal-toast__body {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--font-size-md);                 /* 18px */
  font-weight: var(--font-weight-bold);
  line-height: 1.3;
}

.portal-toast__dismiss {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);                 /* 12px xmark */
  cursor: pointer;
}
.portal-toast__dismiss:hover { background: var(--color-bg); }
.portal-toast__dismiss:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

/* Enter/exit — transform+opacity only; the controller toggles these. */
.portal-toast--entering,
.portal-toast--leaving { opacity: 0; transform: translateX(1rem); }

@media (prefers-reduced-motion: reduce) {
  .portal-toast { transition: none; }
  .portal-toast--entering,
  .portal-toast--leaving { transform: none; }
}

/* ============================================
   Empty State
   ============================================ */
.empty-state {
  text-align: center;
  padding: 48px 24px;
}

.empty-state__icon {
  font-size: 48px;
  color: var(--op-color-border);
  margin-bottom: 16px;
}

.empty-state__title {
  font-size: var(--op-font-size-h3);
  font-weight: 600;
  margin: 0 0 8px;
}

/* HPMR-610 — mixed, like `.text-meta`. Most empty states are wrapped in the
   white `.empty-state--card`, but `shuttle_dashboard/index` and
   `shuttle_dashboard/_grid` render a bare `.empty-state` on the panel, and
   `.media-empty` deliberately fills #EEEFEF and hosts one. Worst surface wins. */
.empty-state__description {
  color: var(--color-text-muted-strong);
  font-size: var(--op-font-size-label);
  margin: 0 0 24px;
}

/* Portal reskin (2026-07-01, spec §3.7) — full-height white card wrapping the
   existing .empty-state, with larger portal-scoped title/icon. ADD. */
.empty-state--card {
  flex: 1;
  min-height: 320px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  background: var(--color-bg);
  border-radius: var(--radius-xl);
}
.empty-state--card .empty-state { padding: var(--space-8); }
.empty-state--card .empty-state__icon { font-size: 24px; color: var(--color-text-muted); margin-bottom: 0; }

.portal-content .empty-state__title {
  font-size: var(--h3-size);                      /* 24px */
  font-weight: var(--font-weight-bold);
}
.portal-content .empty-state__description {
  font-size: var(--font-size-md);                 /* 18px */
  color: var(--color-text);
}

/* ============================================
   Pagination Bar (HPMR-452)
   ============================================
   Implements the .pagination-bar family per the HPM design system
   Pagination spec (.claude/rules/design-components.md §Pagination):
   result count (Regular 12px quiet) + page navigator (bordered
   container of 32px page cells; active page = Passion bg, bold
   white). The spec's per-page dropdown is intentionally omitted —
   per-page is fixed per table (each controller's .per(N) call).
   Rendered by app/views/shared/_pagination.html.erb. */
/* HPMR-610 — the count is the bar's CENTRE section, a sibling of (not inside)
   the white `.pagination-bar__nav`. `shared/_pagination` is rendered outside
   the `.list-container` on most screens (bus_lines, faq_entries, redirects,
   media_library, cms/pages…), so it lands on the panel. 12px — no large-text
   relief. `.pagination-bar__gap` and `--prev/--next` DO live inside the white
   nav and correctly keep --color-text-muted. */
.pagination-bar__count {
  font-size: 12px;
  color: var(--color-text-muted-strong);
}

.pagination-bar__nav {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--op-color-border);
  border-radius: var(--op-radius-sm);
  background: var(--op-color-bg);
  overflow: hidden;
}

.pagination-bar__page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  font-size: 12px;
  color: var(--op-color-text);
  text-decoration: none;
  border-right: 1px solid var(--op-color-border);
  transition: background 0.15s;
}

.pagination-bar__nav > :last-child { border-right: none; }

a.pagination-bar__page:hover {
  background: var(--wa-color-surface-lowered);
  color: var(--op-color-text);
}

.pagination-bar__page--active {
  background: var(--op-color-primary);
  color: #FFFFFF;
  font-weight: 700;
}

.pagination-bar__page--disabled {
  color: var(--op-color-border);
  cursor: default;
}

.pagination-bar__gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  font-size: 12px;
  color: var(--wa-color-text-quiet);
  border-right: 1px solid var(--op-color-border);
}

/* ============================================
   Modal (HPMR-413)
   ============================================
   Implements the .modal family documented in
   .claude/rules/design-components.md. Pre-HPMR-413 the cms-admin
   markup used .modal / .modal__inner / .modal__header / .modal__body
   / .modal__footer / .modal__close-btn / .modal__title (HPMR-21 S05
   bulk-reject + edit-page Unpublish + Force-Transition + Phase 5.5
   Dismiss modals) but the corresponding CSS was never landed —
   so every modal that opened (after HPMR-412 unblocked the Stimulus
   targets) overlapped its host page with no backdrop and no centered
   card. JS toggles `display: flex` to reveal; default is `none`. */
.modal {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: 1055;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 16px;
}

.modal__inner {
  background: var(--op-color-bg);
  border-radius: var(--op-radius-md);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  max-height: calc(100vh - 32px);
  width: 100%;
  display: flex;
  flex-direction: column;
}

/* Size modifiers per design-components.md §Modals.
   xs ~640px / sm ~960px / md ~1408px / lg ~1792px. The viewport
   caps `max-width` at the modal__inner so on smaller screens the
   modal fills the available width up to its max. */
.modal__inner.modal--xs { max-width: 640px; }
.modal__inner.modal--sm { max-width: 960px; }
.modal__inner.modal--md { max-width: 1408px; }
.modal__inner.modal--lg { max-width: 1792px; }

.modal__header {
  padding: 32px;
  border-bottom: 1px solid var(--op-color-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}

/* When the header hosts wizard tabs below the title row, the bottom
   border belongs to the tab strip — drop the header's padding so the
   tabs sit flush against it. Matches the design-components.md note. */
.modal__header--with-tabs { padding-bottom: 0; }

.modal__title {
  font-family: var(--op-font-family);
  font-size: 24px;
  font-weight: 700;
  color: var(--op-color-text);
  margin: 0;
}

.modal__body {
  padding: var(--modal-body-padding);
  overflow-y: auto;
  flex: 1;
}

.modal__footer {
  padding: 32px;
  border-top: 1px solid var(--op-color-border);
  display: flex;
  justify-content: center;
  gap: 16px;
  flex-shrink: 0;
}

.modal__close-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--op-color-text-secondary);
  background: transparent;
  border: none;
  transition: background 0.15s, color 0.15s;
}

.modal__close-btn:hover {
  background: var(--wa-color-surface-lowered);
  color: var(--op-color-text);
}

/* ============================================
   Modal variants (portal reskin 2026-07-01) — confirm + form overlays layered
   on the existing .modal / .modal__inner / .modal__body / .modal__footer shell
   (unchanged). Spec §3.5 + DECISION D1. Reuses the base overlay (rgba .6,
   fixed, centered).
   ============================================ */

/* Both reskin variants use the 16px card radius (base .modal__inner is 8px). */
.modal__inner.modal--confirm,
.modal__inner.modal--form { border-radius: var(--radius-xl); }

/* Class-driven reveal (portal controllers toggle this instead of inline style). */
.modal.modal--open { display: flex; }

/* Right-aligned footer (confirm + form). Base .modal__footer is centered. */
.modal__footer--end { justify-content: flex-end; }

/* --- Confirm variant: centered icon+title, no header bar, no close-X. --- */
.modal__confirm-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-8) var(--space-8) 0;       /* 32 top/sides, 0 bottom */
  text-align: center;
}
/* Target svg too — the FA Pro kit swaps <i> -> <svg>, so an `i`-only selector
   stops matching and the trash icon renders black instead of danger red. */
.modal__confirm-header i,
.modal__confirm-header svg {
  font-size: 22px;
  color: var(--color-danger);
}
.modal--confirm .modal__body {
  padding: var(--space-6) var(--space-8) var(--space-8);  /* 24 top, 32 sides, 32 bottom */
  text-align: left;
}
.modal--confirm .modal__footer {
  padding: var(--space-8);                        /* keeps the base border-top */
}

/* HPMR-1048 — a confirm dialog rendered from a table row (shared/_confirm_action,
   e.g. the HPMR-1017 Delete on the Events list) sits inside an actions <td>
   with `white-space: nowrap`. The dialog is position:fixed but still INHERITS
   white-space, so its body copy stayed on one line and the card scrolled
   sideways. Reset wrapping on the dialog itself, and let a long unbroken
   record name break, so the copy always fits the card at any row depth. */
.modal.modal--confirm { white-space: normal; }
.modal--confirm .modal__body { overflow-wrap: anywhere; }

/* --- Form variant: ~1136px card, scrolls if taller than the viewport. --- */
.modal__inner.modal--form {
  max-width: 1136px;
  max-height: calc(100vh - 2 * var(--space-8));   /* 100vh - 64px */
  overflow: auto;
}

/* ============================================
   Contact form card (portal reskin 2026-07-01, spec §4.3) — the title-bar /
   body / footer chrome for the Add/Edit form. Used inside .modal--form
   (DECISION D1); also valid standalone.
   ============================================ */
.contact-form-card {
  width: 100%;
  max-width: 1136px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-md);
}
.contact-form-card__title-bar {
  padding: var(--space-6) var(--space-8);         /* 24 / 32 */
  border-bottom: 1px solid var(--color-border);
}
.contact-form-card__title {
  margin: 0;
  font-size: var(--h3-size);                      /* 24px */
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}

/* --- Title-bar ASIDE SLOT (HPMR-587 / AU-2 — Figma 546:60879) --------------
   Albina's Edit Admin User modal (546:60877) draws Title Container 546:60879 as
   `flex items-start justify-between`: Header 546:60880 on the left, a status
   chip on the right.

   OPT-IN ONLY — do NOT hoist `display: flex` onto the base rule above.
   `.contact-form-card__title-bar` is shared by 14 partials (shared/_modal_form
   plus 13 bespoke ones); none of them emits `--split`, so none of them changes.
   The slot is rendered by shared/_modal_form's `title_aside_partial` local. */
.contact-form-card__title-bar--split {
  display: flex;
  align-items: flex-start;                        /* frame: items-start */
  justify-content: space-between;
  gap: var(--space-4);                            /* 16px */
}
/* The heading yields before the slot does, so a long title wraps inside the bar
   instead of shoving the chip off the card. Scoped to --split: the 13 plain
   bars keep their block-layout h2 untouched. */
.contact-form-card__title-bar--split .contact-form-card__title {
  min-width: 0;
}
.contact-form-card__title-aside {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);                            /* 8px, if a caller ships two chips */
}

.contact-form-card__body {
  padding: var(--space-8);                        /* 32px */
  display: flex;
  flex-direction: column;
  gap: var(--space-4);                            /* 16px between rows */
}
.contact-form-card__subheading {
  margin: 0;
  font-size: var(--font-size-md);                 /* 18px */
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}
.contact-form-card__footer {
  padding: var(--space-8);                        /* 32px incl. padding-top */
  border-top: 1px solid var(--color-border);
  display: flex;
  justify-content: flex-end;                      /* Cancel + Save */
  gap: var(--space-4);
}

/* Neutralize the card's own frame when it sits inside the modal inner
   (.modal--form supplies the visible card). Dividers/padding stay. */
.modal--form .contact-form-card {
  max-width: none;
  margin: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

/* ============================================
   Toggle / Checkbox
   ============================================ */
.toggle-switch {
  position: relative;
  width: 40px;
  height: 20px;
  display: inline-block;
}

.toggle-switch input { opacity: 0; width: 0; height: 0; }

.toggle-switch__slider {
  position: absolute;
  cursor: pointer;
  top: 0; left: 0; right: 0; bottom: 0;
  background: var(--op-color-border);
  border-radius: 20px;
  transition: 0.2s;
}

.toggle-switch__slider::before {
  content: "";
  position: absolute;
  height: 16px;
  width: 16px;
  left: 2px;
  bottom: 2px;
  background: white;
  border-radius: 50%;
  transition: 0.2s;
}

.toggle-switch input:checked + .toggle-switch__slider { background: var(--op-color-success); }
.toggle-switch input:checked + .toggle-switch__slider::before { transform: translateX(20px); }

/* HPMR-659 — Passion-on toggle, for the Building Owner / Building Type modals
   (Figma `705:116759`, `705:116212`, `705:116849`, `705:116299`, which all draw
   the Active switch in Passion rather than the base rule's green). A MODIFIER
   and not an edit to the shared value: the base `.toggle-switch` has another
   consumer — the CMS block-editor field toggle — whose green is its own design.

   No contrast note is needed for the ON state (Passion #F15B5D is 3.28:1 on
   white, clearing the 3:1 WCAG 1.4.11 asks of a non-text control), and the OFF
   track keeps the base `--op-color-border` #D1D1D1, which is already darker
   than the frame's #F6F6F6. The switch never carries meaning alone in either
   state — the word "Active" is beside it. */
.toggle-switch--brand input:checked + .toggle-switch__slider { background: var(--color-passion); }

/* ============================================
   Pagination
   ============================================ */
.pagination-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* HPMR-929 — no divider between the grid and the paginator, and the gap
     above the bar is exactly 16px on every list: the bar adds no top padding
     of its own, so the 16px is the margin (or the list-layout gap) alone. */
  padding: 0 16px 12px;
  margin-top: var(--space-4);
  border-top: none;
  font-size: var(--op-font-size-micro);
  /* HPMR-610 — the bar itself declares no background, so it inherits the panel
     on every screen that renders it outside the list container. */
  color: var(--color-text-muted-strong);
}

.pagination-bar__pages {
  display: flex;
  gap: 4px;
}

.pagination-bar__page {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--op-radius-sm);
  font-size: var(--op-font-size-label);
  font-weight: 600;
  color: var(--op-color-text);
  text-decoration: none;
  border: 1px solid var(--op-color-border);
}

.pagination-bar__page--active {
  background: var(--op-color-primary);
  color: #fff;
  border-color: var(--op-color-primary);
}

/* ============================================
   Utilities
   ============================================ */
/* ============================================================================
   HPMR-605 — THESE FOUR RULES WERE DEAD, AND ONE OF THEM WAS THE PRECEDENT.
   ============================================================================
   `.text-muted`, `.text-success`, `.text-danger` and `.text-warning` are all
   BOOTSTRAP class names. Bootstrap 5.3.3 loads from the CDN in the layout
   <head> BEFORE this stylesheet, and generates every colour utility with
   `!important` ($enable-important-utilities). `!important` beats source order,
   so for years these four local declarations resolved to nothing at all and the
   admin rendered Bootstrap's palette:

                    local rule claimed    what Bootstrap ACTUALLY painted
     class          (ratio on #EEEFEF)    (ratio on #EEEFEF)
     .text-warning  #B45309  4.36:1       #FFC107  --bs-warning      1.41:1
     .text-success  #16A34A  2.86:1       #198754  --bs-success      3.93:1
     .text-danger   #DC2626  4.19:1       #DC3545  --bs-danger       3.93:1
     .text-muted    #747474  4.06:1       rgba(33,37,41,.75)         6.27:1

   Note the local rules were not themselves AA on this panel either — only
   `.text-muted` came out ahead, and only by accident, because Bootstrap's
   composite is darker than the #747474 we intended.

   `.text-warning` is the sharp end: it had ALREADY been "darkened to #B45309
   for AA" — and that fix never applied. The class was still painting #FFC107,
   1.41:1 on the panel it is used on, i.e. very nearly invisible. A stylesheet
   comment claiming a fix is not a fix.

   Why the sibling utilities were never suspected: `.text-brand` and
   `.text-passion` (HPMR-598) are NOT Bootstrap names, so their local rules DO
   win — auditing those two proved the pattern worked and quietly generalised a
   conclusion that only held for non-colliding names.

   A full diff of this file's 624 class definitions against Bootstrap 5.3.3's
   1323 `!important` utilities found these four and nothing else, so the
   collision set is closed as of this commit. `!important` here is not
   decoration: at equal (0,1,0) specificity it is the only way to take the
   cascade back, and it additionally pins the utilities against the global
   `a:hover` (0,1,1) rule that silently out-specifies bare utility classes on
   links — the trap HPMR-598 had to work around by hand for `.text-passion`.
   ============================================================================ */
.text-muted { color: var(--color-text-muted-strong) !important; }
/* HPMR-598 — NOT FOR TEXT AT BODY SIZE. Passion #F15B5D measures 3.28:1 on
   #FFFFFF, 2.85:1 on the #EEEFEF lowered surface and 2.60:1 on a #FCDEDF
   passion-20 callout. WCAG AA needs 4.5:1 for normal text and only relaxes to
   3:1 at >=18.66px bold / >=24px regular — nothing in this admin uses this
   class at that size. Keep `.text-brand` for DECORATIVE marks only (icons that
   duplicate an adjacent text label, rules, glyphs). For text, and for every
   link, use `.text-passion` below. HPMR-605 audited the semantic siblings this
   note pointed at and found the deeper defect — see the block above.
   See .claude/rules/design.md. */
.text-brand { color: var(--op-color-primary); }
/* AA-safe Passion for text and links: #9B2C2E is 7.52:1 on white, 6.52:1 on
   #EEEFEF, 5.96:1 on #FCDEDF — the same darkening every chip / callout /
   status-pill already applies, and identical to `.detail-field__value--accent`
   (HPMR-591), which was the first place this token was exposed as plain text. */
.text-passion { color: var(--color-passion-text); }
/* The global `a:hover` (#F47C7D — 2.62:1 on white, WORSE than the colour being
   replaced) is a (0,1,1) selector and would silently out-specify a bare (0,1,0)
   utility class on hover. Pinned, with the hover affordance moved to an
   underline, which is contrast-independent. */
a.text-passion:hover,
a.text-passion:focus-visible {
  color: var(--color-passion-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* For a link sitting INSIDE a run of text, where colour would otherwise be the
   sole thing marking it as a link (WCAG 1.4.1). Table-cell links that are the
   entire cell do not need this. */
.text-passion--underlined { text-decoration: underline; text-underline-offset: 2px; }
/* HPMR-605 — see the block above for why these carry `!important`. The values
   are the `-text` token family, not the functional `--color-success` /
   `--color-danger` / `--color-warning` fills: those are calibrated to be read
   as ICON and FILL colours next to a label, and none of them clears 4.5:1 as
   body text on the #EEEFEF panel (2.86 / 4.19 / 1.86 respectively). */
.text-success { color: var(--color-success-text) !important; }
.text-danger { color: var(--color-danger-text) !important; }
.text-warning { color: var(--color-warning-text-strong) !important; }
/* Defined defensively with ZERO call sites. `.text-info` is the one member of
   the Bootstrap family nobody has typed yet, and it is the worst of them —
   --bs-info #0DCAF0 is 1.96:1 on white and 1.70:1 on the panel. Leaving it
   undefined leaves the hole open for whoever types it next. */
.text-info { color: var(--color-info-text) !important; }
.fw-600 { font-weight: 600; }
.fs-micro { font-size: var(--op-font-size-micro); }
.fs-small { font-size: var(--op-font-size-small); }

/* Focus visible */
:focus-visible {
  outline: 2px solid var(--op-color-primary);
  outline-offset: 2px;
  border-radius: var(--op-radius-sm);
}

:focus:not(:focus-visible) { outline: none; }

/* Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--wa-color-neutral-fill-quiet); border: 2px solid var(--op-color-border); }
::-webkit-scrollbar-thumb { background: var(--op-color-primary); border-radius: 2px; opacity: 0.6; }
::-webkit-scrollbar-thumb:hover { opacity: 1; }

/* ============================================
   Code / Monospace
   ============================================ */
code {
  padding: 2px 6px;
  background: var(--wa-color-surface-lowered);
  border-radius: 3px;
  font-size: var(--op-font-size-micro);
}

/* ============================================
   Media Library Grid Cards
   ============================================ */
.card img {
  transition: opacity 0.15s ease;
}

.card:hover img {
  opacity: 0.9;
}

/* ============================================
   HPMR-362 (HPMR-20 S06) — Scoped Exhibitor Profile additions
   ============================================
   New utility classes for the locked-field display pattern + the
   card variants used on /exhibitor/profile. All other classes
   referenced on that page (.card, .card__header, .card__title,
   .card__body, .card__footer, .badge--success, .badge--neutral,
   .flash--*, .page-header__*, .form-field__*) already exist above. */

.locked-field { display: block; margin: 0; }
.locked-field__label {
  font-family: var(--op-font-family);
  font-size: 12px;
  font-weight: 600;
  color: var(--op-color-text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0 0 4px;
}
.locked-field__value {
  font-family: var(--op-font-family);
  font-size: 14px;
  font-weight: 500;
  color: var(--op-color-text);
  margin: 0;
  line-height: 1.5;
}

.card--locked {
  background: var(--wa-color-surface-lowered);
  border-color: transparent;
}
.card--locked .card__header { border-bottom-color: rgba(0, 0, 0, 0.06); }

/* ============================================
   HPMR-680 — the SEO deep link's target block
   ============================================
   Seo::RecommendationsController#cms_page_edit_path_with_context opens this
   editor with ?block_id under a banner reading "Make your edits below". On a
   page with twenty blocks that sentence pointed at nothing, so exactly one
   card is painted as the subject of the visit.

   NOT COLOUR ALONE (WCAG 1.4.1). The card also carries a `.callout--brand`
   reading "Recommended edit - this is the block you were sent to" as its first
   row: text, and therefore legible in greyscale, in a forced-colours mode, and
   to anyone who cannot tell this ring from the plain 1px --op-color-border
   every other card has. The ring is the second signal, never the only one. (A
   chip was tried first and rejected on measurement: the block-summary partial
   already paints up to 24 `.chip--info` on a single page, so a 25th would have
   distinguished nothing.)

   The ring is --color-accent (Passion) so it reads as the same thread as the
   accept-from-rec banner at the top of this screen. That is the one job
   design.md still allows the raw Passion hue: a rule / outline that carries no
   information of its own. NOTHING IN THIS RULE PAINTS TEXT — the marker's own
   label is the `.callout--brand` above, whose pair is --color-passion-text on
   --color-brand-fill (which resolves to --color-passion-20), 5.96:1 per
   design.md. Naming the pair the element actually wears matters here: a
   contrast audit greps this file for it.

   scroll-margin-top is the breathing room page_tabs_controller's
   scrollIntoView({ block: "start" }) leaves above the card, so the marker line
   does not land flush against the top edge of the viewport. --space-8 matches
   .portal-content's own 32px padding, so a revealed card sits where a card at
   the top of the panel would. (CHECKED, not assumed: this shell has NO sticky
   top bar — the dark .admin-topbar was deleted in the HPMR-503 reskin and the
   layout renders .portal-sidebar + .portal-content only — so this value is
   about rhythm, not about clearing anything.) */
.card--target-block {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-passion-20);
  scroll-margin-top: var(--space-8);
}

/* HPMR-795 — same breathing room for autoscroll after "Add Content Block".
   create.turbo_stream stamps data-scroll-to-block-id; sortable-blocks scrolls
   the new card with block:"start". Without this margin the card lands flush
   against the viewport top. Same --space-8 as .card--target-block. */
#content-blocks-list .card[data-sort-order] {
  scroll-margin-top: var(--space-8);
}

.card__header--two-line {
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
}

.card__title-tail {
  font-size: 14px;
  font-weight: 400;
  color: var(--op-color-text-secondary);
  margin-left: 6px;
}

.card__header-link {
  font-family: var(--op-font-family);
  font-size: 13px;
  font-weight: 500;
  color: var(--op-color-text-secondary);
  text-decoration: underline;
  text-decoration-color: var(--op-color-border);
  text-underline-offset: 2px;
  transition: color 120ms ease, text-decoration-color 120ms ease;
}
.card__header-link:hover,
.card__header-link:focus-visible {
  color: var(--op-color-primary);
  text-decoration-color: var(--op-color-primary);
}

@media (max-width: 991.98px) {
  .card__footer--sticky-mobile {
    position: sticky;
    bottom: 0;
    background: var(--op-color-bg);
    box-shadow: 0 -2px 8px rgba(23, 23, 23, 0.06);
    z-index: 10;
  }
}

/* =====================================================================
   HPMR-364 (HPMR-20 S08) — Exhibitor Portal additions
   ---------------------------------------------------------------------
   Scoped under .portal-* and .portal-body. Leans heavily on the
   existing .admin-* CSS (the portal layout reuses .admin-sidebar*,
   .admin-topbar*, .admin-content, .button*, .flash*, .badge*) so the
   visual chrome lines up out of the gate. Tweaks below adjust spacing
   and add the dashboard / activity-specific components.

   A real designer will rework this once UX time is available; treat
   the rules below as the working DRAFT contract. CSS tokens (--op-*
   / --wa-*) are used everywhere — no hardcoded hex.
   ===================================================================== */

.portal-body {
  background: var(--op-color-bg-alt);
}

.portal-layout {
  display: flex;
  min-height: 100vh;
}

/* ===========================================================================
   Exhibitor portal shell (reskin 2026-07-01, spec §2) — white sidebar +
   account footer + light rounded content panel. Replaces the dark topbar
   chrome. Portal-only: every selector is .portal-* (the ADMIN app keeps its
   .admin-sidebar* dark rules, which are untouched).
   =========================================================================== */
.portal-sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 100;
  width: var(--sidebar-width);                 /* 316px */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-6);
  padding: var(--space-8) var(--space-6);       /* 32 / 24 */
  background: var(--color-bg);                   /* white */
  overflow-y: auto;
}

/* top group: logo + nav sections (bottom group is .portal-account) */
.portal-sidebar__top {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.portal-sidebar__logo {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
}
.portal-sidebar__logo img,
.portal-sidebar__logo svg {
  height: 40px;
  width: auto;                                   /* wordmark ~103x40 */
}
.portal-sidebar__logo-label {
  font-size: var(--font-size-base);              /* 16px */
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted);
  white-space: nowrap;
}

.portal-sidebar__nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);                           /* between sections */
}
.portal-sidebar__section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);                           /* title -> items, item -> item */
}
.portal-sidebar__section-title {
  margin: 0;
  padding: 0 var(--space-1);
  font-size: var(--font-size-sm);                /* 14px */
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted);
  text-transform: none;                          /* Proper Case, NOT uppercase */
  letter-spacing: 0;
}

/* Menu item — light-grey chip w/ Passion-tint hover+active. */
.portal-sidebar__link {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-3);                        /* 12px */
  border-radius: var(--radius-md);                /* 8px */
  background: var(--color-bg-alt);                /* #EEEFEF (Figma #f6f6f6) */
  color: var(--color-text);
  font-size: var(--font-size-base);               /* 16px */
  font-weight: var(--font-weight-regular);
  text-decoration: none;
  overflow: clip;
  transition: background 0.15s ease, color 0.15s ease;
}
/* i AND svg — the FA Pro kit swaps <i> -> <svg>, so an `i`-only selector would
   drop the muted color + 24px sizing portal-wide. */
.portal-sidebar__link i,
.portal-sidebar__link svg {
  flex: 0 0 auto;
  width: 24px;                                    /* 24px icon box (D2) */
  font-size: 24px;
  line-height: 1;
  text-align: center;
  color: var(--color-text-muted);
}
.portal-sidebar__link-label {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.portal-sidebar__link:hover {
  background: var(--color-passion-20);            /* #FCDEDF */
  color: var(--color-text);
}
.portal-sidebar__link:hover i,
.portal-sidebar__link:hover svg { color: var(--color-text-muted); }   /* icon stays muted on hover */

.portal-sidebar__link--active,
.portal-sidebar__link[aria-current="page"] {
  background: var(--color-passion-20);
  color: var(--color-text);
}
.portal-sidebar__link--active i,
.portal-sidebar__link--active svg,
.portal-sidebar__link[aria-current="page"] i,
.portal-sidebar__link[aria-current="page"] svg {
  color: var(--color-accent);                     /* only Passion accent in the rail */
}
.portal-sidebar__link:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Events pending-count pill (product feature retained from the current partial). */
.portal-sidebar__count {
  flex: 0 0 auto;
  margin-left: auto;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--wa-color-warning-fill-quiet);
  color: var(--color-warning-text);               /* AA amber, matches .badge--warning */
  font-size: var(--font-size-xs);                 /* 12px */
  font-weight: var(--font-weight-bold);
  line-height: 1.6;
}

/* Account footer — absorbs the deleted topbar identity + sign-out. Spec §2.3 */
.portal-account {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}
.portal-account__identity {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.portal-account__avatar {
  flex: 0 0 auto;
  width: 46px;
  height: 46px;
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--color-passion-20);
  color: var(--color-soft-black);                 /* corrected off-palette #660f11 */
  font-size: var(--font-size-base);               /* 16px initial */
  font-weight: var(--font-weight-regular);
  text-transform: uppercase;
}
.portal-account__text {
  display: flex;
  flex-direction: column;
  min-width: 0;                                   /* allow email to ellipsis */
}
.portal-account__name {
  font-size: var(--font-size-md);                 /* 18px */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  line-height: 1.3;
}
.portal-account__email {
  font-size: var(--font-size-sm);                 /* 14px */
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}
/* button_to renders <form><button>; make the button full-width, Passion Bold. */
.portal-account__logout { margin: 0; }
.portal-account__logout .button { width: 100%; font-weight: var(--font-weight-bold); }
.portal-account__logout .button i,
.portal-account__logout .button svg { font-size: 14px; }

/* Content panel — light-grey floating card w/ left corners rounded 32px. */
.portal-content {
  margin-left: var(--sidebar-width);              /* 316px — offset the fixed sidebar */
  flex: 1;
  min-width: 0;                                   /* shrink below content min-content so the
                                                     .contacts-table scroll container can clamp
                                                     (row-flex item automatic-min-size trap) */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);                            /* header -> body */
  padding: var(--space-8);                        /* 32px */
  background: var(--color-bg-alt);
  border-radius: var(--radius-2xl) 0 0 var(--radius-2xl);  /* top-left + bottom-left */
}

/* Transitional back-compat: the target single-element layout has no
   .portal-main wrapper (main.portal-content is a direct child of
   .portal-layout). If one IS kept, the offset lives on it and is reset on the
   inner panel to avoid doubling. */
.portal-main {
  flex: 1;
  min-width: 0;                                   /* same row-flex shrink guard as .portal-content */
  margin-left: var(--sidebar-width);
  display: flex;
  flex-direction: column;
}
.portal-main > .portal-content { margin-left: 0; }

/* Page header inside the panel — the panel gap owns the spacing. */
.portal-content .page-header {
  margin-bottom: 0;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.portal-content .page-header__subtitle {
  font-size: var(--font-size-base);               /* 16px (was 14px) */
  color: var(--color-text-muted-strong);          /* HPMR-610 — on the panel */
}

/* Contacts list region grows so the empty-state card (flex:1) fills the area
   below the page header, per Figma. */
.portal-content .contacts-index,
.portal-content #contacts-list,
.portal-content #brands-list,
.portal-content #videos-list,
.portal-content #social_links-list {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* Portal checkboxes render in Passion (Figma), not Bootstrap blue. Portal-scoped
   so the admin app's Bootstrap checkboxes are unchanged. accent-color covers native
   inputs; the :checked bg/border covers Bootstrap 5.3's appearance:none .form-check-input. */
.portal-content .form-check-input { accent-color: var(--op-color-primary); }
.portal-content .form-check-input:checked {
  background-color: var(--op-color-primary);
  border-color: var(--op-color-primary);
}

/* Dashboard body (reskin 2026-07-02 — HPMKT Portal design system).
   Figma 36:253 (populated) / 31:3749 (empty). Canonical --color-* / --space-*
   tokens; white cards on the grey (.portal-content) panel; Roobert. Portal-
   scoped only (.portal-* selectors) — the shell + admin app are untouched.
   FA Pro kit swaps <i>->{svg}, so icon color/size is targeted via a class
   or the `svg` selector, never a bare `i` tag. */

.portal-dashboard {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);                            /* 24px between sections (Figma) */
}

/* Greeting ----------------------------------------------------------- */
.portal-dashboard__welcome {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.portal-dashboard__greeting {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.portal-dashboard__title {
  font-size: var(--h2-size);                      /* 32px (Figma greeting) */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
}

/* HPMR-610 — the dashboard welcome header is a bare <header> on the panel
   (#EEEFEF); the stat cards below it are the first white surface. */
.portal-dashboard__subtitle {
  font-size: var(--font-size-base);               /* 16px */
  color: var(--color-text-muted-strong);
  margin: 0;
}

.portal-dashboard__last-sign-in {
  margin: 0;
  color: var(--color-text-muted-strong);
  font-size: var(--font-size-sm);
}
.portal-dashboard__last-sign-in i,
.portal-dashboard__last-sign-in svg {
  margin-right: var(--space-2);
  color: var(--color-accent);
}

/* Shared section title ("Get your profile...", "Recent activity"). */
.portal-dashboard__section-title {
  font-size: var(--h3-size);                      /* 24px (Figma) */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
}

/* Stat cards --------------------------------------------------------- */
.portal-stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-4);                            /* 16px */
}

.portal-stat-card {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  background: var(--color-bg);                    /* white on grey panel */
  border-radius: var(--radius-xl);                /* 16px */
  padding: var(--space-6);                        /* 24px */
  color: var(--color-text);
  text-decoration: none;
  transition: box-shadow 0.15s ease, transform 0.1s ease;
}
.portal-stat-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
  color: var(--color-text);
}

.portal-stat-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.portal-stat-card__label {
  font-size: var(--font-size-sm);                 /* 14px */
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted);
}

.portal-stat-card__count {
  font-size: var(--h2-size);                      /* 32px */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  line-height: 1.1;
}
.portal-stat-card__count--warning { color: var(--color-accent); }   /* Passion — Figma near-quota */
.portal-stat-card__count--danger  { color: var(--color-danger); }

.portal-stat-card__quota { color: inherit; font-weight: inherit; }

/* Small a11y flag icon (retained per spec) — color inherits the count tint. */
.portal-stat-card__flag {
  margin-left: var(--space-2);
  font-size: var(--font-size-base);               /* ~16px */
  vertical-align: baseline;
}

.portal-stat-card__icon {
  flex-shrink: 0;
  width: 58px;
  height: 58px;
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.portal-stat-card__icon i,
.portal-stat-card__icon svg {
  color: var(--color-accent);
  font-size: 24px;
}

/* Quick actions ------------------------------------------------------ */
.portal-quick-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  background: var(--color-bg);
  border-radius: var(--radius-xl);
  padding: var(--space-4);                        /* 16px (Figma) */
}

.portal-quick-actions__title {
  font-size: var(--h3-size);                      /* 24px */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  margin: 0;
}

.portal-quick-actions__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

/* Onboarding checklist ----------------------------------------------- */
.portal-onboarding {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);                            /* title -> list */
}

.portal-onboarding__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);                            /* 8px between task cards (Figma) */
}

.portal-onboarding__step {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--color-bg);
  border-radius: var(--radius-xl);
  padding: var(--space-4);                        /* 16px */
}

.portal-onboarding__icon {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.portal-onboarding__icon i,
.portal-onboarding__icon svg {
  color: var(--color-text-muted);
  font-size: 20px;
}
.portal-onboarding__icon--done { background: var(--color-accent); }
.portal-onboarding__icon--done i,
.portal-onboarding__icon--done svg { color: #fff; }

.portal-onboarding__label {
  flex: 1;
  min-width: 0;
  font-size: var(--font-size-md);                 /* 18px */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

.portal-onboarding__cta { flex-shrink: 0; }

/* Recent activity ---------------------------------------------------- */
.portal-activity-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: var(--color-bg);
  border-radius: var(--radius-xl);
  padding: var(--space-4);                        /* 16px */
}

.portal-activity-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

.portal-activity-preview {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.portal-activity-preview__item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
}
.portal-activity-preview__item:last-child { border-bottom: 0; }

/* Figma activity pill (24:935): reuse the .badge variant helper but render
   the pill shape (rounded-32, 14px). Later in the file than .badge, so this
   wins on equal specificity; the badge--* color still applies. */
.portal-activity-preview__badge {
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-3);         /* 8 / 12 */
  font-size: var(--font-size-sm);                 /* 14px */
  line-height: 1;
}

.portal-activity-preview__summary {
  font-size: var(--font-size-sm);                 /* 14px */
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.portal-activity-preview__time {
  font-size: var(--font-size-sm);                 /* 14px */
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* Activity log table ------------------------------------------------ */

.portal-activity__table {
  width: 100%;
}

.portal-pagination {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  margin-top: 16px;
}

.portal-pagination__status {
  color: var(--op-color-text-secondary);
  font-size: var(--op-font-size-label, 12px);
}

/* Portal responsive (reskin 2026-07-01, spec §1 / CSS §10). Desktop = base;
   queries scale down. Order matters: 1024 first, then 768 (later same-
   specificity rule wins at <=768). */

/* Tablet: collapse to a 98px icon rail (labels hidden, active tint kept). */
@media (max-width: 1024px) {
  .portal-sidebar {
    width: var(--sidebar-width-collapsed);        /* 98px */
    padding: var(--space-8) var(--space-3);
    align-items: center;
  }
  .portal-sidebar__link { justify-content: center; }
  .portal-sidebar__link-label,
  .portal-sidebar__logo-label,
  .portal-sidebar__section-title,
  .portal-sidebar__count,
  .portal-account__text,
  .portal-account__logout-label { display: none; }   /* icon-only rail */
  .portal-sidebar__logo { justify-content: center; }
  .portal-sidebar__logo img,
  .portal-sidebar__logo svg { max-width: 74px; height: auto; }
  .portal-account { align-items: center; }
  .portal-account__logout .button { padding: var(--space-3); }

  .portal-content,
  .portal-main { margin-left: var(--sidebar-width-collapsed); }
  .portal-content {
    border-radius: var(--radius-xl) 0 0 var(--radius-xl);   /* 32 -> 16 */
    padding: var(--space-6);                                /* 32 -> 24 */
  }
}

/* Phone: existing static full-width fallback (labels restored, flat panel).
   STRICTLY below 768 — at exactly 768 the tablet icon-rail (max-width:1024)
   governs, per spec §1 ("768→1024 = 98px rail"; "<768 fallback retained").
   Using 767.98px avoids the boundary where the static full-width sidebar
   would shove .portal-content off-screen inside the row-flex layout. */
@media (max-width: 767.98px) {
  /* Stack the (now static, full-width) sidebar ABOVE the content — otherwise
     the default row-direction .portal-layout puts the full-width sidebar
     beside the panel and shoves it off-screen (horizontal overflow). */
  .portal-layout { flex-direction: column; }
  .portal-sidebar {
    position: static;
    width: 100%;
    height: auto;
    flex-direction: column;
    align-items: stretch;
  }
  .portal-sidebar__link { justify-content: flex-start; }
  .portal-sidebar__link-label,
  .portal-sidebar__logo-label,
  .portal-sidebar__section-title,
  .portal-account__text,
  .portal-account__logout-label { display: block; }   /* re-show what 1024 hid */
  .portal-account { align-items: stretch; }

  .portal-content,
  .portal-main { margin-left: 0; }
  .portal-content {
    border-radius: 0;
    padding: var(--space-4);                        /* 16px */
  }

  /* Dashboard activity preview: allow the middle column to shrink below its
     content min (minmax(0,1fr) vs 1fr) so the nowrap timestamp can't push the
     grid past the viewport at ~320px. Not a reskin — desktop is unchanged. */
  .portal-activity-preview__item { grid-template-columns: auto minmax(0, 1fr) auto; }
}

/* Touch (tablet): grow row-action buttons toward the 44px target. Portal-scoped
   so the admin app's dense tables are untouched. Spec §6. */
@media (pointer: coarse) {
  .portal-content .button--small { padding-top: 11px; padding-bottom: 11px; }
}

/* ===========================================================================
   HPMR-338 (HPMR-20 S04) — Impersonation banner.

   Rendered in the exhibitor_portal layout (and could be rendered in the
   admin application layout if we ever surface a "still impersonating"
   banner there — currently the admin user is signed OUT during an active
   impersonation, so the banner lives in the portal layout only).

   Reskin 2026-07-02 (Figma 284:9750). Soft warning callout: amber-tint
   surface (--wa-color-warning-fill-quiet) + subtle amber border + a Light
   user-secret icon in amber, message in Soft Black, and a quiet-danger
   "End Impersonation" button. Distinct from the portal chrome so the
   operator never forgets they're in support mode. Spacing is now owned by
   the .portal-content flex gap (no bottom margin).
=========================================================================== */

.impersonation-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2);                       /* 8px (Figma) */
  background: var(--wa-color-warning-fill-quiet);
  border: 1px solid var(--wa-color-warning-fill-hover);
  border-radius: var(--radius-lg);               /* 12px (Figma panel radius) */
  color: var(--color-text);
  font-family: var(--font-family-base);
}

.impersonation-banner__lead {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.impersonation-banner__icon {
  flex: 0 0 auto;
  padding: 0 var(--space-2);
  font-size: 16px;
  color: var(--color-warning);
}

.impersonation-banner__text {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--font-size-base);              /* 16px (Figma) */
  line-height: 1.4;
}

.impersonation-banner__text strong {
  font-weight: var(--font-weight-semibold);
}

.impersonation-banner__form {
  display: inline;
  margin: 0;
}

.impersonation-banner__end {
  flex: 0 0 auto;
}

@media (max-width: 768px) {
  .impersonation-banner {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
  }
}

/* ===========================================================================
   HPMR-338 — Cross-admin impersonation start toast.
   Lives inside #admin_toasts container in the admin layout. Phase 6
   pattern: append to channel, severity-colored.
=========================================================================== */

.admin-toast--warning {
  background: var(--wa-color-warning-fill-quiet, #FEF3CD);
  border-left: 4px solid var(--op-color-warning, #F59E0B);
}



/* ===========================================================================
   HPMR-459 (Transportation CMS A-S4) — Route map editor.
   Leaflet needs an explicit height on its container; the vertex marker is
   an L.divIcon styled here (vector circle markers cover the stops).
=========================================================================== */

.route-map__canvas {
  height: 480px;
  border: 1px solid var(--op-color-border);
  border-radius: 4px;
  background: var(--op-color-bg-alt);
}

.route-map__stops {
  max-height: 480px;
  overflow-y: auto;
}

.route-map__stop-row {
  cursor: pointer;
}

.route-map__stop-row--highlight {
  background: var(--op-color-accent-light);
}

.route-map__vertex-marker {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--op-color-primary);
  border: 2px solid var(--op-color-bg);
  box-shadow: 0 0 0 1px var(--op-color-border);
  cursor: grab;
}

.route-map__vertex-list {
  max-height: 320px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.route-map__vertex-index {
  flex: 0 0 2rem;
  text-align: right;
  font-size: 12px;
  font-weight: 600;
  color: var(--op-color-text-muted);
}

/* HPMR-460 (A-S5) — Shuttle Schedule service-days repeatable rows.
   Mirrors the route-map vertex list above (A-S5 review F8 — the HPMR-413
   referenced-but-undefined CSS class). Vertical spacing between rows is
   owned by the list's gap; .shuttle-schedule__service-day-row stays a
   semantic/Stimulus hook (layout via Bootstrap utilities), same contract
   as .route-map__vertex-row. */
.shuttle-schedule__service-day-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.shuttle-schedule__service-day-index {
  flex: 0 0 2rem;
  text-align: right;
  font-size: 12px;
  font-weight: 600;
  color: var(--op-color-text-muted);
}

/* HPMR-203 (Transportation T-C) — passenger-count dashboard grid.
   Defines the dashboard-grid BEM classes referenced by
   shuttle_dashboard/index + _grid (the HPMR-413 referenced-but-undefined-CSS
   discipline: ship the CSS with the markup). Layout via the existing
   .list-container__table + design tokens; this block only adds the grid-
   specific affordances (count cells, hidden-slot marker, horizontal scroll). */
.dashboard-grid-section__title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 1rem;
  font-weight: 700;
  color: var(--op-color-text);
  margin-bottom: 12px;
}

.dashboard-grid__scroll {
  overflow-x: auto;
}

.dashboard-grid__table {
  min-width: max-content;
}

.dashboard-grid__date-cell {
  font-weight: 600;
  white-space: nowrap;
  text-align: left;
}

.dashboard-grid__time-col,
.dashboard-grid__total-col {
  text-align: center;
}

.dashboard-grid__cell {
  text-align: center;
  vertical-align: middle;
}

.dashboard-grid__cell--total {
  font-weight: 700;
  color: var(--op-color-text);
}

.dashboard-grid__cell-empty {
  color: var(--op-color-border);
}

/* Count chip: Passion-tinted, the brand accent for a populated cell.
   The whole chip is a link to the filtered listing. */
.dashboard-grid__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 2.25rem;
  padding: 4px 8px;
  border-radius: 4px;
  background: var(--wa-color-brand-fill-quiet);
  color: var(--op-color-primary);
  font-weight: 700;
  font-size: 14px;
  text-decoration: none;
  transition: background-color 0.15s ease;
}

.dashboard-grid__count:hover {
  background: var(--op-color-accent-light);
  color: var(--op-color-primary-hover);
}

/* Zero-count cell: quiet, no accent — nothing to act on, but still a link to
   confirm the empty slot. */
.dashboard-grid__count--zero {
  background: transparent;
  color: var(--op-color-text-muted);
  font-weight: 400;
}

/* Hidden slot (is_visible=false) — staff still see/work its bookings, but the
   marker signals it is off the public form. */
.dashboard-grid__count--hidden-slot {
  outline: 1px dashed var(--op-color-border);
}

.dashboard-grid__hidden-icon {
  font-size: 10px;
  color: var(--op-color-text-muted);
}

.dashboard-grid__refresh-note {
  display: flex;
  align-items: center;
  gap: 6px;
}

.empty-state--compact {
  padding: 24px 16px;
}

/* ============================================
   Portal media grid (reskin 2026-07-02, Figma 61:4234) — GENERIC, reusable
   thumbnail-card grid for the exhibitor portal's media areas (Gallery now;
   Logos + later media areas reuse the same `.portal-media-*` classes). All
   descendant-scoped under .portal-content so nothing here touches the admin
   app or other surfaces. Token-only (canonical --color-* / --space-* / --radius-*).
   APPENDED — adds nothing to existing rules.
   ============================================ */

/* The grid wrapper + list frame grow so the empty-state card (flex:1) fills the
   area below the page header (mirrors the shared .contacts-index rule; kept as a
   standalone rule so the existing list-fill selector group is untouched). */
.portal-content .gallery-index,
.portal-content #gallery_images-list {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* Responsive card grid — 4-up on a wide desktop (Figma), degrading to fewer
   columns as the panel narrows. min(100%, ..) guards against overflow on very
   narrow viewports. align-content:start keeps rows their natural height rather
   than stretching a single short row down the whole panel. */
.portal-media-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  gap: var(--space-6);                              /* 24px between cards */
  align-content: start;
}

.portal-media-card {
  display: flex;
  flex-direction: column;
  background: var(--color-bg);                      /* white */
  border-radius: var(--radius-md);                  /* 8px */
  overflow: hidden;                                 /* clip the image top corners */
}

/* HPMR-134 - drag affordance + dragging state */
.portal-media-card[draggable="true"] {
  cursor: grab;
}
.portal-media-card--dragging {
  opacity: 0.45;
  cursor: grabbing;
}
.portal-media-card__grip {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius-sm, 4px);
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  pointer-events: none;
}

/* Cover image, fixed Figma aspect ratio (800:534). */
.portal-media-card__image {
  position: relative;
  width: 100%;
  aspect-ratio: 800 / 534;
  background: var(--wa-color-surface-lowered);      /* placeholder tint behind transparent art */
  overflow: hidden;
}
.portal-media-card__image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Placeholder shown when a row has no image_url (defensive; real rows have one). */
.portal-media-card__placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  color: var(--color-text-muted);
  font-size: 28px;
}

/* Top-left order pill overlaid on the image (uses .chip .chip--primary). */
.portal-media-card__badge {
  position: absolute;
  top: var(--space-4);                              /* 16px */
  left: var(--space-4);
}

/* White info area: caption on top, action buttons pinned to the bottom so
   equal-height grid cells line their actions up across a row. */
.portal-media-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);                              /* 16px */
  flex: 1 1 auto;
  padding: var(--space-4);                          /* 16px */
}

.portal-media-card__caption {
  margin: 0;
  font-size: var(--font-size-base);                 /* 16px */
  color: var(--color-text);
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 2;                            /* clamp long filenames to 2 lines */
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.portal-media-card__actions {
  display: flex;
  gap: var(--space-4);                              /* 16px (Figma) */
  margin-top: auto;                                 /* pin to card bottom */
}
.portal-media-card__actions .button {
  flex: 1 1 0;                                      /* Edit + Delete split the row evenly */
  justify-content: center;
}

/* ============================================
   Logos (reskin 2026-07-02, Figma 47:11095 / 48:3632). The approved logo "list"
   node is a TABLE (Preview | URL | Type | Actions), so this reuses the shared
   .contacts-table + .chip families; the only Logos-specific rules are the
   list-fill registration (append — the shared group at ~L1681 is left untouched)
   and the small Preview thumbnail cell.
   ============================================ */

/* Logos list region grows so the empty-state card (flex:1) fills the area below
   the page header, mirroring the shared .contacts-index / #*-list fill group. */
.portal-content #logos-list {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* Preview cell thumbnail — a small framed logo swatch in the first table column
   (Figma 47:11095). Generic name so any future URL-preview table can reuse it. */
.logo-preview {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 40px;
  padding: var(--space-1);                          /* 4px */
  background: #fff;
  border: 1px solid var(--op-color-border);
  border-radius: var(--op-radius-sm);
  overflow: hidden;
}
.logo-preview img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.logo-preview--empty {
  background: var(--wa-color-surface-lowered);
  color: var(--color-text-muted);
}

/* ============================================
   Press Files (reskin 2026-07-02, Figma 62:5646 / 62:6392 / 62:6542). The approved
   list node is a TABLE (File Name | Type | URL | Order | Actions), so this reuses
   the shared .contacts-table + .chip families + the Add/Edit modal-overlay chrome;
   the only Press-Files-specific rule is the list-fill registration below (APPENDED —
   the shared group at ~L1681 is left untouched).
   ============================================ */

/* Press files list region grows so the empty-state card (flex:1) fills the area
   below the page header, mirroring the shared .contacts-index / #*-list fill group. */
.portal-content #press_files-list {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* HPMR-782 - exhibitor New Product Picks / What's New list fill + review UI. */
.portal-content #new_product_picks-list,
.portal-content #whatsnew_events-list {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.editorial-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--op-radius-sm, 4px);
  background: var(--color-bg-alt, #EEEFEF);
}

.editorial-review {
  background: var(--color-bg, #FFFFFF);
  border: 1px solid var(--op-color-border, #D9D9D9);
  border-radius: var(--op-radius-md, 8px);
  padding: 16px 20px;
}

.editorial-review__title {
  font-size: 1.125rem;
  font-weight: 600;
}

.editorial-review__meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 16px;
  margin: 0;
}
.editorial-review__meta dt { color: var(--color-text-muted-strong, #555); font-weight: 500; }
.editorial-review__meta dd { margin: 0; }
.editorial-review__note { max-width: 28rem; }

/* ============================================================================
   Exhibitor Profile (reskin 2026-07-02) — Company Profile screen.
   Figma cyollcfjPhkNPjUWgqqhfI 47:1332 "Profile Content" / 47:2422
   "Showroom & Status" — the two states of ONE white vertical-tab card sitting
   on the grey (.portal-content) panel. Pure-CSS radio tabs (no JS): the two
   hidden #profile-tab-* radios are earlier siblings of .portal-profile__card,
   so the general-sibling combinator toggles the panels + active nav state.

   Portal-scoped only (.portal-profile*) — the admin app + the reskinned shell
   are untouched. Canonical --color-* / --space-* tokens; no hardcoded hex. The
   FA Pro kit swaps <i>->{svg} keeping the element's classes, so icon color/size
   is targeted via a class (never a bare `i` tag).
   APPENDED — nothing above is modified.
   ============================================================================ */

.portal-profile {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);                              /* 24px greeting -> card */
  flex: 1;
  min-height: 0;
}

/* Greeting (Figma "Greeting Section") ------------------------------------- */
.portal-profile__header {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.portal-profile__title {
  margin: 0;
  font-size: var(--h2-size);                        /* 32px (Figma greeting) */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
/* HPMR-610 — `.portal-profile__header` is OUTSIDE `.portal-profile__card`, so
   this subtitle is on the panel. (`.portal-profile__tab`, by contrast, IS
   inside the white card and correctly keeps --color-text-muted.) */
.portal-profile__subtitle {
  margin: 0;
  font-size: var(--font-size-base);                 /* 16px */
  color: var(--color-text-muted-strong);
}

/* Tab shell + card -------------------------------------------------------- */
.portal-profile__tabs {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* Visually-hidden but focusable radios (sr-only). Arrow keys move between
   them (native radio group) which switches tabs; a visible focus ring is
   mirrored onto the active label via :has() below. */
.portal-profile__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.portal-profile__card {
  display: flex;
  flex: 1;
  min-height: 480px;
  background: var(--color-bg);
  border-radius: var(--radius-xl);                  /* 16px (Figma) */
  box-shadow: var(--shadow-sm);
  overflow: hidden;                                 /* clip the rounded corners */
}

/* Left vertical tab menu (Figma "Menu Section") --------------------------- */
.portal-profile__nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex-shrink: 0;
  padding: var(--space-6) 0 var(--space-6) var(--space-6);   /* 24 / 0 / 24 / 24 */
  border-right: 1px solid var(--color-border);
}
.portal-profile__tab {
  display: flex;
  align-items: center;
  min-height: 42px;
  padding: var(--space-2) var(--space-4);
  margin-right: -1px;                               /* straddle the nav divider */
  border-right: 2px solid transparent;
  font-size: var(--font-size-sm);                   /* 14px (Figma) */
  color: var(--color-text-muted);
  white-space: nowrap;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.portal-profile__tab:hover { color: var(--color-text); }

/* Panels (Figma "Profile Info Section") ----------------------------------- */
.portal-profile__panels {
  flex: 1;
  min-width: 0;
  display: flex;
}
.portal-profile__panel {
  display: none;                                    /* the active radio reveals one */
  flex: 1;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
  padding: var(--space-8);                          /* 32px */
}

/* --- Radio-driven tab state (general-sibling combinator, no JS) --- */
#profile-tab-content:checked ~ .portal-profile__card .portal-profile__panel--content { display: flex; }
#profile-tab-showroom:checked ~ .portal-profile__card .portal-profile__panel--showroom { display: flex; }
/* HPMR-671 (sibling sweep) — the same defect in the vertical tab rail of the
   exhibitor Company Profile card: a 14px regular LABEL painted
   --color-accent, 3.28:1 on the card's white background. design.md, HPMR-610:
   "auditing one component family and leaving its siblings is how this class
   of defect keeps coming back". --color-passion-text is 7.52:1 there; the 2px
   right border keeps the accent, exactly as the underline does above. */
#profile-tab-content:checked ~ .portal-profile__card .portal-profile__tab--content,
#profile-tab-showroom:checked ~ .portal-profile__card .portal-profile__tab--showroom {
  color: var(--color-passion-text);
  border-right-color: var(--color-accent);
}
/* Keyboard focus ring on the active label (progressive enhancement). */
.portal-profile__tabs:has(#profile-tab-content:focus-visible) .portal-profile__tab--content,
.portal-profile__tabs:has(#profile-tab-showroom:focus-visible) .portal-profile__tab--showroom {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}

/* Panel header (title + optional right-aligned contact link) --------------- */
.portal-profile__panel-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.portal-profile__panel-title {
  margin: 0;
  font-size: var(--font-size-md);                   /* 18px */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.portal-profile__panel-title-tail {
  margin-left: var(--space-2);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted);
}
.portal-profile__contact-link {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-secondary);
  text-decoration: none;
}
.portal-profile__contact-link:hover,
.portal-profile__contact-link:focus-visible {
  color: var(--color-accent);
  text-decoration: underline;
}

/* The form fills its panel; the action footer is pinned to the bottom. ----- */
.portal-profile__form {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-height: 0;
}
.portal-profile__fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);                              /* 24px between fields (Figma) */
}

/* Two-up row (Logo + Profile Image). Stacks when narrow.
   `.portal-profile__field-row` used to sit alongside this rule and held the
   "Floor + Wing" pair. Wing went with HPMR-641 and Floor is now staff-owned and
   read-only (venue spec §5.6), so the row has no markup left and the rule is
   deleted rather than left dead. */
.portal-profile__media-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
}
.portal-profile__media-row > * { flex: 1 1 320px; min-width: 0; }

/* Media field = 106px preview square + URL input (Figma "Logo Input Container") */
.portal-profile__media-body {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
}
.portal-profile__media-preview {
  flex: 0 0 auto;
  width: 106px;
  height: 106px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--color-bg-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
.portal-profile__media-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* Centered placeholder icon inside the preview square. The media-picker
   controller toggles it (display flex <-> none) opposite the <img>. */
.portal-profile__media-ph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 24px;
  color: var(--color-text-muted);
}

/* Dashed "Upload your image here" dropzone — a big Browse-Media affordance
   (opens the shared media picker). Passion-tinted per Figma 47:1332. */
.portal-profile__dropzone {
  flex: 1 1 0;
  min-width: 0;
  align-self: stretch;
  min-height: 106px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-4);
  text-align: center;
  background: var(--color-passion-20);
  border: 1px dashed var(--color-passion-60);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.portal-profile__dropzone:hover {
  background: var(--color-passion-40);
  border-color: var(--color-passion-80);
}
.portal-profile__dropzone:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}
.portal-profile__dropzone-icon { font-size: 20px; color: var(--color-accent); }
.portal-profile__dropzone-title {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
/* HPMR-610 — same case as `.media-dropzone__hint`: this is a child of the
   `.portal-profile__dropzone` button, so it renders on `--color-passion-20`
   #FCDEDF (3.71:1) and on `--color-passion-40` #F9BDBE while hovered (2.90:1).
   --color-text-secondary #454545 is the only muted-family token that clears AA
   on both (7.60:1 / 5.96:1). */
.portal-profile__dropzone-hint {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}

/* Clear + Browse Media buttons, then the (kept) URL input. */
.portal-profile__media-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.portal-profile__media-url { margin-top: var(--space-2); }

/* Action footer (Figma "Action Section" — top divider, right-aligned) ------ */
.portal-profile__actions {
  margin-top: auto;
  display: flex;
  justify-content: flex-end;
  gap: var(--space-4);
  padding-top: var(--space-8);
  border-top: 1px solid var(--color-bg-alt);
}

/* Locked "Showroom & Status" tiles (Figma "Info Section") ------------------ */
.portal-profile__info-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.portal-profile__info-card {
  flex: 1 1 260px;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.portal-profile__info-label {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.portal-profile__info-value {
  font-size: var(--font-size-base);
  color: var(--color-text);
  word-break: break-word;
}

/* Status chip — solid pill (Figma "Chips/Status"). Brand guide + AA win over
   Figma's #4cbf78: --color-success #16A34A with BOLD >=14px white clears the
   3:1 large-text threshold (~3.3:1); #4cbf78 would fail. */
.portal-profile__status {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
}
.portal-profile__status--active {
  background: var(--color-success);
  color: #fff;
}
/* HPMR-610 — pill fill is #EEEFEF, so its label needs the strong token. */
.portal-profile__status--inactive {
  background: var(--color-bg-alt);
  color: var(--color-text-muted-strong);
}

/* Responsive — stack the tab menu on top below the md breakpoint. --------- */
@media (max-width: 767.98px) {
  .portal-profile__card { flex-direction: column; min-height: 0; }
  .portal-profile__nav {
    flex-direction: row;
    gap: var(--space-2);
    padding: var(--space-4);
    border-right: none;
    border-bottom: 1px solid var(--color-border);
    overflow-x: auto;
  }
  .portal-profile__tab {
    margin-right: 0;
    border-right: none;
    border-bottom: 2px solid transparent;
  }
  #profile-tab-content:checked ~ .portal-profile__card .portal-profile__tab--content,
  #profile-tab-showroom:checked ~ .portal-profile__card .portal-profile__tab--showroom {
    border-right-color: transparent;
    border-bottom-color: var(--color-accent);
  }
  .portal-profile__panel { padding: var(--space-6); }
}

/* ============================================================================
   Underline tab bar (reskin 2026-07-02) — GENERIC, reusable component. First
   used by the exhibitor My Events status filter (Figma cyollcfjPhkNPjUWgqqhfI
   89:8436 tab row: All / Draft / Pending / Approved / Rejected, each with a
   count); intentionally named `.portal-tab*` (not `.events-*`) so Team Members
   (active / pending) and any future portal list can reuse it. Portal-scoped +
   token-only; the admin app + shell are untouched. APPENDED — nothing above is
   modified.
   ============================================================================ */
.portal-tabs {
  display: flex;
  align-items: flex-end;
  gap: var(--space-6);                              /* 24px between tabs (Figma) */
  /* Divider is owned by the container as an INSET shadow (not a border): it
     paints the bottom 1px inside the content box, so the active tab's 2px
     underline overlaps + covers it WITHOUT the tab needing a `margin-bottom:
     -1px` overhang. That overhang was the sole source of the 1px vertical
     overflow that produced a spurious vertical scrollbar (HPMR-500 polish). */
  /* HPMR-882 — NO full-width divider. The inset-shadow rule that used to sit
     here drew a 1px line under every INACTIVE tab across the whole strip; per
     the ticket only the active tab carries an underline (its own 2px
     border-bottom below). Removing the shadow does not reopen the HPMR-500
     scrollbar: no tab overhangs the box (no negative margin). */
  overflow-x: auto;                                 /* horizontal scroll for many-tab rows, never wrap */
  overflow-y: hidden;                               /* belt-and-suspenders: a non-visible overflow-x forces
                                                       overflow-y to compute to `auto`; pin it hidden so no
                                                       stray sub-pixel can ever surface a vertical scrollbar. */
  scrollbar-width: thin;
}
.portal-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);                              /* 8px label -> count */
  padding: var(--space-3) 0;                        /* 12px top/bottom, flush left */
  border: 0;
  background: none;
  border-bottom: 2px solid transparent;
  /* No negative margin: the container's inset-shadow divider means the active
     2px underline already overlaps the divider line. A `margin-bottom: -1px`
     overhang here would re-introduce the 1px vertical overflow / scrollbar. */
  /* HPMR-610 — `background: none` above is the tell: the tab strip is a bare
     <nav> and every shipped `.portal-tabs` (Events, My Events, Shuttle
     Dashboard, SEO Analytics/Search date ranges, Navigation) renders it at page
     level, on the panel. Inactive tabs measured 4.06:1 at 16px regular. */
  color: var(--color-text-muted-strong);
  font-size: var(--font-size-base);                 /* 16px */
  font-weight: var(--font-weight-medium);
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.portal-tab:hover { color: var(--color-text); }
.portal-tab:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.portal-tab--active {
  /* HPMR-671 — the LABEL takes --color-passion-text (6.53:1 on the panel,
     7.52:1 on white), NOT --color-accent (2.85:1 on the panel, 3.28:1 on
     white). Every shipped `.portal-tabs` renders at page level on
     --color-bg-alt, so the panel is the surface that decides. This strip is
     16px semibold and AA relaxes to 3:1 only at >=24px regular / >=18.66px
     bold, so the accent token failed as text on all eleven screens.

     Same correction, same token, same reasoning as the sibling
     `.detail-tabs__tab--active` — the pattern this family should have
     followed from the start (design.md, HPMR-598).

     The 2px underline KEEPS --color-accent: it is the brand mark, and it is
     not the only non-colour carrier — the active tab also steps from
     --font-weight-medium to --font-weight-semibold, so 1.4.1 holds on the
     weight alone. Eight of the ten strips additionally mark the active tab
     `aria-current="page"`; the two SEO date-range strips do NOT, which is a
     4.1.2 gap in those two views' markup, not in this component. */
  color: var(--color-passion-text);
  border-bottom-color: var(--color-accent);
  font-weight: var(--font-weight-semibold);
}
/* Count pill — a quiet number after the label (Figma). Darkens on the active
   tab, muted otherwise. Meaning carried by label + position, not color. */
.portal-tab__count {
  font-size: var(--font-size-sm);                   /* 14px */
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted-strong);            /* HPMR-610 — as .portal-tab */
}
/* HPMR-671 — a count is TEXT (14px regular), so it gets neither the 1.4.11
   non-text carve-out nor the large-text relaxation. It follows the label. */
.portal-tab--active .portal-tab__count { color: var(--color-passion-text); }

/* ============================================================================
   Event status chips (reskin 2026-07-02, Figma 89:8436 Status column). Extend
   the shared `.chip` pill family (L765) with the four approval states. Same AA
   discipline as the contact-type chips: bold >=14px label darkened to clear
   4.5:1 on the pale tint, the leading icon keeps the brighter functional hue
   (meaning carried by icon + text, never color alone). APPENDED.
   ============================================================================ */
.chip--draft    { background: var(--color-passion-20);            color: var(--color-passion-text); }  /* file  ~5.9:1 */
.chip--draft    i, .chip--draft    svg { color: var(--color-accent); }
.chip--pending  { background: var(--wa-color-warning-fill-quiet); color: var(--color-warning-text); }  /* clock ~4.5:1 */
.chip--pending  i, .chip--pending  svg { color: var(--color-warning); }
.chip--approved { background: var(--wa-color-success-fill-quiet); color: var(--color-success-text); }  /* check ~6.5:1 */
.chip--approved i, .chip--approved svg { color: var(--color-success); }
.chip--rejected { background: var(--wa-color-danger-fill-quiet);  color: var(--color-passion-text); }  /* ban   ~6.0:1 */
.chip--rejected i, .chip--rejected svg { color: var(--color-danger); }

/* ============================================================================
   My Events (reskin 2026-07-02, Figma 89:8436 list / 79:6992 empty / confirm
   modals 82:15510 / 79:7913 / 79:7896). Reuses the shared .contacts-table white
   card + .table__* + .chip + .button--*-quiet + .modal--confirm chrome; the only
   Events-specific rules are the list-fill registration, the two-line Title cell
   (wraps + hosts the rejected-reason sub-line), a content-width Actions column,
   and the success (green-icon) confirm-header variant for the Submit modal.
   APPENDED — the shared groups above are left untouched.
   ============================================================================ */

/* List region grows so the empty-state card (flex:1) fills the area below the
   header, mirroring the shared .contacts-index / #*-list fill group. */
.portal-content .events-index,
.portal-content #events-index {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  gap: var(--space-4);                              /* tabs -> table */
}

/* Title column: wrap long titles to 2 lines (Figma) + host the rejected-reason
   sub-line, instead of the shared single-line ellipsis. */
.events-table td.table__cell--title { white-space: normal; }
.table__cell-sub {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--font-size-sm);                   /* 14px */
  font-weight: var(--font-weight-regular);
  color: var(--color-text-muted);
  white-space: normal;
}

/* Actions column shrinks to fit its button row (up to 3 buttons on a Draft row),
   letting the other columns take the slack. Overrides the shared 182px rule
   (later source order + same specificity wins for the .events-table element). */
.events-table th:last-child,
.events-table td:last-child {
  width: 1%;
  white-space: nowrap;
}
.events-table .table__actions { flex-wrap: nowrap; }

/* Inline wrapper for a single confirm-driven action (Submit / Withdraw /
   Delete): keeps the trigger button + its dedicated .modal--confirm together
   under one data-controller="confirm-modal" ancestor (project Stimulus-scoping
   rule) without disturbing the flex action row. The modal itself is
   position:fixed, so it still centers over the viewport. */
.table__action { display: inline-flex; }

/* Submit confirm modal uses a GREEN circle-check (Figma 82:15510) instead of the
   red trash the base .modal__confirm-header forces — target svg too (FA Pro kit
   swaps <i> -> <svg>). */
.modal__confirm-header--success i,
.modal__confirm-header--success svg { color: var(--color-success); }

/* Section subhead helper inside the event form card ("Where is your event?" /
   "Speakers") — a muted line under the .contact-form-card__subheading. */
.contact-form-card__subheading-hint {
  margin: calc(-1 * var(--space-2)) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* ===========================================================================
   Portal EDGE + STATE screens (reskin 2026-07-02) — Figma cyollcfjPhkNPjUWgqqhfI.
   All rules below are portal-scoped (either under .portal-content, or on
   markup that only exists in the exhibitor portal) so the admin surface is
   untouched. Append-only.
=========================================================================== */

/* --- Quota-gated Add button (Figma 273:32657 "My Logos/Limit"). At quota,
   every area header renders `<span class="button disabled">…</span>` inside
   .page-header__actions. Figma shows this as a DISABLED (dimmed) Passion
   primary — not the base grey .button.disabled pill. Scoped to the portal
   page-header action, so the admin app's .button.disabled is unchanged, and
   it applies to every area at once (contacts, logos, brands, gallery, videos,
   press files, social, team members). --- */
.portal-content .page-header__actions .button.disabled,
.portal-content .page-header__actions .button[disabled] {
  background: var(--color-passion);
  color: var(--color-white);
  border-color: var(--color-passion);
  opacity: 0.5;
  font-weight: var(--font-weight-bold);
  cursor: not-allowed;
  pointer-events: none;
}

/* --- Media-picker EMPTY state (Figma 275:33918 "Select Media / Empty").
   Portal-only branch in cms/media_library/picker.html.erb (admin keeps its
   own .empty-state, untouched). The picker modal renders in #media-picker-modal
   at <body> level — OUTSIDE .portal-content — so this state is self-contained
   (it can't inherit the .portal-content empty-state sizing). A grey card fills
   the modal body with a Light image-slash icon + title + hint. --- */
.media-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 360px;
  background: var(--color-bg-alt);               /* #eeefef (Figma #f6f6f6) */
  border-radius: var(--radius-xl);               /* 16px */
}
.media-empty .empty-state { padding: var(--space-8); }
.media-empty .empty-state__icon {
  font-size: 28px;
  color: var(--color-soft-black);
  margin-bottom: var(--space-2);
}
.media-empty .empty-state__title {
  font-size: var(--font-size-md);                /* 18px */
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}
.media-empty .empty-state__description {
  font-size: var(--font-size-base);              /* 16px */
  color: var(--color-text);
  margin: 0;
}

/* --- Portal error pages (Figma 275:34377 = 404, 275:34732 = 403). A white card
   fills the grey .portal-content panel; a faint Passion watermark of the status
   code sits behind the title. Rendered by exhibitor/errors/*.html.erb via the
   exhibitor_portal layout, so .portal-error is a direct flex child of
   .portal-content (flex:1 makes it fill the panel). --- */
.portal-error {
  position: relative;
  flex: 1;
  min-height: 360px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-8);
  background: var(--color-bg);                   /* white */
  border-radius: var(--radius-xl);               /* 16px */
  text-align: center;
  overflow: hidden;
}
.portal-error__code {
  position: absolute;
  top: calc(50% - 46px);                         /* nudge up so it sits behind the title */
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 0;
  font-family: var(--font-family-heading);
  font-weight: var(--font-weight-semibold);
  font-size: 100px;
  line-height: 1;
  color: var(--color-passion-20);                /* faint Passion tint */
  pointer-events: none;
  user-select: none;
}
.portal-error__body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
}
.portal-error__title {
  margin: 0;
  font-size: var(--h3-size);                     /* 24px */
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}
.portal-error__message {
  margin: 0;
  max-width: 640px;
  font-size: var(--font-size-md);                /* 18px */
  color: var(--color-text-muted);
  line-height: 1.4;
}
.portal-error__actions {
  display: flex;
  gap: var(--space-4);
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: var(--space-2);
}

/* ============================================================================
   Exhibitor Account Settings (HPMR-499) — net-new screen.
   Figma cyollcfjPhkNPjUWgqqhfI 287:43413 — one white card (rounded 16px) on the
   grey (.portal-content) panel: identity header (avatar + name/email), a
   First/Last name row, three eye-reveal password fields, and a footer with a
   top divider + right-aligned Save. Canonical --color-* / --space-* tokens +
   Roobert; reuses the shipped .form-field* / .button classes. The FA Pro kit
   swaps <i>->{svg} keeping classes, so the eye/eye-slash state is targeted via
   classes (never a bare `i`). Portal-scoped (.account-settings*) — the admin
   app + reskinned shell are untouched. APPENDED — nothing above is modified.
   ============================================================================ */
.account-settings {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);                              /* 24px header -> card */
  flex: 1;
  min-height: 0;
}

/* Header (Figma "Header Group") ------------------------------------------- */
.account-settings__header {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.account-settings__title {
  margin: 0;
  font-size: var(--h2-size);                        /* 32px (Figma text/32) */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
/* HPMR-610 — header sits outside `.account-settings__card`, i.e. on the panel. */
.account-settings__subtitle {
  margin: 0;
  font-size: var(--font-size-base);                 /* 16px */
  color: var(--color-text-muted-strong);
}

/* Card -------------------------------------------------------------------- */
.account-settings__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);                              /* 24px between blocks */
  padding: var(--space-8);                          /* 32px */
  background: var(--color-bg);
  border-radius: var(--radius-xl);                  /* 16px (Figma) */
  box-shadow: var(--shadow-sm);
}

/* Identity header (avatar + name/email) — display only ------------------- */
.account-settings__identity {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-width: 0;
}
.account-settings__avatar {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--color-passion-20);              /* corrected off-palette #fef2f2 */
  color: var(--color-soft-black);                   /* corrected off-palette #660f11 */
  font-size: var(--h3-size);                        /* 24px initials */
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
}
.account-settings__identity-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.account-settings__name {
  font-size: var(--h3-size);                        /* 24px (Figma text/24) */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
  line-height: 1.3;
}
.account-settings__email {
  font-size: var(--font-size-base);                 /* 16px */
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Form -------------------------------------------------------------------- */
.account-settings__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);                              /* 24px between fields */
}
.account-settings__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
}
.account-settings__row > * { flex: 1 1 280px; min-width: 0; }

/* Footer (Figma "Container" — top divider, right-aligned Save) ------------ */
.account-settings__actions {
  display: flex;
  justify-content: flex-end;
  padding-top: var(--space-4);                      /* 16px */
  border-top: 1px solid var(--color-bg-alt);
}

/* Password reveal button (Figma eye icon). The .form-field__input-wrapper is
   position:relative, so pin the toggle to the right edge + pad the input so
   the value never runs under it. Two icons render; CSS shows the one matching
   the current reveal state (the FA kit preserves .fa-eye / .fa-eye-slash on
   the swapped <svg>). */
.account-settings .form-field__input-wrapper--reveal .form-field__input {
  padding-right: 44px;
}
.account-settings .form-field__reveal {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
}
.account-settings .form-field__reveal:hover { color: var(--color-text); }
.account-settings .form-field__reveal:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.account-settings .form-field__reveal .fa-eye-slash { display: none; }
.account-settings .form-field__reveal.is-revealed .fa-eye { display: none; }
.account-settings .form-field__reveal.is-revealed .fa-eye-slash { display: inline-flex; }

@media (max-width: 575.98px) {
  .account-settings__row { gap: var(--space-4); }
}

/* ============================================================================
   Compact avatar modifier (reskin 2026-07-02, Figma cyollcfjPhkNPjUWgqqhfI
   89:7947). Team Members table rows want a shorter avatar than the 46px
   account-header default — one portal-scoped size override, initials scaled
   down to match. APPENDED — the base `.portal-account__avatar` is untouched.
   ============================================================================ */
.portal-account__avatar--sm { width: 34px; height: 34px; font-size: var(--font-size-sm); }

/* ============================================================================
   ADMIN (HPM Staff) RESKIN — HPMKT design system (HPMR-503 / S1)
   ----------------------------------------------------------------------------
   The admin shell (layouts/application.html.erb) now reuses the exhibitor
   `.portal-*` white-sidebar + rounded grey content panel VERBATIM. This block
   adds ONLY the admin-specific pieces:
     (1) `.portal-content--admin` compatibility shim — restores the pre-reskin
         look of EXISTING admin tables / list-containers / page-headers (which
         the portal-scoped overrides above were tuned for the rebuilt exhibitor
         views and would otherwise change) so not-yet-reskinned staff screens
         keep rendering unchanged inside the new shell;
     (2) `.portal-sidebar--admin` — pin the account footer + scroll the long
         (7-section) admin nav internally;
     (3) reusable admin component scaffolds S2-S6 clone: `.stat-card`,
         `.section-card`, `.list-toolbar`, `.list-table`, the detail-page
         scaffold, and `.status-pill`.
   Canonical --color-* / --space-* / --radius-* tokens only; no hardcoded hex.
   The FA Pro kit swaps <i> -> <svg>, so icon color/size targets a class or the
   `svg` selector, never a bare `i`. APPEND-ONLY — nothing above is modified.
   ============================================================================ */

/* (1) Compatibility shim ---------------------------------------------------
   The `.portal-content …` table/list rules (L648-700) strip the list-container
   card, recolor the header white/muted, and force cells to nowrap+ellipsis —
   correct for the rebuilt exhibitor views, but they would flatten cards and
   overflow WIDE admin tables horizontally out of the panel. Same specificity as
   those rules, APPENDED later -> wins, so admin lists render as they did before
   the reskin. S2-S6 opt into the new `.list-table` per screen. */
.portal-content--admin .list-container {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-bg);
}
.portal-content--admin .table__th {
  background: var(--wa-color-surface-lowered);
  color: var(--color-text);
  font-weight: 600;
  font-size: var(--op-font-size-label);
}
.portal-content--admin .table__tbody-tr td {
  padding: 12px 16px;
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
}
/* Restore the header -> body gap the portal rule zeroes out (panel gap only
   spaces the flashes frame from the view, not the header from its own body). */
.portal-content--admin .page-header { margin-bottom: var(--space-6); }

/* HPMR-880 / HPMR-883 — the space above the page header is the panel's 32px
   padding and nothing else. The #flashes turbo-frame is the panel's first flex
   item; even empty (its toasts are position:fixed) it opened a --space-6 gap,
   so the header sat at 32 + 24 = 56px. `display: contents` drops the frame's
   own box: an empty frame contributes no flex item, and the fixed toast region
   inside it is out of flow either way. Admin layout only — the exhibitor
   portal keeps its impersonation banner -> flashes -> view stacking. */
/* Same trap for `turbo_stream_from` at the top of a view (Campaigns, Media
   Library, SEO Alerts): its empty <turbo-cable-stream-source> became the first
   flex item and pushed the header to 56px. It has no visual content. */
.portal-content--admin > #flashes,
.portal-content--admin > turbo-cable-stream-source { display: contents; }

/* (2) Long admin nav -------------------------------------------------------
   Pin the account footer + scroll the nav internally. Desktop/tablet only — at
   <768 the base sidebar is static/full-width and stacks above the content. */
@media (min-width: 768px) {
  .portal-sidebar--admin { overflow: hidden; }
  .portal-sidebar--admin .portal-sidebar__top {
    flex: 1 1 auto;
    min-height: 0;
  }
  .portal-sidebar--admin .portal-sidebar__nav {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding-bottom: var(--space-2);
    /* Right-side gutter so the internal scrollbar isn't flush against the
       full-width nav-item chips; scrollbar-gutter reserves it so the items
       don't shift if the nav ever fits without scrolling. */
    padding-right: var(--space-2);
    scrollbar-gutter: stable;
  }
  .portal-sidebar--admin .portal-account { flex-shrink: 0; }
}

/* Subtle neutral scrollbar for the long admin sidebar nav ONLY. Overrides the
   global Passion `::-webkit-scrollbar-thumb` (whose 2px-radius rounded ends
   peeked out as small red arcs at the top/bottom scroll extremes — the
   "stray red border artifacts"). Scoped to the admin rail so the public-app +
   every other scroll area keep the global scrollbar untouched. Both the
   standard (`scrollbar-*`) and WebKit pseudo-element declarations resolve to a
   thin, muted-grey thumb, so no Passion shows whichever the browser honors. */
.portal-sidebar--admin .portal-sidebar__nav {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}
.portal-sidebar--admin .portal-sidebar__nav::-webkit-scrollbar { width: 6px; }
.portal-sidebar--admin .portal-sidebar__nav::-webkit-scrollbar-track {
  background: transparent;
  border: 0;
}
.portal-sidebar--admin .portal-sidebar__nav::-webkit-scrollbar-thumb {
  background: var(--color-border);
  border-radius: var(--radius-pill);
  opacity: 1;
}
.portal-sidebar--admin .portal-sidebar__nav::-webkit-scrollbar-thumb:hover {
  background: var(--color-text-muted);
}

/* (3a) Stat cards ----------------------------------------------------------
   Big number + label + tinted icon box (Figma dashboard 97:9475). GENERIC —
   S6 Transportation dashboards reuse it. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
}
.stat-card {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  background: var(--color-bg);
  /* 1px border so the tiles read as distinct cards on BOTH the grey panel (top
     row) AND inside a white .section-card (the Pending Events sub-tiles, which
     were otherwise white-on-white and looked borderless). */
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  padding: var(--space-6);
  color: var(--color-text);
  text-decoration: none;
  transition: box-shadow 0.15s ease, transform 0.1s ease;
}
a.stat-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
  color: var(--color-text);
}
.stat-card__body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.stat-card__label {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stat-card__value {
  font-size: var(--h2-size);                     /* 32px */
  font-weight: var(--font-weight-semibold);
  line-height: 1.1;
  color: var(--color-text);
}
.stat-card__value--sm { font-size: var(--font-size-lg); }   /* long labels (season) */
.stat-card__value--success { color: var(--color-success); }
.stat-card__value--danger  { color: var(--color-danger); }
.stat-card__value--brand   { color: var(--color-accent); }
.stat-card__icon {
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.stat-card__icon i, .stat-card__icon svg { font-size: 20px; color: var(--color-text-muted); }
.stat-card__icon--brand   { background: var(--color-passion-20); }
.stat-card__icon--brand   i, .stat-card__icon--brand   svg { color: var(--color-accent); }
.stat-card__icon--success { background: var(--wa-color-success-fill-quiet); }
.stat-card__icon--success i, .stat-card__icon--success svg { color: var(--color-success); }
.stat-card__icon--warning { background: var(--wa-color-warning-fill-quiet); }
.stat-card__icon--warning i, .stat-card__icon--warning svg { color: var(--color-warning); }
.stat-card__icon--danger  { background: var(--wa-color-danger-fill-quiet); }
.stat-card__icon--danger  i, .stat-card__icon--danger  svg { color: var(--color-danger); }

/* (3b) Section card --------------------------------------------------------
   White rounded panel + optional header (title + trailing action). Used by the
   dashboard panels (Pending Events / Upcoming Transitions) AND the detail-page
   sections (S6). */
.section-card {
  background: var(--color-bg);
  border-radius: var(--radius-xl);
  padding: var(--space-6);
}
.section-card--lg { padding: var(--space-8); }
/* Filled sub-panel — Figma 546:60776 (HPMR-587 / AU-5). A grey-filled panel nested
   INSIDE a screen, with a tighter radius and padding than the white base card.
   Her #F6F6F6 is Figma drift; --color-bg-alt is brand Light Grey #EEEFEF. */
.section-card--filled {
  background: var(--color-bg-alt);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}
.section-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}
.section-card__title {
  margin: 0;
  font-size: var(--h3-size);                     /* 24px */
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.section-card__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-8);
  text-align: center;
  color: var(--color-text-muted);
}
.section-card__empty i, .section-card__empty svg { font-size: 20px; color: var(--color-text-muted); }

/* (3c) List toolbar --------------------------------------------------------
   Search + filter selects + primary/secondary action, right-aligned in the
   list page-header row (Figma exhibitors 132:17466). GENERIC — every S2-S6
   list screen reuses it. */
.list-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);

  /* HPMR-621 — THE FLEX BASE OF A TOOLBAR CELL, DERIVED ONCE.
     `.list-toolbar__control` below is `box-sizing: border-box` (:220), so its
     flex BASE can never resolve below its own horizontal padding + border —
     2 x --space-4 + 2 x 1px. A bare wrapper (`.list-toolbar__search`,
     `.list-toolbar__field`) carries no chrome and would base at 0, and growing
     cells from unequal bases yields unequal finals. Every rule that needs to
     put a chrome-less cell on the same footing as a control references this
     property instead of re-deriving the expression; it had drifted into three
     copies across HPMR-593 / 592 / 589 before this was named.
     Inherited, so it reaches direct children of the toolbar as well. Scoped to
     the component rather than :root — it is this component's geometry, not a
     design token. */
  --list-toolbar-control-chrome: calc(2 * var(--space-4) + 2px);

  /* HPMR-974 — a toolbar SELECT carries more chrome than a text control: it
     shares `.form-field__select`'s 40px right padding (16px inset + 12px
     chevron + 12px clearance), i.e. --space-6 more than the chrome above. A
     border-box select cannot base below its own chrome, so every equal-share
     consumer (the reports band `> *`, the audit band's `--fill` cells) bases
     at the SELECT's chrome — the largest any cell carries — or the selects
     would land exactly 24px wider than the search / date / text cells beside
     them (measured: 149.8 vs 125.8 on Exhibitor Roster at 1400). Derived from
     the property above, never re-derived. */
  --list-toolbar-select-arrow-room: var(--space-6);
  --list-toolbar-cell-base: calc(var(--list-toolbar-control-chrome) + var(--list-toolbar-select-arrow-room));
}
.list-toolbar__control {
  height: 44px;
  padding: 0 var(--space-4);
  font-family: var(--font-family-base);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.list-toolbar__control:focus {
  border-color: var(--color-passion);
  box-shadow: 0 0 0 3px rgba(241, 91, 93, 0.2);
}
select.list-toolbar__control { min-width: 160px; cursor: pointer; }
/* HPMR-888 — filter-control hint text is GREY, never the black value colour.
   Text inputs: the toolbar control had no ::placeholder rule, so it fell to the
   UA default. Selects: an empty-valued "All ..." / "Market Season" blank option
   IS the placeholder, so the closed select paints muted while it is selected;
   the open list keeps real text colour for every option. Same token +
   `opacity: 1` reasoning as HPMR-627 (.form-field__input::placeholder). */
.list-toolbar__control::placeholder,
.list-toolbar .form-field__input::placeholder {
  color: var(--color-text-muted);
  opacity: 1;
}
.list-toolbar select:has(> option:checked[value=""]) { color: var(--color-text-muted); }
.list-toolbar select option { color: var(--color-text); }
/* HPMR-892 (QA reopen) — every filter band that carries the shared Clear
   link (shared/list_filter/_clear, always rendered) snaps to the right edge
   of its container in every state. Clear never appears/disappears, so the
   band's width is constant: zero layout shift when filters change or clear.
   Scoped with :has so non-filter `.list-toolbar` rows keep their layout. */
.list-toolbar:has(.list-filter-clear) { justify-content: flex-end; }
/* The toolbar itself stays full-width (HPMR-918 AC1); only the actions group
   is pushed to the right edge. */
.list-toolbar:has(.list-filter-clear) > .list-toolbar__actions {
  justify-content: flex-end;
  margin-left: auto;
}
.list-toolbar__search { position: relative; display: inline-flex; align-items: center; }
.list-toolbar__search .list-toolbar__control { padding-left: 40px; min-width: 240px; }
.list-toolbar__search-icon {
  position: absolute;
  top: 50%;
  left: var(--space-4);
  transform: translateY(-50%);
  color: var(--color-text-muted);
  font-size: 14px;
  pointer-events: none;
}

/* HPMR-881 - filter feedback for the shared .list-toolbar band.
   [aria-busy] form       - in-flight filter submit (button reads "Filtering...") */
.list-toolbar[aria-busy="true"] .button[disabled],
form[aria-busy="true"] .button[disabled] {
  cursor: progress;
  opacity: 0.7;
}

/* (3d) List table ----------------------------------------------------------
   White rounded table card (Figma list + detail sub-tables). GENERIC — S2-S6
   list screens + the detail-page sub-tables reuse it. Self-contained (does not
   depend on the .portal-content overrides), so it renders identically in admin
   + inside a section-card. */
.list-table {
  width: 100%;
  overflow-x: auto;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.list-table table { width: 100%; border-collapse: collapse; }
/* HPMR-610 — the header strip is #EEEFEF even though the table body is white,
   so the header label alone needed the strong token (4.06:1 -> 5.87:1). The
   body cells below keep --color-text-muted; they really are on white. */
.list-table thead th {
  padding: 12px 16px;
  background: var(--color-bg-alt);
  color: var(--color-text-muted-strong);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-regular);
  text-align: left;
  white-space: nowrap;
  border-bottom: 1px solid var(--color-border);
}
.list-table tbody td {
  padding: 12px 16px;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}
.list-table tbody tr:last-child td { border-bottom: 0; }
.list-table tbody tr:hover { background: rgba(241, 91, 93, 0.03); }
/* When a list-table sits inside a section-card, drop its own frame (the card
   already supplies the white surface) — keep the header rule + row dividers. */
.section-card .list-table { border: 0; border-radius: 0; }

/* (3e) Detail-page scaffold ------------------------------------------------
   Breadcrumb + back/title/subtitle/status-pill header, stacked .section-card
   sections, a 2-col label/value grid, and a sticky action footer (Figma
   reservation 229:19480). S6 exercises the whole scaffold. */
/* HPMR-610 — `.detail-page` is a flex column laid straight onto the panel; the
   breadcrumb, title block and subtitle all sit ABOVE the first `.section-card`,
   so they are on #EEEFEF. (`.detail-field__*` inside those cards is on white
   and correctly keeps --color-text-muted.) */
.detail-page { display: flex; flex-direction: column; gap: var(--space-6); }
.detail-page__breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
.detail-page__breadcrumb a { color: var(--color-text-muted-strong); }
.detail-page__breadcrumb a:hover { color: var(--color-text); }
.detail-page__breadcrumb-current { color: var(--color-text); font-weight: var(--font-weight-medium); }

/* HPMR-879 — ONE breadcrumb slot box for staff admin DETAIL and EDIT pages
   (CoS ruling, Jira 60698): the shared page_header slot and the hand-written
   `.detail-page__breadcrumb--slot` navs. New pages and list pages are not
   opted in and render as before.
   Fixed 24px height + 8px to the title, whatever the crumb holds (or nothing),
   so a page's title sits at the same Y with and without a breadcrumb:
   32px panel padding + 24px slot + 8px = 64px from the panel top.
   When the slot is a direct child of a flex column with a --space-6 gap (the
   admin panel itself, or .detail-page), the negative margin cancels that gap
   so the title still lands 8px below the slot. Admin layout only; the
   exhibitor portal does not use these breadcrumbs. */
.page-header__breadcrumb-slot,
.portal-content--admin .detail-page__breadcrumb--slot {
  height: 24px;
  min-height: 24px;
  margin: 0 0 var(--space-2);
  display: flex;
  align-items: center;
  flex-shrink: 0;
  overflow: hidden;
  white-space: nowrap;
}
.portal-content--admin > .page-header__breadcrumb-slot,
.portal-content--admin > .detail-page__breadcrumb--slot,
.portal-content--admin .detail-page > .page-header__breadcrumb-slot,
.portal-content--admin .detail-page > .detail-page__breadcrumb--slot {
  margin-bottom: calc(var(--space-2) - var(--space-6));
}
.portal-content--admin .detail-page__breadcrumb--slot .detail-page__breadcrumb-current {
  overflow: hidden;
  text-overflow: ellipsis;
}
.detail-page__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.detail-page__heading { display: flex; align-items: center; gap: var(--space-4); min-width: 0; }
.detail-page__titles { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.detail-page__title {
  margin: 0;
  font-size: var(--h2-size);                     /* 32px */
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}
.detail-page__subtitle { margin: 0; font-size: var(--font-size-base); color: var(--color-text-muted-strong); }
.detail-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-6) var(--space-8);
}
.detail-field { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.detail-field__label { font-size: var(--font-size-sm); color: var(--color-text-muted); }
.detail-field__value { font-size: var(--font-size-base); color: var(--color-text); word-break: break-word; }
/* HPMR-591 delta 2 — accented label-value (audit event detail Actor, `494:50302`
   / `494:50335`). Same specificity as the base rule above, so it MUST stay after
   it in source order.
   NOT `--color-accent` / `.text-brand`: #F15B5D on the white modal measures
   3.28:1 and fails WCAG AA for this 16px regular value, and design.md makes AA
   non-negotiable. `--color-passion-text` #9B2C2E is 7.52:1 on white (AA + AAA) —
   the same darkening every chip / callout / status-pill already applies, which
   until now was only ever declared alongside a background. */
.detail-field__value--accent { color: var(--color-passion-text); }
/* HPMR-617 — the THIRD actor colour. Albina, 2026-08-05: "in table row Exhibitor
   is like other actors (System, Admin) because it is common table data
   component, in info popup it is blue (System-black, Admin-primary)". So the
   audit LIST row stays uniform (it already is) and only this modal splits three
   ways: system → `--color-text` #171717, admin_user → `--color-passion-text`
   #9B2C2E, exhibitor_user → here.
   NOT her literal rgb(54,138,217) / #368AD9. Measured on the ACTUAL surface —
   `.modal__inner` is `--op-color-bg` → `--color-bg` #FFFFFF and `.modal__body`
   paints no background of its own, so this value sits on WHITE, not the
   #EEEFEF panel: #368AD9 is 3.62:1 there (3.14:1 if it ever moved to the
   panel), failing AA's 4.5:1 for this 16px regular value — nowhere near the
   >=24px / >=18.66px-bold large-text relaxation. `--color-info-text` #1D4ED8 is
   6.70:1 on white (5.82:1 on the panel), so it clears AA on either surface.
   design.md: the brand guide beats a Figma hex; same correction already applied
   to the Passion actor above and to her #D91414 danger chip. */
.detail-field__value--info { color: var(--color-info-text); }
.detail-footer {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  /* HPMR-1020 — bleed to the panel edges by EXACTLY the panel's own padding.
     `.portal-content` pads 32px above 1024, 24px at 768–1024 (application.css
     tablet block) and 16px on phones, so the bleed tracks one custom property
     per breakpoint instead of a fixed 32px (which overshot the 24px tablet
     padding by 8px and caused a sideways scroll on every New/Edit form). */
  --detail-footer-bleed: var(--space-8);
  margin: 0 calc(-1 * var(--detail-footer-bleed)) calc(-1 * var(--detail-footer-bleed));
  padding: var(--space-4) var(--detail-footer-bleed);
  background: var(--color-bg);
  border-top: 1px solid var(--color-border);
  /* HPMR-1042 - the bar is white and bleeds to the panel's left edge, which is
     exactly where the white sidebar ends, so at the bottom-left it read as one
     surface with the user-profile / Log Out block. A left rule plus a soft
     upward shadow frame it as part of the CONTENT area only. The box (and so
     HPMR-1020's zero edge gap and HPMR-1016's sticky top rule) is unchanged. */
  border-left: 1px solid var(--color-border);
  box-shadow: 0 -4px 12px rgba(23, 23, 23, 0.06);
}
@media (max-width: 1024px) {
  .detail-footer { --detail-footer-bleed: var(--space-6); }
}
@media (max-width: 767.98px) {
  .detail-grid { grid-template-columns: 1fr; }
  .detail-footer { --detail-footer-bleed: var(--space-4); padding-top: var(--space-3); padding-bottom: var(--space-3); }
}

/* (3f) Status pill ---------------------------------------------------------
   Pill + leading icon + tint for the detail-page status + inline sub-table
   statuses (Figma: Confirmed / Unverified / Skipped). AA: label darkened to
   clear 4.5:1 on its pale tint; icon keeps the brighter functional hue (meaning
   carried by icon + text, never color alone). */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  line-height: 1.4;
  white-space: nowrap;
}
.status-pill i, .status-pill svg { font-size: 12px; }
.status-pill--success { background: var(--wa-color-success-fill-quiet); color: var(--color-success-text); }
.status-pill--success i, .status-pill--success svg { color: var(--color-success); }
.status-pill--warning { background: var(--wa-color-warning-fill-quiet); color: var(--color-warning-text); }
.status-pill--warning i, .status-pill--warning svg { color: var(--color-warning); }
.status-pill--danger  { background: var(--wa-color-danger-fill-quiet);  color: var(--color-passion-text); }
.status-pill--danger  i, .status-pill--danger  svg { color: var(--color-danger); }
.status-pill--info    { background: var(--wa-color-info-fill-quiet);    color: var(--color-info); }
.status-pill--info    i, .status-pill--info    svg { color: var(--color-info); }
/* HPMR-610 — as .badge--neutral: the fill is #EEEFEF, so the label needs the
   strong token to clear AA (4.06:1 -> 5.87:1). */
.status-pill--neutral { background: var(--color-bg-alt); color: var(--color-text-muted-strong); }

/* ============================================================================
   ADMIN RESKIN — S2-S6 SHARED-CSS CONSOLIDATION (single application.css owner)
   ----------------------------------------------------------------------------
   The S2-S6 area agents each built their screens from the S1 scaffold above and
   reported a handful of NEW shared rules they wanted promoted out of one-off
   inline styles (they left application.css untouched to stay parallel-safe).
   This block applies those requests: generically named, portal/admin-scoped,
   canonical --color-* / --space-* / --radius-* tokens only, no hardcoded hex, no
   duplicates of existing classes. Overlapping requests are merged into one rule
   (e.g. Registration-States' dashed "not-configured" card + timeline empty card
   both resolve to `.section-card--dashed`). APPEND-ONLY — nothing above touched.
   ============================================================================ */

/* Section-card dashed modifier (Registration States: the "not yet configured"
   state card + the empty timeline step, both a white card with a dashed edge).
   The base `.section-card` is borderless; this only adds the outline. */
.section-card--dashed { border: 1px dashed var(--color-border); }

/* Detail-page status label (Events edit screen top-right "Draft/Pending/…"
   colored text+icon; reusable by any S6 detail header). Meaning is carried by
   the icon + word, never color alone.

   HPMR-610 — the original note here read "every hue clears WCAG AA on white",
   and that was true and irrelevant: this label lives in `.detail-page__header`,
   which is on the PANEL (#EEEFEF), so three of the five hues were being checked
   against a surface they never touch. Re-measured on #EEEFEF:
     neutral  #747474 4.06:1 FAIL -> --color-text-muted-strong   5.87:1
     warning  #B45309 4.36:1 FAIL -> --color-warning-text-strong 6.15:1
     danger   #DC2626 4.19:1 FAIL -> --color-danger-text         5.62:1
     success  #166534 6.19:1 PASS (kept)
     info     #1D4ED8 5.82:1 PASS (kept)
   16px semibold is NOT "large text" (WCAG needs >=18.66px bold or >=24px), so
   4.5:1 is the bar, not 3:1. The ICONS keep the brighter functional hues via
   the sibling rules elsewhere — meaning stays carried by icon + word. */
.detail-status-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-muted-strong);
}
.detail-status-label i, .detail-status-label svg { font-size: 14px; }
.detail-status-label--success { color: var(--color-success-text); }
.detail-status-label--warning { color: var(--color-warning-text-strong); }
.detail-status-label--danger  { color: var(--color-danger-text); }
.detail-status-label--info    { color: var(--color-info-text); }
.detail-status-label--neutral { color: var(--color-text-muted-strong); }

/* Detail-page inline banner (Events: the red "rejected — reason" block; reusable
   for any detail-level notice). Left-accent + tinted fill, matching the
   message-tile pattern in design-patterns.md. Base is neutral; add a modifier. */
.detail-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border-left: 3px solid var(--color-border);
  background: var(--color-bg-alt);
  color: var(--color-text);
  font-size: var(--font-size-sm);
}
.detail-banner i, .detail-banner svg { flex-shrink: 0; margin-top: 2px; }
.detail-banner--danger {
  border-left-color: var(--color-danger);
  background: var(--wa-color-danger-fill-quiet);
  color: var(--color-passion-text);
}
.detail-banner--danger i, .detail-banner--danger svg { color: var(--color-danger); }

/* Detail-footer elevation opt-in (Events: a `position:sticky` footer creates a
   stacking context that trapped a confirm-modal opened from a footer button
   below the fixed sidebar). Opt-in modifier lifts that context above the modal
   layer so footer-launched dialogs paint over the sidebar. */
.detail-footer--elevated { z-index: 1060; }

/* HPMR-1000 - card-style action bar for a short form whose fields sit in ONE
   `.section-card` (Event Location Tags Add / Rename). The base bar bleeds to
   the panel edges with negative margins, which is right when it is the last
   child of the full-height panel, but placed under a card it ran into it. This
   modifier makes it a separate bar below the card: not sticky, no bleed, a
   top gap, the same white surface and padding rhythm as the card, rounded
   border. Buttons keep the base right alignment and gap. */
.detail-footer--card {
  position: static;
  margin: var(--space-4) 0 0;
  padding: var(--space-4) var(--space-6);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  flex-wrap: wrap;
  box-shadow: none;                                 /* HPMR-1042: in-flow card, not a sticky edge */
}
@media (max-width: 767.98px) {
  .detail-footer--card { margin: var(--space-4) 0 0; padding: var(--space-3) var(--space-4); }
}

/* Events index sticky bulk-action bar (reskin 2026-07-03, S4 / Figma 123:14837 +
   123:15757). A floating white card pinned to the bottom of the content column
   while the table scrolls: left = live "N Selected" + Deselect All, right =
   tab-contextual bulk actions. `display` is intentionally OMITTED — the
   bulk-select controller owns the inline none/flex toggle (HPMR-400: a class-level
   `display:flex` would fight the controller's inline `display:none` and leave a
   0-selected bar stuck visible). z-index sits above the table yet below the
   `.modal` layer (1055) so the reject/unpublish dialog still overlays it.

   GENERALIZED 2026-07-23 (HPMR-536 / Phase-2 S1): the identical bar is wanted by
   Broken Links, the Alerts inbox and every future bulk surface, so the canonical
   name is now `.bulk-bar` and `.events-bulk-bar` is retained as a back-compat
   ALIAS on the same rules (selector-list extension only — not one declaration on
   the shipped class changed). New markup uses `.bulk-bar`; the Events index keeps
   working untouched. The `display` omission above is part of the contract and is
   inherited by `.bulk-bar` — `bulk_select_controller` owns the none/flex toggle. */
.bulk-bar,
.events-bulk-bar {
  position: sticky;
  bottom: var(--space-4);
  z-index: 20;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-5);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.bulk-bar__left,
.bulk-bar__right,
.events-bulk-bar__left,
.events-bulk-bar__right {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.bulk-bar__count,
.events-bulk-bar__count {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--font-weight-semibold);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  white-space: nowrap;
}
.bulk-bar__check,
.events-bulk-bar__check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-sm);
  background: var(--color-accent);
  color: var(--color-white);
  font-size: 10px;
}
.bulk-bar__check i,
.bulk-bar__check svg,
.events-bulk-bar__check i,
.events-bulk-bar__check svg { color: var(--color-white); }

/* Page-header lead group (Transportation comms: keep the back+title+subtitle
   lead flexible so a long subtitle doesn't push the primary action below-left in
   the wrap-enabled `.page-header` row). */
.page-header__lead { flex: 1 1 0; min-width: 0; }

/* Dashboard grid section count (Transportation dashboard: the right-aligned
   Passion "N Passengers" tally beside each grid heading — BEM sibling of the
   existing `.dashboard-grid-section__title`). */
.dashboard-grid-section__count {
  color: var(--color-accent);
  font-weight: var(--font-weight-semibold);
  font-size: var(--font-size-sm);
  white-space: nowrap;
}

/* List-table body row-header (Transportation dashboard: the date column is a
   `<th scope="row">`; the S1 `.list-table` rules only style `td`/`thead th`, so
   a body `th` needs the same padding + divider to line up with its row). */
.list-table tbody th {
  padding: var(--space-3) var(--space-4);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-regular);
  color: var(--color-text);
  text-align: left;
  white-space: nowrap;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}
.list-table tbody tr:last-child th { border-bottom: 0; }

/* Form-field checkbox (Transportation comms: `.form-field__checkbox` is
   documented in design-components.md but its CSS was never shipped, so it fell
   back to a native control). Custom Passion box + white check, matching the
   design-system spec. Checkmark drawn with borders (var(--color-white)) so no
   glyph color is hardcoded; focus ring reuses the file's Passion-alpha overlay. */
.form-field__checkbox {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  cursor: pointer;
  display: inline-grid;
  place-content: center;
}
.form-field__checkbox:checked {
  background: var(--color-passion);
  border-color: var(--color-passion);
}
.form-field__checkbox:checked::after {
  content: "";
  width: 5px;
  height: 9px;
  border: solid var(--color-white);
  border-width: 0 2px 2px 0;
  transform: translateY(-1px) rotate(45deg);
}
/* HPMR-677 — THE `outline: none` HERE WAS A 1.4.11 FAILURE, and it was
   invisible precisely because the halo below LOOKS like a focus ring.
   `appearance: none` above means this control is drawn entirely by us, so the
   removed outline was the ONLY thing distinguishing a focused box from an
   unfocused one — the halo composites to roughly #FCDEDF over white, about
   1.26:1, i.e. nothing. Eleven identical 18px boxes stacked in the New
   Scheduled Phase dialog is where that stops being theoretical.
   The ring is `--color-focus` (Soft Black), NOT the global rule's Passion:
   Passion is 3.28:1 on white but only 2.85:1 on the `--color-bg-alt` panel
   several of this class's ~20 call sites sit on, and a focus indicator has to
   clear 3:1 on the WORST surface it reaches (design.md, the two-surface rule).
   The Passion halo is kept as the branded inner cue — it just is not the
   indicator any more. */
.form-field__checkbox:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 3px rgba(241, 91, 93, 0.2);
}

/* Media grid 4-up on the wider admin panel (Media Library). Admin-scoped so the
   exhibitor gallery keeps its 3-up `.portal-media-grid` sizing untouched. */
.portal-content--admin .portal-media-grid {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 248px), 1fr));
}

/* ============================================================================
   ADMIN RESKIN — PHASE 2 SHARED FOUNDATION (HPMR-536 / story S1)
   ----------------------------------------------------------------------------
   Spec: docs/superpowers/specs/2026-07-23-admin-reskin-phase2-spec.md §3.3.
   The CSS carved out of ALL SIX Phase-2 areas (J/K/L/M/N/O), deduped into one
   generic, area-agnostic layer that eleven downstream stories consume. Nothing
   here is named for an area (no `seo-*`, no `bus-*`, no `alert-*`) — an
   area-specific name forces the next agent to fork it.

   Discipline (non-negotiable, per .claude/rules/design.md):
     - canonical --color-* / --space-* / --radius-* / --font-* tokens only;
       ZERO hardcoded hex, size or weight. No NEW --op-* / --wa-* usages.
     - APPEND-ONLY + additive. Nothing above this banner is restyled or renamed;
       the only edits made outside this block were (a) two new :root tokens
       (--font-family-mono, the three --color-phase-* states) and (b) selector-
       list extensions adding `.bulk-bar*` alongside the shipped
       `.events-bulk-bar*` rules.
     - Deliberately NOT built (spec §3.3, explicit): `.pagination-bar__per-page`
       (reverses the HPMR-452 decision, inert without controller support) and
       `.form-field__prefix` (the URL affix — actively misleading, see FU-6).
   ============================================================================ */

/* ----------------------------------------------------------------------------
   (1a) PRODUCTION BUG FIX — `.callout`
   ----------------------------------------------------------------------------
   `.callout` is referenced at TEN call sites and has never existed in CSS: nine
   across eight views (seo/utm/index contributes two, lines 22 and 111) plus
   app/javascript/controllers/block_form_controller.js:57, which injects
   `<div class="callout callout--danger">` as the block-form fetch-failure banner
   with NO inline-style fallback — so the one banner that tells an editor "Failed
   to load form fields. Please try again." renders completely unstyled in
   production today. This is the fourth occurrence of CLAUDE.md Durable Lesson #1
   (after .modal, .badge--info, .form-field__icon).

   Shape follows .claude/rules/design-patterns.md §Notifications > Message Tile:
   left accent border + type-tinted fill + tinted leading icon.

   CO-EXISTENCE CONTRACT (important): several call sites still carry an inline
   `style=` fallback that hardcodes `padding: 16px; border-left: 4px solid …;
   background: …`. Inline styles win, so those sites keep exactly the appearance
   they have today and the class supplies only what the fallback omits (radius,
   type scale, colour, icon treatment, list/paragraph resets). A later story
   deletes the fallbacks and the class takes over cleanly — so there is no
   double-styling collision in either state. For the same reason the base is a
   BLOCK, not a flex row: seo/utm/index.html.erb:111 puts a <strong> and a <div>
   inside one callout, and a flex row would sit them side by side. */
.callout {
  padding: var(--space-4);
  border-left: 4px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-base);
}
/* Leading icon, when the caller puts one first (reports, registration states,
   block_form_controller). Direct-child only, so a callout whose icon lives
   inside a nested `.d-flex` (seo/uptime_targets/index) is left alone. Targets
   BOTH `i` and `svg` — the FA Pro kit swaps <i> -> <svg> (HPMR-497 gotcha). */
.callout > i:first-child,
.callout > svg:first-child { margin-right: var(--space-2); }
/* Callers pass `<ul class="mb-0">` error lists (the form partials) and prose. */
.callout ul,
.callout ol { padding-left: var(--space-5); margin-bottom: 0; }
.callout p:last-child { margin-bottom: 0; }
/* Optional structured slots for new markup; no existing call site uses them. */
.callout__title { font-weight: var(--font-weight-semibold); }
.callout__body  { margin: var(--space-2) 0 0; }

.callout--info    { border-left-color: var(--color-info);    background: var(--color-info-fill);    color: var(--color-info); }
.callout--success { border-left-color: var(--color-success); background: var(--color-success-fill); color: var(--color-success-text); }
.callout--warning { border-left-color: var(--color-warning); background: var(--color-warning-fill); color: var(--color-warning-text); }
.callout--danger  { border-left-color: var(--color-danger);  background: var(--color-danger-fill);  color: var(--color-passion-text); }
.callout--brand   { border-left-color: var(--color-accent);  background: var(--color-brand-fill);   color: var(--color-passion-text); }
/* Icon keeps the brighter functional hue while the label stays at the AA-safe
   darkened text token (meaning carried by icon + text, never colour alone). */
.callout--info    > i:first-child, .callout--info    > svg:first-child { color: var(--color-info); }
.callout--success > i:first-child, .callout--success > svg:first-child { color: var(--color-success); }
.callout--warning > i:first-child, .callout--warning > svg:first-child { color: var(--color-warning); }
.callout--danger  > i:first-child, .callout--danger  > svg:first-child { color: var(--color-danger); }
.callout--brand   > i:first-child, .callout--brand   > svg:first-child { color: var(--color-accent); }
/* Links inside a tinted callout must not fall back to Passion-on-Passion. */
.callout a { color: inherit; text-decoration: underline; }

/* ----------------------------------------------------------------------------
   (1a) PRODUCTION BUG FIX — `.reason-chip`
   ----------------------------------------------------------------------------
   Referenced once, at seo/recommendations/dismiss_confirm.html.erb:60 (the four
   preset dismissal reasons), and never defined.

   Resting = Passion-tinted pill. Selected = filled Passion + white label.
   NOTE (seam, reported to the epic): dismiss_form_controller#highlightChip
   currently paints the selected state with INLINE style.background /
   style.color, which outranks these rules. The class ships now so the view and
   controller can drop their inline styling in story S6; until then the resting
   and selected appearances are unchanged. */
.reason-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-brand-fill);
  color: var(--color-passion-text);
  font-family: var(--font-family-base);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  line-height: 1.2;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.reason-chip:hover { background: var(--color-passion-40); }
.reason-chip:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.reason-chip i, .reason-chip svg { font-size: var(--font-size-xs); }
/* Selected: filled Passion + white label. AA requires white-on-Passion to be
   >= 14px BOLD (design.md §Stack & accessibility) — hence the weight bump.
   The trailing fa-xmark is supplied by the caller. */
.reason-chip--selected {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-white);
  font-weight: var(--font-weight-bold);
}
.reason-chip--selected:hover { background: var(--color-accent-hover); }

/* ----------------------------------------------------------------------------
   (1b-1) ONE bar-track primitive + three label wrappers
   ----------------------------------------------------------------------------
   Areas J (Core Web Vitals x6/page, landing + exit rows) and K (traffic sources)
   each asked for a horizontal bar. Per spec §3.3 this is ONE 6px track with
   three semantic wrappers, not three components.

   Fill width comes from the `--bar-pct` custom property (0-100), set inline by
   the caller: style="--bar-pct: 45;". NEVER a hardcoded percentage, and never a
   `width:` literal in this stylesheet. Colour comes from `--bar-color` or from a
   fill modifier. */
.bar-track,
.threshold-bar__track,
.data-bar-row__track,
.progress-inline__track {
  position: relative;
  width: 100%;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-bg-alt);
  overflow: hidden;
}
.bar-track__fill,
.threshold-bar__fill,
.data-bar-row__fill,
.progress-inline__fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-pill);
  /* clamped so an out-of-range value can never overflow the track */
  width: clamp(0%, calc(var(--bar-pct, 0) * 1%), 100%);
  background: var(--bar-color, var(--color-accent));
  transition: width 200ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .bar-track__fill,
  .threshold-bar__fill,
  .data-bar-row__fill,
  .progress-inline__fill { transition: none; }
}
/* Semantic fill colours (CWV good / needs-improvement / poor, and friends). */
.bar-track__fill--success, .threshold-bar__fill--success, .data-bar-row__fill--success, .progress-inline__fill--success { --bar-color: var(--color-success); }
.bar-track__fill--warning, .threshold-bar__fill--warning, .data-bar-row__fill--warning, .progress-inline__fill--warning { --bar-color: var(--color-warning); }
.bar-track__fill--danger,  .threshold-bar__fill--danger,  .data-bar-row__fill--danger,  .progress-inline__fill--danger  { --bar-color: var(--color-danger); }
.bar-track__fill--info,    .threshold-bar__fill--info,    .data-bar-row__fill--info,    .progress-inline__fill--info    { --bar-color: var(--color-info); }
.bar-track__fill--brand,   .threshold-bar__fill--brand,   .data-bar-row__fill--brand,   .progress-inline__fill--brand   { --bar-color: var(--color-accent); }
.bar-track__fill--neutral, .threshold-bar__fill--neutral, .data-bar-row__fill--neutral, .progress-inline__fill--neutral { --bar-color: var(--color-border-strong); }
.bar-track--lg,
.threshold-bar--lg .threshold-bar__track { height: 10px; }

/* Wrapper 1 — threshold bar: a track under a two-ended label row
   ("0.9s" … "Good < 2.5s"). Used by the six Core Web Vitals metrics. */
.threshold-bar { display: flex; flex-direction: column; gap: var(--space-2); }
.threshold-bar__label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.threshold-bar__label-row strong { color: var(--color-text); font-weight: var(--font-weight-semibold); }

/* Wrapper 2 — data bar row: label + right-aligned value, with the track
   spanning both columns beneath (Top Landing Pages, Exit Pages, traffic). */
.data-bar-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  column-gap: var(--space-4);
  row-gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-3) 0;
}
.data-bar-row + .data-bar-row { border-top: 1px solid var(--color-border); }
.data-bar-row__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--font-size-sm);
  color: var(--color-text);
}
.data-bar-row__label a { color: inherit; }
.data-bar-row__value {
  white-space: nowrap;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
/* Secondary half of a dual value ("12,450 (45%)") stays quiet + regular. */
.data-bar-row__value span { color: var(--color-text-muted); font-weight: var(--font-weight-regular); }
.data-bar-row__track { grid-column: 1 / -1; }

/* Wrapper 3 — inline progress: track and its caption on ONE line. */
.progress-inline { display: flex; align-items: center; gap: var(--space-3); }
.progress-inline__track { flex: 1 1 auto; }
.progress-inline__label {
  flex-shrink: 0;
  white-space: nowrap;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* ----------------------------------------------------------------------------
   (1b-2) `.score-gauge` — donut score dial
   ----------------------------------------------------------------------------
   Eight per Page Audit Details page. NO chart library, NO SVG asset: a
   conic-gradient ring masked into a donut, with the value stacked in the hole.

   Generic knobs, overridable inline or by modifier:
     --gauge-size       outer diameter   (default 112px, per Figma)
     --gauge-thickness  ring width
     --gauge-pct        0-100 progress   (set inline by the caller)
     --gauge-color      ring colour      (or use a colour modifier)
   The mask gradient's opaque stop references a TOKEN (--color-soft-black); a
   mask reads alpha only, so the hue is irrelevant and no hex is hardcoded. */
.score-gauge {
  --gauge-size: 112px;
  --gauge-thickness: 12px;
  --gauge-pct: 0;
  --gauge-color: var(--color-accent);
  position: relative;
  flex-shrink: 0;
  width: var(--gauge-size);
  height: var(--gauge-size);
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
}
.score-gauge::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(
    var(--gauge-color) calc(clamp(0, var(--gauge-pct), 100) * 1%),
    var(--color-border) 0
  );
  -webkit-mask: radial-gradient(farthest-side,
    transparent calc(50% - var(--gauge-thickness)),
    var(--color-soft-black) calc(50% - var(--gauge-thickness) + 1px));
  mask: radial-gradient(farthest-side,
    transparent calc(50% - var(--gauge-thickness)),
    var(--color-soft-black) calc(50% - var(--gauge-thickness) + 1px));
}
.score-gauge__value {
  position: relative;
  font-size: var(--h3-size);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--color-text);
}
/* HPMR-610 — every shipped .score-gauge is hosted inside a .metric-card
   (#EEEFEF), so its 12px caption is measured against grey, not white. */
.score-gauge__label {
  position: relative;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted-strong);
  text-align: center;
}
.score-gauge--sm { --gauge-size: 72px; --gauge-thickness: 8px; }
.score-gauge--sm .score-gauge__value { font-size: var(--font-size-lg); }
.score-gauge--lg { --gauge-size: 144px; --gauge-thickness: 14px; }
.score-gauge--success { --gauge-color: var(--color-success); }
.score-gauge--warning { --gauge-color: var(--color-warning); }
.score-gauge--danger  { --gauge-color: var(--color-danger); }
.score-gauge--info    { --gauge-color: var(--color-info); }
.score-gauge--brand   { --gauge-color: var(--color-accent); }
.score-gauge--neutral { --gauge-color: var(--color-border-strong); }

/* ----------------------------------------------------------------------------
   (1b-3) `.metric-card` — tinted tile that hosts a gauge or a metric
   -------------------------------------------------------------------------- */
.metric-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  text-align: center;
  min-width: 0;
}
/* HPMR-610 — .metric-card is a TINTED tile: its own background is
   --color-bg-alt #EEEFEF, so every label inside it (and the .score-gauge
   dial it hosts) sits on grey, never on the white card behind it. */
.metric-card__label { font-size: var(--font-size-sm); color: var(--color-text-muted-strong); }
.metric-card__value {
  font-size: var(--h3-size);
  font-weight: var(--font-weight-semibold);
  line-height: 1.1;
  color: var(--color-text);
}
.metric-card__sub  { font-size: var(--font-size-xs); color: var(--color-text-muted-strong); }
.metric-card__body { width: 100%; min-width: 0; }
.metric-card--start { align-items: flex-start; text-align: left; }
.metric-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-4);
}

/* ----------------------------------------------------------------------------
   (1b-4) `.split-card` — one card, two labelled halves, 1px vertical divider
   ----------------------------------------------------------------------------
   Landing | Exit pages, Mobile | Desktop. Collapses to stacked on tablet, where
   the divider becomes horizontal (spec §3.3: "must collapse to stacked"). */
.split-card { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.split-card__half { min-width: 0; padding: 0 var(--space-6); }
.split-card__half:first-child { padding-left: 0; }
.split-card__half:last-child  { padding-right: 0; }
.split-card__half + .split-card__half { border-left: 1px solid var(--color-border); }
.split-card__title {
  margin: 0 0 var(--space-4);
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
@media (max-width: 991.98px) {
  .split-card { grid-template-columns: 1fr; }
  .split-card__half { padding: 0; }
  .split-card__half + .split-card__half {
    border-left: 0;
    border-top: 1px solid var(--color-border);
    margin-top: var(--space-6);
    padding-top: var(--space-6);
  }
}

/* ----------------------------------------------------------------------------
   (1b-5) `.section-heading` — bare heading on the panel background
   ----------------------------------------------------------------------------
   Sits OUTSIDE any card (Uptime, Core Web Vitals, Per-Page Quality, Content
   Gaps, Social Preview). Distinct from `.section-card__header`, which lives
   INSIDE the white card. */
.section-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0 0 var(--space-4);
}
.section-heading__title {
  margin: 0;
  font-size: var(--h3-size);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
/* `.section-heading__meta` is declared with `.text-meta` in the Wave-2 seam
   block at the end of this file — one rule, so the two cannot drift. */
.section-heading__actions { display: flex; align-items: center; gap: var(--space-2); }

/* ----------------------------------------------------------------------------
   (1b-6) `.detail-banner` modifiers + `.banner-dismiss`
   ----------------------------------------------------------------------------
   MODIFIERS ON THE SHIPPED `.detail-banner` (line ~3669) — do NOT fork it. The
   base and its `--danger` tint already ship; added here are the three missing
   quiet tints plus the `--solid` treatment Figma draws on the audit run banner
   (J) and the post-Accept CMS-page banner (L): solid fill, a lighter 2px left
   border, a 38px ringed circular icon badge, a white SemiBold-18 title over a
   Regular-16 body in --color-bg-alt.

   Quiet tints first (specificity 0,1,0), solid overrides them at 0,2,0 when both
   classes are present, so `.detail-banner--solid.detail-banner--success` wins
   regardless of source order. */
.detail-banner--success {
  border-left-color: var(--color-success);
  background: var(--color-success-fill);
  color: var(--color-success-text);
}
.detail-banner--success i, .detail-banner--success svg { color: var(--color-success); }
.detail-banner--warning {
  border-left-color: var(--color-warning);
  background: var(--color-warning-fill);
  color: var(--color-warning-text);
}
.detail-banner--warning i, .detail-banner--warning svg { color: var(--color-warning); }
.detail-banner--info {
  border-left-color: var(--color-info);
  background: var(--color-info-fill);
  color: var(--color-info);
}
.detail-banner--info i, .detail-banner--info svg { color: var(--color-info); }

/* Optional structured slots — usable on quiet AND solid banners. */
.detail-banner__content { min-width: 0; flex: 1 1 auto; }
.detail-banner__title {
  margin: 0;
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-heading);
}
.detail-banner__body { margin: var(--space-1) 0 0; font-size: var(--font-size-base); }
/* 38px circular icon badge. The ring is drawn with `currentColor` so it needs no
   colour token of its own and stays correct on every fill. */
.detail-banner__icon {
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  margin-top: 0;
  border-radius: 50%;
  border: 2px solid currentColor;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-base);
}

/* Solid treatment. `--banner-fill` is set by the paired semantic modifier. */
.detail-banner--solid {
  align-items: center;
  padding: var(--space-4) var(--space-5);
  border-left-width: 2px;
  border-left-color: var(--banner-accent, var(--color-border-strong));
  background: var(--banner-fill, var(--color-text));
  color: var(--color-white);
}
.detail-banner--solid i, .detail-banner--solid svg { color: var(--color-white); }
.detail-banner--solid .detail-banner__title { color: var(--color-white); }
.detail-banner--solid .detail-banner__body  { color: var(--color-bg-alt); }
.detail-banner--solid.detail-banner--danger {
  --banner-fill: var(--color-danger);
  --banner-accent: var(--color-danger-fill);
}
.detail-banner--solid.detail-banner--success {
  --banner-fill: var(--color-success);
  --banner-accent: var(--color-success-fill);
}
/* Solid warning uses the DARKENED amber token, not --color-warning: white on
   #F59E0B is ~1.9:1 and fails AA, which is the same a11y correction already
   applied to `.badge--warning` earlier in this file. */
.detail-banner--solid.detail-banner--warning {
  --banner-fill: var(--color-warning-text);
  --banner-accent: var(--color-warning-fill);
}
.detail-banner--solid.detail-banner--info {
  --banner-fill: var(--color-info);
  --banner-accent: var(--color-info-fill);
}
.detail-banner--solid.detail-banner--brand {
  --banner-fill: var(--color-accent);
  --banner-accent: var(--color-passion-20);
}

/* 24px circular dismiss X. CLIENT-SIDE ONLY (spec §3.4): the banners report live
   DB state and must return on reload, so nothing here implies persistence. */
.banner-dismiss {
  flex-shrink: 0;
  align-self: flex-start;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-xs);
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 120ms ease, background 120ms ease;
}
.banner-dismiss:hover { opacity: 1; background: var(--color-bg-alt); }
.detail-banner--solid .banner-dismiss:hover { background: var(--banner-accent, var(--color-bg-alt)); }
.banner-dismiss:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; opacity: 1; }
.banner-dismiss i, .banner-dismiss svg { color: inherit; }

/* ----------------------------------------------------------------------------
   (1b-7) `.timeline-*` — promoted out of an inline <style> block
   ----------------------------------------------------------------------------
   These rules existed ONLY as an inline <style> at seo/audits/timeline.html.erb
   (the single instance of inline <style> in the reskinned admin), written
   against legacy --op-* tokens. Promoted here verbatim in behaviour, re-pointed
   at canonical tokens, and extended with the per-kind tinted 32px icon tile the
   Figma frame adds.

   The `--timeline-color` custom-property contract is PRESERVED byte-for-byte:
   the view sets `style="--timeline-color: …"` per event and the dot still reads
   it. While the inline <style> block remains in the view it wins on document
   order (identical selectors, later in the document), so this promotion changes
   nothing until story S3 deletes it. */
.timeline-rail {
  position: relative;
  margin-left: var(--space-4);
  padding-left: var(--space-8);
  border-left: 2px solid var(--color-border);
}
.timeline-event {
  position: relative;
  padding: var(--space-3) 0;
  margin-bottom: var(--space-1);
}
.timeline-event::before {
  content: "";
  position: absolute;
  left: -41px;
  top: 18px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid var(--color-bg);
  background: var(--timeline-color, var(--color-accent));
}
.timeline-event__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: var(--font-size-sm);
}
.timeline-event__title { font-weight: var(--font-weight-semibold); color: var(--color-text); }
.timeline-event__title a { color: var(--color-text); text-decoration: none; }
.timeline-event__title a:hover { text-decoration: underline; }
.timeline-event__actor { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.timeline-event__time {
  white-space: nowrap;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.timeline-event__summary {
  margin-top: var(--space-1);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* HPMR-610 — the day marker is the ONE part of the audit timeline that is not
   in a card: `seo/audits/timeline` emits it BETWEEN `.section-card` blocks, so
   it renders on the panel while `.timeline-event__*` around it renders on
   white. 12px bold is not "large text" (WCAG wants >=18.66px bold), so 4.5:1
   is the bar and #747474 was at 4.06:1. */
.timeline-day-marker {
  margin: var(--space-6) 0 var(--space-2) calc(-1 * var(--space-4));
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--color-text-muted-strong);
}
/* NEW (Figma): per-kind tinted 32px icon tile leading the event head. The tint
   follows the event kind via a modifier — audit / CMS edit / alert fire — and
   every hue is an existing semantic token. */
.timeline-event__icon {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-sm);
}
.timeline-event__icon i, .timeline-event__icon svg { color: var(--color-text-muted); }
.timeline-event__icon--brand   { background: var(--color-passion-20); }
.timeline-event__icon--brand   i, .timeline-event__icon--brand   svg { color: var(--color-accent); }
.timeline-event__icon--success { background: var(--color-success-fill); }
.timeline-event__icon--success i, .timeline-event__icon--success svg { color: var(--color-success); }
.timeline-event__icon--warning { background: var(--color-warning-fill); }
.timeline-event__icon--warning i, .timeline-event__icon--warning svg { color: var(--color-warning); }
.timeline-event__icon--danger  { background: var(--color-danger-fill); }
.timeline-event__icon--danger  i, .timeline-event__icon--danger  svg { color: var(--color-danger); }
.timeline-event__icon--info    { background: var(--color-info-fill); }
.timeline-event__icon--info    i, .timeline-event__icon--info    svg { color: var(--color-info); }

/* ----------------------------------------------------------------------------
   (1b-8) `.stat-card__head` / `__info` / `__sub` + `.tooltip-panel`
   ----------------------------------------------------------------------------
   The reskinned `.stat-card` has no tooltip element, but every shipped
   `_kpi_card` render passes a `tooltip:` — the only explanatory text for
   Visibility Score, Freshness, etc. Retiring `_kpi_card` without an info
   affordance would be a SILENT INFORMATION REGRESSION (spec §3.3).

   `__head` hosts label + info trigger on one line. It is a NEW optional element
   rather than a change to `.stat-card__label`, because that label carries
   `overflow: hidden; white-space: nowrap` and would clip an appended icon — and
   because `.stat-card` itself must not be modified. */
.stat-card__head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
/* `.stat-card__label` ellipsises via overflow:hidden, which only works if the
   flex item is allowed to shrink below its content width. Contextual rule so the
   shipped `.stat-card__label` declaration block stays untouched. */
.stat-card__head .stat-card__label { min-width: 0; flex: 0 1 auto; }
.stat-card__info {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  line-height: 1;
  cursor: pointer;
}
.stat-card__info:hover { color: var(--color-text); }
.stat-card__info:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; border-radius: var(--radius-sm); }
/* Inline sub-value under the big number ("Median 38 words"). */
.stat-card__sub { font-size: var(--font-size-xs); color: var(--color-text-muted); }

/* Tooltip panel driven by the existing `seo_tooltip_controller` (retires its
   inline-styled panel). The controller positions with inline top/left and
   toggles with inline `display: block|none`, both of which outrank these rules —
   so `display: none` here is a SAFE default (it only governs the first paint,
   before any toggle) rather than a fight with the controller. */
.tooltip-panel {
  display: none;
  position: fixed;
  z-index: 1000;
  width: 280px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-text);
  color: var(--color-white);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-regular);
  line-height: 1.5;
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  box-shadow: var(--shadow-md);
}

/* HPMR-917 reopen — multi-line info tooltip: one block line per Array entry. */
.tooltip-panel__line {
  display: block;
}

/* HPMR-798 — `shared/_info_tooltip`: the (i) help affordance beside an admin
   control or form label, driving the same `seo-tooltip` controller and the
   same `.tooltip-panel` as `.stat-card__info` above.

   The trigger is a real <button>. Its glyph paints --color-text-muted-strong,
   NOT --color-text-muted: it sits on BOTH surfaces — white cards and the
   #EEEFEF .portal-content panel (the event edit header, the page edit header)
   — where #747474 is 4.06:1 and #585C5E is 5.87:1 (6.76:1 on white). It is a
   non-text glyph, so 1.4.11's 3:1 is the bar either way; the strong token
   keeps it clear of the two-surface trap regardless. Pinned by
   spec/views/shared/muted_token_surface_guard_spec.rb.

   24x24 minimum hit area: WCAG 2.5.8 target size. */
.info-tooltip {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}
.info-tooltip__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-text-muted-strong);
  font-size: var(--font-size-sm);
  line-height: 1;
  cursor: help;
}
.info-tooltip__trigger:hover { color: var(--color-text); }
/* HPMR-885 (Figma 115-4335) — the (i) beside a page <h1>: FA Light glyph at
   24px in greys/500, no background/border/padding. Same 24x24 hit area, hover,
   focus ring and Esc behaviour as the base trigger. */
.info-tooltip__trigger--page-header {
  color: var(--color-page-header-info);
  font-size: 24px;
  height: 1em;
}
/* HPMR-885 reopen — the glyph box IS the circle: 1em square, block-level, no
   baseline. Covers both Font Awesome render modes: the kit's SVG replacement
   (`svg.svg-inline--fa`, whose core CSS adds `vertical-align: -.125em` and
   would drop the circle 3px below the baseline) and the webfont <i>. The
   circle-info glyph fills its 512x512 box, so box bottom = circle bottom. */
.info-tooltip__trigger--page-header > i,
.info-tooltip__trigger--page-header > svg {
  display: block;
  width: 1em;
  height: 1em;
  line-height: 1;
  vertical-align: 0;
  overflow: visible;
  contain: layout; /* no text baseline: the webfont glyph's own baseline would
                      otherwise win and leave the circle 3px low */
}
.info-tooltip__trigger:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; border-radius: var(--radius-sm); }

/* A `.form-field__label` with a trailing (i). The label is display:block with
   its own 4px bottom margin; inside the row the ROW carries that margin so the
   label-to-input rhythm is unchanged. No colour — the label keeps its own. */
.form-field__label-row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin-bottom: 4px;
}
.form-field__label-row .form-field__label { margin-bottom: 0; }

/* ----------------------------------------------------------------------------
   (1b-9) `.list-table` cell modifiers
   ----------------------------------------------------------------------------
   Retires ~22 inline `style=` attributes across the SEO + Transportation views:
   ~12 `font-family: ui-monospace,…` (URLs, measurement IDs, lat/lng) and ~10
   `color: … ; font-weight: 600` warning/danger cells. Required BEFORE those
   views can be de-inlined at all. */
.list-table__cell--mono {
  font-family: var(--font-family-mono);
  font-size: var(--font-size-xs);
  word-break: break-all;
}
.list-table__cell--warning { color: var(--color-warning-text); font-weight: var(--font-weight-semibold); }
.list-table__cell--danger  { color: var(--color-danger);       font-weight: var(--font-weight-semibold); }
.list-table__cell--success { color: var(--color-success-text); font-weight: var(--font-weight-semibold); }
.list-table__cell--muted   { color: var(--color-text-muted); }
.list-table__cell--numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.list-table__cell--nowrap  { white-space: nowrap; }
/* Secondary line under a primary cell value (dual-line cells). */
.list-table__cell-sub { display: block; font-size: var(--font-size-xs); color: var(--color-text-muted); }

/* ----------------------------------------------------------------------------
   (1b-10) `.list-table--reorderable` + `__handle` + `__row--child`
   ----------------------------------------------------------------------------
   Two areas independently specced this — the Navigation Items tree (grip +
   indented child rows) and Bus Stops drag/drop — so it is built ONCE, generic.

   NOTE: both shipped controllers (`sortable_navigation`, `bus_stops_reorder`)
   currently signal the dragging row with an inline `style.opacity`. The
   `--dragging` / `--drop-target` classes below are the token-driven replacement
   those controllers can adopt; they are inert until then and collide with
   nothing. */
.list-table--reorderable tbody tr { cursor: default; }
.list-table--reorderable [draggable="true"] { cursor: grab; }
.list-table--reorderable [draggable="true"]:active { cursor: grabbing; }
.list-table__row--dragging { opacity: 0.4; }
.list-table__row--drop-target td,
.list-table__row--drop-target th { box-shadow: inset 0 2px 0 var(--color-accent); }
/* Grip column: fixed width so the handle never steals space from content. */
.list-table__handle {
  width: 36px;
  color: var(--color-border-strong);
  text-align: center;
  cursor: grab;
  user-select: none;
}
.list-table__handle:hover { color: var(--color-text-muted); }
.list-table__handle i, .list-table__handle svg { font-size: var(--font-size-sm); }
/* Nested child row: indent the first cell and let the caller prefix an
   `fa-arrow-turn-down-right`. Depth is overridable via `--row-depth`. */
.list-table__row--child > td:first-child,
.list-table__row--child > th:first-child {
  padding-left: calc(var(--space-4) + (var(--row-depth, 1) * var(--space-8)));
}
.list-table__row-prefix { margin-right: var(--space-2); color: var(--color-border-strong); }

/* ----------------------------------------------------------------------------
   (1b-11) `.repeat-row-table` — bordered inline-input repeatable rows
   ----------------------------------------------------------------------------
   Service Days, Route Points, and any future add-a-row editor: `#` index /
   inline fields / per-row quiet-danger Delete, with a footer "Add …" button. */
.repeat-row-table {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-bg);
}
.repeat-row-table__head,
.repeat-row-table__row {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) 44px;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
}
/* HPMR-610 — head strip is #EEEFEF (the rows below are white). */
.repeat-row-table__head {
  background: var(--color-bg-alt);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
.repeat-row-table__row + .repeat-row-table__row { border-top: 1px solid var(--color-border); }
.repeat-row-table__index {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-muted);
  text-align: right;
}
/* The fields cell is itself a responsive grid so a row can hold 1..n inputs. */
.repeat-row-table__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-3);
  min-width: 0;
}
.repeat-row-table__fields .form-field { margin-bottom: 0; }
.repeat-row-table__actions { display: inline-flex; justify-content: flex-end; }
.repeat-row-table__footer {
  padding: var(--space-3);
  border-top: 1px solid var(--color-border);
  background: var(--color-bg-alt);
}
.repeat-row-table__empty {
  padding: var(--space-6) var(--space-3);
  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* ----------------------------------------------------------------------------
   (1b-12) `.list-toolbar--end` + `.portal-tab` icon variant
   -------------------------------------------------------------------------- */
/* Time Slots left-align their toolbar; Bus Lines / Schedules / Redirects / FAQs
   right-align. `.list-toolbar` sets no justify-content, so this is purely
   additive. */
.list-toolbar--end { justify-content: flex-end; }

/* Icon-bearing tab (Navigation Items: Header / Footer / Mobile). The icon is
   spaced by the tab's OWN `gap: var(--space-2)` — which applies to anonymous
   flex items, so a bare text label is spaced correctly too. A `margin-right`
   here (as sketched in the spec) would DOUBLE that gap to 16px, so only the
   sizing rule is shipped. Targets BOTH `i` and `svg`: the FA Pro kit swaps
   <i> -> <svg> and an `i`-only selector silently no-ops (HPMR-497 gotcha). */
.portal-tab i, .portal-tab svg { font-size: var(--font-size-sm); }
/* HPMR-671 — the icon follows the label. design.md lets a DECORATIVE glyph
   stay on the muted token because 1.4.11 asks only 3:1 of non-text content,
   but --color-accent does not clear even that on the panel (2.85:1) — and
   leaving it accent while the label darkens paints a two-tone tab. */
.portal-tab--active i, .portal-tab--active svg { color: var(--color-passion-text); }

/* ----------------------------------------------------------------------------
   (1b-13) Semantic `.chip--*` variants
   ----------------------------------------------------------------------------
   Extends the shipped `.chip` pill family. Severity (J/K/L), recommendation
   status (L), priority (L), enabled state (L), airport code (O). ALL map onto
   existing semantic tokens — no new colours — and keep the shipped AA
   discipline: bold >= 14px label at the darkened text token, leading icon at the
   brighter functional hue (meaning carried by icon + text, never colour alone). */
/* Severity */
.chip--critical { background: var(--color-danger-fill);  color: var(--color-passion-text); }
.chip--critical i, .chip--critical svg { color: var(--color-danger); }
.chip--warning  { background: var(--color-warning-fill); color: var(--color-warning-text); }
.chip--warning  i, .chip--warning  svg { color: var(--color-warning); }
.chip--info     { background: var(--color-info-fill);    color: var(--color-info); }
.chip--info     i, .chip--info     svg { color: var(--color-info); }
.chip--success  { background: var(--color-success-fill); color: var(--color-success-text); }
.chip--success  i, .chip--success  svg { color: var(--color-success); }
/* HPMR-610 — the four neutral-tint chips fill with --color-bg-alt #EEEFEF, so
   their labels measured 4.06:1. Same correction the coloured chips already
   carry on their own tints (--color-*-text). */
.chip--neutral  { background: var(--color-bg-alt);                color: var(--color-text-muted-strong); }
/* Recommendation status */
.chip--new         { background: var(--color-passion-20);            color: var(--color-passion-text); }
.chip--new         i, .chip--new         svg { color: var(--color-accent); }
.chip--accepted    { background: var(--color-info-fill);    color: var(--color-info); }
.chip--implemented { background: var(--color-success-fill); color: var(--color-success-text); }
.chip--implemented i, .chip--implemented svg { color: var(--color-success); }
.chip--dismissed   { background: var(--color-bg-alt);                color: var(--color-text-muted-strong); }
/* Priority */
.chip--high   { background: var(--color-danger-fill);  color: var(--color-passion-text); }
.chip--high   i, .chip--high   svg { color: var(--color-danger); }
.chip--medium { background: var(--color-warning-fill); color: var(--color-warning-text); }
.chip--medium i, .chip--medium svg { color: var(--color-warning); }
.chip--low    { background: var(--color-bg-alt);                color: var(--color-text-muted-strong); }
/* Enabled / disabled toggle state */
.chip--on  { background: var(--color-success-fill); color: var(--color-success-text); }
.chip--on  i, .chip--on  svg { color: var(--color-success); }
.chip--off { background: var(--color-bg-alt); color: var(--color-text-muted-strong); }
/* Airport / short code: mono, uppercase, tabular — quiet neutral tint. */
.chip--code {
  background: var(--color-bg-alt);
  color: var(--color-text);
  font-family: var(--font-family-mono);
  font-size: var(--font-size-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}
/* Small chip for dense table cells. */
.chip--sm { padding: var(--space-1) var(--space-2); font-size: var(--font-size-xs); }

/* ----------------------------------------------------------------------------
   (1b-14) `.code-block` — grey inset, mono, preserved whitespace
   ----------------------------------------------------------------------------
   Recommendation "Evidence" JSON and any other verbatim payload. Owns its OWN
   `overflow-x: auto` container so a long line scrolls inside the block and never
   makes the page scroll horizontally. */
.code-block {
  margin: 0;
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  color: var(--color-text);
  font-family: var(--font-family-mono);
  font-size: var(--font-size-xs);
  line-height: 1.6;
  white-space: pre;
  overflow-x: auto;
  max-height: 420px;
  overflow-y: auto;
}
.code-block--wrap { white-space: pre-wrap; word-break: break-word; overflow-x: visible; }
.code-inline {
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  font-family: var(--font-family-mono);
  font-size: var(--font-size-xs);
  word-break: break-all;
}

/* ----------------------------------------------------------------------------
   (1b-15) `.detail-page__body` — page-level main + right-rail grid
   ----------------------------------------------------------------------------
   Distinct from the shipped `.detail-grid`, which is a label/value grid INSIDE a
   card. This is the two-column page body (main content + right rail) used by the
   Recommendation detail and every future SEO detail screen. Collapses to one
   column on tablet, rail LAST so the reading order stays main-then-rail. */
.detail-page__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  gap: var(--space-6);
  align-items: start;
}
.detail-page__main,
.detail-page__rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}
@media (max-width: 991.98px) {
  .detail-page__body { grid-template-columns: 1fr; }
}

/* ----------------------------------------------------------------------------
   (1b-16) `.empty-state--inline` — left-aligned muted panel copy
   ----------------------------------------------------------------------------
   For the long HPMR-526 / HPMR-528 explanatory paragraphs. The shipped
   `.empty-state--card` centres its content in a 320px-tall card, which
   compresses 60-90 words into an unreadable column — preservation item P-8
   explicitly forbids that. This variant keeps the copy left-aligned and
   readable, with no card chrome of its own. */
.empty-state--inline {
  display: block;
  flex: none;
  min-height: 0;
  padding: var(--space-4) 0;
  text-align: left;
  background: none;
}
.empty-state--inline .empty-state {
  padding: 0;
  text-align: left;
}
.empty-state--inline .empty-state__icon {
  font-size: var(--font-size-base);
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}
.empty-state--inline .empty-state__title {
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
/* HPMR-610 — this override is MORE specific than the base rule, so leaving it
   on --color-text-muted would silently undo the base fix for every inline
   empty state. `.empty-state--inline` sets `background: none`, i.e. it is
   surface-agnostic by construction; it takes the strong token too. */
.empty-state--inline .empty-state__description {
  max-width: 68ch;
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
.empty-state--inline .empty-state__description + .empty-state__description { margin-top: var(--space-3); }

/* ----------------------------------------------------------------------------
   (1b-17) `.provider-card` — Tracking Setup provider tile
   ----------------------------------------------------------------------------
   UNDESIGNED IN FIGMA (spec §3.3) — inferred from `.section-card` + `.chip--*`
   and flagged for Albina. Name + type chip + mono measurement ID + last
   health-check timestamp + health pill + an edit/delete action row. */
.provider-card {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
}
.provider-card__body { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.provider-card__title-row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; min-width: 0; }
.provider-card__name {
  margin: 0;
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.provider-card__id {
  font-family: var(--font-family-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
  word-break: break-all;
}
.provider-card__meta { font-size: var(--font-size-xs); color: var(--color-text-muted); }
.provider-card__actions { display: inline-flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
.provider-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-4);
}

/* ----------------------------------------------------------------------------
   (1b-18) `.route-map-panel` — Leaflet viewport + stops + coordinate editor
   ----------------------------------------------------------------------------
   The largest net-new CSS in the epic. Two-column card: map viewport left,
   stops mini-list right, coordinate editor below spanning both.

   NOTE: the legacy `.route-map__*` block earlier in this file (HPMR-459) is
   LEFT UNTOUCHED — story S10 retires it when `_route_map.html.erb` is rewritten.
   The new panel is deliberately a different BEM block so both can coexist during
   that migration. Leaflet requires an explicit height on its container. */
.route-map-panel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--space-4);
  align-items: start;
}
.route-map-panel__map {
  height: 400px;
  min-width: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  overflow: hidden;
}
.route-map-panel__stops {
  max-height: 400px;
  overflow-y: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
}
/* HPMR-610 — sticky header fills with #EEEFEF (the stop rows below are white). */
.route-map-panel__stops-header {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg-alt);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
.route-map-panel__stop {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  cursor: pointer;
}
.route-map-panel__stop + .route-map-panel__stop { border-top: 1px solid var(--color-border); }
.route-map-panel__stop:hover { background: var(--color-bg-alt); }
.route-map-panel__stop--active { background: var(--color-passion-20); }
.route-map-panel__stop-index {
  flex: 0 0 2rem;
  text-align: right;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-muted);
}
.route-map-panel__stop-coords {
  font-family: var(--font-family-mono);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* Coordinate (vertex) editor below the map, spanning the full panel width. */
.route-map-panel__points { grid-column: 1 / -1; min-width: 0; }
.route-map-panel__empty {
  padding: var(--space-8);
  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
@media (max-width: 991.98px) {
  .route-map-panel { grid-template-columns: 1fr; }
  .route-map-panel__stops { max-height: 280px; }
}

/* ----------------------------------------------------------------------------
   (1b-19) `.gantt-chart*` + `.phase-legend*` — scheduled-phase timeline
   ----------------------------------------------------------------------------
   For the Edit Page V2 phase timeline (spec §2.4 / BG-18 / BG-20). Replaces the
   pre-reskin single-lane <canvas> with hardcoded hexes: one DOM row per phase,
   colour-coded by the three-state phase scale (--color-phase-* in :root).

   Geometry contract, set inline by the consuming Stimulus controller as
   percentages of the plot width — NEVER a literal here:
     --bar-start   left offset (0-100)
     --bar-span    width       (0-100)
     --today-at    TODAY marker offset (0-100), on the chart root
   `--bar-span` has a minimum so a zero-length phase is still clickable, and
   `--narrow` drops the IN-BAR label on a too-small bar (HPMR-678: the phase
   name still shows, in the row's own label cell).
   BG-20 (nil `start_at` = "Immediately") is a PRODUCT decision, not a CSS one:
   this layer only guarantees a bar clamped into the plot, never off its left
   edge.

   HPMR-678 (Figma 741:73248) — THE FIXED PHASE-NAME COLUMN.
   Every row is now `[ name | track ]`: a fixed-width, right-bordered label cell
   holding the phase name, then the plot track the bar is positioned inside. The
   name is therefore ALWAYS legible, including for a one-day phase whose bar is
   a few pixels wide — which is the whole point, and which no amount of in-bar
   labelling can achieve.

   `--gantt-label-w` IS THE SINGLE SOURCE OF THAT OFFSET, and four rules read
   it: the name cell's flex-basis, the gridline overlay's left inset, the TODAY
   marker's origin, and the month axis's padding. They must agree or the bars
   stop lining up with the months, so they are deliberately expressed against
   one custom property rather than four copies of a number. Overriding it in one
   media query below moves all four together.

   `* { box-sizing: border-box }` (:300) is load-bearing here: the name cell's
   padding and 1px right border are INSIDE its 160px, so `padding-left` of the
   same value on the axis lands on exactly the same pixel. */
.gantt-chart {
  --gantt-label-w: 160px;
  --gantt-row-h: 32px;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.gantt-chart__toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}
/* HPMR-678 — offset by the label column so `justify-content: space-between`
   distributes the month ticks across the TRACK rather than the whole plot.
   Without this the axis is shifted left of every bar by exactly one label
   column and the chart silently reads a month early. */
.gantt-chart__axis {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-left: var(--gantt-label-w);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
/* Plot area. `__grid` paints evenly spaced vertical guides without extra DOM;
   the column count is overridable via --gantt-columns. */
.gantt-chart__rows {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) 0;
  min-width: 0;
}
.gantt-chart__grid {
  position: absolute;
  /* HPMR-678 — left-inset by the label column: the gridlines belong to the
     track, and a gradient started at the plot's left edge would put them at
     different offsets from the bars they are supposed to be guides for. */
  inset: 0 0 0 var(--gantt-label-w);
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to right,
    var(--color-border) 0,
    var(--color-border) 1px,
    transparent 1px,
    transparent calc(100% / var(--gantt-columns, 6))
  );
  opacity: 0.6;
}
.gantt-chart__row {
  position: relative;
  display: flex;
  align-items: stretch;
  height: var(--gantt-row-h);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
}
/* HPMR-678 — the fixed name cell. `display: block` (the flex-item default for a
   <span> given a flex-basis) rather than an inner flexbox, because
   `text-overflow: ellipsis` only applies to a BLOCK container: a long phase name
   in a flex box overflows unclipped instead of truncating. Vertical centring is
   therefore `line-height`, pinned to the same --gantt-row-h the row uses. */
.gantt-chart__row-name {
  flex: 0 0 var(--gantt-label-w);
  min-width: 0;
  padding: 0 var(--space-3);
  border-right: 1px solid var(--color-border);
  font-size: var(--font-size-xs);
  line-height: var(--gantt-row-h);
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The bar's containing block. `min-width: 0` stops a flex item's automatic
   minimum size from refusing to shrink below its content and pushing the track
   past the plot's right edge. */
.gantt-chart__track {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}
.gantt-chart__bar {
  position: absolute;
  top: 0;
  bottom: 0;
  left: clamp(0%, calc(var(--bar-start, 0) * 1%), 100%);
  width: max(2%, clamp(0%, calc(var(--bar-span, 0) * 1%), 100%));
  max-width: calc(100% - clamp(0%, calc(var(--bar-start, 0) * 1%), 100%));
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--phase-color, var(--color-border-strong));
  color: var(--color-white);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
  text-decoration: none;
}
.gantt-chart__bar:hover { color: var(--color-white); filter: brightness(0.95); }
.gantt-chart__bar--ended    { --phase-color: var(--color-phase-ended); }
.gantt-chart__bar--active   { --phase-color: var(--color-phase-active); }
/* Upcoming uses the DARKENED amber token: white on #F59E0B is ~1.9:1 and fails
   AA — the same correction already applied to `.badge--warning`. */
.gantt-chart__bar--upcoming { --phase-color: var(--color-warning-text); }
/* Too narrow to hold its label: hide the in-bar label. HPMR-678 — there is no
   'caller renders it after' any more; `.gantt-chart__row-name` already has it. */
.gantt-chart__bar--narrow { padding: 0; }
.gantt-chart__bar--narrow .gantt-chart__bar-label { display: none; }
.gantt-chart__bar-label { overflow: hidden; text-overflow: ellipsis; }
/* HPMR-678 — `.gantt-chart__row-label` (the label that used to float just past
   a too-narrow bar) is GONE, view and controller with it. It existed only
   because a narrow bar had nowhere else to put its name; `.gantt-chart__row-name`
   now shows every name unconditionally, so keeping it would print the same
   string twice on exactly the rows the frame draws once. */
/* TODAY marker — a full-height rule across the plot with a pill label on top.
   Accent (not danger) per the shipped canvas "NOW" tick (spec §3.5 / OQ-13). */
.gantt-chart__today {
  position: absolute;
  top: 0;
  bottom: 0;
  /* HPMR-678 — --today-at is still a percentage OF THE TRACK, exactly like
     --bar-start, so it is resolved against (plot width - label column) and then
     shifted right by that column. The marker stays the last child of the plot
     so it keeps painting OVER the bars; wrapping it and the grid in a shared
     inset overlay would have been tidier CSS and would have dropped it behind
     them. min/max rather than clamp() so the expression stays a plain
     number-times-length that every calc implementation folds the same way. */
  left: calc(
    var(--gantt-label-w) +
    (100% - var(--gantt-label-w)) * min(100, max(0, var(--today-at, 0))) / 100
  );
  width: 2px;
  background: var(--color-accent);
  pointer-events: none;
}
.gantt-chart__today-label {
  position: absolute;
  top: calc(-1 * var(--space-5));
  left: 50%;
  transform: translateX(-50%);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-accent);
  color: var(--color-white);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  white-space: nowrap;
}
.gantt-chart__empty {
  padding: var(--space-8);
  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
/* HPMR-678 — the squeeze. A fixed 160px column next to a chart has an obvious
   failure mode: on a narrow viewport the column keeps its width and the TRACK
   collapses, so every bar lands in a few pixels and the chart stops meaning
   anything. The frame is a 1920px desktop screen and 160px is what it specifies
   there, so the full width holds at >=768px; below that the column steps down
   and — because all four consumers read --gantt-label-w — the grid, the TODAY
   marker and the month axis follow it in the same declaration. The name still
   renders, now ellipsised sooner. */
@media (max-width: 767.98px) {
  .gantt-chart { --gantt-label-w: 96px; }
}

/* Three-item status legend beside the "Scheduled Phases (N)" heading. */
.phase-legend {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.phase-legend__item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.phase-legend__dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--phase-color, var(--color-border-strong));
}
.phase-legend__item--ended    { --phase-color: var(--color-phase-ended); }
.phase-legend__item--active   { --phase-color: var(--color-phase-active); }
/* HPMR-579 — the legend dot follows the BAR, not the raw token. Ended and
   Active resolve to the same colour in all three places (legend dot, Gantt bar,
   row chip dot) because their tokens need no AA correction. Upcoming does: the
   bar and the chip both render the darkened amber --color-warning-text, and
   this dot alone still pointed at --color-phase-upcoming (raw #F59E0B), so the
   one three-state scale in the UI disagreed with itself on screen. Corrected
   toward the bar rather than the reverse — the bar's darkening is load-bearing
   (white on raw amber is ~1.9:1 and fails AA), so it is the value that cannot
   move. --color-phase-upcoming stays defined: it is still the honest name for
   the phase's brand colour, and fills tinted with it pass AA fine. */
.phase-legend__item--upcoming { --phase-color: var(--color-warning-text); }

/* Dot-chip form of the same scale, for the per-phase row status. Reuses the
   shipped `.chip` pill geometry. */
.chip--phase-ended    { background: var(--color-info-fill);    color: var(--color-info); }
.chip--phase-active   { background: var(--color-success-fill); color: var(--color-success-text); }
.chip--phase-upcoming { background: var(--color-warning-fill); color: var(--color-warning-text); }
.chip__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

/* ============================================================================
   END — ADMIN RESKIN PHASE 2 SHARED FOUNDATION (HPMR-536 / S1)
   ============================================================================ */

/* ============================================================================
   ADMIN RESKIN PHASE 2 — WAVE 2 INTEGRATION SEAMS
   ----------------------------------------------------------------------------
   Applied by the Wave-2 integration pass from the four area agents' declared
   seams. Everything here is GENERIC (no seo-* / cms-* one-offs) and token-based;
   later waves should read this block as the house reference alongside the
   Wave-1 foundation above.
   ============================================================================ */

/* ----------------------------------------------------------------------------
   (2-1) `.text-meta` — the small muted caption, named once
   ----------------------------------------------------------------------------
   Three areas independently reached for "font-size-sm / --color-text-muted"
   and borrowed `.section-heading__meta` out of its parent's context (S3's
   split-card sub-labels, S6's page-header freshness signal, the audit-trigger
   stale message). Declared once here and `.section-heading__meta` is folded
   into the same rule so the two can never drift. */
/* HPMR-610 — this is a BARE utility with 36 call sites, and they split: some
   are inside a white `.section-card` / `.list-table` row (SEO Analytics Clarity
   tiles, the transportation table cells, the shuttle form hints), and some are
   laid straight onto the panel (`seo/analytics|search/index` "Date range:",
   `seo/audits/timeline` result count, `admin/reports/show` row count). A class
   whose parent is not fixed cannot be tuned per call site without the next
   author re-breaking it, so it is measured against the worst surface the admin
   paints — #EEEFEF — exactly as HPMR-605 reasoned for the bare `.text-*`
   utilities. 4.06:1 -> 5.87:1 on the panel; 4.67:1 -> 6.76:1 in a card. */
.text-meta,
.section-heading__meta {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
/* Extra-small variant for inline status captions that sit beside a control
   (e.g. the audit trigger's stale / give-up message). */
.text-meta--xs { font-size: var(--font-size-xs); }
/* Keeps a long inline note from stretching across the full row. */
.text-meta--narrow { display: inline-block; max-width: 22rem; }

/* Caption directly under a `.split-card__title` ("By total session count",
   "Good: 2.5s or less"). Pulls up against the title's bottom margin so the
   pair reads as one heading block. */
.split-card__subtitle {
  margin: calc(-1 * var(--space-3)) 0 var(--space-4);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

/* ----------------------------------------------------------------------------
   (2-2) `.modal__footer--inset` — footer divider that bleeds to the card edge
   ----------------------------------------------------------------------------
   A modal footer holding the submit must live INSIDE the <form>, which lives
   inside `.modal__body` — so the base `.modal__footer` border-top is inset by
   the body's 32px padding instead of spanning the card. Opt-in modifier that
   cancels the body padding. Deliberately NOT a descendant selector: it must not
   retro-fit every existing modal form. */
.modal__footer--inset {
  margin: var(--space-6) calc(-1 * var(--modal-body-padding)) calc(-1 * var(--modal-body-padding));
}

/* ----------------------------------------------------------------------------
   (2-3) `.serp-preview` — Google search-result mock
   ----------------------------------------------------------------------------
   !! DELIBERATELY NOT TOKENISED !!  The five literals below reproduce Google's
   own result styling (link blue #1a0dab, URL green #006621, snippet grey
   #545454, Arial). Replacing them with HPM brand tokens would stop the block
   being a preview of what the page actually looks like on a SERP — the whole
   point of the component. A future token audit must leave these alone. */
.serp-preview {
  max-width: 600px;
  font-family: Arial, Helvetica, sans-serif;
}
.serp-preview__title {
  font-size: 20px;
  line-height: 1.3;
  margin-bottom: 3px;
  color: #1a0dab;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.serp-preview__url {
  font-size: 14px;
  line-height: 1.3;
  margin-bottom: 4px;
  color: #006621;
}
.serp-preview__snippet {
  font-size: 14px;
  line-height: 1.4;
  color: #545454;
}

/* ============================================================================
   END — WAVE 2 INTEGRATION SEAMS
   ============================================================================ */

/* ============================================================================
   HPMR-554 — PER-PAGE SELECTOR + SORTABLE-HEADER HOVER
   ----------------------------------------------------------------------------
   Two additions, both append-only; nothing above this banner is restyled.

   (1) `.pagination-bar__per-page` — the third section of the design system's
       Pagination spec ("Show [N] entries", left), which HPMR-452 shipped
       without and the Phase-2 foundation deliberately left out because no
       controller honoured a per-page param yet. It now does
       (ApplicationController#safe_per_page_param), so the control is real.

   (2) The hover affordance the sortable CMS Pages headers never got. HPMR-552
       shipped `cms_pages_sort_header` but was barred from this stylesheet, so
       the caret was the only signal a header is clickable.

   Tokens only — canonical --color- and --space- names, zero hardcoded values.
   ============================================================================ */

/* ----------------------------------------------------------------------------
   (1) Per-page selector
   ----------------------------------------------------------------------------
   NOT a form (three screens render the bar inside a bulk-select form — see the
   partial's header). One flex item in the bar: a <select> of URLs driven by
   per_page_controller.js, plus a link list that only appears when scripting is
   off. The control is a plain `.form-field__select` scaled DOWN to the 32px
   pagination-cell rhythm: the base field is 43px tall at 16px text, which would
   tower over the 32px page cells and the 12px result count next to it. */
.pagination-bar__per-page {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

/* The accessible name comes from this label WRAPPING the select ("Show …
   entries"), so there is no id/for pair to collide when two bars render. */
.pagination-bar__per-page-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted-strong);          /* HPMR-610 — on the panel */
  white-space: nowrap;
}

.pagination-bar__per-page-select {
  width: auto;
  height: 32px;
  padding: 0 var(--space-8) 0 var(--space-3);
  font-size: var(--font-size-xs);
  background-position: right var(--space-3) center;
}

/* No-JS fallback. Hidden by DEFAULT — with scripting on, the dropdown is the
   whole control and these links must never paint (no flash, and nothing extra
   for a screen reader to read). The noscript <style> in the partial reverses
   both rules, so with scripting off the links are the control instead. */
.pagination-bar__per-page-links {
  display: none;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted-strong);          /* HPMR-610 — on the panel */
  white-space: nowrap;
}

/* HPMR-922 — scripting off: the links are the control. A CSS media query, NOT
   a <noscript><style> in the partial: Turbo parses turbo-frame responses
   (Events status tabs) in a scripting-disabled document, which activated the
   noscript style on every tab swap and swapped the dropdown for links. */
@media (scripting: none) {
  .pagination-bar__per-page .pagination-bar__per-page-label { display: none; }
  .pagination-bar__per-page .pagination-bar__per-page-links { display: inline-flex; }
}

.pagination-bar__per-page-link {
  color: var(--color-text);
  text-decoration: none;
  padding: 0 var(--space-1);
  border-bottom: 1px solid transparent;
}

.pagination-bar__per-page-link:hover {
  border-bottom-color: var(--color-accent);
}

.pagination-bar__per-page-link[aria-current] {
  color: var(--color-accent);
  font-weight: var(--font-weight-bold);
}

/* Figma centres the result count when the selector is present. General-sibling
   selector, so a bar WITHOUT the selector keeps the shipped left alignment
   untouched. */
.pagination-bar__per-page ~ .pagination-bar__count {
  flex: 1 1 auto;
  text-align: center;
}

/* ----------------------------------------------------------------------------
   (2) Sortable column header hover
   ----------------------------------------------------------------------------
   Scoped to `th[aria-sort]`, which is exactly what `cms_pages_sort_header`
   emits and what any future sortable header will emit — a plain <th> can never
   pick this up. The link itself carries Bootstrap `.text-reset`
   (`color: inherit !important`), so the colour change has to land on the caret;
   the underline carries the "this is a link" half. Restrained on purpose: the
   caret simply goes from 50% quiet to full accent, i.e. the same treatment the
   ACTIVE state already uses. */
th[aria-sort] > a {
  cursor: pointer;
  text-underline-offset: var(--space-1);
}

th[aria-sort] > a:hover,
th[aria-sort] > a:focus-visible {
  text-decoration: underline;
}

/* The caret is the part that CAN take colour, and it moves to the same accent
   the sorted state uses — so hovering previews what activating the header does. */
th[aria-sort] > a:hover .table__th-sort,
th[aria-sort] > a:focus-visible .table__th-sort {
  opacity: 1;
  color: var(--color-accent);
  transition: opacity 0.15s ease, color 0.15s ease;
}

/* ============================================================================
   Block-editor Wave 0 (HPMR-569) — shared content-block field chrome
   ----------------------------------------------------------------------------
   Spec: docs/superpowers/specs/2026-07-29-block-editor-spec.md §4.2.

   Six component families consumed by all 24 block forms AND the scheduled-phase
   override form. They live here ONCE because the alternative — each form wave
   inventing its own control chrome — ships four dialects of the same input.

   These rules replace the inline `style=` attributes that
   app/views/cms/shared/fields/_field_repeatable.html.erb and
   app/views/cms/content_blocks/forms/_media_field.html.erb previously carried
   (card backgrounds, 120px thumbnails, dashed borders, danger tinting), all of
   which reached for legacy tokens. Everything below is on the canonical HPMKT
   layer.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   (1) URL adornment — spec §4.3 "URL adornment", ~15 call sites
   ---------------------------------------------------------------------------
   A neutral `https://` chip fused to the left edge of a URL input: ONE border
   around the pair, and the focus ring lands on the pair rather than on the
   input alone.

   Strictly presentational. The chip is not part of the value — the submitted
   string is exactly what the editor typed, and `_field_url` never prefixes it.
   That matters because a real chunk of stored CTA/link values are site-relative
   (`/register/...`), the same finding that kept the media URL input alive
   (spec §10, "Why OQ-6 reversed").
   --------------------------------------------------------------------------- */
.form-field__input-wrapper--adorned {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  transition: border-color 0.15s, box-shadow 0.15s;
}

.form-field__input-wrapper--adorned:focus-within {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 2px rgba(241, 91, 93, 0.3);
}

.form-field__adornment {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  padding: 0 var(--space-3);
  border-right: 1px solid var(--color-border);
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  background: var(--color-bg-alt);
  /* HPMR-610 — the adornment is a #EEEFEF cap on a white input; its own text
     is on the grey, not the white. */
  color: var(--color-text-muted-strong);
  font-family: var(--font-family-mono);
  font-size: var(--font-size-sm);
  line-height: 1;
  white-space: nowrap;
  user-select: none;
}

/* The input surrenders its own frame: the wrapper owns border, radius and focus
   ring now, so leaving the input border on would double it up. */
.form-field__input-wrapper--adorned .form-field__input {
  border: 0;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: transparent;
  min-width: 0;
}

.form-field__input-wrapper--adorned .form-field__input:focus {
  outline: none;
  box-shadow: none;
}

/* ---------------------------------------------------------------------------
   (2) Media dropzone — spec §4.3 "Media picker", the dashed drop area
   ---------------------------------------------------------------------------
   Rendered as a `button type="button"`, not a div: it opens the media picker on
   click, so it must be reachable by keyboard and announced as a control.
   The `--dragover` state is applied by media_picker_controller on
   dragenter/dragover.
   --------------------------------------------------------------------------- */
.media-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  width: 100%;
  min-height: 96px;
  padding: var(--space-4);
  border: 1px dashed var(--color-passion-40);
  border-radius: var(--radius-md);
  background: var(--color-passion-20);
  color: var(--color-text);
  font-family: inherit;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background-color 0.15s;
}

.media-dropzone:hover {
  border-color: var(--color-accent);
}

.media-dropzone:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.media-dropzone--dragover {
  border-color: var(--color-accent);
  border-style: solid;
  background: var(--color-passion-40);
}

.media-dropzone__icon {
  font-size: 20px;
  color: var(--color-accent);
}

.media-dropzone__title {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* HPMR-610 — the upgrade off Cool Grey was right and still short: this hint
   never sits on white OR on the panel. It sits on the dropzone's own
   `--color-passion-20` #FCDEDF fill (#747474 = 3.71:1), and `--dragover`
   swaps that for `--color-passion-40` #F9BDBE, the DARKEST surface any text in
   this admin lands on. Even --color-text-muted-strong is 4.20:1 there, so this
   is the one muted call site that has to go all the way to
   --color-text-secondary #454545: 7.60:1 resting, 5.96:1 on dragover. Still
   plainly secondary next to the #171717 title beside it. */
.media-dropzone__hint {
  font-size: var(--font-size-xs);
  color: var(--color-text-secondary);
}

/* ---------------------------------------------------------------------------
   (3) Media picker layout — spec §4.3, used 20 times across 14 block types
   ---------------------------------------------------------------------------
   Two columns: a fixed 120px thumbnail (preview OR placeholder, never both) and
   a flexible body holding the dropzone, the demoted URL/path disclosure and the
   full-width Alt Text row.

   `--empty` is the no-value state. It is set server-side from the stored value
   so there is no flash of both thumbnails before Stimulus connects, and
   media_picker_controller keeps it in sync at runtime.
   --------------------------------------------------------------------------- */
.media-picker {
  display: flex;
  gap: var(--space-4);
  align-items: flex-start;
}

/* HPMR-579 — now exactly the preview/placeholder square. It was 140px purely to
   give the Browse Media / Clear row room to sit beneath it, and that row has
   moved out of this column into its own row (frame 500:52726), so the extra
   20px was dead width taken from the dropzone. */
.media-picker__thumb {
  flex: 0 0 120px;
  width: 120px;
}

.media-picker__preview {
  display: block;
  width: 120px;
  height: 80px;
  object-fit: cover;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
}

.media-picker__placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 120px;
  height: 80px;
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
  color: var(--color-text-muted);
  font-size: 20px;
}

.media-picker--empty .media-picker__preview { display: none; }
.media-picker:not(.media-picker--empty) .media-picker__placeholder { display: none; }

/* HPMR-676 — a stored path whose image genuinely 404s degrades to the empty
   state instead of a broken-image box. Set by media_picker_controller on the
   img's `error`. The doubled class is load-bearing: the `:not()` rule above
   scores three classes, so the single-class form would lose and the placeholder
   would stay hidden — the exact reason a dead path had nothing to fall back to.
   Later source order settles the tie. */
.media-picker.media-picker--preview-error .media-picker__preview { display: none; }
.media-picker.media-picker--preview-error .media-picker__placeholder { display: flex; }

.media-picker__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* HPMR-579 — its own full-width row BELOW the two-column picker, per the frame,
   which is why the markup moved it out of .media-picker__thumb. The two buttons
   total ~190px against a 120px thumbnail, so as long as this row was confined to
   that column no width setting could keep them on one line; out here there is no
   constraint to fight and nowrap simply holds. */
.media-picker__actions {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.media-picker__actions .button {
  white-space: nowrap;
}

/* The URL/path escape hatch (OQ-6). A native `details` element, so it is
   keyboard accessible and needs no Stimulus controller — and, critically, a
   collapsed `details` still submits the inputs inside it. */
.media-picker__disclosure {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-2);
}

.media-picker__disclosure-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-muted);
  list-style: none;
}

.media-picker__disclosure-toggle::-webkit-details-marker { display: none; }
.media-picker__disclosure-toggle:hover { color: var(--color-text); }

.media-picker__disclosure-toggle:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* Font Awesome Pro swaps the `i` element for an `svg`, so a `.foo i` selector
   never matches — the caret is targeted by its own class instead. */
.media-picker__disclosure-caret {
  font-size: 10px;
  transition: transform 0.15s;
}

.media-picker__disclosure[open] .media-picker__disclosure-caret {
  transform: rotate(90deg);
}

.media-picker__disclosure-panel {
  margin-top: var(--space-2);
}

/* ---------------------------------------------------------------------------
   (4) Repeatable group chrome — spec §3.1, 17 groups across 14 block types
   ---------------------------------------------------------------------------
   Per-item card with an `#N` badge, chevron reorder buttons in bordered squares
   and a labelled danger Delete. The nested variant (sponsors tier then
   sponsors, the only depth-1 group in the system) is a dashed card on white so
   the nesting is legible without a second background tint.
   --------------------------------------------------------------------------- */

/* align-items: flex-start is load-bearing — it stops the "Add X" button
   stretching to the full width of the group. The list opts back out. */
.repeatable {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.repeatable__label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

.repeatable__list { align-self: stretch; }

.repeatable__item {
  padding: var(--space-3);
  margin-bottom: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
}

.repeatable__item--nested {
  border-style: dashed;
  background: var(--color-bg);
}

.repeatable__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.repeatable__badge {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text-muted);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  font-variant-numeric: tabular-nums;
}

.repeatable__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Bordered square chevron buttons — the design replaced the shipped
   fa-arrow-up / fa-arrow-down ghost buttons with chevrons in squares. */
.repeatable__move {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  color: var(--color-text-secondary);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s;
}

.repeatable__move:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.repeatable__move:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.repeatable__delete {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 28px;
  padding: 0 var(--space-3);
  border: 1px solid var(--color-danger-fill);
  border-radius: var(--radius-sm);
  background: var(--color-danger-fill);
  color: var(--color-danger);
  font-family: inherit;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  cursor: pointer;
  transition: background-color 0.15s;
}

.repeatable__delete:hover {
  background: var(--color-danger-fill-hover);
  border-color: var(--color-danger-fill-hover);
}

.repeatable__delete:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* OQ-5 — the add button DISABLES at max rather than disappearing; a control
   that vanishes reads as a bug. The hint below it says why it is dead. */
.repeatable__add:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.repeatable__max-hint {
  margin-block: var(--space-2) 0;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* Monospace textarea — the JSON escape-hatch fields (cards[].tags,
   members[].social_links, tours_schedule.tabs). Replaces an inline
   `style="font-family: monospace"` attribute with the tokenised stack. */
.form-field__textarea--mono {
  font-family: var(--font-family-mono);
  font-size: var(--font-size-sm);
}

/* ---------------------------------------------------------------------------
   (5) Rich-text editor shell — spec §4.3, used 18 times
   ---------------------------------------------------------------------------
   TinyMCE 7 (CDN) owns the toolbar; this is the bordered shell around it plus
   the styling applied to TinyMCE's own nodes. rich_text_controller stamps
   `rich-text-editor__toolbar` onto the editor header and
   `rich-text-editor__group` onto each toolbar group on init, so the design's
   grouped-and-separated toolbar is OUR CSS on TinyMCE's real, wired controls
   rather than decorative markup we render ourselves.

   If TinyMCE's DOM shape ever changes the classes simply do not land and the
   toolbar falls back to the vendor skin — functional, unstyled. It never
   becomes a row of dead buttons.
   --------------------------------------------------------------------------- */
.rich-text-editor {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  overflow: hidden;
}

.rich-text-editor:focus-within {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 2px rgba(241, 91, 93, 0.3);
}

/* Pre-init / no-JS state: the raw textarea IS the editor. Its own border is
   dropped because the shell already draws one. */
.rich-text-editor__body {
  border: 0;
  border-radius: 0;
}

.rich-text-editor__body:focus {
  outline: none;
  box-shadow: none;
}

.rich-text-editor__toolbar {
  border-bottom: 1px solid var(--color-border);
  background: var(--color-bg);
}

/* Vertical rule between tool groups — the design separates 11 controls into 5
   groups. The first group carries no rule so the row does not open with one. */
.rich-text-editor__group + .rich-text-editor__group {
  border-left: 1px solid var(--color-border);
}

/* TinyMCE draws its own frame and its own group separators; the shell already
   supplies both, so the vendor copies are cancelled. Vendor selectors — the
   only two in this block — because they are the elements TinyMCE styles
   directly, not nodes we can class. */
.rich-text-editor .tox-tinymce { border: 0; }
.rich-text-editor .tox-toolbar__group { border-right: 0; }

/* HPMR-992 — the editor can be made TALLER by dragging its bottom-right
   corner (the browser's own resize grip), in Chrome, Edge and Firefox.
   CSS only, on the one shared shell, so every rich-text field behaves the same.

   * The element that resizes is TinyMCE's own frame (.tox-tinymce). It is a
     flex column whose edit area (the iframe) fills it, so the writing area
     follows the new height. The vendor already sets `overflow: hidden`, which
     `resize` requires; it is restated here so the grip can't silently vanish.
   * Vertical only: the width stays the column's.
   * min-height is the configured `height: 200` in rich_text_controller.js
     (about four lines under the toolbar), so it can never shrink below what it
     opens at. max-height keeps it from outgrowing the screen and covering the
     form's Save controls; past it, the editor scrolls internally as before.
   * The height is a VIEW setting only. The browser writes it as an inline
     style on the frame, outside the content, so the saved HTML is unchanged,
     and it resets on the next load.
   The grip is a pointer affordance. Keyboard users reach and edit the field
   exactly as before, and nothing here takes focus or adds a handler. */
.rich-text-editor .tox-tinymce {
  resize: vertical;
  overflow: hidden;
  min-height: 200px;
  max-height: 70vh;
}

/* Pre-init / no-JS: the raw textarea is the editor, so it gets the same grip
   and the same bounds. */
.rich-text-editor__body {
  resize: vertical;
  min-height: 200px;
  max-height: 70vh;
}

/* ---------------------------------------------------------------------------
   (6) Scheduled-phase row chrome — spec §4.2 / §7 (consumed by Wave 6)
   ---------------------------------------------------------------------------
   Wave 0 owns application.css for the whole epic, so the classes Wave 6 needs
   for D-1 (collapse), D-2 (override chips), D-3 (field-override summary),
   D-4 (per-phase preview) and D-5 (empty state) must exist now. They are
   defined here and referenced there; nothing else in this file uses them.

   The three-state phase colour scale they sit beside is already tokenised as
   --color-phase-ended, -active and -upcoming (see :root) and drives the Gantt
   bars, legend dots and row chips from one place.
   --------------------------------------------------------------------------- */
.phase-row {
  margin-bottom: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
}

/* D-1 — the whole summary line is the disclosure control. */
.phase-row__summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  font-family: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text);
}

.phase-row__summary:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: -2px;
}

.phase-row__chevron {
  flex: 0 0 auto;
  color: var(--color-text-muted);
  font-size: 12px;
  transition: transform 0.15s;
}

.phase-row--expanded .phase-row__chevron { transform: rotate(90deg); }

/* D-2 — override keys as chips on the collapsed row, replacing the shipped
   "Overrides: heading, background_image" plain-text line. */
.phase-row__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-left: auto;
}

.phase-row__body {
  padding: 0 var(--space-4) var(--space-4);
  border-top: 1px solid var(--color-border);
}

/* D-3 — read-only key/value summary of what the phase changes. */
.phase-row__overrides {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.phase-row__override {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
}

.phase-row__value-from {
  color: var(--color-text-muted);
  text-decoration: line-through;
}

.phase-row__value-to {
  color: var(--color-text);
  font-weight: var(--font-weight-semibold);
}

/* D-4 — the merged-content preview pane. Rendered inline server-side on
   expand: never broadcast into the node hosting the phase-form controller,
   because Turbo removing that node fires disconnect() and the controller is
   gone (spec §7 D-4 caveat). */
.phase-row__preview {
  margin-top: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-alt);
}

/* D-5 — calendar icon + bold title + explanatory line. */
.phase-row__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-8) var(--space-4);
  color: var(--color-text-muted);
  text-align: center;
}

.phase-row__empty-title {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}

/* ==========================================================================
   HPMR-73 / HPMR-123 — MULTI-PROFILE SELECTOR + SIDEBAR SWITCHER

   Two surfaces, one shared identity block (`shared/_exhibitor_profile_identity`):
     .xprofile-*        the sign-in selector at /exhibitor/choose_profile
                        (exhibitor_auth chrome, background --color-bg)
     .portal-switcher-* the sidebar control in the portal chrome
                        (also --color-bg)

   CONTRAST NOTE (design.md, the two-surface rule). `.xprofile__meta` and
   `.xprofile__monogram` are rendered by BOTH surfaces AND survive a hover that
   swaps the fill to --color-bg-alt, so they are measured against the WORST
   background they reach, not the flattering one: --color-text-muted-strong
   (#585C5E — 6.76:1 on white, 5.87:1 on #EEEFEF) rather than
   --color-text-muted, which passes only on white.

   WHY IT SITS HERE AND NOT AT THE END OF THE FILE — the same reason the AUTH
   SHELL block below gives, learned the same way. It was appended at EOF first,
   which turned spec/views/shared/pagination_spec.rb red: that spec slices its
   subject as "from .pagination-bar__page--prev to the end of the file" and then
   requires every colour/spacing declaration in the slice to be a var(). A list
   reset's `margin: 0` is not a spacing value with a token, and there is no
   --space-0, so honest CSS cannot satisfy a guard that was never aimed at it.
   Nothing here depends on source order: every selector is a class and none of
   them collides with another rule in this file.
   ========================================================================== */

/* --- shared identity block ------------------------------------------------ */
.xprofile__mark {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
}
.xprofile__logo {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
.xprofile__monogram {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.04em;
  color: var(--color-text-secondary);
}
.xprofile__text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  text-align: left;
}
.xprofile__name {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Building placement (HPMR-123). Muted-strong: see the contrast note above. */
.xprofile__meta {
  font-size: var(--font-size-xs);
  color: var(--color-text-muted-strong);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The active profile is marked by an icon AND the word "Current" — never by
   colour alone (1.4.1). */
.xprofile__current {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-bold);
  color: var(--color-success-text);
}
.xprofile__go {
  flex: 0 0 auto;
  color: var(--color-text-muted-strong);
}

/* --- the selector screen -------------------------------------------------- */
.xprofile-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.xprofile-list__item { margin: 0; }
.xprofile-list__form { margin: 0; }
.xprofile-card {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  cursor: pointer;
  text-align: left;
}
.xprofile-card:hover {
  background: var(--color-bg-alt);
  border-color: var(--color-accent);
}
.xprofile-card:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

/* --- the sidebar switcher ------------------------------------------------- */
.portal-switcher {
  position: relative;
  margin-top: var(--space-4);
}
.portal-switcher__toggle {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
}
.portal-switcher__toggle:hover { border-color: var(--color-accent); }
.portal-switcher__toggle:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}
.portal-switcher__current {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  text-align: left;
}
.portal-switcher__label {
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted-strong);
}
.portal-switcher__name {
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.portal-switcher__chevron {
  flex: 0 0 auto;
  color: var(--color-text-muted-strong);
}
/* Toggled by the `hidden` ATTRIBUTE, not a `d-none` class: Bootstrap ships
   d-none with !important, so a class-based reveal cannot be undone here. */
.portal-switcher__panel {
  position: absolute;
  z-index: 200;
  top: calc(100% + var(--space-2));
  left: 0;
  right: 0;
  max-height: 60vh;
  overflow-y: auto;
  padding: var(--space-2);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}
.portal-switcher__panel[hidden] { display: none; }
.portal-switcher__panel-title {
  margin: 0 0 var(--space-2);
  padding: 0 var(--space-2);
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted-strong);
}
.portal-switcher__option-form { margin: 0; }
.portal-switcher__option {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: left;
}
.portal-switcher__option:hover {
  background: var(--color-bg-alt);
  border-color: var(--color-accent);
}
.portal-switcher__option:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}
.portal-switcher__option--current {
  background: var(--color-bg-alt);
  cursor: default;
}
.portal-switcher__option[disabled] { cursor: default; }

/* ==========================================================================
   AUTH SHELL — the unauthenticated sign-in / forgot / reset / invite screens.
   Consumed by layouts/admin_auth.html.erb + layouts/exhibitor_auth.html.erb
   and by app/views/{admin_users,exhibitor_users}/**.

   Figma "Components Library - Portals" (cyollcfjPhkNPjUWgqqhfI):
     exhibitor  Sign in 30:1269 - Forgot 30:1362 - Reset sent 30:2212 -
                Set password 271:31794 - Sign-in error 275:34234 -
                Account Locked 275:34182 - Account Deactivated 275:34323 -
                Invite expired 273:32018
     admin (canvas 2:2)  Sign in 97:9310 - Forgot 97:9352 - Reset link sent 97:9392
   Canonical color / space / radius tokens (brand hex wins over Figma raw:
   Passion #F15B5D, muted grey #747474).

   HPMR-632 — WHY THIS LIVES HERE AND NOT IN THE LAYOUT ANY MORE.

   It shipped as an inline <style> block in layouts/exhibitor_auth.html.erb
   (2026-07-02) and was then CLONED VERBATIM into layouts/admin_auth.html.erb
   (2026-07-03, HPMR-503/S1) — 330 lines identical apart from the comment
   header. Both copies carried

       .xauth-input::placeholder { color: var(--color-text); opacity: 0.4; }

   which is the HPMR-627 defect exactly: an AA-safe token diluted by a separate
   `opacity`, so the source reads compliant, getComputedStyle().color still
   reports #171717, and only the composited pixel is wrong. MEASURED IN
   CHROMIUM (hpmr632_auth_surface.js) on all six reachable unauthenticated
   screens: #171717 @ 0.4 composites to #A2A2A2 on #FFFFFF = 2.55:1, 10 of 10
   placeholders failing — the worst-measuring text in the product, on the first
   screen every HPM staff user and every exhibitor sees.

   HPMR-627 had already fixed the identical bug in .form-field__input::placeholder
   forty lines up in THIS file, and spec/views/shared/form_field_contrast_guard_spec.rb
   already asserts that NO ::placeholder rule anywhere in this file dilutes with
   opacity. That guard reads application.css. It cannot read an ERB layout, so
   the duplicate sat outside its scope for five weeks and the fix had to be
   discovered twice.

   Hoisting was safe to do rather than merely widening the guard, and the check
   was made rather than assumed: every selector below is `.xauth-`-prefixed, and
   `grep -rl xauth app/` returns only the two auth layouts + the
   admin_users/exhibitor_users views they render. No authenticated markup carries
   an .xauth-* class, so nothing on the signed-in side can match these rules —
   confirmed by a full computed-style diff of every element on all seven
   unauthenticated screens plus the dashboard before/after the move.

   Keep it here. A second copy is what caused this.

   WHY IT SITS HERE AND NOT AT THE END OF THE FILE. It was appended at EOF
   first, which turned spec/views/shared/pagination_spec.rb red: that spec
   slices its subject as `css[/^\.pagination-bar__page--prev,.*\z/m]` — "from my
   selector to the end of the file" — so anything appended after it is read as
   part of the pagination block and fails its no-hardcoded-hex assertion. The
   pagination block itself has to stay last (its `width: auto` only beats the
   later `.pagination-bar__page { width: 32px }` by source order — see its own
   comment), so this block goes immediately BEFORE it instead. Nothing here
   depends on source order: every selector below is a class, and none of them
   collides with another rule in this file.
   ========================================================================== */
.xauth-body {
  --xauth-bg: #FEF2F2;               /* Figma primary-main/100 (passion tint) */
  --xauth-card-radius: 24px;         /* Figma card rounded-[24px] */
  margin: 0;
  min-height: 100vh;
  background: var(--xauth-bg);
  color: var(--color-text);
  font-family: var(--font-family-base);
  -webkit-font-smoothing: antialiased;
}

.xauth-split {
  display: grid;
  grid-template-columns: minmax(420px, 780px) 1fr;
  min-height: 100vh;
}

/* ---- Left: white form panel ---------------------------------------- */
.xauth-panel {
  margin: 20px;
  background: var(--color-bg);
  border-radius: var(--xauth-card-radius);
  box-shadow: 2px 2px 30px rgba(0, 0, 0, 0.05);
  overflow: hidden;
}

.xauth-panel__inner {
  display: flex;
  flex-direction: column;
  min-height: calc(100vh - 40px);
  padding: 48px 40px 40px;
}

.xauth-logo {
  display: flex;
  justify-content: center;
  margin-bottom: 24px;
}

.xauth-logo__img {
  height: 56px;
  width: auto;
}

.xauth-content {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  max-width: 480px;
  margin: 0 auto;
  padding: 24px 0;
}

.xauth-footer {
  margin: 24px 0 0;
  text-align: center;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

/* ---- Header (title + subtitle) ------------------------------------- */
.xauth-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  text-align: center;
  margin-bottom: 28px;
}

.xauth-title {
  margin: 0;
  font-size: clamp(1.6rem, 1.35rem + 1vw, 2rem);   /* ~25.6 → 32px */
  font-weight: var(--font-weight-semibold);
  line-height: 1.15;
  color: var(--color-text);
}

.xauth-subtitle {
  margin: 0;
  font-size: 1rem;
  line-height: 1.4;
  color: var(--color-text-muted);
}

/* ---- Form fields --------------------------------------------------- */
.xauth-form { display: flex; flex-direction: column; }

.xauth-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }

.xauth-field__label {
  font-size: 1rem;
  font-weight: var(--font-weight-regular);
  color: var(--color-text);
}

.xauth-field__control { position: relative; display: block; }

.xauth-input {
  width: 100%;
  height: 44px;
  padding: 10px 14px;
  font-family: var(--font-family-base);
  font-size: 1rem;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

/* HPMR-632 — was `color: var(--color-text); opacity: 0.4` = #A2A2A2 on
   #FFFFFF, 2.55:1. Repainted to the full-opacity --color-text-muted that
   HPMR-627 landed on for .form-field__input::placeholder: #747474 on #FFFFFF
   = 4.67:1.

   ONE SURFACE, AND PROVEN. Placeholder text paints on the input's own
   background; .xauth-input sets `background: var(--color-bg)` (#FFFFFF) itself
   nine lines up, and no rule in this file repaints it. The Chromium sweep
   resolved 10 of 10 real placeholders to #FFFFFF.

   `opacity: 1` IS LOAD-BEARING — do not delete it as redundant. Firefox's UA
   stylesheet applies `opacity: 0.54` to ::placeholder, so merely dropping the
   0.4 would hand Firefox ~2.6:1 and reintroduce the bug on one engine only. */
.xauth-input::placeholder { color: var(--color-text-muted); opacity: 1; }

.xauth-input:focus {
  border-color: var(--color-passion);
  box-shadow: 0 0 0 3px rgba(241, 91, 93, 0.25);
}

.xauth-input--password { padding-right: 44px; }

.xauth-input--error {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.22);
}

/* HPMR-632 — was --color-text-muted #747474, which is 4.06:1 on the
   --color-bg-alt #EEEFEF this same rule fills with. Repainted to
   --color-text-muted-strong #585C5E, 5.87:1.

   FOUND BY THE HOIST, NOT BY THE TICKET. spec/views/shared/muted_token_surface_guard_spec.rb
   fails exactly this pairing — a rule that fills --color-bg-alt and paints its
   own text --color-text-muted — and it went red the moment this block moved out
   of the ERB layout and into a file that guard reads. It is the second defect
   the move surfaced, and it is the argument for having made the move.

   AND THE DISABLED EXEMPTION DOES NOT COVER IT. WCAG 1.4.3 exempts "inactive
   user interface components", which is why the .pagination-bar--disabled rule
   lower down keeps its dimming. But this selector list is `:disabled` AND
   `[readonly]`, and a readonly input is NOT inactive — it still takes focus,
   its value is still selectable and copyable, and that value is content. A
   shared selector is bound by its strictest member, so it gets the AA colour. */
.xauth-input:disabled,
.xauth-input[readonly] {
  background: var(--color-bg-alt);
  color: var(--color-text-muted-strong);
  cursor: not-allowed;
}

.xauth-field__hint { font-size: 0.8125rem; color: var(--color-text-muted); }

.xauth-field__error {
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--color-danger);
}

/* ---- Password reveal (eye) ---------------------------------------- */
.xauth-reveal {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.xauth-reveal:hover { color: var(--color-text); }
.xauth-reveal .fa-eye-slash { display: none; }
.xauth-reveal.is-revealed .fa-eye { display: none; }
.xauth-reveal.is-revealed .fa-eye-slash { display: inline-block; }

/* ---- Remember-me / forgot row ------------------------------------- */
.xauth-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 2px 0 22px;
}

.xauth-check { display: inline-flex; align-items: center; gap: 8px; }
.xauth-check input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--color-passion);
  cursor: pointer;
}
.xauth-check label { font-size: 1rem; color: var(--color-text); cursor: pointer; }

/* ---- Buttons ------------------------------------------------------- */
.xauth-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 44px;
  padding: 10px 16px;
  font-family: var(--font-family-base);
  font-size: 1rem;
  font-weight: var(--font-weight-semibold);
  line-height: 1;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  text-decoration: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}
.xauth-btn--primary,
.xauth-btn--primary:link,
.xauth-btn--primary:visited,
.xauth-btn--primary:hover,
.xauth-btn--primary:active,
.xauth-btn--primary:focus {
  color: #fff;                       /* beats application.css a:hover (0,0,1,1) */
}
.xauth-btn--primary {
  background: var(--color-passion);
  border-color: var(--color-passion);
}
.xauth-btn--primary:hover { background: var(--color-passion-80); border-color: var(--color-passion-80); }
/* Quiet secondary action — the sign-out escape hatch on the HPMR-122 forced
   password reset screen. --color-text-secondary (#454545) is 5.96:1 on white,
   so it clears AA as text; the passion --primary above stays the single CTA. */
.xauth-btn--ghost {
  background: transparent;
  border-color: var(--color-border);
  color: var(--color-text-secondary);
}
.xauth-btn--ghost:hover { background: var(--color-bg-alt); border-color: var(--color-text-secondary); }
.xauth-btn--primary:active { background: #e04a4c; border-color: #e04a4c; }
.xauth-btn i, .xauth-btn svg { font-size: 0.875rem; }

/* ---- Links --------------------------------------------------------- */
.xauth-link {
  color: var(--color-passion);
  font-weight: var(--font-weight-semibold);
  text-decoration: none;
  transition: color 0.15s ease;
}
.xauth-link:hover { color: var(--color-passion-80); text-decoration: underline; }

.xauth-form-footer { margin-top: 20px; text-align: center; }

/* ---- Inline alert (recoverable errors / notices) ------------------ */
.xauth-alert {
  margin-bottom: 20px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  border-left: 3px solid;
  font-size: 0.9375rem;
  line-height: 1.4;
}
.xauth-alert--danger  { background: #FEF2F2; border-color: var(--color-danger);  color: #991B1B; }
.xauth-alert--info    { background: var(--color-passion-20); border-color: var(--color-passion); color: var(--color-text); }
.xauth-alert--success { background: #DCFCE7; border-color: var(--color-success); color: #166534; }
.xauth-alert ul { margin: 6px 0 0; padding-left: 18px; }

/* ---- Message-state card (reset-sent / locked / deactivated / expired) */
.xauth-message {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 14px;
}
.xauth-message__icon {
  display: inline-flex;
  font-size: 24px;
  line-height: 1;
  margin-bottom: 2px;
}
.xauth-icon--passion { color: var(--color-passion); }
.xauth-message__title {
  margin: 0;
  font-size: clamp(1.6rem, 1.35rem + 1vw, 2rem);
  font-weight: var(--font-weight-semibold);
  line-height: 1.15;
  color: var(--color-text);
}
.xauth-message__body {
  margin: 0;
  max-width: 420px;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--color-text-muted);
}
.xauth-message__actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  width: 100%;
  margin-top: 10px;
}

/* ---- Right: photo collage ----------------------------------------- */
.xauth-collage {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 44px 44px 44px 24px;
  overflow: hidden;
}
.xauth-collage__grid {
  position: relative;
  width: 100%;
  max-width: 900px;
  aspect-ratio: 972 / 952;
}
.xauth-collage__img {
  position: absolute;
  object-fit: cover;
  border-radius: 6px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}

/* ---- Focus visibility (a11y) -------------------------------------- */
.xauth-body :focus-visible {
  outline: 2px solid var(--color-passion);
  outline-offset: 2px;
}

/* ---- Responsive ---------------------------------------------------- */
@media (max-width: 991.98px) {
  .xauth-split { grid-template-columns: 1fr; }
  .xauth-collage { display: none; }
  .xauth-panel { margin: 0; border-radius: 0; box-shadow: none; }
  .xauth-panel__inner { min-height: 100vh; padding: 40px 24px; }
}

@media (prefers-reduced-motion: reduce) {
  .xauth-input, .xauth-btn, .xauth-link, .xauth-reveal { transition: none; }
}
    

/* ============================================================================
   HPMR-588 (Phase-3 Wave 3, AL-8) — LABELLED PREV / NEXT PAGINATION CONTROLS
   ----------------------------------------------------------------------------
   Figma cyollcfjPhkNPjUWgqqhfI, Pagination master 47:1234, reached from the
   Audit Log bar 529:63340 and the My Activity bar 488:59199 (both draw it
   identically). Its first and last cells are not icon buttons: they are
   `flex gap-8 h-38 px-12` links holding a Font Awesome 7 Pro Light DOUBLE
   chevron plus a Roobert Regular label — `chevrons-left` + "Previous", and
   "Next" + `chevrons-right`. The shipped bar rendered a bare SINGLE chevron
   with only an aria-label, on every admin list; this is the app-wide fix, and
   app/views/shared/_pagination.html.erb is its single call site.

   WHY THIS SITS AT THE BOTTOM OF THE FILE rather than beside the other
   .pagination-bar rules: `.pagination-bar__page` is declared TWICE — once in
   the HPMR-452 block (`min-width: 32px`) and again in the later "Pagination"
   block (`width: 32px`). The later declaration wins, so a labelled cell would
   be clamped to 32px and its text clipped by the nav's `overflow: hidden`.
   Appending here is the only placement that beats it at equal specificity
   WITHOUT editing either shipped rule — which is the point: the numeric cells,
   the gap cell, the active cell and the bar's geometry are byte-for-byte
   unchanged on all 31 list screens that render this component.

   MODIFIERS ON AN EXISTING COMPONENT, not a new family: --prev / --next attach
   to `.pagination-bar__page`, and only the first and last cells ever carry
   them.

   Tokens only. The frame's grey label colour resolves to --color-text-muted
   (brand guide wins over Figma on colour); gap-8 -> --space-2 and px-12 ->
   --space-3 are exact. The frame's 16px type is deliberately NOT taken — it
   would tower over the 14px numeric cells immediately beside it. That is the
   same scaling decision, for the same reason, that HPMR-554 recorded for the
   per-page control higher up this file.
   ============================================================================ */
/* ============================================================================
   API-27 / kiosk settings — referenced-but-undefined CSS classes
   ----------------------------------------------------------------------------
   API-27 (admin API keys) and the kiosk settings PIN form shipped views that
   reference house BEM classes never defined in this file. The
   css_class_definition_spec guard fails on exactly these ten names. Define
   them with existing design tokens; do not park them in KNOWN_UNDEFINED.

   WHY IT SITS HERE AND NOT AT THE END OF THE FILE — same lesson as the
   HPMR-73 / AUTH SHELL blocks above. pagination_spec.rb slices
   "from .pagination-bar__page--prev to EOF" and requires every
   colour/spacing declaration in that slice to be a var(). A list reset's
   margin: 0 is honest CSS with no --space-0 token, so EOF appends turn
   that guard red. Nothing here depends on source order.
   ============================================================================ */

/* Danger alert used by kiosk_settings/edit. BEM alert--danger (not Bootstrap
   alert-danger) so the BOOTSTRAP exemption cannot cover it. */
.alert--danger {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-danger);
  border-radius: var(--radius-md);
  background: var(--color-danger-fill);
  color: var(--color-passion-text);
}

/* Exhibitor autocomplete dropdown under the API key form search input. */
.autocomplete-list {
  list-style: none;
  margin: var(--space-1) 0 0;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  max-height: 12rem;
  overflow: auto;
  background: var(--color-bg);
  position: relative;
  z-index: 2;
}
.autocomplete-list[hidden],
.autocomplete-list[style*="display: none"],
.autocomplete-list[style*="display:none"] {
  display: none;
}
.autocomplete-list li,
.autocomplete-list button,
.autocomplete-list a {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border: 0;
  background: transparent;
  color: var(--color-text);
  cursor: pointer;
  font: inherit;
}
.autocomplete-list li:hover,
.autocomplete-list button:hover,
.autocomplete-list a:hover {
  background: var(--color-bg-alt);
}

/* Revoked status chip on API key index/show. Mirrors chip--rejected / critical. */
.chip--danger {
  background: var(--wa-color-danger-fill-quiet);
  color: var(--color-passion-text);
}
.chip--danger i,
.chip--danger svg {
  color: var(--color-danger);
}

/* Definition-list rows on API key show. */
.detail-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
}
.detail-list__row {
  display: grid;
  grid-template-columns: minmax(8rem, 12rem) minmax(0, 1fr);
  gap: var(--space-3) var(--space-6);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
  align-items: start;
}
.detail-list__row:last-child {
  border-bottom: 0;
}
.detail-list__row dt {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.detail-list__row dd {
  margin: 0;
  font-size: var(--font-size-base);
  color: var(--color-text);
  word-break: break-word;
}

/* Header action cluster (Rotate / Revoke) beside detail-page titles. */
.detail-page__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
}

/* Explicit breadcrumb link/sep classes (views pass class= rather than relying
   on the bare a-descendant rule already on .detail-page__breadcrumb). */
.detail-page__breadcrumb-link {
  color: var(--color-text-muted-strong);
  text-decoration: none;
}
.detail-page__breadcrumb-link:hover {
  color: var(--color-text);
  text-decoration: underline;
}
.detail-page__breadcrumb-sep {
  color: var(--color-text-muted);
}

/* Checkbox + label row used by API key scopes / read-only. */
.form-field__checkbox-wrapper {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-size: var(--font-size-base);
  color: var(--color-text);
  margin: var(--space-1) 0;
}

/* Hint under a form field — same role as .form-field__helper-text. */
.form-field__hint {
  font-size: var(--op-font-size-small);
  color: var(--color-text-muted-strong);
  margin: 4px 0 0;
}

.pagination-bar__page--prev,
.pagination-bar__page--next {
  width: auto;                                 /* beats the 32px clamp above */
  gap: var(--space-2);                         /* frame: gap-8 */
  padding: 0 var(--space-3);                   /* frame: px-12 */
  font-weight: var(--font-weight-regular);     /* frame: Roobert Regular */
  color: var(--color-text-muted);
}

.pagination-bar__page-label {
  white-space: nowrap;
}

/* The disabled treatment is UNCHANGED. It only has to out-specify the colour
   above, which is later in the file than the shipped --disabled rule; two
   classes give it 0-2-0 so document order stops mattering. Keeping the dimming
   matters more now than it did: it is the only signal that "Previous" is dead
   on page 1, back when the cell was a bare glyph there was nothing to read
   anyway. Disabled controls are exempt from WCAG 1.4.3 contrast. */
.pagination-bar__page--prev.pagination-bar__page--disabled,
.pagination-bar__page--next.pagination-bar__page--disabled {
  color: var(--op-color-border);
}

/* Narrow viewports fall back to EXACTLY the pre-HPMR-588 rendering — glyph
   only, 32px-ish cell. Nothing is lost to assistive tech: the accessible name
   still comes from the link's own aria-label ("Previous page" / "Next page"),
   which is also why that attribute was kept when the visible word landed. */
@media (max-width: 575.98px) {
  .pagination-bar__page-label { display: none; }

  .pagination-bar__page--prev,
  .pagination-bar__page--next { padding: 0 var(--space-2); }
}

/* HPMR-886 — the navigator is no longer a white bordered strip of glued cells.
   The container takes the surrounding surface (no background / border / clip)
   and every cell — Previous, each page number, the gap, Next — stands on its
   own with a --space-2 gap between them, so Previous / Next are visibly apart
   from the numbers. Each page cell keeps its own 1px outline + radius so it
   still reads as a control; the active cell stays Passion (rule above, its
   background is untouched). Tokens only: this sits inside the slice that
   spec/views/shared/pagination_spec.rb sweeps for var()-only declarations. */
.pagination-bar__nav {
  gap: var(--space-2);
  background: transparent;
  border: 0;
  overflow: visible;
}
.pagination-bar__nav > .pagination-bar__page {
  border: 1px solid var(--op-color-border);
  border-radius: var(--radius-sm);
}
.pagination-bar__nav > .pagination-bar__page--active { border-color: var(--op-color-primary); }

/* ── HPMR-588 (Phase-3 Wave 3) ─────────────────────────────────────────────
   Two modifiers on existing components, per the house rule that new CSS is a
   modifier on a shipped family and never a new family.

   AL-2 — Figma 529:61914 row 1: the filter controls are equal-width and
   full-bleed. Bootstrap cannot express this here: `.col` is a BARE token and
   css_class_definition_spec's BOOTSTRAP allowlist only matches `col-` WITH the
   hyphen, so `class="col"` would fail the build guard; `.flex-fill` is
   `1 1 auto`, which is content-sized, not equal-width.
   AL-4 — Figma 529:61797: the stat-card icon box is NEUTRAL with a Passion
   glyph, not a Passion-tinted box. Note both halves are required: dropping the
   brand variant alone yields a grey box AND a grey glyph, which is further from
   the frame than the current pink box. */

/* ── HPMR-593 — the equal-width band was broken in two independent ways. ────
   Both are fixed here; both were invisible to every spec we own, because a
   request spec renders HTML and never resolves a cascade.

   (1) SPECIFICITY INVERSION. The shipped `min-width: 0` above was written as a
       bare `.list-toolbar__control--fill` — (0,1,0). It was competing with
       `select.list-toolbar__control { min-width: 160px }` (application.css:3577),
       which is (0,1,1) and therefore WON, at any source order. So the three
       selects were floored at 160px while the four non-select cells floored at
       0, and under `flex: 1 1 0` the text cells were the only ones that could
       give. Measured before the fix, at 1440: 141.5/141.5/160/141.5/160/160/107.5.
       Scoping the modifier under `.list-toolbar` takes it to (0,2,0) — the same
       shape `.list-toolbar--filter-band .list-toolbar__control` (:6173) already
       uses on the Reports band, which is why that band never had this bug. No
       `!important`: the descendant is a real relationship, not a lever.

   (2) CHROME ASYMMETRY. `flex-basis: 0` is resolved against `box-sizing:
       border-box` (:220), so a control's flex BASE cannot go below its own
       padding + border — 2x`--space-4` + 2x1px = 34px — while the bare
       `<span class="list-toolbar__search">` wrapper carries no chrome and bases
       at 0. Growing all seven from unequal bases yields unequal finals: the
       search cell measured EXACTLY 34px short of its siblings at every width,
       not just narrow ones (1920: six at 218, search at 184). Giving the
       wrapper the same base its siblings are floored at restores true equality
       — it is the siblings' chrome expressed in the same tokens that draw it,
       not a magic number. The inner input keeps `width: 100%`, so the 40px
       magnifier inset still lands inside the cell.

   Verified 1920/1600/1440/1366/1280: seven equal cells, no wrap, no page
   overflow-x. */
.list-toolbar .list-toolbar__control--fill { flex: 1 1 var(--list-toolbar-cell-base); min-width: 0; }  /* HPMR-974 — was `1 1 0` */
.list-toolbar .list-toolbar__search--fill {
  flex-grow: 1;
  flex-shrink: 1;
  flex-basis: var(--list-toolbar-cell-base);  /* == a select's padding + border (HPMR-974); defined on `.list-toolbar` */
  min-width: 0;
}
.list-toolbar__search--fill .list-toolbar__control { min-width: 0; width: 100%; }

.stat-card__icon--accent-glyph { background: var(--color-bg-alt); width: 58px; height: 58px; }
.stat-card__icon--accent-glyph i,
.stat-card__icon--accent-glyph svg { font-size: 24px; color: var(--color-accent); }

/* ── HPMR-589 (Phase-3 Wave 4 — Reports) ───────────────────────────────────
   Two modifiers on shipped families (`.list-table`, `.list-toolbar`) plus one
   BEM element on `.list-toolbar`. No new family, no hardcoded hex.

   ---------------------------------------------------------------------------
   RP-1 — `.list-table--fixed`: per-report column allocation.
   ---------------------------------------------------------------------------
   Albina hard-allocates every column on all five report frames, and NONE of
   them scrolls sideways (Figma `cyollcfjPhkNPjUWgqqhfI`: roster header row
   551:67027, exhibitors-by-building 551:70506, events 551:73776, food vendors
   555:63857, shuttle 555:69256 — each a 1540px row whose child widths sum to
   exactly 1540). The widths themselves live with the data, as `COLUMN_WIDTHS`
   on each `Reports::Definition` subclass, and reach the DOM as a `<colgroup>`
   of percentages.

   `table-layout: fixed` is the load-bearing half and is NOT optional. Under the
   shipped default (`auto` — a grep for `table-layout` returned zero hits before
   this rule) a declared width is only a PREFERRED width: a Menu URL cell
   carrying a 100-character unbreakable GrubHub token sets min-content wider
   than its share, min-content wins, the declaration is silently discarded and
   `.list-table`'s `overflow-x: auto` produces the sideways scroll the frames do
   not have. Fixed layout resolves widths from the first row only, so the
   colgroup is authoritative and the overflow is absorbed by wrapping instead.

   `overflow-wrap: anywhere` is the other half — it is what lets a long token
   wrap rather than overflow its (now genuinely fixed) cell, and it is exactly
   what the frames render: the shuttle frame breaks an address mid-word
   ("welchcompan|y.scl@gmail.|com") and the food-vendors frame breaks a URL
   after its query "?". Data-row heights vary 36/56/76/96/136/156/196px across
   the frames for the same reason.

   Header cells KEEP the shipped `white-space: nowrap` and add ellipsis, which
   reproduces the frames' own clipped header ("Departure Airpor" on 555:69256).

   `min-width` is the small-viewport floor. It is a READABILITY threshold, and
   deliberately NOT "the content width at some reference viewport" — the first
   cut of this rule was exactly that, and it was wrong twice over:

   (a) OFF BY THE BORDER. It read `viewport - 316 sidebar - 2 x 32 padding` and
       set 900 for a 1280 viewport. 900 is the width of `.list-table`, not of
       the `<table>` inside it: `* { box-sizing: border-box }` (line ~220) plus
       `.list-table`'s own `1px solid` frame makes the table's containing block
       900 - 2 = 898. The floor therefore sat 2px ABOVE the space available and
       every one of the five reports rendered a 2px horizontal scrollbar at the
       reference viewport — on a rule whose entire purpose was to stop the
       frames scrolling sideways. Anything derived from the padding box has to
       subtract the border.

   (b) DEGENERATE BY CONSTRUCTION. A floor set to the container's own width is
       a no-op at that width and fires 1px below it, so "the container width at
       viewport X" can never be the right kind of number for this property.

   And the derivation's premise — that content width tracks viewport downward —
   is false in this shell. The sidebar collapses to 98px at <=1024 (padding 32
   -> 24) and goes static at <768 (offset 0, padding 16), so the content box
   gets WIDER as the viewport narrows across both breakpoints:

     viewport  1920   1280   1025   1024    768    767    375
     table      1538    898    643    876    620    733    341

   No viewport-keyed number can be right on all three bands. What IS right is
   the mechanism already in use: `min-width` on a `width: 100%` table inside
   `.list-table`'s `overflow-x: auto` is keyed on the CONTAINER, so it reads
   that row directly and needs no media queries.

   720px is chosen to clear the two widths the design has to render clean —
   1280 (898) and the 1024 tablet rail (876) — while still engaging on every
   band below them. What it buys: the narrowest column any definition declares
   (food vendors' "Active", 80/1540 = 5.19%) never renders below ~37px, against
   ~18px at a 375px viewport with no floor at all. Below the floor the table
   stops shrinking and `.list-table`'s pre-existing `overflow-x: auto` takes
   over — i.e. the old behaviour, just rarer, instead of 13 columns at 30px. */
.list-table--fixed table {
  table-layout: fixed;
  min-width: 720px;
}
.list-table--fixed thead th {
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-table--fixed tbody td {
  white-space: normal;
  /* `break-word` first as the wide-support floor (it is already the idiom used
     elsewhere in this file), then `anywhere`, which additionally lets the token
     break count toward min-content — belt and braces, since a cell that cannot
     break is the whole reason this modifier exists. */
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
   RP-4 — `.detail-page__header--nowrap`: keep Back and the exports on one row.
   ---------------------------------------------------------------------------
   Adding the Back button to `.detail-page__heading` pushed the heading's
   max-content past what the header row could hold, and `.detail-page__header`
   is `flex-wrap: wrap` — so the CSV/Excel group broke onto a second line,
   left-aligned under the subtitle. On shuttle_reservations the heading measures
   1422.9px and the export group ~194px, which with the 16px gap wants 1632.9px
   out of 1540.

   Line breaking happens BEFORE flex shrinking and is decided on each item's
   hypothetical main size, so a shrinkable heading does not help: with
   `flex-basis: auto` the heading's hypothetical size IS its max-content, the
   line overflows on paper, and the toolbar wraps before any shrinking is
   considered. The heading has to stop claiming that width in the first place.

     heading   flex: 1 1 0    -> hypothetical size 0, clamped up by min-width
     toolbar   flex: 0 0 auto -> keeps its content width; never squashed into
                                 its own `.list-toolbar` wrap

   `min-width: min(320px, 100%)` is what preserves the mobile stack. It is the
   heading's hypothetical size, so the line breaks — and the toolbar drops below
   the title, as it should — exactly when 320 + 16 + 194 no longer fits, i.e.
   below a ~530px content box. The `min(…, 100%)` half matters at <=320px
   viewports, where the content box is ~288px and a bare 320px floor would push
   the header wider than the panel.

   Every desktop and tablet width clears the threshold, including the two where
   the collapsing sidebar makes the content box wider than the viewport implies
   (1024 -> 876, 767 -> 733), so all five reports hold one row regardless of
   title length; the long DESCRIPTION subtitles simply wrap inside the heading,
   which is what `.detail-page__header`'s `align-items: flex-start` already
   anticipates.

   SCOPED TO A MODIFIER on purpose. `.detail-page__header` is worn by 35 views,
   including admin/audit/index.html.erb, which is live work under another
   ticket; this wave owns the reports screen, so the fix opts in there rather
   than changing the shared class underneath everyone. If it proves right
   everywhere, promoting it is a one-line move. */
.detail-page__header--nowrap > .detail-page__heading {
  flex: 1 1 0;
  min-width: min(320px, 100%);
}
.detail-page__header--nowrap > .list-toolbar {
  flex: 0 0 auto;
}

/* ---------------------------------------------------------------------------
   RP-6 — `.list-toolbar--filter-band`: grow-to-fill, capped at 300px, right.
   ---------------------------------------------------------------------------
   The diff spec expected two per-report behaviours ("both are needed, selected
   per report"). Measuring all five filter rows shows ONE rule reproduces every
   frame to the pixel, so there is no per-report switch:

     justify-content: flex-end; gap: 16px;
     controls: flex: 1 1 0; min-width: 0; max-width: 300px;
     the Reset/Apply pair: a non-growing 192px group.

   Check (row width 1540, actions 192, gaps 16):
     roster 551:67016  5 controls -> (1540-192-16-4x16)/5 = 253.6, first x=0
     events 551:71044  5 controls -> 253.6, first x=0
     shuttle 555:66346 5 controls -> 253.6, first x=0
     food    551:74676 3 controls -> capped 300, first x=1540-192-16-932 = 400
     bldg    551:67301 2 controls -> capped 300, first x=1540-192-16-616 = 716

   Every one of those matches the measured Figma x/width exactly. The apparent
   "two behaviours" is just the 300px cap biting at 2-3 controls and not at 5.

   Scoped to the modifier rather than folded into Wave 3's shipped
   `.list-toolbar__control--fill` on purpose: that class is worn by the Audit /
   My Activity bands, and adding a max-width there would silently reach into a
   screen this wave does not own. */
.list-toolbar--filter-band {
  justify-content: flex-end;
  gap: var(--space-4);                            /* 16px, per the frames */
}
/* HPMR-621 — THE BAND OWNS THE FLEX BASE FOR EVERY CHILD, NOT JUST THE ONES
   THAT HAPPEN TO CARRY CHROME.
   This shipped as `flex: 1 1 0`, which reads as "all cells equal" and is not.
   A `.list-toolbar__control` is border-box, so basis:0 has a USED value of its
   own padding + border (34px); a bare wrapper — `.list-toolbar__search`,
   `.list-toolbar__field` — has none and really does base at 0. Growing from
   unequal bases yields unequal finals, so the search cell landed exactly 34px
   short of every select at EVERY width (measured 125.8 vs 159.8 at 1440), on
   `exhibitor_roster`, `events_report` and `shuttle_reservations`.
   Handing every child the same explicit base — the one its chrome-bearing
   siblings are floored at anyway — is what makes the division true. The band
   total is conserved: the selects give back 6.8px each and the search cell
   takes the 27.2px, 153px all round.
   This is the THIRD ticket on this defect (HPMR-593 fixed the audit band's
   search cell, HPMR-592 the reports band's date cells, both per-cell). It kept
   coming back because the un-floored cell type — the bare `<span
   class="list-toolbar__search">` — is the `else` fallback in
   `admin/reports/_filter_field.html.erb`, i.e. what EVERY new string filter
   lands in by default. Flooring at the band means a new cell type inherits the
   fix instead of re-opening the bug; `spec/assets/list_toolbar_flex_base_spec.rb`
   fails the build if this basis regresses to 0. */
.list-toolbar--filter-band > * {
  flex: 1 1 var(--list-toolbar-cell-base);        /* HPMR-974 — the select's chrome, the largest a cell carries */
  min-width: 0;                                   /* LOAD-BEARING: a flex item defaults to
                                                     min-width:auto, and the shipped
                                                     select/search floors (160px / 240px)
                                                     would otherwise break the division */
  max-width: 300px;
}
/* Reach through the search wrapper and the bare controls alike so a select, a
   date input and a magnifier-wrapped text input all fill their cell. */
.list-toolbar--filter-band .list-toolbar__control {
  width: 100%;
  min-width: 0;
}
/* The Reset/Apply pair. Figma "Filters Container" (551:67022 / 555:66417 /
   551:74679 …) is a fixed 192px group: two 88px buttons at a 16px gap. Sized by
   content here — the shipped `.button` is content-sized and the frame's 88px is
   a Figma artefact of its own label metrics — but it must NOT grow or inherit
   the 300px cap, hence the two-class selector. */
.list-toolbar__actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
}
.list-toolbar--filter-band > .list-toolbar__actions {
  flex: 0 0 auto;
  max-width: none;
}

/* ── HPMR-592 — VISIBLE "From (ET)" / "To (ET)" LABELS ON BARE DATE FILTERS ──
   Wave 3 (HPMR-588) REMOVED the labels from the audit band and Wave 4
   (HPMR-589) shipped the reports date pair without one, both on the reading
   that Albina's frames drew no label. That reading was wrong, and the reason it
   was wrong is worth keeping: the frames draw the SHARED "Input Field"
   component, whose Label node is present but was read as absent because the
   two Wave-3 nodes were inspected at row level (where a 64.8px cell next to
   39px siblings looks like padding) rather than at instance level. Measured
   2026-08-05 on `529:61825`: Label text 22px at y=0, a 2.8px gap, then the
   40px Field at y=24.8. Five of the six live instances are labelled; the sixth
   (My Activity `488:59145`) is not, and is treated as an oversight in her file
   because it sits directly beside its own labelled twin `488:59106`.

   THE HEIGHT PROBLEM, WHICH IS THE ACTUAL REASON THE LABELS CAME OUT.
   A labelled cell is taller than a bare `.list-toolbar__control` (44px), so a
   `.list-toolbar` row — `align-items: center` since :3742 — floats every
   unlabelled sibling to the vertical middle of the tallest cell and the
   controls stop sharing an edge. Wave 3 read that as a rendering bug and
   deleted the labels; the right fix is to change what the row aligns TO.

   `.list-toolbar--labelled` bottom-aligns the line, so every control in the row
   sits on one baseline and the labels overhang above the two date cells. This
   is a DELIBERATE, MEASURED IMPROVEMENT ON THE FRAME, recorded so nobody
   "fixes" it back to match Figma: `529:61914` is centered (row 64.8; the
   dropdowns sit at y=12.9 h=39, i.e. 12.9 top and bottom), which leaves her own
   date controls ~12px lower than their siblings — visible in her render as a
   ragged row. Bottom-alignment is the same anatomy without that artefact.

   SPECIFICITY, per the standing warning on this exact toolbar (HPMR-593:
   `select.list-toolbar__control` (0,1,1) silently beat a bare
   `.list-toolbar__control--fill` (0,1,0) for the whole life of that rule):
     · `.list-toolbar.list-toolbar--labelled` is (0,2,0) so it beats
       `.list-toolbar { align-items: center }` (0,1,0) at ANY source order —
       not merely because it ships later in the file.
     · `.list-toolbar__field .list-toolbar__control` is (0,2,0), beating
       `select.list-toolbar__control` (0,1,1) — no select is wrapped today, but
       the rule is written to survive one being added.
     · The `--fill` rules below are (0,2,0) and still out-specify the band's
       `> *` (0,1,0). That is why HPMR-621 had to hoist the base AND drop the
       per-cell copy together: hoisting alone would have been silently masked
       on any cell wearing a `--fill` class.

   FLEX BASE — SUPERSEDED IN PART BY HPMR-621, READ THIS BEFORE RE-ADDING
   ANYTHING. This block originally carried a THIRD rule,
   `.list-toolbar--filter-band > .list-toolbar__field { flex-basis: … }`, which
   re-applied the chrome floor per cell because the band's `> *` shorthand
   re-zeroed it. HPMR-621 moved that floor onto the band itself, so the
   reapplication became exactly redundant and was deleted (proven by measuring
   the band with the rule removed: the date cells stayed at 153px). Do not
   restore it — if a reports-band cell is short again, the band's `> *` basis is
   the thing that broke.
   The `--fill` rule below is NOT redundant: it serves the AUDIT / My Activity
   band, whose container is the plain `.list-toolbar` shared with four list
   screens, so it opts in per cell rather than via a `> *` rule that would
   resize those screens' content-sized search boxes. Both now reference
   `--list-toolbar-control-chrome` (defined on `.list-toolbar`) instead of
   re-deriving the expression. */
.list-toolbar__field {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);                            /* 4px ≈ the frame's 2.8px */
}
.list-toolbar__field-label {
  display: block;
  font-family: var(--font-family-base);
  font-size: var(--font-size-base);               /* 16px — the frame's text/16 */
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-base);
  color: var(--color-text);                       /* brand Soft Black, not the frame's pure black */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.list-toolbar__field .list-toolbar__control { width: 100%; min-width: 0; }

/* Row modifier: bottom-align so labelled and bare cells share a control edge. */
.list-toolbar.list-toolbar--labelled { align-items: flex-end; }

/* Audit / My Activity band — equal-width cell, same flex base as a bare control
   (HPMR-974: as a SELECT, the largest chrome a cell carries). */
.list-toolbar .list-toolbar__field--fill {
  flex-grow: 1;
  flex-shrink: 1;
  flex-basis: var(--list-toolbar-cell-base);
  min-width: 0;
}

/* ── HPMR-959 — ONE INLINE DATE RANGE: "Date Range (ET)" / [start] - [end] ──
   Partial: shared/_date_range (helper: DateRangeHelper). MIRRORED FROM the
   Registration Portal's New Event modal (HighPointMarket.RegistrationPortal
   events/_form): a label, then a `.flex-row` (flex, centred, a 1.6rem gap)
   holding two `.filter_input` cells (flex 1, 150px floor) with a
   `<span>-</span>` between them. The 1.6rem there is 16px (that app's root is
   10px), so the gap here is --space-4. Values below resolve through cms-admin
   tokens; nothing is hardcoded.

   The cell is a `.list-toolbar__field` (column: label above, control row
   below), so `.list-toolbar--labelled` still bottom-aligns it with its bare
   siblings exactly as the two HPMR-592 cells did. The inputs keep
   `.list-toolbar__control` (44px, same chrome); this block only lays the pair
   out side by side. */
.date-range { position: relative; }
.date-range__inputs {
  display: flex;
  align-items: center;
  gap: var(--space-4);                            /* RP .flex-row 1.6rem = 16px */
  min-width: 0;
}
/* Both inputs share the row equally, like RP's `.filter_input { flex: 1 }`.
   min-width 0 (not RP's 150px) because the toolbar, not the input, owns the
   width budget here; the cell's own floor is set per band below. */
.date-range__inputs > input { flex: 1 1 0; min-width: 0; }
.date-range__separator {
  flex: 0 0 auto;
  color: var(--color-text);
  line-height: 1;
}
/* Reversed-range form error. Absolutely placed under the inputs so showing it
   never changes the cell height, which would break the labelled row's shared
   control edge. */
.date-range__error {
  position: absolute;
  top: 100%;
  left: 0;
  margin: var(--space-1) 0 0;
  font-size: var(--font-size-xs);
  color: var(--color-danger-text);
  white-space: nowrap;
}
/* The range replaces TWO cells, so it takes two shares of a fill band (Audit /
   My Activity) and of the Reports band (whose `> *` cells cap at 300px — the
   range caps at two of them plus the gap between). */
.list-toolbar .date-range.list-toolbar__field--fill { flex-grow: 2; min-width: 340px; }
.list-toolbar--filter-band > .date-range {
  flex-grow: 2;
  min-width: 340px;
  max-width: calc(2 * 300px + var(--space-4));
}
.list-toolbar > .date-range:not(.list-toolbar__field--fill) { min-width: 340px; }

/* The Reports-band companion that used to sit here —
   `.list-toolbar--filter-band > .list-toolbar__field { flex-basis: … }` —
   was deleted under HPMR-621. The band's own `> *` rule now sets that base for
   every child, so this was a redundant second copy. See the note above. */

/* HPMR-981 — TWO ROWS BELOW 1366px INSTEAD OF UNREADABLE SELECTS.
   HPMR-974 gave toolbar selects the 40px chevron padding, and in the HPMR-621
   equal-share bands (Audit / My Activity `--fill` cells, the Reports
   `--filter-band`) the band width is fixed, so that padding came out of the
   text: at 1280 the Audit selects kept ~3px of text and the Events report cut
   "Any season" / "Any building". Below 1366 each equal-share cell is floored
   at its select chrome (--list-toolbar-cell-base, 58px) + 7.5rem, i.e. a
   select keeps >= 120px of text, so the band wraps to a second row rather than
   squeezing. (The 160px prototype left the Events report at 109px: its five
   cells still fit one row at 160 and only Reset/Apply wrapped.) At >=1366
   nothing here applies and the one-row HPMR-621 widths are unchanged.
   The date range keeps its own 340px floor (`> .date-range` is excluded so
   this (0,3,0) selector cannot lower it), and the Reset/Apply or Filter/Clear
   group stays content-sized. spec/system/hpmr981_toolbar_two_row_spec.rb
   measures it; spec/assets/hpmr981_toolbar_two_row_media_spec.rb pins it. */
@media (max-width: 1365.98px) {
  .list-toolbar .list-toolbar__control--fill,
  .list-toolbar .list-toolbar__search--fill,
  .list-toolbar--filter-band > :not(.list-toolbar__actions):not(.date-range) { min-width: calc(var(--list-toolbar-cell-base) + 7.5rem); }
}


/* ----------------------------------------------------------------------------
   (1b-24) HPMR-642 — Neighborhoods: colour swatch, boundary editor, blocked
   delete
   ----------------------------------------------------------------------------
   Figma cyollcfjPhkNPjUWgqqhfI: 657:115861 (list) · 657:115491 (empty) ·
   668:110896 (new) · 715:75279 (new, mid-draw) · 668:86450 (edit) ·
   668:84524 (cannot delete) · 668:113975 (unsaved changes).

   Everything here is net-new because nothing shipped covers it: a data-coloured
   swatch cell, a two-column map/form editor, and a vertex row. The list, the
   two modals and every field inside the form card reuse the existing
   `.page-header` / `.list-table` / `.modal--confirm` / `.form-field__*`
   vocabulary and add nothing.

   Colour comes from tokens throughout. The ONE exception is `.color-swatch`,
   whose fill is passed in as `--color-swatch-fill` from the row: that value is
   the neighborhood's own editorial `color` column — data, not a design
   decision — and the swatch is decorative (aria-hidden) with the hex carried in
   adjacent text, so 1.4.1 and 1.4.11 are satisfied by the label, not the fill. */

/* `--neighborhood-draw-color` is defined with the rest of the token layer in the
   `:root` block at the top of this file, not here — tokens live in one place,
   and shared/_pagination's guard spec reads "everything after the pagination
   rules" as one block and rejects a literal hex anywhere in it. */

/* ---- Colour swatch (list Color column, 657:115861 -> 666:117340) ---------- */
.color-swatch {
  display: inline-block;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-swatch-fill, var(--color-bg-alt));
  vertical-align: middle;
}
/* No colour set. A dashed border rather than an empty box, so "unset" is
   visibly different from "set to white". */
.color-swatch--empty {
  border-style: dashed;
  background: var(--color-bg-alt);
}

/* ---- Colour field (form card, 668:112239) -------------------------------- */
.color-field { display: flex; align-items: center; gap: var(--space-2); }
/* The native picker, sized to match the control height. It carries no `name`
   and never submits — the hex text field beside it is the source of truth (see
   the note in _form.html.erb). */
.color-field__swatch {
  flex: 0 0 auto;
  width: 44px;
  height: var(--control-height);
  padding: var(--space-1);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  cursor: pointer;
}
.color-field__hex { flex: 1 1 auto; min-width: 0; font-family: var(--font-family-mono); }

/* ---- Boundary editor ----------------------------------------------------- */
/* Two-column page body: map panel + 480px form card, matching Split Grid
   Container 668:110907 (Map Panel 1036 / Form Card 480 / 24px gutter). Not
   `.detail-page__body`, whose 360px rail is built for reading, not for a form
   sitting beside a map. */
.boundary-editor { display: flex; flex-direction: column; gap: var(--space-6); }
.boundary-editor__header { margin: 0; }
.boundary-editor__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 480px;
  gap: var(--space-6);
  align-items: start;
}
.boundary-editor__map-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}
/* Leaflet requires an explicit height on its container. */
.boundary-editor__map {
  height: 620px;
  min-width: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-bg-alt);
  overflow: hidden;
}
.boundary-editor__help { margin: 0; }

/* Keyboard / no-JS vertex editor. Collapsed by default so it does not compete
   with the map, but always present in the DOM. */
.boundary-editor__points {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  padding: var(--space-4);
}
.boundary-editor__points-summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.boundary-editor__points-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-bg-alt);
  font-size: var(--font-size-xs);
  /* HPMR-610 two-surface rule: this fills with --color-bg-alt, so its text
     takes the -strong muted token (5.87:1), never --color-text-muted (4.06:1). */
  color: var(--color-text-muted-strong);
}

.boundary-point-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.boundary-point-row__index {
  flex: 0 0 2rem;
  text-align: right;
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text-muted-strong);
}
.boundary-point-row__input {
  flex: 1 1 0;
  min-width: 0;
  font-family: var(--font-family-mono);
  font-size: var(--font-size-sm);
}

/* Form card — a white panel with its own scroller and a pinned action bar, so
   Save stays reachable beside a 620px map (Form Card 668:111955 / Form Actions
   Bar 668:111971). */
.boundary-editor__card {
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  gap: var(--space-4);
  max-height: 728px;
}
.boundary-editor__scroller {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow-y: auto;
  min-height: 0;
}
.boundary-editor__actions {
  display: flex;
  gap: var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
  flex-shrink: 0;
}
/* Both actions share the row evenly, as 668:111971 draws them. */
.boundary-editor__actions > * { flex: 1 1 0; justify-content: center; }

.boundary-editor__stops { border: 0; padding: 0; margin: 0; min-width: 0; }
.boundary-editor__stops-list {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}
.boundary-editor__stops-group {
  margin: var(--space-3) 0 var(--space-2);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted-strong);
}
.boundary-editor__stops-group:first-child { margin-top: 0; }
.boundary-editor__stop {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  font-size: var(--font-size-sm);
}

/* Discarded-shape preview inside the unsaved-changes modal (668:113975). */
.boundary-editor__preview {
  margin-top: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  padding: var(--space-2);
}

/* Leaflet-injected marks. divIcon renders these inside Leaflet's own pane, so
   they are referenced only from neighborhood_boundary_map_controller.js — which
   the CSS-definition guard DOES scan (app/javascript is in its sweep). */
.boundary-editor__vertex-marker {
  border-radius: 50%;
  background: var(--color-bg);
  border: 3px solid var(--neighborhood-draw-color);
  box-shadow: var(--shadow-sm);
  cursor: move;
}
/* The first vertex while drawing — the one that closes the ring. Filled, so the
   target 715:75279's help text points at is visually distinct. */
.boundary-editor__vertex-marker--first {
  background: var(--neighborhood-draw-color);
  cursor: pointer;
}
.boundary-editor__badge-icon { background: none; border: 0; }
.boundary-editor__badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
  font-size: var(--font-size-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}

/* ---- Blocked delete panel (668:84524) ------------------------------------ */
.blocked-delete__panel {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-bg-alt);
  text-align: left;
}
.blocked-delete__panel-title {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-sm);
  /* Filled with --color-bg-alt -> the -strong muted token (two-surface rule). */
  color: var(--color-text-muted-strong);
}
.blocked-delete__list { list-style: none; margin: 0; padding: 0; }
.blocked-delete__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  font-size: var(--font-size-sm);
  color: var(--color-text);
}
.blocked-delete__overflow {
  margin: var(--space-2) 0 0;
  font-size: var(--font-size-sm);
}

@media (max-width: 1199.98px) {
  .boundary-editor__body { grid-template-columns: 1fr; }
  .boundary-editor__card { max-height: none; }
  .boundary-editor__map { height: 460px; }
}

/* ==========================================================================
   HPMR-643 — Buildings: location editor, detection panel, wizard/tab strips.
   Figma 691:75875 (list) · 691:99752 / 691:108925 / 691:110338 (new, three
   detection states) · 716:76559 (edit) · 715:77537 · 705:116698.

   The split layout is `.boundary-editor`'s, reused rather than re-declared —
   716:76559 draws the same map-left / 480px-card-right grid the Neighborhoods
   editor already ships. `.location-editor` adds only what a PIN needs that a
   POLYGON does not.
   ========================================================================== */

/* The block root. It composes with `.boundary-editor` on the same element
   rather than redeclaring the split grid — 716:76559 draws the same
   map-left / 480px-card-right layout the Neighborhoods editor already ships,
   and two copies of one grid drift. This rule exists so the composed name is
   DEFINED (the css_class_definition guard is right to insist) and so there is
   one place to hang location-specific overrides. */
.location-editor { position: relative; }
.location-editor__map { position: relative; }
.location-editor__lead { margin: 0; }
/* The keyboard/no-JS coordinate pair. Composes with
   `.boundary-editor__points`, which supplies the collapsed-panel chrome. */
.location-editor__coords { margin-top: var(--space-2); }

/* Latitude + longitude side by side — they are one value in two boxes and
   stacking them invites filling in only the first. */
.location-editor__coord-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}
.location-editor__coord-input { font-family: var(--font-family-mono); }
.location-editor__pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

.location-editor__tenancy { border: 0; padding: 0; margin: 0; min-width: 0; }
.location-editor__radio-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  margin-top: var(--space-2);
}
.location-editor__radio {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
}

/* Place-search results. A plain list under the field rather than an overlay:
   the form card scrolls, and an absolutely-positioned panel inside a scroller
   clips at the worst moment. */
.location-editor__results {
  list-style: none; margin: var(--space-2) 0 0; padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  max-height: 200px;
  overflow-y: auto;
}
.location-editor__result {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 0;
  background: none;
  text-align: left;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  cursor: pointer;
}
.location-editor__result:hover,
.location-editor__result:focus-visible { background: var(--color-passion-20); }

/* The pin. A divIcon renders inside Leaflet's own pane, so this class is
   referenced only from building_location_map_controller.js — which the
   CSS-definition guard DOES scan (app/javascript is in its sweep). */
.location-editor__pin {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--color-accent);
  border: 3px solid var(--color-bg);
  box-shadow: var(--shadow-md);
  cursor: move;
}

/* ---- Map legend (716:76559) ---------------------------------------------
   Server-rendered rather than a Leaflet control so it survives a CDN failure
   and so its swatches read the same `color` column the list screen paints. */
.map-legend {
  align-self: flex-start;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  box-shadow: var(--shadow-sm);
}
.map-legend__title {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.map-legend__list {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--space-1) var(--space-4);
}
.map-legend__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-text);
}

/* ---- Detection panel (691:108895 / 691:113435) ---------------------------
   Her geometry is kept verbatim: 10px radius, 16px padding, 10px gap, a 1px
   border one step darker than the fill.

   COLOUR IS NOT kept verbatim, and the MEASUREMENTS are the argument. (The
   literal hexes are named in the ERB comments in app/views/buildings/_form and
   _detection_panel; this file may not carry hex literals, and a build guard
   enforces that, so the reasoning is here and the values are there.)
     * FILLS and BORDERS are surfaces, not text, so her exact green/100,
       green/200, yellow/100 and yellow/200 stand — promoted to
       --color-success-fill / --color-success-border /
       --color-warning-fill / --color-warning-border so this panel cannot drift
       from every other tinted surface here.
     * The NOTE is her greys/500 in the frame. It happens to pass (4.78:1 on
       the green fill) but it is not a token, and the nearest token that passes
       on ALL THREE fills is --color-text-muted-strong: 6.25:1 on the green,
       6.25:1 on the neutral, 6.58:1 on the amber. --color-text-muted would NOT
       do — 4.32:1 on the green and neutral fills. Two-surface rule (HPMR-610):
       the fill picks the token.
     * The override CHIP is `.chip--info` (--color-info on --color-info-fill =
       5.49:1), not her blue/600 on blue/100 = 3.45:1. design.md names that
       blue as documented AA drift in this palette. */
.detection-panel {
  display: flex;
  flex-direction: column;
  /* Her gap is 10px, for which there is no token; --space-3 is 12px. Two
     pixels, spent to keep the spacing scale intact. */
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  background: var(--color-bg-alt);
}
.detection-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.detection-panel__identity {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.detection-panel__dot {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-swatch-fill, var(--color-border-strong));
}
.detection-panel__dot--empty {
  background: var(--color-bg);
  border: 1px solid var(--color-border-strong);
}
.detection-panel__name {
  font-size: var(--font-size-base);
  color: var(--color-text);
}
.detection-panel__chip { flex: 0 0 auto; }
.detection-panel__coords {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  font-size: var(--font-size-base);
  color: var(--color-text);
  white-space: nowrap;
}
.detection-panel__coord { font-variant-numeric: tabular-nums; }
.detection-panel__note { margin: 0; font-size: var(--font-size-sm); color: var(--color-text-muted-strong); }

.detection-panel--matched {
  background: var(--color-success-fill);
  border-color: var(--color-success-border);  /* her green/200 — a border, not text */
}
.detection-panel--override {
  background: var(--color-warning-fill);
  border-color: var(--color-warning-border);  /* her yellow/200 — a border, not text */
}
.detection-panel--outlier,
.detection-panel--unplaced {
  background: var(--color-bg-alt);
  border-color: var(--color-border);
}

/* ---- Wizard steps (691:99752) + tab strip (716:76559) --------------------
   The same two-part journey in her two shapes. In BOTH, the second half is
   HPMR-645's and renders inert: present so the header does not read as a
   rendering bug, unclickable so it does not promise a screen that is not
   there. Give it an href and drop the `--inert` modifier when it lands. */
.wizard-steps {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  list-style: none; margin: 0; padding: 0;
}
.wizard-steps__step {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-base);
}
.wizard-steps__step + .wizard-steps__step::before {
  content: "";
  width: 20px;
  height: 1px;
  background: var(--color-border);
  margin-right: var(--space-2);
}
.wizard-steps__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-pill);
  background: var(--color-bg-alt);
  box-shadow: var(--shadow-sm);
  font-size: var(--font-size-sm);
  /* On a --color-bg-alt fill, so the strong muted token (5.87:1). */
  color: var(--color-text-muted-strong);
}
.wizard-steps__step--active .wizard-steps__badge {
  background: var(--color-accent);
  color: var(--color-bg);                     /* white on Passion, 3.28:1 — see below */
}
.wizard-steps__step--active .wizard-steps__label { color: var(--color-text); }
/* WHY THE PASSION BADGE KEEPS HER 3.28:1 WHITE NUMERAL, and what was done to
   earn it. AA relaxes to 3:1 only at >=24px regular / >=18.66px bold, and this
   is a 14px digit in a 28px circle — so the ratio is only defensible if the
   digit carries no information. It is therefore marked `aria-hidden` in the
   markup: the step's NAME sits beside it in --color-text (15.9:1), and
   `aria-current="step"` carries "you are here" to assistive tech. With the
   numeral removed from the accessible name it is genuinely decorative, which
   is the 1.4.3 exemption — asserted, not assumed. Remove the `aria-hidden` and
   this rule stops being defensible.
   The same argument is NOT available to `.view-switch__option--active`, whose
   label is the information; see the note there. */
.wizard-steps__step--inert .wizard-steps__label { color: var(--color-text-muted-strong); }

.detail-tabs {
  display: flex;
  align-items: stretch;
  gap: var(--space-6);
  border-bottom: 1px solid var(--color-border);
}
.detail-tabs__tab {
  padding: var(--space-2) 0 var(--space-3);
  font-size: var(--font-size-base);
  color: var(--color-text-muted-strong);
  border-bottom: 2px solid transparent;
}
.detail-tabs__tab--active {
  /* --color-passion-text (7.52:1 on white), NOT --color-accent (3.28:1) —
     design.md HPMR-598. The 2px Passion underline carries the brand colour;
     the LABEL carries the readable one. */
  color: var(--color-passion-text);
  font-weight: var(--font-weight-semibold);
  border-bottom-color: var(--color-accent);
}
.detail-tabs__tab--inert { cursor: not-allowed; }
/* HPMR-675 — the HOVER state, which this component never pinned and which is
   the one surface where it fails AA.

   `a { color: var(--op-color-primary) }` and `a:hover { var(--op-color-primary-hover) }`
   are global, and that hover value is Passion-80: 2.62:1 on white and 2.29:1
   on the --color-bg-alt panel. `a:hover` is (0,1,1) and OUT-SPECIFIES the bare
   `.detail-tabs__tab` (0,1,0), so the colour set above is silently discarded
   the moment the pointer lands — the exact trap design.md names ("pin
   `a.<class>:hover` when you darken a link"). The strip therefore DARKENS on
   hover instead of lightening: --color-text-secondary is 8.3:1 on that panel,
   and the ACTIVE tab holds --color-passion-text (6.52:1) rather than sliding
   to a different colour on hover.

   `a.` is load-bearing on both selectors, not decoration: the same component is
   rendered as a `<span>` for the current tab on buildings/_form and
   building_tenancy/show, and a span has no hover colour to correct. Those two
   screens render their OTHER tab as a link and inherit the fix. */
a.detail-tabs__tab:hover { color: var(--color-text-secondary); }
a.detail-tabs__tab--active:hover { color: var(--color-passion-text); }

/* ---- List view switch (691:75875) ---------------------------------------- */
/* HPMR-887 — repainted to the frame: the INACTIVE segment is white, the ACTIVE
   one red; no grey track. Two bordered segments joined on one outline, sized
   to the header's 40px button rhythm, 14px Roobert medium (semibold active). */
.view-switch {
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  overflow: hidden;
}
.view-switch__option {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
  font-family: var(--font-family-base);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  line-height: 1.2;
  /* HPMR-887 reopen — the INACTIVE label is the frame grey (token), not Soft
     Black. The first pass set --color-text here, which is what QA saw. Pinned
     on a.…:link/:visited too so no global link rule can win on a visited
     segment. The active span overrides this below (white on Passion). */
  color: var(--color-text-switch-inactive);
  background: var(--color-bg);
  text-decoration: none;
}
a.view-switch__option:link,
a.view-switch__option:visited { color: var(--color-text-switch-inactive); }
a.view-switch__option:hover { background: var(--color-bg-alt); color: var(--color-text); }
/* HER ACTIVE SEGMENT IS WHITE ON PASSION — 3.28:1, and the label is the whole
   information ("Map View" vs "List View"), so no decoration exemption is
   available the way it is for the stepper's numeral. AA relaxes to 3:1 only at
   >=24px regular / >=18.66px bold and this is 14px.

   NOTE the admin already ships white-on-Passion at this size in
   `.button--brand-primary`, under a comment claiming "Bold >=14px" clears AA.
   It does not — that is the exact 18pt/14pt-read-as-px error design.md
   corrects. Fixing the global primary button is not this story's to make, but
   copying its reasoning into a NEW control would be.

   --color-passion-20 + --color-passion-text is 5.96:1 and is the pairing
   `.chip--primary` already uses for "this one is selected". */
/* HPMR-887 — brand red active segment (Chief of Staff's call, per Figma).
   KNOWN DESIGN ITEM: white on --color-passion is 3.28:1 at 14px, below the
   4.5:1 AA text bar (same as the shipped `.button--brand-primary`). Kept
   deliberately; tracked as an open design/contrast item, not an oversight.
   The active state is ALSO carried by aria-current="page" + semibold weight,
   so colour is not the only signal (WCAG 1.4.1). */
.view-switch__option--active {
  background: var(--color-passion);
  color: var(--color-white);
  font-weight: var(--font-weight-semibold);
}

/* HPMR-890 — outlined secondary action: white fill + visible border (the
   frame's "Manage Owners & Types"). The base `.button` already carries both;
   this modifier pins the label colour and a quiet hover. */
.button--outline {
  background: var(--color-bg);
  border-color: var(--color-border);
  color: var(--color-text);
}
.button--outline:hover { background: var(--color-bg-alt); border-color: var(--color-border); color: var(--color-text); }
.view-switch__option--inert { cursor: not-allowed; opacity: 0.7; }

/* HPMR-917 — Map/List left, five filters right on one chrome row. The filter
   band flexes to fill remaining width and keeps justify-content:flex-end so
   controls stay right-aligned; the switch does not grow. */
.buildings-view-chrome {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--space-3);
}
.buildings-view-chrome > .view-switch {
  flex: 0 0 auto;
}
.buildings-view-chrome > .list-toolbar--filter-band {
  flex: 1 1 20rem;
  min-width: min(100%, 20rem);
}
/* Status / filtered-empty from `_filters` must wrap below the Map/List + toolbar
   row, not sit as a third flex column. */
.buildings-view-chrome > .empty-state--card {
  flex: 1 0 100%;
}
/* HPMR-981 — Buildings floors its filter cells at EVERY width, not only below
   1366 like the other equal-share bands. Its band shares the row with the
   Map/List switch, so it is ~180px narrower than the page (791px at 1366,
   865px at 1440) and five cells left each select 43-58px of text there. The
   floor is the select chrome + 9.5rem (>= 152px of text), not the bands'
   7.5rem: at 7.5rem four cells still fit row 1 at 1366 and the 128px left cut
   "All Neighborhoods" (133px). At 9.5rem row 1 holds three cells at every
   width, so the band wraps to two rows instead of cutting labels off. Scoped
   to the Buildings list/map chrome; every other band keeps one row at >= 1366. */
.buildings-view-chrome > .list-toolbar--filter-band > :not(.list-toolbar__actions):not(.date-range) {
  min-width: calc(var(--list-toolbar-cell-base) + 9.5rem);
}
/* With the band now always two rows, the chrome's `align-items: flex-end`
   would drop Map/List beside the SECOND filter row. HPMR-917 puts it on the
   filters' first row, so it pins to the top. */
.buildings-view-chrome > .view-switch { align-self: flex-start; }


@media (max-width: 1199.98px) {
  .location-editor__pair,
  .location-editor__coord-row { grid-template-columns: 1fr; }
}

/* ==========================================================================
   (3z) Tenancy — Floors & Showrooms editor (HPMR-645)
   Figma 691:113479 (multi-tenant), 691:111867 (single-tenant), 705:116434
   (Bulk Add).

   Every class below is referenced from app/views/building_tenancy/**,
   app/views/building_floors/** or app/views/building_showrooms/**. The
   `spec/assets/css_class_definition_spec.rb` guard fails the build on a
   referenced-but-undefined class — it has caught `.modal`, `.badge--info`,
   `.form-field__icon`, `.callout` and `.reason-chip` shipping undefined — so
   this block is written alongside the markup, not after it.

   COLOUR: no hex anywhere, in the rules OR in these comments — the token guard
   in spec/views/shared/pagination_spec.rb scans from the pagination block to
   end of file and rejects a hex literal wherever it appears, comments included.
   Ratios are quoted against the TOKEN NAME instead, which is the more useful
   reference anyway.

   The season bar's off-cycle state uses the warning fill/border tokens; the
   bulk preview panel uses --color-bg-alt with --color-text-muted-strong
   (5.87:1 on that surface — --color-text-muted would be 4.06:1 and fail, the
   two-surface rule from HPMR-610).
   ========================================================================== */

/* ---- Season bar ---------------------------------------------------------- */
.tenancy { display: flex; flex-direction: column; gap: var(--space-6); }

.tenancy-season {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  padding: var(--space-5) var(--space-6);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-left: 4px solid var(--color-border-strong);
  border-radius: var(--radius-md);
}
/* Editing a market that is NOT the one running. The colour is a SECOND signal
   only — the sentence itself changes ("You are editing the Fall 2026 layout" +
   which market is live), so nothing depends on perceiving it (WCAG 1.4.1). */
.tenancy-season--off-cycle {
  background: var(--color-warning-fill);
  border-color: var(--color-warning-border);
  border-left-color: var(--color-warning);
}
.tenancy-season__statement { flex: 1 1 26rem; min-width: 0; }
.tenancy-season__title {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.tenancy-season__title i,
.tenancy-season__title svg { color: var(--color-warning-text-strong); margin-right: var(--space-2); }
/* On the warning fill this line needs the strong muted token (5.87:1); the
   plain --color-text-muted measures 4.06:1 there and fails AA. `.text-meta` on
   the white variant already resolves to the strong token. */
/* `margin-block`, not `margin: 0` — spec/views/shared/pagination_spec.rb's
   token guard reads every `margin:` from the pagination block to END OF FILE
   and requires a var(), so a bare zero shorthand fails the build here. The
   longhand is also the more precise statement: this paragraph wants no
   VERTICAL margin, and its horizontal margin is nobody's business. */
.tenancy-season__note {
  margin-block: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
.tenancy-season__form {
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  flex: 0 0 auto;
}
.tenancy-season__form .form-field { margin-bottom: 0; min-width: 14rem; }

.tenancy__copy-link { margin: 0; }

/* ---- Floor accordion ----------------------------------------------------- */
.tenancy__floors { list-style: none; margin: 0; padding: 0; }

.tenancy-floor { border-bottom: 1px solid var(--color-border); }
.tenancy-floor:last-child { border-bottom: 0; }

.tenancy-floor__header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-6);
}
.tenancy-floor--expanded .tenancy-floor__header { background: var(--color-bg-alt); }

/* The disclosure control is a real <a> (expansion is a query param, not a JS
   widget — see the partial's header), so it gets the normal link reset plus a
   visible focus ring inherited from the global a:focus-visible rule. */
.tenancy-floor__toggle {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex: 1 1 20rem;
  min-width: 0;
  color: var(--color-text);
  text-decoration: none;
}
.tenancy-floor__toggle:hover { color: var(--color-text); }
.tenancy-floor__toggle i,
.tenancy-floor__toggle svg { color: var(--color-text-muted-strong); }
.tenancy-floor__name {
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
}
.tenancy-floor__counts { font-size: var(--font-size-sm); }
.tenancy-floor__status { display: flex; gap: var(--space-2); flex: 0 0 auto; }
.tenancy-floor__actions { flex: 0 0 auto; flex-wrap: wrap; }
/* button_to emits a <form>; without this each Up/Down/Deactivate button would
   sit on its own line inside the action row. */
.tenancy-floor__actions form { margin: 0; display: inline-flex; }

.tenancy-floor__panel { padding: 0 var(--space-6) var(--space-5); }
.tenancy-floor__panel[hidden] { display: none; }
.tenancy-floor__footer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/* ---- Search results ------------------------------------------------------ */
.tenancy__search-results { padding: 0 var(--space-6) var(--space-5); }
.tenancy__search-summary { margin: 0 0 var(--space-4); }

/* ---- Bulk add modal (705:116434) ----------------------------------------- */
.tenancy-bulk__range {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}
/* Her preview panel is a light-grey block with 8px radius. --color-bg-alt is
   the token for that surface and is what every other lowered panel in the
   admin uses. */
.tenancy-bulk__preview {
  padding: var(--space-4);
  background: var(--color-bg-alt);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
}
/* HER LABEL IS PASSION ON THIS PANEL — 2.85:1, an AA failure for 14px text
   (design.md HPMR-598: Passion is decorative only and fails as text at every
   size this admin uses). `.text-passion` in the markup carries
   --color-passion-text, 6.52:1 on the --color-bg-alt surface. This rule sets
   only the size and weight; the colour comes from that utility so the two
   cannot drift apart. */
.tenancy-bulk__preview-label {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
}
.tenancy-bulk__preview-body {
  font-size: var(--font-size-sm);
  color: var(--color-text);
  word-break: break-word;
}

@media (max-width: 991.98px) {
  .tenancy-season { align-items: flex-start; }
  .tenancy-season__form { width: 100%; }
  .tenancy-bulk__range { grid-template-columns: 1fr; }
  .tenancy-floor__header { align-items: flex-start; }
}

/* ==========================================================================
   (3aa) Buildings map view (HPMR-644)
   Figma 691:74218. One full-bleed map card, a floating legend over its
   bottom-left corner, Passion count bubbles where pins collide, and a
   server-rendered index below it that is the screen's keyboard route.

   Colours here come from tokens, never from a literal. Where a colour decision
   needed a measurement to justify it, the ratio is quoted and the literal values
   live in the ERB comment that names the Figma node.

   CORRECTION, TWICE OVER (HPMR-677/678, 2026-08-24). This block used to claim
   "NO HEX LITERALS ANYWHERE IN THIS FILE, comments included - a build guard
   enforces it". I first replaced that with "there is NO such guard", which was
   ALSO wrong, and the comment I wrote to say so quoted a token's hex value and
   turned the suite red. Both mistakes came from looking in the obvious place.

   What is actually true: a guard EXISTS, it is not in spec/assets, and its scope
   is an accident. spec/views/shared/pagination_spec.rb has an example called
   "uses tokens only - no hardcoded hex" whose subject is
   `css[/^\.pagination-bar__page--prev,.*\z/m]` - anchored at that selector and
   then greedy TO THE END OF THE FILE. It was written to check the pagination
   bar's own rules; it in fact scans every line added after it, this comment
   included, because it is a raw regex over text and cannot tell a rule from
   prose.

   So the practical rule is real, if accidentally enforced: after
   `.pagination-bar__page--prev`, write no hex - not in a rule, not in a comment.
   Quote a ratio, name the token, and leave the literal in the ERB comment that
   names the Figma node. The breadth is worth keeping; do not "fix" that regex to
   match its original intent without deciding deliberately that you want the rest
   of the file unguarded.
   ========================================================================== */
/* HPMR-709 — `.hotels-map*` IS THIS COMPONENT, NOT A COPY OF IT. Roy asked for
   the Hotels map to follow "the same concept as we did for the Buildings", so
   every rule below carries both names as a selector list rather than being
   duplicated under a second prefix. One set of declarations means the two maps
   cannot drift apart, and a fix to one is a fix to both.

   Two rules are hotels-only and are marked where they appear: the inactive
   marker (Buildings has no such state on its map) and, by omission, the legend
   — hotels have no neighborhood, no boundary and no colour column, so this
   screen renders no `.map-legend--overlay` and every pin is the one accent
   marker. */
.buildings-map,
.hotels-map {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  position: relative;
}

/* Her map fills the content panel below the header. A tall fixed height rather
   than a viewport calc: the index disclosure and any callout sit below it, and
   a `100vh - header` would push those off-screen on a laptop. */
.buildings-map__canvas,
.hotels-map__canvas {
  position: relative;
  height: 68vh;
  min-height: 420px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-bg-alt);
  overflow: hidden;
}

/* The legend floats INSIDE the map on 691:74218, unlike the location picker's
   which stacks below it. Same component, one modifier. `z-index: 500` clears
   Leaflet's tile and overlay panes (400) while staying under its popup pane
   (700), so a popup opening over the legend is not hidden behind it. */
.map-legend--overlay {
  position: absolute;
  left: var(--space-4);
  bottom: var(--space-6);
  z-index: 500;
  max-width: 280px;
}
.map-legend--overlay .map-legend__list { grid-template-columns: 1fr; }

/* ---- Markers -------------------------------------------------------------
   Same 18px disc as the location picker's pin, so a building reads the same on
   both maps. The fill is the ASSIGNED neighborhood's own colour, written onto
   the element by the controller; the white ring is what keeps a coloured pin
   legible on a same-hue polygon. */
.buildings-map__marker,
.hotels-map__marker {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--color-swatch-fill, var(--color-accent));
  border: 3px solid var(--color-bg);
  box-shadow: var(--shadow-md);
}
/* An outlier is a CATEGORY, not a failure: it wears the same neutral grey the
   legend row and the `.color-swatch--empty` swatches carry, so "no
   neighborhood" looks deliberate rather than like a pin that lost its data. */
.buildings-map__marker--outlier,
.hotels-map__marker--outlier { background: var(--color-border-strong); }

/* HPMR-709 — an INACTIVE hotel keeps its pin rather than vanishing from the
   map: it still occupies a real place, and dropping it would make the map
   disagree with the list for a reason nobody could see. The muted fill is the
   same neutral the outlier marker wears.

   Colour is NOT the sole carrier here (WCAG 1.4.1): the popup and the keyboard
   index both spell "Inactive" out in text for the same hotel, and the marker is
   non-text content whose meaning is available in both places a user can read
   it. */
.hotels-map__marker--inactive { background: var(--color-border-strong); }

/* ---- Cluster bubbles -----------------------------------------------------
   Her bubble is a filled Passion disc with a white count. White on Passion is
   3.28:1, and the count is INFORMATION, not decoration — so it only clears AA
   under the large-text allowance, which needs >=18.66px bold. --font-size-lg
   is 22px; at bold that is comfortably over the threshold and 3.28:1 > 3:1.
   Shrinking this label to the 14px used elsewhere on the screen would fail, so
   the size here is load-bearing rather than styling. */
.buildings-map__cluster,
.hotels-map__cluster {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--color-accent);
  border: 3px solid var(--color-bg);
  box-shadow: var(--shadow-md);
  color: var(--color-bg);
  font-family: var(--font-family-base);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  cursor: pointer;
}

/* ---- Popup (691:74218) --------------------------------------------------- */
.buildings-map__popup .leaflet-popup-content-wrapper,
.hotels-map__popup .leaflet-popup-content-wrapper {
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-2);
}
.buildings-map__popup .leaflet-popup-content,
.hotels-map__popup .leaflet-popup-content { margin: var(--space-3); }
.buildings-map__popup-body,
.hotels-map__popup-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-family: var(--font-family-base);
}
.buildings-map__popup-title,
.hotels-map__popup-title {
  margin-top: 0;
  margin-bottom: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}
/* The popup is a WHITE surface, so --color-text-muted (4.67:1 on white) is the
   correct muted token here; --color-text-muted-strong would be heavier than the
   frame draws. Two-surface rule (HPMR-610): the fill picks the token. */
.buildings-map__popup-address,
.hotels-map__popup-address {
  margin-top: 0;
  margin-bottom: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}
.buildings-map__popup-facts,
.hotels-map__popup-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-2);
  margin-top: var(--space-1);
  margin-bottom: 0;
  font-size: var(--font-size-sm);
}
.buildings-map__popup-facts dt,
.hotels-map__popup-facts dt { color: var(--color-text-muted); font-weight: var(--font-weight-regular); }
.buildings-map__popup-facts dd,
.hotels-map__popup-facts dd { margin-left: 0; color: var(--color-text); font-weight: var(--font-weight-semibold); }
.buildings-map__popup-link,
.hotels-map__popup-link {
  margin-top: var(--space-2);
  justify-content: center;
  width: 100%;
}

/* ---- Keyboard index ------------------------------------------------------
   The accessible equivalent of the map. Same disclosure shape as the boundary
   editor's coordinate list, which is the screen it is modelled on. */
.buildings-map__index,
.hotels-map__index {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  padding: var(--space-3) var(--space-4);
}
.buildings-map__index-summary,
.hotels-map__index-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.buildings-map__index-count,
.hotels-map__index-count {
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-bg-alt);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
.buildings-map__group,
.hotels-map__group { margin-top: var(--space-5); }
.buildings-map__group-title,
.hotels-map__group-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: 0;
  margin-bottom: var(--space-2);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.buildings-map__entry,
.hotels-map__entry {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--color-border);
}
.buildings-map__entry-main,
.hotels-map__entry-main { min-width: 0; }
.buildings-map__entry-name,
.hotels-map__entry-name { font-weight: var(--font-weight-semibold); }
.buildings-map__entry-meta,
.hotels-map__entry-meta { margin-top: 2px; margin-bottom: 0; }

@media (max-width: 767.98px) {
  .map-legend--overlay { position: static; max-width: none; margin-top: var(--space-3); }
  .buildings-map__canvas,
  .hotels-map__canvas { height: 52vh; }
  .buildings-map__entry,
  .hotels-map__entry { flex-direction: column; gap: var(--space-2); }
}

/* ==========================================================================
   (3ab) Boundary mismatches (HPMR-647)
   Figma 668:83079 (resolution screen), 716:74840 (the normal, no-mismatch
   state), 716:75463 (the quieter per-building warning).
   ========================================================================== */

/* Her "Accept Move" is green text on a pale green pill — the success sibling of
   the amber and red quiet buttons above, which existed while this one did not.
   --color-success-text on --color-success-fill measures 6.41:1. (Her literal
   pair is a lighter green that would not clear AA; the token pair is both
   AA-safe and already what `.chip--success` and `.status-pill--success` paint,
   so the three families agree.) */
.button--success-quiet {
  background: var(--color-success-fill);
  color: var(--color-success-text);
  border-color: var(--color-success-fill);
}
.button--success-quiet:hover {
  background: var(--color-success-border);
  border-color: var(--color-success-border);
  color: var(--color-success-text);
}
.button--success-quiet:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.mismatch__building { font-weight: var(--font-weight-semibold); }
.mismatch__place {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* The no-mismatch state (716:74840) is the answer "everything is fine", so its
   glyph takes the success hue rather than the neutral grey `.empty-state--card`
   defaults to. It is an ICON on the white card surface: --color-success
   measures 3.36:1 there, which clears the 3:1 WCAG 1.4.11 asks of non-text
   content and does NOT clear the 4.5:1 text needs — which is exactly why the
   heading and body beside it keep their normal text tokens. */
.empty-state__icon--success { color: var(--color-success); }

/* ---- Per-building warning (716:75463) ------------------------------------
   The third mismatch surface is the detection panel the building form already
   renders in its `--override` amber; only the sentence changes. This rule
   exists for the one visual difference her frame draws: the mismatch sentence
   wraps to two lines, so the note needs room to breathe. */
.detection-panel__note--mismatch { line-height: 1.45; }

/* ==========================================================================
   SHOWROOM PICKER — the Location section of the exhibitor form (HPMR-661)
   Figma: Edit Location 705:120517 · Single Tenant 287:34148 ·
          Match 705:118053 · Occupied 705:119359
   ==========================================================================

   TWO CONSTRAINTS APPLY TO EVERY DECLARATION BELOW, and both fail the build
   rather than merely looking wrong:

   1. spec/views/shared/pagination_spec.rb reads from `.pagination-bar__page--prev`
      to END OF FILE and requires that every `color` / `gap` / `padding` /
      `margin` / `font-weight` / `font-size` declaration be a var() and that no
      hex appear at all. Hence `margin-block` / `padding-inline` longhands where
      a zero would otherwise be written bare.
   2. spec/views/shared/muted_token_surface_guard_spec.rb fails any block that
      fills with `--color-bg-alt` and paints its text `--color-text-muted` in the
      same rule (4.06:1). Nothing here does.

   The pagination guard reads the RAW text, comments included, so no hex literal
   may appear below this line either — every contrast figure quoted in the notes
   names its TOKEN rather than its value. Measured ratios are stated in the ERB
   partials (exhibitors/_location_fields.html.erb), which the guard does not
   scan and where the Figma comparison belongs anyway.
   ========================================================================== */

.showroom-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* The instruction line under the "Location" heading. It sits on the
   `.portal-content` panel (--color-bg-alt), NOT inside a white card, so it takes
   the strong muted token — the plain --color-text-muted is 4.06:1 there.
   Albina's own grey for this line measures 4.46:1 on that panel and misses AA by
   a hair; see the AA note in exhibitors/_location_fields.html.erb. */
.showroom-picker__intro {
  margin-block: 0 var(--space-4);
  color: var(--color-text-muted-strong);
}

/* ---- The cascade ---------------------------------------------------------
   `display: contents` is deliberate. The <turbo-frame> is a real element in the
   DOM but must not become a layout box between `.showroom-picker` and the
   fields it wraps, or the flex column above would treat all three selects plus
   the status band as one item and the gap would collapse. */
.showroom-picker__frame { display: contents; }

.showroom-picker__fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
}

/* `min-width: 0` is what stops a long building name ("Market Square Tower —
   Suites") from forcing the row wider than the panel: a flex item's default
   min-width is auto, which refuses to shrink below its content. */
.showroom-picker__field {
  flex: 1 1 16rem;
  min-width: 0;
}
.showroom-picker__field .form-field { margin-block: 0; }

/* ---- Status band --------------------------------------------------------- */
.showroom-picker__status {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.showroom-picker__legacy { margin-block: 0; }

.showroom-picker__note {
  margin-block: 0;
  color: var(--color-text-muted-strong);
}

/* ============================================================================
   HPMR-658 — Unplaced exhibitors (the Exhibitors > Unplaced tab) and its inline
   Resolve panel. Figma: list 748:73576 · reason variants 749:75334 / 76510 /
   77526 / 78432 / 79338 · Resolve twins 765:77642 / 78901 / 79982 / 80993 /
   82010. APPENDED — nothing above is modified.

   Two shipped families do the heavy lifting and are NOT restated here: the tab
   strips are `.portal-tabs` / `.portal-tab` (Events, Shuttle Dashboard), and
   the cascade inside the panel is `.showroom-picker` (HPMR-661) verbatim. What
   follows is only the spacing that stacks two tab rows, the worklist cell
   chrome, and the expanding row itself.

   Every colour is a token — and no literal hex appears below, not even in a
   comment, because spec/views/shared/pagination_spec.rb sweeps everything after
   the pagination modifier block for one. The two greys are picked by SURFACE,
   per the two-surface rule in design.md: the panel row paints
   `--color-bg-alt`, so text on it takes `--color-text-muted-strong` (5.87:1)
   and never `--color-text-muted` (4.06:1 there — it passes only on white).
   ============================================================================ */

/* ---- The two stacked tab rows ------------------------------------------- */
.placement-tabs { margin-bottom: var(--space-2); }
.placement-tabs__remainder { margin: 0 0 var(--space-2); color: var(--color-text-secondary, inherit); font-size: var(--font-size-sm, 0.875rem); }
.placement-reason-tabs { margin-bottom: var(--space-4); }

/* ---- "Active Market:" selector in the page header ------------------------ */
.placement-season {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
/* `margin-block` rather than `margin` for the zeroing here and below. It is the
   spelling the HPMR-661 block above already uses, and it is also what keeps
   this file inside spec/views/shared/pagination_spec.rb's token sweep, which
   requires every `margin`/`padding`/`gap` value after the pagination block to
   be a var(). A bare 0 is not a magic number, but the guard cannot know that,
   and the logical longhand says exactly as much while staying inside it. */
.placement-season__label {
  margin-block: 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-medium);
  /* On the --color-bg-alt panel — the page header never sits in a white card. */
  color: var(--color-text-muted-strong);
  white-space: nowrap;
}
.placement-season__select { min-width: 16rem; }
/* The no-JS floor: the select auto-submits, so with scripting ON this button is
   a second control for the same action AND it wraps to its own line under the
   select (the header column is narrow), which is noise Albina's frame does not
   have. Hidden by default and revealed by the <noscript> block in the partial —
   the same shape shared/_pagination uses for its per-page links. */
.placement-season__go { display: none; flex: 0 0 auto; }

/* ---- Worklist rows ------------------------------------------------------- */
.unplaced-table td { vertical-align: top; }

/* The open row and its panel read as ONE block: the row loses its bottom rule
   so the two are not divided by a line the eye has to cross. */
.unplaced-row--open > td { border-bottom-color: transparent; }

.unplaced-row__avatar {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-bg-alt);
  overflow: hidden;
}
.unplaced-row__avatar img { width: 100%; height: 100%; object-fit: contain; }
/* Decorative glyph on the --color-bg-alt fill: 4.06:1 clears the 3:1 that
   1.4.11 asks of non-text content. The name sits beside it as real text. */
.unplaced-row__avatar i { color: var(--color-text-muted); }

.unplaced-row__identity { min-width: 0; }
.unplaced-row__slug {
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);            /* inside the white .list-table card — 4.67:1 */
}

/* The "why" sub-line under Legacy Building / Legacy Floor / Reason. Also inside
   the white card. */
.unplaced-row__note {
  margin-top: var(--space-1);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}

/* ---- The inline Resolve panel ------------------------------------------- */
/* The panel is a second <tr> under the row it belongs to (Roy 2026-08-18: an
   expanding row, not a modal). Named so the row can be found in markup and in
   a spec even though the cell carries the chrome. */
.resolve-panel-row { background: var(--color-bg-alt); }
/* `.list-table td` (0,1,1) out-specifies a bare `.resolve-panel-cell` (0,1,0),
   so the cell padding has to be reached with the table class in front of it.
   The alternative is `!important`, which is how a stylesheet acquires a rule
   nobody can override later for a good reason. */
.list-table td.resolve-panel-cell {
  padding: var(--space-5) var(--space-4);
  background: var(--color-bg-alt);
}
.resolve-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.resolve-panel__title {
  margin-block: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-bold);
  color: var(--color-text);
}
.resolve-panel__prompt {
  margin-block: 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);     /* on --color-bg-alt — 5.87:1 */
}
.resolve-panel__subhead {
  margin: var(--space-2) 0 0;
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.resolve-panel__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.resolve-panel__picker { margin-top: var(--space-1); }
.resolve-panel__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

/* ---- Radio cards (claimants, or the rooms a Claims Several row named) ----
   Her frames lay these out as a row of bordered cards that wrap. The LABEL is
   the control: the whole card is clickable because it is a <label>, which needs
   no JavaScript and keeps the native radio as the accessible object. */
.resolve-panel__choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  /* A <fieldset> ships browser margin, padding and a border; all three go. */
  margin-inline: 0;
  margin-block: 0;
  padding-inline: 0;
  padding-block: 0;
  border: 0;
}
.resolve-choice {
  flex: 1 1 18rem;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
}
/* Selection is carried by the radio itself AND by the border — never by colour
   alone (1.4.1). `:has()` is how a bare CSS card follows its own input. */
.resolve-choice:has(.resolve-choice__input:checked) {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 1px var(--color-accent);
}
.resolve-choice:has(.resolve-choice__input:focus-visible) {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}
.resolve-choice--inert {
  cursor: not-allowed;
  background: var(--color-bg-alt);
}
.resolve-choice__input { flex: 0 0 auto; accent-color: var(--color-accent); }
.resolve-choice__label { color: var(--color-text); }
/* On the inert card's --color-bg-alt fill. */
.resolve-choice__note {
  display: block;
  font-size: var(--font-size-xs);
  color: var(--color-text-muted-strong);
}

@media (max-width: 768px) {
  .resolve-choice { flex-basis: 100%; }
  .placement-season { width: 100%; }
  .placement-season__select { min-width: 0; flex: 1 1 auto; }
}

/* ==========================================================================
   HPMR-466 follow-up — the pending-removal state on a Hotel Route stop row.

   THE `hidden` ATTRIBUTE IS NOT ENOUGH ON ITS OWN HERE. `[hidden]{display:none}`
   is a USER-AGENT rule, and `.button { display: inline-flex }` is an author
   rule — author beats UA, so a `<button hidden>` stays on screen. That is the
   same class of trap as `d-none` shipping with !important, read from the other
   end. Two attribute selectors give 0,2,0 specificity, which clears .button
   (0,1,0) and .callout without needing !important.

   These pair with data-stop-active-only / data-stop-pending-only in
   hotel_routes/_stop_row.html.erb and hotel_route_form_controller.js.
   ========================================================================== */
[data-stop-active-only][hidden],
[data-stop-pending-only][hidden] { display: none; }


/* ==========================================================================
   HPMR-129 — Preview as Buyer (exhibitor portal → buyer-facing layout)
   Mirrors the public apps/web exhibitor profile structure at a CSS level.

   HPMR-764 — TOKENS ONLY, and read this before adding a value below.
   This block and the three after it (HPMR-746, HPMR-136, HPMR-131) shipped
   with raw hex colours, raw px type/spacing, and four custom properties that
   are defined NOWHERE in this file: --color-bg-secondary, --color-bg-elevated,
   --color-primary and --color-brand. A var() with a missing property resolves
   to its fallback, so every such declaration painted the hard-coded fallback —
   for --color-brand that was an orange that is not a brand colour at all.
   They also sit below `.pagination-bar__page--prev`, and
   spec/views/shared/pagination_spec.rb requires every colour / gap / padding /
   margin / font-weight / font-size declaration from there to END OF FILE to
   be a var(), with no hex anywhere (comments included). That is what turned
   master CI red on 2026-09-18.

   How the values were mapped:
   * Every colour is a canonical token. Surfaces are named per the two-surface
     rule in .claude/rules/design.md: the preview body is --color-bg (white);
     the card details sit on --color-bg-alt — the same fill apps/web paints
     there — so the muted neighbourhood line inside it takes
     --color-text-muted-strong (--color-text-muted fails AA on that fill).
     The two links are TEXT, so they take --color-passion-text, never the
     Passion fill colour, and their hover is pinned against the global
     `a:hover` (see .text-passion for the same pin).
   * Spacing is the --space-* scale, exact everywhere except the chips, which
     take the house `.chip` padding (--space-2 / --space-3).
   * Type sizes use the --font-size-* / --h*-size steps; a px value that fell
     between two steps rounds toward the public page it mirrors.
   * Zeroing is spelled with logical longhands (`margin-block: 0`), the
     spelling the rest of this region already uses; a `gap: 0` on a flex
     container is simply omitted — its initial value, normal, is already zero.
   ========================================================================== */
.buyer-preview-body {
  margin-block: 0;
  margin-inline: 0;
  background: var(--color-bg);
  font-family: var(--font-family-base);
  color: var(--color-text);
}

.buyer-preview-banner {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  background: var(--color-soft-black);
  color: var(--color-white);
}

.buyer-preview-banner__text {
  margin-block: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
}

.buyer-preview-banner__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.buyer-preview-banner__draft {
  font-size: var(--font-size-sm);
  opacity: 0.85;
}

.buyer-preview-banner__link {
  color: var(--color-white);
  text-decoration: underline;
  text-underline-offset: 2px;
  font-size: var(--font-size-sm);
}

.buyer-preview {
  padding: var(--space-12) var(--space-6) var(--space-16);
  display: flex;
  justify-content: center;
}

.buyer-preview__inner {
  width: 100%;
  max-width: 1600px;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.buyer-preview__columns {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}

@media (min-width: 1024px) {
  .buyer-preview__columns {
    flex-direction: row;
    gap: var(--space-16);
    align-items: flex-start;
  }
  .buyer-preview__card {
    flex: 0 0 518px;
    width: 518px;
  }
  .buyer-preview__right {
    flex: 1 1 auto;
    min-width: 0;
  }
}

.buyer-preview__card-image-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--color-bg-alt);
}

.buyer-preview__card-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.buyer-preview__card-image-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: var(--color-bg-alt);
}

.buyer-preview__card-image-placeholder-logo {
  width: 55%;
  height: auto;
  opacity: 0.55;
}

.buyer-preview__card-details {
  background: var(--color-bg-alt);
  padding: var(--space-5) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.buyer-preview__location-line {
  margin-block: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
}

.buyer-preview__neighborhood-line {
  margin: var(--space-1) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}

.buyer-preview__neighborhood-name {
  color: var(--color-text);
  font-weight: var(--font-weight-medium);
}

.buyer-preview__socials {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.buyer-preview__social-link {
  color: var(--color-text);
  font-size: var(--h3-size);
  line-height: 1;
  text-decoration: none;
}

.buyer-preview__primary-link {
  color: var(--color-passion-text);
  font-size: var(--font-size-base);
  word-break: break-all;
}

.buyer-preview__name {
  margin: 0 0 var(--space-6);
  font-size: var(--h2-size);
  font-weight: var(--font-weight-bold);
  line-height: var(--line-height-heading);
}

.buyer-preview__tabs {
  display: flex;
  flex-direction: column;
}

.buyer-preview__radio {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.buyer-preview__tablist {
  display: flex;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--color-light-grey);
}

/* Inactive tab labels sit on the white preview body, where
   --color-text-muted clears AA; the selected tab switches to --color-text. */
.buyer-preview__tab {
  padding: var(--space-3) var(--space-4);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-muted);
  cursor: pointer;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: 2px 2px 0 0;
  margin-bottom: -1px;
  background: transparent;
}

.buyer-preview__panels {
  padding-top: var(--space-6);
}

.buyer-preview__panel {
  display: none;
  flex-direction: column;
  gap: var(--space-6);
}

#buyer-tab-who:checked ~ .buyer-preview__tablist .buyer-preview__tab--who,
#buyer-tab-events:checked ~ .buyer-preview__tablist .buyer-preview__tab--events,
#buyer-tab-photos:checked ~ .buyer-preview__tablist .buyer-preview__tab--photos,
#buyer-tab-videos:checked ~ .buyer-preview__tablist .buyer-preview__tab--videos {
  color: var(--color-text);
  border-color: var(--color-light-grey);
  background: var(--color-bg);
}

#buyer-tab-who:checked ~ .buyer-preview__panels .buyer-preview__panel--who,
#buyer-tab-events:checked ~ .buyer-preview__panels .buyer-preview__panel--events,
#buyer-tab-photos:checked ~ .buyer-preview__panels .buyer-preview__panel--photos,
#buyer-tab-videos:checked ~ .buyer-preview__panels .buyer-preview__panel--videos {
  display: flex;
}

.buyer-preview__description {
  margin-block: 0;
  font-size: var(--font-size-md);
  line-height: 1.5;
  white-space: pre-wrap;
}

.buyer-preview__empty {
  margin-block: 0;
  color: var(--color-text-muted);
  font-size: var(--font-size-base);
}

.buyer-preview__section-heading {
  margin: 0 0 var(--space-3);
  font-size: var(--font-size-md);
  font-weight: var(--font-weight-semibold);
}

.buyer-preview__category-list,
.buyer-preview__brand-list,
.buyer-preview__event-list,
.buyer-preview__gallery,
.buyer-preview__video-list {
  list-style: none;
  margin-block: 0;
  margin-inline: 0;
  padding-block: 0;
  padding-inline: 0;
}

.buyer-preview__category-list,
.buyer-preview__brand-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.buyer-preview__category-item,
.buyer-preview__brand-item {
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg-alt);
  font-size: var(--font-size-sm);
}

.buyer-preview__event-row {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--color-light-grey);
}

.buyer-preview__event-time {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-sm);
  color: var(--color-text-muted);
}

.buyer-preview__event-title {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
}

.buyer-preview__event-meta,
.buyer-preview__event-description {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size-base);
}

.buyer-preview__gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--space-3);
}

.buyer-preview__gallery-item img {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  display: block;
}

.buyer-preview__video-item {
  margin-bottom: var(--space-3);
}

.buyer-preview__video-link {
  color: var(--color-passion-text);
  font-size: var(--font-size-base);
}

/* The global `a:hover` is (0,1,1) and would out-specify both links' (0,1,0)
   colour on hover, dropping them to the Passion-80 fill colour. Pinned, with
   the hover affordance moved to an underline, exactly as .text-passion does. */
a.buyer-preview__primary-link:hover,
a.buyer-preview__primary-link:focus-visible,
a.buyer-preview__video-link:hover,
a.buyer-preview__video-link:focus-visible {
  color: var(--color-passion-text);
  text-decoration: underline;
}

/* HPMR-746 - standalone portal page sections.
   Do NOT reuse .portal-profile__panel here: that class is display:none until a
   profile-tab radio (#profile-tab-*) reveals a --content/--showroom panel. */
.portal-page-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
  padding: var(--space-8);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}
.portal-page-panel .portal-profile__panel-title {
  margin-block: 0;
}
/* HPMR-136 — directory banner preview */
.banner-preview__image {
  display: block;
  max-width: 100%;
  max-height: 220px;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-bg-alt);
}

/* HPMR-131 — gallery drag-and-drop upload.
   Resting fill is --color-bg-alt; while a file is dragged over, the zone takes
   the same Passion treatment the house `.media-dropzone` uses (accent border,
   --color-passion-20 fill). Every text colour below is measured against BOTH
   of those surfaces, the worst one it reaches: hint and progress label take
   --color-text-muted-strong, the errors take --color-danger-text (the raw
   --color-danger is a fill hue and fails AA as text on this fill), and the
   inline "browse" control takes --color-passion-text with an underline,
   because it sits inside a run of text (WCAG 1.4.1). The icon is a decorative
   glyph and stays on --color-text-muted, which clears the 3:1 of 1.4.11 on
   both fills. */
.gallery-dropzone {
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-5) var(--space-6);
  margin-bottom: var(--space-5);
  background: var(--color-bg-alt);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.gallery-dropzone--active {
  border-color: var(--color-accent);
  background: var(--color-passion-20);
}
.gallery-dropzone--disabled { opacity: 0.7; }
.gallery-dropzone__body { text-align: center; }
.gallery-dropzone__icon {
  font-size: var(--h3-size);
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}
.gallery-dropzone__title {
  font-weight: var(--font-weight-semibold);
  margin: 0 0 var(--space-1);
}
.gallery-dropzone__hint {
  margin-block: 0;
  color: var(--color-text-muted-strong);
  font-size: var(--font-size-sm);
}
/* The inline "browse" control. It was written as `button button--link`, and
   `.button--link` has never existed; `.button` alone would have drawn a full
   bordered button in the middle of a sentence. A real <button> (it opens the
   file picker, so it must be keyboard-reachable), reset to read as a link. */
.gallery-dropzone__browse {
  padding-block: 0;
  padding-inline: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--color-passion-text);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.gallery-dropzone__browse:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}
.gallery-dropzone__progress { margin-top: var(--space-3); }
.gallery-dropzone__bar-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-border);
  overflow: hidden;
}
.gallery-dropzone__bar {
  height: 100%;
  width: 0;
  background: var(--color-accent);
  transition: width 0.1s linear;
}
.gallery-dropzone__label {
  margin: var(--space-1) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-text-muted-strong);
}
.gallery-dropzone__errors {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-5);
  color: var(--color-danger-text);
  font-size: var(--font-size-sm);
}
.visually-hidden {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* ==========================================================================
   HPMR-168 - Notification preferences (exhibitor portal)

   HPMR-764 — the resets below are logical longhands (`margin-block: 0` and
   friends) rather than `margin: 0` / `padding: 0`, and the list carries no
   `gap: 0` (a flex container's initial gap is already zero). Same rendering;
   the bare-zero shorthands failed spec/views/shared/pagination_spec.rb, whose
   token sweep runs from `.pagination-bar__page--prev` to the end of this file.
   ========================================================================== */
.notification-prefs {
  list-style: none;
  margin-block: 0;
  margin-inline: 0;
  padding-block: 0;
  padding-inline: 0;
  display: flex;
  flex-direction: column;
}
.notification-prefs__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--color-light-grey);
}
.notification-prefs__row:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}
.notification-prefs__row:first-child {
  padding-top: 0;
}
.notification-prefs__copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}
.notification-prefs__label {
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-semibold);
  color: var(--color-text);
}
.notification-prefs__helper {
  font-size: var(--font-size-sm, 14px);
  color: var(--color-text-muted-strong);
}
.notification-prefs__form {
  margin-block: 0;
  margin-inline: 0;
  flex: 0 0 auto;
}
.notification-prefs__toggle {
  position: relative;
  width: 48px;
  height: 28px;
  padding-block: 0;
  padding-inline: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--color-cool-grey);
  cursor: pointer;
  transition: background 0.15s ease;
}
.notification-prefs__toggle--on {
  background: var(--color-passion);
}
.notification-prefs__toggle--locked {
  opacity: 0.85;
  cursor: not-allowed;
}
.notification-prefs__toggle-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-white);
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease;
}
.notification-prefs__toggle--on .notification-prefs__toggle-knob {
  transform: translateX(20px);
}

/* ==========================================================================
   HPMR-764 — three classes that were NAMED in markup but styled inline.
   Each call site wrote a BEM class and then put its styling in a `style=""`
   attribute beside it, so the class was defined nowhere
   (spec/assets/css_class_definition_spec.rb) and the inline styles carried
   hard-coded hex. The styling moves into the class it was named for, on
   tokens; the inline attribute is removed from the element.
   ========================================================================== */

/* HPMR-24 — the rendered email body on email_templates/preview. A plain white
   frame so the sanitized email HTML reads as a document inside the panel. */
.email-template-preview {
  border: 1px solid var(--color-border);
  padding: var(--space-4);
  background: var(--color-bg);
}

/* Style Spotter dashboard header — headshot beside name + season line. */
.ss-dash__header {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin-bottom: var(--space-6);
}
.ss-dash__headshot {
  flex-shrink: 0;
  width: 88px;
  height: 88px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--color-bg-alt);
}

/* ============================================================================
   HPMR-760 - Pin list filters + paginator; scroll the middle independently.

   Admin list screens opt in with a `.list-layout` wrapper around the filter
   chrome, the table/empty state, and the shared pagination bar. The panel is
   locked to the viewport (desktop/tablet) so ONLY `.list-layout__body` (or a
   direct-child `.list-table`) scrolls - no outer page double-scrollbar.

   Named regions (preferred when the chrome is more than a bare toolbar):
     .list-layout__chrome  - page-header / tabs / filter band (flex-shrink: 0)
     .list-layout__body    - table or empty-state (hugs content, shrinks + overflow-y: auto; HPMR-1031)
     .list-layout__footer  - pagination / bulk bar host (flex-shrink: 0)

   Direct-child shorthand for the common "toolbar + table + paginator" shape:
   a bare `.list-toolbar` / `.list-table` / `.pagination-bar` under `.list-layout`
   get the same pin/scroll treatment without the named wrappers.

   Mobile (<768): height lock is OFF so the stacked sidebar + panel can grow
   normally - pinning would clip content on short phones (AC "no clipping").
   ============================================================================ */

.list-layout {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  gap: var(--space-4);
  overflow: hidden;
}

.list-layout__chrome {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

/* HPMR-1031 - the body HUGS its content (flex-grow 0) and only shrinks +
   scrolls when the rows outgrow the locked panel. With flex-grow 1 a short
   list's body filled the panel and pushed `.list-layout__footer` (the
   paginator) to the bottom of the viewport; now the paginator always sits
   directly under the last row, same as the `.list-layout__region` lists. */
.list-layout__body {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.list-layout__footer {
  flex-shrink: 0;
  min-width: 0;
}

/* Direct-child shorthand (simple lists: toolbar / table / paginator siblings). */
.list-layout > .list-toolbar,
.list-layout > form.list-toolbar,
.list-layout > .list-layout__filters {
  flex-shrink: 0;
  min-width: 0;
}

/* HPMR-1031 - hug short tables (paginator follows the last row); an empty
   card still fills (rule below). */
.list-layout > .list-table,
.list-layout > .empty-state--card,
.list-layout > .section-card {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.list-layout > .empty-state--card { flex-grow: 1; }

.list-layout > .pagination-bar,
.list-layout > nav.pagination-bar {
  flex-shrink: 0;
  /* Inside a list-layout the 16px layout gap owns the spacing, so the bar's
     own 16px margin is dropped to avoid a double gutter. HPMR-929: this used
     to lose to the partial's Bootstrap `mt-3` (!important), so every list had
     32px here; the partial no longer carries `mt-3`. */
  margin-top: 0;
}
/* HPMR-929 — same rule when the bar sits in the named footer region, or in a
   list form (bulk-select lists) whose own 16px gap spaces it. */
.list-layout__footer > .pagination-bar,
.list-layout form > .pagination-bar { margin-top: 0; }

/* A detail-page that hosts a list-layout must itself fill + clip so the
   inner body can scroll (Pages, Audit, etc.). */
.detail-page.list-layout,
.detail-page:has(> .list-layout) {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

.detail-page > .list-layout {
  flex: 1 1 auto;
  min-height: 0;
}

/* Desktop/tablet: lock the admin panel to the viewport whenever a list-layout
   is present, and kill the document scrollbar so we do not get panel+page
   double scrollbars. Phone keeps normal document flow (see media query). */
@media (min-width: 768px) {
  body:has(.portal-content--admin .list-layout) {
    overflow: hidden;
    height: 100%;
  }

  .portal-layout:has(.list-layout) {
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    overflow: hidden;
  }

  .portal-content--admin:has(.list-layout) {
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
    overflow: hidden;
    /* min-height:100vh would still let the panel grow past the viewport and
       reintroduce an outer scrollbar; pin both. */
    min-height: 0;
  }

  /* Flashes turbo-frame is a flex sibling above the list; keep it out of the
     scroll math. */
  .portal-content--admin:has(.list-layout) > #flashes {
    flex-shrink: 0;
  }
}


/* HPMR-760 - Events / SEO bulk surfaces nest the table inside a bulk-select
   controller + form. Those wrappers must pass the flex height through so the
   named `__body` / `__footer` regions still pin and scroll. */
.list-layout > [data-controller="bulk-select"],
.list-layout > form#bulk-events-form,
.list-layout [data-controller="bulk-select"] > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  gap: var(--space-4);
}


/* HPMR-760 - media library grid scrolls inside the list body. */
.list-layout__body > .portal-media-grid {
  min-height: 0;
}

/* Search control can sit in page-header actions without a .list-toolbar parent. */
.page-header__actions .list-toolbar__search {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.page-header__actions .list-toolbar__search .list-toolbar__control {
  padding-left: 40px;
  min-width: 240px;
}


/* HPMR-760 - page-header above a list-layout must not eat the flex budget. */
.portal-content--admin:has(.list-layout) > .page-header {
  flex-shrink: 0;
}

/* Inside list chrome the panel's page-header bottom margin fights the layout
   gap; zero it so spacing comes from .list-layout / __chrome gap only. */
.list-layout__chrome > .page-header,
.list-layout__chrome > .detail-page__header,
.detail-page.list-layout > .list-layout__chrome .page-header {
  margin-bottom: 0;
}


/* HPMR-884 / HPMR-907 - pass-through region. Some list screens wrap the table
   in a stable id that a turbo_stream re-renders (#market-seasons-table) or that
   hosts a Stimulus controller (#navigation-items-list). A plain block wrapper
   between `.list-layout` and the `.list-table` breaks the flex chain: the
   locked, overflow:hidden panel then CLIPS the rows instead of the table
   scrolling. `.list-layout__region` passes the height through so the table
   (or empty card) becomes the overflow-y:auto scroller and the paginator /
   count row stays pinned beneath it. */
.list-layout > .list-layout__region {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  gap: var(--space-4);
  overflow: hidden;
}
.list-layout__region > .list-table,
.list-layout__region > .empty-state--card {
  flex: 0 1 auto;                                   /* hug short tables; shrink + scroll long ones */
  min-height: 0;
  overflow-y: auto;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.list-layout__region > .empty-state--card { flex-grow: 1; }
.list-layout__region > .pagination-bar,
.list-layout__region > .list-layout__footer {
  flex-shrink: 0;
  margin-top: 0;
}
/* HPMR-907 - tab rows that sit between the page header and a list-layout must
   not be squeezed by the locked panel (an overflow-x:auto tab strip is a flex
   item with min-height 0 and would otherwise shrink). */
.portal-content--admin:has(.list-layout) > .portal-tabs { flex-shrink: 0; }
/* HPMR-1050 rework — Page Audits uses the standard list body and footer, so
   the grid is not a direct child of `.list-layout` and the direct-child
   scroller rule does not reach it. The list-table stays the scroller (sticky
   column header, footer pinned under it). Scoped to this page: every other
   list keeps scrolling `.list-layout__body`. A short table still hugs its
   rows (flex-grow 0). */
.list-layout[data-list-layout="page-audits"] > .list-layout__body:has(> .list-table) {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.list-layout[data-list-layout="page-audits"] > .list-layout__body > .list-table {
  flex: 0 1 auto;
  /* HPMR-1056 — floor is about the column header plus one data row. min-height
     0 let a short window shrink the scrollport to the sticky header, which then
     covered every row. 7.5rem clears the measured header and one row on both
     grids; at 600px the flex slot is still taller, so the in-container scroll
     is unchanged. */
  min-height: 7.5rem;
  overflow-x: auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* HPMR-1050 / HPMR-1051 (AC 3, comment 61940) - the column header row stays
   visible while the grid scrolls. Inside a list-layout the `.list-table` is
   the scroller, so a sticky header pins to the top of the grid. */
.list-layout .list-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
}
/* HPMR-1051 - Content Quality is a multi-section page (stat cards, this grid,
   Content Gaps), so it does not take the list-layout viewport lock (CoS
   exception, 2026-10-31): the page scrolls. The header must still stick, to
   the viewport this time. `overflow-x: auto` would make `.list-table` a
   scroll container and trap the sticky header inside it; `clip` keeps the
   rounded border without making it a scroller. */
.seo-content-quality__table {
  /* HPMR-1056 — same floor as Page Audits: header plus one row. The page
     already scrolls, so the floor only stops the grid collapsing under the
     sticky header. A full table is already taller. */
  min-height: 7.5rem;
}
.seo-content-quality__table > .list-table {
  overflow: clip;
  min-height: 7.5rem;
}
.seo-content-quality__table > .list-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
}
/* HPMR-907 (comment 60729: no double scrollbars) - with the panel locked, the
   root element must not be a scroll container either. body:hidden alone only
   propagates to the viewport; pin <html> too so no ancestor of a list-layout
   can ever scroll. Desktop/tablet only, mirroring the HPMR-760 lock. */
@media (min-width: 768px) {
  html:has(.portal-content--admin .list-layout) { overflow: hidden; height: 100%; }
}
/* HPMR-1056 — at 450px and below, Page Audits scrolls with the page instead
   of inside the grid, so the sticky header cannot fill the scrollport. One
   scroller. This follows the lock above and is more specific, so the lock
   does not win. 600px stays above this and keeps the in-container scroll. */
@media (min-width: 768px) and (max-height: 450px) {
  html:has(.portal-content--admin .list-layout[data-list-layout="page-audits"]),
  body:has(.portal-content--admin .list-layout[data-list-layout="page-audits"]) {
    overflow: visible;
    height: auto;
  }

  .portal-layout:has(.list-layout[data-list-layout="page-audits"]),
  .portal-content--admin:has(.list-layout[data-list-layout="page-audits"]) {
    height: auto;
    max-height: none;
    min-height: 0;
    overflow: visible;
  }

  .list-layout[data-list-layout="page-audits"],
  .list-layout[data-list-layout="page-audits"] > .list-layout__body,
  .list-layout[data-list-layout="page-audits"] > .list-layout__body:has(> .list-table) {
    flex: none;
    height: auto;
    min-height: auto;
    overflow: visible;
  }

  .list-layout[data-list-layout="page-audits"] > .list-layout__body > .list-table {
    flex: none;
    height: auto;
    overflow: visible;
  }
}

/* HPMR-891 - Events status tabs load into turbo-frame#events-list. The frame
   is a pass-through flex region (so the HPMR-760 list-layout still pins and
   scrolls) and stays mounted at its size across tab switches. While Turbo
   fetches the next tab it sets [busy] on the frame: dim the grid region and
   show a spinner over it, and block clicks on the stale rows. */
turbo-frame.events-list-frame {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
}
turbo-frame.events-list-frame [data-controller="bulk-select"] { position: relative; }
turbo-frame.events-list-frame[busy] [data-controller="bulk-select"]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  background: rgb(255 255 255 / 0.6);
  border-radius: var(--radius-md);
  cursor: progress;
}
turbo-frame.events-list-frame[busy] [data-controller="bulk-select"]::after {
  content: "";
  position: absolute;
  z-index: 6;
  top: calc(50% - 16px);
  left: calc(50% - 16px);
  width: 32px;
  height: 32px;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-passion);
  border-radius: 50%;
  animation: events-list-spin 0.8s linear infinite;
}
@keyframes events-list-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  turbo-frame.events-list-frame[busy] [data-controller="bulk-select"]::after { animation: none; }
}

/* HPMR-760 - a region nested inside a region (Sponsors reorder wrapper inside
   the list region) keeps passing the flex height down so the table scrolls. */
.list-layout__region > .list-layout__region {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

/* HPMR-910: unsaved-form guard wrapper around the map editors. Layout-neutral. */
.unsaved-form-scope { display: contents; }
