/* Emgrand Intelligence / Vectra rebrand (2026-08-08): one deliberate dark
   theme, not a light default with a dark override - that part of the
   original rebrand still holds and is unchanged. What changed is the
   in-app accent: Vectra is one Intelligence System nested under EmGrand
   (docs/design/emgrand-brand-style-guide.md), and per that guide's "one
   accent dominates per surface" rule, a page that belongs to Vectra uses
   Vectra's own accent - silver, not EmGrand's brass. Since this whole
   authenticated app IS Vectra's product surface, --accent moved from gold
   to silver; brass is reserved for the EmGrand tier of the nav only (see
   .global-bar below). See 404.html/500.html's own comment on why their
   copy of this palette has to move in lockstep with this one.
   Every value below was RECOMPUTED against WCAG contrast math, not
   carried over by eyeball: silver (#b2b2b3) on --bg/--surface clears
   8.4:1+ as text/icon/border color, silver-tint (#e7e7e5, the hover
   state - brighter not darker, same "reads as live" reasoning as the old
   gold hover) clears 15:1+, and --on-accent (dark text ON a filled
   silver button) reuses --bg itself at 8.88:1 rather than the silver-deep
   token, which measures only ~2.2:1 on these backgrounds and was
   rejected as a foreground color for that reason. Status hues
   (--st-*) are unrelated to brand and are untouched - they still clear
   5.4:1+, and every REAL form input / selectable picker card
   (.form input, .field-picker select, .look-tile, .look-opt,
   .layout-card, .drawing-card) still uses --border-2, which clears the
   3:1 non-text bar (unaffected by the accent change - it was never gold).
   Plain row/section dividers stay on --border on purpose - they are
   decorative, not an outline a user needs to perceive as a boundary. */
:root {
  color-scheme: dark;
  --bg: #14110d;
  --surface: #1c1712;
  /* recessed panel tone (diagram boxes, seal stroke) */
  --bg-2: #241e17;
  --text: #f4ecdb;
  --text-2: #bdae92;
  /* 5.5:1 on --surface / 5.8:1 on --bg */
  --text-3: #9c8d6c;
  /* decorative dividers only - never an interactive element's own
     outline, which is what --border-2 is for */
  --border: #3d3021;
  /* 3.3:1 on --surface, 3.5:1 on --bg - clears WCAG 1.4.11 non-text
     contrast for input/card outlines */
  --border-2: #7a6842;
  /* Vectra's accent (docs/design/emgrand-brand-style-guide.md) - 8.4:1+
     as foreground text/border on --bg/--surface, 8.88:1 as background
     with --on-accent text */
  --accent: #b2b2b3;
  /* brighter, not darker, on hover/press - preserves the "reads as live"
     reasoning the old gold hover state used */
  --accent-2: #e7e7e5;
  --on-accent: #14110d;
  /* EmGrand's accent (parent-brand tier of the nav only, .global-bar) -
     7.36:1 on --bg */
  --brass: #c89b3c;
  --radius: 4px;
  --radius-lg: 6px;
  --font-display: "Avenir Next", "Century Gothic", "Segoe UI Semibold",
    -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  --st-neutral: #93a3b8;
  --st-blue: #7fb3e0;
  --st-violet: #b79cf0;
  --st-green: #6fcf97;
  /* shifted from the old amber's yellow-gold toward orange (#302 lesson,
     applied preemptively): a warning color this close to --accent's hue
     would read as branding, not a status */
  --st-amber: #e8935a;
  --st-red: #e2685f;
}

* { box-sizing: border-box; }
/* author display rules (flex/grid) must never defeat the hidden attribute */
[hidden] { display: none !important; }

/* Keyboard-focus ring (#162): several controls suppress the UA outline
   (outline: none with a border-color swap), which leaves keyboard users
   with no focus indicator at all on buttons/links/toggles. :focus-visible
   fires for keyboard focus only, so mouse users see no change. */
:is(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 15px;
  line-height: 1.6;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3 { font-family: var(--font-display); font-weight: 600; line-height: 1.3; }
h1 { font-size: 22px; margin: 0; }
h2 { font-size: 18px; }

/* Two-tier nav (emgrand-brand-style-guide.md "Layout"): a persistent
   EmGrand parent-brand bar above the Vectra product bar. Both stay on the
   app's one dark ground - .global-bar on --bg (darkest), .appbar on
   --surface (one step up) - rather than switching to a light surface for
   the top tier, which would break the single-dark-theme decision this
   file's header comment documents. Separation comes from that shade
   step plus the accent swap (brass above, silver below), not a color
   bar or edge stripe (style guide: "No accent stripes"). */
.global-bar {
  display: flex; align-items: center;
  padding: 7px 20px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.global-brand {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--brass); font-family: var(--font-display); font-weight: 600;
  letter-spacing: .06em; font-size: 12px; text-transform: uppercase;
}
.appbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 20px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--text);
}
.brand-mark { display: block; flex: none; height: 32px; width: auto; }
.brand-word {
  display: block; font-family: var(--font-display); font-weight: 700;
  letter-spacing: .04em; font-size: 15px;
}
.appbar-nav { display: inline-flex; align-items: center; gap: 16px; }
.appbar-nav a:not(.btn) { color: var(--text-2); font-family: var(--font-display); }
.org-pill {
  font-family: var(--font-display); font-size: 12.5px; color: var(--text-2);
  border: 1px solid var(--border-2); border-radius: 20px; padding: 5px 12px;
}

.container { max-width: 760px; margin: 0 auto; padding: 28px 20px 60px; }

.page-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 0 0 20px;
}
.page-head .muted { color: var(--text-3); font-size: 14px; }

