/*
 * harbour dashboard — components
 *
 * Handcrafted layout + components, consuming only :root variables
 * declared in tokens.css. Per-tenant skins override the variables;
 * the structure stays identical.
 */

/* ---- reset + base ---- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Reserve the vertical-scrollbar gutter at all times (decided
     2026-05-28). Without this the viewport widens on short pages
     (no scrollbar) and narrows on long pages (scrollbar appears),
     shifting every horizontally-positioned element — the filter
     toolbar's reserved Clear-all slot, the rowlist columns, etc.
     — by the scrollbar's width. scrollbar-gutter: stable tells the
     browser to keep the lane reserved either way, so the content
     width is constant across pages. Working with the browser's
     native scroll mechanism (keyboard, momentum, accessibility)
     rather than introducing an inner-scroll container that would
     re-implement those behaviours. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  font-family: var(--brand-body-font);
  font-size: var(--size-base);
  line-height: var(--leading-base);
  color: var(--colour-ink);
  font-feature-settings: "ss01", "ss02", "tnum";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100vh;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--brand-display-font);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  margin: 0;
}

p {
  margin: 0;
}

a {
  color: var(--accent-strong);
  text-decoration: none;
  transition: color var(--duration-fast) var(--ease-out);
}

a:hover {
  color: var(--accent);
}

button {
  font-family: inherit;
  font-size: inherit;
}

/* ---- layout shell ---- */

.app {
  display: grid;
  grid-template-columns: var(--rail-width) 1fr;
  min-height: 100vh;
}

.rail {
  background: var(--colour-surface-soft);
  border-right: 1px solid var(--colour-line);
  padding: var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  position: sticky;
  top: 0;
  height: 100vh;
}

.rail__brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-2);
  min-height: 40px;
}

/* The logo slot. Renders an <img> ONLY when --logo-url is set;
   absent that, the slot is collapsed to zero width. Clean empty
   space, never a placeholder. Achieved via a `[data-logo]` data
   attribute on the slot element.

   Sizing (decided 2026-05-24): 80px wide so wordmark-style logos
   are readable in the rail; height is auto so the source aspect
   ratio drives vertical extent, capped at max-height: 56px so a
   tall logo can't push the rail header out of proportion.
   object-fit: contain handles letterboxing if the source's aspect
   doesn't match the bounding box. */
.rail__logo {
  display: none;
  width: 80px;
  height: auto;
  max-height: 56px;
  object-fit: contain;
  border-radius: var(--radius-sm);
}

.rail__logo[data-logo="present"] {
  display: inline-block;
}

.rail__brand-name {
  font-family: var(--brand-display-font);
  font-weight: var(--weight-semibold);
  font-size: var(--size-md);
  letter-spacing: var(--tracking-tight);
  color: var(--colour-ink-strong);
}

.rail__nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.rail__nav-group-label {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  padding: var(--space-3) var(--space-3) var(--space-2);
}

.rail__nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--colour-ink-soft);
  font-weight: var(--weight-medium);
  font-size: var(--size-sm);
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.rail__nav-item:hover {
  background: var(--accent-tint);
  color: var(--accent-strong);
}

.rail__nav-item--active {
  background: var(--accent-tint);
  color: var(--accent-strong);
  font-weight: var(--weight-semibold);
}

.rail__nav-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  opacity: 0.85;
}

.rail__footer {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--colour-line);
}

.rail__user {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
}

.rail__user-initials {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-ink);
  display: grid;
  place-items: center;
  font-size: var(--size-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
}

.rail__user-email {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Rail footer links — Settings and Sign out share the same chrome
   per S5b (decided 2026-05-20). Font bumped from --size-xs to
   --size-sm so the footer items read as primary nav rather than
   buried micro-text. */
.rail__footer-link {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  align-self: flex-start;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.rail__footer-link:hover {
  color: var(--accent-strong);
  background: var(--accent-tint);
}

/* Legacy class kept for any external references; behaves identically
   to .rail__footer-link. Safe to remove once no template references it. */
.rail__logout {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  align-self: flex-start;
}

.rail__logout:hover {
  color: var(--accent-strong);
  background: var(--accent-tint);
}

/* ---- main content area ---- */

.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.header {
  height: var(--header-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-6);
  border-bottom: 1px solid var(--colour-line);
  background: var(--colour-bg);
  position: sticky;
  top: 0;
  /* z-index bumped from 10 → 100 on 2026-05-23 so the export popover
     (inside .header) wins against in-page floating UI. The previous
     value lost to .journey-filters (z-index: 20, added during the
     date-picker rollout so the picker's own popover would beat
     summary-card stacking contexts). The popover's own z-index: 200
     is meaningless if its sticky ancestor stacking context sits
     below page content — header has to be the highest baseline.
     The header is sticky and visually the always-on-top chrome of
     the page, so 100 is the architecturally honest level. */
  z-index: 100;
}

.header__title {
  font-size: var(--size-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--colour-ink-strong);
  line-height: var(--leading-snug);
}

/* Greeting subtitle (decided 2026-05-24, dashboard greeting brief).
   Renders below the greeting H1 on /enquiries; preserves
   "Enquiries" as the page label so the working surface still
   names itself even when the H1 carries a time-of-day greeting.
   Quiet weight: lower font size, regular weight, soft ink, no
   tracking. The eye reads greeting first, page label second. */
.header__subtitle {
  display: block;
  font-size: var(--size-sm);
  font-weight: var(--weight-regular);
  color: var(--colour-ink-soft);
  letter-spacing: 0;
  margin-top: 2px;
}

.header__meta {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  color: var(--colour-ink-soft);
  font-size: var(--size-sm);
}

.content {
  padding: var(--space-6);
  max-width: var(--content-max-width);
  width: 100%;
  margin: 0 auto;
}

/* ---- summary card row ---- */

/* Four-slot grid, held even when only two slots are populated.
   Per S2 (decided 2026-05-19): the row is the intended visual
   structure; freed slots remain present-but-unallocated until a
   genuine function earns them. The two visible cards land in
   slots 1 + 2; slots 3 + 4 stay empty grid cells (reserved
   whitespace, not collapsed). */
/* Three-card summary row (decided 2026-05-22): Quotes sent, Win
   ratio, Category mix — each takes one equal third. The earlier 4-
   column layout with empty slots 3+4 (then Mix spanning 2) made
   sense when Mix was the only card with chart-shaped content; now
   that Quotes sent and Win ratio also carry trend charts beneath
   their headlines, every card earns the same width and the row
   reads as one coherent surface. */
.summary-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

/* Every summary card uses an identical content-anchoring pattern
   (decided 2026-05-19): top row with label + period toggle, value
   below, delta below. All elements anchored top-left; no card
   centres or floats its content. */
.summary-card {
  background: var(--colour-surface);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow-xs);
  border: 1px solid var(--colour-line);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: var(--space-2);
  transition: transform var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out);
}

.summary-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}

