/* Shared chrome styles for any part app (panel, tabs, viewbar, download row,
   busy overlay) + the light/dark palettes. Imported by framework/mount.js, so
   every part-app gets it; each part's HTML only carries structural markup.
   Skinned to the shared design language (partforge-cloud spec
   2026-07-17-shared-design-language-design.md): zinc-tuned neutrals, blue
   accent, shadcn-shaped radii/shadows/focus rings — plain CSS on --pf-*.
   See docs/AUTHORING-PARTS.md. */
@import "./tokens.css"; /* palette + light overrides; also exported standalone as partforge/tokens.css */
@import "./chrome.css";  /* reusable layout; also exported as partforge/chrome.css */

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; overflow: hidden;
  font: 13px/1.4 var(--pf-sans); }
/* Legacy fallback: a page whose markup predates .pf-stage keeps the old
   full-window viewer. :not() rather than a plain #app rule because an id would
   outrank .pf-stage's positioning and win the cascade. */
#app:not(.pf-stage) { position: fixed; inset: 0; background: var(--pf-bg); }
canvas { display: block; }

/* Legacy fallback: the pre-rail floating card, for markup without .pf-rail. */
#panel:not(.pf-rail) {
  position: fixed; top: 12px; left: 12px; width: 256px;
  max-height: calc(100vh - 24px); overflow-y: auto; z-index: 10;
  background: var(--pf-surface); border: 1px solid var(--pf-border); border-radius: 16px;
  padding: 14px; color: var(--pf-text);
  box-shadow: var(--pf-shadow-float);
}
/* Legacy fallback only: pre-rail markup has a bare <h1>/.sub directly inside
   #panel, with no .pf-rail-head wrapper, so chrome.css's classed
   .pf-rail-head h1/.sub rule (the single source of truth for that typography)
   can't reach it — chrome.css is deliberately id-free (see test/tokens.test.js)
   so this id-scoped restatement has to live here instead. Classed markup never
   hits this rule (its h1 is inside .pf-rail-head, already styled by
   chrome.css); keep the two in sync if the typography ever changes. */
#panel h1 { font-size: 14px; margin: 0 0 2px; color: var(--pf-text-strong); letter-spacing: -0.01em; }
#panel .sub {
  font-family: var(--pf-mono); color: var(--pf-muted); font-size: 10px;
  letter-spacing: 0.04em; text-transform: uppercase;
  margin: 2px 0 0;
}

.seg { display: flex; gap: 2px; margin-bottom: 12px;
  background: var(--pf-surface-2); border-radius: var(--pf-radius-control); padding: 3px; }
.seg button {
  flex: 1; padding: 6px 0; border: 0; border-radius: var(--pf-radius-control);
  background: transparent; color: var(--pf-muted); cursor: pointer;
  font-family: var(--pf-mono); font-size: 11px; letter-spacing: 0.02em;
}
.seg button:hover { color: var(--pf-text-2); }
.seg button.on { background: var(--pf-accent); color: var(--pf-on-accent); }

/* Full-bleed rows: the divider spans the rail's whole width while the content
   sits at the rail's own padding, so each slider gains the ~22px the old box
   border + padding used to take from both sides. */
.section {
  /* No vertical padding at the top: the header band (below) owns the space
     around the title so a section's title sits at the same offset whether
     the section is open or collapsed — toggling must not move it. */
  padding: 0 var(--pf-rail-pad) 11px;
}
/* Divider BETWEEN visible sections. `~` walks all preceding siblings, and a
   relevance-hidden section (.section-hidden, display:none) fails the :not(),
   so the topmost VISIBLE section never draws a hairline under the rail header.
   Condition-hidden sections (.hidden, a `when` that evaluated false) are excluded
   for the same reason. Do not simplify this to `.section:first-child { border-top: 0 }` — that
   matches DOM position rather than visibility, and leaves a stray divider
   floating at the top whenever applyRelevance hides the first section. A bare
   sub-panel section (host.controls inside a custom control) never gets the divider
   because the widget owns its framing. */
.section:not(.section-hidden):not(.hidden) ~ .section:not(.section-hidden):not(.hidden):not(.bare) {
  border-top: 1px solid var(--pf-border);
}
/* Section disclosure header: the whole row is the click target (render.js
   puts the toggle listener on the header; the title button's click bubbles
   to it and the ⓘ stops propagation). Full-bleed: negative margins cancel
   the rail padding so the hover band and the collapsed rules run edge to
   edge while the text keeps the rail's alignment. Row order: title (flex:1),
   ⓘ, chevron on the far right. */
.sec-header {
  display: flex; align-items: center; gap: 6px;
  margin: 0 calc(-1 * var(--pf-rail-pad)) 6px;
  /* The same 13px band open or collapsed: the title never moves on toggle
     and the hover target keeps one size. */
  padding: 13px var(--pf-rail-pad);
  cursor: pointer;
}
.sec-header:hover { background: var(--pf-surface-2); }
.sec-title {
  flex: 1; display: flex; align-items: center;
  margin: 0; padding: 0; border: 0; background: transparent; cursor: pointer;
  text-align: left;
  font-family: var(--pf-mono); font-size: 12px; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--pf-text-2);
}
.sec-header:hover .sec-title { color: var(--pf-text); }
/* Disclosure triangle: closes the header row on the right, after the ⓘ.
   Glyph via ::before — the span must stay text-free (tests match .sec-title
   by exact textContent). Rotation keys off the collapsed class because the
   chevron sits beside, not inside, the aria-carrying button. */
/* A real equilateral triangle (clip-path, not a font glyph): the box is
   exactly the triangle's bounds, so rotating about the center is wobble-free
   — a font's ▾ sits off-center in its em box and lurches when rotated.
   Height = width × √3/2 keeps it equilateral. */
.sec-header .chev, .adv-header .chev {
  display: inline-block; width: 8px; height: 7px; color: var(--pf-muted-2);
  transition: transform 0.15s ease;
}
.sec-header .chev::before, .adv-header .chev::before {
  content: ""; display: block; width: 100%; height: 100%;
  background: currentColor;
  clip-path: polygon(50% 100%, 0 0, 100% 0);
}
.section.collapsed > .sec-header .chev, .adv-wrap.collapsed > .adv-header .chev { transform: rotate(-90deg); }
/* Collapsed section: the header band sits flush between the section's top
   divider and its own bottom rule — text vertically centered, hover and
   click covering the entire band. The -1px bottom margin overlaps this rule
   with the next section's border-top so they read as one line. */
.section.collapsed {
  padding-bottom: 0;
  border-bottom: 1px solid var(--pf-border); margin-bottom: -1px;
}
.section.collapsed .sec-header { margin-bottom: 0; }
.sec-body.hidden { display: none; }
/* Native menulist arrows hug the right border and ignore padding, so the
   panel's selects draw their own — the header chevrons' equilateral triangle
   as a data-URI, inset 10px from the edge. Fixed slate fill: url() can't
   read theme variables, and this shade reads on both themes. */
select.preset, select.select-input {
  width: 100%; background: var(--pf-input-bg); color: var(--pf-text-2);
  border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control);
  padding: 7px 26px 7px 9px;
  font-family: var(--pf-mono); font-size: 11px;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='7'%3E%3Cpolygon points='0,0 8,0 4,7' fill='%238b8b94'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}
.feat { display: flex; align-items: center; gap: 8px; margin: 6px 0;
  color: var(--pf-text-2); cursor: pointer; }
.feat input { cursor: pointer; accent-color: var(--pf-accent); }
.feat-group { margin: 2px 0 8px; padding-left: 10px; border-left: 1px solid var(--pf-border); }
.feat-group.hidden { display: none; }
/* Condition-hidden nodes. Disclosure state uses `.hidden` on `.adv` /
   `.sec-body` (rules above); everything else carrying `.hidden` inside the
   panel is a `when` that evaluated false. */
.section.hidden, .slider.hidden, .feat.hidden, select.preset.hidden { display: none; }
/* Inner fold header: same row anatomy as .sec-header (title left, chevron
   right, whole row clickable) at the subordinate scale. Spans the fold's own
   width — not full-bleed — so it never crosses a feat-group's left border;
   small negative margins let the hover band breathe past the text without
   moving it. */
