/*
 * Rackulate — canvas-first application shell (v2)
 * -----------------------------------------------------------------------
 * Replaces the old three-column "settings page next to a small preview"
 * shell (formerly in app-shell.css) for warehouse.html and racking.html.
 * That shell read as a CAD tool wrapped in nicer CSS. This one is built
 * from a different premise: one dominant canvas per page, a minimal
 * permanent chrome, and everything else — project details, fine-tune
 * numbers, layer toggles — surfaced only when asked for, as a floating
 * inspector or a bottom sheet, never as a fixed sidebar competing with
 * the canvas for space.
 *
 * Reused unchanged from style.css: .nav / .brand / .theme-toggle / .btn /
 * .icon / .hud-chip / .layer-check / .tool-rail(-compact) / mock-* SVG
 * label classes. Reused unchanged from app-shell.css: .suite-label /
 * .status-pill / .panel-title / .field-grid / .choice-group+chip.
 *
 * Backs the real, live application — every control here drives
 * RMProject state through the shared calculation/geometry engine
 * (js/layout.js, js/racking.js, js/install-calc.js etc.) and autosaves
 * via js/project-storage.js, not a static or presentation-only mockup.
 */

/* ---------------------------------------------------------------------
   Page shell — full viewport, non-scrolling. Header keeps its own
   height; everything below (.canvas-stepper-bar, .canvas-stage,
   .canvas-actions) shares the rest.
--------------------------------------------------------------------- */
body.canvas-body {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
body.canvas-body .nav { flex: none; }
.nav-inner-edge { width: 100%; padding: 0 var(--space-5); }

/* =======================================================================
   Shared application shell — sidebar + top bar + main workspace column
   -----------------------------------------------------------------------
   Master-reference shell (Pic/RackMapper-UI-MASTER-REFERENCE.png):
   a full-height left navigation rail (brand at its top, module links,
   Settings/Help pinned at the bottom) beside a per-page column of
   [.app-topbar, workspace content]. Racking is the reference
   implementation this gets built against first; every other workspace
   page adopts the same classes.
   ======================================================================= */
.app-shell-body { flex: 1; min-height: 0; display: flex; flex-direction: row; overflow: hidden; }
.app-shell-main { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }

/* ---- left application sidebar — collapsed to icon-only by default on
   every workspace page (Warehouse/Racking/Layout/3D/Install/Report —
   visual-correction pass), expandable to icon+label via the toggle for
   anyone who wants the full labelled rail. Collapses via a CSS-only checkbox
   (#sidebarCollapse, a preceding <body> sibling of .app-shell-body —
   same convention as .sheet-toggle elsewhere in this app), no JS
   required. ---- */
.app-sidebar {
  flex: none;
  width: 208px;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-4) var(--space-3);
  border-right: 1px solid var(--border);
  background: var(--bg-elevated);
  transition: width var(--duration-fast) var(--ease-out);
  overflow: hidden;
}
.app-sidebar-toggle-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
.app-sidebar-brand { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); flex: none; padding: 0 4px; }
.app-sidebar-brand-link { display: flex; align-items: center; gap: 8px; text-decoration: none; overflow: hidden; min-width: 0; }
/* Compact lockup for the sidebar (vs. the 24x24 header/footer default in
   css/style.css) — same fixed-pixel-slot pattern, just a smaller slot. */
.app-sidebar-brand-link .brand-mark { flex: none; display: flex; width: 20px; height: 20px; }
.app-sidebar-brand-word { font-family: var(--font-display); font-size: 1.02em; font-weight: 700; color: var(--text); white-space: nowrap; }
.app-sidebar-brand-word strong { color: var(--accent); font-weight: 700; }
.app-sidebar-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  min-height: 32px;
  border-radius: var(--radius-sm);
  color: var(--text-faint);
  cursor: pointer;
  flex: none;
}
.app-sidebar-toggle:hover { background: var(--bg-elevated-2); color: var(--text); }
/* The real focusable element is the hidden #sidebarCollapse checkbox
   (this label just triggers it) — style the visible label when the
   checkbox itself has keyboard focus, same sibling-combinator technique
   already used for its :checked state above. */
#sidebarCollapse:focus-visible ~ .app-shell-body .app-sidebar-toggle { outline: 2px solid var(--accent); outline-offset: 2px; }
.app-sidebar-toggle-icon { width: 1em; height: 1em; transform: rotate(180deg); transition: transform var(--duration-fast) var(--ease-out); }
.app-sidebar-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; overflow-y: auto; }
.app-sidebar-link {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 0 12px;
  border-radius: var(--radius-sm);
  font-size: 0.82em;
  font-weight: 600;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
  flex: none;
}
.app-sidebar-link .icon { width: 1.15em; height: 1.15em; flex: none; }
.app-sidebar-link:hover { background: var(--bg-elevated-2); color: var(--text); }
.app-sidebar-link:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.app-sidebar-link.is-active { background: var(--accent); color: #fff; font-weight: 700; }
.app-sidebar-link-label { overflow: hidden; text-overflow: ellipsis; }
/* Hub navigation pass — the Hub link is always the first item in
   .app-sidebar-nav, above the Setup/Design/Deliver workflow groups. It's
   the application's permanent home (distinct from the Warehouse/Racking/
   Layout/etc. workflow stages below it), so it gets a quiet visual
   break — a hairline rule and extra breathing room underneath — instead
   of just blending into the top of the Setup group. Pure :first-child
   scoping, no new class/markup, no change to .app-sidebar-link itself. */
.app-sidebar-nav > .app-sidebar-link:first-child { margin-bottom: var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--border); }
.app-sidebar-bottom { display: flex; flex-direction: column; gap: 2px; flex: none; border-top: 1px solid var(--border); padding-top: var(--space-3); }
/* Settings/Help (Aug 2026) — real buttons now (js/app-settings.js,
   js/app-help.js), reusing .app-sidebar-link's own look exactly. The only
   addition needed is a plain button reset, since every other
   .app-sidebar-link is an <a>. */
.app-sidebar-link-btn { background: none; border: none; width: 100%; text-align: left; font: inherit; -webkit-appearance: none; appearance: none; cursor: pointer; }
/* Kept for any old markup still using it — a fully inert, unclickable row. */
.app-sidebar-link-disabled { color: var(--text-faint); opacity: 0.55; cursor: default; }
.app-sidebar-link-disabled:hover { background: none; color: var(--text-faint); }

/* ---------------------------------------------------------------------
   MAJOR VISUAL/UX PASS — sidebar section grouping
   -----------------------------------------------------------------------
   Purely additive: small uppercase group labels (Setup/Design/Deliver/
   Compliance/Handover) above their clusters, and a "Soon" tag for the
   RAMS/Inspection placeholders that are honest about not existing yet in
   this build (see index.html §17 — do not invent working features).
   Reuses .app-sidebar-link's existing shape/hover for the placeholder
   entries (already-disabled via .app-sidebar-link-disabled above); no
   existing selector changed, no JS touched. --------------------------- */
.app-sidebar-group-label {
  display: block;
  font-size: 0.62em;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: var(--space-3) 10px 2px;
}
.app-sidebar-nav > .app-sidebar-group-label:first-child { padding-top: 0; }
.app-sidebar-link-soon {
  margin-left: auto;
  font-size: 0.6em;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-faint);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-pill);
  padding: 1px 6px;
  flex: none;
}
/* Collapsed sidebar: hide group labels/soon tags entirely (icon-only
   rail) rather than let them wrap or overflow the 60px collapsed width. */
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-group-label,
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-link-soon { display: none; }

/* Collapsed state (checkbox checked = collapsed; unchecked/default =
   the full expanded rail shown in the reference). */
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar { width: 60px; }
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-brand-word,
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-link-label { display: none; }
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-brand,
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-link { justify-content: center; padding: 0; }
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-toggle-icon { transform: rotate(0deg); }
/* Collapsed rail is icons only: hide the "Coming Soon"/"Dev" badges added by
   js/module-gate.js and never allow horizontal overflow. */
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar .modgate-badge { display: none; }
#sidebarCollapse:checked ~ .app-shell-body .app-sidebar-nav { overflow-x: hidden; }

/* layout.html's Layers and Project Health sheets — bug fix. Both
   toggles (#layersSheetToggle/#healthSheetToggle) sit at body level,
   same as every other page's sheet toggle, but on this page their own
   .sheet-backdrop/.canvas-sheet markup lives inside .app-shell-body
   (nested under the sidebar/topbar shell only this page has), so the
   shared ".sheet-toggle:checked ~ .canvas-sheet" rule above — a plain
   sibling combinator — never matched them: .canvas-sheet wasn't a
   direct sibling of the checkbox, it was a sibling's descendant. This
   silently broke opening either sheet (Project Health button included)
   even though the checkbox itself toggled correctly. Same fix already
   used for #sidebarCollapse just above: reach through the shared
   .app-shell-body ancestor with an ordinary descendant combinator
   instead of relying on direct adjacency. */
#layersSheetToggle:checked ~ .app-shell-body .sheet-backdrop[for="layersSheetToggle"],
#healthSheetToggle:checked ~ .app-shell-body .sheet-backdrop[for="healthSheetToggle"] {
  display: block;
}
#layersSheetToggle:checked ~ .app-shell-body .layers-canvas-sheet,
#healthSheetToggle:checked ~ .app-shell-body .health-canvas-sheet {
  transform: translate(-50%, 0);
}

/* Shared sidebar: one thin scrollbar on every page, and the phone menu button. */
.app-sidebar-nav { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.app-sidebar-nav::-webkit-scrollbar { width: 6px; }
.app-sidebar-nav::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
.rm-nav-menu-btn, .rm-nav-backdrop { display: none; }

@media (max-width: 900px) {
  /* Phone / small tablet: the rail becomes an off-canvas drawer opened by the
     top-bar menu button (js/navigation.js adds body.rm-nav-open). Labels are
     always shown in the drawer, regardless of the saved collapsed state. */
  .rm-nav-menu-btn { display: inline-flex; flex: none; }
  .rm-nav-menu-btn.is-floating { position: fixed; top: 8px; left: 8px; z-index: 60; background: var(--surface, #fff); border: 1px solid var(--border); }
  .app-sidebar {
    display: flex !important; position: fixed; top: 0; bottom: 0; left: 0; z-index: 1000;
    width: min(280px, 82vw) !important; transform: translateX(-102%);
    transition: transform .18s ease; box-shadow: var(--shadow-lg, 0 8px 32px rgba(0,0,0,.35));
    padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom);
  }
  body.rm-nav-open .app-sidebar { transform: none; }
  body.rm-nav-open .rm-nav-backdrop { display: block; position: fixed; inset: 0; z-index: 999; background: rgba(0,0,0,.45); }
  #sidebarCollapse:checked ~ .app-shell-body .app-sidebar-brand-word,
  #sidebarCollapse:checked ~ .app-shell-body .app-sidebar-link-label { display: inline !important; }
  #sidebarCollapse:checked ~ .app-shell-body .app-sidebar-group-label { display: block !important; }
  #sidebarCollapse:checked ~ .app-shell-body .app-sidebar-link-soon,
  #sidebarCollapse:checked ~ .app-shell-body .app-sidebar .modgate-badge { display: inline-block !important; }
  #sidebarCollapse:checked ~ .app-shell-body .app-sidebar-brand,
  #sidebarCollapse:checked ~ .app-shell-body .app-sidebar-link { justify-content: flex-start; padding: 0 10px; }
  .app-sidebar-toggle, label.app-sidebar-toggle { display: none !important; }
}
@media (prefers-reduced-motion: reduce) { .app-sidebar { transition: none; } }

/* ---------------------------------------------------------------------
   Application top bar — one compact row per the master reference:
   workspace title + autosave status on the left, Undo/Redo/Save/primary
   action on the right. Replaces the old header-nav + stepper-bar pair;
   the RackMapper wordmark now lives once, at the top of .app-sidebar.
--------------------------------------------------------------------- */
.app-topbar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding: var(--space-3) var(--space-6);
  padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
  border-bottom: 1px solid var(--border);
  background: var(--bg-elevated);
  min-height: 64px;
}
.app-topbar-left { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.app-topbar-title { font-family: var(--font-display); font-size: 1.15em; font-weight: 700; color: var(--text); margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.app-topbar-right { display: flex; align-items: center; gap: var(--space-2); flex: none; }
.app-topbar-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  min-height: 40px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
.app-topbar-icon-btn:hover:not(:disabled) { background: var(--bg-elevated-2); color: var(--text); }
.app-topbar-icon-btn:disabled { color: var(--text-faint); opacity: 0.4; cursor: default; }
.app-topbar-icon-btn .icon { width: 1.1em; height: 1.1em; }
.app-topbar-sep { width: 1px; height: 24px; background: var(--border); margin: 0 4px; flex: none; }
/* GLOBAL CONSISTENCY PASS — was its own hardcoded green (#22c55e),
   different from document-modules.css's status-pill-green (#2bd67b
   fallback) despite meaning the same "success" state; both now read
   css/tokens.css's shared --ok token so every "success" pill in the
   app is the exact same colour. */
.status-pill-success { background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok); }
.status-pill-success .status-pill-dot { background: var(--ok); }
/* Save status (js/project-storage.js): amber while changes are pending, red when a save failed. */
.status-pill-warn { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.status-pill-warn .status-pill-dot { background: var(--warn); }
.status-pill-danger { background: var(--danger-soft); border-color: var(--danger-line); color: var(--danger); }
.status-pill-danger .status-pill-dot { background: var(--danger); }
[data-save-status] { max-width: 100%; }
[data-save-status] [data-save-status-text] { overflow: hidden; text-overflow: ellipsis; }

/* Persistent save-problem / multi-tab banner (js/project-storage.js). */
.rm-save-banner {
  position: fixed; left: 50%; top: 12px; transform: translateX(-50%); z-index: 9000;
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  width: min(760px, calc(100vw - 24px)); box-sizing: border-box;
  padding: 10px 14px; border-radius: var(--radius-md, 10px);
  background: var(--bg-elevated); color: var(--text); font-size: 0.9em; line-height: 1.35;
  border: 1px solid var(--border-strong); box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}
.rm-save-banner.is-danger { border-color: var(--danger-line); background: var(--danger-soft); background-image: linear-gradient(var(--bg-elevated), var(--bg-elevated)); border-left: 4px solid var(--danger); }
.rm-save-banner.is-warn { border-color: var(--warn-line); background-image: linear-gradient(var(--bg-elevated), var(--bg-elevated)); border-left: 4px solid var(--warn); }
.rm-save-banner-text { flex: 1 1 260px; min-width: 0; }
.rm-save-banner-actions { display: flex; flex-wrap: wrap; gap: 8px; }

@media (max-width: 640px) {
  .app-topbar { padding: var(--space-3) var(--space-4); padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px)); flex-wrap: wrap; gap: var(--space-3); }
  .app-topbar-title { font-size: 1em; }
  /* .app-topbar-right alone (theme/details/undo/redo icon buttons + Save +
     the long-text primary action) is wider than any phone in portrait —
     on a 320px-wide screen its own controls measure ~534px. The parent's
     flex-wrap above only moves the whole left/right groups onto separate
     rows; it can't stop a single group from overflowing its own row. Each
     group is given the full row width and allowed to wrap internally, so
     controls that don't fit drop to a second line instead of clipping or
     forcing the page to scroll horizontally. */
  .app-topbar-left, .app-topbar-right { width: 100%; }
  .app-topbar-right { flex-wrap: wrap; justify-content: flex-start; row-gap: var(--space-2); }
  /* Title truncation fix — .app-topbar-left's own later siblings (the
     "Auto-saved" status pill, the Stage Guide chip) are fixed-width flex
     items competing with the title for the same single row; since the
     title's min-width:0 (inherited) lets it shrink before those siblings
     do, on a phone-width row it was squeezed down to "Wareh...". The
     classic flex trick — give the FIRST item that should start a new row
     flex-basis:100% — pushes the status pill (and the Stage Guide chip
     after it in DOM order) onto their own row, so line 1 is just the
     Home/Back icon button(s) + the title, which is what actually needs
     the width. Scoped to .app-topbar-left's own status pill only, not
     the shared .status-pill class used elsewhere on these pages. Title
     keeps its existing ellipsis rule as a safety net only, for a name
     too long to fit even that freed-up row. */
  .app-topbar-left { flex-wrap: wrap; row-gap: var(--space-2); }
  .app-topbar-left .status-pill { flex-basis: 100%; }
}

/* ---------------------------------------------------------------------
   Stepper bar — replaces the old cramped workflow row. Larger step
   circles, a connecting line, and the current step's name spelled out;
   done/future steps stay quiet numbers so the eye goes straight to
   where you are. Project identity sits on the right, not wedged into
   the middle.
--------------------------------------------------------------------- */
.canvas-stepper-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-6);
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--border);
  background: var(--bg-elevated);
}
.canvas-stepper { display: flex; align-items: center; gap: var(--space-2); }
.canvas-step {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.86em;
  font-weight: 700;
  color: var(--text-faint);
  text-decoration: none;
}
/* Non-active steps are real links to that stage — genuine destinations,
   so they navigate rather than sitting there as inert progress dots. */
a.canvas-step { cursor: pointer; }
a.canvas-step:hover { color: var(--text); }
a.canvas-step:hover .canvas-step-dot { border-color: var(--accent-line); color: var(--accent); }
.canvas-step-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  font-size: 0.78em;
  font-weight: 800;
  background: var(--bg-sunken);
  border: 1.5px solid var(--border-strong);
  color: var(--text-faint);
  flex: none;
  transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.canvas-step.is-done .canvas-step-dot { background: var(--bg-elevated-2); border-color: var(--border-strong); color: var(--text-muted); }
.canvas-step.is-active .canvas-step-dot { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: 0 0 0 4px var(--accent-soft); }
.canvas-step.is-active { color: var(--text); }
.canvas-step-label { white-space: nowrap; }
/* UX clarity pass: the stage journey (Warehouse -> Racking -> Layout ->
   RM Install) must be permanently obvious, the way production's own
   always-labelled wizard bar is — labels no longer hide on inactive
   steps at desktop/laptop widths. Still collapses to numbers-only on
   tablet/phone (see the max-width:900px override below) where width is
   genuinely scarce. */
.canvas-step-line { width: 28px; height: 2px; background: var(--border-strong); flex: none; }

.canvas-identity {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.canvas-identity-name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-display);
  font-size: 0.95em;
  font-weight: 700;
  color: var(--text);
  white-space: nowrap;
}
.canvas-identity-name .icon { color: var(--accent); }
.canvas-identity-dims { font-size: 0.82em; font-weight: 600; color: var(--text-faint); white-space: nowrap; }

.canvas-details-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 40px;
  padding: 8px 16px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-strong);
  background: var(--bg-elevated-2);
  color: var(--text);
  font-size: 0.84em;
  font-weight: 700;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
}
.canvas-details-btn:hover { border-color: var(--accent-line); color: var(--accent); background: var(--accent-soft); }
.canvas-details-btn .icon { width: 1em; height: 1em; }

/* ---------------------------------------------------------------------
   The stage — one dominant, full-bleed canvas. Same dot-grid ground as
   the old .preview-canvas, kept because it reads as "drawing surface"
   without saying so in words.
--------------------------------------------------------------------- */
.canvas-stage {
  flex: 1;
  min-height: 0;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-7);
  background:
    linear-gradient(var(--border) 1px, transparent 1px),
    linear-gradient(90deg, var(--border) 1px, transparent 1px);
  background-size: 34px 34px;
  background-position: center;
  overflow: hidden;
}
.canvas-stage svg { position: relative; z-index: 1; width: auto; height: 100%; max-width: 100%; max-height: 100%; }
/* Batch 2 (Warehouse UX): a plain crosshair while any placement tool is
   armed (Column/Door/Fire Exit/Walkway/Obstacle), so it's visually
   obvious the next click/drag on the canvas places something. Toggled
   by js/warehouse.js's armTool(). */
.canvas-stage.is-placing svg { cursor: crosshair; }

/* ---------------------------------------------------------------------
   warehouse.html workspace — canvas-dominant layout (Major Visual/UX
   pass): the drawing canvas is the MAIN area and gets the majority of
   the width (flex:1, order:1); Warehouse Dimensions/Tools/Site Survey
   sit together as one right-hand inspector rail (order:2, fixed
   width), matching the same "dominant canvas + right-hand structured
   inspector" pattern used on Racking's .rack-spec-rail. DOM order and
   every existing id/wiring in js/warehouse.js are unchanged — only
   flex `order` and the border edge moved.
--------------------------------------------------------------------- */
.wh-workspace { flex: 1; min-height: 0; display: flex; overflow: hidden; }
/* Scoped to .wh-workspace (not the shared base .canvas-stage rule) so
   Layout/Racking/Install's own use of .canvas-stage is never affected —
   only Warehouse's canvas needs to be pulled ahead of its now
   right-hand .wh-tools-rail. */
.wh-workspace .canvas-stage { order: 1; }
.wh-tools-rail {
  flex: none;
  /* Widened from 220px — this rail now also carries the Warehouse
     Dimensions fields (moved from the old right-hand rail) above the
     placement tools, so it needs the extra room those inputs need. */
  width: 300px;
  overflow-y: auto;
  padding: var(--space-5);
  border-left: 1px solid var(--border);
  background: var(--bg-elevated);
  display: flex;
  flex-direction: column;
  order: 2;
}
/* Compact list rows, not full-height pill buttons — matches the
   approved reference's Tools list (icon + label, ~34px rows, active
   item picked out with a soft fill, everything else flush against the
   rail edge). Scoped to .wh-tool-list so the shared floating-pill
   .canvas-dock-btn used elsewhere (its own zoom pill, Racking's dock)
   is untouched. */
.wh-tool-list { display: flex; flex-direction: column; gap: 1px; margin-top: var(--space-3); }
.wh-tool-list .canvas-dock-btn { width: 100%; justify-content: flex-start; min-height: 34px; padding: 0 10px; border-radius: var(--radius-sm); }
/* Toolbar polish — Select vs. the placement-tool group beneath it get a
   thin divider (see racking.html-style grouping), and the active tool
   picks up a left accent bar on top of the shared soft-fill .is-active
   state (defined once, further down, for every .canvas-dock-btn
   context) so "which tool is armed" reads at a glance in a vertical
   list, not just a colour shift. Scoped to .wh-tool-list only — the
   shared base .canvas-dock-btn.is-active rule used elsewhere (Racking's
   dock, the zoom pill) is untouched. */
.wh-tool-list-divider { height: 1px; background: var(--border); margin: 5px 0; }
.wh-tool-list .canvas-dock-btn.is-active { border-left: 2px solid var(--accent); padding-left: 8px; font-weight: 700; }

/* Wall Clearance stepper — same full-width, space-between treatment
   Racking's rail already gives every stepper in .rack-spec-rail, so the
   one stepper in this rail reads consistently with the rest of the
   product rather than the smaller default inline pill. */
.wh-tools-rail .big-stepper.stepper-sm { width: 100%; justify-content: space-between; padding: 6px 8px; gap: var(--space-3); }
.wh-tools-rail .stepper-sm .big-stepper-btn { width: 34px; height: 34px; font-size: 1em; flex: none; }
.wh-tools-rail .stepper-sm .big-stepper-value { flex: 1; min-width: 4.5ch; font-size: 1.15em; }

/* Site Survey counts — a compact icon+label+value list (reference's own
   presentation) instead of wrapped pill chips, so 5 categories cost
   ~150px instead of two wrapped rows of oversized chips. */
.wh-survey-counts { display: flex; flex-direction: column; gap: 1px; margin: var(--space-3) 0; }
.wh-survey-count-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px;
  border-radius: var(--radius-sm);
  font-size: 0.78em;
  font-weight: 600;
  color: var(--text-muted);
}
.wh-survey-count-row .icon { width: 15px; height: 15px; flex: none; color: var(--text-faint); }
.wh-survey-count-row-label { flex: 1; min-width: 0; }
.wh-survey-count-row-value { font-family: var(--font-display); font-weight: 700; color: var(--text); }

/* Compact expand-on-focus notes field, replacing the old fixed 3-row
   textarea — collapsed it reads as one clean input row; focusing it
   grows the writing area instead of permanently reserving the space.
   Same #siteSurveyNotes id/data binding as before, styling only. */
.wh-notes-textarea {
  width: 100%;
  min-height: 40px;
  max-height: 40px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
  font-size: 0.8em;
  line-height: 1.35;
  resize: none;
  overflow: hidden;
  box-sizing: border-box;
  transition: max-height var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.wh-notes-textarea::placeholder { color: var(--text-faint); }
.wh-notes-textarea:hover { border-color: var(--border-strong); }
.wh-notes-textarea:focus {
  max-height: 130px;
  overflow-y: auto;
  outline: none;
  background: var(--bg-elevated);
  border-color: var(--accent-line);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Live preview rect while drawing a Walkway by click+drag (Batch 2,
   Part 5) — dashed accent outline, semi-transparent fill, purely
   visual (pointer-events off so it never intercepts its own drag). */
.wh-walkway-preview {
  fill: var(--accent-soft);
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-dasharray: 5 3;
  pointer-events: none;
}

/* Subtle, app-styled scrollbars for the tools rail + the Site Survey
   list — if content genuinely overflows a short viewport, it should
   read as a slim internal scroll affordance, never a fat OS-default
   scrollbar dominating the side of the screen. */
.wh-tools-rail, .survey-list {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.wh-tools-rail::-webkit-scrollbar, .survey-list::-webkit-scrollbar { width: 6px; }
.wh-tools-rail::-webkit-scrollbar-track, .survey-list::-webkit-scrollbar-track { background: transparent; }
.wh-tools-rail::-webkit-scrollbar-thumb, .survey-list::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--radius-pill); }

@media (max-width: 1180px) {
  .wh-tools-rail { width: 260px; }
}
@media (max-width: 900px) {
  .wh-workspace { flex-direction: column; overflow-y: auto; }
  /* max-height + this rail's own overflow-y:auto (base rule above) used to
     give it a second, nested scroll region inside .wh-workspace's own
     scroll — on a touch screen the drag naturally scrolls whichever
     region is under the finger first, so swiping over this rail scrolled
     IT to its own bottom (Site Survey, its last section) without ever
     reaching the rest of the page, making the Warehouse Dimensions
     fields above Site Survey seem to disappear. Letting the rail size to
     its full content height removes the inner scroll region entirely, so
     .wh-workspace is the only scroller and one swipe reaches everything
     in order: canvas, dimensions, Site Survey. */
  .wh-tools-rail { width: auto; max-height: none; overflow-y: visible; border-left: none; border-right: none; border-top: 1px solid var(--border); order: 2; }
  /* flex:1 on .canvas-stage inside a now content-driven (not viewport-
     bound) scroll column had no upper limit, so it could inflate to fill
     most of the screen and push the dimensions fields further down than
     necessary. A fixed viewport-relative height keeps the canvas usefully
     sized without starving the fields/Site Survey below it.
     Scoped to .wh-workspace specifically — .canvas-stage is the same
     class layout.html's drawing canvas uses, and that page has no
     competing content below it fighting for room the way Warehouse's
     dimension fields + Site Survey do, so it should keep its original
     flex:1 (full available height) rather than inherit this 45vh cap. */
  .wh-workspace .canvas-stage { order: 1; flex: none; height: 45vh; min-height: 280px; }
}

/* ---------------------------------------------------------------------
   Layout & Design — visual-correction pass. Left compact labelled tool
   rail + dominant centre canvas + bottom CAD status bar, replacing the
   old floating pill dock / scattered readout chips / floating zoom
   pill. Same structural family as .wh-workspace/.wh-tools-rail above,
   parameterised for Layout's own tool set.
--------------------------------------------------------------------- */
.layout-workspace { flex: 1; min-height: 0; display: flex; overflow: hidden; }
.layout-tools-rail {
  flex: none;
  width: 196px;
  overflow-y: auto;
  padding: var(--space-4) var(--space-3);
  border-right: 1px solid var(--border);
  background: var(--bg-elevated);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.layout-tool-group { display: flex; flex-direction: column; gap: 2px; }
.layout-tool-group-label {
  font-size: 0.66em;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-faint);
  padding: 0 10px 2px;
}
.layout-tool-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  padding: 0 10px;
  border-radius: var(--radius-sm);
  font-size: 0.82em;
  font-weight: 700;
  /* Grey background + accent (purple in dark mode) text at rest — the
     previous dimmed off-white-on-off-white treatment (--text at 0.55
     opacity) was hard to read until hovered. A solid grey surface with
     full-strength accent-coloured text reads clearly at rest, matching
     the rest of the app's button language. */
  background: var(--bg-elevated-2);
  border: 1px solid transparent;
  color: var(--accent);
  opacity: 1;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), opacity var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.layout-tool-btn .icon { width: 1.1em; height: 1.1em; flex: none; }
.layout-tool-btn:hover { background: var(--accent-soft); color: var(--accent); }
.layout-tool-btn:active:not(:disabled) { transform: scale(0.97); }
.layout-tool-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.layout-tool-btn.is-active { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); }
.layout-tool-btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }

/* Secondary view utilities (Pan/Fit/Reset/Paste/Health/Report) — a
   compact icon-only row pinned to the rail's bottom, still reachable
   but not competing with the labelled primary tools above for space. */
.layout-tool-rail-bottom {
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.layout-tool-rail-bottom button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  color: var(--text-faint);
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out);
}
.layout-tool-rail-bottom button .icon { width: 1.05em; height: 1.05em; }
.layout-tool-rail-bottom button:hover { background: var(--bg-elevated-2); color: var(--text); }
.layout-tool-rail-bottom button:active:not(:disabled) { transform: scale(0.97); }
.layout-tool-rail-bottom button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.layout-tool-rail-bottom button.is-active { background: var(--accent-soft); color: var(--accent); }
.layout-tool-rail-bottom button:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }

/* ---- bottom CAD status bar — compact, always the same height,
   replaces the old floating top-left readout chips + floating
   bottom-right zoom pill with one calm strip of real numbers, the way
   a real planning application reports model/view state. ---- */
.layout-status-bar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border);
  background: var(--bg-elevated);
}
.layout-status-group { display: flex; align-items: center; gap: var(--space-5); }
.layout-status-item { display: flex; align-items: baseline; gap: 8px; }
/* Readout stats (Aug 2026: enlarged for readability) — bumped up from
   0.92em/0.68em; the bar's own padding and the separator height grow to
   match so the larger text still sits centred and neatly aligned rather
   than looking cramped inside the same-height strip. */
.layout-status-item strong { font-family: var(--font-display); font-size: 1.28em; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap; }
.layout-status-item label { font-size: 0.78em; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--text-faint); }
.layout-status-sep { width: 1px; height: 28px; background: var(--border-strong); flex: none; }
/* Zoom control reused inline in the status bar instead of its own
   floating pill — same buttons/ids/behaviour, just not overlapping the
   drawing any more. Sized up (Aug 2026) well beyond the shared
   .canvas-zoom-btn/.canvas-zoom-value base (34px btn / 0.78em value,
   still used by the floating pill on Warehouse/Racking) — scoped to this
   inline variant only so those other pages are untouched. */
.canvas-zoom-inline { position: static; border: none; box-shadow: none; background: none; backdrop-filter: none; padding: 0; gap: 4px; }
.canvas-zoom-inline .canvas-zoom-btn { width: 44px; height: 44px; font-size: 1.3em; }
.canvas-zoom-inline .canvas-zoom-value { min-width: 4.6ch; font-size: 1.05em; font-weight: 700; color: var(--text); }

@media (max-width: 1180px) {
  .layout-tools-rail { width: 172px; }
}
@media (max-width: 900px) {
  .layout-workspace { flex-direction: column; overflow-y: auto; }
  .layout-status-group-zoom { width: 100%; justify-content: center; }
  .layout-tools-rail {
    width: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--space-3);
    max-height: none;
    padding: var(--space-2) var(--space-4);
    border-right: none;
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
    overflow-y: hidden;
    order: 2;
  }
  .layout-tool-group { flex-direction: row; gap: 4px; }
  .layout-tool-group-label { display: none; }
  /* 44px is this app's own established minimum touch target (see the
     iPhone-fix block further down this file) — the icon-only rail row
     was still using its desktop 40px height. */
  .layout-tool-btn { padding: 0 10px; min-height: 44px; }
  .layout-tool-btn span { display: none; }
  .layout-tool-rail-bottom { margin-top: 0; padding-top: 0; border-top: none; border-left: 1px solid var(--border); padding-left: var(--space-3); }
  .layout-tool-rail-bottom button { width: 44px; height: 44px; }
  .canvas-stage { order: 1; min-height: 320px; }
  /* Root-cause fix (Layout mobile redesign pass): .layout-workspace
     switches to a plain scrolling column here, so .canvas-stage no
     longer sits inside a fixed-height flex row — it has no definite
     height of its own, just the min-height above. #layoutSvg's own
     `height: 100%` (see .canvas-stage svg, shared rule) only resolves
     against a DEFINITE parent height per the CSS spec; min-height alone
     doesn't count, so the drawing itself was sized unpredictably (often
     far smaller than the 320px box around it) instead of filling it.
     Same fix already proven for warehouse.html's structurally identical
     .wh-workspace .canvas-stage just above (45vh/280px) — a real vh
     height gives the SVG something definite to fill against, and keeps
     the warehouse drawing usefully visible above the tool rail/status
     bar rather than collapsing. Scoped to .layout-workspace so it can't
     affect the shared bare .canvas-stage rule other pages may still
     rely on. */
  .layout-workspace .canvas-stage { flex: none; height: 48vh; min-height: 320px; }
  .layout-status-bar { flex-wrap: wrap; gap: var(--space-3); order: 3; }
}
@media (max-width: 640px) {
  .layout-status-group { flex-wrap: wrap; gap: var(--space-3); }
  .layout-status-item label { display: none; }
}

