/*
 * RackMapper-EP — 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; }
.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); }

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

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

@media (max-width: 900px) {
  /* The top bar's own controls carry the page at phone widths, and the
     sidebar's module links would otherwise eat most of a small screen —
     hide it rather than squeeze the canvas further. */
  .app-sidebar { display: 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);
  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; }
.status-pill-success { background: rgba(34, 197, 94, 0.14); border-color: rgba(34, 197, 94, 0.4); color: #22c55e; }
.status-pill-success .status-pill-dot { background: #22c55e; }

@media (max-width: 640px) {
  .app-topbar { padding: var(--space-3) var(--space-4); 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 — master-reference structure: left Tools +
   Site Survey, centre dominant drawing, right Warehouse Dimensions /
   Selected Object. Three-column row instead of the old canvas-first/
   floating-rail approach; the canvas still gets the majority of the
   width (flex:1 on the centre column, the two side columns fixed).
--------------------------------------------------------------------- */
.wh-workspace { flex: 1; min-height: 0; display: flex; overflow: hidden; }
.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-right: 1px solid var(--border);
  background: var(--bg-elevated);
  display: flex;
  flex-direction: column;
}
/* 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); }

/* 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 {
  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;
}
.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); }
.health-status-pill.health-error { background: rgba(220, 38, 38, 0.12); border-color: rgba(220, 38, 38, 0.4); color: #dc2626; }
.health-status-pill.health-error .status-pill-dot { background: #dc2626; }

/* 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-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;
}
.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);
  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;
}
.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;
  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%;
}
.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). */
.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;
}
/* ---- 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.
   Left-hand position (RackMapper concept reference — Racking is the
   shell's first implementation) instead of the previous right-hand
   rail, so border sits on the right edge now. ---- */
.rack-spec-rail {
  flex: none;
  width: 324px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-6);
  border-right: 1px solid var(--border);
  background: var(--bg-elevated);
  /* 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; }
/* Components zone (Batch 3 hierarchy pass) — Decking + Protection sit on
   a quiet recessed surface so they read as one visually distinct group of
   optional physical additions, separate from the core geometry/pallet
   fields above. Reuses --bg-sunken (already .swatch-card's own resting
   surface) so a swatch card's border is still visible against it — no new
   colour introduced for this. */
.rack-spec-components {
  margin-top: var(--space-7);
  padding: var(--space-5);
  border-radius: var(--radius-md);
  background: var(--bg-sunken);
}
.rack-spec-components .swatch-card { background: var(--bg-elevated); }
.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; }

/* Primary bar — Layout-density pass: one compact HORIZONTAL strip
   instead of three stacked full-width rows, so the live preview above
   keeps as much of the stage's height as possible. Six clearly
   separated blocks left to right — Layout Quantities, Decking, Column
   Protection, Guards / Deflectors, End-of-Rack Barriers, Layout Summary
   — each with its own small uppercase label, divided by a hairline
   border instead of the old row-over-row horizontal dividers. Wraps
   onto a second line only if the viewport genuinely can't fit
   everything; never scrolls internally. Controls within it are sized a
   touch larger than the left rail's own compact steppers/swatches (see
   below) since this bar no longer has to fit three rows in the same
   vertical space. */
.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; }
.primary-bar-row-groups { display: flex; align-items: flex-start; gap: var(--space-6); }
.rack-primary-bar .sheet-section-title { margin-bottom: 6px; font-size: 0.74em; }
.primary-group { flex: none; }
.rack-primary-bar .stepper-sm .big-stepper-btn { width: 34px; height: 34px; }
.rack-primary-bar .stepper-sm .big-stepper-value { font-size: 1.2em; min-width: 2.4ch; }

/* Decking / Column Protection — a compact horizontal pill (icon beside
   the label, single line) instead of the taller stacked-glyph card
   Pallet Type still uses in the left rail, so this row stays short
   while the label text stays full-size and readable. 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; 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 first in the DOM now (left position on desktop, so
     keyboard/tab order matches its visual position there); `order`
     restores "preview first" stacking here on mobile only. */
  .rack-stage { flex-direction: column; overflow-y: auto; }
  .rack-stage-main { flex: none; order: 1; }
  .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-right: none; border-top: 1px solid var(--border); order: 2; }

  /* Bottom action strip (Layout Quantities / Decking / Column Protection /
     Guards / Deflectors / End-of-Rack Barriers / Layout Summary) — on
     desktop this is a single wrapping flex row of blocks, but each block
     is `flex: none` and sized to its own content (e.g. the 3-stepper
     Layout Quantities block, or the Decking/Column Protection pill
     rows), so wrapping the ROW doesn't help once a single block's own
     content is wider than the phone viewport — that block just overflows
     off the right edge instead, reading as "can't see the Single Runs
     stepper" or "can't see all the Decking/Column Protection buttons".
     Stacking each block full-width and letting its own internal groups
     wrap fixes this without touching the desktop layout above 900px. */
  .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; }
  .primary-bar-row-groups { flex-wrap: wrap; row-gap: var(--space-3); }
  #deckingGroup, #columnProtectionSeg { flex-wrap: wrap; 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"] {
  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 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 { 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; }
