/* dock.css — The sticky dock: .sheet__dock, the primary .cta, the split button and its menu.
   Cascade position 9 of 18. Order is load-bearing and is asserted by
   verify-fixtures.mjs; index.html links these explicitly (never @import) because Caddy's
   try_files turns a missing file into a 200 text/html SPA shell.
   Owner after Step 1: section (c).

   NOTE ON `.cta`: the base `.cta` rule below is CONSUMED by the passcode gate, the auth
   dialog (section d) and both feedback panels (section m). Section (c) restyled only the
   dock-scoped selectors — `.dock__row`, `.cta--caret`, `.dock__menu` — so nothing outside
   the dock repaints. */
.sheet__dock {
  flex: 0 1 auto; min-height: 0; padding: 12px 16px 14px; border-top: 1px solid var(--line);
  background: rgb(var(--sn-surface-1-rgb) / 0.9490196078431372); backdrop-filter: blur(10px);
}

/* ══ Room for the dock, reserved by the dock ═══════════════════════════════════════
   The dock is `position: sticky; bottom: 0` as the last child of `.sheet__scroll`
   (the five position declarations live in css/layout.css and are filed back to this
   section), so anything a scrollIntoView brings to the bottom of that scroller lands
   UNDERNEATH it. MEASURED 12 Sep 2026 on staging at 1718x877: the dock covered
   #generationFeedbackSubmit, and scrolling ~130 px by hand was the only way to reach
   Send.

   The first fix for that was `padding-bottom: 112px` on `.generation-feedback.on`, and
   it was wrong three ways: 112 px is ONE measurement of a dock whose height varies
   (#dockConsent appears for a repair refusal, and .cta__note wraps to two or three
   lines on a narrow rail); the feedback panel is not the last child before the dock, so
   #notice and #loadNotice got nothing; and the padding sat inside the panel's own
   `border-bottom`, so an open panel drew a permanently empty bordered box under Send,
   including at full scroll where the sticky dock has come to rest inline and there was
   never an overlap at all.

   `scroll-padding-bottom` is the structural answer: it changes only where the SCROLLER
   stops, paints nothing, costs no layout box, and covers every scrollIntoView in the
   rail rather than one panel. The clearance is built from the dock's own parts instead
   of being measured once in one state — every term is a real element with a real
   worst case, so a change to the dock is a change to this sum, in the dock's own file.
   It is deliberately generous: over-reserving costs a little extra scroll travel,
   under-reserving hides the button. */
:root {
  --sn-dock-clearance: calc(
    2 * var(--sn-space-3)  /* .sheet .sheet__dock's own vertical padding */
    + 46px                 /* .dock__row — one .cta at 14.5px with .85em padding */
    + 46px                 /* #dockConsent — checkbox plus two wrapped lines and 10px */
    + 60px                 /* .cta__note — 8px margin plus up to three wrapped lines */
  );
}
.sheet__scroll { scroll-padding-bottom: var(--sn-dock-clearance); }

.cta {
  width: 100%; padding: .85em 1em; border: 0; border-radius: 11px;
  background: var(--signal); color: var(--signal-ink); font-weight: 700; font-size: var(--sn-text-md);
  display: flex; align-items: center; justify-content: center; gap: 8px;
  transition: filter .2s ease, opacity .2s ease;
}
.cta:hover:not(:disabled) { filter: brightness(1.08); }
/* item 76. `opacity: .35` composited the label to rgb(11,25,21) on rgb(11,81,53) —
   1.93:1 — and the button to 2.06:1 against the page, on the single most important
   element in the empty state. The file had already solved this once: `.cta.is-warn`
   twenty lines below draws an explicit outline and keeps its label at full strength,
   because amber under 35% went muddy brown and looked like a rendering fault. Green got
   the fade and nobody noticed, because a dimmed green still reads as "green button".
   MEASURED after: label 7.23:1 on its own fill, button 3.01:1 against the rail. The
   outline value is a correction to DESIGN-SPEC §4.14 — see css/tokens.css. */