/* ---------------------------------------------------------------------
   layout.html — reuses .canvas-stage/.canvas-readout for the shell;
   these are the page's own bits: the run-label text style, and the two
   states (generated / nothing generated yet).
--------------------------------------------------------------------- */
.layout-canvas-wrap { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
#layoutSvg .layout-run-label {
  font-family: var(--font-body, inherit);
  font-size: 11px;
  font-weight: 700;
  fill: var(--text);
  pointer-events: none;
}
/* CAD-quality rack geometry — Batch 4 visual-redesign pass. Colour
   language now matches the Racking Configurator's own frame/beam palette
   (Batch 3) so a run looks like the same physical product whether you're
   looking at the plan (Layout) or the elevation (Racking): uprights are
   the same structural blue, beams the same structural orange. Both are
   deliberately colour-blind-safe, distinct from purple (accent/selection
   only) and from the red/amber/green already used for clash/relocated/
   fire-exit — uprights and beams read as real structure, not another
   status colour.
   Opacity composition (Part 5 scale-aware detail + locked-run dimming):
   --layout-lod-opacity is written on #layoutSvg itself by
   updateLodOpacity() (js/layout.js), on every render AND on every zoom/
   pinch step (zoom alone never re-runs render(), so this has to be able
   to update independently — see the comment beside that function) — a
   single continuous 0-1 value for how much secondary rack detail the
   current zoom level warrants. --lock is set per-element only on runs
   that are actually locked. Composing them with calc() rather than two
   separate opacity rules is required: opacity rules don't stack, the
   most specific one simply wins, so end posts (always fully visible
   structure) only ever see --lock, while intermediate posts and bay-
   division lines (genuinely secondary at far zoom) see both. */
#layoutSvg .layout-run-beam { stroke: #c2540d; stroke-width: 1.4; opacity: 0.9; pointer-events: none; }
#layoutSvg .layout-run-upright { fill: #2f6fed; stroke: #1d4ed8; stroke-width: 0.5; pointer-events: none; opacity: var(--lock, 1); }
#layoutSvg .layout-run-upright.is-end-post { fill: #1d4ed8; }
#layoutSvg .layout-run-upright.is-lod-detail { opacity: calc(var(--lock, 1) * var(--layout-lod-opacity, 1)); }
#layoutSvg .layout-run-bay-line { pointer-events: none; }
#layoutSvg .layout-run-bay-line.is-lod-detail { opacity: calc(0.4 * var(--layout-lod-opacity, 1)); }
#layoutSvg .layout-run-protection { fill: var(--warn-soft); stroke: var(--warn); stroke-width: 1; pointer-events: none; }
#layoutSvg .layout-run-dim-label {
  font-family: var(--font-body, inherit);
  font-size: 9px;
  font-weight: 700;
  fill: var(--text-faint);
  pointer-events: none;
}

.layout-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
  max-width: 360px;
  position: relative;
  z-index: 1;
}
.layout-empty-state .icon { width: 40px; height: 40px; color: var(--text-faint); margin-bottom: 4px; }
.layout-empty-title { font-size: 1.1em; font-weight: 800; margin: 0; }
.layout-empty-body { font-size: 0.88em; color: var(--text-muted); margin: 0 0 6px; line-height: 1.5; }

/* ---------------------------------------------------------------------
   layout.html — Phase D1 core run editing. Reuses .canvas-dock/
   .canvas-inspector/.canvas-readout unchanged from the shared canvas
   shell above (same components warehouse.html/racking.html already
   use) — these are just the run-drawing states those components react
   to: selected/locked/shaking, plus the resize handles and lock badge
   drawn directly into the SVG by js/layout.js's render().
--------------------------------------------------------------------- */
#layoutSvg { touch-action: none; }
/* #whSvg has the same pointer-drag gestures (resize handles, obstacle
   drag) as #layoutSvg above — found missing its touch-action:none during
   the Phase D6 cleanup pass, which on iOS Safari lets the browser's own
   scroll/pan gesture fight with a drag in progress. */
#whSvg { touch-action: none; }

.layout-run { cursor: grab; }
.layout-run.is-locked { cursor: default; }
.layout-run-lock-icon { color: var(--warn); pointer-events: none; }
.layout-resize-handle {
  fill: var(--bg-elevated);
  stroke: var(--accent);
  stroke-width: 1.5;
  cursor: nwse-resize;
}

/* A locked run that was just clicked/dragged/rotated/deleted gets a
   brief shake rather than a silent no-op, so "why didn't that work"
   never has to be guessed at — the inline message underneath explains
   why. */
@keyframes layoutRunShake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-3px); }
  40% { transform: translateX(3px); }
  60% { transform: translateX(-2px); }
  80% { transform: translateX(2px); }
}
.layout-run.is-shake { animation: layoutRunShake 0.4s ease-in-out; }

/* Inline feedback strip for rejected actions — floats just above the
   dock, never a browser alert(). No auto-dismiss (matches production's
   own equivalent message area): stays until the next message replaces
   it or the page state moves on. */
.layout-action-message {
  position: absolute;
  left: 50%;
  bottom: calc(var(--space-6) + 64px);
  transform: translateX(-50%);
  align-items: center;
  gap: 8px;
  max-width: min(560px, calc(100% - var(--space-6) * 2));
  padding: 9px 18px;
  border-radius: var(--radius-pill);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-md);
  font-size: 0.82em;
  font-weight: 700;
  color: var(--text);
  text-align: center;
  z-index: 5;
}
.layout-action-message.is-warn { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.layout-action-message.is-success { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

/* Dock buttons disabled when there's no selection to act on (Rotate/
   Lock/Delete/Paste/Undo/Redo) — same visual language as
   .segmented-btn[aria-disabled] elsewhere: clearly inert, never silently
   unresponsive. */
.canvas-dock-btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
/* Same treatment, span-compatible — warehouse.html's dock tools/Add Item
   are <span>s (no native :disabled), locked via .is-disabled until valid
   Length/Width exist (js/warehouse.js's syncPlacementToolsAvailability()). */
.canvas-dock-btn.is-disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }

/* ---------------------------------------------------------------------
   layout.html — Phase D2 run property editor + context menu. The
   inspector is the same floating .canvas-inspector card from D1, just
   wider and scrollable so it can hold every production run field
   without ever squeezing the canvas into a narrow strip.
--------------------------------------------------------------------- */
/* Widened from 300px and given a hard overflow-x guard — a real
   customer's browser showed horizontal scroll inside the selected-run
   inspector at the old width, which reads as unfinished on what should
   feel like a Figma/Apple-grade property panel. 320px keeps two-column
   field rows (.ep-field-grid) comfortably clear of their content at
   every zoom level without crowding the canvas it floats over. */
/* Widened substantially (380px -> 520px -> 620px) so the Run Inspector's
   two-column field grids (DIMENSIONS/STORAGE/SPACING/EQUIPMENT &
   PROTECTION) have real horizontal room and never need their own
   scrollbar just to fit, and taller (720px -> 820px max-height) so more
   of a run's fields are visible without scrolling on a full-height
   desktop window. Capped against the viewport so it can never overflow
   narrower/shorter windows, and extra padding to match the larger card.
   Selector bumped to the compound .canvas-inspector.canvas-inspector-wide
   (was plain .canvas-inspector-wide) — with equal specificity, the base
   .canvas-inspector rule further down the file (width:240px, the small
   generic popup size) was winning the cascade on width/padding purely by
   coming later in source order, silently capping every "wide" instance
   back down to 240px regardless of the values here. This is the actual
   fix for "the run inspector looks too small" — the wide variant simply
   wasn't applying. */
.canvas-inspector.canvas-inspector-wide { width: min(620px, calc(100vw - 64px)); max-height: min(90vh, 820px); padding: var(--space-5); display: flex; flex-direction: column; overflow-x: hidden; }
.canvas-inspector-wide .ep-inspector-body {
  overflow-y: auto;
  overflow-x: hidden;
  flex: 1;
  min-height: 0;
  /* Same subtle app-styled scrollbar as the workspace rails (see
     .wh-tools-rail/.rack-spec-rail above) instead of the
     OS-default bar, now that the panel itself is wide enough that content
     rarely needs to scroll sideways at all. */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.canvas-inspector-wide .ep-inspector-body::-webkit-scrollbar { width: 6px; }
.canvas-inspector-wide .ep-inspector-body::-webkit-scrollbar-track { background: transparent; }
.canvas-inspector-wide .ep-inspector-body::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--radius-pill); }

.ep-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.ep-field-label { font-size: 0.76em; font-weight: 700; color: var(--text-faint); display: flex; flex-direction: column; gap: 1px; }
.ep-field-hint { font-size: 0.92em; font-weight: 500; color: var(--text-faint); opacity: 0.85; }
.ep-field input, .ep-field select, .ep-field textarea {
  min-height: 34px;
  padding: 5px 8px;
  font-family: var(--font-sans);
  font-size: 0.86em;
  font-weight: 700;
  color: var(--text);
  background: var(--bg-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  width: 100%;
}
.ep-field textarea { min-height: 52px; font-weight: 500; resize: vertical; }
.ep-field input:disabled, .ep-field select:disabled { opacity: 0.5; cursor: not-allowed; }
.ep-field input:focus, .ep-field select:focus, .ep-field textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.ep-field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.ep-field-grid .ep-field { margin-bottom: 10px; }

.ep-lock-note {
  display: flex; align-items: flex-start; gap: 6px;
  margin: 0 0 10px; padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--warn-soft); border: 1px solid var(--warn-line); color: var(--warn);
  font-size: 0.78em; font-weight: 600; line-height: 1.4;
}
.ep-lock-note .icon { width: 1.1em; height: 1.1em; flex: none; margin-top: 1px; }

.ep-action-row { display: flex; gap: 6px; margin-bottom: 8px; }
.ep-action-row .btn { flex: 1; justify-content: center; padding: 0 8px; font-size: 0.78em; }
.ep-action-row .btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* Selected-run action row's "More" flyout — Copy/Delete, one tap behind
   the four primary actions (Rotate/Duplicate/Lock/More). Anchored to its
   own wrapper (not a modal) so it never blocks the rest of the panel;
   dismissed on outside click same as the canvas context menu. */
.ep-more-menu-wrap { position: relative; flex: 1; display: flex; }
.ep-more-menu-wrap > .btn { width: 100%; }
.ep-more-menu {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  width: 160px;
  flex-direction: column;
  padding: 6px;
  border-radius: var(--radius-md);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
  z-index: 20;
}
.ep-more-menu.is-open { display: flex; }
.ep-more-menu button {
  display: flex; align-items: center; gap: 10px;
  min-height: 40px; padding: 0 10px;
  border-radius: var(--radius-sm);
  font-size: 0.85em; font-weight: 600; color: var(--text);
  background: none; border: none; text-align: left; cursor: pointer;
}
.ep-more-menu button:hover { background: var(--accent-soft); color: var(--accent); }
.ep-more-menu button:hover .icon { color: var(--accent); }
.ep-more-menu button .icon { width: 1.1em; height: 1.1em; flex: none; color: var(--text-muted); }
.ep-more-menu button:disabled { opacity: 0.4; cursor: not-allowed; background: none; color: var(--text); }
.ep-more-menu-danger { color: #dc2626 !important; }
.ep-more-menu-danger .icon { color: #dc2626 !important; }
.ep-more-menu-danger:hover { background: rgba(220, 38, 38, 0.08) !important; }

.ep-more-toggle {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; min-height: 34px; margin: 4px 0 10px;
  border-radius: var(--radius-sm);
  border: 1px dashed var(--border-strong);
  background: none; color: var(--text-muted);
  font-size: 0.78em; font-weight: 700; cursor: pointer;
}
.ep-more-toggle:hover { color: var(--accent); border-color: var(--accent-line); }
.ep-more-toggle .icon { width: 1em; height: 1em; transition: transform var(--duration-fast) var(--ease-out); }

.ep-more-body { padding-top: 2px; border-top: 1px solid var(--border); }
.ep-section-title {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-display); font-size: 0.72em; font-weight: 800;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-faint);
  margin: 14px 0 8px;
}
/* Layout visual/UX pass: reuse the same accent dot marker Racking's
   .sheet-section-title already uses (css/app-shell.css .panel-title-dot)
   for its RUN/FRAME/BEAMS/PALLETS group headings — purely a hierarchy/
   scannability aid, no behaviour change. Sized down slightly (5px vs the
   7px default) to sit right in this smaller floating-inspector heading. */
.ep-section-title .panel-title-dot { width: 5px; height: 5px; flex: none; }
.ep-section-title:first-child { margin-top: 4px; }
.ep-section-title .ep-field-hint { text-transform: none; letter-spacing: 0; font-size: 1em; }

/* Context menu — a small anchored popover (not a modal), same pattern as
   .type-chooser elsewhere: fixed position, dismiss on outside click or
   Escape, disabled items greyed rather than hidden. */
.context-menu {
  position: fixed;
  width: 200px;
  flex-direction: column;
  padding: 6px;
  border-radius: var(--radius-md);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
  z-index: 60;
}
.context-menu.is-open { display: flex; }
.context-menu button {
  display: flex; align-items: center; gap: 10px;
  min-height: 40px; padding: 0 10px;
  border-radius: var(--radius-sm);
  font-size: 0.85em; font-weight: 600; color: var(--text);
  background: none; border: none; text-align: left; cursor: pointer;
}
.context-menu button:hover { background: var(--accent-soft); color: var(--accent); }
.context-menu button:hover .icon { color: var(--accent); }
.context-menu button .icon { width: 1.1em; height: 1.1em; flex: none; color: var(--text-muted); }
.context-menu button:disabled { opacity: 0.4; cursor: not-allowed; background: none; color: var(--text); }
.context-menu button:disabled .icon { color: var(--text-faint); }
.context-menu-divider { height: 1px; background: var(--border); margin: 4px 6px; }
.context-menu-danger { color: #dc2626 !important; }
.context-menu-danger .icon { color: #dc2626 !important; }
.context-menu-danger:hover { background: rgba(220, 38, 38, 0.08) !important; }

/* ---------------------------------------------------------------------
   Phase D3 — layers, view controls, measurements
--------------------------------------------------------------------- */

/* Reused .canvas-zoom/.canvas-zoom-btn (defined below, previously unused)
   now hold real <button>s instead of the plain <span>s they were built
   for — strip default button chrome so they look identical. */
.canvas-zoom-btn {
  border: none;
  background: none;
  font: inherit;
  font-size: 1.1em;
  padding: 0;
}

/* Cursor/affordance states on the canvas while a D3 mode is active. */
#layoutCanvasWrap.is-pannable { cursor: grab; }
#layoutCanvasWrap.is-pannable:active { cursor: grabbing; }
#layoutCanvasWrap.is-space-pan { cursor: grab; }
#layoutCanvasWrap.is-measuring { cursor: crosshair; }
#layoutCanvasWrap.is-measuring .layout-run { cursor: crosshair; }

/* Grid / dimensions overlay drawn by render(). */
#layoutSvg .layout-grid-line { stroke: var(--border); stroke-width: 0.5; stroke-dasharray: 2 3; opacity: 0.6; }
/* Legibility touch-up (demo-polish pass): --text-muted reads clearly
   against the canvas in both themes, where --text-faint was borderline —
   visual only, no change to what's measured or how it's positioned. */
#layoutSvg .layout-dim-line { stroke: var(--text-muted); stroke-width: 1; }
#layoutSvg .layout-dim-label {
  font-family: var(--font-body, inherit);
  font-size: 10.5px;
  font-weight: 700;
  fill: var(--text-muted);
}

/* Obstacles / Fire exits / Walkways layer — simple coloured+labelled
   rects (see render()'s comment on why full per-type production
   graphics are out of scope for D3). Colours chosen for contrast against
   both themes and against the racking fill, not to encode meaning by
   hue alone (each type also gets its own icon). */
#layoutSvg .layout-obstacle-rect { stroke-width: 1.25; opacity: 0.9; }
#layoutSvg .layout-obstacle-column,
#layoutSvg .layout-obstacle-low_beam { fill: var(--warn-soft); stroke: var(--warn); }
/* Fire exits read as green on a real fire/egress drawing, not red (red
   is reserved for clashes/errors elsewhere on this canvas) — visual-
   correction pass. Walkways get a readable, distinct blue so they don't
   fall back to the same neutral grey as every other minor obstacle. */
#layoutSvg .layout-obstacle-fire_exit { fill: rgba(22, 163, 74, 0.16); stroke: #16a34a; }
#layoutSvg .layout-obstacle-walkway { fill: rgba(14, 165, 233, 0.1); stroke: #0ea5e9; stroke-dasharray: 4 3; }
#layoutSvg .layout-obstacle-door,
#layoutSvg .layout-obstacle-office,
#layoutSvg .layout-obstacle-floor_issue,
#layoutSvg .layout-obstacle-keep_clear,
#layoutSvg .layout-obstacle-charging,
#layoutSvg .layout-obstacle-turning { fill: var(--bg-elevated-2); stroke: var(--text-faint); }
#layoutSvg .layout-obstacle-icon { color: var(--text-muted); pointer-events: none; }

/* Measurement lines/labels/action buttons (renderMeasurementLine()). */
#layoutSvg .layout-measure-label-text {
  font-family: var(--font-sans);
  font-size: 10px;
  font-weight: 700;
  fill: var(--text);
}
#layoutSvg .layout-measure-line.is-pinned .layout-measure-label-group { cursor: grab; }
#layoutSvg .layout-measure-action-btn { cursor: pointer; }
#layoutSvg .layout-measure-action-btn circle { transition: opacity var(--duration-fast) var(--ease-out); }
#layoutSvg .layout-measure-action-btn:hover circle { opacity: 0.8; }

/* Measure sub-toolbar — a floating strip of the 7 production measurement
   tools + pinned-dimensions visibility, shown only while Measure mode is
   active (js/layout.js renderMeasureSubToolbar()). */
.measure-sub-toolbar {
  position: absolute;
  top: var(--space-5);
  left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  max-width: calc(100% - 32px);
  padding: 8px;
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--bg-elevated) 92%, transparent);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-md);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.measure-tool-btn {
  min-height: 36px;
  padding: 6px 12px;
  font-family: var(--font-sans);
  font-size: 0.82em;
  font-weight: 700;
  color: var(--text-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}
.measure-tool-btn:hover { background: var(--bg-elevated-2); color: var(--text); }
.measure-tool-btn.is-active { background: var(--measure-active); border-color: var(--measure-active); color: #fff; }
.measure-pinned-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  font-size: 0.8em;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}

/* Layers & Grid sheet content — reuses .canvas-sheet-body's own padding;
   these are just the row/grid pieces specific to this sheet's controls. */
.layer-toggle-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;                 /* touch target */
  padding: 6px 2px;
  font-size: 0.9em;
  font-weight: 600;
  color: var(--text);
  border-bottom: 1px solid var(--border);
}
.layer-toggle-row:last-of-type { border-bottom: none; }
.layer-toggle-row .icon { width: 1.1em; height: 1.1em; color: var(--text-faint); flex: none; }
.layer-toggle-row input[type="checkbox"] {
  margin-left: auto;
  width: 20px;
  height: 20px;
  accent-color: var(--accent);
  flex: none;
}
.grid-size-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.grid-size-row .choice-chip:has(input:checked) {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

/* ---------------------------------------------------------------------
   Phase D4 — clash detection, project health, capacity, BOM
--------------------------------------------------------------------- */

/* Clashing / out-of-bounds runs and obstacles — the strongest signal on
   the canvas, so it overrides the normal selected/locked stroke colour
   (see render()'s comment). The pulse plays once on the render a run
   *becomes* clashing, not on every render of an already-clashing run. */
#layoutSvg .layout-run.is-clash-pulse rect:first-of-type { animation: runClashPulse 0.5s ease-out; }
@keyframes runClashPulse {
  0% { filter: drop-shadow(0 0 0 rgba(220, 38, 38, 0)); }
  40% { filter: drop-shadow(0 0 6px rgba(220, 38, 38, 0.65)); }
  100% { filter: drop-shadow(0 0 0 rgba(220, 38, 38, 0)); }
}
#layoutSvg .layout-obstacle.is-clashing .layout-obstacle-rect { stroke: #dc2626 !important; stroke-width: 3; stroke-dasharray: 5 3; }
#layoutSvg .layout-obstacle.is-approved .layout-obstacle-rect { stroke: var(--warn) !important; stroke-width: 3; stroke-dasharray: 5 3; }
/* Column-in-bay obstacles are clickable straight from the canvas to open
   their Approve/Keep as clash/Move review dialog (same dialog the Project
   Health sheet's Review/Change button already opens) — a visible pointer
   cursor + a brightened stroke on hover is the only affordance needed,
   no new visual language. */
#layoutSvg .layout-obstacle.is-bay-reviewable { cursor: pointer; }
#layoutSvg .layout-obstacle.is-bay-reviewable:hover .layout-obstacle-rect { stroke-width: 4; filter: brightness(1.25); }

/* Always-visible health pill (canvas-identity bar) — distinct colour AND
   icon per level (never colour alone), matching status-pill's own shape. */
.health-status-pill { cursor: pointer; user-select: none; background: var(--bg-elevated-2); border-color: var(--border-strong); color: var(--text-muted); }
.health-status-pill:hover { border-color: var(--accent-line); }
.health-status-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.health-status-pill .icon { width: 1em; height: 1em; }
.health-status-pill.health-ok { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
.health-status-pill.health-ok .status-pill-dot { background: var(--accent); }
.health-status-pill.health-warn { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.health-status-pill.health-warn .status-pill-dot { background: var(--warn); }
/* GLOBAL CONSISTENCY PASS — was its own hardcoded red (#dc2626), a third
   different red alongside status-pill-red/-danger's own values; now
   shares the same --danger token as every other "error/danger" pill. */
.health-status-pill.health-error { background: var(--danger-soft); border-color: var(--danger-line); color: var(--danger); }
.health-status-pill.health-error .status-pill-dot { background: var(--danger); }

/* Demo-readiness pass: Layout is the one page that ended up with BOTH the
   standalone health pill above AND the Stage Guide chip below in the same
   topbar — two badges of identical shape/colour language reading the same
   "Ready"/"N issues" status side by side. Every other workspace page
   (Warehouse/Racking/RM Install) shows only the Stage Guide chip, so this
   hides the redundant duplicate on Layout specifically to match. The
   element is left fully in place (id, class, click handler, live
   className/textContent updates all unchanged) — it still opens the
   Project Health & Materials sheet when clicked, and that sheet remains
   just as reachable via the tool rail's Health icon and the
   layout.html#health deep link. Only the second, visually-duplicate badge
   in the topbar is hidden. */
#healthStatusPill { display: none; }

/* Stage Guide — collapsed status chip (same visual weight/tier language as
   the health pill above, right down to reusing .health-ok/.health-warn/
   .health-error) that expands into a small, anchored, non-modal popover.
   Purely additive UI chrome: js/stage-guide.js reads existing state/
   validation only, nothing here changes app behaviour. */
.stage-guide { position: relative; display: inline-flex; }
.stage-guide-chip {
  font: inherit;
  appearance: none;
  -webkit-appearance: none;
}
.stage-guide-chip .icon { width: 1em; height: 1em; }

.stage-guide-popover {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 60;
  width: max-content;
  min-width: 240px;
  max-width: min(320px, calc(100vw - 32px));
  background: var(--bg-elevated-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: var(--space-4);
  transform-origin: top left;
  opacity: 0;
  transform: translateY(-4px) scale(0.98);
  pointer-events: none;
  transition: opacity 160ms ease-out, transform 160ms ease-out;
}
.stage-guide-popover[data-open="true"] {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  /* Keep the fade (still communicates open/close) but drop the movement. */
  .stage-guide-popover { transition: opacity 120ms ease-out; transform: none; }
}
/* Inside the Report panel's own topbar this sits far to the right of its
   trigger — the JS flips left/right anchoring at open time via inline
   left/right, this just gives the default a sane starting edge. */
.report-topbar-actions .stage-guide-popover { top: calc(100% + 8px); }

.stage-guide-purpose { margin: 0 0 var(--space-2); font-size: 0.85em; color: var(--text-muted); line-height: 1.4; }
.stage-guide-status {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0;
  font-weight: 700;
  font-size: 0.92em;
  line-height: 1.35;
}
.stage-guide-status .icon { width: 1.1em; height: 1.1em; flex: none; margin-top: 0.1em; }
.stage-guide-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; margin-top: 0.4em; }
.stage-guide-status-ok { color: var(--accent); }
.stage-guide-status-warn { color: var(--warn); }
.stage-guide-status-error { color: #dc2626; }
.stage-guide-status-neutral { color: var(--text-muted); }

.stage-guide-missing { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.stage-guide-missing li { font-size: 0.85em; color: var(--text-muted); line-height: 1.4; }
.stage-guide-link {
  display: inline; border: none; background: none; padding: 0; margin-left: 2px;
  font: inherit; font-size: 0.95em; font-weight: 700; color: var(--accent); cursor: pointer; text-decoration: none;
}
.stage-guide-link:hover, .stage-guide-link:focus-visible { text-decoration: underline; }

.stage-guide-supplemental { margin: var(--space-3) 0 0; font-size: 0.82em; color: var(--text-muted); }

/* The recommended next action is the one thing that should visually
   dominate a "Ready" popover — bold, accent-filled, unmissable, but the
   user still has to click it (never auto-navigates). */
.stage-guide-next {
  display: block;
  margin: var(--space-4) 0 0;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent);
  font-weight: 700;
  font-size: 0.9em;
  text-align: center;
  text-decoration: none;
}
a.stage-guide-next:hover, a.stage-guide-next:focus-visible, .stage-guide-next-btn:hover, .stage-guide-next-btn:focus-visible {
  background: var(--accent); color: #fff;
}
.stage-guide-next-btn { display: block; width: 100%; font: inherit; cursor: pointer; }
.stage-guide-next-static { color: var(--text); background: var(--bg-elevated); cursor: default; }

.stage-guide-chip:focus-visible, .stage-guide-link:focus-visible, .stage-guide-next:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* Health/Capacity stat grid + badges — EP's own restyle of production's
   .stat-grid/.stat/.num/.lbl and .badge family (app-canvas.css owns
   colour/shape here; the field list and numbers come straight from
   computeHealth()/computeCapacityAndBOM() in js/layout.js). */
.ep-stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--space-3); margin: var(--space-3) 0; }
.ep-stat { padding: 10px 12px; border-radius: var(--radius-sm); background: var(--bg-sunken); border: 1px solid var(--border); }
.ep-stat-num { display: block; font-family: var(--font-display); font-size: 1.3em; font-weight: 800; color: var(--text); }
.ep-stat-lbl { display: block; font-size: 0.78em; font-weight: 600; color: var(--text-faint); margin-top: 2px; }

.ep-badge {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 14px; border-radius: var(--radius-pill);
  font-size: 0.86em; font-weight: 700;
}
.ep-badge .icon { width: 1.05em; height: 1.05em; }
.ep-badge-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
.ep-badge-ok { background: var(--accent-soft); color: var(--accent); }
.ep-badge-warn, .ep-badge-error { background: rgba(220, 38, 38, 0.12); color: #dc2626; }
.ep-badge-caution { background: var(--warn-soft); color: var(--warn); }
.ep-badge-approved { background: var(--accent-soft); color: var(--accent); }

.clash-list { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.clash-list li {
  padding: 8px 12px; border-radius: var(--radius-sm);
  background: var(--bg-sunken); border: 1px solid var(--border);
  font-size: 0.86em; color: var(--text-muted); line-height: 1.5;
}

.bom-table { width: 100%; border-collapse: collapse; margin-top: var(--space-3); font-size: 0.88em; }
.bom-table th, .bom-table td { padding: 8px 10px; border-bottom: 1px solid var(--border); text-align: left; }
.bom-table th { font-weight: 700; color: var(--text-faint); text-transform: uppercase; font-size: 0.78em; letter-spacing: 0.04em; }
.bom-table td.num, .bom-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* BOM VISUAL/UX PASS — grouped component schedule (in-canvas Health
   sheet BOM tab). Presentation only: js/layout.js's bomGroupItems() puts
   the same bom.items rows under these Frames/Beams/Decking/Protection/
   Accessories bands. Theme-aware (uses --accent, unlike the print
   Report's fixed navy/cyan palette) so it always matches whichever
   app theme — dark or light — is active, same as every other Warehouse/
   Racking/Layout panel. */
.bom-group-row td {
  background: var(--accent-soft);
  border-left: 3px solid var(--accent);
  color: var(--accent);
  font-weight: 800;
  font-size: 0.74em;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 6px 10px 6px 8px;
}
.bom-group-count { float: right; font-weight: 700; letter-spacing: 0.02em; text-transform: none; opacity: 0.85; }
.bom-shims-note {
  display: flex; justify-content: space-between; align-items: center;
  margin-top: var(--space-3); padding: 10px 14px;
  border-radius: var(--radius-sm); background: var(--bg-sunken); border: 1px solid var(--border);
  font-size: 0.88em; font-weight: 700;
}
.bom-shims-qty { color: var(--text-muted); font-weight: 600; }

/* Ambient depth glow — the homepage's .hero-glow technique, scaled
   down and quieted so it reads as soft light sitting behind the
   drawing surface rather than a colored backdrop. Sits behind the
   dot-grid, the SVG and every floating chip (all of which sit at
   z-index:1+), and never intercepts pointer events. Shared by
   warehouse.html's .canvas-stage and racking.html's
   .rack-stage-preview so both canvas pages get the same restrained
   depth treatment already established on the homepage. */
.canvas-stage-glow {
  position: absolute;
  inset: -10% -8% auto -8%;
  height: 60%;
  background: radial-gradient(560px 320px at 50% 6%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%);
  pointer-events: none;
  z-index: 0;
  animation: glowDrift var(--glow-duration) ease-in-out infinite alternate;
}

/* A quiet, dismiss-free hint chip — the one piece of onboarding copy on
   the canvas itself, styled like the HUD chips rather than a banner. */
.canvas-hint {
  position: absolute;
  top: var(--space-5);
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 16px;
  border-radius: var(--radius-pill);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  font-size: 0.8em;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
  z-index: 2;
}
/* Empty/locked state (Warehouse visual/UX refinement pass) — while
   Length/Width aren't valid yet, every non-Select dock button already
   carries .is-disabled (syncPlacementToolsAvailability(), js/warehouse.js).
   :has() reads that existing state to swap the hint copy and mute the
   placeholder boundary — no JS change, and browsers without :has()
   simply keep today's behaviour (default hint, full-strength boundary)
   since these rules just never match. */
.canvas-hint-locked { display: none; }
.wh-workspace:has(.wh-tool-list .canvas-dock-btn[data-tool].is-disabled) .canvas-hint:not(.canvas-hint-locked) { display: none; }
.wh-workspace:has(.wh-tool-list .canvas-dock-btn[data-tool].is-disabled) .canvas-hint-locked { display: inline-flex; }
.wh-workspace:has(.wh-tool-list .canvas-dock-btn[data-tool].is-disabled) #whBoundary { stroke-dasharray: 5 4; opacity: 0.55; }
.wh-workspace:has(.wh-tool-list .canvas-dock-btn[data-tool].is-disabled) #whSvg .mock-dim-label { opacity: 0.45; }
.canvas-hint .icon { color: var(--accent); width: 1.05em; height: 1.05em; }

/* ---------------------------------------------------------------------
   .canvas-dock-btn — the base button style shared with warehouse.html's
   Tools rail (.wh-tool-list .canvas-dock-btn, js/warehouse.js's
   querySelectorAll('.canvas-dock-btn')). Layout no longer uses a
   floating dock (see .layout-tools-rail below, visual-correction pass)
   — the old pill-container/group/separator/icon-only/feature/label
   modifier classes that only Layout ever used have been removed as
   dead CSS; this base rule and .is-active stay because Warehouse still
   depends on them.
--------------------------------------------------------------------- */
.canvas-dock-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 44px;
  padding: 0 16px;
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  font-size: 0.84em;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.canvas-dock-btn .icon { width: 1.15em; height: 1.15em; }
.canvas-dock-btn:hover { background: var(--bg-elevated-2); color: var(--text); }
.canvas-dock-btn:active:not(:disabled) { transform: scale(0.97); }
/* Follows up the note that used to be here: warehouse.js now gives every
   .canvas-dock-btn tabindex/role="button" (bindKeyActivate()), so this
   ring is reachable rather than dead CSS. */
.canvas-dock-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.canvas-dock-btn.is-active { background: var(--accent-soft); color: var(--accent); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* Zoom sits in its own small floating pill, bottom-right, out of the
   dock's way. */
.canvas-zoom {
  position: absolute;
  right: var(--space-6);
  bottom: var(--space-6);
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 2px;
  padding: 4px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--bg-elevated) 92%, transparent);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-md);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  z-index: 3;
  max-width: calc(100% - var(--space-4));
}
.canvas-zoom-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 50%;
  color: var(--text-faint);
  cursor: pointer;
  transition: transform var(--duration-fast) var(--ease-out);
}
.canvas-zoom-btn:hover { background: var(--bg-elevated-2); color: var(--accent); }
.canvas-zoom-btn:active { transform: scale(0.97); }
.canvas-zoom-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.canvas-zoom-value { min-width: 3.6ch; flex: none; text-align: center; font-size: 0.78em; font-weight: 700; color: var(--text-muted); }

/* ---------------------------------------------------------------------
   Floating inspector — appears only once something is selected. A
   compact card, not a sidebar: it sits over the top-right of the stage
   on larger screens and collapses into a small bottom sheet on narrow
   ones. Presentational only — populated with whichever item was
   clicked.
--------------------------------------------------------------------- */
.canvas-inspector {
  position: absolute;
  top: var(--space-6);
  right: var(--space-6);
  width: 240px;
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
  z-index: 4;
  opacity: 0;
  transform: translateY(-8px) scale(0.98);
  pointer-events: none;
  transition: opacity var(--duration-med) var(--ease-out), transform var(--duration-med) var(--ease-out);
}
.canvas-inspector.is-open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }

/* warehouse.html's #inspector is now a plain on-demand floating popup —
   same behaviour as Racking's rack outline card — since Warehouse
   Dimensions moved permanently into the left tools rail. It uses the
   base .canvas-inspector opacity/transform/is-open rules above with no
   page-specific override needed. */

.canvas-inspector-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--space-3);
}
.canvas-inspector-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
  flex: none;
}
.canvas-inspector-title { font-family: var(--font-display); font-weight: 700; font-size: 0.95em; }
.canvas-inspector-close {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  color: var(--text-faint);
  cursor: pointer;
}
.canvas-inspector-close:hover { background: var(--bg-elevated-2); color: var(--text); }
.canvas-inspector-rows { display: flex; flex-direction: column; gap: 8px; }
.canvas-inspector-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); font-size: 0.85em; }
.canvas-inspector-row span { color: var(--text-faint); font-weight: 600; }
.canvas-inspector-row strong { color: var(--text); font-weight: 700; font-family: var(--font-display); }