.summary-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.summary-card__label {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

.summary-card__value {
  font-family: var(--brand-display-font);
  font-size: var(--size-2xl);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
  text-align: left;
}

.summary-card__delta {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  text-align: left;
}

/* Per-card period visibility retired 2026-05-21 — the page-level
   Journey-style filter card on /enquiries is now the single source
   of truth for the active window, and each summary card renders one
   value (no more pre-computed per-period markup, no more data-period
   CSS swap). */

/* ---- Trend charts in Quotes-sent + Win-ratio cards (decided
   2026-05-22) ----
   Both cards carry the same body shape: label, big headline number,
   small delta line, then a compact chart canvas underneath. The
   chart is supporting context for the headline — the eye lands on
   the number first, the chart shape is read second.

   Three different chart silhouettes across the row (line, bars,
   donut) — each card has its own visual signature so the eye learns
   the row in one pass. Same --accent vocabulary throughout; only
   the shape varies. */
.summary-card--trend {
  min-height: 220px; /* match the Mix card's min-height for row rhythm */
}

.trend-chart {
  margin-top: auto; /* push the chart to the bottom of the card */
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.trend-chart__svg {
  width: 100%;
  height: 64px;
  display: block;
  overflow: visible;
}

.trend-chart__area {
  fill: var(--accent);
  fill-opacity: 0.12;
  stroke: none;
}

.trend-chart__line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Plot wrapper: positioning context for the HTML dot overlays. The
   SVG inside stretches to fill via preserveAspectRatio="none" so the
   line+area cover the card width; the dots sit ON TOP of the SVG as
   absolutely-positioned HTML spans so they stay perfectly circular
   regardless of the non-uniform SVG scaling. */
.trend-chart__plot {
  position: relative;
  width: 100%;
  height: 64px;
}

/* Line-chart dots: HTML spans, not SVG circles. Width/height in CSS
   pixels so the shape never stretches with the chart's container
   width. The translate(-50%, -50%) re-centres each dot on its
   left/top anchor (the point's actual chart position). */
.trend-chart__dot {
  position: absolute;
  width: 6px;
  height: 6px;
  background: var(--accent);
  border-radius: 50%;
  opacity: 0.7;
  transform: translate(-50%, -50%);
  transition: width var(--duration-fast) var(--ease-out),
              height var(--duration-fast) var(--ease-out),
              opacity var(--duration-fast) var(--ease-out);
  pointer-events: auto;
}

.trend-chart__dot:hover {
  opacity: 1;
  width: 10px;
  height: 10px;
}

/* Bar chart bars: same accent saturation across every bar — no
   shade ladder, by Marcus' explicit pick. Hover brightens via a
   subtle filter bump matching the donut hover treatment. */
.trend-chart__bar {
  fill: var(--accent);
  transition: filter var(--duration-fast) var(--ease-out);
}

.trend-chart__bar:hover {
  filter: brightness(1.08);
}

.trend-chart__bar--empty {
  /* Zero-height baseline mark for buckets with no won + no lost.
     Renders as nothing visible (zero height) but keeps the SVG node
     for tooltip discovery. */
  fill: transparent;
}

.trend-chart__axis {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
  font-variant-numeric: tabular-nums;
}

.trend-chart__axis-label {
  flex: 1 1 0;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Bar-chart axis labels align to each bar's centre. The flex layout
   above already gives every label an equal share of the row width;
   that share lines up with the bar's x position when the chart's
   left/right margins match the axis row's natural padding (which is
   zero). */

.trend-chart__empty {
  margin: 0;
  padding: var(--space-3) 0;
  color: var(--colour-ink-muted);
  font-size: var(--size-sm);
  font-style: italic;
  text-align: center;
}

/* ---- Category-mix donut card (decided 2026-05-21, rebuilt
   2026-05-22 — visual rebuild) ----
   Third card on the /enquiries summary row, to the right of Quotes
   sent + Win ratio. Spans two grid columns; the original layout
   reserved slots 3+4 as empty whitespace, so this lands in that
   reserved space rather than displacing existing cards.

   Two-column card body: donut left ~2/3, legend right ~1/3. The
   donut is a fixed 180px target with a 36px ring thickness; the
   centre total lives inside the hole. The legend stacks one row per
   slice (swatch + category + raw number), aligned in a tidy column.

   Hover sync donut ↔ legend uses CSS :has() — hovering a slice
   highlights its matching legend row, hovering a legend row brightens
   its matching slice. No JS. Both directions key on matching
   data-category attributes.

   Per-slice colour is a shade of --accent (CSS color-mix expression
   emitted server-side per slice position). Tenant override of
   --accent re-shades the whole card; no per-category hue tokens. */
.summary-card--mix {
  min-height: 220px;
}

.summary-card__toggle {
  display: inline-flex;
  background: var(--colour-bg);
  border-radius: var(--radius-pill);
  padding: 2px;
  border: 1px solid var(--colour-line);
}

.summary-card__toggle-opt {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  padding: 3px var(--space-3);
  min-width: 52px;
  text-align: center;
  border-radius: var(--radius-pill);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.summary-card__toggle-opt:hover {
  color: var(--colour-ink);
}

/* Suppress the browser-default outline on mouse-click but
   restore an accent-coloured outline for keyboard focus. Same
   shape as the existing :focus-visible discipline on
   .enquiry__summary, .priority-star, and
   .surface__category-summary. Without this, clicking either
   toggle pill leaves a black rectangle on the summary card —
   noted on Quotes Sent, Win Ratio, and Category Mix because
   all three cards share this toggle class. */
.summary-card__toggle-opt:focus {
  outline: none;
}

.summary-card__toggle-opt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.summary-card__toggle-opt--active {
  background: var(--colour-surface);
  color: var(--accent-strong);
  box-shadow: var(--shadow-xs);
}

/* Two-column body: donut left, legend right. The grid template uses
   minmax so both columns can shrink under tight viewports but the
   donut never falls below 140px and the legend keeps enough room for
   the longest category name + number. */
.category-mix__body {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(140px, 2fr) minmax(120px, 1fr);
  gap: var(--space-4);
  align-items: center;
  min-height: 0;
}

.category-mix__donut-zone {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.category-mix__donut {
  width: 100%;
  /* Shrunk from 180px to 160px (decided 2026-05-22) to fit the 1/3-
     width Mix card without crowding the legend column. Donut diameter
     isn't load-bearing — 160px still reads proportion fine. */
  max-width: 160px;
  height: auto;
  aspect-ratio: 1 / 1;
  display: block;
}

.category-mix__ring {
  stroke: var(--mix-ring-track);
}

.category-mix__slice {
  transition: stroke-width var(--duration-fast) var(--ease-out),
              filter var(--duration-fast) var(--ease-out);
  cursor: pointer;
}

/* Suppress browser-default outline on mouse-click but restore
   accent ring for keyboard focus, same pattern as
   .summary-card__toggle-opt and the .enquiry__summary /
   .priority-star precedents. Without this, clicking a donut
   slice draws the default outline around the SVG anchor's
   bounding box — visible as a black rectangle around the donut
   area on the Category Mix card. Same rule on the legend row
   below since clicking legend rows fires the same focus path. */
.category-mix__slice-link:focus,
.category-mix__legend-row:focus {
  outline: none;
}

.category-mix__slice-link:focus-visible,
.category-mix__legend-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.category-mix__slice-link {
  pointer-events: visiblePainted;
}

/* Hover on a slice → that slice bumps stroke-width + brightens, and
   the matching legend row gets a background highlight. :has()-based
   sibling targeting; no JS. */
.category-mix__slice:hover {
  stroke-width: 24;
  filter: brightness(1.08);
}

.category-mix__centre {
  font-family: var(--brand-display-font);
  font-size: 10px; /* SVG user units; viewBox is 100x100 */
  font-weight: var(--weight-semibold);
  fill: var(--colour-ink-strong);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.3px;
}

/* Legend column: stacked rows, vertically centred. Each row is a
   three-cell line: swatch, category name (left), number (right-
   aligned). Type set tight so 4-5 rows comfortably fit the card's
   220px min-height with breathing room. */
.category-mix__legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-self: center;
}

.category-mix__legend-row {
  display: grid;
  grid-template-columns: 10px 1fr auto;
  gap: var(--space-2);
  align-items: center;
  padding: 4px var(--space-2);
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--colour-ink);
  font-size: var(--size-xs);
  font-variant-numeric: tabular-nums;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.category-mix__swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
}

.category-mix__legend-name {
  color: var(--colour-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.category-mix__legend-number {
  color: var(--colour-ink-muted);
  font-weight: var(--weight-medium);
  text-align: right;
}

.category-mix__legend-row:hover {
  background: var(--colour-surface-soft);
}

.category-mix__legend-row:hover .category-mix__legend-name {
  color: var(--colour-ink-strong);
}

/* Hover sync donut ↔ legend (decided 2026-05-22). CSS :has() reads
   the descendant hover state from the body ancestor and styles
   another descendant matched by data-category. Two directions:

     1. Slice hover → matching legend row highlights (background +
        ink-strong name).
     2. Legend row hover → matching slice brightens (via
        :has() on the body + descendant selector targeting the
        slice). The slice's own :hover rule already bumps stroke-
        width + brightness — the legend-hover variant invokes the
        same visual end-state without firing the slice's :hover
        directly.

   Per-category rules are required because attribute-value matching
   in CSS can't cross to a sibling-attribute-equality target with a
   wildcard. Categories are pilot-fixed (four known); a future
   category needs a tenant-side CSS append, same shape as adding any
   other category-keyed token. */
.category-mix__body:has(.category-mix__slice-link[data-category="Louvre"]:hover)
  .category-mix__legend-row[data-category="Louvre"],
.category-mix__body:has(.category-mix__legend-row[data-category="Louvre"]:hover)
  .category-mix__legend-row[data-category="Louvre"],
.category-mix__body:has(.category-mix__slice-link[data-category="Acoustic"]:hover)
  .category-mix__legend-row[data-category="Acoustic"],
.category-mix__body:has(.category-mix__legend-row[data-category="Acoustic"]:hover)
  .category-mix__legend-row[data-category="Acoustic"],
.category-mix__body:has(.category-mix__slice-link[data-category="Brise soleil"]:hover)
  .category-mix__legend-row[data-category="Brise soleil"],
.category-mix__body:has(.category-mix__slice-link[data-category="Supporting structures"]:hover)
  .category-mix__legend-row[data-category="Supporting structures"] {
  background: var(--colour-surface-soft);
}

.category-mix__body:has(.category-mix__legend-row[data-category="Louvre"]:hover)
  .category-mix__slice[data-category="Louvre"],
.category-mix__body:has(.category-mix__legend-row[data-category="Acoustic"]:hover)
  .category-mix__slice[data-category="Acoustic"],
.category-mix__body:has(.category-mix__legend-row[data-category="Brise soleil"]:hover)
  .category-mix__slice[data-category="Brise soleil"],
.category-mix__body:has(.category-mix__legend-row[data-category="Supporting structures"]:hover)
  .category-mix__slice[data-category="Supporting structures"] {
  stroke-width: 24;
  filter: brightness(1.08);
}

.category-mix__empty {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  color: var(--colour-ink-muted);
  font-size: var(--size-sm);
  font-style: italic;
}


/* ---- working-surface card ---- */

.surface {
  background: var(--colour-surface);
  border-radius: var(--radius-lg);
  border: 1px solid var(--colour-line);
  box-shadow: var(--shadow-xs);
  overflow: hidden;
}

.surface__head {
  /* Single-row head (decided 2026-05-28, fourth pass): title +
     Ready/All in a left cluster, Clear-all slot + filter
     controls in a right cluster. Free space between the two
     clusters is absorbed by .surface__clear-slot's
     margin-left:auto, which pushes the slot (and the filters
     that sit after it) to the right edge.

     The two-row variant from earlier the same day was retired
     once the chip strip came off — everything fits on one row
     at the dashboard's typical card width and a single horizon
     reads cleaner than a stacked pair. */
  padding: var(--space-5);
  border-bottom: 1px solid var(--colour-line);
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.surface__title-cluster {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

.surface__title {
  font-size: var(--size-md);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
}

.surface__hint {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
}

.surface__body {
  padding: 0;
}

.surface__empty {
  padding: var(--space-9) var(--space-6);
  text-align: center;
  color: var(--colour-ink-muted);
  font-size: var(--size-sm);
  line-height: var(--leading-relaxed);
}

/* ---- row list (working surface) ---- */

/* The row list uses a 12-column grid: a thin group-indicator column
   on the left, then the 11 data columns. Each row is a <details>
   element so expand-to-show-teaching-note is native HTML (no JS
   needed; progressive enhancement). */

.rowlist {
  display: flex;
  flex-direction: column;
  /* Canonical rowlist column template (hoisted 2026-05-27). The header
     row, every data row, the contract-card head, and the breakdown
     card all consume var(--enquiry-grid), so their columns align by
     construction from ONE definition instead of four copies. The
     <1280px variant redefines this single property (drops Quoted by). */
  --enquiry-grid:
    minmax(98px, 0.55fr)   /* Priority */
    minmax(150px, 0.85fr)  /* Status — trimmed 2026-05-27 (was
                              180px/1.05fr). The S5e label-above-bar
                              move means "Awaiting input" sits over the
                              72px bar, not beside it, so the old floor
                              was over-generous; the freed space goes to
                              Category for multi-pill rows. */
    minmax(124px, 0.75fr)  /* Enquiry No */
    minmax(150px, 1.05fr)  /* Contractor */
    minmax(170px, 1.25fr)  /* Contract */
    minmax(150px, 0.85fr)  /* Category — widened 2026-05-27 (was
                              112px/0.65fr) for multiple line-item
                              pills side by side before wrapping */
    minmax(96px, 0.55fr)   /* Value */
    minmax(68px, 0.4fr)    /* Received */
    minmax(68px, 0.4fr)    /* Required */
    minmax(86px, 0.45fr);  /* Quoted by */
}

/* Shared grid template so the header and every data row align.
   Ten columns per the S5 register reopening (decided 2026-05-19,
   dashboard-build S5): Priority, Status, Enquiry No, Contractor,
   Contract, Category, Value, Received, Required, Quoted by. The
   leftmost Priority + Status cluster is the triage block — eye
   reads "what needs me?" before anything else. Status is sortable
   so clicking the header clusters the list by state for whole-
   pipeline triage. Value moves to sit directly after Category per
   the brief (was further right in the 9-col layout). */
.rowlist__head,
.enquiry__data {
  display: grid;
  grid-template-columns: var(--enquiry-grid);
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-2) var(--space-5);
  min-height: 64px; /* S5 brief §2: uniform 64px rows for scannability */
}

/* Inside a contract card the Contract cell is left EMPTY but the
   grid stays the full 9-column template (decided 2026-05-19,
   inner-row alignment fix): every column anchors to the same x
   position as standard rows above and below, so Received /
   Required / Value / Quoted by / Stage / Status line up vertically
   through the entire list. The card header strip carries the
   shared contract identity so the empty inner Contract cell is
   not redundant — it is a deliberate visual gap that keeps the
   grid honest. */

.rowlist__head {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  background: var(--colour-surface-soft);
  border-bottom: 1px solid var(--colour-line);
  position: sticky;
  top: 0;
  z-index: 1;
}

.rowlist__col--right {
  text-align: right;
}

/* Sortable column headers. The link occupies the header cell and
   carries a subtle indicator when active. Per §7.1's quiet-status
   register: never loud, just discoverable. */
.rowlist__sort {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out);
}

.rowlist__sort:hover {
  color: var(--accent-strong);
}

.rowlist__sort--active {
  color: var(--accent-strong);
}

.rowlist__sort-indicator {
  font-size: var(--size-xs);
  width: 0.7em;
  display: inline-block;
  text-align: center;
}

/* Pagination row beneath the list (decided 2026-05-21). Quiet
   register matching the filter bar pills: small, ink-muted, with the
   current page in accent. Lives inside #rowlist so HTMX swaps
   refresh it alongside the rows. Hidden by the template when total
   pages == 1, so the surface stays clean below the row threshold. */
.rowlist__pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--colour-line);
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
}

.rowlist__pagination-status {
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
}

.rowlist__pagination-total {
  font-weight: var(--weight-regular);
  color: var(--colour-ink-muted);
  margin-left: var(--space-1);
}

.rowlist__pagination-controls {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.rowlist__pagination-link {
  min-width: 32px;
  text-align: center;
  padding: 4px var(--space-2);
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  color: var(--colour-ink-muted);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.rowlist__pagination-link:hover {
  color: var(--colour-ink);
  border-color: var(--colour-line);
}

.rowlist__pagination-link--current {
  background: var(--colour-surface-soft);
  border-color: var(--colour-line);
  color: var(--accent-strong);
  font-weight: var(--weight-medium);
  cursor: default;
}

.rowlist__pagination-link--disabled {
  color: var(--colour-line);
  cursor: not-allowed;
}

.rowlist__pagination-link--disabled:hover {
  color: var(--colour-line);
  border-color: transparent;
}

/* Filter input in the surface head: a quiet search field that
   feels at home next to the title. No submit button, no panel. */
.surface__filter {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-3);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.surface__filter:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.surface__filter-icon {
  width: 14px;
  height: 14px;
  color: var(--colour-ink-muted);
  flex-shrink: 0;
}

.surface__filter-input {
  border: 0;
  outline: none;
  background: transparent;
  font: inherit;
  font-size: var(--size-sm);
  color: var(--colour-ink);
  padding: var(--space-2) 0;
  min-width: 220px;
}

.surface__filter-input::placeholder {
  color: var(--colour-ink-muted);
}

/* Summary-card period toggle (Week | Month) on the quotes-sent
   card. Subtle segmented switch, two text-buttons; the active
   option is accent-coloured, the other muted. */
.summary-card--toggle .summary-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.summary-card__toggle {
  display: inline-flex;
  background: var(--colour-bg);
  border-radius: var(--radius-pill);
  padding: 2px;
  border: 1px solid var(--colour-line);
}

.summary-card__toggle-opt {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  padding: 2px var(--space-2);
  min-width: 22px;
  text-align: center;
  border-radius: var(--radius-pill);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.summary-card__toggle-opt:hover {
  color: var(--colour-ink);
}

/* Active toggle option mirrors the parent card's data-period. No
   --active class needed; the data-attribute pair drives it. */
.summary-card[data-period="week"] .summary-card__toggle-opt[data-period-set="week"],
.summary-card[data-period="month"] .summary-card__toggle-opt[data-period-set="month"],
.summary-card[data-period="year"] .summary-card__toggle-opt[data-period-set="year"] {
  background: var(--colour-surface);
  color: var(--accent-strong);
  box-shadow: var(--shadow-xs);
}

/* S5e (decided 2026-05-20): visibility pill on the Enquiries filter
   bar. Active|All toggle that hides or surfaces §4e pricing-input
   rows inline (§2f rows stay exclusively on the On-hold tab in both
   modes). Same shape register as the summary-card W/M/Y toggle so the
   page reads as one visual vocabulary applied in two places. */
.surface__view {
  display: inline-flex;
  background: var(--colour-bg);
  border-radius: var(--radius-pill);
  padding: 2px;
  border: 1px solid var(--colour-line);
}

.surface__view-opt {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  padding: 2px var(--space-3);
  min-width: 44px;
  text-align: center;
  border-radius: var(--radius-pill);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.surface__view-opt:hover {
  color: var(--colour-ink);
}

.surface__view-opt--active {
  background: var(--colour-surface);
  color: var(--accent-strong);
  box-shadow: var(--shadow-xs);
}

.enquiry {
  border-bottom: 1px solid var(--colour-line-soft);
  transition: background-color var(--duration-fast) var(--ease-out);
  /* Scroll-anchor landing offset (decided 2026-05-27). The row IS the
     #enquiry-{id} deep-link target (id on this outer <details>), and
     the document is the scroller with .header sticky at top:0
     (--header-height tall). Without this margin the browser scrolls the
     row's top flush to viewport-top, where the sticky header covers the
     identity summary and the deep-link appears to land inside the
     expanded panel. scroll-margin-top clears the header plus a --space-4
     gap so the row's identity line (priority, contractor, contract,
     category, value, dates) sits just below the header with the
     expanded panel beginning beneath it. Only affects scroll-into-view
     landings; no effect on normal layout. */
  scroll-margin-top: calc(var(--header-height) + var(--space-4));
}

.enquiry:last-of-type {
  border-bottom: none;
}

.enquiry[open] {
  background: var(--colour-surface-soft);
}

.enquiry__summary {
  cursor: pointer;
  list-style: none;
  font-size: var(--size-sm);
  display: block;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.enquiry__summary::-webkit-details-marker {
  display: none;
}

/* Row-hover background removed on the Enquiries surface (S5c,
   decided 2026-05-20): the beige tint on row-hover read as
   noise on the list, particularly under the new status progress
   bar where the hover-bg competed with the bar's state colour.
   Scoped removal — the same --colour-surface-soft hover tint
   STAYS on .export-dropdown__item (correct + wanted on the
   Export-settings rows) and on .surface__category-option
   (correct + wanted in the category-filter dropdown). The fix
   is selector-specific, NOT a shared-token rewrite. */
.enquiry__summary:hover {
  /* hover-bg intentionally absent — see S5c note above. */
}

.enquiry__summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Per §7.2 clarification (decided 2026-05-19): the teaching note
   is the primary intelligence of the enquiry and lives front and
   centre INSIDE the opened enquiry detail panel, not previewed in
   the list row. Keeping the note off the row keeps the list
   scannable. The detail panel below is where the full note shows
   on expand. */

/* The shared contract card: the visual primitive for §2c
   duplicate-tender pairs. One bordered card per duplicate-tender
   group, with a header strip carrying the shared contract
   identity and the nested contractor rows below. Renders as
   structurally distinct from single rows — the grid alignment
   break is intentional. */
.contract-card {
  /* No horizontal margin: the card spans the rowlist's full width
     so its internal grid lines up with every standard row's grid
     (decided 2026-05-19, alignment-fix). Vertical margin only,
     to visually separate the card from neighbouring rows. */
  margin: var(--space-3) 0;
  border: 1px solid var(--accent-tint);
  border-radius: var(--radius-lg);
  background: var(--accent-tint);
  overflow: hidden;
  box-shadow: var(--shadow-xs);
}

/* While a priority confirm popover is open inside a duplicate-tender
   card, let it escape the card's rounded-corner clip (2026-05-27).
   The popover's containing block is the priority cell INSIDE the card,
   so the card's overflow:hidden would otherwise crop the overlay where
   it drops past the card edge. :has scopes the exception to exactly
   when a confirm is open; the card keeps overflow:hidden (clean
   corners) the rest of the time. */
.contract-card:has(.enquiry__cell--confirming) {
  overflow: visible;
}

/* The card header uses the SAME grid template as a standard row's
   data line, so the contract name lands at the same x position as
   every other contract name in the list. Per §7.1 recognition-as-
   function: the eye learns one column position for contracts and
   the card's grouped identity must respect that learned position. */
.contract-card__head {
  /* S5c (decided 2026-05-20): restore the 10-column grid template
     matching the row body so the pill + identity strip anchors
     at the Contract column (col 5). The S5b flex-row shape
     tightened the pill + identity onto one line but bled left
     across the Priority/Status/Enquiry/Contractor columns,
     breaking the grid visually. S5c keeps the single-line
     tightness via a flex wrapper INSIDE the grid cell — pill
     and title remain inline together, but the group as a whole
     starts where the Contract column starts on the rows above
     and below the card.

     S5b history: the prior shape collapsed the grid entirely.
     S5b note preserved in commit history; the lock-history-at-
     small-scale comment chain reads S5 (grid, two-block stack,
     overcorrected) → S5b (flex, tight but misaligned) → S5c
     (grid + inline strip, both). */
  display: grid;
  grid-template-columns: var(--enquiry-grid);
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-2) var(--space-5);
}

/* S5d (decided 2026-05-20): pill and contract title at INDEPENDENT
   grid positions inside the card head. Pill anchors to col 2 (the
   row's Status column) so it sits under the same column as the
   row-level status signals; title anchors to col 5 / -1 (Contract
   onward) so the contract name lines up with contract names in
   the rows above and below. The S5c strip-wrapper has been
   dropped — pill and title are direct children of
   .contract-card__head, each with its own grid-column. */
.contract-card__head > .pill {
  grid-column: 2;
  justify-self: start;
}

.contract-card__title {
  grid-column: 5 / -1;
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  min-width: 0;
}

/* Per Marcus's alignment-fix (decided 2026-05-19): the contract
   name in the card header matches the standard-row Contract cell
   typography exactly. The container carries the special-case
   distinction; the text stays calm and in-grid. */
.contract-card__contract {
  font-size: var(--size-sm);
  font-weight: var(--weight-regular);
  color: var(--colour-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.contract-card__meta {
  font-size: var(--size-xs);
  color: var(--colour-ink-soft);
  font-weight: var(--weight-regular);
}

/* Part A (decided 2026-05-19): a very faint wash of the header
   strip's accent tint on the body so the nested rows read as
   bound to the card. The mix is heavily weighted toward surface
   white so the tint is barely there at rest; the warm beige
   hover/active state on .enquiry__summary cleanly overrides it
   (surface-soft is opaque and wins). Grouping tint = resting
   only; hover/click feedback stays clearly visible. */
.contract-card__body {
  background: color-mix(in srgb, var(--accent-tint) 28%, var(--colour-surface));
  border-top: 1px solid var(--accent-tint);
}

.contract-card .enquiry {
  border-bottom: 1px solid var(--colour-line-soft);
}

.contract-card .enquiry:last-of-type {
  border-bottom: none;
}

.enquiry__cell {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--colour-ink);
}

.enquiry__cell--mono {
  font-variant-numeric: tabular-nums;
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
}

.enquiry__cell--strong {
  font-weight: var(--weight-medium);
  color: var(--colour-ink-strong);
}

.enquiry__cell--muted {
  color: var(--colour-ink-muted);
  font-size: var(--size-xs);
}

.enquiry__cell--num {
  font-variant-numeric: tabular-nums;
  font-weight: var(--weight-medium);
  text-align: right;
  color: var(--colour-ink-strong);
}

.enquiry__cell--pills {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  white-space: normal;
}

/* The opened-enquiry detail panel. Teaching note front and centre
   per §7.2 (clarified 2026-05-19), then the moved-off-the-row
   fields (Email To, Postcode, Sent, Product), then actions. */
.enquiry__detail {
  padding: var(--space-5) var(--space-5) var(--space-5)
           var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: var(--colour-surface-soft);
  border-top: 1px dashed var(--colour-line);
}

.enquiry__detail-heading {
  /* Primary section heading for the expanded view (AI CLASSIFICATION
     ASSESSMENT / PRICE BREAKDOWN). One token above the metadata field
     labels (--size-xs) so it reads as a section heading, not a field
     label (decided 2026-05-27). Small-caps / muted / wide-tracked
     register unchanged. */
  font-size: var(--size-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-muted);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  margin: 0;
}

.enquiry__detail-note {
  /* Sized one step below row data (decided 2026-05-23) so the
     assessment text reads as supporting context rather than competing
     headline weight with the row content. Tighter line-height pairs
     with the smaller size; previous --leading-relaxed (1.7) opened
     the paragraphs out too far at --size-sm. The 72ch cap was
     dropped at the same time so the paragraphs use the full detail-
     panel width (the brief: assessment text fills the available
     horizontal width). Reading-comfort on long paragraphs comes from
     the line-height and the detail-panel's own padding, not from a
     character cap that left two-thirds of the panel empty. */
  font-size: var(--size-sm);
  line-height: var(--leading-base);
  color: var(--colour-ink);
  margin: 0;
}

.enquiry__detail-reason {
  font-size: var(--size-sm);
  color: var(--pill-failure-ink);
  background: var(--pill-failure-bg);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  align-self: flex-start;
  font-weight: var(--weight-medium);
  margin: 0;
}

.enquiry__detail-comments {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  font-style: italic;
  max-width: 72ch;
  margin: 0;
}

.enquiry__detail-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-3) var(--space-5);
  margin: 0;
  padding: var(--space-3) 0 0;
  border-top: 1px solid var(--colour-line-soft);
}

.enquiry__detail-field {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.enquiry__detail-field dt {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

.enquiry__detail-field dd {
  margin: 0;
  font-size: var(--size-sm);
  color: var(--colour-ink);
}

/* ---- Per-line pricing breakdown (decided 2026-05-27; visual
   refinement 2026-05-27) ----
   The expanded panel splits into two columns ONLY when the route
   supplies breakdown lines (/enquiries). The modifier reuses the
   .enquiry__data column template so the breakdown CARD aligns to the
   VALUE column onward. Without the modifier .enquiry__detail stays the
   plain flex column it was, so Journey / On-hold / pricing-input are
   untouched.

   DELIBERATE REOPENINGS of the original breakdown brief (named, not
   silently flipped): (1) the original "no box, alignment does the
   structural work" is reopened; the breakdown now sits in a light card
   frame mirroring .summary-card. (2) CP2's "numbers-left" is reopened;
   values right-align digit-flush as one shared column. The card's
   padding insets its content from the VALUE grid line, so the values
   are flush TO EACH OTHER inside the card (in the Value region) rather
   than pixel-flush under the header total.

   The grid-template comes from var(--enquiry-grid), the canonical
   rowlist column template defined on .rowlist (hoisted 2026-05-27), so
   the breakdown card's value column aligns under the VALUE header by
   construction, base and <1280px alike. */
.enquiry__detail--with-breakdown {
  display: grid;
  grid-template-columns: var(--enquiry-grid);
  gap: var(--space-3);
  align-items: start; /* card + assessment top-align */
}

/* Left column: existing detail content, spanning Priority..Category
   (everything left of Value). Keeps its own flex-column rhythm so the
   default (no-breakdown) panel renders identically. The right padding
   widens the gutter to the card WITHOUT touching the outer grid gap,
   which would otherwise shift the card off the Value column. */
.enquiry__detail--with-breakdown .enquiry__detail-main {
  grid-column: 1 / 7;
  /* Gutter to the breakdown card, widened one token 2026-05-27
     (--space-4 → --space-5). Applied as the assessment column's
     right padding, NOT the grid `gap`: the grid gap is load-bearing
     (it keeps the breakdown card's value column aligned under the
     VALUE header), so the gutter is widened here instead. */
  padding-right: var(--space-5);
  min-width: 0;
}

.enquiry__detail-main {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

/* Right column: the breakdown, spanning Value..end. Light card frame
   mirroring .summary-card (surface bg lifts off the soft panel, lg
   radius, space-5 padding, lightest shadow). One grid so every line's
   value shares a single right-aligned column, flush to each other. */
/* Right column wrapper (2026-05-27): spans Value..end and stacks the
   PRICE BREAKDOWN heading above the card with the same --space-4
   heading-to-content gap the left column (.enquiry__detail-main) uses,
   so both column headings sit at the same height and their content
   starts level. The grid placement moved here off .enquiry__breakdown
   so the heading lives in the column, above the card frame. */
.enquiry__breakdown-col {
  grid-column: 7 / -1;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.enquiry__breakdown {
  display: grid;
  grid-template-columns: auto 1fr; /* value (shared, right-aligned) | text */
  column-gap: var(--space-3);
  row-gap: var(--space-4);
  align-items: baseline; /* value rides the product (first) line */
  min-width: 0;
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs);
  padding: var(--space-5);
}

/* Each line feeds its value + text directly into the breakdown grid
   (display:contents) so values across lines share column 1. */
.breakdown-line {
  display: contents;
}

/* Value: SECONDARY weight, matching the .enquiry__cell--muted date
   cells (xs / muted / regular) so it reads as supporting detail and
   never outweighs the header total. Right-aligned + tabular so the
   shared column is digit-flush. */
.breakdown-line__value {
  grid-column: 1;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: var(--size-xs);
  font-weight: var(--weight-regular);
  color: var(--colour-ink-muted);
  white-space: nowrap;
}

.breakdown-line__text {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

/* Top row: quantity × sub-type, the line the client's eye lands on.
   text-transform normalises the casing at the display layer only: the
   stored sub_type keeps the model's own words (a config literal like
   "screening louvre" or a model phrase like "Carbon Steel Doorset Louvre
   Door"), and capitalize renders them in one consistent Title Case so the
   panel does not read as a mix of lowercase and title-case titles. The
   measurement subtitle below is left untouched (its units/codes keep
   their natural casing: "PPC RAL 9006", "25dB Rw"). */
.breakdown-line__product {
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
  text-transform: capitalize;
}

/* Bottom row: spec detail, quieter, the specifics under the product. */
.breakdown-line__spec {
  font-size: var(--size-xs);
  color: var(--colour-ink-soft);
}

/* Chosen-product code (2026-06-12): the judge's blade choice surfaced
   as a small, glanceable code on the price-breakdown line and the
   calculator-download modal. the client thinks in blades — the code is the
   decision he sanity-checks where the £ figure cannot. A QUIET neutral
   chip on purpose: the accent and the won-green are load-bearing
   elsewhere, so the chosen product borrows neither. A touch of tracking
   and tabular figures so a product CODE reads as a code on the Poppins
   stack. Tenant-neutral: the string is the tenant's own catalogue name,
   rendered verbatim. */
.blade-code {
  display: inline-block;
  margin-left: 0.45em;
  padding: 0 6px;
  border-radius: var(--radius-sm);
  background: var(--pill-neutral-bg);
  color: var(--colour-ink-strong);
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  font-variant-numeric: tabular-nums;
  text-transform: none;
  white-space: nowrap;
  line-height: 1.5;
}

.enquiry__actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-top: var(--space-2);
}

.enquiry__action {
  display: inline-flex;
  align-items: center;
  /* gap holds the icon and label apart in every action button.
     Added 2026-05-23 alongside the icon rollout; tuned to 6px so
     the icon reads as part of the same chip rather than a separate
     element. */
  gap: 6px;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-soft);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}

/* Lucide-style icon next to every action button label (decided
   2026-05-23). 14px square keeps the icon visually subordinate to
   text without disappearing; stroke-width 1.6 matches the rail
   nav icons so the dashboard reads as one icon family. flex-shrink:
   0 prevents the icon collapsing when a long label wraps. */
.enquiry__action-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.enquiry__action:hover {
  color: var(--accent-strong);
  border-color: var(--accent-tint);
  background: var(--accent-tint);
}

.enquiry__action--primary {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent);
}

.enquiry__action--primary:hover {
  background: var(--accent-strong);
  color: var(--accent-ink);
  border-color: var(--accent-strong);
  transform: translateY(-1px);
}

/* On-hold reason banner inside the shared detail panel.
   Renders only when row.on_hold_reason is non-None (set by the
   route for awaiting-input rows on the On-hold surface). The
   banner sits at the TOP of the detail panel above the AI
   assessment because it IS the row's primary scan signal on
   the On-hold surface — what specifically the AI couldn't
   resolve, in one line. S5d, decided 2026-05-20. */

.enquiry__on-hold-reason {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin: 0 0 var(--space-4) 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--colour-bg-awaiting, #fef6ed);
  border-left: 3px solid var(--colour-awaiting, #ba7517);
  font-size: var(--size-sm);
  line-height: var(--leading-snug);
}

.enquiry__on-hold-reason-label {
  flex-shrink: 0;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--size-2xs, 0.7rem);
  font-weight: var(--weight-semibold);
  color: var(--colour-awaiting, #ba7517);
}

.enquiry__on-hold-reason-text {
  color: var(--colour-ink);
  font-weight: var(--weight-medium);
}

/* ---- Detail-panel Note block (2026-06-13, law 8 — make the gap
   visible) ----
   A calm, amber-accented note on the priced detail panel, between the
   metadata fields and the action buttons, left-aligned to the field
   labels. Surfaces a gap the human should see (currently the dropped-
   alternative reason: a quote variant the pricer could not ground), not
   an error. Mirrors the awaiting palette of .enquiry__on-hold-reason but
   reads as a BLOCK: the quiet uppercase "Note" label (the POSTCODE /
   section-heading register) above the stored reason text, which wraps to
   as many lines as needed. Renders only when row.notes is non-empty. */
.enquiry__note {
  margin: var(--space-4) 0 0 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--colour-bg-awaiting, #fef6ed);
  border-left: 3px solid var(--colour-awaiting, #ba7517);
}

.enquiry__note-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-2) 0;
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  font-size: var(--size-2xs, 0.7rem);
  font-weight: var(--weight-semibold);
  color: var(--colour-awaiting, #ba7517);
}

.enquiry__note-icon {
  font-size: var(--size-sm);
  line-height: 1;
}

.enquiry__note-text {
  margin: 0;
  font-size: var(--size-sm);
  line-height: var(--leading-snug);
  color: var(--colour-ink);
}

.enquiry__note-text + .enquiry__note-text {
  margin-top: var(--space-2);
}

/* Per-row action FORMS inside the detail panel (On-hold actions
   are real working POSTs, not anchor stubs like Enquiries). Each
   form sits inline alongside the other actions and matches the
   .enquiry__action button visual register. S5d, decided 2026-05-20. */

.enquiry__action-form {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
}

.enquiry__action-form--supply {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-3);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
}

.enquiry__action-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  flex: 1 1 200px;
}

.enquiry__action-field-label {
  font-size: var(--size-2xs, 0.7rem);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--colour-ink-soft);
}

.enquiry__action-input {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  background: var(--colour-bg);
  font: inherit;
  color: var(--colour-ink);
  transition: border-color var(--duration-fast) var(--ease-out);
}

.enquiry__action-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

/* S5e (decided 2026-05-20): open-text variant of the supply form
   replaces the previous (field_name, value) structured pair. The
   field stretches across the form so the prose has room; the
   textarea inherits font-family + size from the form via font:
   inherit on the base rule. */
.enquiry__action-field--open {
  flex: 1 1 100%;
}

.enquiry__action-input--open {
  min-height: 64px;
  resize: vertical;
  line-height: var(--leading-normal);
}

.enquiry__archive-confirm {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--size-xs);
  color: var(--colour-ink-soft);
  cursor: pointer;
}

.enquiry__archive-confirm input[type="checkbox"] {
  width: 14px;
  height: 14px;
  accent-color: var(--colour-lost, #791f1f);
}

.enquiry__action--danger {
  color: var(--colour-lost, #791f1f);
  border-color: var(--colour-lost-line, rgba(121, 31, 31, 0.3));
  background: var(--colour-surface);
}

.enquiry__action--danger:hover {
  background: var(--colour-lost-bg, #fcebeb);
  color: var(--colour-lost, #791f1f);
  border-color: var(--colour-lost, #791f1f);
}

/* Filled-red commit button (decided 2026-05-27): the Set-aside
   popover's Confirm action. Mirrors --primary's filled shape but in
   the lost-red family, so the opener (outline --danger) and the
   committer (this) read as the same red at different weights — the
   weight signals this is the click that commits. Same token
   (var(--colour-lost, #791f1f)) the outline --danger uses. */
.enquiry__action--danger-solid {
  background: var(--colour-lost, #791f1f);
  color: var(--accent-ink);
  border-color: var(--colour-lost, #791f1f);
}

.enquiry__action--danger-solid:hover {
  background: color-mix(in srgb, var(--colour-lost, #791f1f), #000 14%);
  color: var(--accent-ink);
  border-color: color-mix(in srgb, var(--colour-lost, #791f1f), #000 14%);
  transform: translateY(-1px);
}

/* Disabled state for the Archive button until the confirm checkbox
   is ticked. Form-level :has() selector keeps the relationship
   declarative without JS. */
.enquiry__action-form--archive:has(input[type="checkbox"]:not(:checked))
  .enquiry__action--danger {
  opacity: 0.5;
  pointer-events: none;
}

/* ---- Set-aside reason modal (2026-06-12) ----
   Was an absolute popover dropped below the trigger (decided
   2026-05-27); on bottom-of-list rows the popover clipped behind the
   sibling cards' stacking contexts and could not be completed. Now a
   centred modal in #modal-slot on the shared .modal-backdrop scrim —
   position-independent by construction, dismissed by Cancel, outside
   click, or Escape (modal.js). The control span just wraps the
   trigger button in the actions row. */
.enquiry__archive-control {
  display: inline-flex;
}

.archive-modal__form {
  display: flex;
  flex-direction: column;
  /* Between options: --space-2 (8px), tightened from --space-3
     2026-05-27. With the 2px within-option gap each radio+label+
     clarifier reads as one tight cluster (the breakdown-card
     discipline), the 8px a comfortable-not-generous separator. */
  gap: var(--space-2);
}

/* One reason: radio + label on a line, clarifier muted beneath, in the
   breakdown spec-line register (--size-xs / ink-soft). Inline text,
   not a tooltip — archive is infrequent and the three-reason split's
   data quality depends on the right pick each time. */
.archive-modal__option {
  display: flex;
  flex-direction: column;
  gap: 2px;
  cursor: pointer;
}

.archive-modal__option-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Reset the browser-default radio margin so the radio + label head
   sits tight; the option-head gap is the only spacing between them. */
.archive-modal__option-head input[type="radio"] {
  margin: 0;
}

.archive-modal__option-label {
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
}

.archive-modal__option-clarifier {
  font-size: var(--size-xs);
  color: var(--colour-ink-soft);
  /* Indent past the radio so the clarifier reads as the label's
     subtext, not a new column. */
  padding-left: calc(var(--space-2) + 13px);
}

/* On-hold page sub-line under the surface title. */
.surface__sub {
  margin: var(--space-1) 0 0 0;
  color: var(--colour-ink-soft);
  font-size: var(--size-sm);
  line-height: var(--leading-relaxed);
}

.surface__sub strong {
  color: var(--colour-ink);
  font-weight: var(--weight-semibold);
}

/* Relevance-uncertain row (S5d split, decided 2026-05-20).
   Deliberately NOT the shared enquiry grid — these are items the AI
   could not place as enquiries, so priority/value/category are
   meaningless. The shape is a calm short triage list: three text
   lines per row (Sender + Received | Reason | Actions), generous
   vertical breathing room, no horizontal column grid. */

/* On-hold (relevance-uncertain) surface (S5e redesign, decided
   2026-05-20). The S5d cards-grid shape was over-spaced and treated
   each row as an enquiry with reserved vertical space; the S5e shape
   is a tight email-triage list where collapsed rows scan as dozens-
   at-a-glance and a click expands one row in place into a writing
   surface. Same expand-on-click pattern as the Enquiries detail
   panel via native <details>/<summary>. */

.on-hold-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.on-hold-row {
  background: var(--colour-surface);
  border-bottom: 1px solid var(--colour-line-soft);
}

.on-hold-row:first-child {
  border-top: 1px solid var(--colour-line-soft);
}

.on-hold-row__details {
  margin: 0;
}

.on-hold-row__summary {
  list-style: none;
  cursor: pointer;
  padding: var(--space-3) var(--space-5);
  transition: background-color var(--duration-fast) var(--ease-out);
}

.on-hold-row__summary::-webkit-details-marker {
  display: none;
}

.on-hold-row__summary:hover {
  background: var(--colour-surface-soft);
}

.on-hold-row[open] > .on-hold-row__details > .on-hold-row__summary,
.on-hold-row__details[open] > .on-hold-row__summary {
  background: var(--colour-surface-soft);
}

/* Single tight line: identity (sender + received) + reason +
   inline actions, all on one row. The reason is the centre of
   gravity — let it grow with the available space; identity stays
   tight on the left; actions hug the right edge. */
.on-hold-row__line {
  display: grid;
  grid-template-columns: minmax(180px, auto) 1fr auto;
  gap: var(--space-4);
  align-items: center;
}

.on-hold-row__identity {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
}

.on-hold-row__sender {
  font-size: var(--size-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.on-hold-row__received {
  font-size: var(--size-xs);
  color: var(--colour-ink-soft);
  font-feature-settings: "tnum";
  white-space: nowrap;
}

.on-hold-row__reason {
  font-size: var(--size-sm);
  color: var(--colour-ink);
  line-height: var(--leading-snug);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.on-hold-row__inline-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

.on-hold-row__action-slot {
  display: inline-flex;
  align-items: center;
}

.on-hold-row__action-form {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

/* Disable the inline Archive button until the confirm checkbox is
   ticked. Same :has() declarative pattern used elsewhere on the
   pricing-input surface. */
.on-hold-row__action-form--archive:has(input[type="checkbox"]:not(:checked))
  .enquiry__action--danger {
  opacity: 0.5;
  pointer-events: none;
}

/* Expanded panel — appears under the summary line when the row is
   open. Carries the email body excerpt (top, the source content
   matters first), the AI rationale (middle, what the system tried
   to read), and the supply-context writing surface (bottom, where
   the client's prose lands). The whole panel is contained by the
   <details> element so close-by-click on the summary collapses
   everything cleanly. */
.on-hold-row__panel {
  padding: var(--space-3) var(--space-5) var(--space-5);
  background: var(--colour-bg);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  border-top: 1px solid var(--colour-line-soft);
}

.on-hold-row__section {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.on-hold-row__section-title {
  font-size: var(--size-2xs, 0.7rem);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--colour-ink-soft);
  margin: 0;
}

.on-hold-row__body {
  margin: 0;
  font-family: inherit;
  font-size: var(--size-sm);
  line-height: var(--leading-relaxed);
  color: var(--colour-ink);
  background: var(--colour-surface);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  white-space: pre-wrap;
  overflow-x: auto;
}

.on-hold-row__rationale {
  margin: 0;
  font-size: var(--size-sm);
  line-height: var(--leading-snug);
  color: var(--colour-ink);
}

.on-hold-row__supply-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.enquiry__action[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.7;
}

/* ---- job-fit dot (per-row quiet indicator) ---- */

/* Per B2 (decided 2026-05-19): the classification engine routes
   enquiries by target-work categories (the client's modular /
   roof-screen / big-louvre mental segmentation). The dot exposes
   that bit per row as a quiet 6px mark in the enquiry-no cell.
   Single colour across all target-fit categories — the dot just
   says "this fits one of your target work types"; the specific
   category sits on the tooltip. Minimal weight per §7.1
   subtraction-over-addition. */
.jobfit-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  vertical-align: middle;
  margin-right: var(--space-2);
  flex-shrink: 0;
  opacity: 0.85;
}

/* ---- nav badge (count circle next to left-rail nav items) ---- */

.rail__nav-badge {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 18px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-pill);
  font-size: var(--size-xs);
  font-weight: var(--weight-semibold);
  background: var(--pill-awaiting-bg);
  color: var(--pill-awaiting-ink);
  line-height: 1;
}

.rail__nav-badge--muted {
  background: var(--pill-neutral-bg);
  color: var(--pill-neutral-ink);
}

/* ---- value-band dropdown (sits beside contractor search) ---- */

.surface__filters {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* ---- active-filter visibility (decided 2026-05-28, 3rd pass) ---- */

/* Lit state for each chip-eligible control wrapper, derived in
   CSS via :has() reading the input's live property state. No
   server-side --active class; the lit state is a function of
   the very element whose value seeds the URL params, so the
   two cannot disagree (P48 cell-renders-truth).

     - .surface__filter lights when the contractor input has a
       value. :not(:placeholder-shown) tracks the live value
       property, not the stale value attribute — typing or a
       JS .value='' both flip it immediately, with no roundtrip.

     - .surface__category lights when ANY checkbox descendant is
       :checked. :checked tracks the live property too, so
       unchecking the last box turns the wrapper unlit instantly
       (the bug that motivated this third pass).

     - .surface__band lights when an option other than "all" is
       :checked. The "all" sentinel is the unlit default; every
       other value of band is a lit filter.

   The earlier-same-day 2nd pass used --active classes set by
   Jinja conditions. Those classes didn't re-render on rowlist
   inner-loop swaps (the wrappers aren't in the OOB partial),
   so the lit state lagged the actual filter state by one click
   after Clear all or after unchecking the last category. CSS
   :has() with live pseudo-classes closes that gap structurally:
   no class, no stale class possible.

   Browser support: :has() shipped Chromium 105 / Firefox 121 /
   Safari 15.4 — every browser the pilot dashboard targets. */
.surface__filter:has(.surface__filter-input:not(:placeholder-shown)) {
  border-color: var(--accent);
  background: var(--accent-tint);
}

.surface__filter:has(.surface__filter-input:not(:placeholder-shown))
.surface__filter-icon,
.surface__filter:has(.surface__filter-input:not(:placeholder-shown))
.surface__filter-input {
  color: var(--accent-strong);
}

.surface__filter:has(.surface__filter-input:not(:placeholder-shown))
.surface__filter-input::placeholder {
  color: var(--accent-strong);
}

.surface__category:has(input[type="checkbox"]:checked)
.surface__category-summary {
  border-color: var(--accent);
  background-color: var(--accent-tint);
  color: var(--accent-strong);
}

.surface__band:has(option:checked:not([value="all"])) .surface__band-select {
  border-color: var(--accent);
  background-color: var(--accent-tint);
  color: var(--accent-strong);
}

/* Reserved width for the Category count "(N)" so the dropdown
   button doesn't grow horizontally when a count appears.
   inline-block + min-width holds the span's box at "(N)" width
   even when the conditional content is empty; the gap from the
   parent <summary>'s flex layout provides the visual separator
   between "Category" and the reserved slot. Sized for single-
   digit counts (pilot has 4 categories; the rule absorbs up to
   "(9)" comfortably and clips at "(99)" if a tenant ever needs
   more — easy to widen). */
.surface__category-count {
  display: inline-block;
  min-width: 2ch;
  text-align: left;
}

/* Reserved slot for the Clear-all button. Always rendered;
   min-width holds the space so the button appearing in the
   active state introduces no reflow around it. Width is sized
   to the longest plausible label ("Clear all (99)" at the
   current size-sm font) plus the button's internal padding.
   margin-left:auto pins the slot to the far right of the
   toolbar row regardless of how many chips and filters sit
   to its left. */
.surface__clear-slot {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  /* "Clear all (99)" at size-sm weight-medium is ~14ch incl.
     parens and digits; pad with the button's L/R padding so
     the slot's content-box accommodates the button's border
     box at the widest expected count. */
  min-width: calc(14ch + 2 * var(--space-3));
}

.surface__clear {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--accent-strong);
  background: transparent;
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.surface__clear:hover {
  border-color: var(--accent);
  background: var(--accent-tint);
}

.surface__clear:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.surface__band {
  display: inline-flex;
  align-items: center;
}

.surface__band-select {
  font-family: inherit;
  font-size: var(--size-sm);
  color: var(--colour-ink);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a8a8a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 12px;
  padding-right: calc(var(--space-3) + 18px);
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.surface__band-select:hover {
  border-color: var(--colour-ink-muted);
}

.surface__band-select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

/* ---- summary row layouts ---- */


/* ---- pipeline stage indicator: a left-to-right fill that reads
   unambiguously as progress (not as a battery or signal-strength
   meter). The fill width is driven by the --progress CSS variable
   set inline on the .stagebar element. Below the bar, the
   stage-label text confirms the journey position in the client's
   language. ---- */

.enquiry__cell--stage {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  white-space: nowrap;
}

.stagebar {
  display: block;
  width: 100%;
  max-width: 100px;
  height: 5px;
  background: var(--colour-line);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.stagebar__fill {
  display: block;
  height: 100%;
  width: var(--progress, 0%);
  background: var(--accent);
  border-radius: var(--radius-pill);
  transition: width var(--duration-slow) var(--ease-out);
}

.stagebar__label {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
  font-weight: var(--weight-medium);
}

/* ---- status pill ---- */

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-base);
  background: var(--pill-neutral-bg);
  color: var(--pill-neutral-ink);
  line-height: 1.4;
  white-space: nowrap;
}

.pill--active { background: var(--pill-active-bg); color: var(--pill-active-ink); }
.pill--success { background: var(--pill-success-bg); color: var(--pill-success-ink); }
.pill--awaiting { background: var(--pill-awaiting-bg); color: var(--pill-awaiting-ink); }
.pill--failure { background: var(--pill-failure-bg); color: var(--pill-failure-ink); }
.pill--holding { background: var(--pill-holding-bg); color: var(--pill-holding-ink); }

/* ---- login surface ---- */

.login {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-6);
}

.login__card {
  background: var(--colour-surface);
  border-radius: var(--radius-xl);
  border: 1px solid var(--colour-line);
  box-shadow: var(--shadow-md);
  padding: var(--space-7);
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.login__heading {
  font-family: var(--brand-display-font);
  font-size: var(--size-xl);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--colour-ink-strong);
}

.login__intro {
  color: var(--colour-ink-soft);
  font-size: var(--size-sm);
  line-height: var(--leading-relaxed);
}

.login__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field__label {
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
}

.field__input {
  font-family: inherit;
  font-size: var(--size-base);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  color: var(--colour-ink);
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.field__input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--size-base);
  font-weight: var(--weight-medium);
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.button--primary {
  background: var(--accent);
  color: var(--accent-ink);
}

.button--primary:hover {
  background: var(--accent-strong);
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}

.button--primary:active {
  transform: translateY(0);
}

.login__confirmation {
  background: var(--pill-success-bg);
  color: var(--pill-success-ink);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--size-sm);
  line-height: var(--leading-relaxed);
}

/* ---- staggered page-load reveal ---- */

@keyframes harbour-rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

.reveal {
  opacity: 0;
  animation: harbour-rise var(--duration-slow) var(--ease-out) forwards;
}

.reveal[data-stagger="0"] { animation-delay: 0ms; }
.reveal[data-stagger="1"] { animation-delay: 30ms; }
.reveal[data-stagger="2"] { animation-delay: 60ms; }
.reveal[data-stagger="3"] { animation-delay: 90ms; }
.reveal[data-stagger="4"] { animation-delay: 120ms; }
.reveal[data-stagger="5"] { animation-delay: 150ms; }
.reveal[data-stagger="6"] { animation-delay: 180ms; }

@media (prefers-reduced-motion: reduce) {
  .reveal { animation: none; opacity: 1; transform: none; }
  .summary-card:hover, .button--primary:hover { transform: none; }
  /* The in-flight shimmer is a motion-only liveness cue; drop it
     entirely (decided 2026-05-28). The bar's width still reports
     progress, and the "Re-classifying…" label still carries the
     working state, so no signal is lost. */
  .status-bar--in-flight .status-bar__fill::after { display: none; }
  .status-bar--working .status-bar__fill::after { display: none; }
}

/* ================================================================
   S5 register reopening (decided 2026-05-19, dashboard-build S5)
   ================================================================

   Visual treatment for the new columns introduced by the S5 brief:
   Priority (leftmost, editable stars), Status (bar OR terminal
   tag, replacing the pill+stage pair), Status dot (redundant cue on
   the Enquiry no cell), Category (compact tag), low-priority
   inline indicator, and the multi-select category filter dropdown.

   Hex tokens are locked per the brief §4 and live in tokens.css;
   class vocabulary is locked per the rowlist template. This block
   is the visual extension only — extending the existing slothui-
   clean-modern-bright register, not inventing a new voice.
   ================================================================ */


/* ---- Priority cell (column 1) -------------------------------- */

.rowlist__col--priority {
  /* Header label for the new leftmost column. Plain label, not
     sortable (sort-by-priority is the kind of feature that earns
     the column header treatment once real usage shows a need). */
  text-align: left;
}

.enquiry__cell--priority {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  white-space: nowrap;
  border-radius: var(--radius-sm);
  padding: 2px 4px;
  margin-left: -4px;
  /* Positioning anchor for the confirm popover (2026-05-27): the
     popover drops BELOW the stars via top:100%, the .export-dropdown
     trigger-and-panel pattern. overflow:visible is REQUIRED alongside
     position:relative here: the cell inherits overflow:hidden from
     .enquiry__cell (column text-ellipsis), which would otherwise clip
     the popover to this ~22px box and hide it (that was the regression
     of the first fix). The cell holds stars, never truncatable text,
     so dropping the clip is safe. Guarded by
     test_priority_cell_does_not_clip_the_confirm_popover. */
  position: relative;
  overflow: visible;
}

/* While the confirm popover is open the cell's stars are inert
   placeholders (the popover carries the action). Freeze them at their
   resting glyph: no pointer, no hover-preview cross-fade. */
.enquiry__cell--confirming .priority-star {
  cursor: default;
}
.enquiry__cell--confirming:hover .priority-star::before { opacity: 1; }
.enquiry__cell--confirming:hover .priority-star--filled::before { opacity: 0; }
.enquiry__cell--confirming:hover .priority-star::after { opacity: 0; }
.enquiry__cell--confirming:hover .priority-star--filled::after { opacity: 1; }

/* S5c fix (decided 2026-05-20): the earlier .enquiry__cell--priority:hover
   surface-soft tint was the source of the "beige bleed on Enquiries
   rows" complaint — hovering anywhere along the row crossed into the
   priority cell and lit it up. Removed entirely. Star-button hover
   preview (the cross-fade between ☆ and ★ glyphs on .priority-star
   hover) is a separate rule and stays — that's the genuine
   interactivity signal, scoped to the buttons themselves. */

.priority-star {
  /* Button reset — no border, no background, just the glyph. The
     glyph itself is drawn via ::before / ::after pseudo-elements
     so we can cross-fade between ☆ and ★ on hover preview without
     touching the HTML text. The native button text is hidden but
     remains in the accessibility tree (aria-label + aria-pressed
     do the actual signal work for screen readers). */
  position: relative;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  color: transparent; /* hide the HTML text glyph; ::before/::after render */
  display: inline-grid;
  place-items: center;
  border-radius: var(--radius-sm);
  transition: transform var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.priority-star::before,
.priority-star::after {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 17px;
  line-height: 1;
  transition: opacity var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
  pointer-events: none;
}

.priority-star::before {
  content: '☆';
  color: var(--priority-star-empty);
  opacity: 1;
}

.priority-star::after {
  content: '★';
  color: var(--priority-star-filled);
  opacity: 0;
}

/* Resting filled state: ★ visible, ☆ hidden. */
.priority-star--filled::before { opacity: 0; }
.priority-star--filled::after { opacity: 1; }

/* Hover-preview: when the cell is hovered, every star reads as
   filled by default (preview ink, not the resting filled ink),
   then the after-:hover-sibling rule below empties out stars to
   the right of the hovered one. Net effect: stars 1..N where N is
   the cursor's position appear filled; stars N+1..3 appear empty.
   No JS — pure :hover + general-sibling combinator. */
.enquiry__cell--priority:hover .priority-star::before {
  opacity: 0;
}
.enquiry__cell--priority:hover .priority-star::after {
  opacity: 1;
  color: var(--priority-star-preview);
}

.enquiry__cell--priority .priority-star:hover ~ .priority-star::after {
  opacity: 0;
}
.enquiry__cell--priority .priority-star:hover ~ .priority-star::before {
  opacity: 1;
  color: var(--priority-star-empty);
}

/* The star itself nudges slightly on direct hover so the
   cursor-target has perceivable feedback. */
.priority-star:hover {
  background: var(--colour-surface);
  transform: translateY(-1px);
}

.priority-star:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.priority-star:active {
  transform: translateY(0);
}

/* Error state: cell renders an inline message instead of stars.
   Set by the route when set_priority raises (rare; defence in
   depth for the cell-renders-truth posture). */
.enquiry__cell--priority-error {
  background: var(--pill-failure-bg);
}

.priority-cell__error {
  font-size: var(--size-xs);
  color: var(--pill-failure-ink);
  font-weight: var(--weight-medium);
  white-space: normal;
  line-height: var(--leading-snug);
}


/* ---- Status column (column 2) -------------------------------- */

.enquiry__cell--status {
  /* The cell renders either an active .status-bar or a terminal
     .status-tag. Both shapes are single-line as of S5b (decided
     2026-05-20, dashboard-build S5b iteration): the original S5
     active treatment was a vertical stack (progress bar on top,
     label beneath) which read as two things when it should be
     one — Marcus's feedback. The terminal tag was already single-
     line and stays untouched. */
  display: flex;
  align-items: center;
  white-space: nowrap;
}

/* --- active mode: horizontal progress bar + same-hue label --- */
/* S5b iteration (decided 2026-05-20): the active-state shape was
   reverted from a vertical bar+label stack to a single line. The
   chosen shape is an editorial-list category marker: a 3px
   vertical stripe in the state's locked hex on the left, the
   state label in the same hex to its right, both vertically
   centred on one line. The stripe + text together carry one
   signal — "this row is in amber territory / deep-blue territory
   / blue territory" — without the bar-over-label dissonance the
   previous treatment had.

   Active-vs-terminal visual distinction is preserved: terminal
   states use a tinted-background pill (won-bg + ink, lost-bg +
   ink); active states use a stripe + bare ink, no pill chrome.
   The two shapes read as visually distinct categories of state,
   not the same component with a colour swap.

   The status-stripe is also visually coherent with the status-dot
   on the Enquiry-no cell: same colour vocabulary, different shape
   (dot is round, stripe is vertical bar). The eye reads "left-
   edge of Enquiry-No is amber, left-edge of Status is amber" as
   one consistent signal across the row.

   Progress visualization (the prior --progress CSS variable that
   set fill-width on the old bar) is dropped: the categorical
   state alone is what scans matter for triage; the precise
   pipeline position doesn't earn the second-line cost. The
   --progress var is still emitted by the row template (harmless)
   for any future revival.

   S5c iteration (decided 2026-05-20): progress-fill restored. The
   S5b "progress doesn't drive the client's decisions" rationale was
   wrong — the bar's fill LENGTH is the at-a-glance pipeline-
   position scan a sales director reads across a list, load-
   bearing and not decorative. The S5b single-line constraint was
   the right move (replacing the bar+label vertical stack); the
   S5b stripe shape that replaced the bar was the over-correction.
   S5c keeps the single-line constraint and restores the
   horizontal progress bar with proportional fill driven by the
   --progress CSS variable. Visual shape: a 72px × 5px rounded
   bar to the left, state label inline to its right, both on one
   line, both vertically centred. The bar uses a hard-stop CSS
   linear-gradient (state-fill from 0% to --progress%, state-track
   from --progress% to 100%) so one element renders both track
   and fill cleanly. Terminal states (.status-tag--won / --lost)
   are unchanged — they kept the existing single-line pill shape
   through both S5b and S5c. */

.status-bar {
  /* S5e iteration (decided 2026-05-20): label sits ABOVE the bar,
     not to its right. Marcus's correction during the post-S5d
     review: the side-by-side shape ate horizontal column width and
     read as two things next to each other rather than one labelled
     widget. The stacked shape reads as a single status indicator
     (label + visual progress) and lets the Status column shrink
     a notch. The constituent spans are unchanged in HTML; the
     stack direction is owned by this rule. */
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  min-height: 22px;
}

.status-bar::before {
  /* No pseudo-element track — the bar widget (.status-bar__fill)
     renders both track and fill via a single gradient. */
  content: none;
}

.status-bar__fill {
  /* The bar widget. Hard-stop linear gradient renders state-fill
     up to --progress% and state-track from --progress% to 100%
     in one paint. Pure CSS, no extra elements, no JS. */
  display: inline-block;
  width: 72px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: linear-gradient(
    to right,
    var(--state-fill, var(--status-default-fill)) 0%,
    var(--state-fill, var(--status-default-fill)) var(--progress, 0%),
    var(--state-track, var(--status-default-track)) var(--progress, 0%),
    var(--state-track, var(--status-default-track)) 100%
  );
  flex-shrink: 0;
  transition: background var(--duration-slow) var(--ease-out);
}

/* Make --progress an animatable registered property (decided
   2026-05-27; inherits-fix 2026-05-27). A custom property driving a
   gradient stop jumps when it changes unless it is registered with a
   type; registering it as a <percentage> lets the in-flight bar
   interpolate smoothly between the discrete enquiry_advanced checkpoints
   inflight_progress.js sets. Every existing --progress consumer already
   uses a percentage value (status-bar gradient, .stagebar__fill width),
   so the type is safe. Browsers without Houdini @property ignore this
   and degrade to instant steps — the bar still advances, just without
   easing.

   inherits MUST be true. --progress is set on the PARENT (.status-bar /
   .stagebar inline style) but read by the gradient/width on the CHILD
   (.status-bar__fill / .stagebar__fill). Unregistered custom properties
   inherit by default, which is what made that parent→child hand-off
   work before registration. Registering with inherits:false silently
   broke it — the child stopped inheriting the parent's value and fell
   back to initial-value:0%, emptying every fill (static and in-flight)
   on every surface. inherits:true restores the default hand-off while
   keeping the property animatable. */
@property --progress {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

/* In-flight bar eases the gradient stop as the pipeline advances.
   Scoped to the in-flight modifier so normal status bars — which set
   --progress once at render and never change it — are untouched. The
   background transition is restated here because the shorthand on the
   base rule would otherwise be replaced.

   position:relative + overflow:hidden host the shimmer overlay below
   and clip its sweep to the bar's pill radius. */
.status-bar--in-flight .status-bar__fill {
  position: relative;
  overflow: hidden;
  transition:
    --progress var(--duration-slow) var(--ease-out),
    background var(--duration-slow) var(--ease-out);
}

/* Board liveness (2026-06-12): the same shimmer overlay host, on the
   LIVE polling path's status-bar--working (the SSE status-bar--in-flight
   is starved on the real box, D0167). No --progress transition here —
   the board sets --progress once per render and never eases it. */
.status-bar--working .status-bar__fill {
  position: relative;
  overflow: hidden;
}

/* Idle-state shimmer (decided 2026-05-28). Between the discrete
   enquiry_advanced checkpoints the bar can sit unchanged for 10-20s
   while re-classification runs; a soft highlight sweeping left-to-right
   keeps the row reading as "working" rather than frozen. It is a
   liveness signal only — an ::after overlay over the fill, so the
   progress gradient (the bar's actual width) is untouched; the shimmer
   never changes what the bar reports. transform:translateX is the
   cheap-to-animate property. Pure CSS, no JS.

   Scoped to .status-bar--in-flight, so when the row settles to a final
   status the resolve-swap (CP3) replaces it with a row that lacks the
   modifier and the overlay — and its animation — simply ceases to
   exist. No teardown code needed. */
@keyframes harbour-bar-shimmer {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}

.status-bar--in-flight .status-bar__fill::after,
.status-bar--working .status-bar__fill::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Narrow, soft, low-opacity white band centred in the sweep. */
  background: linear-gradient(
    90deg,
    transparent 40%,
    rgba(255, 255, 255, 0.45) 50%,
    transparent 60%
  );
  animation: harbour-bar-shimmer 1.7s ease-in-out infinite;
  /* --shimmer-delay is the wall-clock phase the board stamps per render
     (Jinja inflight_shimmer_delay) so the sweep stays continuous across
     the 5s whole-pane swap instead of restarting from 0 on each fresh
     node. Unset on the SSE in-flight path (which mutates one node in
     place, never re-creating it) → 0s, unchanged. */
  animation-delay: var(--shimmer-delay, 0s);
  pointer-events: none;
}

.status-bar__label {
  /* Same-hue darker ink. The state's locked hex doubles as the
     label colour, so bar and text carry one coherent state
     signal. Size matches the surrounding row text (size-sm) so
     the Status column feels weighted equally with Contractor +
     Contract in the visual scan. */
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--state-fill, var(--colour-ink-soft));
  letter-spacing: var(--tracking-base);
  line-height: 1.2;
  white-space: nowrap;
}

/* State modifiers — the locked hex tokens wire into the
   --state-fill CSS variable that both stripe and label consume.
   --state-track is kept defined for any future surface (e.g.
   hover tints) without behavioural cost today. */
.status-bar--awaiting {
  --state-fill: var(--status-awaiting-fill);
  --state-track: var(--status-awaiting-track);
}
.status-bar--priced {
  --state-fill: var(--status-priced-fill);
  --state-track: var(--status-priced-track);
}
.status-bar--sent {
  --state-fill: var(--status-sent-fill);
  --state-track: var(--status-sent-track);
}
.status-bar--default {
  --state-fill: var(--status-default-fill);
  --state-track: var(--status-default-track);
}

/* --- terminal mode: tag (Won/Lost) ---------------------------- */

.status-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 10px 4px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--size-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-base);
  line-height: 1.4;
  white-space: nowrap;
}

.status-tag--won {
  background: var(--status-won-bg);
  color: var(--status-won-ink);
}

.status-tag--lost {
  background: var(--status-lost-bg);
  color: var(--status-lost-ink);
}

/* Set aside (decided 2026-05-27): archived rows appear only in the
   All view, rendered as a quiet neutral-grey terminal tag (no icon)
   so the eye reads them as off-the-active-board, distinct from the
   coloured Won/Lost outcomes. */
.status-tag--archived {
  background: var(--pill-neutral-bg);
  color: var(--pill-neutral-ink);
}

.status-tag__icon {
  display: inline-grid;
  place-items: center;
  width: 14px;
  height: 14px;
  border-radius: var(--radius-pill);
  font-size: 10px;
  line-height: 1;
  font-weight: var(--weight-bold);
}

.status-tag--won .status-tag__icon {
  background: rgba(39, 80, 10, 0.14); /* won-ink at 14% — same hue */
}

.status-tag--lost .status-tag__icon {
  background: rgba(121, 31, 31, 0.12); /* lost-ink at 12% — same hue */
}


/* ---- Status dot (on Enquiry no cell) ------------------------ */

.status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
  vertical-align: middle;
  margin-right: var(--space-2);
  flex-shrink: 0;
  /* Subtle inner shadow so the dot reads as a solid token even on
     warm-tinted backgrounds; matches the same lift register the
     dashboard uses elsewhere. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.04);
}

.status-dot--awaiting { background: var(--status-awaiting-fill); }
.status-dot--priced { background: var(--status-priced-fill); }
.status-dot--sent { background: var(--status-sent-fill); }
.status-dot--won { background: var(--status-won-ink); }
.status-dot--lost { background: var(--status-lost-ink); }
.status-dot--archived { background: var(--pill-neutral-ink); }
.status-dot--default { background: var(--colour-ink-muted); }


/* ---- Category cell + tag (column 6) ------------------------- */

.enquiry__cell--category {
  /* Tag-bearing cell, left-aligned. Tight padding inside; the tag
     does the work of carrying the visual weight, the cell wrapper
     stays minimal. */
  overflow: visible;
}

.category-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-base);
  line-height: 1.4;
  background: var(--colour-bg);
  color: var(--colour-ink-soft);
  border: 1px solid var(--colour-line);
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Multiple line-item categories as pills (decided 2026-05-27). Pills
   sit side by side and wrap to a second line within the Category cell
   when they exceed its width; flex-wrap wraps regardless of the
   inherited white-space:nowrap (which keeps each pill on one line). A
   wrap can grow the row past its 64px min-height — the S5 uniform-row
   property deliberately relaxes here for multi-category enquiries. */
.category-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  min-width: 0;
}


/* ---- Low-priority indicator (inline with contractor) ------- */

.low-priority-indicator {
  /* Subdued chip for the Lincoln-pattern signal — informational,
     never a decline-recommendation, so the visual weight stays low:
     same neutral pill palette as the muted nav badge, smaller type,
     slightly desaturated. Lives in the detail panel's signal line
     (2026-06-12; it used to sit in the list row's contractor cell,
     where on contractor-less rows it read as the name). */
  display: inline-flex;
  align-items: center;
  padding: 1px var(--space-2);
  border-radius: var(--radius-sm);
  font-size: 10px;
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  background: var(--pill-neutral-bg);
  color: var(--pill-neutral-ink);
  line-height: 1.4;
  white-space: nowrap;
  vertical-align: middle;
  opacity: 0.85;
}

/* The detail panel's signal line: chip + one muted sentence, sitting
   quietly above the assessment heading. */
.enquiry__low-priority-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
}


/* ---- Category multi-select filter ---------------------------- */

.surface__category {
  /* The <details> wrapper. position:relative so the open panel
     can absolutely-position below the summary without affecting
     the filter bar's flex layout. */
  position: relative;
  display: inline-flex;
}

.surface__category-summary {
  /* Looks like the band-select chrome so the two filters read as
     siblings. The native disclosure marker is suppressed. */
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: inherit;
  font-size: var(--size-sm);
  color: var(--colour-ink);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  padding-right: calc(var(--space-3) + 18px);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a8a8a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 12px;
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.surface__category-summary::-webkit-details-marker {
  display: none;
}

.surface__category-summary::marker {
  content: '';
}

.surface__category-summary:hover {
  border-color: var(--colour-ink-muted);
}

.surface__category-summary:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

/* Open-state caret rotation so the chevron flips to ▲. */
.surface__category[open] .surface__category-summary {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%232a8fad' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m18 15-6-6-6 6'/></svg>");
}

.surface__category-options {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  min-width: 220px;
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-2);
  /* Subtle entry: the panel rises a few pixels into place when
     the user opens the dropdown. Honours the same lift register as
     summary cards. */
  animation: harbour-rise var(--duration-base) var(--ease-out) both;
}

.surface__category-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--size-sm);
  color: var(--colour-ink);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.surface__category-option:hover {
  background: var(--colour-surface-soft);
}

/* Custom checkbox styled to match the dashboard's accent register.
   The native checkbox is hidden but kept accessible (screen
   readers + keyboard focus); the ::before + ::after pseudo-elements
   on the label-wrapped checkbox render the visible state. */
.surface__category-option input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  border: 1px solid var(--colour-line);
  border-radius: 4px;
  background: var(--colour-surface);
  cursor: pointer;
  position: relative;
  flex-shrink: 0;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.surface__category-option input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
}

.surface__category-option input[type="checkbox"]:checked::after {
  /* The check glyph: a small SVG drawn as background-image so the
     stroke colour matches the accent-ink without relying on text
     rendering. */
  content: '';
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 12px;
}

.surface__category-option input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


/* ---- 64px row height accommodation -------------------------- */

/* The status-bar's two-line layout (bar + label) is the tallest
   active-state shape (~30px). The status-tag is single-line
   (~26px). The 64px row min-height set on .enquiry__data above
   gives both shapes generous vertical breathing room; this rule
   ensures the cell content stays vertically centred regardless
   of which shape rendered. */
.enquiry__cell--priority,
.enquiry__cell--status,
.enquiry__cell--category {
  align-self: center;
}


/* ================================================================
   S5c-Item 6 — Priority-confirm modal (inline cell replacement)
   ================================================================

   The confirm modal renders in place of the priority cell when a
   user clicks a star (S5c-Item 6, decided 2026-05-20). The modal
   mediates override against the AI's score + rationale; it is
   NOT a warning ("are you sure you want to do this risky thing"),
   it is a moment of mutual respect — the system states its
   judgement, the human consciously steps in above it. Visual
   register reflects that posture: calm, generous breathing room,
   no alarm colours, the AI's words quoted with care.

   The modal lives at grid-column position of the priority cell
   it replaces and uses absolute positioning to span beyond that
   column's width — it needs room for the rationale prose. The
   surrounding row stays in place; only the cell's content
   changes. ================================================================ */

.priority-confirm {
  /* Absolute overlay anchored BELOW the priority cell, matching the
     .export-dropdown__panel pattern (top: 100% of a position:relative
     trigger). left:0 because the priority cell is column 1 at the far
     left, so the card opens downward from the left edge and stays on
     screen (the mirror of the export panel's right:0, which hugs the
     page's right edge). The cell sets overflow:visible so this overlay
     is not clipped; see the note there. */
  display: block;
  background: var(--colour-surface);
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-4);
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 50;
  /* Fixed compact width (2026-05-27), matching the export panel's
     width discipline (a set width, not a stretchy min/max). The
     previous min-width:320/max-width:480 let the card stretch into a
     wide panel that bled off-screen. 320px is the export popover's own
     pre-widening width and is ample for this shorter content. */
  width: 320px;
  /* Reset the inherited nowrap (2026-05-27): the popover is nested
     inside .enquiry__cell--priority, which sets white-space:nowrap to
     keep the stars on one line. white-space inherits, so without this
     the heading + footer prose stay on one line and overflow the 320px
     box. The .priority-confirm__inline-stars span re-asserts nowrap so
     the inline ★ groups never split. overflow-wrap guards a long
     unbreakable token. */
  white-space: normal;
  overflow-wrap: break-word;
  animation: harbour-rise var(--duration-base) var(--ease-out) both;
}

.priority-confirm__heading {
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
  margin: 0 0 var(--space-3);
  line-height: var(--leading-snug);
}

.priority-confirm__inline-stars {
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
  letter-spacing: 1px;
  white-space: nowrap;
}

.priority-confirm__closing {
  /* Body register, not fine-print (decided 2026-05-27): matches the
     heading's --size-sm so the footer reads comfortably. */
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  margin: 0 0 var(--space-3);
  line-height: var(--leading-relaxed);
}

.priority-confirm__actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
}

.priority-confirm__confirm,
.priority-confirm__cancel {
  font-family: inherit;
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.priority-confirm__confirm {
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid var(--accent);
}

.priority-confirm__confirm:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

.priority-confirm__cancel {
  background: var(--colour-surface);
  color: var(--colour-ink-soft);
  border: 1px solid var(--colour-line);
}

.priority-confirm__cancel:hover {
  color: var(--colour-ink);
  border-color: var(--colour-ink-muted);
}


/* ================================================================
   S5b — Export Data dropdown + Settings page
   ================================================================

   Visual treatment for the auto-export feature decided 2026-05-20.
   The Export Data button lives top-right in the header on every
   authed page, opens as a <details> dropdown, content dispatched by
   the page's `active` variable. The Settings page renders a single
   sparse block (auto-export) per the explicit anti-bloat constraint.
   ================================================================ */

/* ---- Export Data button (header) ---------------------------- */

.export-dropdown {
  position: relative;
  display: inline-flex;
}

.export-dropdown__summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: inherit;
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.export-dropdown__summary::-webkit-details-marker { display: none; }
.export-dropdown__summary::marker { content: ''; }

.export-dropdown__summary:hover {
  border-color: var(--colour-ink-muted);
}

.export-dropdown__summary:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}

.export-dropdown[open] .export-dropdown__summary {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
  background: var(--accent-tint);
  color: var(--accent-strong);
}

.export-dropdown__icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* The dropdown panel — anchored to the right edge of the button
   so it doesn't overflow the viewport on the right. Width-capped
   so the date-range row sits comfortably without forcing the
   panel wider than necessary. */
.export-dropdown__panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  /* Highest floating UI on the page (decided 2026-05-23). The
     prior z-index: 10 lost to .journey-filters' z-index: 20 (added
     during the date-picker rollout so the picker's own popover
     would win against summary-card stacking contexts). 200 sits
     comfortably above everything else on the dashboard: 50 (rail
     header chrome), 50 (.date-range-picker wrapper), 100
     (.date-range-picker__popover when it's NOT nested inside the
     export popover — when it IS nested it inherits this stacking
     context anyway). Future floating UI should sit below 200
     unless it's specifically meant to layer above the export
     popover. */
  z-index: 200;
  /* Widened from 320 → 380 alongside the side-by-side download
     buttons (decided 2026-05-23 update). Two buttons in a row need
     enough horizontal room that neither label wraps or feels
     squeezed; 380 gives each ~170px which sits comfortably for the
     longest realistic tenant name. */
  width: 380px;
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  /* Internal padding bumped from --space-2 → --space-4 (decided
     2026-05-23 update) so the popover reads as a small panel rather
     than a tight box. Summary card uses --space-5 (24px) which would
     feel bloated in a 380px popover; --space-4 (16px) gives clear
     breathing room without wasting the inner width. */
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: 2px;
  animation: harbour-rise var(--duration-base) var(--ease-out) both;
}

.export-dropdown__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--size-sm);
  color: var(--colour-ink);
  text-decoration: none;
  background: transparent;
  border: 0;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.export-dropdown__item:hover {
  background: var(--colour-surface-soft);
}

/* .export-dropdown__item--stub removed 2026-05-23: the per-surface
   stub items it styled (Statistics, Journey snapshot) were dropped
   in the popover restructure. */

.export-dropdown__item-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.export-dropdown__item-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--colour-ink-muted);
}

.export-dropdown__item-label {
  font-weight: var(--weight-medium);
  color: var(--colour-ink-strong);
}

/* Unified export popover form (decided 2026-05-23). Replaces the
   S5c .export-dropdown__enquiries-form + .export-dropdown__rolling-row
   + .export-dropdown__period-btn + .export-dropdown__item-window
   shape with a single flex column: section heading, date-range row,
   two side-by-side primary Download buttons, then the
   include_awaiting checkbox. */
.export-dropdown__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Section heading at the top of the popover (decided 2026-05-23
   update). Same uppercase / muted-ink / wide-tracked register the
   detail panel uses for AI CLASSIFICATION ASSESSMENT — quiet label
   that frames the scope without competing with the inputs below. */
.export-dropdown__section-heading {
  font-size: var(--size-xs);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-muted);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  margin: 0;
}

/* The custom [data-date-range-picker] wrapper inside the export
   popover uses the same component as /enquiries' time-window
   control (one picker, every surface). Layout overrides here let
   the picker fit the 320px popover width: from/to fields each
   1fr, dash between, full-width row. */
.date-range-picker--in-export {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: var(--space-2);
  align-items: center;
  width: 100%;
}

.date-range-picker--in-export .date-range-picker__field {
  display: flex;
  min-width: 0; /* let the field shrink inside the grid cell */
}

/* The picker popover defaults to left: 0 (anchored to the wrapper's
   left edge). Inside the export popover, which itself sits at
   right: 0 of the page, that default would overflow the viewport's
   right edge by ~340px. Anchor the picker popover to the right
   edge of its wrapper here so it grows leftward into the body of
   the page instead — same component, contextual positioning. */
.date-range-picker--in-export .date-range-picker__popover {
  left: auto;
  right: 0;
}

.export-dropdown__date {
  font-family: inherit;
  font-size: var(--size-xs);
  color: var(--colour-ink);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-sm);
  padding: 4px var(--space-2);
  width: 100%;
  min-width: 0;
}