.btn {
  display: inline-flex; align-items: center; gap: 6px;
  font: inherit; font-size: 14px; cursor: pointer;
  padding: 8px 14px; border-radius: var(--radius);
  border: 1px solid var(--border-2); background: var(--surface); color: var(--text);
}
.btn:hover { border-color: var(--text-3); text-decoration: none; }
.btn-primary { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.btn-primary:hover { background: var(--accent-2); }
/* #586: a destructive action sitting in the same table cell as an
   ordinary one. "Remove" and "Save role" are one misclick apart on the
   team page, so the destructive half must not look identical to its
   neighbour. Outline rather than a filled red block: the row is not an
   alarm, and a solid red button reads as the page's primary action. */
.btn-danger { color: var(--st-red); border-color: var(--st-red); }
.btn-danger:hover { background: var(--st-red); color: #ffffff; border-color: transparent; }
/* Several small forms sharing one table cell. Each <form> is a block by
   default, so without this the role picker and the remove button stack
   into two rows per member. */
.inline-form { display: inline-flex; align-items: center; gap: 6px; margin-right: 8px; }
/* a clicked run/preview button stays visible and turns happy blue while
   the job runs (never dimmed away by the disabled state) */
.btn-clicked, .btn-clicked:disabled, .btn-primary.btn-clicked {
  background: #3b82f6; color: #ffffff; border-color: transparent;
  opacity: 1; cursor: default;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 20px 22px;
}

/* #639: the search box and page nav shared by the job and project
   indexes. Both wrap rather than shrink - the search input is the part
   that must stay usable at narrow widths (#601's no-fixed-widths note). */
.list-search {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 8px; margin: 0 0 16px;
}
.list-search input[type="search"] {
  flex: 1; min-width: 0;
  font: inherit; font-size: 14px;
  padding: 8px 12px; border-radius: var(--radius);
  border: 1px solid var(--border-2);
  background: var(--surface); color: var(--text);
}
/* #1309: the Project/Jobs list status filter chips - shares the shape
   .org-pill already uses (border-radius 20px pill, --border-2 outline)
   but that class is a static label, not an interactive toggle, so this
   is its own component rather than a reuse that would need an :is()
   split for the .on state. */
.filter-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
.filter-chip {
  font-size: 12.5px; padding: 5px 12px; border-radius: 999px;
  border: 1px solid var(--border-2); color: var(--text-2);
}
.filter-chip:hover { border-color: var(--text-3); text-decoration: none; }
.filter-chip.on { background: var(--accent); color: var(--on-accent); border-color: transparent; font-weight: 600; }
.pager {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 12px; margin: 18px 0 0;
}
.pager [data-testid$="-pager-status"] { margin-inline: auto; order: 0; }

.job-list { display: flex; flex-direction: column; }
.job-row {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 4px;
  border-top: 1px solid var(--border);
  color: var(--text);
}
.job-row:hover { background: color-mix(in srgb, var(--text) 3%, transparent); }
.job-row:last-child { border-bottom: 1px solid var(--border); }
.job-row .grow { flex: 1; min-width: 0; }
.job-row .name { font-weight: 500; }
.job-row .sub { color: var(--text-3); font-size: 13px; }
.job-row .chev { color: var(--text-3); }
/* #1311: a run whose source data has since been corrected is still
   fully downloadable - is_superseded is a label, not a revocation
   (see the view's own comment) - but it is history, not current, and
   should read that way at a glance rather than needing the trailing
   "from data since corrected" text to be read to notice it. */
.job-row-superseded { opacity: 0.55; }

/* #1311: the project-detail action group (New run / Correct data /
   the muted can't-rerun note) - was three unrelated blocks scattered
   through the page, now one row so they read as "here is what you can
   do" rather than as separate discoveries. */
.project-actions { display: flex; flex-direction: column; gap: 10px; margin: 16px 0; }
.project-actions details summary { list-style: none; cursor: pointer; }
.project-actions details summary::-webkit-details-marker { display: none; }
.project-actions details[open] summary { margin-bottom: 10px; }

/* #1311: the promoted "needs attention" card. NOT .ops-card-alarm
   (--st-red) by default - awaiting the client's OWN confirmation is
   the ramp's waiting-on-a-person violet everywhere else in this app
   (.status-awaiting_confirmation), not an error, and a red alarm card
   would overstate a state that is not actually wrong. A failed run
   genuinely is an alarm, so that one case keeps .ops-card-alarm
   instead - see project_detail.html's class choice. */
.needs-attention { border-color: var(--st-violet); background: color-mix(in srgb, var(--st-violet) 7%, var(--surface)); }
.needs-attention h2 { color: var(--st-violet); }
/* Shared with the failed case's .ops-card-alarm h2 - a status badge
   sits beside the heading text in both variants, so both need the
   same row layout regardless of which color rule applies. */
.needs-attention h2, .ops-card-alarm h2 { display: flex; align-items: center; gap: 10px; }

.badge {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 500; white-space: nowrap;
  padding: 3px 10px; border-radius: 999px;
  color: var(--fg, var(--st-neutral));
  background: color-mix(in srgb, var(--fg, var(--st-neutral)) 15%, var(--surface));
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/* #585: status pill mapping onto the --st-* ramp. Violet is the hue for
   a state waiting on a PERSON; every other in-flight state is blue,
   because the client did not cause it to pause. This
   corrects the original mapping, which had those two swapped -
   awaiting_confirmation read amber (an outcome hue) and generating_maps
   read violet (implying it waits on the client, which it does not). */
.status-draft { --fg: var(--st-neutral); }
.status-queued, .status-analyzing, .status-processing,
.status-generating_maps { --fg: var(--st-blue); }
.status-awaiting_confirmation { --fg: var(--st-violet); }
.status-queued::before, .status-analyzing::before,
.status-processing::before, .status-generating_maps::before {
  animation: pulse 1.6s ease-in-out infinite;
}
/* #600. The JOB DETAIL poller (_job_detail_js.html) adds .status-unknown
   when it gives up, so the last status it read stops passing for a live
   one. The list poller in base.html does NOT - it clears its interval and
   calls vectraPollStopped to show the notice, and its rows keep whatever
   status they last rendered. An earlier version of this comment said
   "both pollers", which was never true; if the list ever needs the same
   treatment, the class is here and unused for it.
   It has to come after the .status-* rules above (same specificity, source
   order decides) and it kills the pulse as well as the colour - the
   pulsing dot is what makes a badge read as live, so leaving it running
   would undo the point. */
.status-unknown { --fg: var(--st-neutral); }
.status-unknown::before { animation: none; }

/* The #600 notice. Not .messages: that block is server-rendered per
   request, this one ships hidden on every page and is revealed by JS. */
.page-notice {
  margin: 0 0 14px; padding: 10px 14px;
  border: 1px solid color-mix(in srgb, var(--st-amber) 40%, var(--surface));
  border-left: 3px solid var(--st-amber);
  border-radius: 8px;
  background: color-mix(in srgb, var(--st-amber) 7%, var(--surface));
  color: var(--text-1, inherit); font-size: 14px;
}
.page-notice a { color: var(--st-amber); }

/* Draws the eye to Outputs when the settle-triggered reload (job_detail's
   poller) lands and this section is what just appeared - reusing --accent
   rather than --st-green because this fires for a settled preview too,
   and status-completed_preview is deliberately NOT green (see below). */
#job-outputs.just-settled {
  animation: outputs-settled-flash 2s ease-out;
  border-radius: 8px;
}
@keyframes outputs-settled-flash {
  0% {
    background: color-mix(in srgb, var(--accent) 16%, var(--surface));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 55%, transparent);
  }
  100% { background: transparent; box-shadow: 0 0 0 3px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  #job-outputs.just-settled {
    animation: none;
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}
@media (prefers-reduced-motion: reduce) {
  .badge::before { animation: none; }
}
.status-completed { --fg: var(--st-green); }
.status-completed_warnings { --fg: var(--st-amber); }
/* #1202: a settled preview is its own word, and it takes the ramp's
   waiting-on-a-person hue rather than green, because green reads as
   "done" and the run is not - it waits on the client to buy the full
   delivery, the same shape as awaiting_confirmation. Not blue: blue is
   the ramp's in-flight hue, and the pulse that separates an in-flight
   badge from a settled one is switched off wholesale under
   prefers-reduced-motion, so a blue terminal badge and a queued badge
   would be the same pill for those readers. */
.status-completed_preview { --fg: var(--st-violet); }
/* #545: the shared .badge 15% mix leaves failed-status text at 4.43:1,
   just under AA's 4.5:1 - the only status that misses. Narrower mix here
   lightens this one badge's background without touching --st-red itself
   (used elsewhere for alarms/errors/limits where this isn't a factor). */
.status-failed { --fg: var(--st-red); background: color-mix(in srgb, var(--st-red) 10%, var(--surface)); }
/* #588: an operator-written ledger row (a comp) rendered distinctly from
   a customer-driven one, reusing .badge rather than a new component. */
.badge-comp { --fg: var(--st-violet); }

/* #1198: the preview display gallery - the marked rasters ARE the
   preview surface, and the mark in the pixels is the anti-substitution
   measure, not the styling.

   Three across: the client judges the product by seeing the SET, which
   a one-per-row column made them scroll for. The view renders one cell
   per EXHIBIT, so a FULL sample fills whole rows - that much the sample
   size is chosen for.

   A part-filled last row is normal, not a fault, and this comment said
   otherwise twice. The grid holds every exhibit rastered on the job,
   not one press's sample: presses accumulate, and `_preview_size`
   clamps to what is still undrawn, which is what the run form's
   "Preview the remaining N exhibits" branch draws. Any total that is
   not a multiple of the column count leaves a short last row. Nothing
   here may depend on it being full.

   The columns collapse below the narrow breakpoint, where three across
   leaves each page too small to read.

   The page fills its column rather than being fitted into a square
   cell: an exhibit is portrait, so a square cell gutters it down to
   about three fifths of the width available - and the burned-in mark
   has to stay legible at whatever size this lands on, which is the only
   size the client ever sees it.

   `aspect-ratio: auto <ratio>` reserves a portrait box until the raster
   decodes and then yields to the raster's own ratio. Plain `height:
   auto` reserved nothing, so the gallery collapsed to a couple of
   pixels until the lazy images arrived - which both shifted the page
   under the reader and put every cell inside the viewport at once,
   defeating the lazy loading it is paired with. `contain` is the belt
   to that braces: wherever the reserved ratio is the one in force, a
   page letterboxes in its cell rather than being stretched into it. */
.preview-gallery {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
}
.preview-page {
  width: 100%; height: auto; aspect-ratio: auto 3 / 4;
  object-fit: contain; display: block;
  background: var(--bg-2);
  border: 1px solid color-mix(in srgb, var(--st-neutral) 35%, var(--surface));
  border-radius: 6px;
}
@media (max-width: 640px) {
  /* One per row: at this width a page scaled to half the screen is not
     readable, and a preview whose pages cannot be read is not a
     demonstration of anything. */
  .preview-gallery { grid-template-columns: repeat(1, 1fr); }
}
.badge-comp::before { content: none; }

.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }

.progress-area { display: flex; align-items: center; gap: 10px; margin: 4px 0 14px; }
.spinner {
  flex: none; width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid var(--border-2); border-top-color: var(--accent);
  animation: spin 0.9s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; border-top-color: var(--border-2); }
}
@keyframes spin { to { transform: rotate(360deg); } }
.progress-track {
  display: none; flex: 1; max-width: 280px; height: 6px;
  border-radius: 999px; overflow: hidden;
  background: color-mix(in srgb, var(--text) 10%, transparent);
}
.progress-fill {
  height: 100%; width: 0; border-radius: 999px;
  background: var(--accent); transition: width 0.6s ease;
}
/* uncounted engine phases (import, save, layout prep): a sliding sheen
   says "alive" while the phase text says WHAT is happening */