.breakdown-group-title { font-family: var(--font-display); font-size: 0.78em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); margin: 0 0 var(--space-3); }
.breakdown-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); padding: 6px 0; 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); }

/* 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; }
  /* 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;
    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; }
}
@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); 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-bottombar { border-bottom: none; border-top: 1px solid var(--border); justify-content: space-between; flex-wrap: wrap; }
.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); 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); 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; }

.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; }
}

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

/* ---- Dashboard hero (Aug 2026) — compact, above-the-fold hero: headline
   + real project stats on the left, the actual view3d.js 3D renderer on
   the right (js/home-3d-preview.js, same "Example"-badged real-renderer
   technique Row 4 of the Product Showcase below already uses). Sits on
   the same fixed #homeBgScene dark wash the rest of the pre-#product page
   does, so it reuses .home-hero-title/.home-hero-sub/.home-hero-actions/
   .home-capstat-bar verbatim rather than new colour rules — those are
   already correct against that permanently-dark background in both
   themes (see the Hero comment just below for why). .home-showcase-visual
   (panel chrome) and .home-iso-wrap (the canvas itself) are the same
   components Row 4 already uses, unmodified. ---- */
/* Sized ~18% larger than the base --space tokens (calc() off the token
   rather than jumping a whole step on the scale, which would have been a
   ~50% jump) so the hero fills more of the page above the fold, per
   feedback after the first pass shipped. Scoped to .home-dash-hero only —
   .home-iso-wrap and .home-hero-visual-bar below get their own scoped
   .home-dash-hero-prefixed overrides for the same reason, so Row 4 of the
   Product Showcase (which reuses those same base classes) is untouched. */
/* Bottom padding raised to --space-11 (168px, tokens.css's own "generous
   top/bottom breathing room" value) so there's a clearly bigger gap
   between the purple hero wash and #product's opaque black background
   below it, rather than the two nearly touching. */
.home-dash-hero { position: relative; padding: calc(var(--space-8) * 1.18) 0 var(--space-11); }
.home-dash-hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: 1fr 1fr; gap: calc(var(--space-8) * 1.18); align-items: center; }
.home-dash-hero-copy .home-capstat-bar { margin-top: 0; }
/* Small header row above the dash-hero's stat bar (Aug 2026, View Demo
   fix) — a muted uppercase label plus the shared "Example" badge
   (.home-hero-visual-badge, same class as every other illustrative panel
   on this page), toggled by renderDashStats() in index.html depending on
   whether a real project exists yet. Dash-hero-scoped only — the
   unrelated static Capability strip further down the page also uses
   .home-capstat-bar for its own layout, but never this header row. */
.home-capstat-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.home-capstat-head-label { font-size: 0.72em; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--text-faint); }

/* ---- Ambient hero mesh (Aug 2026) — pure CSS, no shader/canvas/JS
   dependency, adapted from a React/shader reference the project owner
   liked (see js/hero-visual-rotator.js's own header for the equivalent
   note on the rotator). Three soft, heavily blurred radial-gradient
   blobs in the app's existing purple/orange accent duo (same hex pair as
   tokens.css's dark-theme --accent/--graphic-2, hardcoded here rather
   than var()-driven for the same reason .home-hero-title/.home-hero-sub
   above are — this sits on the permanently-dark #homeBgScene wash, which
   never flips with the theme toggle), each drifting slowly via the same
   "position:absolute blurred radial-gradient + slow alternate drift"
   technique .hero-glow/.home-bg-scene-glow already use elsewhere in this
   file, just layered as three blobs instead of one for a subtle mesh-
   gradient feel. Sits at z-index:0, behind .home-dash-hero-grid's
   z-index:1 content; prefers-reduced-motion is already handled globally
   by style.css's `* { animation-duration: 0.001ms !important }` rule. */
