/* bench.css — "the blade is the page" (the blade redesign, PR #19;
   see docs/HISTORY.md). Loaded by `/` only, after identity.css. Everything the existing,
   tested calibration engine already serves (#card-outline, #card-px, the
   edge/orient fieldsets, #confirm-match, #discard-calibration,
   .calibration-limits, .first-run-steps) is REUSED here, relocated into
   .bench and restyled — none of it is reimplemented. bench.js only adds:
   the vertical blade ticks (drawn from rule.js's own R.ticks(), the same
   data #rule already renders from, laid out along a blade instead of a
   face), and a thin mirror of ruler.js's own computed state onto the
   pill/stamp. (A card vernier — tap marks beside the blade — was in the
   first version of this file and is not, for now; see bench.js's own
   header for why it was dropped on review.)

   `transform` is used freely here (the vertical slider, the stamp's tilt):
   the "nothing may scale the face" guard in tests/structure.js reads
   ruler.css only, and the thing it protects — the #rule SVG's own geometry
   — is never transformed, here or anywhere. */

/* Standard visually-hidden pattern: present for a screen reader and the tab
   order, invisible and out of the bench's layout. Used for the four-step
   instructions (#match-h + .first-run-steps) rather than duplicating them,
   without them competing for space with the blade on the first screen. The
   edge/orientation choice, the seed field and the limits are NOT .vh'd —
   review on PR #19 found five controls clipped to 1x1px with no visible
   focus indicator to land on (WCAG 2.4.7); they are real, visible content
   inside .bench-options-panel below instead. */
.vh {
  position: absolute !important; width: 1px !important; height: 1px !important;
  overflow: hidden !important; clip: rect(0 0 0 0) !important; white-space: nowrap !important;
  border: 0 !important; padding: 0 !important; margin: -1px !important;
}

/* The house header (kit/base.css's .site-header) is replaced on this route
   by .bench-top — kept in the DOM (the "carries the site header and brand"
   house check reads it, and it is still valid markup/navigation), just not
   painted, so the bench is truly the first thing a visitor sees. */
.site-header { display: none; }
main.wrap { padding-top: 0; }
/* Safety net for the >=960px full-bleed rule on .bench, far below: see the
   comment there for why a 100vw box needs one. Scoped to this page only
   (bench.css loads on `/` alone) rather than added to kit/base.css, which
   has no element that reads for 100vw. */
body { overflow-x: hidden; }

/* `/` prints in B612 Mono and Barlow Semi Condensed (identity.css's print
   block), but the screen never draws in them, so on a reader's first Ctrl+P
   they were still unloaded and Chrome printed a fallback monospace. One
   clipped, transparent glyph in each print face makes the browser load all
   three with the page, in CSS alone, so this works with scripting off too.
   Screen only, out of flow, inside the bench's `overflow: clip`, and silent
   to a screen reader (empty alt text). */
@media screen {
  .bench::before, .bench::after, .bench-top::before {
    content: "0" / "";
    position: absolute; left: 0; top: 0;
    width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%);
    color: transparent;
    pointer-events: none;
  }
  .bench::before { font: 400 1px/1 "B612 Mono", monospace; }
  .bench::after { font: 700 1px/1 "B612 Mono", monospace; }
  .bench-top::before { font: 700 1px/1 "Barlow Semi Condensed", sans-serif; }
}