.progress-fill.indeterminate {
  background: repeating-linear-gradient(
    -60deg,
    var(--accent) 0 10px,
    color-mix(in srgb, var(--accent) 55%, transparent) 10px 20px
  );
  background-size: 200% 100%;
  animation: progress-slide 1.1s linear infinite;
  transition: none;
}
@keyframes progress-slide {
  from { background-position: 0 0; }
  to { background-position: 23px 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* was the one animation the reduce block missed (#162): the striped
     gradient stays as the "alive" cue, it just doesn't slide */
  .progress-fill.indeterminate { animation: none; }
}
.progress-text { font-size: 12px; color: var(--text-3); }
/* the copy of the progress area directly below the run buttons */
.confirm-actions .confirm-progress { flex-basis: 100%; margin: 10px 0 0; }
.detail-msg { color: var(--text-2); margin: 6px 0 4px; min-height: 1.2em; }
/* Finding row (#585): the confirm screen's per-finding review unit - label,
   detected value, a confirm control, and an optional change disclosure
   underneath. It already exists as two shapes rather than one, kept
   separate on purpose because they are not the same box model: .kv (below)
   is the single-line form (zone, parcel count) and .parcel-check (further
   down) adds a sample list and/or a .field-picker disclosure beneath the
   label for findings that need more room (parcel ID, owner, access roads,
   Line List, footprint). Both drive their confirm control through the same
   .addon-mini.verify-toggle switch and their disclosure through the same
   .field-picker, which is what actually collapses the eleven current
   variations into shared markup - a third wrapper class here would only
   rename what's already unified. */
.kv { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; border-top: 1px solid var(--border); font-size: 14px; }
/* the confirmation page's parcel count - the NUMBER only: bold Times
   New Roman at 2.24em (Pete's spec, 2026-07-18: 40% up, then another
   60% on the number itself; the word "parcels" stays row-sized) */
.parcel-count {
  font-family: "Times New Roman", Times, serif;
  font-weight: 700;
  font-size: 2.24em;
}
.kv:first-of-type { border-top: none; }
.kv .k { color: var(--text-2); }

.section-title { font-size: 15px; font-weight: 500; margin: 20px 0 6px; }
.outputs { list-style: none; padding: 0; margin: 0; }
.outputs li { border-top: 1px solid var(--border); }
/* #1312: the whole row is the download target, not a trailing link -
   .dl-label keeps "Download" as visible, accessible link text rather
   than relying on the filename alone to say what the row does. */
.outputs li a {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0; color: var(--text); text-decoration: none;
}
.outputs li a:hover { color: var(--accent); }
.outputs li a:hover .dl-label { text-decoration: underline; }
/* overflow-wrap: file names are unbroken tokens (1.002001_shapefiles.zip)
   - without a break opportunity they'd overflow the flex box straight
   under the download link */
.outputs .oname { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.outputs .dl-label { flex: none; white-space: nowrap; margin-left: auto; color: var(--text-2); font-size: 13px; }
/* #1312: a plain-language file-type chip ahead of the filename, so
   scanning for "did my KMZ come through" doesn't mean reading every
   extension in turn. Neutral by default; a handful of common
   deliverable extensions (filetype_class in display.py) get their own
   color, and anything else just keeps the neutral chip rather than
   going unstyled or missing. */
.filetype {
  flex: none; font-size: 10.5px; font-weight: 700; letter-spacing: .02em;
  padding: 2px 6px; border-radius: 3px;
  font-family: ui-monospace, "SF Mono", Consolas, "Cascadia Code", monospace;
  background: color-mix(in srgb, var(--st-neutral) 18%, var(--surface));
  color: var(--st-neutral);
}
.filetype-pdf { background: color-mix(in srgb, var(--st-red) 18%, var(--surface)); color: var(--st-red); }
.filetype-kmz { background: color-mix(in srgb, var(--st-blue) 18%, var(--surface)); color: var(--st-blue); }
.filetype-zip { background: color-mix(in srgb, var(--st-green) 18%, var(--surface)); color: var(--st-green); }
.empty { color: var(--text-3); font-size: 14px; padding: 4px 0; }

.outputs-head { margin: 4px 0 14px; }
.btn-glow {
  box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 55%, transparent),
              0 0 3px color-mix(in srgb, var(--accent) 70%, transparent);
}
.btn-glow:hover {
  box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 75%, transparent),
              0 0 5px color-mix(in srgb, var(--accent) 85%, transparent);
}
.btn-sm { padding: 5px 11px; font-size: 13px; white-space: nowrap; }

/* Deliverable tree node (#585): recursive - the same node renders a
   spread, a county, an owner, or a leaf parcel. .parcel-folder is the
   node itself (the <details>/<summary> row plus its per-level download
   button); .spread-folder, .county-folder and .owner-folder are thickness/
   indent modifiers layered on the same base for the three groupings
   (spread -> county -> parcel; owner -> parcel; and the flat fallback,
   which is bare .parcel-folder with no modifier). One component already
   serves all of them - see .spread-folder / .county-folder / .owner-folder
   further down for where each grouping opts in. */
.parcel-folder {
  display: flex; align-items: flex-start; gap: 12px;
  border-top: 2px solid var(--border-2); padding: 6px 10px 6px 6px;
}
/* collapsed bars carry an accent gradient: bright at the center and
   brightest at the right, under the Download parcel button */
.parcel-folder:has(details:not([open])) {
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--accent) 3%, transparent) 0%,
    color-mix(in srgb, var(--accent) 16%, transparent) 50%,
    color-mix(in srgb, var(--accent) 9%, transparent) 72%,
    color-mix(in srgb, var(--accent) 30%, transparent) 100%
  );
}
.parcel-folder details { flex: 1; min-width: 0; }
.parcel-folder summary {
  display: flex; align-items: center; gap: 10px;
  padding: 5px 0; cursor: pointer; list-style: none; user-select: none;
}
.parcel-folder summary::-webkit-details-marker { display: none; }
.folder-icon { flex: none; width: 0; height: 0; margin-left: 2px;
  border-left: 9px solid var(--text-2);
  border-top: 6px solid transparent; border-bottom: 6px solid transparent;
  transition: transform 0.15s;
  filter: drop-shadow(0 0 4px var(--accent));
}
/* child-scoped: every tier carries .parcel-folder, so a descendant
   selector would pre-rotate every NESTED arrow the moment the outer
   tier opens (the "arrows stop rotating after the first tier" bug) */
.parcel-folder details[open] > summary .folder-icon { transform: rotate(90deg); }
.folder-files { margin-left: 21px; }
.folder-files li { font-size: 13px; }
.folder-files li:first-child { border-top: none; }
.parcel-folder .btn-sm { margin-top: 3px; }

