/* Reusable chrome LAYOUT for a partforge app: the shell, the viewer column
   ("stage"), the full-height controls rail, its resize seam, and the two
   floating pill groups. This file owns the shell's layout and its structural
   surfaces — the rail's/head's/foot's background, border, shadow, and text
   color, and the seam pill's background — all expressed through overridable
   --pf-* tokens. Appearance of the controls INSIDE the rail (the panel
   widgets themselves) stays in app.css.

   Class-based on purpose. partforge-cloud's sandbox builds its own DOM
   (#viewer / #pfc-controls) and could never reuse an id-keyed sheet, so the
   layout is expressed as .pf-* classes and exported standalone as
   "partforge/chrome.css". app.css keeps :not(.pf-*) fallbacks so legacy
   id-only markup renders its previous floating look untouched.

   Prerequisite: this sheet consumes --pf-* custom properties (--pf-rail-w,
   --pf-rail-pad, --pf-border, --pf-surface, --pf-bg, --pf-text, --pf-muted,
   --pf-accent, --pf-shadow-rail) but does not import them — a standalone
   consumer must also load "partforge/tokens.css" (kept separate so the two stay
   independently composable). The host is also responsible for giving
   .pf-shell a height (e.g. `height: 100%` on an ancestor chain rooted at
   `html, body`, or `position: absolute; inset: 0`); this sheet does not size
   the shell itself.

   See docs/superpowers/specs/2026-07-26-controls-rail-layout-design.md. */

/* ---- shell: viewer column + rail, side by side --------------------------- */
.pf-shell {
  display: flex;
  /* containing block for the absolutely-positioned seam */
  position: relative;
  overflow: hidden;
}

/* ---- stage: the viewer column, which owns its floating chrome ------------
   min-width: 0 lets the column shrink past the canvas's intrinsic width, so
   dragging the rail wider actually narrows the viewer instead of overflowing.

   min-height: 0 is the same escape hatch for the OTHER axis, and it is what
   the narrow layout needs: there the shell is a column, so the canvas's
   height — a real pixel height the renderer wrote, not an intrinsic 150px —
   becomes this flex item's automatic minimum size. Without it the stage
   refuses to shrink, and the tab bar below it is pushed off the bottom of
   the screen: still display:flex, still in the DOM, simply not on screen.
   That failure survived a `display` assertion in the smoke check, which is
   why that check now asserts the bar is inside the viewport instead. */
.pf-stage {
  flex: 1;
  position: relative;
  min-width: 0;
  min-height: 0;
  background: var(--pf-bg);
}

/* ---- rail: a full-height right edge, set back from the viewer ------------
   Square-cornered: it is an edge, not a card. The shadow is INSET on its left
   side — the viewer casts onto the rail, which is what makes the rail read as
   set back. An outer shadow would read as floating above the viewer. */
.pf-rail {
  flex: none;
  width: var(--pf-rail-w);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  background: var(--pf-surface);
  border-left: 1px solid var(--pf-border);
  box-shadow: var(--pf-shadow-rail);
  color: var(--pf-text);
  /* Discrete changes (toggle, Home/End, double-click) animate; a drag never
     does — an animated width fights the pointer and costs an extra WebGL
     buffer reallocation every frame. */
  transition: width .15s ease;
}
.pf-rail[inert] { border-left-width: 0; }

/* Head and foot are flex-fixed rather than sticky, so the scroll container is
   exactly .pf-rail-body. On a full-height rail the export buttons must never
   scroll out of reach. */
.pf-rail-head, .pf-rail-foot { flex: none; }
.pf-rail-head { padding: 12px var(--pf-rail-pad); border-bottom: 1px solid var(--pf-border); }
.pf-rail-foot { padding: 12px var(--pf-rail-pad); border-top: 1px solid var(--pf-border); }