.cta:disabled {
  background: var(--sn-accent-disabled-bg);
  color: var(--sn-accent-disabled-ink);
  box-shadow: inset 0 0 0 1.5px var(--sn-accent-disabled-edge);
  cursor: not-allowed;
}
.cta__note { margin-top: 8px; font-size: 12px; color: var(--dim2); text-align: center; line-height: 1.4; }
.cta__note.is-ok { color: var(--sn-ok-soft); }
.cta__note.is-warn { color: var(--warn); }

/* ══ The split control (items 3, 5) ═══════════════════════════════════════════════
   MEASURED BEFORE: `gap: 6px` with the caret wrapped in a plain `.dock__more` block —
   `align-items: stretch` stretched the WRAPPER, and the `height: auto` caret inside it
   computed to 16 + 2x12.325 = 40.65 px beside a 44.95 px primary. Two objects, 4.3 px
   out of line, with a notch at the bottom right.
   AFTER: `gap: 0`, the caret is a direct flex child, and `align-self: stretch` with
   `height: auto` makes it exactly as tall as whichever primary is showing. One radius
   across the pair, mirrored at the seam, so the two halves read as one object
   (REFERENCE-STUDY §4.2, MakerWorld's 479x48 `Download STL/CAD Files`). */
.dock__row { display: flex; gap: 0; align-items: stretch; }
/* MEASURED trap: the caret IS a `.cta`, so a bare `.dock__row > .cta { flex: 1 1 auto }`
   (specificity 0,2,0) beats `.cta--caret { flex: 0 0 44px }` (0,1,0) and the caret came
   out 39.63 px wide instead of 44. The grow rule excludes it explicitly. */
.dock__row > .cta { min-width: 0; text-decoration: none; }
.dock__row > .cta:not(.cta--caret) { flex: 1 1 auto; }
.cta--download { color: var(--signal-ink); }
/* Only when the caret is actually on screen does the primary give up its right radius.
   In `idle`, `ready`, `working` and `failed` there is no menu, and the primary is a
   plain full-width pill exactly as before. */
.dock__row:has(> .cta--caret:not([hidden])) > .cta:not(.cta--caret) {
  border-radius: 11px 0 0 11px;
}
/* Amber, and generic rather than download-only: the one primary that can build a
   file nobody has checked must never wear the same green as a proven one. */
.cta.is-warn { background: var(--warn); color: var(--sn-warn-ink-on-fill); }
/* Locked, not broken. Amber under the ordinary 35% disabled opacity goes muddy
   brown and reads as a rendering fault, so the locked state is drawn on purpose:
   outline and label at full strength, because the whole point is that the user
   reads what the tick above will unlock before they tick it. */
.cta.is-warn:disabled {
  opacity: 1; background: rgb(var(--sn-warn-rgb) / 0.10980392156862745); color: var(--warn);
  box-shadow: inset 0 0 0 1.5px rgb(var(--sn-warn-rgb) / 0.4); cursor: not-allowed;
}
/* The door out of a repair refusal. No card and no border on purpose: the tick
   and its sentence sit directly against the button they unlock, because one more
   bordered box in this rail is how a consequence gets skimmed past. */
.consent {
  display: grid; grid-template-columns: 16px 1fr; gap: 9px; align-items: start;
  margin: 0 0 10px; font-size: 12px; line-height: 1.45; color: var(--warn); cursor: pointer;
}
.consent input { width: 15px; height: 15px; margin: 1px 0 0; accent-color: var(--warn); cursor: pointer; }

/* The caret half: a FIXED 44 px column (REFERENCE-STUDY §4.3 — the caret is a fixed
   narrow column, never a percentage), the mirrored radius, and no padding of its own so
   its height is whatever the row stretches it to. `anchor-name` is what the menu below
   hangs off. */
.cta--caret {
  flex: 0 0 44px; width: 44px; padding: 0; align-self: stretch; height: auto;
  position: relative;
  border-radius: 0 11px 11px 0;
  background: var(--sn-accent-tint-hi); color: var(--signal);
  anchor-name: --sn-dock-more;
  transition: background-color var(--sn-dur-fast) var(--sn-ease-out);
}
/* The seam. DESIGN-SPEC §4.16: 1px x 22px (50% of the 44 px control) in black at 25%,
   inset vertically rather than a full-height edge, so it reads as one object divided
   rather than two objects abutting (REFERENCE-STUDY §4.2: MakerWorld's rule is 28 of
   48 px and drawn in the foreground colour, not a border tint). */