/* nested deliverable tree (long jobs): spread -> county -> parcel */
.spread-folder { border-top: 3px solid var(--border-2); }
.spread-folder > details > summary .oname { font-weight: 600; }
.county-folder { margin-left: 20px; border-top: 1px solid var(--border); }
.county-folder .parcel-folder { margin-left: 20px; border-top: 1px solid var(--border); }

.parcel-check { padding: 12px 0 10px; border-top: 1px solid var(--border); }
.parcel-check-q { margin: 0 0 6px; font-weight: 700; }
.parcel-check-field { margin: 0 0 4px; font-weight: 700; color: var(--accent); }
.parcel-check-samples {
  list-style: none; margin: 0; padding: 0;
  font-size: 13px; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.parcel-check-samples li { padding: 1px 0; }

/* verification toggles (confirmation page): compact yes/no switch used on
   each analysis question and category row */
.check-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.addon-mini {
  display: inline-flex; align-items: center; gap: 8px;
  cursor: pointer; user-select: none; flex: none;
  min-height: 24px; /* #545: WCAG 2.5.8 - the bare 22px switch undershot this */
}
.addon-mini .addon-state { width: 24px; font-size: 12px; }
.analysis-layers .addon-mini { justify-content: flex-end; }
.field-picker { margin: 10px 0 4px; }
/* the picker question reads like the other check questions: full text
   color (white in dark theme), bold */
.field-picker .parcel-check-q { margin: 0 0 6px; }
.field-picker .parcel-check-q label { color: var(--text); font-weight: 700; cursor: pointer; }
.field-picker select {
  font: inherit; font-size: 14px; padding: 7px 10px; min-width: 260px;
  border: 1px solid var(--border-2); border-radius: var(--radius);
  background: var(--surface); color: var(--text);
}
.field-picker select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* route-overview map: provided centerline over OpenStreetMap */
.route-map {
  height: 340px; margin: 8px 0 4px;
  border: 1px solid var(--border-2); border-radius: var(--radius);
  background: var(--bg);
}
.route-map-legend {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 6px 0 0; font-size: 13px; color: var(--text-2);
}
.route-chip {
  flex: none; width: 22px; height: 0; border-radius: 2px;
  border-top: 4px solid #1d4ed8;
}
.route-chip-true { border-top-style: dashed; border-top-color: #0d9488; margin-left: 10px; }

/* typical-section style chooser: 3 tiles (original + two magnifier
   families); the family tiles open a 6-style popup for the final pick.
   Thumbnails are the real engine generator's output. */
.look-picker { margin-top: 14px; }
.look-tiles {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 12px; margin-top: 8px;
}
@media (max-width: 640px) { .look-tiles { grid-template-columns: 1fr; } }
.look-tile {
  position: relative; display: flex; flex-direction: column; gap: 4px;
  padding: 8px; border: 2px solid var(--border-2);
  border-radius: var(--radius); background: var(--surface);
}
label.look-tile { cursor: pointer; }
.look-tile input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.look-thumbwrap {
  display: flex; align-items: center; justify-content: center;
  background: #ffffff; border: 1px solid var(--border); border-radius: 4px;
  padding: 4px; min-height: 152px; overflow: hidden;
}
.look-thumbwrap img { max-width: 100%; max-height: 148px; height: auto; width: auto; display: block; }
.look-name { font-weight: 600; font-size: 13px; margin-top: 2px; }
.look-desc { font-size: 12px; line-height: 1.35; min-height: 1.2em; }
.look-tile:hover { border-color: var(--text-3); }
.look-tile.sel {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.look-tile:has(> input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.look-check {
  position: absolute; top: 8px; right: 8px; width: 22px; height: 22px;
  border-radius: 50%; background: var(--accent); color: var(--on-accent);
  display: none; align-items: center; justify-content: center;
  font-size: 13px; z-index: 2;
}
.look-tile.sel .look-check { display: flex; }
.look-choose {
  margin-top: 4px; align-self: flex-start; font: inherit; font-size: 12px;
  font-weight: 600; color: var(--accent); background: transparent;
  border: 1px solid var(--border-2); border-radius: 6px;
  padding: 6px 10px; cursor: pointer;
}
.look-choose:hover { border-color: var(--accent); }
.look-choose:disabled { color: var(--text-3); cursor: default; opacity: 0.7; }
.look-tile.locked { opacity: 0.85; }

/* the choose-style popup (modal overlay) */
.look-pop {
  position: fixed; inset: 0; z-index: 60; display: flex;
  align-items: center; justify-content: center; padding: 20px;
}
.look-pop[hidden] { display: none; }
.look-pop-backdrop { position: absolute; inset: 0; background: rgba(10, 12, 14, 0.5); }
.look-pop-sheet {
  position: relative; width: min(620px, 100%); max-height: 86vh; overflow: auto;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}
.look-pop-head {
  position: sticky; top: 0; display: flex; align-items: center; gap: 10px;
  padding: 14px 16px; font-weight: 600;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.look-pop-x {
  margin-left: auto; width: 30px; height: 30px; border-radius: 6px;
  /* #545: this is a real control, not a decorative panel edge - its
     default (non-hover) state must clear WCAG 1.4.11 on its own. */
  border: 1px solid var(--border-2); background: var(--bg-2); color: var(--text);
  cursor: pointer; font-size: 14px; line-height: 1;
}
.look-pop-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding: 16px;
}
@media (max-width: 560px) { .look-pop-grid { grid-template-columns: repeat(2, 1fr); } }
.look-opt {
  position: relative; display: flex; flex-direction: column; gap: 4px;
  cursor: pointer; padding: 6px; border: 2px solid var(--border-2);
  border-radius: var(--radius); background: var(--surface);
}
.look-opt input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.look-opt img {
  width: 100%; height: auto; background: #ffffff;
  border: 1px solid var(--border); border-radius: 4px;
}
.look-opt-name { font-size: 12px; font-weight: 600; text-align: center; }
.look-opt:hover { border-color: var(--accent); }
.look-opt.cur {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 30%, transparent);
}
.look-opt:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.look-opt-check {
  position: absolute; top: 6px; right: 6px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--accent); color: var(--on-accent);
  display: none; align-items: center; justify-content: center; font-size: 12px;
}
.look-opt.cur .look-opt-check { display: flex; }

/* typical footprint cross-section diagram (confirmation page) */
/* frosted-ice typical section (Pete's approved design, 2026-07-18):
   the SVG carries its own dark blueprint card, legend and dimensions -
   styling lives inline in the SVG, CSS only sizes it. Full-width and
   stacked: the denser drafting text needs the room. */
.xsec { display: block; width: 100%; max-width: 700px; height: auto; margin: 8px 0 2px; }
/* primary + 2nd-most-common configurations side by side (wrap when narrow) */
.xsec-pair { display: flex; flex-direction: column; gap: 18px; align-items: stretch; }
.xsec-cell { min-width: 0; }
.xsec-sub { margin: 8px 0 0; font-weight: 700; }
.suffix-input {
  display: flex; align-items: center; margin-top: 8px; max-width: 340px;
  border: 1px solid var(--border-2); border-radius: var(--radius);
  background: var(--surface); overflow: hidden;
}
.suffix-input input {
  flex: 1; min-width: 0; font: inherit; font-size: 14px;
  padding: 7px 4px 7px 10px; border: none; outline: none;
  background: transparent; color: var(--text);
}
.suffix-input .suffix {
  flex: none; padding-right: 10px; font-size: 14px; color: var(--text-3);
  user-select: none; white-space: nowrap;
}
/* The wrapper sets overflow:hidden, which clips the inner input's outline
   (#471 review LOW-4) - so the ring has to live on the wrapper itself. */
.suffix-input:has(:focus-visible) {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.suffix-input:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.custom-config { margin-top: 10px; }
.width-fields { display: flex; gap: 10px; flex-wrap: wrap; }
.width-fields label {
  display: flex; flex-direction: column; gap: 3px;
  font-size: 12px; color: var(--text-2);
}
.width-fields input {
  width: 96px; font: inherit; font-size: 14px; padding: 6px 8px;
  border: 1px solid var(--border-2); border-radius: var(--radius);
  background: var(--surface); color: var(--text);
}
.width-fields input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
/* (the old flat-diagram classes - xsec-ws/esmt/cl*/dim/feet/legend/
   swatch - retired with the frosted-ice port; rollback lives in
   _xsec_svg_legacy.html + git) */
.cl-offset-alert {
  margin-top: 8px; padding: 10px 12px; border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--st-amber) 55%, transparent);
  background: color-mix(in srgb, var(--st-amber) 12%, transparent);
}
.cl-offset-alert .cl-offset-head { margin: 0 0 6px; font-size: 13px; }