/* Foot ACTIONS, the counterpart to app.css's `#viewbar button`: any button a
   host puts in the foot inherits the shared chrome, so a host-drawn control
   sits beside a built-in one without restating the metrics — or, more to the
   point, without re-deriving the states. Both hosts had independently written
   the same padding/radius/mono stack, and only one of them had remembered to
   exclude :disabled from :hover, so a dead button still lit up under the
   pointer in the other. That bug is unreachable from here.

   Buttons only; the ROW is the host's. A foot can hold a bare row
   (partforge-cloud) or a labelled group (the demo pages' .dl/.dl-head), and a
   container rule here would have to pick one. This mirrors the viewbar only
   as far as the analogy holds: there, the pill itself is a single known
   element partforge can own outright.

   Base is the quiet outline treatment, because that is the safe thing to
   inherit by accident; the loud one is opt-in via .pf-primary. Equal
   specificity to app.css's `.dl-row button`, which is imported after this
   file and therefore still wins — the legacy download row renders exactly as
   before, and can drop .dl-row whenever it likes. */
.pf-rail-foot button {
  flex: 1;
  padding: 8px 0;
  border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-control);
  background: transparent;
  color: var(--pf-text-2);
  font-family: var(--pf-mono);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.06em;
  cursor: pointer;
}
.pf-rail-foot button:hover:not(:disabled) {
  border-color: var(--pf-accent);
  color: var(--pf-text-strong);
}
.pf-rail-foot button:disabled { opacity: .45; cursor: default; }

/* The foot's main call to action — accent-filled, so it reads as the primary
   thing to do with a finished part. */