.export-dropdown__date:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-tint);
}

.export-dropdown__date-dash {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
}

/* Two Download buttons sit side-by-side in a grid row (decided
   2026-05-23 update). Equal width via 1fr 1fr. When only one
   button renders (no tenant display_name configured), the single
   button fills the first cell at half-width — visually consistent
   with the two-button case but reads as a single option, not a
   choice. */
.export-dropdown__download-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
}

/* Primary Download button. Two side-by-side instances in the row:
   Default layout, then tenant layout (when branding.display_name
   is set). Icon + label aligned tight. */
.export-dropdown__download {
  font-family: inherit;
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid var(--accent);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}

.export-dropdown__download:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
  transform: translateY(-1px);
}

.export-dropdown__download-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* Include-awaiting checkbox. Sits BELOW the download buttons in
   the new shape (decided 2026-05-23) as a quiet scope modifier;
   the clicked submit captures its value at submit time regardless
   of visual order. Default-checked so the export is the most-
   complete snapshot unless the user opts out. */
.export-dropdown__include {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--size-xs);
  color: var(--colour-ink-soft);
  cursor: pointer;
}

.export-dropdown__include input[type="checkbox"] {
  width: 14px;
  height: 14px;
  accent-color: var(--accent);
}

.export-dropdown__separator {
  height: 1px;
  background: var(--colour-line-soft);
  /* Horizontal margin zeroed when the panel padding bumped to
     --space-4 (decided 2026-05-23 update). The previous --space-2
     inset stacked with the panel padding to push the separator
     24px from each edge — too inset against the panel boundary.
     Now the line spans the full padded width, sitting flush with
     the form content above it. Vertical margin stays for breathing
     room above and below. */
  margin: var(--space-2) 0;
}