.adv-header {
  display: flex; align-items: center; gap: 6px;
  margin: 8px -6px 0; padding: 8px 6px;
  /* Transparent borders reserve the collapsed rules' pixels so toggling
     swaps only their color — the title never moves and the hover target
     keeps one size, matching the section-header treatment. */
  border-top: 1px solid transparent; border-bottom: 1px solid transparent;
  border-radius: 6px; cursor: pointer;
}
.adv-header:hover { background: var(--pf-surface-2); }
.adv-toggle {
  flex: 1; margin: 0; padding: 0; border: 0;
  background: transparent; color: var(--pf-muted); cursor: pointer;
  font-family: var(--pf-mono); font-size: 12px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase; text-align: left;
}
.adv-header:hover .adv-toggle { color: var(--pf-muted-2); }
/* Collapsed fold: the reserved borders take color — rules above and below,
   text vertically centered between them (the section-band treatment at fold
   scale). Square corners while the rules show. */
.adv-wrap.collapsed > .adv-header {
  border-top-color: var(--pf-border); border-bottom-color: var(--pf-border);
  border-radius: 0;
}
.adv.hidden { display: none; }
.adv { margin-top: 4px; }

.slider { margin: 9px 0; }
.row { display: flex; justify-content: space-between; align-items: center;
  margin: 0 0 4px; gap: 8px; }