/* racking.html's rack selection outline — same on-demand pattern as
   .canvas-inspector above: hidden until the rack is actually selected,
   faded in rather than popping, so the canvas is clean on first load
   and the outline reads as a genuine interaction state, not permanent
   chrome. Scoped by ID since #rackSelection only exists on that page. */
#rackSelection { opacity: 0; transition: opacity var(--duration-fast) var(--ease-out); pointer-events: none; }
#rackSelection.is-open { opacity: 1; }

/* ---------------------------------------------------------------------
   Live readout — a small horizontal strip of stat chips, used for the
   handful of numbers worth always seeing at a glance (echoes the
   quality reference's "Project Overview" strip, without becoming a
   sidebar of its own).
--------------------------------------------------------------------- */
.canvas-readout {
  position: absolute;
  top: var(--space-6);
  left: var(--space-6);
  display: flex;
  gap: var(--space-2);
  z-index: 3;
}
.canvas-readout-chip {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 7px 14px;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--bg-elevated) 92%, transparent);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.canvas-readout-value { font-family: var(--font-display); font-size: 1.05em; font-weight: 700; color: var(--text); line-height: 1.1; }
.canvas-readout-label { font-size: 0.66em; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-faint); }

/* ---------------------------------------------------------------------
   Bottom sheet — the on-demand home for everything that doesn't need
   to be permanently visible: project metadata, precise dimensions,
   notes, advanced racking fields. Opens from a checkbox toggle (no
   script needed for open/close), slides up over the stage rather than
   pushing it — the canvas never resizes to make room.
--------------------------------------------------------------------- */
.sheet-toggle { position: absolute; opacity: 0; pointer-events: none; }
.sheet-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.38);
  z-index: 55;
}
.sheet-toggle:checked ~ .sheet-backdrop { display: block; }

.canvas-sheet {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 100%);
  width: min(640px, 100vw);
  max-height: min(72vh, 640px);
  display: flex;
  flex-direction: column;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-xl);
  z-index: 56;
  transition: transform var(--duration-med) var(--ease-out);
}
.sheet-toggle:checked ~ .canvas-sheet { transform: translate(-50%, 0); }

/* Home's New Project sheet only — opens centred on screen instead of
   bottom-docked (every other page's own .canvas-sheet, e.g. Project
   details/Layers/Health, is unaffected and stays bottom-attached). */
.canvas-sheet.home-new-project-modal {
  top: 50%;
  bottom: auto;
  transform: translate(-50%, calc(-50% + 16px));
  opacity: 0;
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-lg);
  transition: transform var(--duration-med) var(--ease-out), opacity var(--duration-med) var(--ease-out);
}
.sheet-toggle:checked ~ .canvas-sheet.home-new-project-modal {
  transform: translate(-50%, -50%);
  opacity: 1;
}

/* Install's Estimate Breakdown sheet only — was bottom-docked like the
   default .canvas-sheet above, which on short/rotated viewports could
   open with its top edge above 0 and its bottom edge below the visible
   area (nothing scrolled it back into view, and the backdrop/close
   controls could end up outside the viewport too, unable to be clicked).
   Centred instead, same technique as .home-new-project-modal above.
   Other pages' own .canvas-sheet (Project details/Layers/Health) are
   unaffected. max-height/.canvas-sheet-body's own overflow-y:auto (both
   inherited from the base .canvas-sheet rules) keep the whole panel
   within the viewport with internal scrolling when content is tall,
   while .canvas-sheet-head (title + close X) stays outside that
   scrollable area so it's always visible. */
.canvas-sheet.install-breakdown-modal {
  top: 50%;
  bottom: auto;
  transform: translate(-50%, calc(-50% + 16px));
  opacity: 0;
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-lg);
  transition: transform var(--duration-med) var(--ease-out), opacity var(--duration-med) var(--ease-out);
}
.sheet-toggle:checked ~ .canvas-sheet.install-breakdown-modal {
  transform: translate(-50%, -50%);
  opacity: 1;
}

/* install.html bug fix — Project details sheet + Estimate Breakdown modal
   are both independent, body-level #toggle/.sheet-backdrop/.canvas-sheet
   triples (same convention as every other page's single sheet), but
   #breakdownSheetToggle's markup sits AFTER #projectSheetToggle's in the
   DOM. The shared ".sheet-toggle:checked ~ .sheet-backdrop"/"~
   .canvas-sheet" rules above use a plain (unscoped) sibling combinator, so
   opening Project also matched Breakdown's later backdrop/sheet — forcing
   Breakdown's backdrop visible and its modal into its open transform/
   opacity even though #breakdownSheetToggle itself was still unchecked.
   That's why clicking "View Breakdown" while Project was open appeared to
   do nothing (Breakdown was already sitting in its "open" visual state as
   an unwanted side effect) and why an extra backdrop could sit over page
   content. Scope Breakdown's open state to its own checkbox only; higher
   specificity (ID) wins over the generic rules regardless of source
   order. No other page has two sheets like this, so nothing else needs
   this override. Paired with a small JS fix in install.html that
   auto-closes whichever sheet is open before opening the other. */
#projectSheetToggle:checked ~ .sheet-backdrop[for="breakdownSheetToggle"] {
  display: none;
}
#projectSheetToggle:checked ~ .canvas-sheet.install-breakdown-modal {
  transform: translate(-50%, calc(-50% + 16px));
  opacity: 0;
}

.canvas-sheet-handle {
  flex: none;
  display: flex;
  justify-content: center;
  padding: 10px 0 4px;
  cursor: pointer;
}
.canvas-sheet-handle::before {
  content: '';
  width: 36px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--border-strong);
}
.canvas-sheet-head {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.canvas-sheet-title { font-family: var(--font-display); font-size: 1.05em; font-weight: 700; }
.canvas-sheet-close {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  color: var(--text-faint);
  cursor: pointer;
}
.canvas-sheet-close:hover { background: var(--bg-elevated-2); color: var(--text); }
.canvas-sheet-tabs { flex: none; display: flex; gap: 6px; padding: var(--space-3) var(--space-5) 0; }
.canvas-sheet-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-5); }
.canvas-sheet-panel { display: none; }

/* Sheet tab wiring — same body-level-radio + ~ combinator technique used
   throughout this prototype, so switching between a page's own sheet
   tabs (racking.html: Advanced/Project — Dimensions and Protection have
   both moved out to the permanent .rack-spec-rail) needs no script. The
   radios sit as body children immediately before .canvas-sheet, so ~
   reaches both the tab labels and the panels. warehouse.html's own
   Details sheet has no tabs at all — Warehouse Specification and Site
   Survey are permanent rail panels now, so its sheet is Project
   identity only, always shown. */
#sheetTabProject:checked ~ .canvas-sheet .canvas-sheet-tabs label[for="sheetTabProject"],
#sheetTabProtection:checked ~ .canvas-sheet .canvas-sheet-tabs label[for="sheetTabProtection"],
#sheetTabAdvanced:checked ~ .canvas-sheet .canvas-sheet-tabs label[for="sheetTabAdvanced"],
#sheetTabAdvancedNotes:checked ~ .canvas-sheet .canvas-sheet-tabs label[for="sheetTabAdvancedNotes"] {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}
#sheetTabProject:checked ~ .canvas-sheet #sheetPanelProject,
#sheetTabProtection:checked ~ .canvas-sheet #sheetPanelProtection,
#sheetTabAdvanced:checked ~ .canvas-sheet #sheetPanelAdvanced,
#sheetTabAdvancedNotes:checked ~ .canvas-sheet #sheetPanelAdvancedNotes {
  display: block;
}
.canvas-sheet-tabs label:hover { border-color: var(--accent-line); color: var(--text); }

/* Phase D4: Project Health & Materials sheet — its own tab wiring (same
   technique, scoped to .canvas-sheet-wide so it can't cross-talk with the
   Layers sheet's plain .canvas-sheet, which has no tabs of its own).
   Reaches through .app-shell-body for the same reason the sheet's own
   open/close rule does above (#healthSheetToggle) — the 4 tab radios
   are body-level, same as every other sheet-driving toggle, but
   .canvas-sheet-wide itself is nested inside .app-shell-body on this
   page, so a plain "~" sibling combinator alone never reached it. */
.canvas-sheet-wide { width: min(760px, 100vw); }
#healthTabOverview:checked ~ .app-shell-body .canvas-sheet-wide .canvas-sheet-tabs label[for="healthTabOverview"],
#healthTabClashes:checked ~ .app-shell-body .canvas-sheet-wide .canvas-sheet-tabs label[for="healthTabClashes"],
#healthTabCapacity:checked ~ .app-shell-body .canvas-sheet-wide .canvas-sheet-tabs label[for="healthTabCapacity"],
#healthTabBom:checked ~ .app-shell-body .canvas-sheet-wide .canvas-sheet-tabs label[for="healthTabBom"] {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}
#healthTabOverview:checked ~ .app-shell-body .canvas-sheet-wide #healthPanelOverview,
#healthTabClashes:checked ~ .app-shell-body .canvas-sheet-wide #healthPanelClashes,
#healthTabCapacity:checked ~ .app-shell-body .canvas-sheet-wide #healthPanelCapacity,
#healthTabBom:checked ~ .app-shell-body .canvas-sheet-wide #healthPanelBom {
  display: block;
}

/* ---------------------------------------------------------------------
   Big stepper — the primary way to change a small-range whole number
   (beam levels, pallets per bay, bays per run, guard counts). Large
   touch targets either side of a bold, glanceable value.
--------------------------------------------------------------------- */
.big-stepper {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  padding: 6px;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--border-strong);
  background: var(--bg-sunken);
}
.big-stepper-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  font-size: 1.1em;
  font-weight: 700;
  color: var(--text-muted);
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.big-stepper-btn:hover { border-color: var(--accent-line); color: var(--accent); }
.big-stepper-btn:active { transform: scale(0.97); }
/* wireStepper()/wireSnapField()/the beamLevelsStepper loop (js/racking.js)
   and the wallClearance +/- (js/warehouse.js) now give every
   .big-stepper-btn tabindex/role="button", so this ring is reachable. */
.big-stepper-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.big-stepper-value {
  min-width: 2.4ch;
  text-align: center;
  font-family: var(--font-display);
  font-size: 1.5em;
  font-weight: 700;
  color: var(--text);
}

/* Compact variant, used inline within a field row rather than as a
   standalone hero control. */
.stepper-sm { padding: 4px; gap: var(--space-2); }
.stepper-sm .big-stepper-btn { width: 32px; height: 32px; font-size: 0.95em; }
.stepper-sm .big-stepper-value { font-size: 1.1em; min-width: 2ch; }

/* Required-field invalid highlight (demo-polish pass — Frame Height must
   be entered before Generate/Continue). Reuses the same layoutRunShake
   keyframe Layout's own .layout-run.is-shake already uses, so an invalid
   Generate attempt is never a silent no-op — the field visibly shakes and
   reddens for a moment, in addition to the validation summary message. */
.big-stepper.is-field-invalid { animation: layoutRunShake 0.4s ease-in-out; border-color: #dc2626; }
/* Same treatment for a .snap-field (Warehouse's Roof/Eaves Height is now
   required before Continue to Racking, same rule as Frame Height above). */
.snap-field.is-field-invalid .snap-field-value { animation: layoutRunShake 0.4s ease-in-out; border-color: #dc2626; }
/* Same treatment for a plain .field (Warehouse Length/Width are now
   required before Continue to Racking, same rule as Roof Height above). */
.field.is-field-invalid input { animation: layoutRunShake 0.4s ease-in-out; border-color: #dc2626; }

/* ---------------------------------------------------------------------
   Snap slider — for the handful of fields where a real dimension in
   millimetres matters (frame height/depth, beam length, aisle, flue).
   A slider with visible ticks at standard sizes does the choosing;
   tapping the value itself reveals a precise numeric override for the
   rare case someone needs an exact non-standard figure.
--------------------------------------------------------------------- */
.snap-field { display: flex; flex-direction: column; gap: 10px; }
.snap-field-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-2); }
.snap-field-label { font-size: 0.82em; font-weight: 700; color: var(--text); }
/* Given a real, obvious input box (border + background) rather than
   borderless click-to-edit text — this is Warehouse's only use of the
   class (grep-confirmed), used for Roof / Eaves Height, an important
   spec value that needs to visibly BE a number field, not read like a
   plain label. */
.snap-field-value {
  font-family: var(--font-display);
  font-size: 0.92em;
  font-weight: 700;
  color: var(--text);
  background: var(--bg-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 4px 8px;
  cursor: text;
  text-align: right;
  width: 6ch;
  transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
}
.snap-field-value:hover { border-color: var(--text-faint); }
.snap-field-value:focus {
  outline: none;
  background: var(--bg-elevated);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  color: var(--accent);
}
.snap-track {
  position: relative;
  height: 30px;
  display: flex;
  align-items: center;
}
.snap-track input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--border-strong);
  outline: none;
  margin: 0;
}
.snap-track input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--accent);
  border: 3px solid var(--bg-elevated);
  box-shadow: 0 0 0 1px var(--accent-line), var(--shadow-sm);
  cursor: pointer;
  margin-top: -1px;
}
.snap-track input[type="range"]::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--accent);
  border: 3px solid var(--bg-elevated);
  box-shadow: 0 0 0 1px var(--accent-line), var(--shadow-sm);
  cursor: pointer;
}
.snap-track input[type="range"]::-webkit-slider-runnable-track { background: transparent; }
.snap-ticks { position: absolute; left: 11px; right: 11px; top: 50%; display: flex; justify-content: space-between; pointer-events: none; }
.snap-tick { width: 2px; height: 6px; border-radius: 1px; background: var(--border-strong); transform: translateY(-50%); }
.snap-field-hint { font-size: 0.78em; font-weight: 500; color: var(--text-faint); }

/* ---------------------------------------------------------------------
   Swatch cards — the visual, tap-to-choose alternative to a <select>
   for Pallet Type / Decking / Column Protection / End-of-Rack
   Protection. Each card carries a small glyph so the choice is
   recognised by shape, not just read as text.
--------------------------------------------------------------------- */
.swatch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: var(--space-3); }
.swatch-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: var(--space-4) var(--space-3);
  border-radius: var(--radius-md);
  border: 1.5px solid var(--border-strong);
  background: var(--bg-sunken);
  cursor: pointer;
  /* min-width: 0 overrides the grid-item default of min-width: auto (which
     equals the card's content min-content width, e.g. "Chipboard"'s
     unbreakable label width) — without this, a long label can force its
     track wider than the grid's own minmax() minimum, pushing the card
     past the edge of its swatch-grid (demo-polish pass). */
  min-width: 0;
  transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.swatch-card:hover { border-color: var(--accent-line); transform: translateY(-2px); }
.swatch-card:active { transform: translateY(0) scale(0.97); }
/* Same follow-up as .canvas-dock-btn above: racking.js's wireToggleGroup()
   now gives every [data-value] child (including .swatch-card) tabindex/
   role="button", so this ring is reachable rather than dead CSS. */
.swatch-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.swatch-card.is-selected { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 1px var(--accent-line); }
.swatch-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-sm);
  background: var(--bg-elevated-2);
  color: var(--text-muted);
}
.swatch-card.is-selected .swatch-glyph { background: var(--bg-elevated); color: var(--accent); }
.swatch-glyph .icon { width: 1.3em; height: 1.3em; }
/* Column Protection — Fixed Guard and Deflector now share the identical
   guard-shaped glyph (#i-shield, see racking.html — Deflector used to be
   a different alert-triangle icon) and are told apart purely by colour
   instead of shape: Fixed Guard = yellow, Deflector = orange, in both
   the resting and selected states so the distinction reads at a glance
   without needing to select one first. Blue is deliberately not used
   here — it stays reserved for racking/uprights (the structural #2f6fed
   colour used on Layout/Racking elsewhere). Scoped to #columnProtectionSeg
   only — Pallet Type/Decking's swatch cards are unaffected. */
#columnProtectionSeg .swatch-card[data-value="fixed"] .swatch-glyph { background: rgba(234, 179, 8, 0.16); color: #eab308; }
#columnProtectionSeg .swatch-card[data-value="fixed"].is-selected .swatch-glyph { background: rgba(234, 179, 8, 0.26); color: #eab308; }
#columnProtectionSeg .swatch-card[data-value="deflector"] .swatch-glyph { background: rgba(249, 115, 22, 0.16); color: #f97316; }
#columnProtectionSeg .swatch-card[data-value="deflector"].is-selected .swatch-glyph { background: rgba(249, 115, 22, 0.26); color: #f97316; }
.swatch-label { font-size: 0.78em; font-weight: 700; color: var(--text); text-align: center; line-height: 1.25; }
.swatch-sub { font-size: 0.7em; font-weight: 600; color: var(--text-faint); text-align: center; }

/* Segmented control — reuses .choice-group/.choice-chip visuals but as
   a real toggle group (Yes/No, single/double run, kit form) rather
   than the static illustrative chips elsewhere. */
.segmented { display: inline-flex; padding: 3px; border-radius: var(--radius-pill); background: var(--bg-sunken); border: 1.5px solid var(--border-strong); }
.segmented-btn {
  min-height: 36px;
  padding: 6px 16px;
  border-radius: var(--radius-pill);
  font-size: 0.82em;
  font-weight: 700;
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.segmented-btn:active:not([aria-disabled="true"]) { transform: scale(0.97); }
/* wireToggleGroup() (js/racking.js) now gives every non-disabled
   [data-value] child, including .segmented-btn, tabindex/role="button". */
.segmented-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.segmented-btn.is-selected { background: var(--accent); color: #fff; box-shadow: var(--shadow-sm); }
/* Clearly-disabled segmented option — visually distinct from both the
   normal and selected states, never silently inert. (Racking's own 2D
   Plan/3D View toggle is fully live — see .rack-preview-head below —
   this is for any other segmented control gated on a real precondition.) */
.segmented-btn[aria-disabled="true"] { cursor: not-allowed; opacity: 0.4; }
.segmented-btn[aria-disabled="true"].is-selected { background: var(--bg-elevated-2); color: var(--text-muted); box-shadow: none; opacity: 0.7; }

/* Live Preview header row — title + 2D Plan/3D View toggle. */
.rack-preview-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }

/* Decking quantity — appears directly under the Decking swatch group only
   once a real decking type is selected (production's deckingQtyField). */
.decking-qty-field { margin-top: var(--space-4); }

/* Disclosure — collapsed-by-default secondary controls inside a sheet
   panel (e.g. Racking's pallet orientation override), lighter-weight than
   a full sheet-section since it's off by default. */
.field-disclosure {
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
}
.field-disclosure summary {
  cursor: pointer;
  font-size: 0.82em;
  font-weight: 700;
  color: var(--text-muted);
  list-style: none;
}
.field-disclosure summary::-webkit-details-marker { display: none; }
.field-disclosure summary::before {
  content: '\25B8';
  display: inline-block;
  margin-right: 8px;
  color: var(--text-faint);
  transition: transform var(--duration-fast) var(--ease-out);
}
.field-disclosure[open] summary::before { transform: rotate(90deg); }
.field-disclosure summary:hover { color: var(--text); }

/* ---------------------------------------------------------------------
   Config section heading — a lighter version of .panel-title for use
   inside a bottom sheet, where sections stack in one scroll rather than
   behind tabs.
--------------------------------------------------------------------- */
.sheet-section + .sheet-section { margin-top: var(--space-6); padding-top: var(--space-6); border-top: 1px solid var(--border); }
.sheet-section-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-display);
  font-size: 0.8em;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 var(--space-4);
}

/* ---------------------------------------------------------------------
   Bottom action bar — unchanged in spirit from the old shell, just
   renamed into this system so both pages share one definition.
--------------------------------------------------------------------- */
.canvas-actions {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border);
  background: var(--bg-elevated);
}
.canvas-actions-left { display: flex; align-items: center; gap: var(--space-3); }
/* ---- Shared bottom taskbar tools (UX clarity pass, Warehouse-first) ----
   The centre section of .canvas-actions now also carries the page's
   tools, so Select/Add/etc. are docked flush in the existing bottom
   bar instead of floating over the canvas in a separate left rail.
   Racking and Layout adopt this same .canvas-actions-center/
   .taskbar-group structure in later passes — this establishes the
   pattern, it doesn't invent a second bottom-bar system. Buttons reuse
   .canvas-dock-btn unchanged (shape, 44px target, active state), so no
   click-wiring changed. */
.canvas-actions-center { flex: 1; display: flex; align-items: center; justify-content: center; gap: var(--space-3); min-width: 0; overflow-x: auto; }
.taskbar-group { display: flex; align-items: center; gap: 3px; padding: 3px; border-radius: var(--radius-pill); background: var(--bg-sunken); flex: none; }
.taskbar-sep { width: 1px; height: 24px; background: var(--border-strong); flex: none; }

/* ---------------------------------------------------------------------
   warehouse.html — Phase 3 additions
   -----------------------------------------------------------------------
   Production's Warehouse workflow (typed dimensions, Site Survey list,
   validation badges) reused behind the canvas-first shell above. Every
   rule below is presentation only — js/warehouse.js owns the real data
   binding — but it has to support two things production's own CSS never
   needed: numeric values that are BOTH a touch control (slider/stepper)
   AND directly typeable (precision entry always available), and a real
   dynamic list of items instead of a static illustration.
--------------------------------------------------------------------- */

/* Slider/stepper value displays, promoted from a plain <span> to a real
   <input type="number"> — same visual footprint, so dragging the slider
   or stepping +/- still reads exactly as before, but every value can
   also be typed directly and isn't capped to the control's own range. */
input.snap-field-value,
input.big-stepper-value {
  font-family: inherit;
  -moz-appearance: textfield;
}
input.snap-field-value::-webkit-outer-spin-button,
input.snap-field-value::-webkit-inner-spin-button,
input.big-stepper-value::-webkit-outer-spin-button,
input.big-stepper-value::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.snap-field-value-wrap { display: inline-flex; align-items: baseline; gap: 3px; }
.snap-field-unit { font-size: 0.78em; font-weight: 700; color: var(--text-faint); }
input.big-stepper-value { background: none; border: none; text-align: center; }
input.big-stepper-value:focus { outline: none; }

/* Validation badges — checkWarehouseSizeSanity() / checkRoofHeightWarning(),
   ported verbatim from production, restyled with the --warn token trio. */
.field-warning {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  background: var(--warn-soft);
  border: 1px solid var(--warn-line);
  color: var(--warn);
  font-size: 0.8em;
  font-weight: 600;
  line-height: 1.5;
}
.field-warning .icon { width: 1.1em; height: 1.1em; flex: none; margin-top: 1px; }
.field-warning b { font-weight: 800; }
.field-warning-body { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-width: 0; }
.field-warning-fix {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: 0 10px;
  border-radius: var(--radius-pill);
  background: var(--bg-elevated);
  border: 1px solid var(--warn-line);
  color: var(--warn);
  font-size: 0.92em;
  font-weight: 700;
  cursor: pointer;
}
.field-warning-fix:hover { background: var(--warn-soft); }

/* Generate Layout validation — validateGenerateInputs()/buildFitFailureHTML(),
   ported verbatim from production (Phase C). Renders inside .field-warning/
   .field-warning-body; these classes style production's own markup
   (.fit-fail/.fit-fail-stats/etc.) with EP's --warn token trio instead of
   production's .badge/.fit-fail page-specific CSS. */
.fit-fail { display: flex; flex-direction: column; gap: 8px; width: 100%; }
.fit-fail-headline { font-weight: 800; font-size: 1.05em; color: var(--warn); margin: 0; }
.fit-fail-subhead { font-weight: 700; opacity: 0.85; margin: 4px 0 0; }
.fit-fail-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 6px 16px;
  margin: 4px 0;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
}
.fit-fail-stat { display: flex; flex-direction: column; gap: 2px; font-weight: 600; }
.fit-fail-stat span { font-size: 0.85em; opacity: 0.75; font-weight: 500; }
.fit-fail-stat strong { font-size: 1em; }
.fit-fail-list { margin: 4px 0 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
.fit-fail-list li { font-weight: 600; }

/* Site Survey list — the bottom-sheet's non-canvas path for adding,
   reviewing and removing every obstacle, so nothing ever requires
   touching the drawing to accomplish. */
.survey-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--space-4); max-height: 280px; overflow-y: auto; }
.survey-list-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: var(--space-5) var(--space-3);
  font-size: 0.82em;
  font-weight: 500;
  color: var(--text-faint);
  text-align: center;
}
.survey-list-empty .icon { width: 26px; height: 26px; color: var(--text-faint); }
.survey-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 9px 10px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--bg-sunken);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
}
.survey-row:hover, .survey-row.is-active { border-color: var(--accent-line); background: var(--accent-soft); }
.survey-row-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  background: var(--bg-elevated-2);
  color: var(--text-muted);
  flex: none;
}
.survey-row.is-advisory .survey-row-icon { color: var(--warn); }
.survey-row:hover .survey-row-icon, .survey-row.is-active .survey-row-icon { color: var(--accent); }
.survey-row-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.survey-row-title { font-size: 0.86em; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.survey-row-meta { font-size: 0.74em; font-weight: 600; color: var(--text-faint); }
.survey-row-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  color: var(--text-faint);
  flex: none;
}
.survey-row-remove:hover { background: var(--bg-elevated-2); color: #dc2626; }

/* Type chooser — a small anchored popover, not a modal, per the "avoid
   pop-ups, prefer floating/contextual controls" rule. Shared by the
   dock's Obstacle button (6 less-common types) and the sheet's Add Item
   button (all 10) — js/warehouse.js fills #typeChooserList to match. */
.type-chooser-backdrop { position: fixed; inset: 0; z-index: 59; display: none; }
.type-chooser-backdrop.is-open { display: block; }
.type-chooser {
  position: fixed;
  width: 226px;
  max-height: 320px;
  overflow-y: auto;
  padding: 8px;
  border-radius: var(--radius-lg);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
  z-index: 60;
  opacity: 0;
  transform: translateY(6px) scale(0.98);
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.type-chooser.is-open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.type-chooser-title { padding: 4px 8px 8px; font-size: 0.68em; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-faint); }
.type-chooser-item {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 0 10px;
  border-radius: var(--radius-sm);
  font-size: 0.85em;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
}
.type-chooser-item:hover { background: var(--accent-soft); color: var(--accent); }
.type-chooser-item .icon { width: 1.15em; height: 1.15em; flex: none; color: var(--text-muted); }
.type-chooser-item:hover .icon { color: var(--accent); }

/* Floating inspector — editable fields (Phase 3 supersedes the earlier
   read-only rows: same card, same open/close behaviour, real inputs). */
.canvas-inspector-title-wrap { flex: 1; min-width: 0; }
.canvas-inspector-title-input {
  width: 100%;
  padding: 4px 0;
  font-family: var(--font-display);
  font-size: 0.95em;
  font-weight: 700;
  color: var(--text);
  background: none;
  border: none;
  border-bottom: 1.5px solid var(--border-strong);
  transition: border-color var(--duration-fast) var(--ease-out);
}
/* Form-consistency pass: this used to be invisible until focused (a
   transparent border-bottom) and used --accent-line (translucent) for its
   focus colour where every other field in the app uses solid --accent —
   same underline family as #dayRateInput (install.html), which already
   got this right; this just catches title-input up to match it. */
.canvas-inspector-title-input:hover { border-bottom-color: var(--text-faint); }
.canvas-inspector-title-input:focus { outline: none; border-bottom-color: var(--accent); }
.canvas-inspector-title-input::placeholder { color: var(--text-faint); font-weight: 600; }
.canvas-inspector-select {
  min-height: 30px;
  padding: 2px 20px 2px 6px;
  font-family: var(--font-sans);
  font-size: 0.82em;
  font-weight: 700;
  color: var(--text);
  background: var(--bg-sunken) var(--select-chevron) no-repeat right 5px center;
  background-size: 11px;
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  max-width: 148px;
}
/* Selected-item panel hierarchy (Warehouse visual/UX refinement pass) —
   a hairline rule between the Type row (object identity) and the
   dimension/position fields below it, so "what this is" reads as
   visually separate from "its editable numbers" instead of one
   undifferentiated stack. Scoped to the pairing that only exists in
   warehouse.html's inspector (.canvas-inspector-row-pair is confirmed
   unused elsewhere) so Racking/Layout's own simpler inspector rows are
   untouched. */
.canvas-inspector-row + .canvas-inspector-row-pair { margin-top: 2px; padding-top: 10px; border-top: 1px solid var(--border); }
.canvas-inspector-row-pair { display: flex; gap: var(--space-3); }
.canvas-inspector-row-pair label { flex: 1; display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.canvas-inspector-row-pair label span { color: var(--text-faint); font-weight: 600; font-size: 0.78em; }
.canvas-inspector-row-pair input {
  min-height: 34px;
  padding: 5px 8px;
  /* X/Y/W/D/H are real coordinates and measurements, not free text — Geist
     Mono here matches every other technical readout in the app (dimension
     labels, stepper values). .canvas-inspector-row-pair is warehouse.html-
     only (confirmed unused elsewhere), so this doesn't touch Layout/
     Racking's own inspector fields. */
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.86em;
  font-weight: 700;
  color: var(--text);
  background: var(--bg-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  width: 100%;
}
.canvas-inspector-row-pair input:focus,
.canvas-inspector-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.canvas-inspector-delete { width: 100%; justify-content: center; margin-top: var(--space-4); color: #dc2626; border-color: var(--border-strong); }
.canvas-inspector-delete:hover { border-color: #dc2626; background: rgba(220, 38, 38, 0.08); }

/* Obstacles rendered on the canvas from real state — one visual language
   for every type (icon centred in a rounded rect) rather than seven
   bespoke shapes, coloured by production's own hard/advisory flag. */
/* Select tool: pointer over any item (it's selectable); grab once it's
   the current selection (it's now draggable); grabbing while an actual
   drag gesture is in progress (js/warehouse.js toggles is-dragging on
   the <svg> for the duration of the gesture, same idea as
   .canvas-stage.is-placing above for the armed-tool crosshair). */
/* Warehouse dimension readouts (#whDimTopLabel/#whDimLeftLabel/the origin
   "0, 0" tag) — same shared .mock-dim-label class the homepage mockups
   use, scoped here to #whSvg only so Home's own use of that class is
   untouched. Geist Mono, matching every other technical/numeric readout
   in the app (racking.html's #rackSvg gets the identical treatment). */
#whSvg .mock-dim-label { font-family: var(--font-mono); font-weight: 700; }
.wh-obstacle { cursor: pointer; }
.wh-obstacle.is-selected { cursor: grab; }
#whSvg.is-dragging, #whSvg.is-dragging .wh-obstacle.is-selected { cursor: grabbing; }
.wh-obstacle-rect { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1; transition: stroke-width var(--duration-fast) var(--ease-out); }
.wh-obstacle.is-advisory .wh-obstacle-rect { fill: var(--warn-soft); stroke: var(--warn); stroke-dasharray: 3 2; }
.wh-obstacle-icon { color: var(--accent); pointer-events: none; }
.wh-obstacle.is-advisory .wh-obstacle-icon { color: var(--warn); }
/* Selection — a strong, unambiguous purple outline regardless of the
   item's own type colour (fire exit green / walkway blue / column blue
   above all still apply otherwise), so there's never any doubt what's
   currently selected. #whSvg + !important beats the more specific
   [data-type=...] colour overrides below, same technique Layout's own
   .is-clashing/.is-approved already use for the same reason. */
#whSvg .wh-obstacle.is-selected .wh-obstacle-rect { stroke: var(--accent) !important; stroke-width: 3 !important; filter: drop-shadow(0 0 5px var(--accent-glow)); }
#whSvg .wh-obstacle.is-selected .wh-obstacle-icon { color: var(--accent) !important; }
.wh-obstacle.is-placing { opacity: 0.55; }
/* Clash highlight for manually placed items (D4-style red, matching
   Layout's own clashing treatment) — obstacle-vs-obstacle overlap only;
   see js/warehouse.js's lightweight clash check. Selection still wins
   visually (a selected+clashing item shows the purple selection ring,
   not red) since knowing *what's selected* matters most while actively
   fixing it. */
#whSvg .wh-obstacle.is-clashing:not(.is-selected) .wh-obstacle-rect { stroke: #dc2626 !important; stroke-width: 2.5 !important; stroke-dasharray: 4 3; }

/* Warehouse UI finishing pass — semantic colour for the 3 safety/
   structural obstacle types that read as misleadingly identical when
   every item shares the same accent-purple fill (same "everything
   purple" problem already fixed on Layout's own drawing). Reuses the
   exact colour values from that pass so a column/fire exit/walkway
   looks the same on both pages. Every other type (door, office,
   low_beam, floor_issue, keep_clear, charging, turning, generic
   obstacle) keeps the existing accent/advisory colouring untouched —
   only these three get a data-type override, and .is-advisory (soft/
   advisory items) still wins over type colour so the hard/advisory
   distinction production relies on is never hidden. */
#whSvg .wh-obstacle[data-type="fire_exit"]:not(.is-advisory) .wh-obstacle-rect { fill: rgba(22, 163, 74, 0.16); stroke: #16a34a; }
#whSvg .wh-obstacle[data-type="fire_exit"]:not(.is-advisory) .wh-obstacle-icon { color: #16a34a; }
#whSvg .wh-obstacle[data-type="walkway"]:not(.is-advisory) .wh-obstacle-rect { fill: rgba(14, 165, 233, 0.1); stroke: #0ea5e9; stroke-dasharray: 4 3; }
#whSvg .wh-obstacle[data-type="walkway"]:not(.is-advisory) .wh-obstacle-icon { color: #0ea5e9; }
#whSvg .wh-obstacle[data-type="column"]:not(.is-advisory) .wh-obstacle-rect { fill: rgba(79, 143, 224, 0.16); stroke: #4f8fe0; }
#whSvg .wh-obstacle[data-type="column"]:not(.is-advisory) .wh-obstacle-icon { color: #4f8fe0; }

/* ---------------------------------------------------------------------
   racking.html — the configurator stage
   -----------------------------------------------------------------------
   Unlike warehouse.html, the racking stage isn't one edge-to-edge
   canvas: the preview stays dominant (it owns the majority of the
   vertical space and all the floating chrome above), but the handful
   of choices that most change the rack's shape and character — Pallet
   Type, Beam Levels, Ground Level, Decking — sit permanently visible
   directly beneath it, large and tactile, rather than a full page of
   fields competing with the drawing.
--------------------------------------------------------------------- */
.rack-stage {
  flex-direction: row;
  align-items: stretch;
  justify-content: flex-start;
  padding: 0;
  overflow: hidden;
}
.rack-stage-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  order: 1;
}
/* ---- Racking Specification panel — the precise, real-millimetre
   fields (Frame Height/Depth, Beam Levels, Bay Width, Aisle Width,
   Flue, Pallets per Bay, Storage Configuration), permanently visible
   beside the preview. Reuses .snap-field / .big-stepper /
   .field-disclosure as-is, just no longer gated behind Fine-tune.
   Right-hand "structured configuration inspector" position (Major
   Visual/UX pass) — flex `order` moves it visually to the right of
   the now-dominant live preview while leaving its DOM position (and
   every existing js/racking.js id-based wire-up) exactly where it
   was, so tab/keyboard order and all event bindings are untouched.
   Border moves to the left edge to match its new position. ---- */
.rack-spec-rail {
  flex: none;
  width: 324px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-6);
  border-left: 1px solid var(--border);
  background: var(--bg-elevated);
  order: 2;
  /* Subtle, dark, app-integrated scrollbar — the compact spacing pass
     above should mean this rarely engages on a normal desktop viewport,
     but on shorter displays it must never read as a bright OS-default
     scrollbar dominating the panel. */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.rack-spec-rail::-webkit-scrollbar { width: 6px; }
.rack-spec-rail::-webkit-scrollbar-track { background: transparent; }
.rack-spec-rail::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--radius-pill); }
.rack-spec-rail .panel-title { margin-bottom: var(--space-7); }
/* Single-column layout pass — every rule below is scoped to
   .rack-spec-rail only (never the bare .field/.big-stepper/.swatch-card/
   .segmented themselves, which are shared by every other page's settings
   panels) so Warehouse/Install/Report are completely unaffected.
   Related fields that used to sit two-per-row in a cramped .field-grid
   now stack one full-width row per option (see racking.html — the
   field-grid wrappers around those pairs were removed there); every
   stepper/segmented toggle in this rail stretches to that full row width
   with its value/buttons spread out (justify-content: space-between)
   instead of staying a small pill hugging the left edge, which is what
   guarantees a value like "1,100 mm" always has room either side and
   never clips, and keeps the +/- buttons comfortably away from the
   value. .field-grid itself is kept only for the two spots that still
   use it as a plain 1-column stack (Custom Pallet Dims, Advanced), where
   row-gap still applies. */
.rack-spec-rail .field-grid { column-gap: var(--space-6); row-gap: var(--space-5); }
.rack-spec-rail .field { gap: 8px; }
.rack-spec-rail .big-stepper.stepper-sm { width: 100%; justify-content: space-between; padding: 6px 8px; gap: var(--space-3); }
.rack-spec-rail .stepper-sm .big-stepper-btn { width: 34px; height: 34px; font-size: 1em; flex: none; }
.rack-spec-rail .stepper-sm .big-stepper-value { flex: 1; min-width: 4.5ch; font-size: 1.15em; }
.rack-spec-rail .segmented { width: 100%; }
.rack-spec-rail .segmented-btn { flex: 1; text-align: center; }
/* Pallet Type — kept together on one row (Standard / Euro / Custom), but
   sized to share the row evenly (flex: 1) instead of a fixed pixel width,
   so it always fits the rail's actual content width without overflowing
   or clipping the "1200×1000" sub-label — a fixed-width version of this
   previously ran wider than the rail's own content box. */
.rack-spec-rail .primary-swatch-grid { display: flex; gap: var(--space-2); }
.rack-spec-rail .primary-swatch-grid .swatch-card { flex: 1 1 0; width: auto; min-width: 0; padding: 10px 6px; gap: 6px; }
.rack-spec-rail .primary-swatch-grid .swatch-glyph { width: 32px; height: 32px; }
.rack-spec-rail .primary-swatch-grid .swatch-glyph .icon { width: 1.1em; height: 1.1em; }
.rack-spec-rail .primary-swatch-grid .swatch-label { font-size: 0.76em; }
.rack-spec-rail .primary-swatch-grid .swatch-sub { font-size: 0.66em; }
.rack-stage-preview {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  background:
    linear-gradient(var(--border) 1px, transparent 1px),
    linear-gradient(90deg, var(--border) 1px, transparent 1px);
  background-size: 34px 34px;
  background-position: center;
}
.rack-stage-preview svg { position: relative; z-index: 1; width: auto; height: 100%; max-width: 100%; max-height: 100%; }
.rack-stage-preview .is-selectable { cursor: pointer; }
/* Preview dimension readouts (#frameHeightLabel/#beamLengthLabel) — same
   shared .mock-dim-label class the homepage mockups use, scoped here to
   #rackSvg only so Warehouse/Home's own use of that class is untouched.
   Switched to Geist Mono (matches every other technical/numeric readout
   in the app per the VISUAL MODERNISATION PASS rule below) and given a
   touch more size/contrast so the two real measurements on the now-
   dominant preview are easy to read at a glance, not a faint afterthought. */
#rackSvg .mock-dim-label { font-family: var(--font-mono); font-size: 10px; font-weight: 700; fill: var(--text-muted); }
/* Beam level height readouts beside the 2D elevation (generated by paintRack). */
#rackSvg .rack-level-label { font-family: var(--font-mono); font-size: 8.5px; font-weight: 600; fill: var(--text-muted); }

/* Primary bar — Racking Configurator visual/UX pass: Layout Quantities,
   Decking, Column Protection, Guards/Deflectors and End-of-Rack Barriers
   have all moved up into .rack-spec-rail's new Run/Beams/Pallets/
   Accessories sections (every configuration field now lives in one
   place), so this strip below the preview is left with just its one
   true role — the live Generate Layout summary/action row. Kept as a
   flex row (rather than collapsing to block-level) so .rack-summary-
   content's own internal layout still controls alignment unchanged. */
.rack-primary-bar {
  flex: none;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: stretch;
  padding: var(--space-3) var(--space-6);
  border-top: 1px solid var(--border);
  background: var(--bg-elevated);
}
.primary-bar-block {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: none;
  padding: var(--space-2) var(--space-5);
  border-left: 1px solid var(--border);
}
.primary-bar-block:first-child { padding-left: 0; border-left: none; }
.primary-bar-row-label { font-size: 0.7em; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-faint); margin: 0 0 var(--space-2); white-space: nowrap; }

/* Decking / Column Protection — a compact horizontal pill (icon beside
   the label, single line) instead of the taller stacked-glyph card
   Pallet Type uses. Now rendered inside the narrower .rack-spec-rail
   (see Accessories section there), so it wraps onto multiple lines
   rather than the single unbroken row it used to force in the wider
   bottom bar. IDs are unique to this page, so scoping directly off them
   (rather than a shared modifier class) keeps this from ever touching
   Pallet Type's own swatch styling. */
#deckingGroup, #columnProtectionSeg { display: flex; flex-wrap: wrap; gap: var(--space-2); }
#deckingGroup .swatch-card, #columnProtectionSeg .swatch-card {
  flex-direction: row;
  align-items: center;
  width: auto;
  gap: 8px;
  padding: 8px 14px;
  min-height: 42px;
}
#deckingGroup .swatch-glyph, #columnProtectionSeg .swatch-glyph { width: 26px; height: 26px; flex: none; }
#deckingGroup .swatch-glyph .icon, #columnProtectionSeg .swatch-glyph .icon { width: 1.05em; height: 1.05em; }
#deckingGroup .swatch-label, #columnProtectionSeg .swatch-label { font-size: 0.84em; white-space: nowrap; }