.cta--caret::before {
  content: ''; position: absolute; left: 0; top: 50%; translate: 0 -50%;
  width: 1px; height: 22px; background: rgb(var(--sn-black-rgb) / .25);
}
/* item 7. `.cta:hover { filter: brightness(1.08) }` does apply to the caret, but
   brightening a 26 %-alpha tint over a near-black rail is not perceptible — MEASURED
   composite moves rgb(45,66,55) to rgb(48,71,59), a 1.05:1 step. The hover is therefore
   a real fill change (.26 -> .34 alpha) and the inherited brightness filter is switched
   off so hover changes fill and NOTHING else (REFERENCE-STUDY §18). */
.cta--caret:hover:not(:disabled) {
  filter: none;
  background: rgb(var(--sn-accent-rgb) / .34);
}
.cta--caret[aria-expanded="true"] { background: rgb(var(--sn-accent-rgb) / .40); }
/* The ring sits INSIDE the pill, so it never straddles the seam or the row's edge. */
.cta--caret:focus-visible { outline: 2px solid var(--sn-focus); outline-offset: -3px; }

/* ══ The menu (items 1, 2, 3, 4, 7, 8) ════════════════════════════════════════════
   MEASURED BEFORE: fill `rgb(var(--sn-surface-2-rgb) / .8)` composited to rgb(21,21,25)
   over a rgb(17,17,22) backdrop — 1.03:1 — with `backdrop-filter: blur(12px)` that was a
   NO-OP, because `.sheet__dock` is itself a Backdrop Root and the menu sits above the
   dock where that root has painted nothing. The "Bed 35 to 60 °C" row of the run sheet
   was legible straight through the menu's own label.
   AFTER: an opaque `--sn-surface-3` (#1e1e24) surface, no backdrop-filter at all, and the
   hairline delivered as stop 1 of the box-shadow with NO `border` property, so no floating
   surface in this app can show a doubled edge (COMPONENT-PLAN §0.1 rule 11).

   Why that does not contradict DESIGN-SPEC §2.2's "borders are `border: 1px solid` —
   never `box-shadow: inset`": §2.2 is about BOXES IN THE LAYOUT, where a real border is
   needed for `align-items: stretch` and radius inheritance — it is exactly why the caret
   above measured 4.3 px short. A top-layer popover participates in no layout at all, so
   the elevation grammar (§0.1 rule 11, §2.3, §10.3) governs instead.

   Width: NOT §4.16's `width: max-content; min-width: 100%` — mocks/NOTES.md B3 measured
   that at 508 px, because the containing block is the full-width dock row and min-width
   beats max-width. That is a worse version of item 3. */
.dock__menu {
  position: fixed; inset: auto; margin: 0;
  width: max-content; min-width: 200px; max-width: 320px;
  padding: var(--sn-space-1);
  border: 0;
  border-radius: 11px;
  background: var(--sn-surface-3);
  color: var(--ink);
  box-shadow: 0 0 0 1px var(--sn-line-raised), var(--sn-shadow-raised);
  overflow: visible;
}
/* Right edges flush with the trigger, 6 px above it, flipping below only if there is no
   room above (item 3). The JS fallback in js/dock.js does the same arithmetic once, at
   open time, where `anchor-name` is unsupported. */
@supports (anchor-name: --sn-probe) {
  .dock__menu {
    position-anchor: --sn-dock-more;
    position-area: block-start span-inline-start;
    margin-bottom: 6px;
    position-try-fallbacks: flip-block;
  }
}
.dock__menu:popover-open {
  animation: dock-menu-in var(--sn-dur-fast) var(--sn-ease-out);
}
@keyframes dock-menu-in { from { opacity: 0; translate: 0 4px } }
/* item 4: one line, always. 36 px tall at 13.5px/20px, `white-space: nowrap`, and the
   menu is sized by its content up to 320 px so the longest label the dock can produce
   ("Prepare it again with the current settings") still fits on one line.
   item 8: the row used to transition the `all` keyword, which animates width, padding and
   height as well as colour, so any state change could produce a reflow animation. Only the
   two colours that actually change are named now, and both are compositor-friendly. */