.row label { font-family: var(--pf-mono); font-size: 11px; color: var(--pf-muted-2); letter-spacing: 0.01em; }
.row .val { display: flex; align-items: baseline; gap: 4px; flex: none; }
.row .num {
  width: 54px; text-align: right; font-family: var(--pf-mono); font-size: 12px; font-variant-numeric: tabular-nums;
  background: var(--pf-input-bg); color: var(--pf-text-strong);
  border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 3px 6px;
}
.row .num:focus { outline: none; border-color: var(--pf-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
.row .num::-webkit-outer-spin-button, .row .num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.row .num { -moz-appearance: textfield; }
.row .unit { font-family: var(--pf-mono); color: var(--pf-muted); font-size: 10px; }
.text-input {
  display: block; width: 100%; font: 12px/1.4 var(--pf-mono);
  background: var(--pf-input-bg); color: var(--pf-text-strong);
  border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 6px 8px;
}
textarea.text-input { min-height: 64px; resize: vertical; }
.text-input:focus { outline: none; border-color: var(--pf-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }

/* ── file drop targets ──────────────────────────────────────────────────────
   Two forms of the same widget. The LABELLED form is a tile the user is meant
   to find: it doubles as the thumbnail once there is something to show, so a
   preview, a drop zone and a click-to-choose occupy one box rather than three.
   The AMBIENT form is for a control that already has a visible way in (the font
   control's catalog button) — it covers the control invisibly and only appears
   while a file is over it, so dropping stays available without spending rail
   height advertising itself. */
.file-drop {
  position: relative; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px;
  min-height: 56px; margin-top: 6px; padding: 8px;
  background: var(--pf-input-bg); color: var(--pf-hint);
  border: 1px dashed var(--pf-border); border-radius: var(--pf-radius-control);
  text-align: center; cursor: pointer;
}
.file-drop:focus-visible { outline: none; border-color: var(--pf-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
.file-drop-hint { font: 11px/1.35 var(--pf-sans); }

/* `contain` rather than `cover` because artwork and depth maps are content, not
   decoration — cropping one to fill the box would misrepresent what is about to
   be built. The two preview kinds then diverge, because a photograph and a piece
   of line art want opposite things from the box around them. */
.file-drop > svg, .file-drop > img {
  display: block; max-width: 100%; max-height: 96px; object-fit: contain;
}
.file-drop > svg { fill: var(--pf-text-2); }
/* Once there IS a preview, the frame moves off the tile and onto the artwork.
   The tile stays full width — it is the drop target, and a full-width one is
   easier to hit — but goes invisible, so what the eye sees is a rounded box
   hugging the picture with the hint underneath it, rather than a picture
   floating inside a much larger box. The border stays at 1px and only loses its
   colour, so nothing shifts when a thumbnail arrives. */
.file-drop.has-thumb {
  border-color: transparent; background: none; padding: 0; gap: 4px;
}
.file-drop.has-thumb > img, .file-drop.has-thumb > svg {
  border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-control);
  background: var(--pf-input-bg);
}
/* A RASTER is a filled rectangle, so the border sits directly on the pixels —
   padding around one reads as a mount around a picture. `border-radius` on an
   `<img>` clips the image itself, which is what rounds the corners. Sized by
   `max-height` rather than `height` so a small source is shown at its own size
   instead of being upscaled into a blur.
   `width: auto` is load-bearing: this element also carries `.image-preview`,
   which pins it to `width: 100%` and letterboxes the picture inside a
   rail-width box with `object-fit`. That is the right behaviour for a preview
   that fills its container and the wrong one for a frame meant to hug, and it
   is invisible in the markup — the box just silently stops matching the image. */
.file-drop.has-thumb > img { max-height: 96px; max-width: 100%; width: auto; margin: 0; }
/* A VECTOR is line art: strokes running into the border would collide with it
   rather than be framed by it, so it keeps padding inside its own frame. Sizing
   by height with `width: auto` lets the viewBox's aspect supply the width, so
   the frame hugs the drawing instead of the SVG's default 300x150 box. */
.file-drop.has-thumb > svg { height: 96px; width: auto; max-width: 100%; padding: 8px; }

/* The empty-state hint and the replace hint trade places: exactly one is visible
   in either state, so a tile is never without an instruction. */
.file-drop-hint-replace { display: none; }
.file-drop.has-thumb .file-drop-hint { display: none; }
.file-drop.has-thumb .file-drop-hint-replace { display: block; }

/* Ambient: no box of its own. Laid over the control, inert to the pointer so
   every click still reaches the button underneath — the one affordance this
   form exists to protect. */
/* Only the controls that actually carry an ambient overlay become positioned —
   `.slider` is shared by every widget in the rail, and making all of them a
   containing block to serve one would be a wide change for a narrow need. */
.slider:has(> .file-drop-ambient) { position: relative; }

.file-drop-ambient {
  position: absolute; inset: 0; margin: 0; padding: 0;
  min-height: 0; background: none; border: 0; pointer-events: none;
}
/* `dragover` is the only thing that reveals it. Pointer events stay off even
   then: the drop event still fires on the element, and turning them on would
   swallow a click that merely passed through mid-drag. */
.file-drop-ambient.file-drop-over {
  border: 1px dashed var(--pf-accent); border-radius: var(--pf-radius-control);
  background: color-mix(in oklab, var(--pf-accent) 12%, transparent);
}

/* Shared drag feedback for the labelled form. */
.file-drop.file-drop-over {
  border-color: var(--pf-accent);
  background: color-mix(in oklab, var(--pf-accent) 12%, transparent);
}

.file-drop-error {
  margin-top: 5px; font: 11px/1.35 var(--pf-sans); color: var(--pf-err);
}

/* the `type: "font"` control — a button that shows the current face IN it */
.font-btn { width: 100%; display: flex; align-items: center; gap: 8px; text-align: left; cursor: pointer;
  background: var(--pf-input-bg); color: var(--pf-text-strong);
  border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 7px 9px; }
.font-btn:hover { border-color: color-mix(in oklab, var(--pf-accent) 45%, var(--pf-border)); }
.font-btn:focus-visible { outline: none; border-color: var(--pf-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
.font-btn .fname { flex: 1; min-width: 0; font-size: 15px; line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.font-btn .fvar { flex: none; font: 10px/1 var(--pf-mono); color: var(--pf-muted); }
.font-btn .caret { flex: none; opacity: .5; }
.text-input.warn { border-color: var(--pf-err); color: var(--pf-err); }

/* the `type: "image"` control — a live preview above either a URL field or a
   picker button (mirrors .font-btn; a thumbnail stands in for the in-face name). */
.image-preview { display: block; width: 100%; max-height: 120px; object-fit: contain;
  margin-bottom: 6px; border-radius: var(--pf-radius-control); background: var(--pf-input-bg); }
.image-btn { width: 100%; display: flex; align-items: center; gap: 8px; text-align: left; cursor: pointer;
  background: var(--pf-input-bg); color: var(--pf-text-strong);
  border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 7px 9px; }
.image-btn:hover { border-color: color-mix(in oklab, var(--pf-accent) 45%, var(--pf-border)); }
.image-btn:focus-visible { outline: none; border-color: var(--pf-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
.image-btn-thumb { flex: none; width: 28px; height: 28px; object-fit: cover;
  border-radius: calc(var(--pf-radius-control) - 2px); background: var(--pf-surface-2); }
.image-btn .iname { flex: 1; min-width: 0; font-size: 13px; line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.image-btn .caret { flex: none; opacity: .5; }

/* the image picker's thumbnail grid — same takeover chrome as the font picker
   (.picker/.pk-head/.pk-search), a grid in place of the virtualized list. */
.pk-img-grid { flex: 1; overflow-y: auto; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 8px;
  padding: 10px var(--pf-rail-pad) 12px; align-content: start; }
.pk-img-card { display: flex; flex-direction: column; gap: 4px; border: 1px solid transparent;
  border-radius: var(--pf-radius-control); background: transparent; cursor: pointer; padding: 4px; }
.pk-img-card:hover { border-color: color-mix(in oklab, var(--pf-accent) 45%, var(--pf-border)); }
.pk-img-card[data-sel="true"] { border-color: var(--pf-accent); background: var(--pf-accent-soft); }
.pk-img-thumb { width: 100%; aspect-ratio: 1; object-fit: cover;
  border-radius: calc(var(--pf-radius-control) - 2px); background: var(--pf-surface-2); }
.pk-img-cap { font: 9px/1.3 var(--pf-mono); color: var(--pf-hint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* crafted range slider — hairline track + CAD-blue handle (the panel's signature control) */
input[type="range"] { -webkit-appearance: none; appearance: none; width: 100%; height: 18px; margin: 0; background: transparent; cursor: pointer; }
input[type="range"]::-webkit-slider-runnable-track { height: 3px; border-radius: 2px; background: var(--pf-border); }
input[type="range"]::-moz-range-track { height: 3px; border-radius: 2px; background: var(--pf-border); }
input[type="range"]::-moz-range-progress { height: 3px; border-radius: 2px; background: var(--pf-accent); }
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 14px; height: 14px; margin-top: -5.5px;
  border-radius: 50%; background: var(--pf-accent); border: 2px solid var(--pf-surface); box-shadow: 0 0 0 1px var(--pf-accent);
  transition: box-shadow .12s ease;
}
input[type="range"]::-moz-range-thumb {
  width: 14px; height: 14px; border-radius: 50%; background: var(--pf-accent);
  border: 2px solid var(--pf-surface); box-shadow: 0 0 0 1px var(--pf-accent);
}
input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 5px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
input[type="range"]:focus-visible { outline: none; }
input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 5px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
input[type="range"]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 5px color-mix(in oklab, var(--pf-accent) 35%, transparent); }

/* recommended band: a tinted span of the track between --band-lo and --band-hi */
.slider.has-band input[type="range"]::-webkit-slider-runnable-track {
  background: linear-gradient(to right,
    var(--pf-border) var(--band-lo),
    color-mix(in oklab, var(--pf-accent) 30%, var(--pf-border)) var(--band-lo),
    color-mix(in oklab, var(--pf-accent) 30%, var(--pf-border)) var(--band-hi),
    var(--pf-border) var(--band-hi));
}
.slider.has-band input[type="range"]::-moz-range-track {
  background: linear-gradient(to right,
    var(--pf-border) var(--band-lo),
    color-mix(in oklab, var(--pf-accent) 30%, var(--pf-border)) var(--band-lo),
    color-mix(in oklab, var(--pf-accent) 30%, var(--pf-border)) var(--band-hi),
    var(--pf-border) var(--band-hi));
}
/* value box outside the recommended band */
.row .num.warn { border-color: var(--pf-err); color: var(--pf-err); }

button.action {
  width: 100%; margin-top: 8px; padding: 9px; border: 0; border-radius: var(--pf-radius-control);
  background: var(--pf-accent); color: var(--pf-on-accent); font-weight: 500; cursor: pointer;
}
button.action:hover:not(:disabled) { background: color-mix(in oklab, var(--pf-accent) 90%, black); }
button.ghost { background: transparent; border: 1px solid var(--pf-border); color: var(--pf-text-2); font-weight: 500; }
button.ghost:hover:not(:disabled) { background: var(--pf-surface-2); }
button.action:disabled { opacity: .5; cursor: default; }

/* Custom controls (type: "custom"): a part-authored widget in the rail. The
   slot inherits the rail's cascade (font, colours, --pf-* tokens); these rules
   give BARE elements the built-in controls' look so a widget written with
   plain <button>/<input>/<select> still reads as native, supply a themed
   floor for hand-drawn SVG, and provide two opt-in classes for SVG: .pf-hit
   (a clickable region) and .pf-drag (a dragged one — touch-action: none is
   what lets a finger drag it on a phone instead of scrolling the sheet). */
.pf-custom { margin: 9px 0; }
.pf-custom.hidden { display: none; }
.pf-custom-slot { display: flow-root; }
/* Every selector in this block is wrapped so the WHOLE thing resolves to
   (0,0,0) — including the :not([type=...]) chain, which is otherwise an
   attribute selector per :not() and would still out-specify a class rule
   like .text-input (0,1,0) or .row .num (0,2,0). At true zero specificity,
   any class rule wins, so a named built-in class (.seg button, .text-input,
   select.select-input, …) always beats these bare-element defaults. */
/* `fill` inherits in SVG, so this is the floor for everything the widget draws
   and did not colour — including <text>. SVG's own initial fill is BLACK, which
   is invisible on the dark theme, and that is what an author gets for free
   unless the rail supplies a token. Inherited rather than matched, so a child
   carrying its own fill="…" attribute still wins. */
:where(.pf-custom) :where(svg) { display: block; max-width: 100%; height: auto; fill: var(--pf-text-2); }
:where(.pf-custom) :where(button) {
  font: inherit; font-size: 12px; padding: 5px 9px; cursor: pointer;
  background: transparent; border: 1px solid var(--pf-border); color: var(--pf-text-2);
  border-radius: var(--pf-radius-control);
}
:where(.pf-custom) :where(button:hover:not(:disabled)) { background: var(--pf-surface-2); }
:where(.pf-custom) :where(button:disabled) { opacity: .5; cursor: default; }
:where(.pf-custom) :where(input:not([type="range"]):not([type="checkbox"]):not([type="radio"]), select) {
  font: 12px/1.4 var(--pf-mono);
  background: var(--pf-input-bg); color: var(--pf-text-strong);
  border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 6px 8px;
}
:where(.pf-custom) :where(input:focus, select:focus, button:focus-visible) {
  outline: none; border-color: var(--pf-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent);
}
/* A region the reader clicks. The RESTING fill and stroke are what make it read
   as a control: without them a .pf-hit polygon is a black slab with no border
   until it is hovered or selected. Zero specificity, so a widget that colours
   regions from its own data still wins — with an inline `style`, since a
   `fill="…"` attribute loses to any rule that matches the element, even this
   one. Hover and selected below keep their own (0,2,x) weight. */
:where(.pf-custom) :where(.pf-hit) { cursor: pointer; fill: var(--pf-surface-2); stroke: var(--pf-border); }
.pf-custom .pf-hit:hover { fill: color-mix(in oklab, var(--pf-accent) 25%, transparent); }
/* Selected is the SOLID accent, not a wash of it. A translucent accent lands on a
   different value in each theme — pale over the light background, dark over the
   dark one — so no single label colour is readable on both. Solid accent is one
   colour in both, which is what makes --pf-on-accent below a rule rather than a
   guess. A widget that would rather tint its own region overrides with `style`. */
.pf-custom .pf-hit.selected { fill: var(--pf-accent); stroke: var(--pf-accent); }
/* A label NESTED inside the region — the structure to recommend, because it is the
   only one where the rail can keep the text readable as the region changes colour
   underneath it. `fill` inherits, so without these the label takes the region's OWN
   fill: grey on grey at rest, and accent on accent once selected. `stroke` inherits
   too, and that one is worse than it looks: the region's outline colour gets painted
   around every glyph, which at label sizes is most of the glyph — pale ghosts of the
   numbers at rest, blue ones when selected. Hence stroke: none, not just a fill. */
:where(.pf-custom) :where(.pf-hit) text { fill: var(--pf-text-2); stroke: none; }
.pf-custom .pf-hit.selected text { fill: var(--pf-on-accent); }
.pf-custom .pf-drag { touch-action: none; }
.pf-custom-error {
  margin: 4px 0; padding: 6px 8px; border: 1px solid var(--pf-err); border-radius: var(--pf-radius-control);
  color: var(--pf-err); font-family: var(--pf-mono); font-size: 11px; line-height: 1.4; word-break: break-word;
}
.pf-custom-error-title { font-weight: 600; margin-bottom: 2px; }
/* A sub-panel inside a custom control (host.controls): no header, no indent. */
.section.bare > .sec-body { padding: 0; }
.section.bare { padding: 0; border: 0; }

.dl { margin-top: 14px; }
.dl-head {
  font-family: var(--pf-mono); font-size: 10px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--pf-muted-2); margin-bottom: 7px;
}
.dl-row { display: flex; gap: 6px; }
.dl-row 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;
}
.dl-row button:hover:not(:disabled) { border-color: var(--pf-accent); color: var(--pf-text-strong); }
.dl-row button:disabled { opacity: .45; cursor: default; }
#status { font-family: var(--pf-mono); margin-top: 12px; min-height: 16px; color: var(--pf-status);
  font-size: 11px; font-variant-numeric: tabular-nums; }
#status.err { color: var(--pf-err); }
.hint { font-family: var(--pf-mono); margin-top: 8px; color: var(--pf-hint); font-size: 10px; letter-spacing: 0.02em; }

/* keyboard focus ring shared across the panel's interactive controls */
.seg button:focus-visible, select.preset:focus-visible, .dl-row button:focus-visible,
button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible, #viewbar button:focus-visible,
.pf-view-select:focus-visible,
.pf-float-rail-toggle:focus-visible, .pf-sketch-toolbar button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent);
}

/* `when` disabling. Distinct from .irrelevant (relevance dimming) on purpose:
   two mechanisms that look identical make the panel impossible to reason about. */
.disabled { opacity: 0.5; pointer-events: none; }

/* The wrapper a titled inner group renders into. Conditions hide the wrapper;
   the disclosure hides `.adv` inside it. Unused until `when` becomes authorable
   in phase 5, but the class exists from Task 7 and must have a rule. */
.adv-wrap.hidden { display: none; }

/* part tabs (placement: .pf-float-tabs; legacy markup keeps the old float) */
#topbar:not(.pf-float-tabs) { position: fixed; top: 12px; left: 50%; transform: translateX(-50%); z-index: 15; }
#topbar .seg {
  margin: 0; padding: 4px; background: var(--pf-surface); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-pill);
  box-shadow: var(--pf-shadow-float);
}
/* flex: the panel's `.seg button` is `flex: 1 1 0%` — equal-width columns,
   which is right for a radio group in the rail and wrong here. With a zero
   basis every tab gets the SAME width, so the widest label does not get to
   size its own column: import-demo's two tabs came out 106px each while
   "Reference overlay" needs ~127, and the text simply drew outside the pill's
   rounded background (measured in Chromium: scrollWidth 238 against
   clientWidth 221). Sizing each tab to its own content is what the nowrap note
   below always claimed happened, and is also what makes the pill's natural
   width honest — view-tabs.js's collapse measures exactly this.

   nowrap: with content-sized columns a long label would otherwise wrap onto a
   second line, making this pill taller than the single fixed height
   debug-overlay.js positions itself below (see its comment) — the overlay
   would then overlap the now-two-row tabs. Letting the label force the column
   wider instead keeps the pill single-row regardless of label length, and
   hands the overflow to the collapse rather than to a second row.

   view-tabs.js ALSO sets nowrap inline on every button it generates, and that is
   the copy that actually protects the rule: a host restyling the pill from
   scratch (partforge-cloud's `#viewer #part button`, with no `.seg` class)
   never sees this selector, and long labels wrapped there for exactly that
   reason. This one still covers hand-written markup on partforge's own pages. */
#topbar .seg button { flex: 0 0 auto; min-width: 70px; padding: 7px 10px; white-space: nowrap; }

/* The tabs' narrow-stage fallback. When view-tabs.js measures the segmented
   pill as wider than the stage's top-centre slot it flips `data-pf-tabs` to
   "menu", and this dropdown stands in for the buttons.

   Ungated on `.seg` / `#topbar` for the same reason #viewbar's chrome below is:
   partforge-cloud dresses the pill with its own `#viewer #part` rules and
   neither of those selectors reaches it — and it is the host that most needs
   this, since its stage is a column beside an editor rather than a full window.

   27px tall, which is what the buttons it replaces measure (11px mono + 7px of
   padding top and bottom). Matched rather than approximated so the pill's own
   height does not change as it swaps — it sits at the stage's top centre, where
   a 1px jump on every resize past the threshold would read as a twitch — and so
   the collapsed pill stays under the fixed height debug-overlay.js parks itself
   below. Capped in width so a long label ellipsizes rather than pushing the
   pill back out to the width that collapsed it in the first place. */
.pf-view-select {
  box-sizing: border-box;
  height: 27px; max-width: min(220px, 100%);
  padding: 0 24px 0 9px;
  border: 0; border-radius: var(--pf-radius-control);
  background: transparent; color: var(--pf-text-2);
  font-family: var(--pf-mono); font-size: 11px; letter-spacing: 0.02em;
  cursor: pointer; appearance: none; text-overflow: ellipsis;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='7'%3E%3Cpolygon points='0,0 8,0 4,7' fill='%238b8b94'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
}
/* `hidden` is the switch view-tabs.js flips between the two representations,
   and a host that gives tab buttons a `display` of their own would beat the
   UA's [hidden] rule — the trap `#viewbar button[hidden]` below already guards
   against. Said again here against the layout state, which lives on the
   element itself and so holds whatever a host does to the pill around it. */
[data-pf-tabs="menu"] > button[data-part] { display: none; }
[data-pf-tabs="segmented"] > .pf-view-select { display: none; }

/* viewer controls. APPEARANCE is ungated: partforge-cloud re-anchors #viewbar's
   position in sandbox.css but inherits this pill chrome, so gating it on a class
   the cloud never sets would strip the editor's viewbar. PLACEMENT comes from
   .pf-float-viewbar; the :not() keeps legacy markup's old top-right float. */
#viewbar {
  display: flex; gap: 4px; padding: 4px;
  background: var(--pf-surface); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-pill);
  box-shadow: var(--pf-shadow-float);
}
/* Sketch mode hides the whole bar while it owns the stage (mount.js) — the
   display: flex above is author-origin and beats the UA's [hidden] rule, the
   same trap the buttons' own [hidden] rule below already guards against. */
#viewbar[hidden] { display: none; }
#viewbar:not(.pf-float-viewbar) { position: fixed; top: 12px; right: 12px; z-index: 15; }
#viewbar button {
  width: 34px; height: 34px; border: 0; border-radius: var(--pf-radius-control);
  background: transparent; color: var(--pf-muted-2); cursor: pointer;
  font-size: 15px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
/* the display: flex above is author-origin and beats the UA's [hidden] rule,
   so any viewbar button hidden via the `hidden` property (today: #rail-toggle
   below the narrow breakpoint, where the pane tab bar takes over) needs this
   to actually disappear — the same trap .pf-cutaway-actions[hidden] below
   already guards against. */
#viewbar button[hidden] { display: none; }
#viewbar .pf-cutaway-actions { display: flex; gap: 4px; }
#viewbar .pf-cutaway-actions[hidden] { display: none; }
#viewbar .pf-cutaway-actions button { width: auto; min-width: 56px; padding: 0 8px; }
#viewbar .pf-measure-actions { display: flex; gap: 4px; }
#viewbar .pf-measure-actions[hidden] { display: none; }
#viewbar .pf-measure-actions button { width: auto; min-width: 56px; padding: 0 8px; }
#viewbar button:disabled { opacity: .38; cursor: not-allowed; }
#viewbar button:disabled:hover { color: var(--pf-muted-2); background: transparent; }
#viewbar button:hover { color: var(--pf-text); background: var(--pf-surface-2); }
#viewbar button.on { background: var(--pf-accent); color: var(--pf-on-accent); }

/* Sketch-mode toolbar: the #viewbar pill idiom, top-centre, owning the mode
   while the viewbar itself is hidden (mount toggles both).

   `left: 50%` is a shrink-to-fit trap for an absolutely positioned element:
   its auto width shrink-wraps against the space between `left` and the
   containing block's right edge, which here is only HALF the stage, not the
   whole stage — so the row wraps (stranding the close button on its own
   centred second line, on top of .pf-sketch-hint) well before it actually
   needs to. `width: max-content` opts back into sizing to the row's
   preferred (unwrapped) width; `max-width` below still caps that on stages
   too narrow for the full row, so genuine wrapping on narrow stages still
   works. */
.pf-sketch-toolbar {
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 4px; padding: 4px;
  background: var(--pf-surface); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-pill); box-shadow: var(--pf-shadow-float);
  z-index: 20; width: max-content; max-width: calc(100% - 16px); flex-wrap: wrap; justify-content: center;
}
.pf-sketch-toolbar[hidden] { display: none; }
.pf-sketch-toolbar .sep { width: 1px; align-self: stretch; margin: 4px 2px; background: var(--pf-border); }
.pf-sketch-toolbar button {
  width: 34px; height: 34px; border: 0; border-radius: var(--pf-radius-control);
  background: transparent; color: var(--pf-muted-2); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.pf-sketch-toolbar button[data-action="send"] { width: auto; min-width: 56px; padding: 0 10px; }
.pf-sketch-toolbar button:hover { color: var(--pf-text-2); background: var(--pf-surface-2); }
.pf-sketch-toolbar button:disabled { opacity: .35; cursor: default; background: transparent; color: var(--pf-muted); }
.pf-sketch-toolbar button.on { background: var(--pf-accent); color: var(--pf-on-accent); }
.pf-sketch-toolbar .pf-swatch { width: 26px; height: 26px; margin: 4px 1px; border-radius: 50%; }
.pf-sketch-toolbar .pf-swatch::before {
  content: ""; width: 14px; height: 14px; border-radius: 50%;
  background: var(--sw); box-shadow: 0 0 0 2px color-mix(in oklab, var(--sw) 25%, transparent);
}
.pf-sketch-toolbar .pf-swatch.on { background: var(--pf-surface-2); }
.pf-sketch-toolbar .pf-swatch.on::before { box-shadow: 0 0 0 2.5px var(--pf-bg), 0 0 0 4.5px var(--sw); }
.pf-sketch-hint {
  position: absolute; top: 58px; left: 50%; transform: translateX(-50%);
  font-family: var(--pf-mono); font-size: 10px; letter-spacing: .04em;
  color: var(--pf-hint); z-index: 19; pointer-events: none; white-space: nowrap;
}
.pf-sketch-hint[hidden] { display: none; }
/* hand-tool cursors on the ink canvas */
.pf-ink-canvas.hand { cursor: default; }
.pf-ink-canvas.hand.over { cursor: grab; }
.pf-ink-canvas.hand.handle { cursor: crosshair; }
/* Pointer-followers are CSS cursors, not canvas drawings: anything that must
   track the pointer per-mousemove would force a full-canvas redraw per event
   (annotate-mode used to do exactly that for the rotate glyph and eraser
   ring, and it read as lag). The compositor renders cursors for free. */
.pf-ink-canvas.hand.rotate { cursor: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2224%22 height=%2224%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cg stroke=%22%23000%22 stroke-width=%224.5%22 opacity=%22.55%22%3E%3Cpath d=%22M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8%22/%3E%3Cpath d=%22M21 3v5h-5%22/%3E%3C/g%3E%3Cg stroke=%22%23fff%22 stroke-width=%222%22%3E%3Cpath d=%22M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8%22/%3E%3Cpath d=%22M21 3v5h-5%22/%3E%3C/g%3E%3C/svg%3E") 12 12, alias; }
.pf-ink-canvas.hand.dragging { cursor: grabbing; }
/* The ring's 16px radius mirrors annotate-mode.js's ERASER_PX — keep them
   in step so the cursor shows the true brush footprint. */
.pf-ink-canvas.erasing { cursor: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2236%22 height=%2236%22 viewBox=%220 0 36 36%22 fill=%22none%22%3E%3Ccircle cx=%2218%22 cy=%2218%22 r=%2216%22 stroke=%22%23000%22 stroke-width=%223.5%22 opacity=%22.55%22/%3E%3Ccircle cx=%2218%22 cy=%2218%22 r=%2216%22 stroke=%22%23fff%22 stroke-width=%221.5%22/%3E%3C/svg%3E") 18 18, crosshair; }

/* ---- the rail toggle: a bare floating icon at the stage's top right --------
   APPEARANCE only (placement lives in chrome.css, per the split at the top of
   this file). Through 2026-08-20 this was the last button in #viewbar's pill;
   it now floats on its own, in the corner diagonally opposite the pill.

   Unadorned at rest — no background, no border, no shadow — so a control that
   is always over the model reads as an icon the viewer carries rather than as a
   second toolbar competing with the first. Explicitly a PLACEHOLDER ("just be a
   floating icon for now"): the position is settled, the treatment is not.

   38px is the MEASURED height of the centred view selector (`#topbar .seg`:
   4px padding + a 28px tab + 4px + 2 hairlines, the same 38 on every page), and
   the reference is that selector rather than #viewbar's 44px bar it briefly
   matched: both floats sit at `top: 12px`, so equal heights put the two on one
   line and their centres coincide by construction. Sizing to the selector is
   also what keeps the hover chip from reading as oversized — the chip IS this
   box. Note the trade: 38px is under the 44px tap-target floor
   scripts/check-app.mjs enforces, and the check cannot see it, because it
   measures targets at 320px where rail.js has already hidden this button.

   A rounded rect, and deliberately NOT the view cube's projection circle: two
   different controls in two different corners, where a shared silhouette would
   invite reading one as the other. --pf-radius-control, the token `.seg` and its
   tabs use, so the chip reads as part of the selector's family (the selector's
   own outer corner is rounder — `#topbar .seg` overrides it to --pf-radius-pill).

   The hover fill is WHITE, and that is a placeholder as well — asked for in as
   many words, and true only over the dark viewer. Translucent rather than #fff
   so it reads as a lift off the viewer instead of a hole punched in it, and so
   the icon keeps its contrast on top; in the LIGHT theme a white fill is close
   to invisible whatever the alpha. When this stops being a placeholder the fill
   wants the --pf-surface family (--pf-surface-2, as `#viewbar button:hover`
   uses) so both themes get a real surface — deliberately not substituted here.

   The focus ring is NOT restated: the button joins the shared :focus-visible
   selector list above. It used to inherit that through `#viewbar button`, which
   no longer matches it. */
.pf-float-rail-toggle {
  width: 38px; height: 38px;
  border: 0; border-radius: var(--pf-radius-control);
  background: transparent; box-shadow: none;
  color: var(--pf-muted-2); cursor: pointer;
  font-size: 15px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
/* The same trap `#viewbar button[hidden]` guards against, inherited along with
   the `display: flex` above: it is author-origin and beats the UA's
   `[hidden] { display: none }`, so rail.js's `toggle.hidden = narrow` would
   otherwise leave this fully visible below the narrow breakpoint — where the
   pane tab bar owns pane selection and a second collapse affordance must not
   compete with it. scripts/check-app.mjs asserts the computed display. */
.pf-float-rail-toggle[hidden] { display: none; }
.pf-float-rail-toggle:hover { color: var(--pf-text); background: rgb(255 255 255 / 14%); }
/* GREY IN BOTH STATES, deliberately. rail.js still toggles `.on` when the rail
   collapses, and inside #viewbar that class picked up the accent fill; there is
   no rule for it here on purpose, so collapsed and expanded look identical. The
   chevron `d` swap is the signal (it flips to point back out of the divider) and
   aria-expanded carries it for assistive tech — a tint would have been a second
   signal on a bare icon over the model, which is what the accent-coloured
   version read as. Do not add one back without asking. */

/* ---- view cube ------------------------------------------------------------
   APPEARANCE only (placement lives in chrome.css, per the split at the top of
   this file).

   The VIEW STYLE button replaced the cube's projection toggle
   (view-style-controls.js, 2026-09-24); it now lives in #viewbar, and only a
   host without one gets it over the cube's corner (its appearance is in the
   next section, its placement in chrome.css). The toggle's history there ran:

   Through 2026-08-19 the projection toggle sat in its own `.pf-viewcube-pill`
   card below the cube, deliberately borrowing #viewbar's chrome so it read as
   one of the viewer controls. The 2026-08-20 revision moved it beside the
   cube instead, at a size and a bottom-aligned baseline that a #viewbar-style
   card could not shrink to — so it became a small bare 24px circle in its own
   right (`.pf-viewcube-toggle`), no card, no border, no shadow. A same-day
   follow-up moved it OVER the cube's bottom-right corner, took its resting
   fill away (a tinted disc on the drawing read as a blob, not a control) and
   then its accent colour, leaving the glyph as the one signal of the
   projection. The view style button went the other way on purpose: the
   request was for a control that is clearly SEEN — first #viewbar's card
   chrome over the corner, now a button IN #viewbar. The projection itself has
   no control at all since 2026-09-24: it is automatic (orthographic on a view
   cube face view, perspective otherwise — viewer.js's tweenCameraTo). */
.pf-viewcube-canvas {
  display: block;
  touch-action: none;
}

/* ---- view style button + popover (view-style-controls.js) -------------------
   In #viewbar the button is a plain `#viewbar button` (34px, transparent,
   the bar's hover / `.on` / focus) and needs nothing here beyond the
   divider that opens the appearance group. The card chrome below is the
   FALLBACK's alone — the button over the cube's corner, for a host with no
   #viewbar — where it has to read as a control on top of the drawing: 30px,
   the viewbar's own compact button size (at 34px it covered a third of the
   101px narrow cube). Scoped to the stack so none of it (the float shadow in
   particular, which `#viewbar button` does not reset) leaks into the bar. */
.pf-viewbar-divider {
  width: 1px; align-self: stretch; margin: 6px 2px; flex: none;
  background: var(--pf-border);
}
.pf-viewbar-divider[hidden] { display: none; }
.pf-viewcube-stack > .pf-view-style-button {
  width: 30px; height: 30px; border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control);
  background: var(--pf-surface); box-shadow: var(--pf-shadow-float);
  color: var(--pf-muted-2); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.pf-view-style-button[hidden] { display: none; }
.pf-viewcube-stack > .pf-view-style-button:hover, .pf-viewcube-stack > .pf-view-style-button.on { color: var(--pf-text); background: var(--pf-surface-2); }
.pf-view-style-popover {
  position: absolute; z-index: 30;
  width: min(300px, calc(100% - 24px)); overflow: auto;
  padding: 12px; display: flex; flex-direction: column; gap: 12px;
  background: var(--pf-surface); border: 1px solid var(--pf-border);
  border-radius: 12px; box-shadow: var(--pf-shadow-float);
  color: var(--pf-text); font-size: 12px;
}
.pf-view-style-popover[hidden] { display: none; }
.pf-view-style-heading { margin: 0; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--pf-muted-2); }
.pf-view-style-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.pf-view-style-tile {
  display: flex; flex-direction: column; gap: 4px; padding: 0; border: 0;
  background: none; color: var(--pf-muted-2); cursor: pointer; font: inherit; text-align: center;
}
.pf-view-style-thumb {
  display: block; aspect-ratio: 1; border-radius: 8px; overflow: hidden;
  background: var(--pf-surface-2); border: 1px solid var(--pf-border);
}
.pf-view-style-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pf-view-style-thumb img[hidden] { display: none; }
.pf-view-style-tile[aria-pressed="true"] { color: var(--pf-text); }
.pf-view-style-tile[aria-pressed="true"] .pf-view-style-thumb { border-color: var(--pf-accent); box-shadow: 0 0 0 1px var(--pf-accent); }
.pf-view-style-tile[data-busy] .pf-view-style-thumb { opacity: 0.6; }
/* A tile whose environment is still loading pulses (the retired viewbar ✦
   toggle's signal, moved onto the thing that was clicked). */
.pf-view-style-tile[data-busy] .pf-view-style-thumb { animation: pf-busy-pulse .8s ease-in-out infinite alternate; }
@keyframes pf-busy-pulse { from { opacity: 1; } to { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .pf-view-style-tile[data-busy] .pf-view-style-thumb { animation: none; } }
.pf-view-style-button:focus-visible, .pf-view-style-tile:focus-visible {
  outline: none; box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent);
}

/* At very narrow widths the full-size viewbar (5 icon buttons + cutaway's
   Flip/Reset, ~314px) is wider than a small phone's viewport minus the
   stage's 12px margins, and .pf-shell { overflow: hidden } clips the excess
   off its left edge (caught by scripts/check-app.mjs's containment check).
   Shrink the bar instead of letting it wrap — a two-row bar would need its
   own anchoring rework, while shrinking is a pure appearance change. */
@media (max-width: 360px) {
  #viewbar { gap: 3px; }
  #viewbar button { width: 30px; height: 30px; font-size: 13px; }
  #viewbar .pf-cutaway-actions, #viewbar .pf-measure-actions { gap: 3px; }
  #viewbar .pf-cutaway-actions button, #viewbar .pf-measure-actions button { min-width: 44px; padding: 0 6px; }
}

/* ---- measurement mode -----------------------------------------------------
   Dimensions render in-scene — three.js objects placed alongside the part in
   the viewer, not a DOM/SVG overlay. This CSS block only styles the viewbar
   chrome: the unit tag shown next to the measure controls. */
.pf-measure-unit {
  font-family: var(--pf-mono); font-size: 10px; color: var(--pf-muted);
  align-self: center; user-select: none; cursor: pointer;
}
#viewbar .pf-measure-actions button.pf-measure-unit { min-width: auto; padding: 0 6px; }
.pf-measure-unit:hover { color: var(--pf-text-2); }

/* animation transport bar (placement: chrome.css's .pf-anim-bar). APPEARANCE
   is ungated for the same reason as #viewbar above. */
.pf-anim-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px;
  /* Never shorter than #viewbar's card sharing the same bottom edge:
     34px button + 2×4px padding + 2×1px border (border-box). */
  min-height: 44px;
  background: var(--pf-surface); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-control); box-shadow: var(--pf-shadow-float);
}
.pf-anim-bar button {
  border: 0; background: transparent; color: var(--pf-muted);
  cursor: pointer; font-size: 13px; padding: 2px 4px;
}
.pf-anim-bar button:hover { color: var(--pf-text-2); }
/* Play/pause is two PERSISTENT inline SVGs toggled by [data-playing] — never
   the ▶/⏸ characters (U+23F8 renders as a color emoji on iOS) and never a
   child swap (WebKit drops a click whose press started on a replaced node —
   see animation-controls.js). */
.pf-anim-play { display: inline-flex; align-items: center; justify-content: center; }
.pf-anim-play .pf-anim-glyph-pause { display: none; }
.pf-anim-play[data-playing] .pf-anim-glyph-play { display: none; }
.pf-anim-play[data-playing] .pf-anim-glyph-pause { display: block; }
.pf-anim-title, .pf-anim-pick {
  font-family: var(--pf-mono); font-size: 11px; color: var(--pf-text-2);
}
.pf-anim-pick {
  background: var(--pf-input-bg); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-control); padding: 3px 6px;
  /* The widest thing in the bar after the timeline, and the cheapest to give
     up: a select's default `min-width: auto` floors it at its longest option,
     so under pressure it would hold full width while the scrubber collapsed.
     Let it yield first, and faster than everything else — but not to a bare
     chevron: below this the control stops naming the animation it selects. */
  min-width: 44px; flex-shrink: 4;
}
/* A child of the stage, not of the bar (the capped bar clips its contents, and
   this sits above them) — animation-controls.js sets left/bottom per reveal,
   stage-relative. z-index clears .pf-anim-bar's 15. */
.pf-anim-chapter {
  position: absolute; transform: translateX(-50%); z-index: 16;
  font-family: var(--pf-mono); font-size: 10px; color: var(--pf-text-2);
  background: var(--pf-surface); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-control); box-shadow: var(--pf-shadow-float);
  padding: 3px 8px; white-space: nowrap;
  pointer-events: none;
  /* Out of flow, so nothing else constrains it: a long chapter label would
     otherwise run past both ends of the scrubber and off a narrow stage.
     Capped at the wrap's nominal width and ellipsised instead. */
  max-width: 220px; overflow: hidden; text-overflow: ellipsis;
  opacity: 0; transition: opacity .12s ease;
}
.pf-anim-chapter.pf-show { opacity: 1; }
/* 220px when there is room. The floor is what matters under pressure: a
   timeline is a pointing target, and below ~96px it stops being one — the
   chapter ticks collapse onto each other and a drag can't resolve a chapter.
   Everything else in the bar yields before this does. */