.results { width: 100%; border-collapse: collapse; font-size: 14px; }
.results th { text-align: left; font-weight: 500; color: var(--text-2); font-size: 13px; padding: 6px 8px 6px 0; border-bottom: 1px solid var(--border-2); }
.results td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--border); }
.results .num { text-align: right; font-variant-numeric: tabular-nums; }
/* #1314: a header carrying a "(sort by ...)" sub-line (the ops fleet
   table) used to wrap wherever the column happened to be narrow enough
   to break it, mid-phrase, because the sub-line was inline with the
   label rather than its own line. Forcing it onto a deliberate second
   line reads correctly at any column width - the wrap point is now a
   design choice, not an accident of how much room the column has. */
.results th .muted { display: block; margin-top: 2px; }

/* #587: /ops/ fleet table. `.results` above covers layout; this is only
   the one thing that table doesn't already have - an alarm color for the
   reporter verdict and the "at limit" flag, matching --st-red's use
   elsewhere (status-failed). Wrapped in a scrolling container rather than
   letting the table itself overflow the page: at six columns this is the
   widest table in the app and the narrowest viewport it has to render in. */
.ops-table-wrap { overflow-x: auto; }

/* #1088 (SA-9), the operator shell's own bar. Three things separate it from
   .appbar, and none of them is a new colour:
     - the --bg-2 ground, one step up from .appbar's --surface;
     - the "Operator" tag;
     - different nav words (Fleet / Onboard / Back to app).

   It shipped with a 2px brass bottom border, under a comment claiming that
   reused --brass's "parent-brand/authority tier" meaning. Gate-1 killed both
   halves. docs/design/emgrand-brand-style-guide.md says "brass is allowed
   inside the Vectra brand mark and nowhere else on Vectra pages", and
   separately "No accent stripes. ... never a color bar, edge stripe, or
   single-side border" - so a brass single-side border on a Vectra page
   breaks two rules at once. And .global-bar was never precedent for it: its
   own border is 1px --border, and brass appears there only as .global-brand's
   TEXT colour, exactly as the token's own comment says ("parent-brand tier of
   the nav only").

   So the differentiation is carried by TINT, which the same guide sanctions
   ("Depth and emphasis come from shadow, tint, dashed borders, or a solid
   ring"): the --bg-2 ground, plus a silver-ringed tag. --accent is Vectra's
   own accent and is allowed here; --st-amber/--st-red are not an option
   either, being status colours, and a bar permanently the colour of a warning
   is the always-on alarm this repo keeps designing away from. */
.opsbar {
  display: flex;
  /* WRAPS, and .appbar does not need to (#1090, found by walking the page
     at 375px). The two bars are otherwise identical, and the client one
     fits: its nav ends at 355px of a 375px viewport. This one does not,
     because .opsbar-brand carries the "Operator" tag on top of the same
     mark and wordmark, and those ~85px are what pushed every /ops/ page
     into a horizontal scroll. Measured before and after: scrollWidth 460
     -> 375, and the bar grows one line, 97px -> 109px, only at widths
     where it would otherwise overflow. */
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 20px;
  background: var(--bg-2);
  border-bottom: 1px solid var(--border);
}
.opsbar-brand { display: inline-flex; align-items: center; gap: 10px; }
/* SHARED, and deliberately so: this is both the "Operator" tag in
   ops_base.html and the nav-ops link base.html renders in the CLIENT
   appbar for a superuser. They are one affordance seen from two sides -
   the way in, and the marker saying you took it - so they are meant to
   look identical, and editing this rule changes BOTH bars.
   accounts/test_ops_shell.py pins the coupling from the client side; a
   deliberate divergence means two classes, not an edited one. */
.ops-tag {
  font-family: ui-monospace, "SF Mono", Consolas, "Cascadia Code", monospace;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .14em;
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 2px 9px;
}
.opsnav { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 16px; }
.opsnav a:not(.btn) { color: var(--text-2); font-family: var(--font-display); }
/* The current section, marked with BOTH a colour change and an underline -
   colour alone would carry the whole signal, which #585's own a11y pass
   refuses (WCAG 1.4.1). aria-current="page" is set alongside it in the
   template for anyone not reading either. */
.opsnav a.here { color: var(--text); border-bottom: 2px solid var(--accent); padding-bottom: 2px; }
.opsnav-sep { width: 1px; height: 18px; background: var(--border-2); }
/* Public price sheet (#33 repricing): same job as .ops-table-wrap above,
   on a page an anonymous visitor reaches from a phone. The rate card is
   three columns and the volume ladder two, so this is a guard against a
   long product label pushing the PAGE sideways, not a routine scroller. */
.table-scroll { overflow-x: auto; }
.ops-alarm { color: var(--st-red); font-weight: 600; }

/* #1090 (SA-2/SA-3/D7), the organization summary. Nothing here introduces a
   colour: the grid and the panels are layout, and the one thing that can go
   red reuses --st-red, which is already this app's status red. */
.ops-crumb { margin: 0 0 14px; font-size: 14px; }
/* #1315: lays out the org-detail action rows now that each <a> carries
   .btn/.btn-sm - a plain row of pipe-separated text before, so this is
   the same flex-with-gap layout .confirm-actions already uses for a
   button row elsewhere, under its own name rather than reused directly
   since the two are not otherwise related. */
.ops-org-action-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0 0 12px; }

/* #1089 (D8), the attention region. A tinted card rather than a stripe or a
   heading in red: the style guide sanctions "shadow, tint, dashed borders,
   or a solid ring" and bans edge stripes, and the same --st-red tint the
   org page's alarm card uses keeps one meaning for one colour across the
   surface. */
.attention {
  background: color-mix(in srgb, var(--st-red) 7%, var(--surface));
  border: 1px solid var(--st-red);
  border-radius: var(--radius-lg);
  padding: 14px 18px;
  margin-bottom: 18px;
}
.attention h2 { margin: 0 0 8px; font-size: 15px; color: var(--st-red); }
.attention ul { margin: 0; padding: 0; list-style: none; }
.attention li {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px;
  padding: 5px 0;
}
/* The all-clear costs one row and never a card: it is the state the page is
   in on most days, and a full alarm-sized panel saying nothing is wrong
   would take the top of the screen to report the absence of news. */
.attention-clear {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px;
  margin: 0 0 18px; font-size: 14px;
}
/* W1's row. Tinted, not hidden and not a stripe - it has to read as a row
   about an organization, because that is what it is. */
.ops-row-error td {
  background: color-mix(in srgb, var(--st-red) 8%, transparent);
}

/* A RING, not an edge stripe. docs/design/emgrand-brand-style-guide.md bans
   "a color bar, edge stripe, or single-side border on a card or section" and
   sanctions "a solid ring" in the same sentence, so the alarm state changes
   the card's whole border rather than adding one edge - and tints the ground
   the way .status-failed already does, which is this app's own precedent for
   a red surface rather than a new invention. */
.ops-card-alarm {
  border-color: var(--st-red);
  background: color-mix(in srgb, var(--st-red) 7%, var(--surface));
}
.ops-card-alarm h2 { color: var(--st-red); }

/* auto-fit rather than a fixed column count, the same technique .tiers uses
   further down this file: the facts grid holds six items on a wide screen and
   has to reflow to one on a phone without a media query per breakpoint.

   220px, MEASURED rather than guessed. .container is 760px wide, so 240px
   gave two columns and three rows; 220px gives the three the wireframe
   drew, at 230.7px each, with every cell's scrollWidth inside its
   clientWidth and the whole grid 77px shorter. Below ~700px it drops to two
   and then to one on its own. */