.pf-rail-foot button.pf-primary {
  border-color: var(--pf-accent);
  background: var(--pf-accent);
  color: var(--pf-on-accent);
}
.pf-rail-foot button.pf-primary:hover:not(:disabled) {
  background: color-mix(in oklab, var(--pf-accent) 88%, #000);
  border-color: color-mix(in oklab, var(--pf-accent) 88%, #000);
}

/* A square icon-only action sitting beside the primary one. Sized to match
   the viewbar's icon buttons, and flex-none so the primary action keeps the
   remaining width rather than the two splitting it evenly. */
.pf-rail-foot button.pf-icon {
  flex: 0 0 auto;
  width: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--pf-muted);
}
.pf-rail-foot button.pf-icon:hover:not(:disabled) {
  background: var(--pf-surface-2);
  border-color: var(--pf-border);
  color: var(--pf-text-2);
}

.pf-rail-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Rail-head title/subtitle typography — class-only (see this file's header:
   no host-supplied id can be assumed), so a host that adopts this sheet with
   its own ids (e.g. partforge-cloud) gets a correctly-shaped rail head with
   correctly-styled text, not just a correctly-shaped empty box. Legacy
   id-only markup (a bare <h1>/.sub with no .pf-rail-head wrapper) can't be
   reached from here without an id selector, so app.css restates the same
   two rules scoped to #panel for that one case — see the comment there. */
.pf-rail-head h1 {
  font-size: 14px; margin: 0 0 2px; color: var(--pf-text-strong); letter-spacing: -0.01em;
}
.pf-rail-head .sub {
  font-family: var(--pf-mono); color: var(--pf-muted); font-size: 10px;
  letter-spacing: 0.04em; text-transform: uppercase;
  margin: 2px 0 0;
}

/* ---- resize seam --------------------------------------------------------
   An OVERLAY, not a flex item. partforge-cloud's seam is a real 12px column
   because its card is inset from the window with a gutter to live in; this rail
   is flush against the viewer behind a hairline, so a flex item would open a
   visible stripe of page background.

   max(0px, …) is what parks the seam flush at the window edge when the rail is
   collapsed, so a fresh drag can pull it back out. The element is created by
   rail.js — no host markup declares it. */
.pf-rail-seam {
  position: absolute;
  top: 0; bottom: 0;
  right: max(0px, calc(var(--pf-rail-w) - 6px));
  z-index: 20;
  width: 12px;
  display: flex; align-items: center; justify-content: center;
  touch-action: none;
  cursor: ew-resize;
}
/* Collapsed, the only legal direction is left. */
.pf-rail-seam[data-collapsed] { cursor: w-resize; }
.pf-rail-seam:focus-visible { outline: none; }
/* Invisible at rest; the affordance is a short centred pill that appears only
   on hover, keyboard focus, or during a drag. */
.pf-rail-seam > span {
  pointer-events: none;
  width: 3px; height: 100px; border-radius: 999px;
  background: transparent;
  transition: background-color .12s ease;
  /* Nudges only the pill 5px off the divider hairline it otherwise sits flush
     against — the seam's own 12px hit target stays centred on the boundary
     (that line is what a user aims at to drag) and must NOT move. */
  transform: translateX(-5px);
}
.pf-rail-seam:hover > span,
[data-pf-dragging] .pf-rail-seam > span { background: var(--pf-muted); }
/* Keyboard focus must read as distinct from hover/drag, not just present —
   the accent pill, not the muted one, is the only thing that says "focus
   landed here" for a keyboard user. */
.pf-rail-seam:focus-visible > span { background: var(--pf-accent); }

/* While dragging, the cursor must stay correct even when the pointer is out
   over the viewer, and the viewer must not react to it. Pointer capture keeps
   the events coming; these two rules are the second belt. */
[data-pf-dragging] { cursor: ew-resize; user-select: none; }
[data-pf-dragging] .pf-stage { pointer-events: none; }
[data-pf-dragging] .pf-rail,
[data-pf-key-resizing] .pf-rail { transition: none; }

/* ---- floating chrome: PLACEMENT ONLY, absolute within the stage ----------
   Deliberately no appearance here. partforge-cloud's sandbox.css re-anchors
   #viewbar's position with its own rules but inherits the pill's chrome
   (background/border/radius/shadow) from app.css, so that chrome must live in
   app.css ungated — not be duplicated into a class the cloud never sets. This
   file owns where things sit; app.css owns what they look like. */
.pf-float-tabs, .pf-float-viewbar, .pf-float-rail-toggle { position: absolute; z-index: 15; }
/* Every inset below is SHARED with whichever other float sits on that edge, so
   the stage's margin is stated once per edge and not once per element. The rail
   toggle (2026-08-20: out of #viewbar's pill, floating on its own at the top
   right) is the tab strip's `top` with the viewbar's `right` — a corner
   symmetric with the viewbar's, by construction rather than by two numbers that
   happen to agree today. Retune the margin and all three corners follow.
   Appearance, including the `[hidden]` guard that lets rail.js hide the toggle
   below the narrow breakpoint, is in app.css. */
.pf-float-tabs, .pf-float-rail-toggle { top: 12px; }
.pf-float-viewbar, .pf-float-rail-toggle { right: 12px; }
.pf-float-tabs { left: 50%; transform: translateX(-50%); }
.pf-float-viewbar { bottom: 12px; }

/* ---- view cube stack: PLACEMENT ONLY (see the rule above) ----------------
   Bottom-right, stacked above #viewbar. The offset is measured, not
   hardcoded: mount.js publishes --pf-viewbar-clear on the stage from a
   ResizeObserver on #viewbar, mirroring --pf-anim-clear. It sits there rather
   than in viewcube-controls.js (the precedent being animation-controls.js,
   which publishes its own --pf-anim-clear) because the value describes the
   VIEWBAR's vertical claim, not the cube's, and mount owns both elements. The
   56px fallback is the standard viewbar's 12px bottom + 44px height, so the
   stack still sits right if the observer never fires.

   The VIEW STYLE button (view-style-controls.js) now lives in #viewbar, the
   bottom toolbar, in its APPEARANCE group beside #theme (2026-09-24, at the
   user's request after a live mock, following Fusion 360's
   display-settings-in-the-nav-bar convention) — the cube's corner is clear
   again. Only a host with NO #viewbar still gets it here, over the cube's
   bottom-right corner (the `.pf-viewcube-stack > .pf-view-style-button`
   rule below is scoped to exactly that fallback). Its history: the
   PROJECTION TOGGLE it replaced had moved three times (through 2026-08-19 in
   its own `.pf-viewcube-pill` card BELOW the cube, the stack a column;
   earlier on 2026-08-20 a bare circle BESIDE the cube, the stack a
   `row-reverse` flex row 167px wide at the full cube; later that day OVER
   the cube's bottom-right corner). The view style button took that corner;
   for a few hours on 2026-09-24 it moved out beside the cube, to its left
   (at 34px and card-chromed it covered a third of the 101px narrow cube, and
   beside the cube it had to dodge the animation transport bar); later that
   day it came back over the corner ("to the bottom right of the direction
   widget"), at 30px; and later still it moved into the bottom toolbar.

   In the fallback it sits inside the stack's box, so it cannot change the
   stack's measured size — the size viewcube-controls.js publishes as
   data-pf-w/h and the transport bar's crowding rule reads is still the
   canvas alone (135px, or 101 below the narrow breakpoint). It is a later
   sibling than the cube's wrapper, so it paints over the canvas with no
   z-index of its own.

   The stack keeps its right edge on the viewbar's (`.pf-float-viewbar` above
   is `right: 12px` too), so the cube still lines up with the toolbar by
   construction. The popover is NOT in the stack: it is appended to the stage
   and placed at open time (above the toolbar pill, or above the fallback
   button).

   The 8px that used to sit between the stack and the viewbar is now 3px
   (2026-08-20: "lower the whole box so it's closer to the bottom bar"). 3
   rather than 0 because the stack's own box MUST NOT overlap #viewbar: the
   stack is a later sibling at the same z-index, and its canvas is
   pointer-events:auto across its whole footprint, so any vertical overlap
   would silently swallow clicks on the viewbar's rightmost buttons (both are
   right: 12px). --pf-viewbar-clear is published rounded to whole px, so a
   fractional viewbar height can move this by half a pixel either way; 3px
   absorbs that and still reads as a deliberate gap rather than a collision.
   The other ~5px of the lowering comes from inside the canvas — see
   cube-geom.js's CUBE_DOWN_BIAS_PX; that is where the rest of the perceived
   gap actually lives, and it is capped by the axis labels' clearance, so
   this pair (3 + 5) is the whole slack that exists without either overlapping
   the viewbar or shrinking the cube.

   The stack itself is pointer-transparent. Its box IS the canvas's box, so
   there is no dead gap in it; the canvas (and a fallback view style
   button) opt back in below. The
   declaration stays because the stack's own box is still an element over the
   viewer, and a stray hit on it (its padding-free edges, or a future child
   before it opts in) should reach the model behind rather than be swallowed. */
.pf-viewcube-stack {
  position: absolute;
  right: 12px;
  bottom: calc(var(--pf-viewbar-clear, 56px) + 3px);
  z-index: 15;
  display: flex;
  pointer-events: none;
}
.pf-viewcube-stack[hidden] { display: none; }
.pf-viewcube-canvas, .pf-viewcube-stack > .pf-view-style-button { pointer-events: auto; }
/* The fallback only (no #viewbar): over the cube's bottom-right corner.
   Absolute against the stack, which is itself absolute and so already the
   containing block. In #viewbar the button is an ordinary flex item. */
.pf-viewcube-stack > .pf-view-style-button { position: absolute; right: 0; bottom: 0; }

/* The keyboard surface: six per-view buttons standing in for the DOM focus a
   canvas cannot give us. Visually hidden rather than display:none — the latter
   takes them out of the tab order, which is the whole point of them.

   The hiding properties are on the BUTTONS, not on their wrapper, so that
   :focus-visible can undo them. A non-`none` clip-path clips the element's
   whole SUBTREE and makes the element a containing block for fixed-position
   descendants, so with the clip on the wrapper no rule on a focused child
   could escape it — the reveal below was dead CSS, and six buttons sat in the
   tab order with no visible focus indicator at all. On the buttons themselves
   there is no clipping ancestor to get out of.

   The wrapper keeps only `position: absolute`, which takes it out of the
   stack's flex flow. That mattered when the stack had a gap (a zero-height
   flex item still earned it, pushing the cube out of place); it still matters
   now that it does not, because an in-flow second item would widen the stack
   past the canvas — and the stack's width is what the crowding rule reads. */
.pf-viewcube-key { position: absolute; }
.pf-viewcube-key button {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.pf-viewcube-key button:focus-visible {
  position: fixed;
  width: auto; height: auto;
  margin: 0; padding: 2px 6px;
  overflow: visible;
  clip-path: none;
  pointer-events: auto; /* the stack is pointer-transparent; a revealed control is not */
}

/* --- animation transport bar (generated by animation-controls.js) ----------
   PLACEMENT ONLY, per the rule above; appearance lives in app.css next to
   #viewbar's, so a host that re-anchors this bar still inherits its chrome.
   animation-controls.js may inline-override left/transform/max-width (and
   overflow while width-capped) to hold a 10px gap to #viewbar, and clears
   the overrides whenever centered placement fits.

   animation-controls.js also publishes --pf-anim-clear on the STAGE element:
   the px distance from the stage's bottom edge to the visible bar's top (0px
   while the active view has no animations; unset when no view declares any).
   A host floating its own chrome at the stage's bottom-centre should anchor it
   at calc(var(--pf-anim-clear, 0px) + <gap>) to stack above the bar. */
.pf-anim-bar {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
  z-index: 15; max-width: calc(100% - 24px);
}

/* ---- the narrow-layout tab bar ------------------------------------------
   Created by mobile-tabs.js (no host markup declares it). Hidden by default:
   below the breakpoint the rail sits beside the viewer and needs no tab, so
   the media query below is what reveals it.

   The [hidden] rule is not redundant. `hidden` is how mobile-tabs.js stands
   the bar down when a HOST owns pane selection, and the UA's
   [hidden] { display: none } is a UA-origin rule that the media query's
   author-origin display:flex would otherwise beat — leaving a second,
   competing tab bar on screen inside the host's own. */
.pf-tabbar {
  display: none;
  flex: none;
  border-top: 1px solid var(--pf-border);
  background: var(--pf-surface);
  color: var(--pf-text);
  /* The home indicator on a notched phone. Zero everywhere else. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.pf-tabbar[hidden] { display: none; }
.pf-tabbar button {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 6px 0;
  border: 0;
  background: transparent;
  color: var(--pf-muted);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.02em;
  cursor: pointer;
}
.pf-tabbar button[aria-pressed="true"] { color: var(--pf-accent); }

/* ---- narrow layout: ONE pane at a time, chosen by a tab bar ---------------
   The old layout here stacked the rail as a 45vh strip under the viewer, which
   left both panes too cramped to use on a phone. Instead the shell becomes a
   column of [pane][tab bar] and shows exactly one pane, full height, keyed on
   data-pf-pane (written by mobile-tabs.js, or by a host through its
   setHostPane).

   Written as :not([data-pf-pane="rail"]) so a MISSING attribute falls to the
   stage rather than showing both panes at once — the layout is correct from
   first paint, before any JS runs.

   The seam stays hidden and resize is absent at this width (rail.js also
   refuses to start a drag). Collapse has no meaning here either: rail.js
   suppresses it entirely below this breakpoint, so there is deliberately no
   .pf-rail[inert] rule left — nothing sets inert at this width. */
@media (max-width: 719px) {
  .pf-shell { flex-direction: column; }
  .pf-shell:not([data-pf-pane="rail"]) .pf-rail { display: none; }
  .pf-shell[data-pf-pane="rail"] .pf-stage { display: none; }
  /* Whichever pane shows fills the column. --pf-rail-w, the left border, and
     the inset shadow all existed to make the rail read as a set-back right
     EDGE; none of that means anything when the rail IS the whole surface. */
  .pf-rail {
    flex: 1;
    width: auto;
    border-left: 0;
    box-shadow: none;
  }
  .pf-rail-seam { display: none; }
  .pf-tabbar { display: flex; }
  /* Lift the transport bar clear of the viewbar. On a phone the bar is nearly
     the full stage width (max-width: 100% - 24px) while #viewbar sits at the
     bottom-right of the same stage, at the same z-index — so at bottom: 14px
     the transport bar paints straight over cutaway/reframe/theme and
     makes them unclickable. The viewbar occupies 12px…56px from the bottom
     (12px offset + 4px padding + 34px button + 4px padding + 2px border);
     64px clears it with an 8px gap. */
  .pf-anim-bar { bottom: 64px; }
}

/* Placement half of app.css's touch layout — same query, so the two must be
   edited together. That layout stacks the bar into rows, and a shrink-to-fit
   absolutely-positioned box sizes itself to its widest single item (195px,
   measured at 390px) rather than to the room available, wrapping every row far
   earlier than it needs to. State the width outright.

   The lift off the viewbar comes along for a landscape phone, which is wider
   than 719px and so misses the block above. It also keeps this clear of the
   viewbar CLAMP in animation-controls.js: with the bands no longer
   intersecting, applyPlacement() returns before touching left/transform, so
   the stacked bar stays centred instead of being slid sideways by a rule that
   was reasoning about a single-row bar.

   The cap is for a tablet — a coarse pointer on a wide stage — where an
   unbounded stacked bar would span 700px+ and strand the reset button across a
   mostly empty row. A phone is narrower than the cap and still fills its width. */
@media (max-width: 719px), (pointer: coarse) {
  .pf-anim-bar { width: calc(100% - 24px); max-width: 520px; bottom: 64px; }
}

/* ---- host-driven rail layouts (partforge-cloud) ---------------------------
   data-pf-rail-layout is written by mobile-tabs.js from the host's
   setRailLayout. Two modes, both host-only (nothing in partforge's own UI can
   enter them):

   DOCK — the host draws a bottom sheet over the iframe. --pf-rail-inset is the
   sheet's full height: the shell pads by it so the stage (and its bottom
   chrome) clears the sheet. With a "rail" lease the rail renders INTO the
   sheet region — but only its bottom --pf-rail-dock-h px, because the host's
   opaque chrome strip (handle + tabs) covers the top of the region. The spec
   (2026-08-31-responsive-layout-redesign) has the two-numbers reasoning. Note
   that a dock lease ABOVE 719px does not enter the block below at all: the
   rail stays side by side with the stage at its usual --pf-rail-w (rail.js
   keeps the width there, and only suspends resize/collapse/toggle).

   OVERLAY — the rail is a right-edge drawer OVER the stage, open while
   data-pf-rail-open is present (rail.js's toggle sets it; mobile-tabs.js's
   stage pointerdown clears it). The stage never resizes. Width is its own
   min(288px, 85%) — NOT --pf-rail-w, which rail.js zeroes under an overlay
   lease at any window width (and under any layout below the breakpoint).

   These come after the narrow block above because they override its one-pane
   hiding, and before the reduced-motion block below so that block's
   transition:none wins the tie at equal specificity. */
.pf-shell[data-pf-rail-layout="dock"] { padding-bottom: var(--pf-rail-inset, 0px); }
@media (max-width: 719px) {
  /* pane=rail on a phone: stage above, rail docked in the sheet region. The
     two rules below override the narrow block's one-pane hiding.

     The shell keeps its --pf-rail-inset padding in this pane too — the stage
     is the same height whichever pane shows — and the rail is positioned
     absolutely INTO that padding, filling the sheet region's bottom
     --pf-rail-dock-h px. The difference between the two lengths is the strip
     the host draws across the top of the sheet region, and it must stay
     empty: an earlier version zeroed the padding and stacked the rail
     straight under the stage in flow, which sent the stage's bottom
     --pf-rail-inset − --pf-rail-dock-h px (the strip's height) UNDER the
     host's opaque strip — with the viewbar, the view cube and the transport
     bar behind it, unreachable, on every rail-pane sheet position. */
  .pf-shell[data-pf-rail-layout="dock"][data-pf-pane="rail"] .pf-stage { display: block; }
  .pf-shell[data-pf-rail-layout="dock"][data-pf-pane="rail"] .pf-rail {
    display: flex;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: var(--pf-rail-dock-h, 0px);
    width: auto;
    border-left: 0;
    border-top: 1px solid var(--pf-border);
    box-shadow: none;
  }
}
.pf-shell[data-pf-rail-layout="dock"] .pf-rail-seam { display: none; }

/* visibility, not just the transform: collapse is suppressed under a host
   layout (rail.js), so nothing else makes a shut drawer inert — it would sit
   off-canvas as a run of invisible tab stops. The delay is what keeps it on
   screen for the 200ms it takes to slide out; opening flips it back at once,
   which is why the open state restates the transition without it. */
.pf-shell[data-pf-rail-layout="overlay"] .pf-stage { display: block; }
.pf-shell[data-pf-rail-layout="overlay"] .pf-rail {
  display: flex;
  position: absolute;
  top: 0; right: 0; bottom: 0;
  flex: none;
  width: min(288px, 85%);
  border-left: 1px solid var(--pf-border);
  box-shadow: var(--pf-shadow-rail), -16px 0 32px rgb(0 0 0 / .35);
  transform: translateX(102%);
  visibility: hidden;
  transition: transform 200ms ease, visibility 0s 200ms;
  z-index: 30;
}
.pf-shell[data-pf-rail-layout="overlay"][data-pf-rail-open] .pf-rail {
  transform: translateX(0);
  visibility: visible;
  transition: transform 200ms ease;
}
.pf-shell[data-pf-rail-layout="overlay"] .pf-rail-seam,
.pf-shell[data-pf-rail-layout="overlay"] .pf-tabbar { display: none; }

/* ---- reduced motion -----------------------------------------------------
   Collapsing the rail slides 288px of layout across the screen — the first
   layout-scale animation in the framework, and the kind of movement a
   vestibular-sensitive user actually feels. Honour the preference: the rail
   still collapses and still resizes, it just arrives instead of travelling.
   Scoped to what this layout introduces; the pre-existing busy spinner is a
   state indicator and is left alone. */
@media (prefers-reduced-motion: reduce) {
  .pf-rail, .pf-rail-seam > span { transition: none; }
  /* The overlay drawer states its own transition twice, at higher specificity
     than the blanket rule above, so both states have to be named here or the
     drawer would keep sliding. Killing the transition also drops the closing
     visibility delay, which is right: with no slide there is nothing to wait
     for, and the drawer leaves the a11y tree the moment it shuts. */
  .pf-shell[data-pf-rail-layout="overlay"] .pf-rail,
  .pf-shell[data-pf-rail-layout="overlay"][data-pf-rail-open] .pf-rail { transition: none; }
}

/* ---- annotation ink layer: a transparent 2D canvas over the viewer --------
   Shown only while annotation mode is on. It deliberately owns pointer events
   while visible — that is what freezes orbit/pan/zoom during drawing. #viewbar
   itself is hidden for the duration (mount.js toggles both), replaced by
   .pf-sketch-toolbar (app.css, z 20) which now holds Undo/Clear/Send; this
   layer sits below that at z 10 so the toolbar stays clickable over it. */
.pf-ink-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 10;
  /* Pencil cursor (the viewbar toggle's lucide pencil), hotspot at the tip.
     White halo under a dark stroke keeps it readable over both the model and
     the backdrop; crosshair is the no-custom-cursor fallback. */
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cg stroke='white' stroke-width='4.5'%3E%3Cpath d='M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z'/%3E%3Cpath d='m15 5 4 4'/%3E%3C/g%3E%3Cg stroke='%231d232b' stroke-width='2'%3E%3Cpath d='M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z'/%3E%3Cpath d='m15 5 4 4'/%3E%3C/g%3E%3C/svg%3E") 2 22, crosshair;
  touch-action: none;
}
.pf-ink-canvas[hidden] { display: none; }