.pf-anim-scrub-wrap {
  position: relative; display: inline-flex; align-items: center;
  width: 220px; min-width: 80px;
}
/* Capped bar: the pagers go, and their width goes to the timeline. */
.pf-anim-bar.pf-squeezed .pf-anim-page { display: none; }
.pf-anim-scrub { width: 100%; accent-color: var(--pf-accent); }
.pf-anim-tick {
  position: absolute; top: 50%; width: 2px; height: 8px; margin-top: -4px;
  background: var(--pf-muted); pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .pf-anim-chapter { transition: none; }
}

@media (max-width: 360px) {
  /* #viewbar's card is 40px here (30px buttons) — keep the floor in step. */
  .pf-anim-bar { min-height: 40px; }
}

/* ---- transport bar, touch layout ----------------------------------------
   The rules above size the BAR; these size the things you press inside it. At
   a 13px glyph in 2px/4px of padding, play, reset and the ‹ › pagers measure
   ~20x20, with 8px between neighbours — mouse targets. Measured on a 390px
   stage: a tap 12px off the pause button's centre, ordinary finger error,
   landed on the bar's background and did nothing; a little further landed on
   a pager, which SWITCHES ANIMATION. The scrubber fared worse, flex-shrinking
   toward nothing.

   So give every control the 44px minimum, and accept the rows that costs: at
   44px per target they do not fit a 320-390px stage on one line.

   Both query conditions matter. The width half is the framework's own narrow
   layout (chrome.css's 720px breakpoint, measured on the STAGE — partforge in
   an iframe sized to a phone-width card gets it too); the pointer half catches
   a phone in landscape, wider than 720px and still all thumbs.

   The bar stays a single flat flex line that WRAPS, in source order — no
   `order` overrides, deliberately: a reordered flex row splits visual order
   from DOM order, and with it from focus order, so a keyboard on a narrow
   window would tab in spatial zigzags. Wrapping in source order keeps
   reading, tab and touch order one sequence, and keeps the pagers bracketing
   the bar's ends like the wide layout:

       ‹ [ animation v ] (i) ▶        <- wraps where the width runs out
         [===·===·==o===]  ↺ ›

   The timeline takes whatever width its row has left and drops to a wider
   row of its own below ~360px. scripts/check-app.mjs's checkTransportTargets
   pins the result. */