/* #1313: .facts/.fact/.fact-sub are shared aliases for this component,
   not a second one - account.html (customer-facing) reuses the same
   stat-tile pattern /ops/ already had, so the selectors below match
   both the ops-prefixed classes still used across the ops screens and
   the unprefixed ones a non-ops page can use without carrying an
   "ops" name into its markup. */
.ops-facts, .facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
  margin: 18px 0;
}
/* #1307 widened ops_org.html's shell to .container-wide (980px) without
   re-measuring this grid, which the 220px minmax above was tuned to
   .container's 760px specifically for - at 940px content width that
   minmax fits FOUR columns, not the three the wireframe drew, gate-1
   caught rendering it live. 280px, MEASURED the same way: 3*280+2*14=868
   fits inside 940px (72px slack, the same shape of margin the 220px/77px
   pair above has at 760px), 4*280+3*14=1162 does not, and three columns
   fill the row at 304px each. */
.container-wide .ops-facts, .container-wide .facts {
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.ops-fact, .fact {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px 18px;
}
.ops-fact dt, .fact dt {
  font-size: 12px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-3); margin-bottom: 6px;
}
.ops-fact dd, .fact dd { margin: 0; font-size: 15px; }
/* Block, so it always takes its own line: several of these sub-lines are the
   half of the answer that stops the line above being wrong on its own - both
   retention windows, billable-vs-recorded overage - and a sub that wrapped up
   beside its figure would read as a qualifier rather than a second fact. */
.ops-fact-sub, .fact-sub {
  display: block; margin-top: 4px;
  font-size: 12.5px; color: var(--text-3);
}

.ops-panels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 18px;
  margin: 18px 0;
}
.ops-panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 8px;
}
.ops-panel-head h2 { margin: 0; font-size: 16px; }
.ops-ledger-section { margin-top: 18px; }

.actions { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border); }
.confirm-actions { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.confirm-actions p { margin: 0; }

.addon-row {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 0; border-top: 1px solid var(--border);
  cursor: pointer; user-select: none;
}
.addon-row:first-of-type { border-top: none; }
.addon-label { flex: 1; font-size: 14px; }
.addon-check { position: absolute; opacity: 0; pointer-events: none; }
.addon-toggle {
  flex: none; position: relative; width: 40px; height: 22px;
  border-radius: 999px; background: var(--border-2);
  transition: background 0.15s;
}
.addon-toggle::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--surface); transition: left 0.15s;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.addon-check:checked ~ .addon-toggle { background: var(--accent); }
.addon-check:checked ~ .addon-toggle::after { left: 21px; }
.addon-check:focus-visible ~ .addon-toggle {
  /* The input itself is opacity:0, so the global ring lands on something
     nobody can see and this glow was the sole indicator (#471 review
     HIGH-3). A real outline is the indicator; the glow is decoration. */
  outline: 2px solid var(--accent); outline-offset: 2px;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}
.addon-state { flex: none; width: 26px; font-size: 13px; color: var(--text-3); }
.addon-state::before { content: attr(data-off); }
.addon-check:checked ~ .addon-state { color: var(--accent); }
.addon-check:checked ~ .addon-state::before { content: attr(data-on); }

.layout-picker { display: flex; gap: 14px; flex-wrap: wrap; margin: 10px 0 4px; }
.layout-card {
  position: relative; display: flex; flex-direction: column; gap: 6px;
  width: 150px; padding: 8px; cursor: pointer;
  border: 2px solid var(--border-2); border-radius: var(--radius);
  background: var(--surface);
}
.layout-card:hover { border-color: var(--text-3); }
.layout-card:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.layout-card:has(input:checked) {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.layout-card input { position: absolute; opacity: 0; pointer-events: none; }
.layout-card img {
  width: 100%; height: 190px; object-fit: contain;
  background: #fff; border-radius: 4px;
}
.layout-noimg {
  display: flex; align-items: center; justify-content: center;
  height: 190px; color: var(--text-3); font-size: 12px;
  border: 1px dashed var(--border-2); border-radius: 4px;
}
.layout-name { font-size: 12px; text-align: center; color: var(--text-2); word-break: break-all; }
.analysis-layers { margin: 10px 0 4px; }
.warn-list {
  list-style: none; margin: 12px 0 4px; padding: 10px 14px;
  border: 1px solid color-mix(in srgb, var(--st-amber) 45%, var(--border));
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--st-amber) 8%, var(--surface));
  font-size: 13px; color: var(--text-2);
}
.warn-list li { padding: 2px 0; }
.warn-list li::before { content: "⚠ "; color: var(--st-amber); }
/* Layout QC panel: findings about OUR pages (docs/layout-pipeline.md) */
.qc-sev {
  display: inline-block; min-width: 62px; text-align: center;
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .04em; border-radius: 4px; padding: 1px 6px;
  margin-right: 6px;
}
/* delivery scorecard (confirmation page): data-quality verdicts over
   the raw delivery - informational, never blocking */
.sc-pass { background: color-mix(in srgb, var(--st-green) 18%, var(--surface)); color: var(--st-green); }
.sc-warn { background: color-mix(in srgb, var(--st-amber) 18%, var(--surface)); color: var(--st-amber); }
.scorecard {
  margin: 0 0 14px; padding: 12px 14px; border-radius: var(--radius-lg);
  border: 1px solid var(--border); background: color-mix(in srgb, var(--text) 2%, transparent);
}
.scorecard-head { margin: 0 0 6px; font-size: 14px; }
.scorecard-list { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.scorecard-list li { padding: 3px 0; }
.back { display: inline-block; color: var(--text-2); font-size: 14px; margin-bottom: 12px; }

.form .field { margin: 0 0 16px; }
.form label { display: block; font-size: 13px; color: var(--text-2); margin: 0 0 5px; }
.form input, .form select, .form textarea {
  width: 100%; font: inherit; font-size: 14px;
  padding: 8px 10px; border: 1px solid var(--border-2);
  border-radius: var(--radius); background: var(--surface); color: var(--text);
}
.form input:focus, .form select:focus, .form textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.form input[type="file"] { padding: 8px; background: var(--bg); cursor: pointer; }

/* Consent checkbox (signup + reconsent, #522): shared class so both pages
   render it identically. Was `label class="muted"` on signup (the one
   control a user must actively agree to, styled as the faintest text on
   the page) and a bare, unstyled `label class="checkbox"` on reconsent
   that `.form input` was stretching to a full-width bordered text-box
   look (the "floats awkwardly" the issue describes) - this overrides that
   with the higher-specificity type selector below. Full text weight, not
   .muted; the wrapping <label> keeps the whole sentence clickable.
   Selector is `.form label.consent-check`, not the bare class: `.form
   label` above is (0,1,1) and a bare `.consent-check` is only (0,1,0), so
   display/color/font-size silently lost to it - measured via
   getComputedStyle, not eyeballed, since this file has no screenshot
   tooling. */
.form label.consent-check {
  display: flex; align-items: flex-start; gap: 10px;
  color: var(--text); font-size: 14px; line-height: 1.45;
  cursor: pointer; margin: 0 0 5px;
}
.consent-check input[type="checkbox"] {
  flex: none; width: 18px; height: 18px; margin: 3px 0 0;
  padding: 0; border-radius: 3px;
  accent-color: var(--accent);
  cursor: pointer;
}

/* Show/hide password (#463). The wrapper and button are created by JS, so
   without it the field is an ordinary password input and none of this
   applies - the point being that a JS-less render must never be a
   plaintext one. */
.pw-wrap { display: flex; gap: 6px; align-items: stretch; }
.pw-wrap input { flex: 1 1 auto; min-width: 0; }
.pw-toggle {
  /* min-width, because "Hide" is wider than "Show": without it the input
     rewidths on every toggle and the caret jumps (#463 review L8). */
  flex: 0 0 auto; min-width: 68px; font: inherit; font-size: 13px; padding: 0 8px;
  border: 1px solid var(--border-2); border-radius: var(--radius);
  background: var(--bg); color: var(--text-2); cursor: pointer;
}
.pw-toggle:hover { color: var(--text); border-color: var(--text-2); }
.pw-toggle:focus-visible {
  /* No `outline: none` here (#470 review F1). Suppressing it and
     substituting a color-mix glow is precisely the pattern #471/#472
     removed - and #472's stylesheet test names this selector, so keeping it
     would turn main red the moment both landed. The global #162 rule
     already puts the 2px accent ring on the button; the border and glow are
     decoration on top of it. */
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
/* Announced by screen readers, never shown: the button's own name changes
   too, but a name change is only heard on re-focus, and the person needs
   to know the state changed at the moment they pressed it. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  border: 0;
}

/* #545: off-screen until focused, so a keyboard/screen-reader user can jump
   past the appbar nav, but sighted mouse users never see it. */
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: 10px 16px; background: var(--surface); color: var(--text);
  border-radius: 0 0 6px 0;
}
.skip-link:focus { left: 0; }

.dropzone {
  position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 22px 16px; text-align: center;
  border: 1.5px dashed var(--border-2); border-radius: var(--radius);
  color: var(--text-2); font-size: 13px;
  transition: border-color 0.15s, background-color 0.15s;
}
.dropzone.dragover, .dropzone:hover { border-color: var(--accent); }
.dropzone.dz-error { border-color: var(--st-red); }
.dropzone.dragover { background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }
.dropzone input[type="file"] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer;
}
/* #545/#162: the real input is opacity:0 and covers the whole zone, so the
   browser's own ring is painted on something invisible - a keyboard user
   tabbing to the upload control saw NOTHING. Same shape, and same fix, as
   the six controls already parametrized in test_stylesheet.py; these two
   were simply missed when that list was drawn up. */