.home-dash-hero-mesh { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.home-dash-hero-mesh-blob { position: absolute; border-radius: 50%; filter: blur(70px); will-change: transform; }
.home-dash-hero-mesh-blob-a { top: -14%; left: 2%; width: 460px; height: 460px; background: radial-gradient(circle, #8b5cf6, transparent 70%); opacity: 0.5; animation: homeHeroMeshDriftA var(--glow-duration) ease-in-out infinite alternate; }
.home-dash-hero-mesh-blob-b { top: 8%; right: -8%; width: 400px; height: 400px; background: radial-gradient(circle, #ff8a3d, transparent 70%); opacity: 0.3; animation: homeHeroMeshDriftB calc(var(--glow-duration) * 1.3) ease-in-out infinite alternate; }
.home-dash-hero-mesh-blob-c { bottom: -20%; left: 26%; width: 500px; height: 500px; background: radial-gradient(circle, #6d28d9, transparent 72%); opacity: 0.38; animation: homeHeroMeshDriftA calc(var(--glow-duration) * 0.85) ease-in-out infinite alternate-reverse; }
@keyframes homeHeroMeshDriftA {
  0%   { transform: translate(0, 0) scale(1); }
  100% { transform: translate(3%, 2%) scale(1.08); }
}
@keyframes homeHeroMeshDriftB {
  0%   { transform: translate(0, 0) scale(1); }
  100% { transform: translate(-4%, 3%) scale(1.05); }
}

/* Glass eyebrow pill (Aug 2026) — scoped to the dashboard hero only
   (other pages' plain-text .eyebrow is untouched). Translucent
   background + blurred backdrop over the new mesh above, in keeping with
   the shader reference's glass badge. backdrop-filter degrades gracefully
   (the translucent background/border alone still read fine) on the rare
   browser without support, so no extra fallback branch is needed — same
   "every visual step has a safe fallback" philosophy the rest of this
   codebase already follows. */
.home-dash-hero .eyebrow { position: relative; z-index: 1; padding: 7px 15px; border-radius: var(--radius-pill); background: rgba(255, 255, 255, 0.07); border: 1px solid rgba(255, 255, 255, 0.16); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
/* "Info tab" (the 3-dot/title/"Example"-badge chrome strip atop the 3D
   panel) made taller/larger here rather than in the shared
   .home-hero-visual-bar rule, and the panel itself sized up to match —
   both scoped to the dashboard hero so Row 4's showcase panel keeps its
   original size. */
.home-dash-hero .home-hero-visual-bar { padding: calc(var(--space-3) * 1.4) calc(var(--space-4) * 1.15); }
.home-dash-hero .home-hero-visual-dot { width: 9px; height: 9px; }
.home-dash-hero .home-hero-visual-title { font-size: 0.86em; }
.home-dash-hero .home-hero-visual-badge { font-size: 0.74em; padding: 4px 11px; }
/* 3D Visualisation hero slide only (Aug 2026 follow-up) — the shared
   crossfade holder (.home-hero-visual-rotator) is now sized in JS to fit
   whichever of the six slides is tallest (see js/hero-visual-rotator.js),
   which is usually a taller slide like Installation Estimate or Project
   Report, not this one. This box's own aspect-ratio/min-height (inherited
   from the base .home-iso-wrap rule) stayed unchanged on purpose — it's
   what keeps the 3D view correctly framed, matching the camera fit
   js/home-3d-preview.js calibrated for, so widening/heightening it
   further would start cropping the model rather than showing more of it.
   What was missing was centring: margin-top/bottom:auto on a flex column
   child (.home-hero-visual-slide is display:flex;flex-direction:column)
   shares any leftover holder height evenly above and below this one
   slide's content instead of leaving it all bunched underneath, without
   touching the info bar above it or any of the other five slides. */
.home-dash-hero .home-iso-wrap { min-height: 307px; max-height: 560px; margin-top: auto; margin-bottom: auto; }
@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); }
  .home-dash-hero .home-iso-wrap { min-height: 260px; max-height: 420px; margin-top: auto; margin-bottom: auto; }
}
@media (max-width: 900px) {
  .home-dash-hero { padding: var(--space-7) 0 var(--space-9); }
}

/* ---- Hero — large centred headline, real New/Open actions, then a
   full-width illustrative product visual underneath (reusing .hero-glow
   from style.css for the ambient background wash). ---- */
.home-hero { position: relative; overflow: clip; padding: var(--space-10) 0 var(--space-8); }
.home-hero-inner { position: relative; z-index: 1; text-align: center; max-width: 800px; margin: 0 auto; }
/* Colour deliberately hardcoded, not var(--text) — the hero (and the
   Workflow section head just below it, see #workflow rules further
   down) sit directly on #homeBgScene, a fixed dark purple/near-black
   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
   was rendering unreadable dark text on that permanently-dark
   background — these values are the exact same ones dark mode's own
   --text/--text-muted already resolve to, so dark mode is pixel-
   identical; only Light Mode changes. */
/* display:inline-flex + column here (rather than the plain block h1 this
   used to be) so the box shrink-wraps to its widest line — always
   "Plan it. See it." itself, never the shorter rotating phrases — and
   .home-hero-title-line-rotator can then be centred WITHIN that measured
   width via align-self:stretch + text-align:center. Fixes the rotator
   sitting flush left under "Plan it. See it." instead of centred beneath
   it, which is what it always visually needs to be regardless of the
   surrounding hero copy's own left alignment (eyebrow/sub/actions are
   unaffected — this only changes how the h1's own two lines relate to
   each other). */
.home-hero-title { display: inline-flex; flex-direction: column; align-items: flex-start; font-size: clamp(2.3em, 5vw, 3.8em); font-weight: 700; line-height: 1.08; letter-spacing: -0.02em; margin: 0 0 var(--space-5); color: #f5f4f7; }
.home-hero-title-line { display: block; }
.home-hero-title-line-rotator { align-self: stretch; text-align: center; }

/* ---- Headline rotator (vanilla, no framework) — js/hero-rotator.js
   toggles is-current/is-exit; every visual is expressed here as plain
   CSS transitions on transform/opacity, eased with the app's existing
   --ease-out curve (the same "smooth, not bouncy" cubic-bezier used
   elsewhere, e.g. the hero blueprint's boundary draw-in). inline-flex +
   an explicit (JS-measured) width keeps the container's box a fixed
   size regardless of which phrase is showing, so a phrase change never
   reflows the h1, the buttons below it, or the blueprint further down
   the page — the one hard requirement of this effect. vertical-align:
   bottom keeps it sitting on the same baseline as the static first line
   above it (no js — a plain <span> at default line-height already
   would; this only matters once overflow:hidden + position:relative are
   added below). ---- */
.home-hero-rotator {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 1.22em;
  /* CSS-only fallback width (~"Hand it over.", the longest phrase) so the
     no-JS/pre-JS render is already correctly sized — position:absolute
     children can't contribute to an intrinsic width, so this can't be
     "auto". js/hero-rotator.js measures the real phrases on load and
     overwrites this with an exact pixel value; the ch-based guess is
     close enough that the swap is not noticeable. */
  width: 13ch;
  overflow: hidden;
  vertical-align: top;
}
.home-hero-rotator-phrase {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(0.55em);
  transition: transform 600ms var(--ease-out), opacity 600ms var(--ease-out);
}
.home-hero-rotator-phrase.is-current { opacity: 1; transform: translateY(0); }
.home-hero-rotator-phrase.is-exit { opacity: 0; transform: translateY(-0.55em); }

@media (prefers-reduced-motion: reduce) {
  /* Belt-and-braces alongside js/hero-rotator.js's own matchMedia check:
     even if a phrase were left mid-transition, this guarantees no motion
     and no overlap — only the current phrase is ever visible. */
  .home-hero-rotator-phrase { transition: none; transform: none; }
}

.home-hero-sub { font-size: 1.12em; color: #9c98ab; max-width: 640px; margin: 0 auto var(--space-6); line-height: 1.6; }
.home-hero-actions { position: relative; z-index: 2; pointer-events: auto; display: flex; align-items: center; justify-content: center; gap: var(--space-3); margin-bottom: var(--space-8); 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 RackMapper
     purple, so a script failure just freezes the opening brand moment
     rather than showing a broken/blank background. */
  --home-bg-h: 262;
  --home-bg-s: 46;
  --home-bg-l: 15;
  /* 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; }
}
.home-product-card { display: flex; flex-direction: column; }
/* 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); }

/* ---- 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 { width: 8px; height: 8px; border-radius: 50%; background: var(--border-strong); flex: none; }
.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: var(--space-2) 0; }
  .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; }
}