@media (max-width: 719px), (pointer: coarse) {
  .pf-anim-bar {
    flex-wrap: wrap; justify-content: center;
    column-gap: 4px; row-gap: 2px; padding: 4px 8px;
  }
  .pf-anim-bar button {
    min-width: 44px; min-height: 44px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .pf-anim-title, .pf-anim-pick { min-width: 0; }
  /* The picker is a target too, not a caption — it opens the animation list. */
  .pf-anim-pick { max-width: 100%; min-height: 44px; padding: 3px 10px; }
  /* The 200px basis makes the timeline BID for real room: at a phone width it
     loses the bid, wraps, and inherits most of a row; 120px is the floor it
     can be squeezed to when sharing one. */
  .pf-anim-scrub-wrap { flex: 1 1 200px; min-width: 120px; width: auto; min-height: 44px; }
  /* The INPUT is the thing a finger has to land on — height on the wrap alone
     leaves an ~18px native strip as the real target, with dead slack around
     it. The track paints centered, so this is invisible; only the hit area
     grows. */
  .pf-anim-scrub { min-height: 44px; }
}

/* On a phone the full transport is a two-row player squatting on a small
   stage. Collapse it to the decisions that matter — which animation, and
   play/pause: the timeline, reset and ⓘ stand down below the narrow
   breakpoint. WIDTH-keyed, deliberately not the coarse-pointer condition the
   target-size block above shares: a coarse-pointer iPad has the room and
   keeps the full transport; only the small screen loses the player bar.
   Playback itself still visits every chapter; scrubbing wants a real
   screen. */
@media (max-width: 719px) {
  .pf-anim-bar .pf-anim-scrub-wrap,
  .pf-anim-bar .pf-anim-reset,
  .pf-anim-bar .pf-anim-info { display: none; }
}

/* Legacy id-only markup only: classed markup's viewbar lives inside .pf-stage
   (bottom-right, see chrome.css's .pf-float-viewbar) so it never meets the
   top-left floating #panel card. Legacy markup still floats #viewbar top-right
   and #panel top-left (both `position: fixed`), which collide once the window
   gets narrow enough — restore the old narrow-width rule that moves #viewbar
   to bottom-centre and gives #panel room, scoped with :not(.pf-*) so it can't
   affect the new layout. */
@media (max-width: 680px) {
  #panel:not(.pf-rail) { max-height: calc(100vh - 80px); }
  #viewbar:not(.pf-float-viewbar) {
    top: auto; right: auto; bottom: 12px; left: 50%;
    transform: translateX(-50%);
  }
}