.dropzone:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.dropzone .dz-icon { font-size: 20px; line-height: 1; color: var(--text-3); }
.dropzone .dz-file { color: var(--accent); font-weight: 500; }
.form .help { font-size: 12px; color: var(--text-3); margin: 5px 0 0; }

/* confirmation-page logo upload (prints lower-left on every exhibit) */
.logo-upload {
  position: relative; display: flex; align-items: center; gap: 10px;
  border: 1.5px dashed var(--border-2); border-radius: var(--radius);
  padding: 12px 14px; margin: 6px 0 2px;
  color: var(--text-2); font-size: 13px; cursor: pointer;
  transition: border-color 0.15s;
}
.logo-upload:hover { border-color: var(--accent); }
.logo-upload input[type="file"] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer;
}
.logo-upload:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.form .error { font-size: 13px; color: var(--st-red); margin: 5px 0 0; }
.form-row { display: flex; gap: 14px; }
.form-row .field { flex: 1; }

/* ---- public pages (landing / pricing / account stubs) ---- */

.container-wide { max-width: 980px; }

.btn-lg { padding: 10px 18px; font-size: 15px; }

.hero { text-align: center; padding: 44px 0 36px; }
.hero-title { font-size: 34px; font-weight: 600; max-width: 640px; margin: 0 auto; }
.hero-sub { color: var(--text-2); max-width: 560px; margin: 14px auto 0; }
.hero-actions { display: flex; justify-content: center; gap: 12px; margin-top: 22px; }

.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 8px 0 28px; }
.step { position: relative; }
.step h2 { margin: 10px 0 6px; font-size: 16px; }
.step p { margin: 0; font-size: 14px; }
.step-no {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 15%, var(--surface));
  color: var(--accent); font-size: 13px; font-weight: 600;
}

.sample { display: flex; gap: 22px; align-items: center; }
.sample-copy { flex: 1; }
.sample-copy h2 { margin-top: 0; }
/* .sample-thumb removed with the "(placeholder)" box it styled (#544).
   Layout previews are #245; whatever that adds will want its own sizing
   rather than a dashed-border shell kept alive on spec. */

/* auto-fit, not repeat(3): three tiers today, and a fourth (or a second
   currency column) must not require a CSS edit to stop leaving a dead
   quarter of the row. The shared mobile collapse at .steps below still
   flattens both to one column. */
.tiers { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 18px; }
.tier { display: flex; flex-direction: column; gap: 4px; }
.tier h2 { margin: 0; font-size: 16px; }
.tier-featured { border-color: var(--accent); }
.tier-price { font-size: 26px; font-weight: 600; margin: 4px 0; }
.tier-price .per { font-size: 13px; font-weight: 400; color: var(--text-3); }
.tier-list { list-style: none; padding: 0; margin: 0 0 14px; flex: 1; }
.tier-list li { font-size: 13px; color: var(--text-2); padding: 4px 0; border-top: 1px solid var(--border); }
.tier .btn { justify-content: center; }

/* Volume discounts (2026-08-15, Andre): the ladder is the thing an annual
   commitment BUYS, and the page rendered it as flat as everything else -
   a bullet in the same weight and colour as the term line beside it, and
   an "included with Annual" sentence set in the same muted fine print as
   the marginality note below the table. These rules extend the accent
   .tier-featured already gives the annual card down to the section that
   card's commitment pays for - deliberately the same border colour, so
   the two read as one thing rather than as two competing highlights.

   GUARD SCOPE, stated exactly, because the near-miss version of this
   sentence is what gate-1 caught: .tier-unlock, .volume-panel,
   .volume-head, .badge-unlock and .volume-off are all applied inside
   pricing.html's `volume_bands` / `volume_rows` guard, so a plan
   configured without bands advertises nothing. .tier-lacks is the
   EXCEPTION and is applied to an unguarded line on purpose - see its own
   note below, and pricing.html's header comment, which already records
   why that line has no key to hang on.

   Contrast recomputed against this sheet's tokens, not assumed - the
   panel tint lightens the background these sit on, so the header
   comment's numbers do not carry over. The binding one: --text-3 on the
   6% tint is 4.94:1, down from 5.45:1 on --surface and the only value
   here anywhere near AA's 4.5:1 for 13px body text. --text-2, --accent
   as text and as the 1px outline, and .badge-unlock's --accent on
   .badge's own 15% mix all clear it by margins wide enough that quoting
   them would just be four more numbers to go stale. */
.tier-list li.tier-unlock { color: var(--accent); font-weight: 600; }
/* The monthly card's mirror-image line. "Unlocks with annual" only reads
   if the tier WITHOUT the ladder is visibly the lesser of the two, so the
   pair is styled as a pair - neither line means much on its own.
   The line this lands on is deliberately unguarded (pricing.html's header
   comment: it asserts the ABSENCE of a ladder, so there is no config key
   to gate it on), which means this rule alone CAN paint with the guard
   closed. That is muting a line that stays true when no ladder exists, so
   it advertises nothing - but it is the one rule in this block that
   renders in that state, and an audit of the guard has to know it. */
.tier-list li.tier-lacks { color: var(--text-3); }

.volume-panel {
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
  border: 1px solid var(--accent);
  border-radius: var(--radius-lg);
  padding: 18px 20px;
  margin-bottom: 18px;
}
.volume-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.volume-head h2 { margin: 0; }
.badge-unlock { --fg: var(--accent); }
/* Not a status pill: .badge::before paints a dot that reads as a live
   state, and a published rate has none. .badge-comp is the precedent for
   suppressing it on a non-status reuse - it does the same thing without
   recording why, so the reason is written out here rather than cited. */
.badge-unlock::before { content: none; }
.volume-table .volume-off { color: var(--accent); font-weight: 600; }

.small { font-size: 13px; }
.muted { color: var(--text-3); }

.footer {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  max-width: 980px; margin: 0 auto; padding: 18px 20px 28px;
  border-top: 1px solid var(--border); font-size: 13px;
}
.footer-nav { display: inline-flex; gap: 14px; }
.footer-nav a { color: var(--text-2); }

@media (max-width: 720px) {
  .steps, .tiers { grid-template-columns: 1fr; }
  .sample { flex-direction: column; }
  .hero-title { font-size: 26px; }
  /* was display:none - Jobs/Account simply vanished on phones with no
     other way to reach them (#162). Compact and wrappable instead. */
  .appbar-nav { flex-wrap: wrap; gap: 10px; justify-content: flex-end; }
  .appbar-nav a:not(.btn) { font-size: 13px; }
}