.export-dropdown__item--meta .export-dropdown__item-label {
  color: var(--colour-ink-soft);
  font-weight: var(--weight-regular);
}


/* ---- Settings page (tabbed panel) --------------------------- */

/* The Settings surface is a tabbed panel: an icon-tab strip over a
   per-domain content area (decided 2026-05-28). The strip rides a
   hairline baseline; the active tab is marked by a single accent
   underline sitting on that baseline, with its ink lifting from soft to
   strong and its icon picking up the accent. Inactive tabs stay neutral
   and take a quiet surface-soft fill on hover. One accent, used once —
   §7.1. */
.settings-tabs {
  display: flex;
  gap: var(--space-1);
  border-bottom: 1px solid var(--colour-line);
  margin-bottom: var(--space-5);
}

.settings-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  margin-bottom: -1px; /* overlap the strip baseline so the underline rides it */
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-soft);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  border-top-left-radius: var(--radius-sm);
  border-top-right-radius: var(--radius-sm);
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.settings-tab:hover {
  color: var(--colour-ink);
  background: var(--colour-surface-soft);
}

.settings-tab__icon {
  width: 17px;
  height: 17px;
  flex-shrink: 0;
  opacity: 0.8;
}

.settings-tab--active {
  color: var(--colour-ink-strong);
  font-weight: var(--weight-semibold);
  border-bottom-color: var(--accent);
}