/* ---- Layout Summary block — read-only totals (the exact values that
   used to float over the preview as .canvas-readout) plus the Generate/
   Regenerate action, grouped together so the result and the button that
   produces it sit in the same place. Takes the row's remaining width. ---- */
.rack-summary-block { flex: 1 1 320px; }
.rack-summary-content { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); flex-wrap: wrap; height: 100%; }
.rack-summary-stats { display: flex; gap: var(--space-6); flex-wrap: wrap; }
.rack-summary-stat { display: flex; flex-direction: column; gap: 1px; }
.rack-summary-stat-value { font-family: var(--font-display); font-size: 1.2em; font-weight: 700; color: var(--text); line-height: 1.1; }
.rack-summary-stat-label { font-size: 0.68em; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-faint); }
.rack-summary-actions { display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-2); flex: none; }
.rack-summary-actions #generateValidationSummary { margin: 0; max-width: 360px; }
.rack-summary-buttons { display: flex; gap: var(--space-3); }
@media (max-width: 640px) {
  .rack-summary-content { align-items: stretch; }
  .rack-summary-actions { align-items: stretch; }
  .rack-summary-buttons { justify-content: flex-end; }
}

.run-length-strip {
  display: flex;
  gap: 3px;
  margin-top: var(--space-3);
  max-width: 260px;
  flex-wrap: wrap;
}
.run-length-strip span {
  width: 9px;
  height: 20px;
  border-radius: 2px;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  flex: none;
}

@media (max-width: 1280px) {
  .rack-spec-rail { width: 284px; }
}
@media (max-width: 1180px) {
  .primary-bar-block { padding-left: var(--space-4); padding-right: var(--space-4); }
}
@media (max-width: 900px) {
  /* Racking Specification stacks below the preview instead of squeezing
     it — same fields, no duplicate mobile copy, per the responsive
     brief (side panels may become stacked sections on small screens).
     The panel is right of the preview on desktop (order: 2, set in the
     base rule above); that same order keeps it stacking *after* the
     preview here on mobile too, so this block only needs to reset the
     rail's sizing/border for a stacked layout, not the order itself. */
  .rack-stage { flex-direction: column; overflow-y: auto; }
  .rack-stage-main { flex: none; }
  .rack-stage-preview { min-height: 340px; }
  /* Same nested-scroll-region fix as .wh-tools-rail in warehouse.html
     above: this rail's own max-height + overflow-y:auto (base rule)
     created a second scroll area inside .rack-stage's own scroll, so a
     touch swipe over the rail scrolled only the rail to its own bottom
     (Layout Quantities/Decking/Guards) without reaching the frame
     height/beam/pallet fields above them, reading as "missing details".
     Letting it size to its full content height means .rack-stage is the
     only scroller and one swipe reaches every field top to bottom. */
  .rack-spec-rail { width: auto; max-height: none; overflow-y: visible; border-left: none; border-top: 1px solid var(--border); }

  /* Bottom strip now holds only Layout Summary, so it just needs to
     stack full-width like every other stacked block on mobile. */
  .rack-primary-bar { flex-direction: column; align-items: stretch; }
  .primary-bar-block {
    padding: var(--space-3) 0;
    border-left: none;
    border-top: 1px solid var(--border);
  }
  .primary-bar-block:first-child { border-top: none; padding-top: 0; }
  #deckingGroup, #columnProtectionSeg { row-gap: var(--space-2); }
  .rack-summary-block { flex: none; }
}

/* =======================================================================
   install.html — full desktop application shell
   -----------------------------------------------------------------------
   Warehouse and Racking are canvas-first (one drawing dominates). RM
   Install is naturally data-driven instead, so it embraces a classic
   desktop-app composition: a narrow application sidebar, a scrollable
   main workspace organised into clearly bounded sections (Project
   Overview / Installation Settings / Optional Extras / Notes), and a
   permanently visible summary panel on the right where Total Labour
   Cost dominates. Every section is a short uppercase label followed
   by a bounded, rounded panel — information-dense but never a
   spreadsheet, because grouping and spacing carry the hierarchy, not
   borders around every cell. Structurally identical in both themes;
   only token values (accent/surface colours) differ.
   ======================================================================= */

.install-shell { flex: 1; min-height: 0; display: flex; overflow: hidden; }

/* ---- main workspace — no artificial width cap: the sidebar and
   summary panel stay compact and fixed, and everything else belongs
   to the workspace, which now runs edge to edge (bar its own padding)
   almost to the summary panel, the way a real desktop application's
   centre column would. The very high max-width below is only a
   safety ceiling for ultra-wide monitors, not a working constraint on
   any normal desktop or laptop screen. ---- */
.install-main { flex: 1; min-height: 0; overflow-y: auto; }
/* Batch 5: tightened outer padding/gap (was --space-7/--space-8 right and
   bottom, --space-5 between sections) so the whole estimator sits more
   compactly — the goal is most or all of it fitting comfortably at a
   normal 1920x1080 desktop without feeling cramped. */
.install-main-inner {
  max-width: 2000px;
  margin: 0 auto;
  padding: var(--space-5) var(--space-6) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.install-page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
}
.install-back-link { display: inline-flex; align-items: center; gap: 6px; font-size: 0.8em; font-weight: 700; color: var(--text-faint); margin-bottom: var(--space-2); cursor: pointer; }
.install-back-link .icon { width: 1em; height: 1em; transform: rotate(180deg); }
.install-back-link:hover { color: var(--accent); }
.install-page-head h1 { font-family: var(--font-display); font-size: 1.5em; font-weight: 700; margin: 0 0 4px; }
.install-page-head p { font-size: 0.88em; font-weight: 500; color: var(--text-faint); margin: 0; }

/* ---- unified workspace panel — Project Overview, Installation
   Settings, Optional Extras and Notes used to live in three or four
   separate floating rounded cards with gaps between them, which read
   as "a stack of web cards" rather than a real application workspace.
   They now share a single bordered surface with internal hairline
   dividers between sections — the way a property panel in Figma,
   Linear or an Autodesk tool actually works: one container, many
   sections, never a repeated card component. ---- */
.install-workspace-panel {
  display: flex;
  flex-direction: column;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.install-workspace-panel > * { border-top: 1px solid var(--border); }
.install-workspace-panel > *:first-child { border-top: none; }

.install-ws-section { padding: var(--space-5) var(--space-6); display: flex; flex-direction: column; gap: var(--space-4); }
.install-ws-title {
  font-family: var(--font-display);
  font-size: 0.74em;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ---- From Layout — Batch 5 (RM Install visual consolidation). Replaces
   the old 7-tile "Project Overview" dashboard strip (large stat-card
   figures, generous per-tile padding) with a single compact status line:
   same 7 read-only facts, same ID'd elements js/install.js already
   writes to every recompute(), just given the visual weight of a caption
   instead of a row of headline numbers — it's context for the estimate
   below, not a result in its own right, and shouldn't visually compete
   with the actual inputs. No Warehouse Type / Location fields — no
   production RM Install counterpart, so none are shown. ---- */
.install-ws-section--compact { padding: var(--space-4) var(--space-6); gap: var(--space-1); }
.install-from-layout { display: flex; flex-direction: column; gap: 3px; }
.install-from-layout-eyebrow {
  font-family: var(--font-display);
  font-size: 0.68em;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.install-from-layout-facts {
  margin: 0;
  font-size: 0.92em;
  font-weight: 600;
  color: var(--text);
}
.install-from-layout-facts strong { font-family: var(--font-display); font-weight: 700; color: var(--text); }
.install-from-layout-sub {
  margin: 0;
  font-size: 0.78em;
  font-weight: 500;
  color: var(--text-faint);
}
.install-from-layout-sub strong { font-weight: 700; color: var(--text-muted); }
.install-from-layout-hint { font-style: italic; }

/* ---- Installation Settings + Optional Extras — one shared row of
   the workspace panel, split by a single vertical rule instead of two
   separate rounded boxes with a gap between them. Settings takes the
   larger share of the width since its own field grid needs the room. ---- */
.install-ws-split { display: grid; grid-template-columns: 1.3fr 1fr; }
.install-ws-split > .install-ws-col:first-child { border-right: 1px solid var(--border); }
.install-ws-col { padding: var(--space-5) var(--space-6); display: flex; flex-direction: column; gap: var(--space-5); min-width: 0; }

/* Installation Settings — the dominant working area of the page.
   Caption-above-control fields arranged two columns wide (Crew Size |
   Labour Rate, Working Days | Start Date, Weekend Work | Site
   Complexity). Batch 5: tightened from a 48px/96px row/column gap (which
   read as "spread across the page" rather than "a compact input group")
   down to a still-comfortable, genuinely clickable 20px/40px — density
   without cramping. */
.install-settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5) var(--space-6); }
.install-field { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

/* Crew Size — fixed, not editable. A quiet sunken chip (small icon +
   value, muted text) rather than the same large tap-to-edit display
   type Labour Day Rate uses, so the two fields never look like
   equally-editable controls at a glance (Batch 5). */
.install-field-readonly .install-field-label { color: var(--text-muted); }
.install-readonly-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  margin: 0;
  padding: 9px 14px;
  font-family: var(--font-sans);
  font-size: 0.86em;
  font-weight: 700;
  color: var(--text-muted);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.install-readonly-chip .icon { width: 15px; height: 15px; color: var(--text-faint); flex: none; }

/* Labour Day Rate — the one field that actually gates the estimate.
   A soft accent-tinted cell (not a bigger font — same size as before)
   marks it as the primary input without shouting, echoing the
   Racking Configurator's own snap-field accent treatment (Batch 5). */
.install-field-primary .install-field-control {
  padding: var(--space-3) var(--space-4);
  background: color-mix(in srgb, var(--accent) 5%, var(--bg-sunken));
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-md);
}
.install-field-primary .install-rate-input-wrap input { border-bottom-color: var(--accent-line); }
.install-field-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-family: var(--font-display);
  font-size: 0.86em;
  font-weight: 600;
  color: var(--text);
}
.install-field-hint { font-family: var(--font-sans); font-size: 0.74em; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--text-faint); }
.install-field-control { display: flex; align-items: center; }
.install-field-control select,
.install-field-control input[type="date"],
.install-field-control #startDateField {
  width: 100%;
  min-height: 42px;
  padding: 8px 14px;
  font-family: var(--font-sans);
  font-size: 0.88em;
  font-weight: 600;
  color: var(--text);
  background: var(--bg-sunken);
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.install-field-control #startDateField { cursor: text; }
.install-field-control select {
  padding-right: 36px;
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 14px;
  appearance: none;
  -webkit-appearance: none;
}
.install-field-control select:focus,
.install-field-control input[type="date"]:focus,
.install-field-control #startDateField:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

/* Crew Size (#crewOverrideInput) is a plain, unstyled native number
   input by default — it fell outside the select/date rule above, so it
   rendered with the browser's own light input chrome instead of this
   page's dark field styling. Locked/readonly (see the field markup),
   so no pointer/focus-glow affordances are needed, just the same
   background/border/text treatment as every other input box here. */
#crewOverrideInput {
  width: 100%;
  min-height: 42px;
  padding: 8px 14px;
  font-family: var(--font-sans);
  font-size: 0.88em;
  font-weight: 600;
  color: var(--text);
  background: var(--bg-sunken);
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: default;
}

.install-field-rate .install-field-control { flex-direction: column; align-items: stretch; gap: var(--space-3); }

/* Labour Day Rate — a real numeric input (not a slider) so it can
   honestly represent production's "blank = no cost assumed" state;
   a range input has no way to be blank. */
.install-rate-input-wrap { display: flex; align-items: baseline; gap: 6px; }
.install-rate-prefix { font-family: var(--font-display); font-size: 1.6em; font-weight: 700; color: var(--text-faint); }
.install-rate-input-wrap input {
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 1.6em;
  font-weight: 700;
  color: var(--text);
  background: none;
  border: none;
  border-bottom: 1.5px solid var(--border-strong);
  padding: 2px 0;
}
.install-rate-input-wrap input::placeholder { color: var(--text-faint); font-weight: 600; }
.install-rate-input-wrap input:focus { outline: none; border-bottom-color: var(--accent); }

/* Optional extras — premium settings rows, not a spreadsheet: no
   dividing lines between rows, just spacing and a soft, contained
   hover state, the way Apple Settings or Linear preferences group
   items rather than a table. */
/* Batch 5: rows tightened (was --space-4 all round + 38px icons) — a
   compact professional estimate-builder list rather than a settings
   panel with room to spare, matching the density target for this page. */
.install-extras-list { display: flex; flex-direction: column; gap: 1px; }
.extras-row { display: flex; align-items: center; gap: var(--space-3); padding: 9px var(--space-3); border-radius: var(--radius-sm); transition: background var(--duration-fast) var(--ease-out); }
.extras-row:hover { background: color-mix(in srgb, var(--text) 4%, transparent); }
.extras-icon { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); background: var(--accent-soft); color: var(--accent); flex: none; }
.extras-icon .icon { width: 15px; height: 15px; }
.extras-label { font-size: 0.86em; font-weight: 700; color: var(--text); }
.extras-sub { font-size: 0.72em; font-weight: 500; color: var(--text-faint); }
/* Price and toggle always reserve their column width — visibility
   (not display) is what switches off, so every row's icon/title/
   price/toggle line up in the same four columns whether or not that
   extra is currently on. */
/* Form-consistency pass: this used to be background:none/border:none —
   fully invisible as an editable field until hovered/focused, with no box
   at all. Now matches .snap-field-value's already-established treatment
   for this exact shape of control (a compact, right-aligned, ~6-character
   editable number) instead of being a one-off borderless pattern. */
.extras-amount {
  display: inline-flex;
  align-items: baseline;
  justify-content: flex-end;
  gap: 3px;
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--accent);
  font-size: 0.9em;
  background: var(--bg-sunken);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 4px 8px;
  width: 6ch;
  text-align: right;
  margin-right: var(--space-2);
  visibility: hidden;
  /* Same reasoning as .rm-switch above — this is a real editable field
     (contenteditable), not a zoomable surface. */
  touch-action: manipulation;
  transition: border-color var(--duration-fast) var(--ease-out), background var(--duration-fast) var(--ease-out);
}
.extras-row.is-on .extras-amount { visibility: visible; }
.extras-amount:hover { border-color: var(--text-faint); }
.extras-amount:focus {
  outline: none;
  background: var(--bg-elevated);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* ---- primary action — Batch 5: was a full-width, 72px-tall bar with a
   heavy accent glow, reading as an ecommerce-checkout CTA rather than a
   professional estimating tool's confirm button. Every figure already
   updates live as fields change; this button is a normal-sized, left-
   aligned confirm action (the app's own standard .btn sizing — see
   css/style.css's .btn/.btn-accent — no page-specific size override) that
   forces a fresh recompute and pulses the total, kept per the design
   reference so it's never a dead button, just no longer the dominant
   element on the page. A quiet hint alongside it makes clear the button
   isn't the only way the estimate updates. ---- */
/* Switch — track + thumb, button-based (role="switch") so it behaves
   like a real toggle without needing a hidden checkbox. Used by the
   Optional Extras rows and the Weekend Work row. */
.rm-switch {
  position: relative;
  flex: none;
  width: 46px;
  height: 27px;
  border-radius: var(--radius-pill);
  background: var(--border-strong);
  border: none;
  cursor: pointer;
  /* Removes the ~300ms tap-delay/double-tap-zoom ambiguity some mobile
     browsers still apply to small controls sitting inside a scrollable
     panel — this is a real toggle button, never a pinch/zoom target. */
  touch-action: manipulation;
  transition: background var(--duration-fast) var(--ease-out);
}
.rm-switch::before {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 21px;
  height: 21px;
  border-radius: 50%;
  background: #fff;
  box-shadow: var(--shadow-sm);
  transition: transform var(--duration-fast) var(--ease-out);
}
.rm-switch[aria-checked="true"] { background: var(--accent); }
.rm-switch[aria-checked="true"]::before { transform: translateX(19px); }
.rm-switch:disabled { opacity: 0.35; cursor: not-allowed; }

@media (max-width: 900px) {
  /* Weekend Work / optional-cost toggles (Installation Calculator) — the
     visible track is only 27px tall, well under the app's own 44px touch
     target rule, so a tap that lands a few px above/below the pill
     (easy on a phone) misses the button entirely and reads as "the
     switch doesn't activate". Expands the real hit area via an invisible
     ::after (no background, so nothing about the switch's look changes)
     rather than resizing the visible pill, which would change its
     appearance on every screen size including desktop. */
  .rm-switch::after { content: ''; position: absolute; inset: -9px; }
}

/* Breakdown sheet — plain label/value rows grouped under small
   headings, matching the read-only reporting style used elsewhere
   (report.js) rather than inventing a new table component. */
.breakdown-group { margin-bottom: var(--space-6); }
.breakdown-group:last-child { margin-bottom: 0; }
/* INSTALLATION VISUAL/UX PASS — group titles were plain accent-coloured
   text; now a filled navy/cyan band (var(--accent)/var(--accent-soft)),
   matching the BOM pass's grouped-schedule row treatment (js/layout.js's
   .bom-group-row) so the app's two "grouped breakdown" surfaces read as
   one consistent design language rather than two different patterns. */
.breakdown-group-title {
  font-family: var(--font-display);
  font-size: 0.72em;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  background: var(--accent-soft);
  border-left: 3px solid var(--accent);
  margin: 0 0 var(--space-3);
  padding: 6px 10px;
  border-radius: var(--radius-sm);
}
.breakdown-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); padding: 6px 2px; border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.breakdown-row:last-child { border-bottom: none; }
.breakdown-row span { font-size: 0.85em; font-weight: 500; color: var(--text-muted); }
.breakdown-row strong { font-family: var(--font-display); font-size: 0.9em; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; text-align: right; }
.breakdown-row strong.is-total { color: var(--accent); }
.breakdown-note { font-size: 0.8em; color: var(--text-faint); line-height: 1.5; margin: 0 0 var(--space-4); }

/* Grand Total group — the one figure this whole sheet builds up to gets
   a quietly prominent card (same accent-tinted-box language as the
   summary panel's own .install-summary-total), not just another plain
   row, so the sheet still reads "here's the number" even after four
   groups of detail. */
.breakdown-group-grand {
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--accent) 8%, var(--bg-elevated));
  border: 1px solid var(--accent-line);
}
.breakdown-group-grand .breakdown-group-title { background: none; border-left: none; padding: 0; margin-bottom: 6px; }
.breakdown-group-grand .breakdown-row { border-bottom: none; padding: 2px 0; }
.breakdown-group-grand .breakdown-row strong.is-total { font-size: 1.3em; }

/* Summary strip at the top of the sheet — same compact stat-tile pattern
   used by the printable Report's BOM/Capacity sections (js/report.js's
   statGrid()), reused here rather than invented fresh, so the "what does
   this whole estimate add up to" answer is visible before scrolling
   through every group underneath. */
.breakdown-stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); margin-bottom: var(--space-5); }
.breakdown-stat { padding: 8px 6px; border-radius: var(--radius-sm); background: var(--bg-sunken); border: 1px solid var(--border); text-align: center; }
.breakdown-stat-num { display: block; font-family: var(--font-display); font-size: 1.1em; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.breakdown-stat-lbl { display: block; font-size: 0.62em; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-faint); margin-top: 2px; }
.breakdown-stat-total { background: color-mix(in srgb, var(--accent) 8%, var(--bg-elevated)); border-color: var(--accent-line); }
.breakdown-stat-total .breakdown-stat-num { color: var(--accent); }
@media (max-width: 480px) {
  .breakdown-stat-grid { grid-template-columns: repeat(2, 1fr); }
}

/* Brief pulse whenever a live figure updates — enough motion to read as
   "alive", not enough to be distracting. */
@keyframes valuePulse {
  0% { transform: scale(1); }
  35% { transform: scale(1.045); }
  100% { transform: scale(1); }
}
.value-pulse { animation: valuePulse 420ms var(--ease-out); }

/* ---- right summary panel — permanently visible, Total Labour Cost
   is the one figure allowed to be visually loud; everything else is
   a quiet label/value row so the hierarchy stays obvious at a glance.
   Widened and given more internal air so the seven figures it holds
   read as a considered summary, not a dense list crammed into a
   narrow rail. ---- */
.install-summary {
  flex: none;
  width: 320px;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-6) var(--space-5);
  border-left: 1px solid var(--border);
  background: var(--bg-elevated);
  overflow-y: auto;
}
.install-summary-title { font-family: var(--font-display); font-size: 0.92em; font-weight: 700; margin: 0; }

/* ---- Batch 5: restrained incomplete state. Previously the summary
   panel showed the SAME huge 2.5em gradient-text total box at all
   times, just with its text swapped to "Not calculated yet" — a
   full-weight purple result box announcing that there IS no result yet,
   which was the single biggest contributor to the panel feeling like a
   dashboard rather than an estimate in progress. Now: while Labour Day
   Rate is unset, a small quiet notice takes the total's place instead
   (installSummaryPanel.is-incomplete, toggled by js/install.js from the
   exact same costResult.hasRate the total's own text already depended
   on), and the total block itself is hidden rather than shown empty.
   Every row that doesn't actually depend on a day rate — crew size,
   estimated labour days, chargeable days, both dates — stays visible
   the whole time, since those figures are genuinely already known. ---- */
.install-summary-missing { display: none; }
.install-summary.is-incomplete .install-summary-missing {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: var(--space-5);
  border-radius: var(--radius-md);
  background: var(--bg-sunken);
  border: 1px dashed var(--border-strong);
}
.install-summary.is-incomplete .install-summary-total { display: none; }
.install-summary-missing-title { margin: 0; font-size: 0.82em; font-weight: 600; color: var(--text-muted); line-height: 1.4; }
.install-summary-missing-link {
  align-self: flex-start;
  font-family: var(--font-display);
  font-size: 0.82em;
  font-weight: 700;
  color: var(--accent);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--accent-line);
  text-underline-offset: 3px;
}
.install-summary-missing-link:hover { text-decoration-color: var(--accent); }
.install-summary-missing-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }

.install-summary-rows { display: flex; flex-direction: column; gap: var(--space-6); }
.install-summary-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); }
.install-summary-row span { font-size: 0.8em; font-weight: 600; color: var(--text-faint); }
.install-summary-row strong { font-family: var(--font-display); font-size: 1em; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }

.install-summary-total {
  position: relative;
  padding: var(--space-6) var(--space-5);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--accent) 8%, var(--bg-elevated));
  border: 1px solid var(--accent-line);
  overflow: hidden;
}
.install-summary-total-label { display: block; font-size: 0.72em; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 10px; }
.install-summary-total-value {
  display: block;
  font-family: var(--font-display);
  font-size: 2.2em;
  font-weight: 700;
  letter-spacing: -0.01em;
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 4px 14px var(--accent-glow));
}
/* Strong "next step" CTA (demo-polish pass) — full width, sits right
   under the total so it reads as the obvious way to move on from Install.
   Deliberately the loud .btn-accent treatment (same gradient/hover-lift
   every other primary action on this app uses) so it visually outranks
   the quiet .install-summary-breakdown ghost button beneath it. */
.install-summary-report-cta { width: 100%; margin-top: var(--space-4); }
.install-summary-breakdown {
  width: 100%;
  justify-content: center;
  margin-top: var(--space-2);
  min-height: 40px;
  font-size: 0.86em;
  background: transparent;
  border-color: var(--border);
}
.install-summary-breakdown:hover { background: var(--bg-elevated-2); }

/* ---------------------------------------------------------------------
   Responsive
--------------------------------------------------------------------- */
@media (max-width: 900px) {
  .canvas-step:not(.is-active) { display: none; }
  .canvas-step-line { display: none; }
  .canvas-identity-dims { display: none; }
  .canvas-inspector { width: 210px; }
  /* Layout's Run Inspector (#runInspector) carries real sectioned content
     (RUN/DIMENSIONS/STORAGE/SPACING/EQUIPMENT & PROTECTION/More Details)
     — 210px would clip its two-column field grids and the action row.
     Compound selector (matches the desktop .canvas-inspector.canvas-
     inspector-wide rule's specificity) so this tablet override reliably
     wins over BOTH the generic 210px rule just above and the desktop
     620px rule outside this media query, purely on source order — still
     narrows from the desktop 620px so it never eats the whole tablet
     viewport. */
  .canvas-inspector.canvas-inspector-wide { width: min(440px, calc(100vw - 24px)); }
  /* warehouse.html's #inspector (selected-object fields: Type/Label/X/
     Y/W/D) reverts to a bottom, thumb-zone sheet on tablet/phone instead
     of a small floating card — full width, capped height, own internal
     scroll (same "keep the warehouse visible while editing" principle —
     the canvas stays visible above it, never a full-screen modal). ID
     selector outweighs the generic .canvas-inspector rule just above. */
  #inspector {
    left: var(--space-3);
    right: var(--space-3);
    top: auto;
    bottom: var(--space-3);
    width: auto;
    max-height: 46vh;
    /* Without display:flex here, .ep-inspector-body's flex:1/overflow-y:
       auto (meant to make the body scroll internally once content is
       taller than the 46vh cap) had no effect — #inspector never opted
       into the flex layout those rules depend on. The panel is
       bottom-anchored (positioned by `bottom`, growing upward as content
       grows), so overflowing content pushed the head — and its only
       close button — up past the top of the screen with no way to
       scroll back to it: exactly "comes up big, no way to get off it"
       when placing/editing an obstacle with several fields open at once.
       Scrolling now happens inside the body instead, so the head with
       the close button always stays pinned in view. */
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  /* .layout-tools-rail's own 900px behaviour is defined with the rest of
     its rules further down (visual-correction pass). */
}