/* Drawing-type chooser (#87): three product cards on the data loading page */
.drawing-picker { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin: 8px 0 18px; }
.drawing-card {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 8px; padding: 18px 14px 14px; text-align: center; cursor: pointer;
  border: 2px solid var(--border-2); border-radius: 10px; background: var(--bg-2);
  transition: border-color 0.15s, box-shadow 0.15s;
}
.drawing-card input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.drawing-card:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.drawing-card:has(input:checked) {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}
.drawing-logo { width: 64px; height: 64px; }
.dlogo-frame { fill: none; stroke: var(--text-2); stroke-width: 2.5; }
.dlogo-line { fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.dlogo-accent { fill: var(--accent); stroke: none; }
.dlogo-seal { fill: none; stroke: var(--bg-2); stroke-width: 2; }
.drawing-name { font-weight: 700; }
.drawing-desc { line-height: 1.35; }
.drawing-disclaimer { font-size: 12px; color: var(--st-amber); line-height: 1.35; }

/* Plat-type sub-picker (#773): hidden unless Legal Plats is the checked
   drawing_type, reactively - same :has() mechanism as the card highlight
   above, so it needs no JS and no page reload to appear. */
.plat-type-field { display: none; }
.form:has(input[name="drawing_type"][value="legal_plats"]:checked) .plat-type-field {
  display: block;
}
/* Separate takings (#85): toggles inside a tract folder */
.sep-takes { margin: 6px 0 4px 21px; display: flex; flex-direction: column; gap: 4px; }
.sep-takes-row { display: inline-flex; align-items: center; gap: 8px; }
.sep-takes-label { font-size: 13px; }
.sep-takes-list { margin-left: 4px; font-size: 13px; }
.sep-takes-list summary { cursor: pointer; color: var(--text-2); }

/* Landowner grouping toggle + owner folders (Pete, 2026-07-16) */
.outputs-head { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.owner-group-toggle { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; }
.owner-folder { border-top: 3px solid var(--border-2); }
.owner-folder > details > summary .oname { font-weight: 600; }
.owner-folder .parcel-folder { margin-left: 20px; border-top: 1px solid var(--border); }

/* Detail sheets choice (#88) */
.details-choice { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 14px; font-size: 14px; }
.details-choice label { display: flex; align-items: baseline; gap: 8px; }
.details-choice .details-fix { margin-left: 24px; color: var(--text-2); font-size: 13px; }

/* Allowance meter (#585): plan usage against the included allowance -
   account page today, confirm panel and the operator fleet table once
   #583/#587 land. A bar only for the case it can actually measure: a
   bounded, per-period allowance. The gate is `quota.included and
   quota.renews` (account.html) - `and`, NOT `is not None`, which this
   comment said until 2026-08-21 and which stopped being the same
   question when #782 part B set the paid allowances to 0: `is not None`
   is TRUE for monthly and annual, the template gate is false for them,
   and the corrected note on .allowance-fill.at-limit below was left
   contradicting this one. Unlimited (included is None - nothing to
   measure against), zero (nothing to draw) and lifetime/one-time
   allowances (renews is false - not a period fraction) all stay
   text-only; drawing a bar for any of them would imply a ceiling or a
   reset that isn't there.

   Restoring `is not None` in account.html's gate - not here; this file
   only styles the bar - would not produce broken CSS, which an earlier
   draft of this note claimed: `{% widthratio %}` catches the zero
   division and renders "0", so monthly and annual would draw a
   permanently empty bar rather than an invalid declaration. Wrong, but
   quietly. */
.allowance-meter { margin: 8px 0; }
.allowance-track {
  height: 8px; border-radius: 999px; overflow: hidden;
  background: var(--bg-2);
}
.allowance-fill { height: 100%; background: var(--accent); }
/* At quota.at_limit (accounts/plans.py's quota_state): the fill reads as
   exhausted, not just full - the same st-red the status ramp uses for a
   terminal FAILED. Unreachable under today's four PLANS entries, and by a
   blunter route than this comment used to claim: the bar is gated on
   `quota.included and quota.renews` (account.html), and since #782 part B
   zeroed included production NO shipped plan renders it at all - monthly
   and annual are 0, enterprise is None, `inactive` is 0 and renews=False.
   The one entry where at_limit can be True is `inactive`, which is also
   the one that can never draw the bar. Kept because
   at_limit is quota_state()'s general per-plan field, not something this
   component invented, and a future bounded plan with overage disabled
   would hit it. */
.allowance-fill.at-limit { background: var(--st-red); }

/* Consequence panel (#585): the block naming what a destructive action
   deletes, stated before the client presses the button rather than only
   inside a native confirm() dialog. Used today by the destructive re-run
   (a FAILED job's partial output) and discard; deactivate and offboard
   stay in Django admin (see docs/frontend-rebuild.md) so they don't
   instantiate this yet. Reuses the amber warning treatment already
   established by .warn-list/.cl-offset-alert rather than inventing a
   second "this is dangerous" language. */
.consequence-panel {
  margin: 10px 0; padding: 12px 14px; border-radius: var(--radius-lg);
  border: 1px solid color-mix(in srgb, var(--st-amber) 45%, var(--border));
  background: color-mix(in srgb, var(--st-amber) 8%, var(--surface));
}
.consequence-panel .consequence-head {
  margin: 0 0 6px; font-size: 13px; font-weight: 600; color: var(--st-amber);
}
.consequence-panel p { margin: 0 0 8px; font-size: 13px; color: var(--text-2); }
.consequence-panel p:last-child { margin-bottom: 0; }
/* --- #1091, the billing ledger ------------------------------------------ */

/* A credit is money coming BACK, and `amount_dollars` already says so in
   text: it puts the sign outside the dollar mark ("-$120.00") because
   "$-120.00" reads as a typo rather than as a refund. This is the visual
   half of the same statement. Not the only signal, deliberately - colour
   alone would leave the distinction invisible to a reader who cannot see
   it, and the sign carries the meaning on its own. */
.ops-credit { color: var(--st-green); }

/* A ledger date is one token. Without this "19 Aug 2026" wraps inside a
   narrow first column and the table reads as three ragged lines per row. */
.nowrap { white-space: nowrap; }

/* #1308: job-run two-column layout. .job-run-form is display:contents so
   its two children (.job-run-fields, .job-run-panel) become direct items
   of THIS grid instead of being confined to the one cell the <form> box
   would otherwise occupy - the form still submits as a single POST, only
   its rendered box disappears. .job-run-left/.job-run-fields share column
   1 stacked in source order; .job-run-panel spans both rows in column 2
   and stays in view while the settings above it scroll, since the cost,
   the soft-cap checkbox and Run are the entire point of this screen. */
.job-run-grid {
  display: grid; grid-template-columns: 1.3fr 1fr; gap: 20px;
  align-items: start;
}
.job-run-form { display: contents; }
.job-run-left { grid-column: 1; grid-row: 1; }
.job-run-fields { grid-column: 1; grid-row: 2; }
.job-run-panel {
  grid-column: 2; grid-row: 1 / span 2;
  position: sticky; top: 16px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: 16px 18px;
}
.job-run-panel .actions { margin-top: 14px; padding-top: 14px; }
.job-run-panel .confirm-actions {
  flex-direction: column; align-items: stretch;
}
/* The metric-fallback banner renders as .job-run-grid's direct child (the
   include boundary adds no wrapper) - full width, since it REPLACES the
   two-column decision area rather than sharing it with a settings column
   that isn't there. The superseded/blocking refusal text is a different
   case: it stays in .job-run-panel (grid-column:2, below), the same slot
   Run would occupy, since it explains why that action isn't offered. */
.job-run-grid > .cl-offset-alert { grid-column: 1 / -1; }
@media (max-width: 720px) {
  .job-run-grid { grid-template-columns: 1fr; gap: 0; }
  .job-run-left, .job-run-fields, .job-run-panel {
    grid-column: 1; grid-row: auto;
  }
  .job-run-panel {
    position: static; border: none; padding: 0; background: none;
  }
}