.dock__menu button, .dock__menu a {
  display: flex; align-items: center; width: 100%; height: 36px;
  padding: 0 var(--sn-space-3);
  border: 0; background: none; cursor: pointer;
  color: var(--ink); font-size: var(--sn-text-sm); line-height: 20px;
  text-align: left; white-space: nowrap; text-decoration: none;
  border-radius: var(--sn-radius-sm);
  transition: background-color var(--sn-dur-fast) var(--sn-ease-out),
              color var(--sn-dur-fast) var(--sn-ease-out);
}
.dock__menu button:hover, .dock__menu a:hover {
  background: rgb(var(--sn-white-rgb) / .06);
}
/* Regression review M1: a plate whose check was not a clean pass says so in amber, at
   full strength (DESIGN §2: never dimmed). */
.dock__menu button.is-warn, .dock__menu a.is-warn { color: var(--sn-warn); }
/* item 7: a keyboard user gets what a mouse user gets, plus a ring drawn INSIDE the row
   so it can never straddle the menu's own hairline. */
.dock__menu button:focus-visible, .dock__menu a:focus-visible {
  background: rgb(var(--sn-white-rgb) / .06);
  outline: 2px solid var(--sn-focus); outline-offset: -2px;
}

/* ══ UI Quiet — the floating viewport toolbar (DESIGN §5.4) ═════════════════════════════
   The dock moved into the toolbar in phase 1 (css/shell.css seats it). This restyles it:
   icon buttons at 32 px, then ONE pill (height 40, radius 12) holding the primary, a 1 px
   divider, the secondary label, and — when there is a menu — the caret. The pill carries
   the tone (accent, amber, disabled, working) and the controls inside it are transparent,
   so the three read as one object whatever state the dock is in.
   Selectors lead with #viewToolbar because css/shell.css comes later in the cascade and
   its `.vtb .dock__row` rules must lose on specificity, not on !important. */
.vtb__tool {
  display: grid; place-items: center; width: 32px; height: 32px; padding: 0;
  border: 0; border-radius: var(--sn-radius-sm); background: none; color: var(--sn-ink-dim); cursor: pointer;
  transition: background-color var(--sn-dur-fast) var(--sn-ease-out), color var(--sn-dur-fast) var(--sn-ease-out);
}
.vtb__tool:hover { background: var(--sn-surface-2); color: var(--sn-ink); }
.vtb__tool[aria-pressed="true"] { background: var(--sn-surface-3); color: var(--sn-ink); }
.vtb__tool:focus-visible { outline: 2px solid var(--sn-focus); outline-offset: -2px; }
/* Nothing on the bed, nothing to spin: the icons join the bar when a model does. */
html[data-route="empty"] #viewToolbarTools { display: none; }
#viewToolbarTools:not(:empty) { padding-right: var(--sn-space-1); }

#viewToolbar .vtb__bar { border-radius: var(--sn-radius-lg); }
#viewToolbar .dock__row {
  min-width: 0; min-height: 40px; align-items: stretch; align-self: center; max-width: 100%;
  border-radius: var(--sn-radius-md); overflow: hidden;
  background: var(--sn-accent); color: var(--sn-accent-ink);
  transition: background-color var(--sn-dur-fast) var(--sn-ease-out);
}
#viewToolbar .dock__row > .cta {
  width: auto; min-height: 40px; border-radius: 0; box-shadow: none; filter: none;
  background: transparent; color: inherit; font-size: var(--sn-text-sm); font-weight: 600;
}
#viewToolbar .dock__row > .cta:not(.cta--caret) { flex: 0 1 auto; padding: 0 var(--sn-space-4); }
#viewToolbar .dock__row > .cta--caret { flex: 0 0 36px; width: 36px; }
#viewToolbar .dock__row > .cta--caret::before,
#viewToolbar .dock__sub::before {
  content: ''; position: absolute; left: 0; top: 50%; translate: 0 -50%;
  width: 1px; height: 16px; background: currentColor; opacity: .3;
}
/* The label after the divider. Full-strength ink on the fill: never faded by opacity
   (DESIGN §2 Contrast — 6B faded it to 4.24:1). The divider above is not text. */