/* install.html's three-column shell — collapses in stages so the
   summary panel and every section stay fully visible (never hidden
   behind a drawer) at every width, per the "don't hide features"
   brief. Installation Settings and Optional Extras stay side by side
   from large desktop down through laptop widths, matching the design
   reference, and only stack once the shell itself drops to a single
   column at tablet width — "stack only if genuinely necessary". */
@media (max-width: 1280px) {
  /* laptop — sidebar and summary reduce slightly, but every label
     stays fully visible and readable; only the centre workspace's own
     gaps tighten. */
  .install-summary { width: 300px; }
  .install-main-inner { gap: var(--space-4); padding: var(--space-5) var(--space-5) var(--space-6); }
  .install-ws-col { padding: var(--space-5); }
  .install-ws-section { padding: var(--space-4) var(--space-5); }
}
@media (max-width: 900px) {
  /* tablet/mobile — the calculator's own shell (main workspace + summary
     panel) stacks into a single column; real cross-page navigation is
     the shared .app-sidebar now, which hides itself at this breakpoint
     (rule above) in favour of the top bar carrying the page. */
  .install-shell { flex-direction: column; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; }
  /* Same nested-scroll-trap fix as .wh-tools-rail/.rack-spec-rail above:
     .install-main's desktop rule (flex:1; min-height:0; overflow-y:auto)
     turns it into its OWN bounded scroll region once .install-shell goes
     single-column, so a touch swipe over the Installation Settings /
     Optional Extras area (Day Rate, Fuel, MEWP etc.) only scrolled that
     region to its own short flex-allocated height instead of reaching
     the rest of the page — reading as "can't see Day Rate/Fuel/MEWP" or
     "the calculator isn't scrollable". Letting it size to full content
     height means .install-shell is the only scroller. */
  .install-main { flex: none; min-height: 0; overflow-y: visible; }
  .install-summary { width: auto; flex: none; overflow-y: visible; border-left: none; border-top: 1px solid var(--border); }
  .install-main-inner { padding: var(--space-5) var(--space-5) var(--space-6); }

  /* below this point the shell itself has gone single-column, so
     Installation Settings / Optional Extras and the internal Settings
     field grid stack too — genuinely necessary, per the brief. */
  .install-ws-split { grid-template-columns: 1fr; }
  .install-ws-split > .install-ws-col:first-child { border-right: none; border-bottom: 1px solid var(--border); }
  .install-settings-grid { grid-template-columns: 1fr; gap: var(--space-5); }

  /* Bug fix: "View Project Report" was technically reachable, just
     buried at the very bottom of a long single-column page — Project
     Overview, every Installation Settings field, every Optional Extra,
     THEN the summary panel, THEN finally this button (confirmed via a
     live check: it rendered well over 1500px down the page). A first-
     time user has no reason to scroll that far, and even a deliberate
     tap can miss if the button was never scrolled into view first —
     which reads as "the link doesn't work" even though the href/click
     handler were always fine. Pinning it as a fixed bottom bar (the
     same floating/bottom-sheet principle already used for the zoom
     control and inspectors elsewhere) makes it reachable with one thumb
     from anywhere on the page — no scrolling required — matching how a
     native iOS primary action behaves. env() keeps it clear of the home
     indicator; the summary panel gets matching bottom padding so its
     last row is never hidden behind the fixed bar. */
  .install-summary-report-cta {
    position: fixed;
    /* Root-cause fix: the base rule above sets width:100% (correct for
       its normal in-flow position). Once this becomes position:fixed,
       having left + right + width:100% all specified at once is
       over-constrained CSS — per spec the browser must then ignore
       `right` and honour `width` literally, so the box rendered as
       16px + 100% of the viewport wide, its right edge sitting 16px
       past the actual screen edge. width:auto here lets left/right
       alone define the box, which is the correct way to size a fixed
       bar with equal side margins. */
    width: auto;
    left: var(--space-4);
    right: var(--space-4);
    bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
    z-index: 20;
    margin-top: 0;
    box-shadow: var(--shadow-xl);
  }
  .install-summary { padding-bottom: 76px; }

  /* Root-cause fix: Labour Day Rate is the one field styled as an
     "underline" hero number (no border box, padding: 2px 0 — see
     .install-rate-input-wrap input above) rather than a boxed input
     like every other field on this page. Its real tappable box was
     only ~35px tall — under the app's own 44px minimum — while the
     accent-tinted card around it (.install-field-primary
     .install-field-control's own padding) LOOKS like the tappable
     area but isn't: that padding belongs to a wrapping div, not the
     <input> itself, so taps anywhere in that visually-obvious card
     except the thin text sliver landed on nothing. Extra vertical
     padding on the input itself (same transparent background/no
     border-box, so the look is unchanged beyond a bit more breathing
     room) grows its real hit area to match. */
  .install-rate-input-wrap input { padding: 14px 0; }
}
@media (max-width: 640px) {
  .canvas-stepper-bar { gap: var(--space-3); padding: 8px var(--space-4); }
  .canvas-stage { padding: var(--space-4); }
  .canvas-dock-btn span { white-space: nowrap; }
  .canvas-zoom { bottom: var(--space-4); right: var(--space-4); }
  .canvas-readout { top: var(--space-4); left: var(--space-4); }
  /* Compound selector included alongside the plain one so the wide Run
     Inspector variant (higher specificity than the plain 900px/desktop
     rules above) still collapses to this full-width phone bottom sheet
     instead of getting stuck at the tablet 440px width. Explicit
     max-height added here: without it, the wide variant's own desktop
     cap (min(90vh, 820px), meant for a large screen) carried straight
     through to phones, so a Run Inspector with several field sections
     could balloon to ~90% of the viewport — reading as an oversized
     panel dominating the page rather than a compact utility card,
     regardless of how little of it actually had content. Capped well
     below the viewport so the canvas above it stays visibly in view,
     matching warehouse.html's own #inspector treatment just above. */
  .canvas-inspector, .canvas-inspector.canvas-inspector-wide { top: auto; bottom: calc(var(--space-4) + 66px); right: var(--space-4); left: var(--space-4); width: auto; max-height: 48vh; }
  .canvas-actions { padding: var(--space-3) var(--space-4); padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px)); gap: var(--space-2); }
  .canvas-actions .btn { padding-left: 16px; padding-right: 16px; }
  .swatch-grid { grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); }
  .rack-primary-bar { padding: var(--space-4); gap: var(--space-5); }
  .install-main-inner { padding: var(--space-4) var(--space-4) var(--space-6); gap: var(--space-5); }
}

/* ---------------------------------------------------------------------
   Phase D5 — 3D Viewer. A full-viewport overlay above the 2D canvas
   (the native iOS pattern for immersive 3D/AR content — not a bottom
   sheet or modal, since the brief for this phase explicitly wants the
   canvas to dominate the page). Same premium dark/light glass chrome as
   the rest of the app; the scene itself stays legible in both themes
   because its background/floor/fog colours are fixed rather than
   theme-linked, same as production.
--------------------------------------------------------------------- */
.view3d-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(4, 3, 8, 0.72);
  z-index: 200;
  opacity: 1;
  transition: opacity var(--duration-med) var(--ease-out), display var(--duration-med) allow-discrete;
}
@starting-style { .view3d-backdrop { opacity: 0; } }
.view3d-panel {
  position: fixed;
  inset: 0;
  z-index: 201;
  display: flex;
  flex-direction: column;
  background: #1a1d24; /* matches the 3D scene background so the canvas edge never "seams" */
  opacity: 1;
  transform: scale(1);
  transition: opacity var(--duration-med) var(--ease-out), transform var(--duration-med) var(--ease-out), display var(--duration-med) allow-discrete;
}
@starting-style { .view3d-panel { opacity: 0; transform: scale(0.98); } }
/* Entrance only — JS still flips display:none synchronously on close (see
   js/view3d.js close3DView()), which every existing display-state test
   asserts on immediately after clicking Close/Escape. Delaying that would
   break those tests, so there's no matching exit fade; opening still gets
   a real, restrained materialize-in via @starting-style, which needs no
   JS timing changes at all. */
.view3d-topbar,
.view3d-bottombar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  background: color-mix(in srgb, var(--bg-elevated) 88%, transparent);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  z-index: 2;
}
.view3d-topbar { padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px)); }
.view3d-bottombar { border-bottom: none; border-top: 1px solid var(--border); justify-content: space-between; flex-wrap: wrap; padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px)); }
.view3d-camviews {
  display: flex;
  align-items: center;
  gap: 4px;
  margin: 0 auto;
  padding: 4px;
  border-radius: var(--radius-pill);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
}
.view3d-toolbar-btn {
  min-height: 36px;
  padding: 0 14px;
  border-radius: var(--radius-pill);
  color: var(--text-muted);
  font-size: 0.82em;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.view3d-toolbar-btn:hover { background: var(--bg-elevated-2); color: var(--text); }
.view3d-toolbar-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.view3d-toolbar-btn.active { background: var(--accent-soft); color: var(--accent); }
.view3d-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 40px;
  padding: 0 16px;
  border-radius: var(--radius-pill);
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 0.84em;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
  flex: none;
}
.view3d-btn:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); }
.view3d-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.view3d-btn .icon { width: 1.1em; height: 1.1em; }
/* "Edit Selected Rack" starts disabled (nothing selected yet) — same
   disabled treatment as .canvas-dock-btn elsewhere in the app. */
.view3d-btn:disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }

.view3d-canvas-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}
#view3DCanvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none; /* required so one/two-finger gestures orbit/pinch the
                          scene instead of scrolling or zooming the page */
  cursor: grab;
}
#view3DCanvas:active { cursor: grabbing; }
.view3d-message {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
  text-align: center;
  color: var(--text-muted);
  font-size: 0.95em;
  font-weight: 600;
  z-index: 1;
  pointer-events: none;
}

.view3d-layers { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.view3d-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 36px;
  padding: 0 6px;
  font-size: 0.82em;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
}
.view3d-chip input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); }

.view3d-legend { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.view3d-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 0 10px;
  border-radius: var(--radius-pill);
  font-size: 0.78em;
  font-weight: 700;
  color: var(--text-muted);
  cursor: pointer;
  transition: opacity var(--duration-fast) var(--ease-out);
}
.view3d-legend-item:hover { background: var(--bg-elevated-2); color: var(--text); }
.view3d-legend-item.hidden-status { opacity: 0.4; }
.view3d-swatch { width: 10px; height: 10px; border-radius: 50%; flex: none; }

@media (max-width: 640px) {
  .view3d-topbar { flex-wrap: wrap; padding: var(--space-2) var(--space-3); padding-top: calc(var(--space-2) + env(safe-area-inset-top, 0px)); gap: var(--space-2); }
  .view3d-camviews { order: 3; width: 100%; justify-content: center; margin: 0; overflow-x: auto; }
  .view3d-bottombar { padding: var(--space-2) var(--space-3); padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px)); gap: var(--space-2); }
  .view3d-legend { overflow-x: auto; flex-wrap: nowrap; }
}

/* ---------------------------------------------------------------------
   Phase D6 — Project Report / Export workspace. Same full-viewport
   overlay pattern as the 3D viewer (view3d-backdrop/panel above), sized
   for a real document workspace: left section nav, centre live preview,
   right output options — never a narrow card in the middle of the page.
   On screen this is themed like the rest of the app; the preview paper
   itself always renders in a fixed light/print palette (.report-paper),
   since that's what will actually print, and needs to stay legible and
   predictable regardless of which app theme is active.
--------------------------------------------------------------------- */
.report-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(4, 3, 8, 0.72);
  z-index: 200;
  opacity: 1;
  transition: opacity var(--duration-med) var(--ease-out), display var(--duration-med) allow-discrete;
}
@starting-style { .report-backdrop { opacity: 0; } }
.report-panel {
  position: fixed;
  inset: 0;
  z-index: 201;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  opacity: 1;
  transform: scale(1);
  transition: opacity var(--duration-med) var(--ease-out), transform var(--duration-med) var(--ease-out), display var(--duration-med) allow-discrete;
}
@starting-style { .report-panel { opacity: 0; transform: scale(0.98); } }
/* Entrance only — same rationale as .view3d-panel above: closeReport()
   sets display:none synchronously and layout-phaseD6.test.js asserts on
   it immediately, so there's no matching exit fade here either. */

/* style.css's global prefers-reduced-motion block only zeroes
   animation-duration (keyframe animations) — it doesn't touch `transition`,
   so the new entrance fades above need their own explicit opt-out here. */
@media (prefers-reduced-motion: reduce) {
  .view3d-backdrop, .view3d-panel, .report-backdrop, .report-panel { transition-duration: 0.001ms; }
}
.report-topbar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  background: color-mix(in srgb, var(--bg-elevated) 88%, transparent);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  z-index: 2;
}
.report-topbar-title { margin: 0 auto; font-family: var(--font-display); font-weight: 700; font-size: 0.95em; color: var(--text); }
.report-topbar-actions { display: flex; align-items: center; gap: 8px; }

.report-body {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: 200px 1fr 280px;
}

.report-nav {
  border-right: 1px solid var(--border);
  background: var(--bg-elevated);
  overflow-y: auto;
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.report-nav-item {
  text-align: left;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  font-size: 0.84em;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
}
.report-nav-item:hover { background: var(--bg-elevated-2); color: var(--text); }

.report-preview-wrap {
  overflow-y: auto;
  padding: var(--space-7) var(--space-6);
  background: var(--bg-sunken);
}
.report-paper {
  max-width: 820px;
  margin: 0 auto;
  background: #ffffff;
  color: #1c2536;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-xl);
  padding: 56px 60px;
  font-family: var(--font-sans);
}

.report-options {
  border-left: 1px solid var(--border);
  background: var(--bg-elevated);
  overflow-y: auto;
  padding: var(--space-5);
}
.report-options-title { font-family: var(--font-display); font-size: 0.85em; font-weight: 700; color: var(--text); margin: 0 0 10px; }
.report-options-title:not(:first-child) { margin-top: var(--space-6); }
.report-preset-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--space-4); }
.report-preset-btn {
  min-height: 40px;
  padding: 0 14px;
  border-radius: var(--radius-md);
  background: var(--bg-elevated-2);
  border: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.85em;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}
.report-preset-btn:hover { color: var(--text); }
.report-preset-btn.active { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); }
.report-primary-action { width: 100%; justify-content: center; margin-top: 4px; }
.report-hint { font-size: 0.78em; color: var(--text-faint); margin: 6px 0 0; line-height: 1.4; }
.report-option-sections { display: flex; flex-direction: column; }
.report-option-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  font-size: 0.85em;
  font-weight: 600;
  color: var(--text);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.report-option-row:last-child { border-bottom: none; }
.report-option-row input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); flex: none; }