.bench {
  /* Review on PR #19 round 2, minor: the plan and mockup pin the blade to
     the screen edge; `main.wrap`'s own 1rem side padding (the house gutter
     every OTHER section on this page still wants) was insetting it 16px.
     Cancelling the padding with an equal negative margin is exact and
     carries no risk of its own — unlike a 100vw-width trick, it cannot
     widen the box past the viewport on a desktop with a non-overlay
     scrollbar, because it never reads the viewport at all. */
  margin-left: -1rem; margin-right: -1rem;

  position: relative;
  height: 100svh;
  /* Review on PR #19 round 2, minor: a bare 560px floor is taller than the
     viewport itself in landscape on a phone (100svh is ~390px at 844x390),
     so the bench RENDERED taller than the screen and the confirm row sat
     below the first scroll — the exact "zero scrolling" scope this page
     exists to deliver. min() caps the floor at the real viewport height,
     so it only ever raises a SHORT bench, never forces a scroll. */
  min-height: min(560px, 100svh);
  overflow: clip;
  background: var(--mat, #1E5B4F);
  color: var(--on-mat, #F7F8F4);
  --blade: #F3C318;
  --mat: #1E5B4F;
  --grid: #8CC2AE;
  --red: #D2341E;
  --sheet: #F7F8F4;
  --on-mat: #F7F8F4;
  --on-mat-dim: #B8D5CA;
}
@media (prefers-color-scheme: dark) {
  .bench { --blade: #E8B90F; --mat: #143F37; }
}

.bench-top {
  position: absolute; inset: 0 0 auto 0; height: 44px;
  display: flex; align-items: center; gap: 10px;
  padding: 0 6px 0 14px;
  background: #16191B; color: #F7F8F4;
  z-index: 8;
}
.bench-top .mark { display: flex; align-items: center; gap: 8px; color: inherit; text-decoration: none; min-height: 44px; }
.bench-top .mark b { font: 700 19px/1 var(--tick, sans-serif); }
.bench-top .mark svg { flex: none; }

/* Review on PR #19 round 2, minor: with .site-header hidden (above), the
   only nav this route carried was .bench-top .nav (Ruler/Sheets/Methodology,
   and only past 960px — see the max-width:959.98px rule far below) and the
   dock (Ruler/Sheets/Guide). Neither reaches /screen-ruler/ or /calibrate/
   at ANY width. A Menu disclosure, visible at every width, carries the
   SAME five links .site-nav already does (so it says nothing new — it is
   a second, always-reachable place to tap them), rather than widening the
   existing .nav's own breakpoint or the dock's three slots. */
.bench-menu { position: relative; margin-left: auto; }
.bench-menu summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; color: inherit;
}
.bench-menu summary::-webkit-details-marker { display: none; }
.bench-menu summary:focus-visible { outline: 3px solid #fff; outline-offset: -3px; }
.bench-menu-panel {
  position: absolute; right: 0; top: calc(100% + 6px);
  z-index: 26; min-width: 11rem;
  display: flex; flex-direction: column;
  background: var(--panel, #fff); color: var(--ink, #16191B);
  border-radius: 10px; padding: 0.4rem;
  box-shadow: 0 8px 24px rgba(0,0,0,.35);
}
.bench-menu-panel a {
  display: flex; align-items: center; min-height: 44px; padding: 0 0.7rem;
  color: inherit; text-decoration: none; border-radius: 6px;
  font: 600 14px/1 var(--sans, sans-serif);
}
.bench-menu-panel a:hover, .bench-menu-panel a:focus-visible { background: var(--accent-soft, #E7F1EC); }
.bench-menu-panel a[aria-current="page"] { color: var(--accent, #1E5B4F); }

.bench-blade {
  position: absolute; left: 0; top: 44px; bottom: 0; width: 64px;
  background-color: var(--blade); /* Review on PR #19 round 2, minor: a solid fallback — color-mix() alone leaves a browser without it with no yellow blade at all. */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--blade) 86%, #000) 0, var(--blade) 16%,
    color-mix(in srgb, var(--blade) 82%, #fff) 46%, var(--blade) 74%,
    color-mix(in srgb, var(--blade) 90%, #000) 100%);
  box-shadow: 3px 0 0 rgba(0,0,0,.18), 6px 0 14px rgba(0,0,0,.22);
  z-index: 4;
}
.bench-blade svg, .bench-blade-h svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* Shared by both blades — the vertical strip on every width and the
   desktop-only horizontal one past 960px (bench.js's bladeTicksMarkup()
   draws the same classed elements into both). Review on PR #19, major: only
   `.bench-blade`'s own classes were styled here, so the horizontal blade's
   ticks carried no stroke (invisible) and only its two FILLED rects — the
   decade box and the zero bar, which paint black with no `fill` rule of
   their own — showed at all. */
.bench-blade .t1, .bench-blade .t5, .bench-blade .t10,
.bench-blade-h .t1, .bench-blade-h .t5, .bench-blade-h .t10 { stroke: #16191B; fill: none; shape-rendering: crispEdges; }
.bench-blade .t1, .bench-blade-h .t1 { stroke-width: 1; }
.bench-blade .t5, .bench-blade-h .t5 { stroke-width: 1.2; }
.bench-blade .t10, .bench-blade-h .t10 { stroke-width: 1.6; }
.bench-blade .n, .bench-blade-h .n { font: 700 15px/1 var(--tick, sans-serif); fill: #16191B; text-anchor: middle; dominant-baseline: central; }
.bench-blade .dm, .bench-blade-h .dm { fill: var(--red); }
/* Review on PR #19 round 2, minor: yellow-on-vermilion measured 2.97:1
   light / 2.68:1 dark at 15px bold, under the 4.5:1 floor. White on
   #D2341E measures 4.94:1 and is the fix the review itself specified. */
.bench-blade .kn, .bench-blade-h .kn { fill: #fff; }
.bench-blade .z, .bench-blade-h .z { fill: var(--red); }

.bench-mat {
  position: absolute; left: 64px; top: 44px; right: 0; bottom: 0;
  background: radial-gradient(120% 70% at 20% 10%, rgba(255,255,255,.05), transparent 60%), var(--mat);
}
.bench-mat .grid { position: absolute; inset: 0; width: 100%; height: 100%; }
.bench-mat .grid .g1, .bench-mat .grid .g5 { fill: none; stroke: var(--grid); }
.bench-mat .grid .g1 { stroke-opacity: .34; stroke-dasharray: 3 4; }
.bench-mat .grid .g5 { stroke-opacity: .62; stroke-dasharray: 7 4; }
.bench.is-cal .bench-mat .grid .g1,
.bench.is-cal .bench-mat .grid .g5 { stroke-dasharray: none; }

.bench-gtag {
  position: absolute; right: 10px; bottom: 70px; z-index: 6;
  display: flex; align-items: center; gap: 7px;
  color: var(--on-mat-dim); font: 600 12px/1 var(--tick, sans-serif);
}
.bench-gtag i { width: 20px; height: 0; border-top: 1.5px dashed var(--grid); }
.bench.is-cal .bench-gtag i { border-top-style: solid; }

/* ---------- the ghost card + its controls, relocated from .tool-panel ----
   top:0, not some arbitrary padding — this box's top edge is where
   #card-outline's own top:0 sits (below), and that is also where the
   vertical blade's zero tick renders (.bench-blade shares .bench-mat's own
   top). Review on PR #19, minor: at the old top:54px the card started 54px
   below .bench-mat's top while the blade's zero tick stayed AT that top,
   so the ghost card did not start at zero — the one thing a ruler's own
   subject is supposed to do. */
.bench-cal {
  position: absolute; left: 10px; right: 10px; top: 0; bottom: 8px;
  z-index: 6;
}
/* ---------- confirm / discard / options trigger --------------------------
   Round 1 anchored these to the BENCH's own bottom after they were found
   clipped by the dock / inside the card. Round 2, B1: a bottom anchor is
   still a fixed point on the SAME axis a real card's far edge travels
   along (an iPhone 15's 85.6mm long edge is ~517 CSS px upright), so a
   long enough card still reaches it — the anchor only moved the collision.

   THE FIX LEAVES THE CARD'S AXIS ENTIRELY. This block is promoted to a
   direct child of #bench (see index.html), not a descendant of .bench-cal,
   so bench.js's wire() can relocate it into .bench-dock below 960px (a
   fixed strip no card length reaches — see `.bench-dock .bench-confirm`
   below), and so it escapes .bench-cal's z-index:6 context, which is what
   let .bench-dock's 7 beat .bench-options-panel's 20 regardless of the
   number on the panel (round 2, B2). >=960px (dock hidden; a horizontal
   card's short-edge height leaves room below it) it stays in flow here,
   bottom-anchored, as round 1 left it — round 2 did not reproduce a
   card-path collision at desktop sizes, only at the phone widths the dock
   covers. */
.bench-confirm {
  position: absolute; left: 10px; right: 10px;
  bottom: calc(54px + env(safe-area-inset-bottom, 0px) + 12px);
  display: flex; flex-wrap: wrap; gap: 8px;
}
/* Review on PR #19 round 2, minor: once calibrated the ghost is hidden
   (`.bench.is-cal .card-outline{display:none}`, below) but this block —
   and the grip two rules down — stayed on the mat with nothing left to
   match against; dragging the grip moved an outline nobody could see.
   Hidden for BOTH calibrated and the dock-relocated case alike (the
   `display:contents` rule below only ever applies while uncalibrated —
   see bench.js's relocateConfirm() — so this is the only rule that needs
   to know about "calibrated" at all). */
.bench.is-cal .bench-confirm { display: none; }
/* Review on PR #19 round 2, B1's "at minimum" fallback, kept as a second
   line of defence alongside the relocation above: the confirm/discard
   buttons' own background is opaque (below), but the ROW's gaps and any
   slack around a wrapped button are not, and at >=960px (where this block
   is NOT relocated into the dock) a very long card could still in
   principle reach under them. Transparent gaps pass the tap through to
   whatever is underneath — the grip, if the card's far edge happens to be
   there — rather than silently eating it. */
.bench-confirm { pointer-events: none; }
.bench-confirm > * { pointer-events: auto; }
/* min-height, not just padding — the estate's 44px phone floor. Measured at
   39.75px before this (0.85rem text + 0.5rem padding, no floor), caught by
   tests/bench-live.js's real-browser hit-test rather than by eye. */
.bench-confirm > .btn { font-size: 0.85rem; padding: 0.5rem 0.8rem; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; }
/* kit/base.css's primary .btn reads var(--accent) for its background, and
   identity.css sets --accent to the SAME cutting-mat green .bench already
   paints with — invisible-on-invisible, found by eye in a render where
   "This matches my card" had no visible button, just bold text. Ink,
   here only, keeps it readable against the mat without touching --accent
   for the rest of the page (prose links, etc. still want the mat green). */
.bench-confirm > .btn:not(.secondary) { background: #16191B; border-color: #16191B; color: #F7F8F4; }
/* Review on PR #19 round 2, minor: #0f62fe (the house focus color,
   identity.css keeps it for the safety reason given there) on the mat
   green measures 1.58:1. White is what #card-px and .bench-options
   summary already use on this same mat, at 7.9:1 — same fix, same place. */
.bench-confirm > .btn:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
/* Review on PR #19 round 2, B2's "keep confirm reachable while [the
   options panel is] open": the panel (z-index:20, below) is a sibling of
   this block once promoted out of .bench-cal, not a descendant of it, so
   outranking it needs these THREE triggers to carry their own stacking
   order rather than inheriting whatever this block (deliberately left at
   the default z-index so it does not re-trap the panel the way .bench-cal
   used to) has. 25 beats the panel's 20 wherever they would otherwise
   overlap, at any width. */
.bench-confirm > .btn, .bench-options > summary { position: relative; z-index: 25; }
/* ruler.css's .outline-stage is a padded white panel (border, background,
   1rem padding) for the house-template layout; stripped here so only the
   dashed ghost card shows on the mat. (Found by eye in a rendered
   screenshot: the panel's own background was the "second white box" behind
   the ghost card.) */
.bench .outline-stage { position: static; background: none; border: 0; padding: 0; margin: 0; overflow: visible; }
.bench .card-outline {
  position: absolute; left: 0; top: 0;
  width: var(--outline-w, 200px); height: var(--outline-h, 126px);
  background: transparent;
  border: 1.5px dashed rgba(247,248,244,.85);
  border-radius: 0 6px 6px 0;
  pointer-events: none;
}
.bench.is-cal .card-outline { display: none; }
/* Review on PR #19 round 2, minor: the grip has nothing left to drag once
   the ghost it moves is hidden (above) — dragging it moved an outline
   nobody could see. Hidden alongside the ghost rather than only the
   ghost, for the same calibrated state. */
.bench.is-cal .match-controls { display: none; }
.bench .corner { display: none; } /* ruler.js paints corners on .card-outline; the dashed ghost replaces them visually */

/* #card-px: the native range input IS the drag handle. Track hidden, thumb
   restyled as the vermilion grip — native pointer AND keyboard dragging
   (arrow keys step by the engine's own rails), nothing reimplemented.

   GEOMETRY IS SET BY bench.js's layoutSlider(), not by this rule — a plain
   `left:50%;top:50%;width:var(--grip-len);transform:translate(-50%,-50%)
   rotate(90deg)` (centering a track THE SAME LENGTH AS THE CARD on the
   card's own center) was the shipped version, and review on PR #19, major,
   measured what it actually did: a 60px drag moved the outline 355 to
   474px (about 2x gain), and the grip sat at 16% along the card rather
   than at its far edge. The native thumb's screen position is
   `trackLeft + thumbWidth/2 + (value-min)/(max-min) * (trackWidth-thumbWidth)`
   — for that to equal `value` itself (which is what #card-outline's own
   --outline-w/--outline-h ARE, in the same px, from the same origin), the
   track width has to equal `(max-min) + thumbWidth` (so the (value-min)
   term has a 1:1 coefficient) and the track's own left/top has to sit at
   `min - thumbWidth/2` (so the thumb's floor value lands exactly at `min`
   px from the origin, not at 0). Both of those are per-edge (min/max
   change with which edge is selected) and the origin itself is per-mode
   (left in horizontal, top in vertical) — arithmetic a static rule cannot
   do, so layoutSlider() does it on every repaint from the SAME --outline-w/
   --outline-h/min/max #card-outline and this input already carry; this
   rule supplies everything that is NOT that arithmetic.

   POSITIONING CONTEXT: --outline-w/--outline-h are set by ruler.js as an
   INLINE style on #card-outline itself, so only #card-outline and ITS
   descendants can read them via var() — a CSS custom property does not
   reach a sibling. #card-px lives inside .match-controls, a sibling of
   #card-outline (moving it to be a descendant would put it under
   #card-outline's aria-hidden="true", removing it from the accessibility
   tree entirely), so .match-controls is pinned to the SAME origin
   #card-outline uses (left:0, top:0 of the shared .bench-cal ancestor)
   and layoutSlider() reads --outline-w/--outline-h off the ghost directly. */
/* margin:0 overrides kit/base.css's .controls{margin:0.75rem 0} — a margin
   applies to a position:absolute box same as any other, so the 12px top
   margin was quietly offsetting every one of layoutSlider()'s px-exact
   numbers below (found while verifying finding 7's fix against a REAL
   render: the thumb landed 12px off from the math until this was zeroed). */
.bench .match-controls { position: absolute; left: 0; top: 0; margin: 0; }
.bench input#card-px {
  -webkit-appearance: none; appearance: none;
  position: absolute; left: 0; top: 0;
  width: 126px; height: 36px;
  margin: 0;
  transform: none;
  background: transparent;
  touch-action: none;
}
.bench input#card-px:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
.bench input#card-px::-webkit-slider-runnable-track { background: transparent; height: 36px; }
.bench input#card-px::-moz-range-track { background: transparent; height: 36px; border: 0; }
.bench input#card-px::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 30px; height: 36px; border-radius: 8px;
  background: var(--red);
  box-shadow: 0 1px 3px rgba(0,0,0,.4);
  cursor: ew-resize;
}
.bench input#card-px::-moz-range-thumb {
  width: 30px; height: 36px; border-radius: 8px; border: 0;
  background: var(--red); cursor: ew-resize;
}

/* The slider's own label/output stay in the DOM (accessible name + live
   value) but are not part of the bench's visible layout — restyled small
   and placed as a caption rather than hidden, so a sighted keyboard user
   dragging the handle still sees the number change. Position is set by
   layoutSlider() (left/top in px, a translate to center on whichever axis
   is cross-wise) rather than by a percentage here: .match-controls is a
   zero-size anchor pinned to the origin (above), not a box the size of
   the card, so a CSS percentage of it would not track the thumb. */
.bench .match-controls label.inline { display: none; }
.bench .match-controls output {
  position: absolute; left: 0; top: 0;
  white-space: nowrap;
  background: rgba(22,25,27,.8); border-radius: 6px; padding: 3px 8px;
  font: 600 11px/1 var(--mono, monospace); color: var(--on-mat);
  opacity: 0; pointer-events: none; transition: opacity .15s ease;
}
/* Shown only while the handle itself is actively being operated by
   keyboard or pointer — a sighted user dragging or arrow-stepping sees the
   number change; it does not sit permanently over the mat otherwise. */
.bench .match-controls:focus-within output,
.bench .match-controls:hover output { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .bench .match-controls output { transition: none; } }
/* ---------- the options drawer -------------------------------------------
   Review on PR #19 round 1, major: the edge radio, the orientation radio,
   the screen-size seed field and its button, and the walkthrough link were
   all `.vh`'d or clipped to 1x1px INSIDE the bench — present for a screen
   reader and the tab order, but focusable with no visible focus indicator
   to land on, which fails WCAG 2.4.7. A sighted keyboard user tabbing
   through the bench landed on nothing they could see five times over.

   <details>/<summary> gives a visible, keyboard-operable disclosure for
   free (no new JS: open/close, the tab stop, and the expanded/collapsed
   state are all native). Collapsed, `.bench-options-panel` is simply not
   in the layout (the UA stylesheet's default for a <details> child that
   is not its <summary>) — zero footprint on the first screen. Open, it is
   `position: fixed` rather than `absolute`: `.bench` clips its own
   absolutely-positioned children (`overflow: clip`, for the cutting-mat
   illusion), and this content can run long enough to need its own scroll,
   so it escapes as a plain bottom sheet instead of fighting that clip.

   Round 2, B2: that disclosure covered the confirm/discard buttons once
   opened, with no way to close it (Escape and tap-outside are not native
   <details> behavior), and its z-index:20 was trapped inside .bench-cal's
   own z-index:6, losing to .bench-dock's 7 regardless of the number on
   the panel. Three fixes: (1) promoting .bench-confirm (and this
   <details>) out of .bench-cal, above, un-traps the panel's z-index; (2)
   bench.js wires Escape, an outside pointerdown, and .bench-options-close
   (below) to one close() — native <summary> re-click still works too;
   (3) the panel's `bottom` offset, below, matches .bench-confirm's, so at
   <960px it sits ABOVE the dock rather than behind it, keeping confirm
   reachable without a z-index fight over the same pixels. */
.bench-options {
  background: rgba(22,25,27,.65);
  border-radius: 10px;
}
.bench-options summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center;
  min-height: 44px; padding: 0 0.9rem;
  color: var(--on-mat); font: 600 13px/1.2 var(--sans, sans-serif);
}
.bench-options summary::-webkit-details-marker { display: none; }
.bench-options summary::before {
  content: "+"; margin-right: 0.6rem;
  font: 700 13px/1 var(--mono, monospace); color: var(--blade);
}
.bench-options[open] summary::before { content: "–"; }
.bench-options summary:focus-visible { outline: 3px solid #fff; outline-offset: -3px; border-radius: 10px; }

.bench-options-panel {
  position: fixed; left: 0; right: 0; z-index: 20;
  bottom: calc(54px + env(safe-area-inset-bottom, 0px) + 12px);
  max-height: min(70vh, 100svh - 120px); overflow-y: auto;
  background: var(--panel, #fff); color: var(--ink, #16191B);
  border-radius: 16px;
  padding: 1rem;
  box-shadow: 0 8px 30px rgba(0,0,0,.35);
}
.bench-options-panel fieldset.choice:first-child { margin-top: 0; }
.bench-options-panel fieldset.choice label { min-height: 44px; }
.bench-options-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; margin-bottom: 0.5rem;
}
.bench-options-head .bench-options-title { margin: 0; font: 700 15px/1.3 var(--sans, sans-serif); }
.bench-options-close {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin: -0.5rem -0.25rem 0 0;
  background: none; border: 0; border-radius: 8px; color: inherit; cursor: pointer;
}
.bench-options-close:hover { background: var(--accent-soft, #E7F1EC); }
.bench-options-close:focus-visible { outline: 3px solid var(--focus, #0f62fe); outline-offset: 1px; }

/* ---------- mobile: confirm/discard/options relocate INTO the dock ------
   Review on PR #19 round 2, B1: at <960px the dock is the one fixed strip
   whose position never depends on the card's own length (unlike anything
   anchored inside .bench-cal, bottom or not). bench.js's relocateConfirm()
   appends #bench-confirm here while uncalibrated; `display: contents`
   removes the wrapper's own box so its three children (confirm, discard,
   the options <details>) become grid items of .bench-dock's own
   `repeat(3,1fr)` directly, in place of .dock-tools (hidden by the sibling
   rule on .dock-tools, below) — the same three-slot dock, a different set
   of things in it, never six at once. */
.bench-dock .bench-confirm { display: contents; }
.bench-dock .bench-confirm > .btn,
.bench-dock .bench-confirm > details.bench-options { background: none; border: 0; border-radius: 0; }
.bench-dock .bench-confirm > details.bench-options {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: 44px;
}
.bench-dock .bench-confirm > .btn,
.bench-dock .bench-confirm > details.bench-options > summary {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; width: 100%; height: 100%; min-height: 44px;
  padding: 0; margin: 0;
  color: #9db3ab; font: 700 11px/1 var(--sans, sans-serif);
}
.bench-dock .bench-confirm > details.bench-options > summary::before { content: none; }
/* Hidden only while #bench-confirm is actually relocated here (uncalibrated,
   <960px) — see relocateConfirm() in bench.js. Once calibrated, or at
   >=960px, bench.js moves #bench-confirm back out, this selector no
   longer matches, and .dock-tools returns to .bench-dock's own default
   grid display with no extra rule needed for that direction. */
.bench-dock:has(.bench-confirm) .dock-tools { display: none; }

/* ---------- status pill / stamp ------------------------------------------ */
/* Review on PR #19, major: `.bench-pill{display:flex}` outranks the UA
   `[hidden]{display:none}` rule (equal specificity, this one comes later in
   the sheet), so after calibrating, `#bench-pill[hidden]` stayed painted
   — flex-laid-out, visible, sitting under the stamp drawn on top of it.
   `[hidden]` wins explicitly for both now. */
.bench-pill[hidden], .bench-stamp[hidden] { display: none; }

.bench-pill {
  position: absolute; right: 8px; top: 54px; z-index: 7; max-width: 200px;
  display: flex; gap: 8px; align-items: flex-start; text-align: left;
  background: var(--sheet); color: #16191B; border-radius: 10px; padding: 8px 10px;
  font: 400 12px/1.3 var(--sans, sans-serif);
  box-shadow: 0 2px 0 rgba(0,0,0,.18), 0 6px 18px rgba(0,0,0,.18);
}
.bench-pill b { font-weight: 700; }
.bench-pill .dot { flex: none; width: 11px; height: 11px; margin-top: 2px; border-radius: 50%; border: 1.5px dashed #16191B; }

.bench-stamp {
  position: absolute; right: 10px; top: 54px; z-index: 7;
  transform: rotate(-3.5deg); transform-origin: 100% 0;
  background: var(--blade); color: #16191B; border: 2px solid #16191B; border-radius: 3px;
  padding: 6px 9px 5px; text-align: left;
  box-shadow: inset 0 0 0 3px var(--blade), inset 0 0 0 4px #16191B, 0 4px 12px rgba(0,0,0,.28);
}
.bench-stamp .s1 { display: block; font: 700 13px/1.05 var(--tick, sans-serif); letter-spacing: .05em; white-space: nowrap; }
.bench-stamp .s2 { display: block; margin-top: 3px; font: 400 10px/1.1 var(--mono, monospace); }
@media (prefers-reduced-motion: no-preference) {
  .bench-stamp.fresh { animation: bench-thunk .3s ease-out; }
}
@keyframes bench-thunk { 0% { transform: rotate(-3.5deg) scale(1.2); opacity: 0; } 100% { transform: rotate(-3.5deg) scale(1); opacity: 1; } }

/* ---------- bottom dock --------------------------------------------------- */
.bench-dock {
  position: absolute; left: 74px; right: 8px;
  bottom: calc(8px + env(safe-area-inset-bottom, 0px));
  height: 54px; z-index: 7;
  background: #16191B; border-radius: 14px;
  display: grid; grid-template-columns: repeat(3, 1fr);
  box-shadow: 0 6px 20px rgba(0,0,0,.3);
}
.bench-dock :where(a, button):not(.bench-options-panel *) {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: 44px;
  color: #9db3ab; text-decoration: none;
  font: 700 11px/1 var(--sans, sans-serif);
}
.bench-dock a[aria-current="page"], .bench-dock button[aria-current="page"] { color: #F7F8F4; }
/* Review on PR #19, minor: the dock's icons are unfilled <rect>/<path>
   shapes, so with no `fill` of their own they painted at SVG's default
   black regardless of the link's own color (dimmed grey, or white for the
   current page) — black on #16191B either way. */
.bench-dock svg * { fill: currentColor; }
.bench-dock .ub { font: 700 12px/1 var(--mono, monospace); border: 1.5px solid currentColor; border-radius: 4px; padding: 2px 5px; }

/* ---------- desktop: a second blade across the top for an L-square -------
   Base/hidden rules FIRST, the >=960px override second — CSS source order,
   not specificity, decides a tie between a bare selector and one inside a
   media query, so these have to precede the block that re-enables them or
   they win at every width including desktop (found by eye: the top blade
   never appeared in a 1280px render). */
.bench-blade-h { display: none; }
.bench-corner { display: none; }
@media (min-width: 960px) {
  /* Review on PR #19 round 2, minor: past 960px `main.wrap`'s own
     `--maxw` (62rem) caps and centers the bench too, same complaint as
     the gutter above but the negative-margin trick above cannot reach it
     (it cancels the PADDING, not the max-width). A 100vw width, scoped to
     this one breakpoint, reaches the true screen edge; `body` gets an
     `overflow-x: hidden` safety net two rules down so a desktop browser
     whose scrollbar is NOT an overlay (reserving a few px the vw unit
     does not know about) cannot turn this into the horizontal scroll the
     phone-first floor forbids — the nested `.rule-wrap{overflow-x:auto}`
     keeps its own independent scroll regardless, since that rule governs
     the page, not that element. */
  .bench { width: 100vw; max-width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); min-height: min(620px, 100svh); }
  .bench-top { padding-left: 18px; }
  .bench-blade { top: 100px; }
  .bench-mat { top: 100px; left: 64px; }
  .bench-pill, .bench-stamp { top: 100px; }
  .bench-confirm { left: 74px; }
  .bench-blade-h {
    display: block; position: absolute; left: 64px; right: 0; top: 44px; height: 56px; z-index: 4;
    background-color: var(--blade); /* solid fallback, same reasoning as .bench-blade above */
    background: linear-gradient(180deg,
      color-mix(in srgb, var(--blade) 86%, #000) 0, var(--blade) 16%,
      color-mix(in srgb, var(--blade) 82%, #fff) 46%, var(--blade) 74%,
      color-mix(in srgb, var(--blade) 90%, #000) 100%);
    box-shadow: 0 3px 0 rgba(0,0,0,.18), 0 6px 14px rgba(0,0,0,.22);
  }
  .bench-corner {
    display: block; position: absolute; left: 0; top: 44px; width: 64px; height: 56px; z-index: 5;
    background: var(--blade);
  }
  .bench-corner::after { content: ""; position: absolute; left: 10px; bottom: 10px; width: 20px; height: 20px; border-left: 1.5px solid #16191B; border-bottom: 1.5px solid #16191B; }
  .bench-dock { display: none; }
}

@media (max-width: 959.98px) { .bench-top .nav { display: none; } }
@media (min-width: 960px) {
  .bench-top .nav { display: flex; gap: 2px; margin-left: 20px; height: 100%; }
  .bench-top .nav a { display: flex; align-items: center; padding: 0 12px; color: #9db3ab; text-decoration: none; font: 600 14px/1 var(--tick, sans-serif); }
  .bench-top .nav a[aria-current="page"] { color: #F7F8F4; }
}

/* Print (`/` only): the bench, the readout, the ruler face, its controls and
   the cross-check are screen instruments drawn in screen pixels, so none of
   them is true size on paper. They are left off; the prose, the FAQ and the
   .print-note pointer to /printable-ruler/ (ruler.css) remain. */
@media print {
  .site-header,
  .site-footer,
  #bench,
  #readout,
  main.wrap > .tool-panel {
    display: none !important;
  }
  body { background: #ffffff; color: #000000; overflow: visible; }
}