#busy {
  position: absolute; inset: 0; z-index: 20; pointer-events: none;
  display: none; flex-direction: column; align-items: center;
  justify-content: center; gap: 14px;
}
#busy.show { display: flex; }
#busy .ring {
  width: 46px; height: 46px; border-radius: 50%;
  border: 4px solid var(--pf-border); border-top-color: var(--pf-accent);
  animation: spin 0.9s linear infinite;
}
#busy .phase { color: var(--pf-text); font-size: 13px; }
@keyframes spin { to { transform: rotate(360deg); } }

/* info glyph + description popover */
.info {
  appearance: none; border: none; background: none; cursor: pointer;
  color: var(--pf-muted); font-size: 12px; line-height: 1; padding: 0 0 0 5px;
  vertical-align: middle;
}
.info:hover { color: var(--pf-text-2); }
.info:focus-visible { outline: none; border-radius: 4px;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
.popover {
  position: fixed; z-index: 50; max-width: 280px; max-height: 50vh; overflow: auto;
  background: var(--pf-surface); color: var(--pf-text); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-pill); padding: 10px 12px;
  box-shadow: var(--pf-shadow-float);
  font-size: 12px; line-height: 1.5;
}
.popover[hidden] { display: none; }
.popover img { max-width: 100%; height: auto; border-radius: 6px; }
.popover code { font-family: var(--pf-mono); background: var(--pf-surface-2); padding: 0.05em 0.35em; border-radius: 4px; font-size: 0.9em; }
.popover a { color: var(--pf-accent); }
.popover p:first-child { margin-top: 0; }
.popover p:last-child { margin-bottom: 0; }