.dock__sub {
  position: relative; display: inline-flex; align-items: center; flex: 0 0 auto;
  padding: 0 var(--sn-space-4) 0 var(--sn-space-3); white-space: nowrap;
  font-size: var(--sn-text-sm); font-weight: 500; cursor: default;
}
#viewToolbar .dock__row:has(> .dock__sub:not([hidden])) > .cta:not(.cta--caret):not([hidden]) { padding-right: var(--sn-space-3); }
/* The caret is a plain .cta, so a warn row also matched "some enabled .cta" and hovered
   green (e2e re-verification 2, shot 134). A row holding a visible warn action never
   takes the accent hover. */
#viewToolbar .dock__row:not(:has(> .cta.is-warn:not([hidden]))):has(> .cta:not(:disabled):not([hidden])):hover { background: var(--sn-accent-hover); }
#viewToolbar .dock__row > :focus-visible { outline: 2px solid currentColor; outline-offset: -4px; border-radius: var(--sn-radius-sm); }

/* Amber: the primary that can build or hand over a file nobody has checked. */
#viewToolbar .dock__row:has(> .cta.is-warn:not([hidden])) { background: var(--sn-warn); color: var(--sn-warn-ink-on-fill); }
/* Locked amber (repair consent not yet ticked): outline and label at full strength. */
#viewToolbar .dock__row:has(> #prepare.is-warn:disabled:not([hidden])) {
  background: var(--sn-warn-tint); color: var(--sn-warn); box-shadow: inset 0 0 0 1.5px var(--sn-warn-edge);
}
/* Disabled, not preparing: item 76's drawn disabled state, on the whole pill. */
#viewToolbar #sheetDock:not([data-dock-state="working"]) .dock__row:has(> #prepare:disabled:not([hidden]):not(.is-warn)) {
  background: var(--sn-accent-disabled-bg); color: var(--sn-accent-disabled-ink);
  box-shadow: inset 0 0 0 1.5px var(--sn-accent-disabled-edge);
}
/* Preparing… | 58%: a quiet surface with a 5 % white fill growing left to right. */
#viewToolbar #sheetDock[data-dock-state="working"] .dock__row {
  background: linear-gradient(90deg, rgb(var(--sn-white-rgb) / .05) var(--sn-prep-fill, 0%), transparent var(--sn-prep-fill, 0%)), var(--sn-surface-2);
  color: var(--sn-ink-dim); box-shadow: none;
}
#viewToolbar #sheetDock[data-dock-state="working"] #prepare { cursor: progress; }
/* The shared 14 px busy spinner (css/base.css) is redundant beside the percent. */
#viewToolbar #sheetDock[data-dock-state="working"] #prepare[data-busy]::before { display: none; }

/* The old text view tools (#tools, adopted into #bandR2 by js/viewer.js): Reset and Top
   moved to the camera column, Turntable and Bed face to the icons above. Underside,
   Wireframe, Build plate and Run log have no simple-view home and are tester controls, so
   the row is shown in the tester view only, stacked in the toolbar's column above the bar
   (the column is flex, so the two can never overlap). */
html:not([data-detail="full"]) #bandR2 { display: none; }
/* Regression review M2: on a testing host the Run log button is a testing surface and
   follows `data-testing`, not the detail toggle. The simple view shows the row with that
   one button; the other text tools stay tester-view only. */
html[data-testing]:not([data-detail="full"]) #bandR2:not([hidden]) { display: flex; }
html[data-testing]:not([data-detail="full"]) #bandR2 #tools > :not(#btnRunLog) { display: none; }
/* Stage audit S2 (24 Sep): the working note is the one place that says preparing takes
   MINUTES, so it is no longer tester-only (it used to be hidden here in the simple view,
   as a repeat of the step list; it is not a repeat now). See preparingNote() in
   js/pure/dockstate.js. */

/* Declutter round 2: a toolbar note that carries only tester detail (js/dock.js) takes no
   line in the simple view. */
html:not([data-detail="full"]) #dockNote[data-detail-note-only] { display: none; }
/* An expired result has no model on the bed and no file to download: the bed note
   "download the .3mf to see it" would point at a file that is gone (STAGE-AUDIT S4). */
html[data-result-expired] #vpBedNote { display: none; }