/* ---- the printable document itself — always a fixed light palette ---- */
.report-title { font-family: var(--font-display); font-size: 1.9em; font-weight: 700; margin: 0 0 6px; color: #1c2536; }
.report-meta-line { font-size: 0.9em; color: #475569; margin-bottom: 28px; padding-bottom: 16px; border-bottom: 2px solid #334155; }
.report-section { margin-bottom: 30px; }
.report-section-title { font-family: var(--font-display); font-size: 1.15em; font-weight: 700; margin: 0 0 10px; color: #1c2536; padding-bottom: 6px; border-bottom: 1px solid #e2e6ee; }
.report-subtitle { font-size: 0.9em; font-weight: 700; margin: 14px 0 6px; color: #334155; }
.report-note { font-size: 0.82em; color: #667085; margin: 0 0 10px; line-height: 1.5; }
.report-fields { display: flex; flex-direction: column; }
.report-row { display: flex; justify-content: space-between; gap: var(--space-3); padding: 6px 0; font-size: 0.88em; border-bottom: 1px solid #f0f1f5; }
.report-row-label { color: #667085; font-weight: 600; }
.report-row-value { color: #1c2536; font-weight: 700; }

.report-stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 10px 0; }
.report-stat { padding: 10px 12px; border-radius: 10px; background: #f7f9fd; border: 1px solid #e2e6ee; }
.report-stat-num { display: block; font-family: var(--font-display); font-size: 1.2em; font-weight: 700; color: #1c2536; }
.report-stat-lbl { display: block; font-size: 0.72em; color: #667085; margin-top: 2px; }

.report-chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 10px; }
.report-chip { padding: 4px 10px; border-radius: 999px; background: #f0f4ff; color: #2f5fe0; font-size: 0.78em; font-weight: 700; }

.report-table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 0.85em; }
.report-table th, .report-table td { padding: 7px 10px; border-bottom: 1px solid #e2e6ee; text-align: left; }
.report-table th { color: #667085; font-size: 0.82em; text-transform: uppercase; letter-spacing: 0.03em; }
.report-table td.report-num, .report-table th.report-num { text-align: right; font-variant-numeric: tabular-nums; }
.report-shims-note { display: flex; justify-content: space-between; padding: 8px 10px; margin-top: 4px; background: #f7f9fd; border-radius: 8px; font-size: 0.85em; font-weight: 700; color: #1c2536; }

/* ---- BOM VISUAL/UX PASS — grouped component schedule ----
   Presentation only: js/report.js's bomGroupItems() re-sorts the exact
   same bom.items list into these row bands, no item/quantity changed.
   Uses Rackulate's real brand navy (#1c2536, already the report's title
   colour above) + cyan (#0072a8, the light-theme --accent token) rather
   than a one-off colour, so the printed BOM reads as branded engineering
   output, not a generic table — matching the navy/cyan language already
   used across Warehouse/Racking/Layout/3D. */
.report-bom-group-row td {
  background: #eaf5fb;
  border-left: 3px solid #0072a8;
  border-bottom: 1px solid #d6e8f0;
  color: #0a4a68;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.74em;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 7px 10px 7px 8px;
}
.report-bom-group-count { float: right; font-weight: 700; color: #0072a8; letter-spacing: 0.02em; text-transform: none; }
.report-bom-table tbody tr:not(.report-bom-group-row):hover td { background: #fafbfe; }

/* ---- INSTALLATION VISUAL/UX PASS — printable Installation Estimate ----
   Same navy (#1c2536) / cyan (#0072a8) band language as the BOM group
   rows just above, applied to the Install Time / Labour Cost / Optional
   Extras field groups instead of table rows (this section is a plain
   .report-fields list, not a table). Presentation only — every value
   inside still comes from js/report.js's renderInstallEstimateBlock(),
   which reads est.cost/est.time/est.settings exactly as the RM Install
   page's own breakdown sheet does. */
.report-install-group { margin: 10px 0; }
.report-install-group-title {
  margin: 0 0 4px;
  padding: 6px 10px;
  background: #eaf5fb;
  border-left: 3px solid #0072a8;
  color: #0a4a68;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.74em;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  border-radius: 6px;
}
.report-install-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 10px 14px;
  margin: 12px 0;
  background: #eaf5fb;
  border: 1px solid #cfe8f4;
  border-radius: 10px;
}
.report-install-total span { font-size: 0.78em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: #0a4a68; }
.report-install-total strong { font-family: var(--font-display); font-size: 1.3em; font-weight: 700; color: #0072a8; }

.report-badge { display: inline-flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: 10px; font-size: 0.85em; font-weight: 700; margin: 4px 0; }
.report-badge-ok { background: #eefbf3; color: #14532d; border: 1px solid #b7ecc9; }
.report-badge-warn { background: #fdeceb; color: #9c2b22; border: 1px solid #f3b6b0; }
.report-badge-caution { background: #fef3e2; color: #9a5b00; border: 1px solid #f4d19b; }
.report-list { margin: 6px 0 10px; padding-left: 20px; font-size: 0.85em; color: #334155; }
.report-list li { margin-bottom: 4px; }

.report-drawing { margin: 12px 0; border: 1px solid #e2e6ee; border-radius: 10px; overflow: hidden; background: #fbfbfd; }
.report-drawing-svg { display: block; width: 100%; height: auto; }
.report-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 0.78em; color: #475569; }
.report-legend-item { display: inline-flex; align-items: center; gap: 6px; }
.report-swatch { width: 12px; height: 12px; border-radius: 3px; border: 1px solid #94a3b8; display: inline-block; }
.report-swatch-dash { background: #fff; border: 1.5px dashed #dc2626; }

.report-3d-image img { max-width: 100%; border-radius: 10px; border: 1px solid #e2e6ee; }
.report-notes-text { font-size: 0.88em; line-height: 1.6; color: #334155; white-space: pre-wrap; }
.report-footer { margin-top: 40px; padding-top: 12px; border-top: 1px solid #e2e6ee; font-size: 0.72em; color: #94a3b8; text-align: center; }

/* ---- Handover Pack cover sheet — only rendered/shown when
   generateHandoverPack() has built it; a plain Print/PDF never includes
   it (mirrors production's body.handover-pack-print gating). ---- */
.report-cover { padding-bottom: 32px; margin-bottom: 32px; border-bottom: 2px dashed #cbd5e1; }
.report-cover-eyebrow { font-family: var(--font-display); font-size: 0.85em; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; color: #2f5fe0; margin-bottom: 14px; }
.report-cover-title { margin: 0 0 18px; font-size: 2em; font-family: var(--font-display); color: #1c2536; }
.report-cover-line { font-size: 0.95em; color: #444; margin-bottom: 4px; }
.report-cover-contents, .report-cover-notes { margin-top: 28px; padding-top: 16px; border-top: 1px solid #ccc; }
.report-cover-label { font-weight: 700; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.06em; color: #667085; margin-bottom: 8px; }
.report-cover-contents ul { margin: 0; padding-left: 20px; }
.report-cover-contents li { margin-bottom: 4px; font-size: 0.9em; }
.report-cover-notes p { white-space: pre-wrap; line-height: 1.5; font-size: 0.9em; }
.report-cover-3d { margin-top: 28px; padding-top: 16px; border-top: 1px solid #ccc; }
.report-cover-3d img { max-width: 100%; margin-top: 8px; border-radius: 8px; border: 1px solid #ccc; }
.report-cover-disclaimer { margin-top: 28px; padding-top: 14px; border-top: 1px solid #ccc; font-size: 0.78em; color: #667085; line-height: 1.5; }
.report-cover-footer { margin-top: 40px; font-size: 0.72em; color: #94a3b8; }

@media (max-width: 900px) {
  /* .report-body is a CSS grid with no explicit rows, sized by its flex
     parent (.report-panel, position:fixed;inset:0 — full-screen) to
     exactly the space left under .report-topbar. On desktop that's fine:
     3 columns share that one bounded row, each scrolling internally. Once
     collapsed to 1 column here, preview + options stack into separate
     rows instead — rows whose combined height is almost always taller
     than one phone screen. Without overflow-y:auto here, that excess
     content had nowhere to go: .report-body's own box doesn't grow past
     its flex allocation, and .report-panel is a fixed, non-scrolling
     element, so anything past the first screenful was simply unreachable
     — the options panel appeared to "cover" the bottom of the preview
     because there was no way to scroll past it. */
  .report-body { grid-template-columns: 1fr; overflow-y: auto; }
  .report-nav { display: none; } /* section jump-links are a nice-to-have on a wide screen; on narrow screens the preview scrolls linearly instead */
  .report-options { border-left: none; border-top: 1px solid var(--border); }
  .report-preview-wrap { padding: var(--space-5) var(--space-4); overflow-y: visible; overflow-x: hidden; }
  .report-paper { padding: 32px 24px; }
  /* Report Stats grid was a hard-coded 4 equal columns with no mobile
     override — on a 390px phone, minus .report-paper's own padding
     above, each column had roughly 70px to work with, well under what a
     figure like "21,600 mm" needs unwrapped. Grid columns don't shrink
     below their content's natural minimum width by default, so the grid
     as a whole overflowed its container instead, forcing the horizontal
     scrollbar/clipped values reported. Two columns gives every stat
     comfortable room on a phone; desktop/tablet keep the original 4.
     Table cells get a word-break safety net for the same reason (a long
     BOM description or dimension string that can't wrap could do the
     same thing) — harmless on a wider screen, where nothing needs to
     break. Print/PDF output is untouched: both rules live inside this
     same screen-only breakpoint, same as the reduced .report-paper
     padding just above. */
  .report-stat-grid { grid-template-columns: repeat(2, 1fr); }
  .report-table td, .report-table th { overflow-wrap: break-word; word-break: break-word; }
  /* .report-topbar never wrapped and .report-topbar-title has margin:0
     auto to centre it, so on a narrow phone the actions group (Missing
     requirements pill + Print + PDF) was pushed past the right edge —
     not deleted, just off-screen and unreachable, which is what read as
     "missing" Print/PDF buttons. Wrapping puts the actions on their own
     full-width row under the title/close button instead of clipping
     them. */
  .report-topbar { flex-wrap: wrap; row-gap: var(--space-2); }
  .report-topbar-actions { width: 100%; flex-wrap: wrap; justify-content: flex-start; }
}

/* ---------------------------------------------------------------------
   Print — only the report paper's own content should ever reach paper.
   Applied only while the report workspace is open (body.report-print-mode,
   set by js/report.js's open()/close()), so nothing else in the app needs
   its own print rules. Every application chrome element (dock, sheets,
   inspector, 3D viewer, nav, the report workspace's own topbar/nav/
   options) is either a sibling of .report-panel (hidden by the first
   rule below) or carries .no-print inside the panel (hidden by the
   second) — only .report-paper's rendered sections ever print.
--------------------------------------------------------------------- */
@media print {
  body.report-print-mode > *:not(.report-panel) { display: none !important; }
  body.report-print-mode .no-print { display: none !important; }
  body.report-print-mode .report-panel { position: static; background: #fff; }
  body.report-print-mode .report-preview-wrap { position: static; overflow: visible; padding: 0; background: #fff; }
  body.report-print-mode .report-paper { max-width: 100%; margin: 0; box-shadow: none; border-radius: 0; padding: 0; }
  body.report-print-mode .report-body { display: block; }

  .report-section, .report-table tr, .report-stat, .report-drawing { break-inside: avoid; page-break-inside: avoid; }
  .report-avoid-break-before { break-before: auto; }
  .report-cover { break-after: page; page-break-after: always; border-bottom: none; }
}


/* ---------------------------------------------------------------------
   BILL OF MATERIALS — document layout (js/report.js renderBomSection()).
   Part of the printable paper, so it uses the paper's fixed light palette
   in both themes. Restrained: one accent rule, hairline table dividers,
   tabular numerals, no cards or gradients.
--------------------------------------------------------------------- */
.bomdoc { color: #1c2536; font-size: 0.9em; }
.bomdoc-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; padding-bottom: 14px; border-bottom: 2px solid #1c2536; }
.bomdoc-brand { display: flex; align-items: center; gap: 14px; min-width: 0; }
.bomdoc-logo { max-height: 52px; max-width: 160px; object-fit: contain; }
.bomdoc-company { font-size: 0.82em; line-height: 1.45; color: #475569; }
.bomdoc-wordmark { font-family: var(--font-display); font-weight: 800; font-size: 1.35em; letter-spacing: 0.01em; color: #1c2536; }
.bomdoc-titleblock { text-align: right; flex: none; }
.bomdoc-doctype { font-size: 0.7em; letter-spacing: 0.12em; text-transform: uppercase; color: #0072a8; font-weight: 700; }
.bomdoc-title { margin: 2px 0 0; font-family: var(--font-display); font-size: 1.7em; font-weight: 700; color: #1c2536; }
.bomdoc-meta { display: grid; grid-template-columns: repeat(3, 1fr); margin: 0; border-bottom: 1px solid #cbd2dc; }
.bomdoc-meta > div { padding: 8px 10px 8px 0; border-bottom: 1px solid #e2e6ee; min-width: 0; }
.bomdoc-meta dt, .bomdoc-info dt { font-size: 0.68em; letter-spacing: 0.06em; text-transform: uppercase; color: #667085; font-weight: 700; }
.bomdoc-meta dd, .bomdoc-info dd { margin: 2px 0 0; font-weight: 600; overflow-wrap: anywhere; }
.bomdoc-heading { margin: 22px 0 8px; font-family: var(--font-display); font-size: 0.95em; font-weight: 700; color: #1c2536; letter-spacing: 0.02em; }
.bomdoc-info { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0 24px; }
.bomdoc-info > div { padding: 6px 0; border-bottom: 1px solid #e2e6ee; }
.bomdoc-note { margin: 0 0 8px; font-size: 0.8em; color: #475569; line-height: 1.45; }
.bomdoc-table { width: 100%; border-collapse: collapse; font-size: 0.86em; border-top: 1.5px solid #1c2536; }
.bomdoc-table th { text-align: left; padding: 7px 8px; font-size: 0.74em; letter-spacing: 0.05em; text-transform: uppercase; color: #475569; border-bottom: 1.5px solid #1c2536; background: #f4f6f9; }
.bomdoc-table td { padding: 6px 8px; border-bottom: 1px solid #e2e6ee; vertical-align: top; }
.bomdoc-table .bomdoc-no { width: 2.6em; text-align: right; color: #667085; font-variant-numeric: tabular-nums; }
.bomdoc-table .bomdoc-qty { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.bomdoc-table .bomdoc-uom { width: 3.8em; color: #667085; }
.bomdoc-table th.bomdoc-qty { white-space: normal; }
.bomdoc-sub { display: block; font-size: 0.82em; color: #667085; font-weight: 400; }
.bomdoc-group td { background: #eaf2f7; color: #0a4a68; font-weight: 800; font-size: 0.74em; letter-spacing: 0.06em; text-transform: uppercase; border-bottom: 1px solid #cfe0ea; border-left: 3px solid #0072a8; padding: 5px 8px; }
.bomdoc-table tfoot td { border-top: 1.5px solid #1c2536; border-bottom: none; font-weight: 700; padding-top: 8px; }
.bomdoc-foot { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-top: 22px; padding-top: 8px; border-top: 1px solid #cbd2dc; font-size: 0.72em; color: #667085; }
@media (max-width: 700px) {
  .bomdoc-head { flex-direction: column; align-items: flex-start; }
  .bomdoc-titleblock { text-align: left; }
  .bomdoc-meta, .bomdoc-info { grid-template-columns: 1fr; }
  .bomdoc-table { display: block; overflow-x: auto; }
}
@media print {
  .bomdoc-table { display: table; overflow: visible; }
  .bomdoc-table thead { display: table-header-group; }   /* column headings repeat on every page */
  .bomdoc-table tfoot { display: table-footer-group; }
  .bomdoc-table tr { break-inside: avoid; page-break-inside: avoid; }
  .bomdoc-group { break-after: avoid; page-break-after: avoid; }
  .bomdoc-heading { break-after: avoid; page-break-after: avoid; }
  /* The BOM may span pages, so its section must not be forced onto one. */
  @page :first { @top-left { content: none; } }   /* full header is on page 1; later pages get the compact running header */
  body.report-print-mode .report-section-title, body.report-print-mode .report-section h2, body.report-print-mode .report-section h3 { break-after: avoid; page-break-after: avoid; }
  body.report-print-mode .report-section[data-report-section="bom"] { break-inside: auto; page-break-inside: auto; }
  /* Running footer on every printed page: document id + page number where the
     browser supports page margin boxes (Chromium); the in-flow footer above
     still closes the document everywhere else. */
  @page { margin: 14mm 12mm 16mm; @top-left { content: "Rackulate \2014  Project Report"; font-size: 8pt; color: #667085; } @bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 8pt; color: #667085; } }
}

/* ---------------------------------------------------------------------
   App Settings / Help panels (Aug 2026) — js/app-settings.js,
   js/app-help.js. Docked to the right on wide screens (a floating panel,
   not a full workspace takeover — the canvas stays visible behind the
   dimmed backdrop) and slides up from the bottom as a true bottom sheet
   on narrow/touch screens, same open/close mechanics (display toggle,
   backdrop click / close button / Escape) as .report-panel above. Shared
   by every page with the .app-sidebar shell — only the panel markup
   itself is duplicated per page, same convention as .app-sidebar.
--------------------------------------------------------------------- */
.app-settings-backdrop, .app-help-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(4, 3, 8, 0.55);
  z-index: 210;
  opacity: 1;
  transition: opacity var(--duration-med) var(--ease-out), display var(--duration-med) allow-discrete;
}
@starting-style { .app-settings-backdrop, .app-help-backdrop { opacity: 0; } }

.app-settings-panel, .app-help-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 211;
  width: min(420px, 100vw);
  display: flex;
  flex-direction: column;
  background: var(--bg-elevated);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-xl);
  opacity: 1;
  transform: translateX(0);
  transition: opacity var(--duration-med) var(--ease-out), transform var(--duration-med) var(--ease-out), display var(--duration-med) allow-discrete;
}
@starting-style { .app-settings-panel, .app-help-panel { opacity: 0; transform: translateX(24px); } }
.app-help-panel { width: min(760px, 100vw); }

@media (prefers-reduced-motion: reduce) {
  .app-settings-backdrop, .app-settings-panel, .app-help-backdrop, .app-help-panel { transition-duration: 0.001ms; }
}

/* Bottom-sheet layout on touch/narrow screens — same content, docked to
   the bottom edge with rounded top corners instead of the side, so it
   reads as a native sheet rather than a cramped narrow drawer. */
@media (max-width: 640px) {
  .app-settings-panel, .app-help-panel {
    top: auto;
    left: 0;
    right: 0;
    width: auto;
    max-height: 86vh;
    border-left: none;
    border-top: 1px solid var(--border);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    transform: translateY(24px);
  }
  @starting-style { .app-settings-panel, .app-help-panel { transform: translateY(100%); } }
}

.app-settings-body { display: flex; flex-direction: column; gap: var(--space-5); }
.app-settings-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
.app-settings-row-label { font-size: 0.88em; font-weight: 700; color: var(--text); }
.app-settings-row-hint { font-size: 0.78em; color: var(--text-faint); margin-top: 2px; max-width: 26ch; }
.app-settings-number {
  width: 90px;
  height: 40px;
  padding: 0 12px;
  border-radius: var(--radius-md);
  background: var(--bg-sunken);
  border: 1.5px solid var(--border-strong);
  color: var(--text);
  font-size: 0.88em;
  font-weight: 700;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.app-settings-number:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.app-settings-reset-row { padding-top: var(--space-4); border-top: 1px solid var(--border); }
.app-settings-reset-row .btn { width: 100%; justify-content: center; }

.app-help-content { flex: 1; min-height: 0; display: flex; }
.app-help-nav {
  flex: none;
  width: 190px;
  border-right: 1px solid var(--border);
  background: var(--bg-elevated-2);
  overflow-y: auto;
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.app-help-nav-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  background: none;
  border: none;
  font: inherit;
  font-size: 0.83em;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
}
.app-help-nav-item:hover { background: var(--bg-elevated); color: var(--text); }
.app-help-nav-item.is-active { background: var(--accent); color: #fff; font-weight: 700; }
.app-help-body { flex: 1; min-width: 0; overflow-y: auto; padding: var(--space-5) var(--space-6); }
.app-help-body-title { margin: 0 0 var(--space-4); font-family: var(--font-display); font-size: 1.1em; font-weight: 700; color: var(--text); }
.app-help-body p { font-size: 0.88em; line-height: 1.6; color: var(--text-muted); margin: 0 0 var(--space-4); }
.app-help-body p:last-child { margin-bottom: 0; }
.help-shortcut-list { margin: 0; }
.help-shortcut-list dt { font-size: 0.85em; font-weight: 700; color: var(--text); margin-top: var(--space-4); }
.help-shortcut-list dt:first-child { margin-top: 0; }
.help-shortcut-list dd { margin: 3px 0 0; font-size: 0.83em; line-height: 1.5; color: var(--text-muted); }

@media (max-width: 640px) {
  .app-help-content { flex-direction: column; }
  .app-help-nav { width: auto; flex-direction: row; overflow-x: auto; border-right: none; border-bottom: 1px solid var(--border); }
  .app-help-nav-item { white-space: nowrap; flex: none; }
}

/* ---------------------------------------------------------------------
   Home / index.html — showcase-quality rebuild. Home is NOT another
   RackMapper workspace: Warehouse/Racking/Layout/3D/Install are fixed-
   height CAD-tool screens (body.canvas-body, overflow:hidden); Home is
   a normal, scrolling, browser-based welcome/product/project hub
   (body.home-body below), free to use large hero typography, section
   reveals and restrained motion the way a premium SaaS marketing page
   would. Composition, spacing rhythm, nav style and section flow are
   modelled directly on the approved marketing/rackmapper-showcase.html
   concept. .wrap / .nav* / .brand* / .btn* / .eyebrow / .theme-toggle /
   .hero-glow (+ its keyframe) / .site-footer / .reveal / mock-* SVG
   label classes are reused unchanged from style.css's existing
   (previously unwired) homepage design tokens — confirmed unused by
   any other real page before being adopted here. Rackulate still
   has no multi-project backend (see js/project-storage.js — one
   autosave draft + explicit file export/import, plus a small, purely-
   additive "recent projects" event log recorded only at real Save/Open
   moments — that log is still recorded even though Home's own "Your
   Projects" section was removed in a later demo-polish pass; nothing
   else currently reads it, but removing the recording itself would be
   touching the storage layer for no reason).
--------------------------------------------------------------------- */
body.home-body { min-height: 100vh; min-height: 100dvh; overflow-x: hidden; }

/* Full-screen desktop pass — Home only (scoped to body.home-body, which
   no other page uses). style.css's shared .wrap/--content-width stay
   untouched for every other page; this widens the container just for
   Home so its sections use more of a large desktop viewport, without
   stretching any individual card/button/graphic — everything inside
   still sizes and wraps exactly as before, just within a roomier column.
   Reverts to the shared 1200px at the same narrower breakpoint style.css
   already uses, so tablet/mobile are unaffected. */
@media (min-width: 1181px) {
  body.home-body .wrap { max-width: 1440px; }
  body.home-body .home-section { padding: var(--space-10) 0; }
  body.home-body .home-cta { padding: var(--space-10) 0; }
  body.home-body .home-capstat-section { padding-bottom: var(--space-9); }
}

/* Nav simplification (RECOMPOSE PASS) — "make it feel engineered, not
   like a template". Scoped to body.home-body's own nav only; .btn/.nav/
   .theme-toggle are shared with every workspace page (Warehouse, Racking,
   Layout, Install...) which must keep their own pill styling untouched —
   this only tightens the corner radius on Home's own top-bar controls
   from a full pill down to a small technical rectangle, and matches the
   theme toggle to the same shape instead of a circle. No buttons removed
   or rewired — same ids, same click handlers, same New/Open/Continue/Hub
   entry points, text/icons unchanged. */
body.home-body .nav .btn { border-radius: var(--radius-sm); }
body.home-body .nav .theme-toggle { border-radius: var(--radius-sm); }

/* ---- Nav simplification (Homepage + Hub composition pass) --------------
   Four full pill/ghost buttons plus a theme toggle sat shoulder-to-
   shoulder in .nav-actions — busy, and every one of them read with equal
   visual weight regardless of importance. Only the ids/handlers this
   markup already wires (navHubLink / navContinueProject / navNewProject /
   navOpenProject) matter functionally; all of that is untouched. This
   just demotes the two secondary links (Hub, Open Project) to plain text
   so the one primary action — New Project, or Continue Working once a
   draft exists — is the only real "button" left in the bar. Scoped to
   body.home-body's own nav only, same convention as the border-radius
   rule above. */
/* Hero refinement pass: gap tightened slightly further (space-3 -> 10px)
   and the ghost links' own horizontal padding trimmed a touch — spacing
   refinement only, same demoted-link architecture from the composition
   pass above (Hub/Open Project stay plain text, New Project/Continue
   Working stay the one real button). */
body.home-body .nav-actions { gap: 10px; }
body.home-body .nav-actions .btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
  padding: 8px 9px;
  font-weight: 600;
}
body.home-body .nav-actions .btn-ghost:hover {
  color: var(--text);
  background: var(--bg-elevated-2);
  border-color: transparent;
  transform: none;
  box-shadow: none;
}
body.home-body .nav-actions .btn-ghost .icon { opacity: 0.7; }
@media (max-width: 900px) {
  /* #navHubLink's label already hides at this width via style.css's own
     mobile nav rules elsewhere — this just tightens the remaining gap. */
  body.home-body .nav-actions { gap: var(--space-2); }
}

/* ---- Dashboard hero (RECOMPOSE PASS — rejected-pass fix) ----------------
   The previous version of this hero was rejected: purple/violet ambient
   glow, a broken overlapping rotator, an example-stats block in the
   hero, a boxed/rounded/drop-shadowed video that read as a stock media
   embed, and a generic "text left / video-in-a-card right" SaaS layout.
   This block rebuilds it against the approved concept image: a static
   two-line headline (no rotation), short practical copy, two CTAs, and
   one large, de-boxed, edge-fading cinematic visual occupying ~58-62% of
   the composition (copy column ~38-42%) — see .home-dash-hero-grid's
   0.72fr/1fr split below, which already lands in that range and didn't
   need to change. Padding trimmed from the old "+18%/space-11" oversized
   values down to the same compact figures the rest of this page's
   sections use, since an oversized hero was part of the "huge empty
   areas" complaint. --------------------------------------------------- */
.home-dash-hero { position: relative; padding: var(--space-7) 0 var(--space-6); }
/* Hero refinement pass: copy column widened slightly (0.62fr -> 0.68fr)
   so "From empty space" has room to sit on one deliberate line at normal
   desktop widths without forcing the video visual to give up its
   dominance — video is still the larger share (~59.5% vs ~40.5%), still
   the full-bleed visual per the prior pass, just not squeezed to the
   point of breaking the headline. Bottom padding trimmed (space-8 ->
   space-6) as part of tightening the dead space before #product below. */
.home-dash-hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0, 0.68fr) minmax(0, 1fr); gap: var(--space-7); align-items: stretch; }

/* ---- Composition pass (Homepage + Hub pass) ----------------------------
   The recompose pass above got the hero to the right palette/headline/
   proportions, but the visual still sat fully inside .wrap like a framed
   media block — same distance from the right edge as the copy column's
   left edge. This pushes it further: the visual now breaks out of .wrap
   on its right side and runs flush to the actual viewport edge, using the
   standard "full-bleed child of a centred container" calc() (the visual's
   own right edge normally sits at 50% of the viewport since .wrap is
   centred; this pulls it out to 100vw, accounting for .wrap's own
   max-width/gutter so it lines up exactly instead of guessing a fixed
   value). body.home-body already sets overflow-x:hidden (see the
   full-screen-desktop-pass rule below), so this negative margin can never
   open a horizontal scrollbar. Left column, copy width ratio and the
   video/scrim/loop-cycle JS are untouched — this is a pure composition
   change. ------------------------------------------------------------ */
@media (min-width: 981px) {
  .hero-hp-visual {
    margin-right: calc(-1 * max(0px, (100vw - var(--content-width)) / 2) - var(--space-5));
  }
}

/* ---------------------------------------------------------------------
   Cinematic hero visual (RECOMPOSE PASS) — the real Higgsfield clip,
   substantially larger and no longer boxed: no border-radius, no
   box-shadow, stretches to the full height of the hero row (align-self
   inherited from the grid's own align-items:stretch above) instead of
   being capped to a fixed aspect-ratio box. .hero-hp-visual-fallback
   (inline SVG, always rendered) still sits behind .hero-hp-visual-video
   (the real <video>) exactly as before — nothing about that pairing, the
   loop-cycle scrim, or js/hero-visual.js's playback logic changed here,
   only this container's own shape. .hero-hp-visual-fade now fades BOTH
   the left edge (into the copy column) and the bottom edge (into
   whatever sits below the hero), so the visual reads as bleeding into
   the page rather than a rectangle dropped on top of it. --------------- */
.hero-hp-visual {
  position: relative;
  min-height: 540px;
  /* Composition pass: no more uniform rounded-rectangle "media card" —
     only the top-left inward corner (where it meets the copy column)
     keeps a small radius; the other three corners now run flush to the
     viewport's right edge and the hero row's own top/bottom, which is
     what actually reads as "bleeding into the page" rather than a
     container with visible edges on every side. */
  border-radius: var(--radius-sm) 0 0 0;
  overflow: hidden;
  isolation: isolate;
}
.hero-hp-visual-fallback, .hero-hp-visual-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.hero-hp-visual-video { z-index: 1; background: transparent; }
/* Loop-cycle scrim (real video wired in) — js/hero-visual.js toggles
   .is-active on this at the end of each ~8s play-through: the video
   holds its final frame briefly, this fades to opaque over it (a soft
   "fade to dark" instead of a hard native loop cut), the video is
   reset + restarted underneath while still hidden by the scrim, then
   .is-active is removed so the restarted video fades back in. */
.hero-hp-visual-video-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: #14171d;
  opacity: 0;
  transition: opacity 0.7s var(--ease-out);
}
.hero-hp-visual-video-scrim.is-active { opacity: 1; }
.hero-hp-visual-fade {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background:
    linear-gradient(90deg, var(--bg) 0%, color-mix(in srgb, var(--bg) 88%, transparent) 14%, color-mix(in srgb, var(--bg) 62%, transparent) 30%, color-mix(in srgb, var(--bg) 28%, transparent) 46%, transparent 62%),
    linear-gradient(to top, var(--bg) 0%, transparent 22%, transparent 100%);
}
/* (The decorative technical overlay — construction line, dimension callouts and
   "Designed in Rackulate" tag — was removed: the hero visual is now the image/clip only.) */
@media (max-width: 980px) {
  /* Stacked mobile layout — the visual is no longer breaking out to the
     viewport edge (the @media (min-width:981px) rule above doesn't
     apply), so it goes back to a normal, fully-rounded contained block. */
  .hero-hp-visual { min-height: 340px; border-radius: var(--radius-sm); }
}

/* ---- Connected 9-stage workflow chain (RECOMPOSE PASS) — plain text +
   arrow separators under the hero grid, deliberately not nine cards; see
   index.html's comment on this block. ---- */
/* Hero refinement pass: the workflow strip was reading as tiny footer
   metadata — bumped up to feel like the important product journey it
   is: larger type (0.76em -> 0.98em), a stronger/brighter top rule
   (opacity 0.08 -> 0.16, an accent-tinted line instead of plain white)
   and bigger connection nodes (see .home-hero-chain-dot below), while
   margin-top comes down slightly (space-7 -> space-6) as part of
   tightening the dead space before the next section. Still plain text +
   arrows, non-interactive — no cards, no new section. */
.home-hero-chain { margin-top: var(--space-6); padding-top: var(--space-5); border-top: 1px solid rgba(120, 200, 255, 0.16); overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); }
/* Right-to-left ticker: two identical lists in one track, track slides by
   exactly -50% (one list) and loops; the arrow separator is on EVERY li
   (including each list's first) so the seam between copies is identical
   to the gap between any two stages. */
.home-hero-chain-track { display: flex; width: max-content; animation: home-chain-scroll 42s linear infinite; will-change: transform; }
@keyframes home-chain-scroll { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-50%, 0, 0); } }
.home-hero-chain-list {
  list-style: none;
  display: flex;
  flex: none;
  flex-wrap: nowrap;
  align-items: center;
  margin: 0;
  padding: 0;
  font-family: var(--font-mono);
  font-size: 0.98em;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #b8b4c8;
}
.home-hero-chain-list li { white-space: nowrap; display: inline-flex; align-items: center; gap: 9px; }
.home-hero-chain-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-soft);
  flex: none;
}
.home-hero-chain-list li::before { content: '\2192'; display: inline-block; margin: 0 15px 0 6px; color: var(--accent); opacity: 0.85; font-weight: 400; }
@media (max-width: 900px) {
  .home-hero-chain-list { font-size: 0.82em; }
  .home-hero-chain-list li::before { margin: 0 10px 0 5px; }
}
@media (prefers-reduced-motion: reduce) {
  .home-hero-chain { -webkit-mask-image: none; mask-image: none; }
  .home-hero-chain-track { animation: none; width: auto; }
  .home-hero-chain-list { flex-wrap: wrap; row-gap: var(--space-2); }
  .home-hero-chain-list:nth-child(2) { display: none; }
  .home-hero-chain-list li:first-child::before { display: none; }
}
@media (max-width: 1180px) {
  .home-dash-hero-grid { gap: var(--space-6); }
}
@media (max-width: 980px) {
  .home-dash-hero-grid { grid-template-columns: 1fr; gap: var(--space-6); align-items: stretch; }
}
@media (max-width: 900px) {
  .home-dash-hero { padding: var(--space-6) 0 var(--space-7); }
}

/* ---- Hero copy — static two-line headline (no rotator: "do not rotate
   the core headline"), short sub-copy, left-aligned actions row. These
   three classes are used only by .home-dash-hero-copy now — the old
   standalone centred .home-hero/.home-hero-inner section they were
   originally written for is dead markup (not present anywhere in
   index.html), so left-alignment is set directly on the base rules
   rather than via a second scoped override. Colours stay hardcoded, not
   var(--text) — this sits directly on #homeBgScene, a fixed dark wash
   that never flips with the theme toggle (see js/hero-background.js's
   header comment); var(--text) flips to near-black in Light Mode, which
   would be unreadable here. ---- */
/* Hero refinement pass: clamp's upper bound brought down from 3.8em to
   3.15em, and each line is locked to white-space:nowrap — at the old
   size, "From empty space" could exceed the copy column's width at
   ordinary desktop viewports and wrap mid-phrase ("empty" / "space" on
   separate lines), breaking the required two-line "From empty space /
   to handover." composition. Combined with the wider copy column above,
   this keeps both lines intact as single lines from small desktop widths
   up through ultra-wide, while still reading as a large hero headline
   (3.15em is still bigger than any other type on the page). */
.home-hero-title { display: flex; flex-direction: column; font-size: clamp(2.1em, 4.2vw, 3.15em); font-weight: 700; line-height: 1.1; letter-spacing: -0.02em; margin: 0 0 var(--space-5); color: #f5f4f7; }
.home-hero-title-line { display: block; white-space: nowrap; }
.home-hero-sub { font-size: 1.12em; color: #9c98ab; max-width: 46ch; margin: 0 0 var(--space-7); line-height: 1.6; }
.home-hero-actions { position: relative; z-index: 2; pointer-events: auto; display: flex; align-items: center; justify-content: flex-start; gap: var(--space-3); flex-wrap: wrap; }

/* =======================================================================
   Home background scene — a fixed, scroll-driven colour wash behind the
   entire page (markup: #homeBgScene, an early child of <body>, see
   index.html). The hero itself is plain, unpinned, in-flow content (see
   .home-hero-* rules above); this one scene — not the hero — is what
   visually responds to scroll, sitting behind everything from the top of
   the page down through the Product Showcase section.

   Revision pass: this replaced an earlier Three.js warehouse-camera
   background (a synthetic 3D racking scene rendered to a <canvas>) after
   review found the racking scene itself wasn't reading well visually —
   see js/hero-background.js's header for the fuller history. No canvas,
   no WebGL, no vendored three.js dependency now: js/hero-background.js
   writes three small custom properties (--home-bg-h/-s/-l, plain numbers
   — hue in degrees, saturation/lightness in percent, unitless so calc()
   can freely derive lighter/darker/desaturated variants below) onto
   #homeBgScene as the user scrolls, and every gradient here reads them —
   the same "JS computes one small derived value, CSS renders it" division
   of labour this feature's scroll system has used from the very start
   (originally --hero-progress, then --wh-progress, now these three).

   Explicit two-layer stacking model, chosen deliberately over a negative
   z-index (which risks silently getting buried under an ancestor's own
   stacking context): the background sits at z-index:0, .home-page-fg at
   z-index:1. pointer-events:none keeps this decorative layer from ever
   intercepting a click/tap meant for the real page. Once the scroll
   reaches its final value the colour settles and holds there (no auto-
   fade-out, driven entirely in JS — see hero-background.js's driver)
   until the page's own opaque sections (Product Showcase's .is-elevated
   background) naturally cover it in normal scroll.
   ------------------------------------------------------------------- */
.home-bg-scene {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* Safe no-JS/pre-JS default — exactly progress:0's deep navy (RECOMPOSE
     PASS: was a purple/violet hue, rejected — "return to deep navy/
     near-black with restrained cyan technical accents"; same mechanism,
     values only), so a script failure just freezes the opening brand
     moment rather than showing a broken/blank background. */
  --home-bg-h: 215;
  --home-bg-s: 12;
  --home-bg-l: 11;
  /* Base wash: two soft off-centre highlights (top-left "key light",
     bottom-right fainter fill, both a touch lighter than the base tone)
     plus a wide centre-to-edge vignette and a subtle top-to-bottom
     gradient — restrained radial lighting and a controlled vignette,
     entirely colour-driven off the three custom properties above, no
     shapes/objects anywhere in it. */
  background:
    radial-gradient(1100px 700px at 26% 8%, hsl(calc(var(--home-bg-h) * 1deg) calc(var(--home-bg-s) * 1%) calc((var(--home-bg-l) + 16) * 1%) / 0.55), transparent 62%),
    radial-gradient(900px 620px at 88% 84%, hsl(calc((var(--home-bg-h) - 22) * 1deg) calc((var(--home-bg-s) * 0.7) * 1%) calc((var(--home-bg-l) + 7) * 1%) / 0.3), transparent 65%),
    radial-gradient(1500px 1000px at 50% 45%, transparent 42%, hsl(calc(var(--home-bg-h) * 1deg) calc(var(--home-bg-s) * 1%) calc(max(var(--home-bg-l) - 7, 0) * 1%) / 0.65) 100%),
    linear-gradient(180deg, hsl(calc(var(--home-bg-h) * 1deg) calc(var(--home-bg-s) * 1%) calc(var(--home-bg-l) * 1%)) 0%, hsl(calc(var(--home-bg-h) * 1deg) calc(var(--home-bg-s) * 1%) calc(max(var(--home-bg-l) - 3, 0) * 1%)) 100%);
}

/* Gentle movement — the app's own established ambient-glow technique
   (.hero-glow/.canvas-stage-glow's slow drift, see style.css's glowDrift
   keyframe and --glow-duration token), reused rather than reinvented,
   scaled up to sit behind the whole page. Restrained: a slow, subtle
   translate/scale drift, never a distinct moving shape. Coloured off the
   same custom properties as the base wash so it always stays tonally
   consistent as the page scrolls. prefers-reduced-motion is already
   handled globally (style.css's `* { animation-duration: 0.001ms
   !important }` rule), so no separate override is needed here. */
.home-bg-scene-glow {
  position: absolute;
  inset: -12% -10% -10% -10%;
  background: radial-gradient(900px 640px at 30% 18%, hsl(calc(var(--home-bg-h) * 1deg) calc(var(--home-bg-s) * 1%) calc((var(--home-bg-l) + 20) * 1%) / 0.4), transparent 68%);
  animation: glowDrift var(--glow-duration) ease-in-out infinite alternate;
}

/* Readability scrim — a further dark wash + edge vignette over the whole
   scene so foreground text/cards always have enough contrast, strongest
   at the very top (where the hero headline sits) and at the edges,
   lightest through the centre. */
.home-bg-scene-scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(6,5,10,0.4) 0%, rgba(6,5,10,0.12) 24%, rgba(6,5,10,0.08) 55%, rgba(6,5,10,0.35) 100%),
    radial-gradient(ellipse at center, transparent 48%, rgba(6,5,10,0.42) 100%);
}

/* Foreground wrapper — plain positioning context, explicit z-index:1 so
   it always paints above the fixed background per the approved stacking
   model (never relies on source order alone). */
.home-page-fg { position: relative; z-index: 1; }

/* Pause-detection sentinel — zero-size, purely a scroll-position marker
   for js/hero-background.js's IntersectionObserver (see index.html). */
.home-bg-scene-sentinel { height: 0; margin: 0; padding: 0; }

/* ---- Capability strip — 4 short, generic product descriptors, never
   live user stats or fabricated numbers. ---- */
.home-capstat-section { padding-bottom: var(--space-8); }
.home-capstat-bar { display: grid; grid-template-columns: repeat(4, 1fr); border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--bg-elevated); overflow: hidden; }
.home-capstat-cell { padding: var(--space-5) var(--space-4); text-align: center; border-left: 1px solid var(--border); display: flex; flex-direction: column; gap: 6px; }
.home-capstat-cell:first-child { border-left: none; }
.home-capstat-value { font-family: var(--font-display); font-size: 1.5em; font-weight: 700; color: var(--accent); }
.home-capstat-label { font-size: 0.78em; font-weight: 600; color: var(--text-muted); line-height: 1.35; }

/* ---- Shared full-width section rhythm — alternating plain/elevated
   bands (border-topped, tinted background) the way the showcase's own
   stats-bar / workflow / RM Install sections alternate, so the page
   reads as one flowing document rather than a stack of identical grey
   boxes. ---- */
.home-section { padding: var(--space-9) 0; }
.home-section.is-elevated { background: var(--bg-elevated); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.home-section-head { max-width: 700px; margin: 0 0 var(--space-7); }
.home-section-title-lg { font-family: var(--font-display); font-size: clamp(1.7em, 3vw, 2.3em); line-height: 1.15; margin: 0 0 var(--space-3); font-weight: 700; }
.home-section-sub { font-size: 1.02em; color: var(--text-muted); line-height: 1.6; margin: 0; }
/* #workflow is the one .home-section that isn't .is-elevated (no opaque
   card background of its own) and sits directly over the fixed dark
   #homeBgScene wash, same reasoning as .home-hero-title/.home-hero-sub
   above — hardcoded to dark mode's own existing values so only Light
   Mode's contrast changes. #product's section head keeps the normal
   theme-following colour since .is-elevated gives it a real opaque
   background to sit on in both themes. */
#workflow .home-section-title-lg { color: #f5f4f7; }
#workflow .home-section-sub { color: #9c98ab; }
/* #help is the other non-.is-elevated .home-section — same permanently-
   dark #homeBgScene backdrop, same fix. Note this only needs to cover the
   section head (eyebrow/h2/sub): the six .home-help-card tiles below
   already have their own var(--bg-elevated) card background that flips
   light/dark correctly alongside var(--text)/var(--text-muted), so their
   text is left theme-following on purpose — hardcoding it white would
   make it unreadable on the white card background Light Mode gives it. */
#help .home-section-title-lg { color: #f5f4f7; }
#help .home-section-sub { color: #9c98ab; }

/* ---- The Workflow — six explanatory stage cards (not links — demo-
   polish pass) with short plain-language guidance, so someone who has
   never project-managed a racking job before can see what happens at
   each stage; is-current/is-complete reflect genuine project state,
   never fabricated. No hover-lift/shadow escalation: that affordance
   used to signal "clickable", which is no longer true, so it's gone
   rather than left as a misleading cue. ---- */
.home-workflow-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-5); }
.home-workflow-card { display: flex; flex-direction: column; gap: 10px; padding: var(--space-5); border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--bg-elevated); text-decoration: none; color: var(--text); }
.home-workflow-card-top { display: flex; align-items: center; justify-content: space-between; }
.home-workflow-num { font-family: var(--font-display); font-size: 0.72em; font-weight: 800; color: var(--text-faint); letter-spacing: 0.08em; }
.home-workflow-icon { width: 44px; height: 44px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; background: var(--bg-elevated-2); border: 1px solid var(--border-strong); color: var(--text-muted); flex: none; transition: background var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out); }
.home-workflow-icon .icon { width: 1.25em; height: 1.25em; }
.home-workflow-label { font-family: var(--font-display); font-size: 1.05em; font-weight: 700; }
.home-workflow-desc { font-size: 0.86em; color: var(--text-muted); line-height: 1.5; }
.home-workflow-card.is-complete .home-workflow-icon { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
.home-workflow-card.is-current { border-color: var(--accent-line); background: linear-gradient(160deg, var(--accent-soft), transparent 60%), var(--bg-elevated); box-shadow: var(--shadow-sm); }
.home-workflow-card.is-current .home-workflow-icon { background: var(--accent); border-color: var(--accent); color: #fff; }
.home-workflow-card.is-current .home-workflow-label { color: var(--accent); }

/* ---- Help (Aug 2026) — a short FAQ preview, third top-nav destination.
   Plain static cards (no JS-driven state, unlike .home-workflow-card
   above), same card chrome language as the rest of the page. ---- */
.home-help-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-5); }
.home-help-card { padding: var(--space-5); border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--bg-elevated); }
.home-help-card h3 { margin: 0 0 8px; font-family: var(--font-display); font-size: 0.98em; font-weight: 700; color: var(--text); }
.home-help-card p { margin: 0; font-size: 0.86em; color: var(--text-muted); line-height: 1.5; }
@media (max-width: 980px) {
  .home-help-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .home-help-grid { grid-template-columns: 1fr; }
}

/* ---- Product Showcase (Aug 2026, captioned grid) — a grid of six
   .home-product-card cells, one per real RackMapper stage, each pairing
   an example visual with a short title + description underneath
   (.home-showcase-row/.home-showcase-copy/-tag/-points below are the
   older two-column row layout this replaced — left in place rather than
   deleted, since nothing currently references them and removing dead
   rules isn't part of this change). Every visual reuses the hero's
   frame-chrome classes (bar/dot/title/badge) plus its own inner
   component (rack spec panel / mini tool dock / isometric 3D / install
   summary / report + BOM), all built from real app terminology, each
   carrying an "Example" badge so it's never mistaken for saved project
   data. The exact same six visuals, in the same order, also appear as
   crossfading slides in the hero above (see .home-hero-visual-rotator
   below and js/hero-visual-rotator.js). ---- */
.home-product-visual-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-6); align-items: start; }
@media (max-width: 980px) {
  .home-product-visual-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
  .home-product-visual-grid { grid-template-columns: 1fr; }
}
/* --featured (recompose pass) — asymmetric "bento" composition instead of
   six identical cards: the lead card spans 2 columns x 2 rows (grid's own
   default sparse auto-placement then slots the next two cards into the
   remaining column beside it, and the last three fill the row below — a
   large product-led panel with smaller supporting panels around it, not
   a uniform grid). align-items:stretch (grid's own default, not
   overridden here) is what lets the lead's flex-column card fill that
   full 2-row height; .home-showcase-visual's normal fixed 300px height
   is swapped for flex:1 only on this one card so it grows to fill it. */
@media (min-width: 981px) {
  .home-product-visual-grid--featured .home-product-card--lead { grid-column: span 2; grid-row: span 2; }
  .home-product-visual-grid--featured .home-product-card--lead .home-showcase-visual { height: auto; flex: 1; min-height: 0; }
}
.home-product-card { display: flex; flex-direction: column; }

/* ---- Composition pass — visual hierarchy between the lead panel and its
   five supporting panels, so scale is reinforced by weight/chrome too,
   not just grid span. The lead keeps the strong treatment (shadow-lg,
   thicker accent-toned border, a top rule that reads as "the primary
   instrument"); supporting panels are deliberately flatter/quieter
   (shadow-sm, plain border) so they read as call-outs around the lead
   rather than six panels of equal visual weight. ---------------------- */
.home-product-card--lead .home-showcase-visual {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-xl);
}
@media (min-width: 981px) {
  .home-product-visual-grid--featured .home-product-card:not(.home-product-card--lead) .home-showcase-visual {
    box-shadow: none;
    border-color: var(--border);
  }
}
/* Stage index (replaces the three neutral window-control dots) — a small
   mono ordinal in the same reading order as the workflow chain in the
   hero above, so the six panels read as connected stages of one process
   rather than six unrelated app screenshots that happen to share a
   layout. */
.home-product-stage-no {
  font-family: var(--font-mono);
  font-size: 0.72em;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--accent);
  flex: none;
}
/* Uniform preview height (Aug 2026 tidy pass) — the six visuals are very
   different natural heights (a short SVG diagram vs. a data table), so
   without a shared height the grid read as ragged/unbalanced. Fixed
   height + overflow:hidden crops the couple of taller visuals cleanly
   (their HTML content was also trimmed alongside this change so the crop
   is minimal/graceful, not mid-row); a soft bottom fade signals "there's
   more" rather than looking like an accidental cut. This is deliberately
   different from the hero rotator above, which must never crop — these
   are static decorative previews sitting side by side, not the one
   holder a visitor's eye returns to on every cycle. */
.home-product-card .home-showcase-visual { position: relative; height: 300px; display: flex; flex-direction: column; }
.home-product-card .home-showcase-visual::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 40px;
  background: linear-gradient(180deg, transparent, var(--bg-elevated));
  pointer-events: none;
}
.home-product-card .home-hero-visual-canvas,
.home-product-card .home-rack-panel,
.home-product-card .home-mini-dock-row,
.home-product-card .home-iso-wrap,
.home-product-card .home-install-panel,
.home-product-card .home-report-doc {
  flex: 1;
  min-height: 0;
}
/* Text block sized to hold the tallest description at this column count
   (3-col desktop) so all 6 titles sit on the same baseline and every
   card's bottom edge lines up, even though descriptions run 1-2 lines. */
.home-product-card-copy { padding: var(--space-4) 2px 0; min-height: 84px; }
.home-product-card-copy h3 { font-family: var(--font-display); font-size: 1.08em; font-weight: 700; line-height: 1.25; margin: 0 0 6px; color: var(--text); }
.home-product-card-copy p { font-size: 0.88em; margin: 0; color: var(--text-muted); line-height: 1.55; }
@media (max-width: 980px) {
  .home-product-card .home-showcase-visual { height: 280px; }
  .home-product-card-copy { min-height: 0; }
}
@media (max-width: 640px) {
  .home-product-card .home-showcase-visual { height: 260px; }
}
.home-showcase-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-8); align-items: center; padding: var(--space-8) 0; border-top: 1px solid var(--border); }
.home-showcase-row:first-of-type { border-top: none; padding-top: 0; }
.home-showcase-row.is-reverse .home-showcase-copy { order: 2; }
.home-showcase-tag { display: inline-flex; align-items: center; gap: 6px; padding: 5px 13px; border-radius: var(--radius-pill); background: var(--bg-elevated-2); border: 1px solid var(--border-strong); font-size: 0.72em; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-faint); margin-bottom: var(--space-4); }
.home-showcase-copy h3 { font-family: var(--font-display); font-size: 1.7em; font-weight: 700; line-height: 1.2; margin: 0 0 var(--space-4); }
.home-showcase-copy p { font-size: 1em; margin: 0 0 var(--space-5); color: var(--text-muted); line-height: 1.65; }
.home-showcase-points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.home-showcase-points li { display: flex; align-items: center; gap: 10px; font-size: 0.9em; font-weight: 600; color: var(--text); }
.home-showcase-points .icon { width: 16px; height: 16px; color: var(--accent); flex: none; }
.home-showcase-visual { border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--border-strong); background: var(--bg-elevated); box-shadow: var(--shadow-lg); }
/* Composition pass: the five supporting Built Around The Work panels get
   a tighter, more "engineered" corner radius than the lead's — visually
   smaller/quieter components clustered around one primary panel, echoing
   the same restrained-radius language the rest of the app already uses
   (css/tokens.css's own --radius-sm comment). Scoped narrowly so the
   hero's rotator/showcase-row reuses of .home-showcase-visual elsewhere
   are unaffected. */
@media (min-width: 981px) {
  .home-product-visual-grid--featured .home-product-card:not(.home-product-card--lead) .home-showcase-visual {
    border-radius: var(--radius-md);
  }
}

/* ---- Hero visual rotator (Aug 2026, crossfade rebuild) — ONE single
   fixed-size holder (per the "keep one single hero image holder"
   requirement) that #homeHeroVisualRotator's six .home-hero-visual-slide
   children crossfade inside of, instead of the container reflowing to
   whichever slide is current. The holder itself now owns the card chrome
   (border/radius/background/shadow) that used to live on each slide's own
   .home-showcase-visual class — slides are stripped back to plain content
   panes (see the ".home-hero-visual-rotator .home-showcase-visual"
   override below) so there's never a "card inside a card" double border
   during the fade.

   Real two-layer crossfade, not a display swap: js/hero-visual-rotator.js
   briefly sets BOTH the outgoing and incoming slide to display:block at
   once, forces a reflow, then flips is-active on the incoming slide and
   off the outgoing one in the same tick — both opacities animate at once
   (1->0 and 0->1 simultaneously), which is what actually reads as a
   crossfade rather than a flick. Deliberately not @starting-style here
   (that pattern is entrance-only, fine for single-panel toggles like
   .report-panel/.app-settings-panel elsewhere in this file, but wrong for
   a continuously-cycling rotator — it never fades the outgoing slide
   out). The outgoing slide is returned to display:none by the JS once its
   fade-out transition ends, so — same as before — only the active (or
   momentarily mid-transition) slide is ever laid out; the live 3D
   Visualisation slide (#homeDashPreviewWrap) still only becomes part of
   the layout right as its turn comes around, so js/home-3d-preview.js's
   own lazy-init IntersectionObserver still fires at essentially the same
   moment it always did, not eagerly on page load. */
/* Height is a min-height fallback only — js/hero-visual-rotator.js
   measures every slide's real (unscaled) content height on load/resize
   and sets an explicit inline height on this element equal to the
   tallest one, so the holder always grows to fit whichever slide needs
   the most room. No slide is ever clipped and none of them scroll (see
   .home-hero-visual-slide's overflow:hidden below — this used to be
   overflow:auto, which is exactly what was producing a scrollbar on
   taller slides like Installation Estimate/Project Report). If JS fails
   to run, this min-height is the safe static fallback (comfortably fits
   the shortest slides; taller ones would clip in that no-JS edge case
   only). */
.home-hero-visual-rotator {
  position: relative;
  min-height: 320px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-strong);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  transition: height var(--duration-slow) var(--ease-out);
}
.home-hero-visual-rotator .home-showcase-visual {
  border: none;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
.home-hero-visual-slide {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  opacity: 0;
  overflow: hidden;
  transition: opacity var(--duration-slow) var(--ease-out);
}
.home-hero-visual-slide.is-active { display: flex; opacity: 1; }
@media (max-width: 980px) {
  .home-hero-visual-rotator { min-height: 280px; }
}
@media (prefers-reduced-motion: reduce) {
  .home-hero-visual-rotator { transition-duration: 0.001ms; }
  .home-hero-visual-slide { transition-duration: 0.001ms; }
}

/* Showcase visual title bar (all six Product Showcase rows) — was
   missing its padding/layout rules entirely, so each title (Warehouse
   Planner, Racking Configurator, Layout Editor, 3D Visualisation,
   Installation Estimate, Project Report) sat with zero left padding
   flush against .home-showcase-visual's own rounded corner/overflow:
   hidden edge, clipping the first letter. Mirrors the equivalent
   .app-preview-topbar/.mock-dot/.app-preview-title window-chrome
   pattern already used by the hero mockup in style.css. */
.home-hero-visual-bar { display: flex; align-items: center; gap: 6px; padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); background: var(--bg-elevated-2); }
/* .home-hero-visual-dot (three neutral window-control dots) retired by
   the Homepage+Hub composition pass — see .home-product-stage-no above
   for what replaced it on the six Built Around The Work panels; no other
   page ever used this class (confirmed by grep before removing it). */
.home-hero-visual-title { display: flex; align-items: baseline; margin-left: var(--space-2); font-size: 0.78em; font-weight: 600; color: var(--text-muted); }
.home-hero-visual-badge { margin-left: auto; padding: 3px 9px; border-radius: var(--radius-pill); background: var(--bg-elevated); border: 1px solid var(--border-strong); font-size: 0.68em; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--text-faint); flex: none; }

/* Racking spec panel (Row 2) */
.home-rack-panel { display: flex; align-items: stretch; }
.home-rack-panel-preview { flex: 1; padding: var(--space-6); background: linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px); background-size: 26px 26px; display: flex; align-items: center; justify-content: center; }
.home-rack-panel-spec { width: 200px; flex: none; border-left: 1px solid var(--border); background: var(--bg-elevated-2); padding: var(--space-5) var(--space-4); }
/* Racking Configurator hero slide only (Aug 2026 follow-up) — same fix as
   the 3D Visualisation slide just above: the shared crossfade holder
   (.home-hero-visual-rotator) is sized in JS to fit whichever of the six
   slides is tallest, which is usually taller than this one's own natural
   content (preview + spec sidebar). margin-top/bottom:auto on this flex
   column child (.home-hero-visual-slide is display:flex;flex-direction:
   column) shares any leftover holder height evenly above and below this
   slide's panel instead of leaving it pinned to the top with empty space
   underneath — same technique, doesn't touch the panel's own sizing/
   proportions (no crop risk), doesn't touch the info bar above it, and is
   scoped to the hero copy only — the Product Showcase grid's identical
   .home-rack-panel card keeps its normal in-flow layout. */
.home-dash-hero .home-rack-panel { margin-top: auto; margin-bottom: auto; }
.home-rack-spec-row { display: flex; justify-content: space-between; align-items: baseline; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 0.82em; }
.home-rack-spec-row:last-child { border-bottom: none; }
.home-rack-spec-row span { color: var(--text-faint); font-weight: 600; }
.home-rack-spec-row strong { font-family: var(--font-display); color: var(--text); font-weight: 700; }
.home-rack-spec-title { font-family: var(--font-display); font-size: 0.72em; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); margin: 0 0 var(--space-4); }

/* Layout mini tool-dock (Row 3) */
.home-mini-dock-row { display: flex; align-items: stretch; }
.home-mini-dock { display: flex; flex-direction: column; gap: 4px; padding: 10px; background: var(--bg-elevated-2); border-right: 1px solid var(--border); flex: none; }
.home-mini-dock-btn { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 50%; color: var(--text-faint); }
.home-mini-dock-btn.is-active { background: var(--accent-soft); color: var(--accent); }
.home-mini-dock-btn .icon { width: 16px; height: 16px; }

/* 3D preview (Row 4) — real js/view3d.js render output via
   js/home-3d-preview.js, on-demand-rendered <canvas> instead of the
   previous hand-drawn isometric SVG. Fixed aspect-ratio box (rather than
   the old padding-around-a-capped-width-SVG approach) so the canvas gets
   a real, predictable pixel area to fit the synthetic layout into. */
.home-iso-wrap { position: relative; aspect-ratio: 16 / 10; min-height: 260px; max-height: 420px; display: flex; align-items: center; justify-content: center; background: radial-gradient(circle at 50% 20%, var(--accent-soft), transparent 60%), var(--bg-sunken); overflow: hidden; }
.home-iso-canvas { display: block; width: 100%; height: 100%; }
.home-iso-message { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; padding: var(--space-5); text-align: center; font-size: 0.82em; font-weight: 600; color: var(--text-faint); background: var(--bg-sunken); }
.home-iso-message.is-visible { display: flex; }
.home-iso-camera-row { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 6px; z-index: 1; }
.home-iso-cam-btn { padding: 6px 14px; border-radius: var(--radius-pill); background: var(--bg-elevated); border: 1px solid var(--border-strong); font-size: 0.7em; font-weight: 700; color: var(--text-faint); font-family: inherit; cursor: pointer; transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; }
.home-iso-cam-btn:hover { color: var(--text); border-color: var(--border); }
.home-iso-cam-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.home-iso-cam-btn.is-active { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

/* Install summary panel (Row 5) */
.home-install-panel { padding: var(--space-6); }
.home-install-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; margin-bottom: var(--space-5); }
.home-install-tile { padding: 0 var(--space-4); border-left: 1px solid var(--border); }
.home-install-tile:first-child { border-left: none; padding-left: 0; }
.home-install-tile b { display: block; font-family: var(--font-display); font-size: 1.3em; font-weight: 700; }
.home-install-tile span { font-size: 0.66em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-faint); }
.home-install-cost-row { display: flex; align-items: center; justify-content: space-between; padding: var(--space-4); border-radius: var(--radius-md); background: var(--accent-soft); border: 1px solid var(--accent-line); margin-bottom: var(--space-4); }
.home-install-cost-label { font-size: 0.78em; font-weight: 700; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
.home-install-cost-value { font-family: var(--font-display); font-size: 1.9em; font-weight: 800; color: var(--accent); }
.home-install-meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.home-install-meta-item { padding: 12px 14px; border-radius: var(--radius-sm); background: var(--bg-elevated-2); border: 1px solid var(--border); font-size: 0.8em; }
.home-install-meta-item span { display: block; color: var(--text-faint); font-weight: 600; font-size: 0.85em; margin-bottom: 3px; }
.home-install-meta-item strong { font-family: var(--font-display); }

/* Report + BOM panel (Row 6) */
.home-report-chip-row { display: flex; gap: 8px; flex-wrap: wrap; padding: var(--space-5) var(--space-5) 0; }
.home-report-chip { padding: 5px 12px; border-radius: var(--radius-pill); background: var(--bg-elevated-2); border: 1px solid var(--border-strong); font-size: 0.72em; font-weight: 700; color: var(--text-muted); }
.home-report-doc { padding: var(--space-5); }
.home-bom-table { width: 100%; border-collapse: collapse; font-size: 0.82em; }
.home-bom-table th { text-align: left; font-size: 0.7em; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-faint); padding: 0 14px 10px; font-weight: 700; }
.home-bom-table td { padding: 12px 14px; border-top: 1px solid var(--border); }
.home-bom-table tr td:last-child, .home-bom-table tr th:last-child { text-align: right; }

/* ---- Final CTA ---- */
.home-cta { position: relative; overflow: clip; text-align: center; padding: var(--space-9) 0; border-top: 1px solid var(--border); }
.home-cta-inner { position: relative; z-index: 1; }
.home-cta h2 { font-family: var(--font-display); font-size: clamp(1.8em, 3.6vw, 2.6em); font-weight: 700; max-width: 640px; margin: 0 auto var(--space-4); }
.home-cta p { max-width: 520px; margin: 0 auto var(--space-6); font-size: 1.02em; color: var(--text-muted); line-height: 1.6; }

/* ---------------------------------------------------------------------
   Home — responsive
--------------------------------------------------------------------- */
@media (max-width: 1180px) {
  .home-showcase-row { gap: var(--space-6); }
}
@media (max-width: 980px) {
  .home-workflow-grid { grid-template-columns: repeat(2, 1fr); }
  .home-showcase-row { grid-template-columns: 1fr; gap: var(--space-5); }
  .home-showcase-row.is-reverse .home-showcase-copy { order: 0; }
  .home-rack-panel { flex-direction: column; }
  .home-rack-panel-spec { width: auto; border-left: none; border-top: 1px solid var(--border); }
  .home-install-tiles { grid-template-columns: repeat(2, 1fr); gap: var(--space-4); }
  .home-install-tile { border-left: none; padding-left: 0; }
}
@media (max-width: 900px) {
  /* .app-sidebar's own display:none at this breakpoint is already
     defined once, in the shared shell rules above; Home no longer
     renders that markup at all (body.home-body has no sidebar), and
     .nav-links { display:none; } at this same breakpoint is already
     defined once in style.css's shared responsive rules. */
  .home-capstat-bar { grid-template-columns: repeat(2, 1fr); }
  .home-capstat-cell:nth-child(3) { border-left: none; }
  .home-capstat-cell:nth-child(3), .home-capstat-cell:nth-child(4) { border-top: 1px solid var(--border); }
  .home-hero { padding: var(--space-7) 0 var(--space-6); }
  .home-section, .home-cta { padding: var(--space-7) 0; }
}
@media (max-width: 720px) {
  .nav-actions .btn-sm span, .nav-actions .btn-sm { white-space: nowrap; }
}
@media (max-width: 640px) {
  .home-hero-actions .btn { flex: 1; justify-content: center; }
  .home-hero-actions { flex-direction: column; align-items: stretch; }
  .home-capstat-bar { grid-template-columns: 1fr 1fr; }
  .home-workflow-grid { grid-template-columns: 1fr; }
  .nav-actions .btn-sm { padding: 0 12px; }
  .nav-actions .btn-sm .icon { display: none; }
  /* Even with the icon hidden and padding trimmed above, the brand
     wordmark + theme toggle + New Project + Open Project buttons still
     measure wider than a phone in portrait (~512px, verified against a
     320px viewport) — .nav-inner has no wrap of its own, so that content
     was being cut off at the screen edge instead of shrinking. Letting
     both rows wrap keeps every button fully visible (drops to a second
     line) instead of clipping, with .nav switching from a fixed height to
     an auto one so the wrapped second row has room to render. */
  .nav { height: auto; min-height: var(--nav-height); padding: calc(var(--space-2) + env(safe-area-inset-top, 0px)) 0 var(--space-2); }
  .nav-inner { flex-wrap: wrap; row-gap: var(--space-2); }
  .nav-actions { flex-wrap: wrap; justify-content: flex-end; row-gap: var(--space-2); }
}

/* ---------------------------------------------------------------------
   Mobile edge breathing room (iPhone/small-screen safe area)
   Appended at file end so it wins the cascade over every narrower-
   breakpoint rule above targeting the same selectors, without editing
   those rules directly. Longhand left/right only — vertical padding on
   every one of these components is left exactly as already tuned.
   The warehouse/racking/layout canvas stage itself is deliberately not
   touched here: it needs to stay full-bleed for panning/zooming.
--------------------------------------------------------------------- */
@media (max-width: 600px) {
  .app-topbar {
    padding-left: var(--edge-pad-l);
    padding-right: var(--edge-pad-r);
  }
  .canvas-sheet {
    width: min(640px, calc(100vw - var(--edge-pad-l) - var(--edge-pad-r)));
  }
  .report-topbar {
    padding-left: var(--edge-pad-l);
    padding-right: var(--edge-pad-r);
  }
  .report-preview-wrap {
    padding-left: var(--edge-pad-l);
    padding-right: var(--edge-pad-r);
  }
  .install-main-inner {
    padding-left: var(--edge-pad-l);
    padding-right: var(--edge-pad-r);
  }
  .layout-tools-rail {
    padding-left: var(--edge-pad-l);
    padding-right: var(--edge-pad-r);
  }
}

/* ---------------------------------------------------------------------
   iPhone/mobile layout — root-cause fixes (not general padding)
   -----------------------------------------------------------------------
   Two specific, verified bugs found by inspecting computed values, not a
   blanket mobile stylesheet:

   1) Safari zooms the whole page on focus of any form control whose
      computed font-size is under 16px. Every form field in this app sets
      its font-size in em against the 16px body default, tuned for
      desktop density (0.84em-0.95em throughout, i.e. ~13.5-15px) — under
      16px on every phone, every field, on every page. Fixed at the
      element-type level (input/select/textarea) so it reaches every
      field regardless of which of the app's several field-style classes
      (.field, .ep-field, .install-field-control, .app-settings-number,
      the unclassed dimension/spec inputs, etc.) it happens to use,
      without hunting down and duplicating a dozen separate class rules.
      The !important is deliberately narrow: gated behind this ≤640px
      query and scoped to three form-control element types only, so it
      cannot reach anything outside an actual input/select/textarea and
      never applies above phone width.

   2) Four specific controls sit under Apple's 44px minimum touch target
      on phones (verified against their own CSS, not assumed): the
      bottom-sheet close button (32px), the app topbar's icon buttons
      (40px), EP's compact inline form fields (34px), and the Warehouse
      tool-list's compact dock button (34px). Every other control in the
      app (.btn, the main .canvas-dock-btn, .field input/select) was
      already at or above 44px and is left untouched.

   3) Install Calculator touch audit: .extras-amount (Optional Extras'
      cost fields) is the app's one live contenteditable "field" —
      everywhere else uses a real <input>. It was missed by fix (1)
      above because that rule only targets input/select/textarea
      element types, so tapping to edit an extra's cost still zoomed
      the whole page (its own font-size is 0.9em, under 16px). Same
      fix, same reasoning, just reaching the one field the element-type
      selector couldn't.
--------------------------------------------------------------------- */
@media (max-width: 640px) {
  input, select, textarea, .extras-amount { font-size: 16px !important; }

  .canvas-sheet-close { width: 44px; height: 44px; }
  .app-topbar-icon-btn { min-width: 44px; min-height: 44px; }
  .ep-field input, .ep-field select, .ep-field textarea { min-height: 44px; }
  .wh-tool-list .canvas-dock-btn { min-height: 44px; }
}

/* ---------------------------------------------------------------------
   VISUAL MODERNISATION PASS — technical/numeric typography
   -----------------------------------------------------------------------
   Purely additive: applies var(--font-mono) (Geist Mono) to the existing
   numeric-readout classes below so dimensions, quantities, IDs and totals
   read as instrument data, per the approved Rackulate 2.0 concept. No
   selector is modified or removed, no markup/JS touched — these rules
   only add one declaration on top of classes that already exist and are
   already rendered by the app's real JS (layout.js/racking.js/
   install.js/report.js). Safe to omit with zero functional effect.
--------------------------------------------------------------------- */
.canvas-readout-value, .canvas-zoom-value, .layout-dim-label, .layout-run-dim-label,
.ep-stat-num, .home-capstat-value, .home-install-cost-value, .install-summary-total-value,
.rack-summary-stat, .fit-fail-stat, .bom-shims-qty, .decking-qty-field,
.hud-chip, .canvas-identity-dims, .big-stepper-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Product showcase — real in-app screenshots (img/product/*.webp), captured
   from a sample project. Replaces the earlier hand-drawn SVG "Example"
   panels. Cropped top-aligned so each card shows the working area of the
   screen at a consistent height; the existing bottom fade still applies. */
.home-product-card .home-product-shot {
  flex: 1;
  min-height: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top left;
  display: block;
  background: var(--bg-sunken);
}

/* Warehouse Planner — live cursor X/Y readout (metres) */
.canvas-coords { position: absolute; left: 16px; bottom: 16px; z-index: 5; padding: 8px 12px; border-radius: 8px; font: 600 15px/1 var(--font-mono, ui-monospace, monospace); color: var(--text-muted); background: var(--bg-elevated); border: 1px solid var(--border-strong); pointer-events: none; font-variant-numeric: tabular-nums; }
/* --surface-2 was never defined in tokens.css, so this chip always fell back to a
   dark translucent fill — fine in dark mode, but unreadable dark-on-dark in light
   mode. It now uses the real theme tokens so both themes keep full contrast. */
.canvas-coords b { display: inline-block; min-width: 4.2ch; text-align: right; font-weight: 700; color: var(--text); }

/* Product screenshots — framing. The captured screens are light-UI; in dark
   mode they sit inside a recessed, bordered mat with a little inset so they
   read as deliberate "screens on the page" rather than white blocks. The
   images themselves are untouched (no recolour/filter). */
.home-product-card .home-showcase-visual { overflow: hidden; }
.home-product-card .home-product-shot { border-radius: 4px; }
html:not([data-theme="light"]) .home-product-card .home-showcase-visual {
  background: var(--bg-sunken);
  border-color: var(--border-strong);
}
html:not([data-theme="light"]) .home-product-card .home-showcase-visual::after { display: none; }
/* Frame only (border + outline ring): the image keeps its original flex/height
   sizing so the lead (Warehouse) card can never collapse to zero height. */
html:not([data-theme="light"]) .home-product-card .home-product-shot {
  box-sizing: border-box;
  border-top: 1px solid var(--border-strong);
  outline: 1px solid var(--border-strong);
  outline-offset: -1px;
}
/* 3D Visualisation — larger on screen so the racking detail reads. Same
   frame; the image is simply displayed bigger inside it, centred on the racking. */
/* (3D card now uses the standard card sizing — same as Installation Estimate / Project Report.) */

/* Project Health — floating centred dialog (was a bottom-docked sheet).
   Same panel, tabs and content; only its placement changes. Centred in the
   viewport, capped to the viewport height with the body scrolling inside,
   close control stays in the fixed header. Closed = hidden (no off-screen
   slide), open = fade/rise into place. */
.canvas-sheet.health-canvas-sheet {
  top: 50%; bottom: auto; left: 50%;
  width: min(760px, calc(100vw - 24px));
  max-height: calc(100vh - 48px);
  max-height: calc(100dvh - 48px);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  transform: translate(-50%, calc(-50% + 14px));
  opacity: 0; visibility: hidden; pointer-events: none;
  z-index: 90;
  transition: transform var(--duration-med) var(--ease-out), opacity var(--duration-med) var(--ease-out), visibility 0s linear var(--duration-med);
}
#healthSheetToggle:checked ~ .app-shell-body .health-canvas-sheet {
  transform: translate(-50%, -50%);
  opacity: 1; visibility: visible; pointer-events: auto;
  transition-delay: 0s;
}
.health-canvas-sheet .canvas-sheet-handle { display: none; }
.health-canvas-sheet .canvas-sheet-head, .health-canvas-sheet .canvas-sheet-tabs { flex: none; }
.health-canvas-sheet .canvas-sheet-body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
#healthSheetToggle:checked ~ .app-shell-body .sheet-backdrop[for="healthSheetToggle"] { z-index: 89; }
@media (max-width: 520px) {
  .canvas-sheet.health-canvas-sheet { width: calc(100vw - 16px); max-height: calc(100dvh - 24px); }
}
@media (prefers-reduced-motion: reduce) {
  .canvas-sheet.health-canvas-sheet { transition: none; }
}

/* Closed centred dialogs must not intercept pointer events.
   Cause of the dead Project Hub filter tabs: the New Project and Estimate
   Breakdown dialogs are position:fixed, centred, and "closed" only by
   opacity:0 — they stayed visibility:visible / pointer-events:auto, so an
   invisible 640px panel sat over the middle of the page and swallowed
   clicks. Closed = hidden + inert; open restores both. */
.canvas-sheet.home-new-project-modal,
.canvas-sheet.install-breakdown-modal {
  visibility: hidden; pointer-events: none;
  transition: transform var(--duration-med) var(--ease-out), opacity var(--duration-med) var(--ease-out), visibility 0s linear var(--duration-med);
}
.sheet-toggle:checked ~ .canvas-sheet.home-new-project-modal,
.sheet-toggle:checked ~ .canvas-sheet.install-breakdown-modal {
  visibility: visible; pointer-events: auto; transition-delay: 0s;
}
#projectSheetToggle:checked ~ .canvas-sheet.install-breakdown-modal {
  visibility: hidden; pointer-events: none;
}

/* Homepage hero headline — second line is longer ("Designed. Planned.
   Delivered."), so allow it to wrap on narrow screens instead of overflowing. */
@media (max-width: 640px) {
  .home-hero-title { font-size: clamp(1.7em, 8vw, 2.1em); }
  .home-hero-title-line { white-space: normal; }
}

/* Warehouse Planner (lead) screenshot — slightly smaller inside its frame,
   with even breathing room. Padding only: the image keeps its flex/height
   sizing so it cannot collapse (dark-mode regression guard). */
@media (min-width: 981px) {
  .home-product-card--lead .home-showcase-visual { padding: 18px 22px; box-sizing: border-box; }
  .home-product-card--lead .home-product-shot { border-radius: 6px; }
}
@media (max-width: 980px) {
  .home-product-card--lead .home-showcase-visual { padding: 10px 12px; box-sizing: border-box; }
}


/* Hero video as the full-section background.
   Layers: video (z0) -> left readability gradient (.hero-hp-visual-fade, z3
   inside the visual) -> hero content (copy, buttons, workflow strip, z2).
   The visual is taken out of the grid and pinned to the section; the grid
   no longer creates its own containing block so the video spans the whole
   hero. Gradient colours are fixed dark (hero text is fixed light in both
   themes), so readability does not depend on the light/dark setting. */
.home-dash-hero { overflow: hidden; isolation: isolate; }
.home-dash-hero-grid { position: static; z-index: auto; }
.home-dash-hero .hero-hp-visual {
  position: absolute; inset: 0; z-index: 0;
  width: auto; min-height: 0; margin: 0; border-radius: 0;
}
.home-dash-hero-copy, .home-dash-hero .home-hero-chain { position: relative; z-index: 2; }
.home-dash-hero .hero-hp-visual-fade {
  background:
    linear-gradient(90deg, rgba(10,12,18,0.92) 0%, rgba(10,12,18,0.78) 22%, rgba(10,12,18,0.42) 42%, rgba(10,12,18,0.08) 62%, transparent 75%),
    linear-gradient(to top, var(--bg) 0%, transparent 18%);
}
@media (max-width: 980px) {
  .home-dash-hero .hero-hp-visual { border-radius: 0; }
  .home-dash-hero .hero-hp-visual-fade {
    background:
      linear-gradient(180deg, rgba(10,12,18,0.72) 0%, rgba(10,12,18,0.6) 55%, rgba(10,12,18,0.4) 100%),
      linear-gradient(to top, var(--bg) 0%, transparent 18%);
  }
}

/* Hero height +~24% (477px -> 590px desktop, 560px -> 690px mobile) so the
   full-background video has more vertical room. Content stays centred. */
.home-dash-hero { min-height: 590px; display: flex; flex-direction: column; justify-content: center; }
@media (max-width: 980px) { .home-dash-hero { min-height: 690px; } }


/* ======================================================================
   Homepage colour consistency — charcoal / white / Hub green.
   Scoped to body.home-body (and the always-dark hero + CTA bands), so the
   shared theme and every other module keep their own tokens. Dark theme
   only for surfaces. The accent is the Project Hub's own primary-button
   colour: #00c2d8 with #0092a6 for hover (the two stops of the Hub's
   .btn-accent gradient, i.e. tokens.css dark --accent / --accent-2).
   Light theme uses the deeper Hub stop #0092a6 (and #006d7c, the same
   step down, for hover/text) so accent text stays readable on near-white.
   No glows, no accent gradients.
   ====================================================================== */
html:not([data-theme="light"]) body.home-body {
  --bg: #15171a;
  --bg-elevated: #1c1f23;
  --bg-elevated-2: #25292e;
  --bg-sunken: #101214;
  --text: #f4f5f6;
  --text-muted: #b4b9bf;
  --accent: #00c2d8;
  --accent-2: #0092a6;
  --accent-soft: rgba(0, 194, 216, 0.16);
  --accent-line: rgba(0, 194, 216, 0.45);
  --accent-glow: transparent;
}
html[data-theme="light"] body.home-body {
  --accent: #0092a6;
  --accent-2: #006d7c;
  --accent-soft: rgba(0, 146, 166, 0.1);
  --accent-line: rgba(0, 146, 166, 0.3);
  --accent-glow: transparent;
}
/* Small accent-coloured text on the light surfaces (nav bar, product cards)
   uses the deeper step. */
html[data-theme="light"] body.home-body .nav .brand-word strong,
html[data-theme="light"] body.home-body .home-product-stage-no { color: #006d7c; }
/* Light nav bar is translucent over the dark hero, so its links use the
   full text colour (hover is still marked by the accent underline). */
html[data-theme="light"] body.home-body .nav-links a { color: var(--text); }
/* Hero and closing CTA are always dark, whatever the theme. */
.home-dash-hero, .home-cta {
  --accent: #00c2d8;
  --accent-2: #0092a6;
  --accent-soft: rgba(0, 194, 216, 0.16);
  --accent-line: rgba(0, 194, 216, 0.45);
  --accent-glow: transparent;
}
/* The closing CTA and the footer have no background of their own: they sit
   on the fixed near-black #homeBgScene, which does not flip with the theme
   (see js/hero-background.js). So in light mode they take the same
   foreground tokens the homepage uses in dark mode, instead of inheriting
   the light theme's dark text. */
html[data-theme="light"] body.home-body .home-cta,
html[data-theme="light"] body.home-body .site-footer {
  --text: #f4f5f6;
  --text-muted: #b4b9bf;
  --text-faint: #9aa0a6;
  --border: #1c1f26;
  --border-strong: #2e333d;
  --accent: #00c2d8;
  --accent-2: #0092a6;
  --accent-soft: rgba(0, 194, 216, 0.16);
  --accent-line: rgba(0, 194, 216, 0.45);
  color: var(--text);
}
/* The hero's fallback SVG is a warehouse blueprint illustration, not UI
   branding — it keeps its original blueprint blue. */
.hero-hp-visual-fallback { --accent: #1688D4; --accent-2: #1074b8; }
/* Open Rackulate buttons: dark charcoal text (the app's own #12141a) on the
   Hub teal pair — 8.5:1 on #00c2d8, 5.0:1 on the #0092a6 hover. White was
   2.2:1. The light-theme nav button uses the same pair as the hero/CTA
   buttons, since no text colour reaches 4.5:1 on the deeper #006d7c. */
.home-body .btn-accent { background: var(--accent); box-shadow: none; color: #12141a; }
.home-body .btn-accent:hover { background: var(--accent-2); box-shadow: none; }
html[data-theme="light"] body.home-body .nav .btn-accent { background: #00c2d8; }
html[data-theme="light"] body.home-body .nav .btn-accent:hover { background: #0092a6; }
.home-hero-chain-dot { box-shadow: none; }
.home-hero-chain { border-top-color: rgba(255, 255, 255, 0.14); }
.home-bg-scene-glow { opacity: 0.5; }

/* Hero video — readability shading behind the copy column (kept dark and
   strongest under the headline/description, fading out before the middle
   of the video so bright frames never wash the text out) plus a short soft
   bottom blend into the next section's own surface (var(--bg-elevated),
   the #product section background; follows the theme). The blend stays
   below the workflow strip so the strip always sits on the dark shading. */
.home-dash-hero .hero-hp-visual-fade {
  background:
    linear-gradient(90deg, rgba(10,12,18,0.92) 0%, rgba(10,12,18,0.88) 26%, rgba(10,12,18,0.62) 40%, rgba(10,12,18,0.2) 56%, transparent 70%),
    linear-gradient(to top, var(--home-product-bg, var(--bg-elevated)) 0%, color-mix(in srgb, var(--home-product-bg, var(--bg-elevated)) 55%, transparent) 5%, transparent 13%);
}
@media (max-width: 980px) {
  .home-dash-hero .hero-hp-visual-fade {
    background:
      linear-gradient(180deg, rgba(10,12,18,0.78) 0%, rgba(10,12,18,0.82) 30%, rgba(10,12,18,0.8) 52%, rgba(10,12,18,0.55) 72%, rgba(10,12,18,0.4) 100%),
      linear-gradient(to top, var(--home-product-bg, var(--bg-elevated)) 0%, color-mix(in srgb, var(--home-product-bg, var(--bg-elevated)) 55%, transparent) 9%, transparent 26%);
  }
}


/* ======================================================================
   Homepage bottom — FAQ, closing CTA, footer (homepage-scoped; the shared
   .site-footer rules in style.css are left alone for other pages).
   ====================================================================== */
/* FAQ: slightly larger type and more generous spacing */
body.home-body .home-help-grid { gap: var(--space-6); }
body.home-body .home-help-card { padding: var(--space-6); }
body.home-body .home-help-card h3 { font-size: 1.1em; margin-bottom: 10px; }
body.home-body .home-help-card p { font-size: 0.95em; line-height: 1.65; color: var(--text-muted); }

/* Closing CTA: shorter, button centred directly under the heading text */
body.home-body .home-cta, .home-cta { padding: var(--space-7) 0; }
.home-cta h2 { margin-bottom: var(--space-3); }
.home-cta p { margin-bottom: var(--space-5); }
.home-cta-actions { display: flex; justify-content: center; }
@media (max-width: 640px) { .home-cta-actions .btn { width: 100%; justify-content: center; } }

/* Footer: larger, higher-contrast text */
body.home-body .site-footer { padding: var(--space-7) 0; }
body.home-body .footer-tagline { font-size: 0.95em; color: var(--text); }
body.home-body .footer-links { gap: var(--space-2) var(--space-5); }
body.home-body .footer-links a { font-size: 0.95em; color: var(--text); }
body.home-body .footer-note { font-size: 0.92em; color: var(--text-muted); line-height: 1.5; }
body.home-body .footer-note a { color: var(--text); }
body.home-body .footer-links a:hover, body.home-body .footer-links a:focus-visible,
body.home-body .footer-note a:hover, body.home-body .footer-note a:focus-visible { color: var(--accent); }

/* ======================================================================
   Homepage product screenshots — click-to-enlarge viewer (js/product-lightbox.js)
   ====================================================================== */
.pshot-trigger { cursor: zoom-in; }
.pshot-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Hover affordance (no text): the frame edge lifts slightly. */
.pshot-trigger { transition: border-color var(--duration-fast, 0.15s) var(--ease-out, ease-out); }
.pshot-trigger:hover, .pshot-trigger:focus-visible { border-color: rgba(255, 255, 255, 0.34); }
html[data-theme="light"] .pshot-trigger:hover, html[data-theme="light"] .pshot-trigger:focus-visible { border-color: rgba(16, 26, 34, 0.35); }

.pshot-viewer {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; background: rgba(8, 9, 12, 0.88);
  cursor: zoom-out;
}
.pshot-viewer[hidden] { display: none; }
.pshot-viewer-fig { margin: 0; max-width: min(96vw, 1800px); max-height: 94vh; display: flex; flex-direction: column; align-items: center; gap: 10px; cursor: default; }
.pshot-viewer-img {
  display: block; max-width: 100%; max-height: calc(94vh - 40px);
  width: auto; height: auto; object-fit: contain;
  border-radius: 6px; border: 1px solid rgba(255, 255, 255, 0.16);
  background: #101214; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}
.pshot-viewer-cap { color: #d6d9dd; font-size: 13px; font-weight: 600; }
.pshot-viewer-close {
  position: absolute; top: 14px; right: 18px; width: 44px; height: 44px;
  border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(20, 22, 26, 0.85); color: #fff; font-size: 26px; line-height: 1; cursor: pointer;
}
.pshot-viewer-close:hover { background: rgba(40, 44, 50, 0.95); }
.pshot-viewer-close:focus-visible { outline: 2px solid var(--accent, #00c2d8); outline-offset: 2px; }
html.pshot-open { overflow: hidden; }
@media (prefers-reduced-motion: no-preference) {
  .pshot-viewer:not([hidden]) { animation: pshotFade 0.15s ease-out; }
  @keyframes pshotFade { from { opacity: 0; } to { opacity: 1; } }
}


/* Unified dark page background (dark theme). Below the hero video the page
   is ONE continuous near-black surface — the fixed #homeBgScene — with every
   section transparent, so there is no horizontal banding. The blue/yellow
   glows (scene glow + the closing CTA's .hero-glow) are removed. Three
   irregular desaturated-blue beams live on the fixed scene (::after), sit across the
   content area, and are faded/shifted by scroll (js/hero-background.js sets
   --home-beam-o / --home-beam-y). The hero's bottom blend reads the same
   colour via --home-product-bg. */
html:not([data-theme="light"]) body.home-body { --home-product-bg: #0a0b0d; }
html:not([data-theme="light"]) .home-bg-scene { background: #0a0b0d; overflow: hidden; }
html:not([data-theme="light"]) .home-bg-scene-glow,
html:not([data-theme="light"]) .home-bg-scene-scrim { display: none; }
html:not([data-theme="light"]) .home-bg-scene::after {
  content: ""; position: absolute; left: 0; right: 0; top: -12%; bottom: -12%;
  pointer-events: none;
  opacity: var(--home-beam-o, 0.85);
  transform: translate3d(0, var(--home-beam-y, 0px), 0);
  will-change: opacity, transform;
  background:
    /* beam 1 — left, steeper, closer to the content column */
    linear-gradient(103deg, transparent 6%, rgba(140,170,210,0.020) 11%, rgba(140,170,210,0.050) 15%, rgba(140,170,210,0.150) 17.4%, rgba(140,170,210,0.050) 20%, rgba(140,170,210,0.016) 25%, transparent 31%),
    /* beam 2 — centre-right, wider halo, shallower */
    linear-gradient(113deg, transparent 44%, rgba(140,170,210,0.016) 49%, rgba(140,170,210,0.045) 53.5%, rgba(140,170,210,0.130) 56%, rgba(140,170,210,0.045) 59%, rgba(140,170,210,0.014) 64%, transparent 71%),
    /* beam 3 — right, thin */
    linear-gradient(98deg, transparent 72%, rgba(140,170,210,0.018) 77%, rgba(140,170,210,0.055) 80%, rgba(140,170,210,0.120) 81.4%, rgba(140,170,210,0.040) 83.4%, transparent 90%);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 16%, #000 84%, transparent 100%);
          mask-image: linear-gradient(180deg, transparent 0%, #000 16%, #000 84%, transparent 100%);
}
html:not([data-theme="light"]) body.home-body #product.home-section,
html:not([data-theme="light"]) body.home-body .home-cta {
  background: transparent; border-top-color: transparent; border-bottom-color: transparent;
}
html:not([data-theme="light"]) body.home-body #product.home-section { padding: var(--space-8) 0 var(--space-7); }
.home-cta .hero-glow { display: none; }

/* Tighter vertical rhythm between the product section, FAQ and closing CTA
   (homepage only; the large 128px section padding left big dead gaps). */
body.home-body .home-section-head { margin-bottom: var(--space-6); }
body.home-body #help.home-section { padding: var(--space-7) 0 var(--space-8); }
@media (max-width: 980px) {
  html:not([data-theme="light"]) body.home-body #product.home-section { padding: var(--space-7) 0 var(--space-6); }
  body.home-body #help.home-section { padding: var(--space-6) 0 var(--space-7); }
}
body.home-body .site-footer { border-top-color: rgba(255, 255, 255, 0.07); background: transparent; }


/* ======================================================================
   How It Works — compact connected workflow (homepage only).
   Sits on the permanently dark page background, so text colours are fixed
   light (same approach as #help's section head). Hub-green accent only on the
   nodes and arrows.
   ====================================================================== */
#how-it-works { scroll-margin-top: calc(var(--nav-height, 64px) + var(--space-5)); --hiw-accent: #00c2d8; }
#help, #product { scroll-margin-top: calc(var(--nav-height, 64px) + var(--space-5)); }
body.home-body #how-it-works.home-section { padding: var(--space-7) 0; }
#how-it-works .home-section-title-lg { color: #f5f4f7; }
#how-it-works .home-section-sub { color: #b4b9bf; }
#how-it-works .eyebrow { color: #b4b9bf; }
.hiw-flow { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0 var(--space-5); }
.hiw-step { position: relative; padding-top: var(--space-4); border-top: 2px solid rgba(255, 255, 255, 0.16); }
.hiw-step::after { content: ""; position: absolute; top: -2px; left: 0; width: 40%; height: 2px; background: var(--hiw-accent); }
.hiw-link { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-3); font-family: var(--font-display); font-size: 0.82em; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #f5f4f7; }
.hiw-node { display: none; }
.hiw-to { color: #f5f4f7; }
.hiw-arrow { position: relative; flex: none; width: 22px; height: 2px; background: var(--hiw-accent); }
.hiw-arrow::after { content: ""; position: absolute; right: -1px; top: -4px; width: 0; height: 0; border-left: 7px solid var(--hiw-accent); border-top: 5px solid transparent; border-bottom: 5px solid transparent; }
.hiw-step p { margin: 0; font-size: 0.92em; line-height: 1.6; color: #b4b9bf; }
.hiw-note { margin: var(--space-6) 0 0; padding-top: var(--space-4); border-top: 1px solid rgba(255, 255, 255, 0.10); font-size: 0.92em; color: #b4b9bf; max-width: 70ch; }
@media (max-width: 1100px) {
  .hiw-flow { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-6) var(--space-5); }
}
@media (max-width: 640px) {
  .hiw-flow { grid-template-columns: 1fr; gap: 0; border-left: 2px solid rgba(255, 255, 255, 0.16); margin-left: 4px; }
  .hiw-step { border-top: none; padding: 0 0 var(--space-5) var(--space-5); }
  .hiw-step::after { top: 4px; left: -6px; width: 10px; height: 10px; border-radius: 50%; }
}

/* ======================================================================
   Installation Estimate summary + Project Report side panel — dark-UI fixes.
   - The "Why this number?" info buttons and popovers had no styles at all
     (browser-default white square buttons, popover text printed inline).
   - Row labels used --text-faint (low contrast); dates wrapped mid-value.
   - Report panel buttons used undefined .btn-primary/.btn-secondary and
     rendered as default white controls.
   Presentation only — no calculation or flow changes.
   ====================================================================== */
.install-summary-row { position: relative; align-items: center; gap: var(--space-3); }
.install-summary-row > span { display: inline-flex; align-items: center; gap: 8px; font-size: 0.84em; font-weight: 600; color: var(--text-muted); line-height: 1.35; }
.install-summary-row > strong { text-align: right; white-space: nowrap; flex: none; }
.install-summary-total-label { color: var(--text-muted); }
.install-summary .install-info { display: inline-flex; flex: none; }
.install-info-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; flex: none;
  border-radius: 50%; border: 1px solid var(--border-strong);
  background: transparent; color: var(--text-muted); cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.install-info-btn .icon { width: 12px; height: 12px; }
.install-info-btn:hover, .install-info-btn[aria-expanded="true"] { color: var(--text); background: var(--bg-elevated-2); border-color: var(--text-muted); }
.install-info-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.install-info-popover {
  display: none; position: absolute; left: 0; right: 0; top: calc(100% + 8px); z-index: 20;
  padding: 10px 12px; border-radius: var(--radius-md);
  background: var(--bg-elevated-2); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lg);
}
.install-info-popover.is-open { display: block; }
.install-info-popover p { margin: 0; font-size: 0.78em; font-weight: 500; line-height: 1.5; color: var(--text); text-align: left; }

.report-options .btn-primary, .report-options .btn-secondary, .report-options .btn-ghost {
  min-height: 40px; padding: 0 16px; border-radius: var(--radius-md);
  font-size: 0.86em; font-weight: 700; letter-spacing: 0; white-space: normal; text-align: center;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.report-options .btn-primary { background: var(--accent); color: #fff; border: 1px solid var(--accent); box-shadow: none; }
.report-options .btn-primary:hover:not(:disabled) { background: var(--accent-2); border-color: var(--accent-2); }
.report-options .btn-primary:disabled { background: var(--bg-elevated-2); border-color: var(--border); color: var(--text-faint); cursor: not-allowed; opacity: 1; }
.report-options .btn-secondary { background: var(--bg-elevated-2); color: var(--text); border: 1px solid var(--border-strong); cursor: pointer; }
.report-options .btn-secondary:hover { border-color: var(--text-muted); }
.report-options .btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--border-strong); cursor: pointer; }
.report-options .btn-ghost:hover { background: var(--bg-elevated-2); }
.report-options .report-hint { color: var(--text-muted); }
.issue-design-panel { display: flex; flex-direction: column; gap: var(--space-3); }
.issue-design-status { display: flex; align-items: flex-start; gap: 10px; padding: 12px; border-radius: var(--radius-md); background: var(--bg-elevated-2); border: 1px solid var(--border); }
.issue-design-status-label { font-weight: 700; font-size: 0.88em; color: var(--text); }
.issue-design-status-detail { margin-top: 2px; font-size: 0.8em; line-height: 1.45; color: var(--text-muted); }
.issue-design-actions { display: flex; flex-direction: column; gap: 8px; }
.issue-design-actions .btn { width: 100%; }

/* =========================================================================
   Racking Configurator — compact right-hand specification panel.
   Presentation only, scoped to .rack-spec-rail. The panel's spacing tokens
   are re-declared locally so the many inline margins in racking.html
   (var(--space-5..8)) tighten together without touching the markup, and
   every field, id and control is unchanged and still always visible.
   ========================================================================= */
.rack-spec-rail {
  --space-5: 10px; --space-6: 12px; --space-7: 14px; --space-8: 16px;
  width: 292px;
  padding: 14px 14px 18px;
}
.rack-spec-rail .panel-title { margin-bottom: 12px; font-size: 0.78em; }
.rack-spec-rail .sheet-section-title {
  font-size: 0.7em; padding-bottom: 5px; border-bottom: 1px solid var(--border); margin-top: 4px;
}
.rack-spec-rail .field { gap: 4px; }
.rack-spec-rail .field-grid { column-gap: 10px; row-gap: 10px; }
.rack-spec-rail .field-label { font-size: 0.74em; letter-spacing: 0.02em; }
.rack-spec-rail .snap-field-hint { font-size: 0.7em; line-height: 1.3; }
.rack-spec-rail .big-stepper.stepper-sm { padding: 3px 5px; gap: 6px; }
.rack-spec-rail .stepper-sm .big-stepper-btn { width: 26px; height: 26px; font-size: 0.9em; }
.rack-spec-rail .stepper-sm .big-stepper-value { font-size: 1em; min-width: 4ch; }
.rack-spec-rail .segmented-btn { padding: 5px 8px; font-size: 0.82em; min-height: 0; }
.rack-spec-rail .primary-swatch-grid { gap: 6px; }
.rack-spec-rail .primary-swatch-grid .swatch-card { padding: 7px 4px; gap: 4px; }
.rack-spec-rail .primary-swatch-grid .swatch-glyph { width: 26px; height: 26px; }
.rack-spec-rail #deckingGroup, .rack-spec-rail #columnProtectionSeg { gap: 6px; }
.rack-spec-rail #deckingGroup .swatch-card, .rack-spec-rail #columnProtectionSeg .swatch-card { padding: 5px 10px; min-height: 32px; gap: 6px; }
.rack-spec-rail #deckingGroup .swatch-glyph, .rack-spec-rail #columnProtectionSeg .swatch-glyph { width: 20px; height: 20px; }
.rack-spec-rail .run-length-strip { margin-top: 6px; }
.rack-spec-rail .run-length-strip span { height: 14px; }
@media (max-width: 1280px) {
  .rack-spec-rail { width: 268px; }
}
@media (max-width: 900px) {
  .rack-spec-rail { width: auto; }
}

/* Racking Configurator 3D preview — measurement panel. The canvas and panel
   share a flex row, so the panel takes its own space beside the canvas and
   can never cover the rack; stacks under the canvas on narrow screens. */
.view3d-canvas-wrap--split { display: flex; flex-direction: row; }
.view3d-canvas-wrap--split #view3DCanvas { flex: 1 1 auto; min-width: 0; width: auto; }
.view3d-info {
  flex: none; align-self: flex-start; margin: 12px 12px 0 0; padding: 8px 10px;
  display: flex; flex-direction: column; gap: 6px; min-width: 150px;
  background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px;
  font-size: 11px; line-height: 1.2;
}
.view3d-info:empty { display: none; }
.view3d-info-row { display: flex; align-items: center; gap: 7px; }
.view3d-info-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.view3d-info-label { color: var(--text-muted); flex: 1 1 auto; white-space: nowrap; }
.view3d-info-value { font-family: var(--font-mono); font-weight: 700; color: var(--text); white-space: nowrap; }
@media (max-width: 640px) {
  .view3d-canvas-wrap--split { flex-direction: column; }
  .view3d-canvas-wrap--split #view3DCanvas { flex: 1 1 auto; min-height: 0; width: 100%; }
  .view3d-info { align-self: stretch; margin: 6px 10px 8px; flex-direction: row; flex-wrap: wrap; gap: 6px 14px; }
}

/* Data Management (Settings) + Wipe Rackulate Data confirmation dialog — js/app-settings.js, js/data-wipe.js */
.app-settings-data { margin-top: var(--space-5); padding-top: var(--space-4); border-top: 1px solid var(--border); }
.app-settings-data-title { margin: 0 0 6px; font-size: 0.88em; font-weight: 700; color: var(--text); }
.app-settings-data .btn { margin-top: var(--space-3); width: 100%; justify-content: center; }
.wipe-backdrop { position: fixed; inset: 0; z-index: 400; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(4, 3, 8, 0.7); }
.wipe-dialog { width: min(560px, 100%); max-height: calc(100vh - 32px); overflow-y: auto; padding: 22px; border-radius: 12px; background: var(--bg-elevated); border: 1px solid var(--danger); box-shadow: 0 16px 48px rgba(0,0,0,.5); color: var(--text); }
.wipe-dialog h2 { margin: 0 0 10px; font-size: 1.1em; color: var(--danger); }
.wipe-dialog p { margin: 0 0 10px; font-size: 0.88em; line-height: 1.55; color: var(--text-muted); }
.wipe-dialog p strong { color: var(--text); }
.wipe-found { color: var(--text) !important; }
.wipe-final { font-weight: 600; color: var(--text) !important; }
.wipe-label { display: block; margin: 14px 0 6px; font-size: 0.84em; font-weight: 600; color: var(--text); }
.wipe-label code { padding: 1px 6px; border-radius: 4px; background: var(--bg-elevated-2); font-family: var(--font-mono); letter-spacing: 0.04em; }
.wipe-input { width: 100%; box-sizing: border-box; min-height: 44px; padding: 8px 12px; border-radius: 8px; border: 1px solid var(--border-strong); background: var(--bg-elevated-2); color: var(--text); font: 600 16px var(--font-mono, monospace); letter-spacing: 0.06em; }
.wipe-input:focus-visible { outline: 2px solid var(--danger); outline-offset: 1px; }
.wipe-result { margin: 12px 0 0; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border-strong); font-size: 0.86em; font-weight: 600; }
.wipe-result[hidden] { display: none; }
.wipe-result.is-ok { border-color: var(--ok); background: var(--ok-soft); color: var(--ok); }
.wipe-result.is-error { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
.wipe-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; flex-wrap: wrap; }
.wipe-actions .btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.wipe-tab-notice { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 500; padding: 10px 16px; border-radius: 10px; background: var(--danger-soft); border: 1px solid var(--danger); color: var(--text); font-size: 0.85em; font-weight: 600; }
.wipe-tab-notice a { color: var(--accent); margin-left: 6px; }

/* ---------------------------------------------------------------------
   Targeted fixes (BOM branding + print/PDF). Items 6 & 7.
--------------------------------------------------------------------- */
/* BOM document header: Rackulate mark + "RACKULATE | BILL OF MATERIALS". */
.bomdoc-brand { align-items: center; }
.bomdoc-mark { width: 34px; height: 39px; flex: none; }
.bomdoc-wordmark { font-size: 0.95em; line-height: 1.1; color: #0a2540; font-weight: 500; letter-spacing: 0.02em; }
.bomdoc-wordmark strong { color: #0072a8; font-weight: 800; }
.bomdoc-brand .bomdoc-title { margin: 3px 0 0; font-size: 1.35em; letter-spacing: 0.04em; color: #0a2540; }
.bomdoc-title-brand { color: #0072a8; }
.bomdoc-title-sep { color: #94a3b8; font-weight: 400; padding: 0 2px; }
.bomdoc-head { border-bottom-color: #0a2540; }
.bomdoc-titleblock { display: flex; align-items: center; gap: 12px; }
.bomdoc-titleblock .bomdoc-company { text-align: right; }

/* Report options: selectable items read as selectable, active ones as active. */
.report-option-row { padding: 0 8px; border-left: 3px solid transparent; transition: background var(--duration-fast, 120ms); }
.report-option-row:hover { background: var(--bg-elevated-2); }
.report-option-row:has(input:checked) { border-left-color: var(--accent); background: var(--accent-soft); }
.report-option-row:not(:has(input:checked)) { color: var(--text-muted); }
.report-option-row input:focus-visible, .report-preset-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.report-preset-btn.active { border-left: 3px solid var(--accent); }

@media print {
  /* The page must flow naturally — the app shell pins html/body to the
     viewport with hidden overflow, which would clip a multi-page BOM. */
  html:has(body.report-print-mode), body.report-print-mode { height: auto !important; min-height: 0 !important; overflow: visible !important; display: block !important; background: #fff !important; }
  body.report-print-mode .report-backdrop { display: none !important; }
  body.report-print-mode .report-panel { display: block !important; position: static !important; inset: auto !important; height: auto !important; overflow: visible !important; transform: none !important; opacity: 1 !important; }
  body.report-print-mode .report-body, body.report-print-mode .report-preview-wrap { display: block !important; height: auto !important; overflow: visible !important; }
  body.report-print-mode .report-paper { color: #1c2536; background: #fff; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  body.report-print-mode .bomdoc-group td, body.report-print-mode .bomdoc-table th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* ---------------------------------------------------------------------
   Stage availability (js/navigation.js): stages whose prerequisites are not
   in the project yet stay visible but greyed, with a short reason chip.
--------------------------------------------------------------------- */
.app-sidebar-link.is-stage-locked, .hub-module-tile.is-stage-locked { opacity: 0.55; cursor: not-allowed; }
.app-sidebar-link.is-stage-locked:hover { background: none; color: var(--text-faint); }
.stage-lock-badge {
  margin-left: auto; flex: none; white-space: nowrap;
  font-size: 0.62em; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  padding: 2px 7px; border: 1px dashed var(--border-strong); border-radius: 999px; color: var(--text-muted);
}
#sidebarCollapse:checked ~ .app-shell-body .stage-lock-badge { display: none; }
.stage-toast {
  position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 12px); z-index: 3000;
  max-width: min(560px, calc(100vw - 32px)); padding: 10px 14px;
  background: var(--bg-elevated); color: var(--text); border: 1px solid var(--border-strong); border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm); font-size: 0.88em; font-weight: 600; box-shadow: 0 6px 24px rgba(0,0,0,.35);
  opacity: 0; pointer-events: none; transition: opacity .15s, transform .15s;
}
.stage-toast.is-on { opacity: 1; transform: translate(-50%, 0); }

/* Install: weekend day rates */
.install-weekend-day { display: flex; align-items: center; gap: 8px; min-height: 44px; font-size: 0.9em; color: var(--text); }
.install-weekend-day input { width: 20px; height: 20px; }
.install-weekend-warning { margin: 6px 0 0; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--warn, #d97706); color: var(--text); font-size: 0.85em; line-height: 1.4; }
.install-weekend-warning::before { content: "Warning: "; font-weight: 700; }

/* ---- Report builder: drag handle + reorder controls ---- */
.report-option-draggable { padding-left: 0; }
.report-option-label { flex: 1 1 auto; display: flex; align-items: center; gap: 10px; min-height: 40px; cursor: pointer; }
.report-drag-handle { flex: none; width: 28px; min-height: 40px; border: 0; background: none; color: var(--text-faint); cursor: grab; font-size: 14px; letter-spacing: -2px; }
.report-drag-handle:hover, .report-drag-handle:focus-visible { color: var(--accent); }
.report-drag-handle:focus-visible, .report-move-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.report-move-btns { flex: none; display: inline-flex; gap: 2px; }
.report-move-btn { width: 26px; height: 26px; border: 1px solid var(--border); border-radius: 4px; background: transparent; color: var(--text-muted); font-size: 9px; cursor: pointer; }
.report-move-btn:hover:not(:disabled) { color: var(--accent); border-color: var(--accent-line); }
.report-move-btn:disabled { opacity: 0.3; cursor: default; }
.report-option-draggable.is-dragging { opacity: 0.45; }
.report-option-draggable.is-drop-above { box-shadow: 0 -2px 0 var(--accent); }
.report-option-draggable.is-drop-below { box-shadow: 0 2px 0 var(--accent); }

.report-option-draggable { cursor: grab; user-select: none; -webkit-user-select: none; }
.report-option-draggable:active { cursor: grabbing; }
.report-option-draggable .report-option-label { cursor: pointer; }
.report-option-draggable input, .report-option-draggable img { -webkit-user-drag: none; }

/* ---- Configurator: recommended vs selected upright protection ---- */
.protection-advice { margin-top: var(--space-3); }
.protection-advice-box { padding: 10px 12px; border: 1px solid var(--border-strong); border-left: 3px solid var(--accent); border-radius: var(--radius-sm, 6px); font-size: 0.85em; line-height: 1.5; background: var(--bg-elevated); }
.protection-advice-box.is-warn { border-left-color: var(--warn); }
.protection-advice-box.is-ok { border-left-color: var(--ok); }
.protection-advice-title { font-family: var(--font-mono); font-size: 0.78em; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px; }
.protection-advice-title em { font-style: normal; color: var(--warn); }
.protection-advice-diff { margin: 2px 0 6px; font-weight: 600; }
.protection-advice-box.is-warn .protection-advice-diff { color: var(--warn); }
.protection-advice-box.is-ok .protection-advice-diff { color: var(--ok); }
.protection-advice-foot { margin-top: 6px; font-size: 0.82em; color: var(--text-faint); }
.protection-advice-note { margin: 0 0 8px; font-size: 0.82em; }
.protection-advice-note.is-warn { color: var(--warn); }

/* ---- Rack Protection (compact, three areas) ---- */
.rack-protection-title { font-size: 0.95em; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: var(--space-2); }
.rack-protection-area { padding: var(--space-3) 0; border-top: 1px solid var(--border-strong); }
.rack-protection-sub { margin: 0 0 var(--space-2); font-family: var(--font-mono); font-size: 0.78em; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }
.rack-protection-check { display: flex; gap: 8px; align-items: flex-start; margin-top: var(--space-3); font-size: 0.88em; cursor: pointer; }
.rack-protection-check input { margin-top: 3px; }
.protection-advice-row { display: flex; justify-content: space-between; gap: 8px; }
.protection-advice-row .is-muted { color: var(--text-muted); }
.protection-advice-box.is-info { border-left-color: var(--border-strong); }
.protection-advice-box.is-stale { border-left-color: var(--warn); opacity: 0.8; }
.protection-advice details { margin-top: 4px; font-size: 0.82em; color: var(--text-muted); }
.protection-summary { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; font-size: 0.88em; }
.protection-summary dt { color: var(--text-muted); }
.protection-summary dd { margin: 0; font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; }
.layout-stale-note { display: block; flex-basis: 100%; padding: 8px 10px; border-left: 3px solid var(--warn); background: var(--bg-elevated); font-size: 0.82em; line-height: 1.45; }
.layout-stale-note strong { display: block; color: var(--warn); }

/* ---- RAMS page: status, acknowledgement, Coming Soon ---- */
.rams-ack { margin-top: var(--space-3); }
.rams-ack-title { margin: 0 0 6px; font-family: var(--font-mono); font-size: 0.78em; font-weight: 800; letter-spacing: 0.06em; color: var(--text-muted); }
.rams-ack-text { margin: 0 0 var(--space-3); padding: 8px 14px; border-left: 3px solid var(--border-strong); font-size: 0.9em; line-height: 1.5; }
.rams-ack-text p { margin: 0 0 6px; }
.rams-ack-check { display: flex; gap: 8px; align-items: flex-start; margin-bottom: var(--space-3); font-size: 0.9em; cursor: pointer; }
.rams-ack-check input { margin-top: 3px; }
.rams-ack-record { margin: var(--space-3) 0 0; font-size: 0.85em; color: var(--text-muted); }
.status-pill.status-pill-ok { color: var(--ok); }
.rams-soon { border: 1px dashed var(--accent); background: var(--bg-elevated); text-align: left; }
.rams-soon-tag { display: inline-block; margin: 0 0 6px; padding: 3px 10px; border: 1px solid var(--accent); border-radius: var(--radius-pill); color: var(--accent); font-family: var(--font-mono); font-size: 0.78em; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; }
.rams-soon-title { margin: 0 0 6px; font-size: 1.5em; letter-spacing: 0.04em; text-transform: uppercase; }
.rams-soon-text { margin: 0; color: var(--text-muted); max-width: 60ch; }

.install-kitform { margin-top: var(--space-3); }
.install-kitform .segmented { margin-top: 4px; }

/* Weekend labour: full-width block, Saturday and Sunday side by side (stacks on narrow screens). */
.install-weekend-block { grid-column: 1 / -1; display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4) var(--space-6); padding-top: var(--space-3); border-top: 1px solid var(--border-strong); }
.install-weekend-block[hidden] { display: none; }
.install-weekend-block .install-weekend-warning { grid-column: 1 / -1; }
@media (max-width: 720px) { .install-weekend-block { grid-template-columns: 1fr; } }

/* Warehouse Planner — corner resize handle on the selected item */
#whSvg .wh-resize-handle { fill: var(--accent); stroke: var(--bg, #fff); stroke-width: 1.5; cursor: nwse-resize; }

/* =====================================================================
   UI POLISH & CONSISTENCY OVERHAUL — shared, centralised rules.
   Appended last so it wins over per-page rules without touching them.
   ===================================================================== */

/* -- Job 4: save/status indicators — neutral when fine, colour only when it matters -- */
[data-save-status][data-save-state="saved"] { background: transparent; border-color: var(--border); color: var(--text-muted); }
[data-save-status][data-save-state="saved"] .status-pill-dot { background: var(--text-faint); }
.status-pill { font-variant-numeric: tabular-nums; }

/* -- Job 5: type scale — ordinary UI labels never below 12px; numbers align -- */
.layout-status-item label,
.hub-metric-label, .hub-metric-sub,
.hub-prog-text, .hub-prog-state, .hub-closeout-step-label,
.hopack-readiness-tile-lbl, .hopack-list-head,
.docmod-jt-tracker-stat span, .rackid-status-pill, .hub-dash-status-pill { font-size: max(12px, 0.78em); }
.layout-status-item strong, .hub-metric-value, .docmod-jt-tracker-stat b,
.hopack-readiness-tile-val, .app-topbar .status-pill { font-variant-numeric: tabular-nums; }

/* -- Job 6: buttons — one primary (.btn-accent), quiet secondary (.btn-ghost), touch-friendly -- */
.btn:disabled, .btn[aria-disabled="true"], .btn.is-loading { opacity: .5; cursor: not-allowed; pointer-events: none; }
.btn.is-loading { cursor: progress; }
.app-topbar .btn { min-height: 40px; }
.app-topbar-right .btn-accent { max-width: 100%; }
.install-continue-label { overflow: hidden; text-overflow: ellipsis; }
.hub-next-bar-btn { min-height: 40px; padding: 8px 18px; font-size: 0.9em; }

/* -- Job 3: responsive shell fixes -- */
.app-topbar { flex-wrap: wrap; row-gap: var(--space-2); }
.app-topbar-left { flex: 1 1 auto; }
.app-topbar-right { flex-wrap: wrap; justify-content: flex-end; }
.layout-status-bar { flex-wrap: wrap; row-gap: var(--space-2); min-width: 0; }
.layout-status-group { flex-wrap: wrap; min-width: 0; row-gap: var(--space-2); }
.layout-status-group-zoom { flex: none; margin-left: auto; }
.canvas-zoom-inline { position: static !important; }
.docmod-jt-tracker-canvas-wrap, .docmod-panel, .report-table-wrap { max-width: 100%; }
.hub-dash-progress { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; }
.hub-dash-prog-step { min-width: 76px; }
.hub-prog-text { white-space: normal; text-align: center; overflow-wrap: anywhere; }
@media (max-width: 1100px) {
  .app-topbar-right .btn-accent { padding-inline: 14px; }
}
@media (max-width: 900px) {
  .docmod-panel table, .bom-table, .report-table { display: block; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  /* Single compact header row: title shrinks/ellipsises, actions scroll rather than stack. */
  .app-topbar { flex-wrap: nowrap; padding-inline: var(--space-3); gap: var(--space-2); }
  .app-topbar-title { max-width: 38vw; }
  .app-topbar-right { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; min-width: 0; }
  .app-topbar-right::-webkit-scrollbar { display: none; }
  .app-topbar-right > * { flex: none; }
  .app-topbar-sep { display: none; }
  .layout-status-bar { display: grid; grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3); }
  .layout-status-group { display: contents; }
  .layout-status-sep { display: none; }
  .layout-status-item { flex-direction: column; align-items: flex-start; gap: 0; }
  .layout-status-item label { display: block; font-size: 12px; }
  .layout-status-group-zoom { grid-column: 1 / -1; display: flex; justify-content: center; margin: 0; }
}
@media (max-width: 640px) {
  .layout-status-item label { display: block; }
}

/* -- Job 7: page widths by module type -- */
.install-main-inner { max-width: 1400px; margin-inline: auto; }
.docmod-main-inner { margin-inline: auto; }
.docmod-main-inner:has(.docmod-jt-tracker-panel) { max-width: 1400px; }
.docmod-main-inner:has(#hpReadinessPanel) { max-width: 960px; }
.docmod-main-inner:has(#ramsFileInput) { max-width: 1040px; }

/* -- Job 8: Handover readiness states (neutral unless genuinely blocking) -- */
.hopack-readiness-tile { border-left: 3px solid var(--border-strong); }
.hopack-readiness-tile-state { font-size: 12px; font-weight: 700; color: var(--text-faint); }
.hopack-state-complete { border-left-color: var(--ok); }
.hopack-state-complete .hopack-readiness-tile-state { color: var(--ok); }
.hopack-state-warning { border-left-color: var(--warn); }
.hopack-state-warning .hopack-readiness-tile-state { color: var(--warn); }
.hopack-state-blocking { border-left-color: var(--danger); background: var(--danger-soft); }
.hopack-state-blocking .hopack-readiness-tile-state { color: var(--danger); }
.hopack-next-action { margin: 0 0 var(--space-3); font-size: 0.88em; font-weight: 600; color: var(--text); }

/* -- Job 9: planned (unreleased) features — subtle, never a large card -- */
.rams-soon { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin: 0; padding: var(--space-2) var(--space-3); border: 1px dashed var(--border-strong); border-radius: var(--radius-md); background: transparent; color: var(--text-muted); font-size: 0.86em; }
.rams-soon-tag { margin: 0; padding: 1px 8px; font-size: 12px; letter-spacing: .04em; color: var(--text-faint); border-color: var(--border-strong); }
.rams-soon-text { margin: 0; max-width: none; }
.app-sidebar-link-soon { font-size: 11px; }

/* -- Job 10: Job Tracker empty drawing area, shared empty-state, scrollbars -- */
.docmod-jt-tracker-canvas-wrap { background: var(--bg-elevated-2); min-height: 0; }
.docmod-jt-tracker-canvas-wrap #jtTrackerSvg { max-height: 70vh; }
.docmod-jt-tracker-canvas-wrap .docmod-empty { margin: var(--space-4); }
.docmod-empty { background: var(--bg-elevated-2); }
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.home-hero-chain-planned { font-size: 12px; font-weight: 600; color: var(--text-faint); }

/* -- Final pre-deployment fix: tablet/mobile overlaps (Layout stats, Install actions, Hub export) --
   Controls now wrap instead of being forced onto one clipped row; nothing is hidden or scrolled away. */
@media (max-width: 900px) {
  .app-topbar { flex-wrap: wrap; row-gap: var(--space-2); }
  .app-topbar-left { flex: 1 1 100%; min-width: 0; }
  .app-topbar-title { max-width: 100%; }
  .app-topbar-right { flex: 1 1 100%; flex-wrap: wrap; overflow: visible; justify-content: flex-start; row-gap: var(--space-2); }
  .app-topbar-right > * { flex: 0 0 auto; min-width: 0; }
  .install-continue-label { overflow: visible; white-space: nowrap; }
  .layout-status-bar { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); overflow: visible; }
  .layout-status-item { min-width: 0; }
  .layout-status-item strong { font-size: 1.1em; overflow: visible; text-overflow: clip; white-space: normal; overflow-wrap: anywhere; max-width: 100%; }
  .layout-status-item label { white-space: normal; line-height: 1.2; }
  .hub-topbar { flex-wrap: wrap; row-gap: var(--space-2); }
  .hub-topbar-search { flex: 1 1 100%; max-width: none; }
  .hub-topbar-actions { flex-wrap: wrap; margin-left: 0; width: 100%; }
}

/* Sidebar labels (e.g. "Inspection") wrap rather than clip at narrow supported widths. */
.app-sidebar-link { gap: 8px; padding-inline: 10px; }
/* The label is never truncated or wrapped; the badge is compact and never shrinks. */
.app-sidebar-link-label { overflow: visible; text-overflow: clip; white-space: nowrap; overflow-wrap: normal; min-width: 0; flex: none; }
.app-sidebar-link-soon, .app-sidebar-link .modgate-badge { flex: none; white-space: nowrap; margin-left: auto; font-size: 9px; letter-spacing: .03em; padding: 1px 5px; }
.app-sidebar-nav { overflow-x: hidden; }
/* Desktop: a little extra width so "Inspection" + its "Coming Soon" badge both fit on one line. */
@media (min-width: 1101px) { .app-sidebar { width: 228px; } #sidebarCollapse:checked ~ .app-shell-body .app-sidebar { width: 60px; } }