/* relevance: controls/sections that don't affect the on-screen parts */
.irrelevant { opacity: 0.45; }
.irrelevant:hover { opacity: 0.7; }
.section-hidden { display: none; }

/* shared viewer tooltip */
#pf-hover-tip,
.pf-hover-tip {
  position: fixed; z-index: 30; pointer-events: none; display: none;
  padding: 4px 9px; border-radius: 8px; max-width: min(260px, calc(100vw - 16px));
  background: var(--pf-surface); border: 1px solid var(--pf-border);
  color: var(--pf-text-strong); font-size: 12px; line-height: 1.35;
  box-shadow: 0 2px 4px -2px rgb(0 0 0 / .15);
}
#pf-hover-tip.show,
.pf-hover-tip.show { display: block; }
#pf-hover-tip .pf-hover-sub,
.pf-hover-tip .pf-hover-sub { color: var(--pf-muted); font-size: 11px; margin-left: 7px; }
#pf-hover-tip .pf-hover-sub:empty,
.pf-hover-tip .pf-hover-sub:empty { display: none; }
#pf-hover-tip .pf-hover-hint,
.pf-hover-tip .pf-hover-hint { color: var(--pf-muted); font-size: 10.5px; margin-top: 1px; }
#pf-hover-tip .pf-hover-hint:empty,
.pf-hover-tip .pf-hover-hint:empty { display: none; }

/* request-a-pick: agent prompt banner, floated top-centre well below the part tabs,
   laid out like a chat message (avatar + text). Slides/fades in when shown; themed. */
#pf-pick-banner {
  position: fixed; top: 78px; left: calc(50% - var(--pf-rail-w) / 2); transform: translateX(-50%);
  z-index: 30; max-width: min(56ch, calc(100vw - 24px));
  padding: 10px 36px 10px 12px;
  background: var(--pf-surface); color: var(--pf-text);
  border: 1px solid var(--pf-border); border-radius: var(--pf-radius-pill);
  box-shadow: var(--pf-shadow-float);
  font-size: 12px; line-height: 1.45;
  animation: pf-pick-in .18s ease;
}
#pf-pick-banner .pf-pick-row { display: flex; align-items: center; gap: 10px; }
#pf-pick-banner .pf-pick-avatar {
  flex: none; width: 30px; height: 30px; border-radius: 50%; font-size: 17px;
  display: flex; align-items: center; justify-content: center;
  background: var(--pf-surface-2); border: 1px solid var(--pf-border);
}
#pf-pick-banner .pf-pick-msg { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
#pf-pick-banner .pf-pick-label { color: var(--pf-muted); font-size: 11px; }
#pf-pick-banner .pf-pick-prompt { color: var(--pf-text-strong); font-weight: 600; }
/* Below the rail's breakpoint the rail stacks under the viewer, so the stage is
   full width again and the banner centres on the window. */