.settings-tab--active:hover {
  background: transparent; /* the active tab doesn't take the hover fill */
}

.settings-tab--active .settings-tab__icon {
  opacity: 1;
  color: var(--accent-strong);
}

/* Honest placeholder for a domain whose content has not landed yet
   (checkpoint-staged build). Calm, centred, muted — shows the slot
   exists without faking populated state. */
.settings-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: var(--space-8) var(--space-5);
  text-align: center;
}

.settings-placeholder__icon {
  width: 28px;
  height: 28px;
  color: var(--colour-ink-muted);
  opacity: 0.7;
}

.settings-placeholder__text {
  font-size: var(--size-sm);
  color: var(--colour-ink-muted);
  margin: 0;
}

.settings__body {
  padding: var(--space-5);
}

.settings-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 540px;
}

.settings-block__heading {
  font-size: var(--size-md);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
  letter-spacing: var(--tracking-tight);
  margin: 0;
}

.settings-block__note {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  line-height: var(--leading-relaxed);
  margin: 0;
  padding-top: var(--space-2);
  border-top: 1px solid var(--colour-line-soft);
  max-width: 64ch;
}

.settings-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.settings-row--column {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
}

.settings-row__label {
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
  min-width: 110px;
}

/* Toggle — visual on/off control. Inert for the S5b demo (the
   underlying input is disabled). The track + thumb render the
   off-state regardless of checked. */