@media (max-width: 719px) {
  #pf-pick-banner { left: 50%; }
}
#pf-pick-close {
  position: absolute; top: 6px; right: 6px; appearance: none;
  width: 20px; height: 20px; padding: 0;
  border: 1px solid var(--pf-border); border-radius: 6px;
  background: transparent; color: var(--pf-muted);
  cursor: pointer; font-size: 13px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
#pf-pick-close:hover { color: var(--pf-text-strong); border-color: var(--pf-accent); }
@keyframes pf-pick-in {
  from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* ?pick clipboard mode: a bottom-left toggle button + a transient token toast. */
#pf-pick {
  position: fixed; left: 12px; bottom: 12px; z-index: 9999;
  font: 12px system-ui, sans-serif; padding: 6px 10px; cursor: pointer;
}
#pf-pick.on { outline: 2px solid var(--pf-accent); }
#pf-pick-toast {
  position: fixed; left: 12px; bottom: 48px; z-index: 9999; max-width: 60ch;
  font: 12px var(--pf-mono); padding: 6px 10px; border-radius: 8px;
  background: var(--pf-surface); color: var(--pf-text);
  border: 1px solid var(--pf-border);
  white-space: pre-wrap; word-break: break-word; display: none;
}
#pf-pick-toast.show { display: block; }

/* dimension->control reveal: a decaying accent pulse that hands off to the
   focused control's own focus ring */
/* outline (not box-shadow) so outline-offset can space the ring away from
   the control without touching layout */
.pf-param-flash { animation: pf-param-flash 900ms ease-out; outline-offset: 5px; border-radius: 6px; }
@keyframes pf-param-flash {
  0% { outline: 3px solid color-mix(in oklab, var(--pf-accent) 70%, transparent); }
  100% { outline: 3px solid transparent; }
}
@media (prefers-reduced-motion: reduce) { .pf-param-flash { animation: none; } }

/* ---- the `type: "font"` picker -------------------------------------------
   A TAKEOVER panel: it covers the rail on desktop and the single visible pane
   below the narrow breakpoint, rather than expanding inline under the control.
   Choosing a typeface is a browse task, not a slider nudge — it earns the whole
   surface, and one layout for both widths removes a mode from the widget.
   Ported from spike/font-picker.html; the sizes, timings and densities here
   were settled against that running build (design spec §6). */

/* The rail is the picker's containing block; nothing else in it is positioned. */
.pf-rail { position: relative; }

/* `position: absolute` is what places the takeover — and, just as load-bearing,
   what makes .picker a POSITIONED box for the panes below, which are inset: 0
   against it. Without that, the variants pane escapes the picker and fills the
   rail (an hour of the spike went to exactly this). */
.picker {
  position: absolute; inset: 0; z-index: 20;
  display: flex; flex-direction: column;
  background: var(--pf-surface); color: var(--pf-text);
}

.pk-head { flex: none; padding: 10px var(--pf-rail-pad) 8px;
  border-bottom: 1px solid var(--pf-border); background: var(--pf-surface); }
.pk-titlebar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.pk-titlebar b { flex: 1; min-width: 0; font: 600 11px/1 var(--pf-sans);
  letter-spacing: .04em; text-transform: uppercase; color: var(--pf-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pk-x { border: 0; background: transparent; color: var(--pf-muted); cursor: pointer;
  padding: 2px 4px; font-size: 15px; line-height: 1; }
.pk-x:hover { color: var(--pf-text); }

.pk-search { width: 100%; font: 12px/1.4 var(--pf-mono); background: var(--pf-input-bg);
  color: var(--pf-text-strong); border: 1px solid var(--pf-border);
  border-radius: var(--pf-radius-control); padding: 6px 8px 6px 26px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Ccircle cx='5' cy='5' r='3.4' fill='none' stroke='%238b8b94' stroke-width='1.3'/%3E%3Cpath d='M7.6 7.6 L10.5 10.5' stroke='%238b8b94' stroke-width='1.3' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: 8px center; }
.pk-search:focus { outline: none; border-color: var(--pf-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }

/* The virtualized list: .pk-spacer carries the full scroll height and each row
   is absolutely placed at translateY(i * 44px). */
.pk-list { flex: 1; overflow-y: auto; overscroll-behavior: contain; position: relative; }
.pk-spacer { position: relative; width: 100%; }
.pk-row { position: absolute; left: 0; right: 0; display: flex; align-items: center; gap: 8px;
  padding: 0 var(--pf-rail-pad); cursor: pointer;
  border-bottom: 1px solid color-mix(in oklab, var(--pf-border) 45%, transparent); }
.pk-row:hover { background: var(--pf-surface-2); }
.pk-row.sel { background: var(--pf-accent-soft); box-shadow: inset 2px 0 0 var(--pf-accent); }
.pk-main { flex: 1; min-width: 0; }
/* 16px, and the row height is reserved by the virtualizer, so a face arriving
   late swaps glyphs in place instead of shifting the list. */
.pk-face { font-size: 16px; line-height: 1.15; color: var(--pf-text-strong);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pk-sub { font: 9px/1.3 var(--pf-mono); color: var(--pf-hint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pk-meta { flex: none; font: 9px/1 var(--pf-mono); color: var(--pf-hint); }
/* A row whose own face has not arrived yet reads as pending rather than wrong. */
.pk-row.loading .pk-face { opacity: .38; }

/* Two panes sliding inside .pk-panes: browse, then that family's weights. The
   footer sits BELOW this box rather than under it, so Done stays reachable from
   either pane — picking a weight never has to exit to commit. */
.pk-panes { position: relative; flex: 1; min-height: 0; overflow: hidden; }
.pk-pane { position: absolute; inset: 0; display: flex; flex-direction: column;
  background: var(--pf-surface);
  transition: transform .26s cubic-bezier(.4, 0, .2, 1), opacity .26s ease; }
.pk-pane[data-pane="variants"] { transform: translateX(100%); opacity: 0; pointer-events: none; }
.picker.at-variants .pk-pane[data-pane="browse"] { transform: translateX(-22%); opacity: .3; pointer-events: none; }
.picker.at-variants .pk-pane[data-pane="variants"] { transform: translateX(0); opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) { .pk-pane { transition: none; } }

.pk-back { display: flex; align-items: center; gap: 6px; border: 0; background: transparent;
  color: var(--pf-muted); font: 10px/1 var(--pf-mono); cursor: pointer; padding: 0 0 8px; }
.pk-back:hover { color: var(--pf-text); }
.pk-vlist { flex: 1; overflow-y: auto; padding: 4px var(--pf-rail-pad) 12px; }
.vrow { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; cursor: pointer;
  background: transparent; border: 0;
  border-bottom: 1px solid color-mix(in oklab, var(--pf-border) 45%, transparent);
  padding: 9px 0; color: var(--pf-text-strong); }
.vrow:hover { background: var(--pf-surface-2); }
.vrow.on, .vrow.on .vlabel { color: var(--pf-accent); }
.vrow .vsample { flex: 1; min-width: 0; font-size: 16px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vrow .vlabel { flex: none; font: 10px/1 var(--pf-mono); color: var(--pf-muted); }

.pk-hint { flex: none; padding: 7px var(--pf-rail-pad) 0; font: 9px/1.4 var(--pf-mono); color: var(--pf-hint); }
.pk-empty { margin: 0; padding: 22px var(--pf-rail-pad); font: 11px/1.6 var(--pf-mono); color: var(--pf-hint); }

.pk-foot { flex: none; display: flex; align-items: center; gap: 10px;
  padding: 9px var(--pf-rail-pad); border-top: 1px solid var(--pf-border); background: var(--pf-surface); }
.pk-sel { flex: 1; min-width: 0; font: 10px/1.4 var(--pf-mono); color: var(--pf-hint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pk-sel b { font-weight: 500; color: var(--pf-text-2); }
.pk-done { flex: none; border: 0; border-radius: var(--pf-radius-control); cursor: pointer;
  background: var(--pf-accent); color: var(--pf-on-accent); font: 11px/1 var(--pf-mono);
  letter-spacing: .04em; padding: 8px 17px; }
.pk-done:hover { filter: brightness(1.09); }
.picker :where(button, input):focus-visible { outline: 2px solid var(--pf-accent); outline-offset: 2px; }