.settings-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  cursor: not-allowed;
}

.settings-toggle input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.settings-toggle__track {
  display: inline-block;
  width: 36px;
  height: 20px;
  background: var(--colour-line);
  border-radius: var(--radius-pill);
  position: relative;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.settings-toggle__track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  background: var(--colour-surface);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-xs);
  transition: transform var(--duration-fast) var(--ease-out);
}

.settings-toggle__label {
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
}

/* Segmented choice — Weekly / Monthly. The active option carries
   accent-tint background; the other is muted. Inert for demo
   (radios disabled). */
.settings-segmented {
  display: inline-flex;
  background: var(--colour-bg);
  border-radius: var(--radius-pill);
  padding: 2px;
  border: 1px solid var(--colour-line);
}

.settings-segmented__option {
  display: inline-flex;
  align-items: center;
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  padding: 4px var(--space-3);
  border-radius: var(--radius-pill);
  cursor: not-allowed;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.settings-segmented__option--active {
  background: var(--colour-surface);
  color: var(--accent-strong);
  box-shadow: var(--shadow-xs);
}

.settings-segmented__option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.settings-input {
  font-family: inherit;
  font-size: var(--size-sm);
  color: var(--colour-ink);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  width: 100%;
}

/* Placeholder styling — fields render in greyed/placeholder tone
   so the demo doesn't imply configured state that isn't true. The
   `disabled` attribute on the input is the honest signal; this
   styling is the visual reinforcement. Per S5b honesty discipline:
   show capability, don't lie about state. */
.settings-input--placeholder {
  color: var(--colour-ink-muted);
  background: var(--colour-bg);
  border-style: dashed;
}

.settings-input--placeholder::placeholder {
  color: var(--colour-ink-muted);
  opacity: 1;
}


/* ---- Product management (CP4) ------------------------------- */

/* Category strip: cards over the product table. The active card carries
   a soft accent outline (follows the tenant accent, green for WPL). The
   trailing dotted card is the inert Add-category placeholder (CP5). */
.pstrip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.pcat {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 140px;
  padding: var(--space-3) var(--space-4);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  text-decoration: none;
  color: var(--colour-ink);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}

.pcat:hover {
  box-shadow: var(--shadow-sm);
  transform: translateY(-1px);
}

.pcat--active {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), var(--shadow-sm);
}

.pcat__name {
  font-size: var(--size-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
}

.pcat__count {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
}

.pcat--add {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: transparent;
  border-style: dashed;
  color: var(--colour-ink-muted);
  box-shadow: none;
}

.pcat--add[disabled] {
  cursor: not-allowed;
  opacity: 0.8;
}

.pcat--add:hover {
  transform: none;
  box-shadow: none;
}

.pcat__add-mark {
  font-size: var(--size-md);
  line-height: 1;
}

/* Meta line below the table heading. */
.ptable__meta {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
  margin: 0 0 var(--space-3);
}

/* Horizontal-scroll wrapper: wide schemas scroll, the identity column
   stays put via position:sticky on its cells. */
.ptable-wrap {
  overflow-x: auto;
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
}

.ptable {
  border-collapse: collapse;
  width: max-content;
  min-width: 100%;
  font-size: var(--size-sm);
}

.ptable__th {
  background: var(--colour-surface-soft);
  color: var(--colour-ink-soft);
  font-weight: var(--weight-semibold);
  font-size: var(--size-xs);
  text-align: left;
  white-space: nowrap;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--colour-line);
}

.ptable__th--num {
  text-align: right;
}

.ptable__unit {
  color: var(--colour-ink-muted);
  font-weight: var(--weight-regular);
}

/* The identity column is sticky-left (sticky on horizontal scroll only;
   vertical-sticky headers misbehave inside a horizontal-scroll box). An
   opaque background lets scrolled cells pass behind it; a hairline seam
   marks the edge. The header corner stacks above the body cells. */
.ptable__th--identity {
  position: sticky;
  left: 0;
  z-index: 2;
  box-shadow: 1px 0 0 var(--colour-line);
}

.pcell {
  padding: 0;
  border-bottom: 1px solid var(--colour-line-soft);
  background: var(--colour-surface);
  vertical-align: middle;
}

.ptable__row:hover .pcell {
  background: var(--colour-surface-soft);
}

.pcell--identity {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--colour-surface);
  box-shadow: 1px 0 0 var(--colour-line);
  font-weight: var(--weight-medium);
}

.ptable__row:hover .pcell--identity {
  background: var(--colour-surface-soft);
}

/* Editors: borderless until hover/focus, so the grid reads calm but
   every cell is live (the spreadsheet register). */
.pcell__editor {
  font-family: inherit;
  font-size: var(--size-sm);
  color: var(--colour-ink);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  width: 100%;
  min-width: 7ch;
  transition: border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.pcell__editor:hover {
  border-color: var(--colour-line);
}

.pcell__editor:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--colour-surface);
  box-shadow: 0 0 0 2px var(--accent-tint);
}

.pcell__input--number {
  text-align: right;
}

.pcell__numwrap {
  display: flex;
  align-items: center;
}

.pcell__unit {
  color: var(--colour-ink-muted);
  font-size: var(--size-xs);
  padding-right: var(--space-3);
  white-space: nowrap;
}

.pcell__notes {
  resize: vertical;
  min-height: 2.2em;
  line-height: var(--leading-snug);
  min-width: 18ch;
}

/* Status pill editor: the existing .pill component, made a button. */
.pcell__pill {
  border: none;
  cursor: pointer;
  margin: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--size-xs);
}

.pill--neutral {
  background: var(--pill-neutral-bg);
  color: var(--pill-neutral-ink);
}

/* Save-confirmation flash: a brief accent wash on the swapped-in cell. */
.pcell--flash {
  animation: harbour-cell-flash var(--duration-slow) var(--ease-out);
}

@keyframes harbour-cell-flash {
  0% { background: var(--accent-tint); }
  100% { background: var(--colour-surface); }
}

/* Rejected save: soft-red border on the reverted cell. */
.pcell--error .pcell__editor,
.pcell--error .pcell__pill {
  border: 1px solid var(--status-lost-ink);
  box-shadow: 0 0 0 2px var(--status-lost-bg);
}

.pcell__msg {
  display: block;
  padding: var(--space-2) var(--space-3);
  font-size: var(--size-xs);
  color: var(--status-lost-ink);
}

/* Add-product row, below the table. */
.ptable__add {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-4);
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--accent-strong);
  background: transparent;
  border: 1px dashed var(--colour-line);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}

.ptable__add:hover {
  background: var(--accent-tint);
  border-color: var(--accent);
}

.ptable__add-mark {
  font-size: var(--size-md);
  line-height: 1;
}

/* Ghost button: the bare-category empty-state placeholder (inert CP4). */
.btn-ghost {
  padding: var(--space-2) var(--space-4);
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-soft);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  cursor: pointer;
}

.btn-ghost[disabled] {
  cursor: not-allowed;
  opacity: 0.7;
}

@media (prefers-reduced-motion: reduce) {
  .pcell--flash { animation: none; }
}


/* ---- Product management: row actions / kebab (CP-delete) ---- */

/* Room below the table so a last-row kebab popover has space inside the
   horizontal-scroll box (overflow-x:auto forces overflow-y:auto, which
   would otherwise clip a popover dropping past the last row). */
.ptable-wrap {
  padding-bottom: var(--space-9);
}

/* Sticky-right actions column, mirroring the sticky-left identity column
   on the opposite edge: opaque background so scrolled cells pass behind,
   the seam shadow flipped to the left edge, width hugging the kebab
   (icon + padding tokens, no fixed number). */
.ptable__th--actions {
  position: sticky;
  right: 0;
  z-index: 2;
  width: 1%;
  white-space: nowrap;
  box-shadow: -1px 0 0 var(--colour-line);
}

.pcell--actions {
  position: sticky;
  right: 0;
  z-index: 1;
  width: 1%;
  padding: 0 var(--space-1);
  text-align: center;
  background: var(--colour-surface);
  box-shadow: -1px 0 0 var(--colour-line);
}

.ptable__row:hover .pcell--actions {
  background: var(--colour-surface-soft);
}

/* Kebab: discoverable at rest (0.4 opacity), full on row hover / focus /
   open. Native <details>, so the popover needs no JS. */
.pkebab {
  display: inline-block;
  position: relative;
}

.pkebab__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--colour-ink-soft);
  cursor: pointer;
  opacity: 0.4;
  list-style: none;
  transition: opacity var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.pkebab__trigger::-webkit-details-marker {
  display: none;
}

.ptable__row:hover .pkebab__trigger,
.pkebab[open] .pkebab__trigger,
.pkebab__trigger:focus-visible {
  opacity: 1;
}

.pkebab__trigger:hover {
  background: var(--colour-surface-soft);
}

.pkebab__icon {
  width: 16px;
  height: 16px;
}

/* Popover: dropped below the kebab, right-aligned so it does not spill
   off the sticky column's right edge. */
.pkebab__menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  min-width: 11rem;
  padding: var(--space-1);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  text-align: left;
}

/* Flipped upward by kebab_popover.js when opening down would clip at the
   bottom of the horizontal-scroll box. */
.pkebab--up .pkebab__menu {
  top: auto;
  bottom: calc(100% + var(--space-1));
}

.pkebab__item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--size-sm);
  color: var(--colour-ink);
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  white-space: nowrap;
}

.pkebab__item:hover {
  background: var(--colour-surface-soft);
}

.pkebab__item--danger {
  color: var(--status-lost-ink);
}

.pkebab__item--danger:hover {
  background: var(--status-lost-bg);
}

.pkebab__item--disabled {
  color: var(--colour-ink-muted);
  cursor: not-allowed;
}

.pkebab__item--disabled:hover {
  background: transparent;
}

.pkebab__item--cancel {
  display: inline-flex;
  width: auto;
  color: var(--colour-ink-muted);
  font-size: var(--size-md);
  line-height: 1;
  padding: var(--space-1) var(--space-2);
}

.pkebab__divider {
  height: 1px;
  margin: var(--space-1) 0;
  background: var(--colour-line-soft);
}

.pdel {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

/* A deprecated product reads muted in the grid (the lifecycle status made
   visible beyond the kebab label). */
.ptable__row--deprecated .pcell__editor,
.ptable__row--deprecated .pcell__pill {
  opacity: 0.55;
}

/* The 409 delete-refusal message: inline, aria-live, above the table. The
   kebab disables delete when a product is referenced, so this is the
   server-authority backstop for a race or a hand-crafted request. */
.pm-message:empty {
  display: none;
}

.pmsg--refusal {
  display: block;
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  font-size: var(--size-sm);
  color: var(--status-lost-ink);
  background: var(--status-lost-bg);
  border-radius: var(--radius-md);
}


/* ================================================================
   S5b — Journey Kanban surface
   ================================================================

   Four-column Kanban with the client-language column headers (§7.2
   forward markers). Decided 2026-05-20, supersedes the staged-
   progress-indicator framing for the Journey view. Visual register
   matches the existing dashboard voice — soft rounded corners,
   subtle lift-on-hover, status colour vocabulary shared with the
   Enquiries surface.
   ================================================================ */

/* ---- Time-window filter bar above the board ---------------- */

.journey-filters {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-5);
  padding: var(--space-3) var(--space-4);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xs);
  /* Stacking context for the date-range picker popover (decided
     2026-05-23). Without this the popover loses to the summary-card
     row that follows — .summary-card.reveal has transform: translateY(0)
     after the page-load animation finishes, which creates its own
     stacking context, and a deeper z-index inside .journey-filters
     would only compete inside .journey-filters' own context. Lifting
     the entire control wins at the level the comparison actually
     happens. */
  position: relative;
  z-index: 20;
}

.journey-filters__toggle {
  display: inline-flex;
  background: var(--colour-bg);
  border-radius: var(--radius-pill);
  padding: 2px;
  border: 1px solid var(--colour-line);
}

.journey-filters__period {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  padding: 4px var(--space-3);
  min-width: 56px;
  text-align: center;
  border-radius: var(--radius-pill);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.journey-filters__period:hover {
  color: var(--colour-ink);
}

.journey-filters__period--active {
  background: var(--colour-surface);
  color: var(--accent-strong);
  box-shadow: var(--shadow-xs);
}

.journey-filters__custom {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 280px;
}

.journey-filters__custom-label {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
}

.journey-filters__date {
  font-family: inherit;
  font-size: var(--size-xs);
  color: var(--colour-ink);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-sm);
  padding: 4px var(--space-2);
}

.journey-filters__date:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-tint);
}

.journey-filters__custom-dash {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
}

/* Date-range picker wrapper (decided 2026-05-23).
   Wraps the from/to inputs + dash into one anchor element so the
   popover (added in the picker JS) can position itself relative to
   the field group rather than to one individual input. Preserves
   the flex flow the inline inputs used to have so the
   pre-checkpoint visual is unchanged when JS is absent.
   The .date-range-picker__field wrappers each hold a visible text
   input plus its hidden ISO sibling. Picker chrome lives below in
   the "Date-range picker popover" section. */
.date-range-picker {
  position: relative;
  /* z-index lifts the picker into its own stacking context so the
     popover wins against the summary-card row that follows it in the
     DOM. Without this, .summary-card siblings render on top of the
     absolutely-positioned popover (no z-index parent → painter's
     algorithm by DOM order, later wins). */
  z-index: 50;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.date-range-picker__field {
  display: inline-flex;
}

/* ---- Date-range picker popover (decided 2026-05-23) -------------
   Hand-rolled custom picker per the brief. Visual register matches
   the summary cards: warm white surface, hairline border, no heavy
   shadow. --accent (WPL green under wpluk overrides; --accent for
   WSI default) drives the selection + range tints. Picker DOM is
   built in date_range_picker.js; this stylesheet owns every pixel
   so we never inherit system-styled chrome. */

.date-range-picker__popover {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: 100;
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-4);
  font-family: var(--brand-body-font);
  user-select: none;
}

.date-range-picker__popover[hidden] {
  display: none;
}

.date-range-picker__months {
  display: flex;
  gap: var(--space-5);
}

.date-range-picker__month {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 224px;
}

.date-range-picker__month-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--space-1);
  min-height: 28px;
}

.date-range-picker__month-title {
  font-size: var(--size-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
  letter-spacing: var(--tracking-tight);
}

.date-range-picker__nav-arrow {
  width: 24px;
  height: 24px;
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  color: var(--colour-ink-soft);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.date-range-picker__nav-arrow:hover {
  background: var(--colour-line-soft);
  color: var(--colour-ink);
}

.date-range-picker__nav-arrow:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--accent-tint);
}

/* Spacer keeps the title visually centred within each month-header
   when the opposite side is the nav arrow's neighbour rather than an
   arrow itself (only one arrow per month-pair, far edges). */
.date-range-picker__nav-spacer {
  width: 24px;
  height: 24px;
}

.date-range-picker__weekdays,
.date-range-picker__grid {
  display: grid;
  grid-template-columns: repeat(7, 32px);
  gap: 2px;
}

.date-range-picker__weekday {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
  text-align: center;
  padding: var(--space-1) 0;
  font-weight: var(--weight-medium);
}

.date-range-picker__day {
  width: 32px;
  height: 32px;
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: var(--size-sm);
  color: var(--colour-ink);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-variant-numeric: tabular-nums;
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.date-range-picker__day--blank {
  cursor: default;
  pointer-events: none;
}

/* Hover preview for empty (non-range) cells. Specificity matters:
   the `:not(.day--blank)` we'd otherwise add for clarity bumps the
   rule to (0,0,3,0), which beats the (0,0,1,0) range-end fill and
   leaves an in-range-tinted endpoint when the mouse rests on it.
   Blank cells set `pointer-events: none` so :hover never fires on
   them anyway — the simpler (0,0,2,0) selector is both safer and
   correct. */
.date-range-picker__day:hover {
  background: var(--accent-tint);
  color: var(--accent-strong);
}

.date-range-picker__day--today {
  font-weight: var(--weight-semibold);
  /* Hairline ring around today so it's findable without competing
     with the accent selection chrome. */
  box-shadow: inset 0 0 0 1px var(--colour-line);
}

/* In-range cells get a soft translucent accent fill. Live preview
   uses the same tint as committed range so the eye reads one
   selection state, not two. The endpoints sit on top with the solid
   accent fill. */
.date-range-picker__day--in-range {
  background: var(--accent-tint);
  color: var(--accent-strong);
  border-radius: 0;
}

/* Connect the endpoint pills to the in-range fill: round only the
   outer corners so the range reads as one continuous bar. */
.date-range-picker__day--range-start {
  background: var(--accent);
  color: var(--accent-ink);
  border-top-left-radius: var(--radius-sm);
  border-bottom-left-radius: var(--radius-sm);
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.date-range-picker__day--range-end {
  background: var(--accent);
  color: var(--accent-ink);
  border-top-right-radius: var(--radius-sm);
  border-bottom-right-radius: var(--radius-sm);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

/* When start == end (single-day range or solo anchor mid-pick) both
   classes land on the same cell; the rule below restores the full
   pill rounding so it doesn't render as half a bar. */
.date-range-picker__day--range-start.date-range-picker__day--range-end {
  border-radius: var(--radius-sm);
}

/* Anchor-only state (pending first click, no hover yet): the
   range-start cell sits alone so it should be a full pill, not a
   square-on-the-right. The CSS above already handles this via the
   single-class case — no extra rule needed. */

.date-range-picker__day--range-start:hover,
.date-range-picker__day--range-end:hover {
  background: var(--accent-strong);
  color: var(--accent-ink);
}

.date-range-picker__footer {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--colour-line-soft);
}

.date-range-picker__clear {
  font-family: inherit;
  font-size: var(--size-xs);
  color: var(--colour-ink-soft);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  transition: background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.date-range-picker__clear:hover {
  background: var(--colour-line-soft);
  color: var(--colour-ink);
}

/* Nested-mode footer (decided 2026-05-23 update). When the picker
   sits inside another popover (data-date-range-picker="nested"),
   the footer carries an additional Apply button: Clear left-aligned
   (quieter secondary action), Apply right-aligned (primary green
   commit-and-close). Footer alignment flips from flex-end to
   space-between to put one button at each edge. Standalone usage
   (default mode) keeps the existing single-button right-aligned
   layout. */
.date-range-picker[data-date-range-picker="nested"] .date-range-picker__footer {
  justify-content: space-between;
}

.date-range-picker__apply {
  font-family: inherit;
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--accent-ink);
  background: var(--accent);
  border: 1px solid var(--accent);
  cursor: pointer;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              transform var(--duration-fast) var(--ease-out);
}

.date-range-picker__apply:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
  transform: translateY(-1px);
}

/* The visible inputs are readonly text fields — drop the native
   text-cursor caret since clicking opens a picker, not a typing
   surface. Keep the focus ring (inherited from .journey-filters__
   date:focus). */
.date-range-picker__input {
  cursor: pointer;
  caret-color: transparent;
}

.journey-filters__apply {
  font-family: inherit;
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  padding: 4px var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--colour-surface);
  color: var(--colour-ink);
  border: 1px solid var(--colour-line);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out);
}

.journey-filters__apply:hover {
  border-color: var(--colour-ink-muted);
}

.journey-filters__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
}

.journey-filters__window {
  font-weight: var(--weight-medium);
  color: var(--colour-ink);
}

/* ---- The Kanban board itself --------------------------------- */

.journey-board {
  display: grid;
  grid-template-columns: repeat(3, minmax(260px, 1fr));
  gap: var(--space-4);
  /* Allow the three columns to scroll horizontally if the viewport
     can't fit them all comfortably. Kanban + narrow viewport is
     always a tension; horizontal scroll is the natural escape
     hatch rather than stacking columns vertically (which defeats
     the across-pipeline-glance point of a board). Wider min-width
     than the prior four-column shape since each column now has
     more breathing room to take. */
  overflow-x: auto;
  padding-bottom: var(--space-3);
  align-items: start;
}

.journey-column {
  background: var(--colour-surface-soft);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-height: 220px;
}

.journey-column__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 0 var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--colour-line);
}

.journey-column__title {
  font-family: var(--brand-display-font);
  font-size: var(--size-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
  letter-spacing: var(--tracking-tight);
  line-height: 1.3;
}

.journey-column__count {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  min-width: 24px;
  text-align: right;
}

.journey-column__cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.journey-column__empty {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
  text-align: center;
  padding: var(--space-5) var(--space-2);
  font-style: italic;
}

/* ---- Cards in the Kanban ------------------------------------- */

.journey-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-left: 3px solid var(--state-fill, var(--colour-ink-muted));
  border-radius: var(--radius-md);
  padding: var(--space-3);
  box-shadow: var(--shadow-xs);
  transition: transform var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.journey-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}

/* S5c (decided 2026-05-20): the card root is a <details> element
   so click expands to reveal the shared detail-panel macro. The
   <summary> is the existing card-head/contract/foot block (now
   the click target). Open state extends the card downward
   without affecting other columns — independent column heights
   are the native Kanban shape per Marcus's no-exclusive-expand
   decision. */

.journey-card__summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.journey-card__summary::-webkit-details-marker {
  display: none;
}

.journey-card__summary::marker {
  content: '';
}

.journey-card[open] {
  /* Subtle visual cue that the card is open. Same lifted-surface
     register as the hover state but persistent while open. */
  box-shadow: var(--shadow-sm);
}

.journey-card__detail {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--colour-line-soft);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* S5d fix (decided 2026-05-20): the detail-panel macro is shared
   with the Enquiries rowlist, which uses --size-base for the
   assessment note prose — too large for the denser Journey card
   context where the rest of the card is --size-xs/--size-sm. Scope
   the macro's font sizes DOWN when rendered inside a journey card,
   matching the card's own register. The Enquiries rowlist's
   detail-panel scale is untouched. */
/* S5e (decided 2026-05-20): the shared on-hold-reason callout is
   a loud orange banner on /pricing-input where it earns its
   weight (the reason IS the row's primary scan signal there). On
   a Journey card it was visually massive next to the rest of the
   card's text — Marcus's correction. Override the callout chrome
   inside a journey card: tight inline label + one-line reason,
   no banner background, no border-left stripe, type scale matches
   the card's other text. The pricing-input surface keeps the
   loud banner; the journey-card surface gets the proportionate
   version. §2f relevance-uncertain rows no longer reach the
   Journey board at all (filter in _bucket_into_columns above), so
   this rule only ever applies to §4e pricing-input cards in the
   Input-needed column. */
.journey-card .enquiry__on-hold-reason {
  display: block;
  margin: 0;
  padding: 0;
  background: transparent;
  border: 0;
  font-size: var(--size-sm);
  line-height: var(--leading-snug);
}

.journey-card .enquiry__on-hold-reason-label {
  display: inline;
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--size-2xs, 0.7rem);
  font-weight: var(--weight-semibold);
  margin-right: var(--space-2);
}

.journey-card .enquiry__on-hold-reason-text {
  font-weight: var(--weight-regular);
  color: var(--colour-ink-soft);
}

/* The detail-panel section headings (AI CLASSIFICATION ASSESSMENT,
   PRICE BREAKDOWN) inherit the global .enquiry__detail-heading
   --size-sm (2026-05-27): the prior 10px journey override sat BELOW
   the --size-sm body prose and POSTCODE label in the same card, an
   inverted hierarchy. No journey-scoped size now; small-caps / muted /
   tracking come from the shared class. */

/* S5e (decided 2026-05-20): journey-card detail-panel text bumped
   one notch in line with the summary-text bump on the cards
   themselves. xs→sm so the expanded panel reads at the same body
   scale as the card summary, not noticeably smaller. */
.journey-card__detail .enquiry__detail-note {
  font-size: var(--size-sm);
  line-height: var(--leading-snug);
}

.journey-card__detail .enquiry__detail-reason,
.journey-card__detail .enquiry__detail-comments {
  font-size: var(--size-sm);
}

.journey-card__detail .enquiry__detail-field dt,
.journey-card__detail .enquiry__detail-field dd {
  font-size: var(--size-sm);
}

/* Parity reopening (decided 2026-05-27): the journey-shrunk action
   buttons (was font-size:10px / 2px padding) are gone. The expanded
   Journey card now carries the SAME action set as the Enquiries
   expanded view, so the buttons inherit the default .enquiry__action
   treatment unchanged — one rendering, two surfaces. Only the Open
   deep-link keeps a bespoke quiet register, below. */

/* The "Open in Enquiries view" link sits on its own row at the bottom
   of the expanded detail, below the action buttons. flex-basis:100%
   forces it onto a fresh line in the wrapped .enquiry__actions flow.
   Small text action, not a primary CTA — the user already expanded the
   card in-place; this is the navigate-away path for when they want it. */
.journey-card__detail .journey-card__open {
  flex-basis: 100%;
  margin-top: var(--space-1);
}

.journey-card__detail .enquiry__action--meta {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: transparent;
  text-decoration: underline;
  text-decoration-color: var(--colour-line);
}

.journey-card__detail .enquiry__action--meta:hover {
  color: var(--accent-strong);
  text-decoration-color: var(--accent);
  background: transparent;
  border-color: transparent;
}

/* State-coloured left border. Same colour vocabulary as the
   status-bar stripe + status-dot — across the dashboard, the
   left-edge state marker is the consistent visual signal for
   "where this row sits in the pipeline." */
.journey-card--awaiting { --state-fill: var(--status-awaiting-fill); }
.journey-card--priced { --state-fill: var(--status-priced-fill); }
.journey-card--sent { --state-fill: var(--status-sent-fill); }
.journey-card--won { --state-fill: var(--status-won-ink); }
.journey-card--lost { --state-fill: var(--status-lost-ink); }
.journey-card--default { --state-fill: var(--colour-ink-muted); }

.journey-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

/* S5e (decided 2026-05-20): card body type bumped one notch on
   each line — Marcus's correction during review that the cards
   read too small for comfortable scanning. Contractor moves
   sm→base (the primary scan-target on the card); contract moves
   xs→sm so the sub-line reads without squinting but stays
   subordinate to the contractor. Density stays usable because
   the column-width budget is fixed and the lines remain single-
   purpose. */
.journey-card__contractor {
  font-size: var(--size-base);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}

.journey-card__priority {
  font-size: var(--size-sm);
  color: var(--colour-ink-muted);
  letter-spacing: 1px;
  flex-shrink: 0;
}

.journey-card__contract {
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.journey-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding-top: var(--space-1);
  border-top: 1px solid var(--colour-line-soft);
}

.journey-card__value {
  font-size: var(--size-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-strong);
  font-variant-numeric: tabular-nums;
}

.journey-card__pending-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  font-size: 10px;
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  background: var(--pill-neutral-bg);
  color: var(--pill-neutral-ink);
  line-height: 1.4;
}

/* The status-tag from the Enquiries surface (Won/Lost) reuses
   here on the Sent column's cards. The existing .status-tag
   rules apply unchanged; this rule narrows their default
   spacing inside a card context. */
.journey-card .status-tag {
  font-size: 10px;
  padding: 2px var(--space-2) 2px 6px;
  line-height: 1.4;
}

.journey-card .status-tag__icon {
  width: 12px;
  height: 12px;
  font-size: 9px;
}


/* ---- Narrow-screen fallback --------------------------------- */

/* Per S5 brief §6 width watch-point: on screens narrower than
   1280px the 10-column grid risks horizontal scroll. The brief
   names Quoted by as the first column to drop. This media query
   hides Quoted by below 1280px so the remaining 9 columns
   continue to fit without forcing scroll. The data stays in the
   detail panel via row expansion — nothing is lost. */
@media (max-width: 1280px) {
  /* Drop Quoted by (the 10th cell) on the head + data rows, then
     redefine the canonical template to its 9-column variant in ONE
     place. Every consumer of var(--enquiry-grid) follows: head, data
     rows, contract-card head, breakdown card. The 1/7 and 7/-1 spans
     in the breakdown hold in both the 10-col and 9-col templates. */
  .rowlist__head > :nth-child(10),
  .enquiry__data > :nth-child(10) {
    display: none;
  }
  .rowlist {
    --enquiry-grid:
      minmax(98px, 0.55fr)
      minmax(150px, 0.85fr)  /* Status — trimmed (see base grid) */
      minmax(124px, 0.75fr)
      minmax(150px, 1.05fr)
      minmax(170px, 1.25fr)
      minmax(150px, 0.85fr)  /* Category — widened for multi-pills */
      minmax(96px, 0.55fr)
      minmax(68px, 0.4fr)
      minmax(68px, 0.4fr);
  }
}


/* ================================================================
   S7 — Catalogue schema editor (CP5)
   ================================================================

   One modal serves three entry points (Add-category card, bare-state
   Add-attributes button, +Add-attribute affordance on a populated
   table). The editable rows are always NEW attributes; existing ones
   show read-only in edit mode — the UI is additive-only, the storage
   guard is the rail.
   ================================================================ */

/* Meta line + +Add-attribute affordance on the populated table. The
   affordance sits opposite the meta on the same baseline so it reads
   as a quiet edit on the schema, not a primary action competing with
   Add product below the table. */
.ptable__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin: 0 0 var(--space-3);
}

.ptable__head .ptable__meta {
  margin: 0; /* head wrapper owns the bottom gap */
}

.ptable__add-attr {
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.ptable__add-attr:hover,
.ptable__add-attr:focus-visible {
  color: var(--accent-strong);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ---- Modal scaffold -------------------------------------------- */

/* Full-viewport scrim. z-index sits above the highest in-page popover
   (export popover at 200) so the modal can't be overlapped. The card
   inside is centered via flex. #modal-slot lives at body level (in
   base.html, just before </body>) — when the slot was nested inside
   #settings-panel, transformed ancestors (the reveal animations,
   .surface) captured the position:fixed and the scrim only dimmed the
   content region, leaving the nav rail at full brightness. Body-level
   placement gives the scrim a clean path to the viewport. The scrim
   colour is tokenised (--colour-scrim) — first modal primitive, every
   future modal inherits it. */
.modal-backdrop {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--colour-scrim);
  /* Dim + blur travel together (2026-06-12): the shared overlay
     treatment, one token pair for every modal on the board. */
  -webkit-backdrop-filter: blur(var(--scrim-blur));
  backdrop-filter: blur(var(--scrim-blur));
  z-index: 1000;
}

.modal-card {
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-5);
  width: min(560px, 100%);
  max-height: calc(100vh - 2 * var(--space-4));
  overflow-y: auto;
}

.modal-card__title {
  margin: 0 0 var(--space-4);
  font-size: var(--size-md);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
}

/* Narrow variant for small confirmations (the set-aside reasons). */
.modal-card--narrow {
  width: min(400px, 100%);
}

/* Optional header row: title left, quiet close-X right (the Emma
   close register, in the light-mode vocabulary). When the head is
   used, the title's own bottom margin moves to the head. */
.modal-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.modal-card__head .modal-card__title {
  margin: 0;
}

.modal-card__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--colour-ink-soft);
  cursor: pointer;
}

.modal-card__close:hover,
.modal-card__close:focus-visible {
  color: var(--colour-ink);
  background: var(--colour-line-soft, var(--colour-line));
}

.modal-card__close svg {
  width: 16px;
  height: 16px;
}

.modal-card__error {
  margin: 0 0 var(--space-4);
  padding: var(--space-2) var(--space-3);
  font-size: var(--size-sm);
  color: var(--status-lost-ink);
  background: var(--status-lost-bg);
  border-radius: var(--radius-md);
}

.modal-card__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

/* ---- Modal fields & read-only existing block ------------------- */

.modal-field {
  display: block;
  margin: 0 0 var(--space-4);
}

.modal-field__label {
  display: block;
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  margin-bottom: var(--space-1);
}

.modal-field__readonly {
  margin: 0 0 var(--space-4);
  font-size: var(--size-sm);
  color: var(--colour-ink-soft);
}

/* Read-only existing-attributes block. With the body-level scrim
   dimming the whole dashboard around the modal, the modal-card edge
   plus the scrim do the work of setting the modal apart — the inner
   read-only block doesn't need a heavy frame, just the standard 1px
   hairline. Two register voices stay intact: white-with-hairline for
   read-only context here, soft-tinted-with-hairline for the editable
   attr-row cards below. No shadow (modal-card carries the depth). */
.modal-existing {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  background: var(--colour-surface);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
}

.modal-existing__note {
  margin: 0 0 var(--space-2);
  font-size: var(--size-xs);
  font-style: italic;
  color: var(--colour-ink-muted);
}

.modal-existing__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.modal-existing__list li {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--size-sm);
}

.modal-existing__name {
  color: var(--colour-ink-strong);
  font-weight: var(--weight-medium);
}

.modal-existing__type {
  color: var(--colour-ink-muted);
  font-size: var(--size-xs);
}

/* ---- Attribute-row editor: each attribute is one card with name+type
   on the primary line and unit / options each on their own line below.
   The earlier four-columns-on-one-row layout cramped every field; on
   the modal's natural width the placeholders couldn't display in full.
   Two-row card keeps name field generous, lets type select size to its
   content, and removes the horizontal competition between unit and
   options (one shows at a time when JS is on; both stack cleanly when
   it isn't). ---------------------------------------------------- */

.attr-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.attr-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "name type"
    "unit unit"
    "options options";
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--colour-surface-soft);
  border: 1px solid var(--colour-line);
  border-radius: var(--radius-md);
}

.attr-row__name    { grid-area: name; }
.attr-row__type    { grid-area: type; }
.attr-row__unit    { grid-area: unit; }
.attr-row__options { grid-area: options; }

/* unit/options use [hidden] for type-driven progressive enhancement
   (category_modal.js). They still submit when hidden — alignment of
   the parallel attr_* form lists is preserved; the route ignores unit
   unless type=number and options unless type=dropdown. With grid-area
   placement, a hidden row collapses cleanly (display:none takes the
   item out of layout; the grid row auto-sizes to 0). */
.attr-row__unit[hidden],
.attr-row__options[hidden] {
  display: none;
}

.attr-rows__add {
  align-self: flex-start;
  font-size: var(--size-xs);
}

/* ---- Modal action buttons -------------------------------------- */

.modal-cancel {
  font-size: var(--size-sm);
  color: var(--colour-ink-muted);
  text-decoration: none;
  padding: var(--space-2) var(--space-3);
}

.modal-cancel:hover,
.modal-cancel:focus-visible {
  color: var(--colour-ink-strong);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.btn-primary {
  padding: var(--space-2) var(--space-4);
  font-size: var(--size-sm);
  font-weight: var(--weight-semibold);
  color: var(--accent-ink);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  cursor: pointer;
}

.btn-primary:hover,
.btn-primary:focus-visible {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

.btn-primary[disabled] {
  cursor: not-allowed;
  opacity: 0.6;
}

/* ---- enquiry detail (client per-enquiry reading view, D0189a) ----
   The estimator's read of one enquiry: the distilled overview, then
   page-by-page what Scout saw. Reuses the .surface / .pill / token
   register; adds only the page-disclosure row + its measurements badge.
   The page row is a native <details>: short label always visible, full
   summary on open (and on the summary's title= hover tooltip), keyboard-
   reachable, graceful without CSS or JS. */

.enquiry-detail__overview {
  padding: var(--space-5);
}

.enquiry-detail__overview-text {
  margin: 0;
  font-size: var(--size-base);
  line-height: var(--leading-relaxed);
  color: var(--colour-ink);
}

.enquiry-detail__overview-empty {
  margin: 0;
  font-size: var(--size-sm);
  color: var(--colour-ink-muted);
  line-height: var(--leading-relaxed);
}

.enquiry-detail__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

/* Category tag: accent-tinted, the one accent moment on the overview
   card. Tenant accent override re-tints it for free. */
.overview-tag {
  display: inline-block;
  padding: 2px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--accent-tint);
  color: var(--accent-strong);
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  text-transform: capitalize;
}

/* Per-attachment group: a filename header, then its page rows. */
.page-group {
  border-bottom: 1px solid var(--colour-line-soft);
}
.page-group:last-child {
  border-bottom: 0;
}

.page-group__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5) var(--space-2);
}

.page-group__file {
  font-size: var(--size-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-ink-strong);
  word-break: break-word;
}

.page-group__count {
  font-size: var(--size-xs);
  color: var(--colour-ink-muted);
  white-space: nowrap;
}

.page-group__pages {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: 0 var(--space-5) var(--space-3);
}

/* One page = a native disclosure. */
.page-read {
  border: 1px solid var(--colour-line-soft);
  border-radius: var(--radius-md);
  background: var(--colour-surface);
  transition:
    border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}
.page-read:hover {
  border-color: var(--colour-line);
  background: var(--colour-surface-soft);
}
/* Flagged (carries measurements): a single sparing accent cue on the
   left edge, the load-bearing "there are numbers here" signal an
   estimator scans for. */
.page-read--flagged {
  border-left: 3px solid var(--accent);
}

.page-read__summary {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  font-size: var(--size-sm);
  list-style: none;
}
.page-read__summary::-webkit-details-marker {
  display: none;
}

.page-read__num {
  flex: none;
  min-width: 2.2em;
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  color: var(--colour-ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Short label: one line by default (ellipsis); wraps in full once open. */
.page-read__label {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--colour-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.page-read[open] .page-read__label {
  white-space: normal;
}

.page-read__badge {
  flex: none;
  padding: 1px var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--accent-tint);
  color: var(--accent-strong);
  font-size: var(--size-xs);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
}

.page-read__chevron {
  flex: none;
  width: 14px;
  height: 14px;
  color: var(--colour-ink-muted);
  transition: transform var(--duration-fast) var(--ease-out);
}
.page-read[open] .page-read__chevron {
  transform: rotate(180deg);
}

.page-read__full {
  margin: 0;
  padding: 0 var(--space-4) var(--space-3)
    calc(var(--space-4) + 2.2em + var(--space-3));
  font-size: var(--size-sm);
  line-height: var(--leading-base);
  color: var(--colour-ink-soft);
}

.enquiry-detail__foot {
  margin-top: var(--space-5);
}
