:root{
  --bg:#171a1f; --panel:#1e222a; --panel2:#252b34; --panel3:#292f3b; --line:#323a46;
  --slider:#485363;--text:#e9e7df; --muted:#8b93a1; --accent:#58b8d6; --accent-ink:#0c1a20;
  --warn:#e0764f; --paper:#fbf9f3; --bed:#475161; --paper-edge:#5b6472;
  --mono:'IBM Plex Mono',ui-monospace,monospace; --disp:'Space Grotesk',system-ui,sans-serif;
  --guide-color:#4a4436;   /* overwritten in JS (updateGuideColor) to keep contrast with the current page color */
  --sel-color:#58b8d6;     /* overwritten in JS (updateSelColor) to the ink color once the page color gets too close to --accent */

  /* ---- type scale ----------------------------------------------------------
     Six sizes, three tracking values. Roles:
       --fs-hero     modal logo wordmark
       --fs-brand    header brand name
       --fs-btn      button text (primary buttons + header actions)
       --fs-control  every control label: slider / checkbox / pill-toggle /
                     select label, layer & saved-view names, modal body rows,
                     compact buttons (view grid, tabs, list-header buttons)
       --fs-data     mono readouts & inputs: stats, value fields, number/select
                     inputs, footer, floating text labels, hint paragraphs
       --fs-caps     uppercase micro-labels (.vpLabel, .boxLabel, rows header…)
       --fs-caps-lg  the one louder uppercase label: #panel h2 section headers
     Weights in use: 400 / 600 / 700 only. */
  --fs-hero:24px; --fs-brand:15px; --fs-btn:13px; --fs-control:12px;
  --fs-data:11px; --fs-caps:9px; --fs-caps-lg:10px;
  --ls-hero:.2em; --ls-caps:.12em; --ls-btn:.02em;

  /* ---- shape / effect tokens ---------------------------------------------- */
  --check-size:15px;                        /* every checkbox, one size */
  --edge-glass:rgba(255,255,255,.09);       /* hairline on frosted-over-viewport chrome */
  --divider:rgba(255,255,255,.05);          /* dashed row separators (layer / saved-view lists) */
  --shadow-float:0 8px 24px rgba(0,0,0,.38);/* floating panels & context menu */
  --disabled-opacity:.4;                    /* one faded state for :disabled / .ctlDisabled */
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{background:var(--bg);color:var(--text);font-family:var(--disp);font-size:14px;
  display:grid;grid-template-rows:54px 1fr 30px;overflow:hidden}
button,input,select{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* ---------- scrollbars ---------- */
/* Firefox: native shape/size kept as-is (scrollbar-width left at its
   default 'auto'), just recolored — thumb accent, track transparent. */
*{scrollbar-color:var(--accent) transparent}
/* WebKit/Chromium: same idea. Track/corner/buttons all transparent so
   only the thumb reads as accent-colored; native width/height/radius are
   left untouched (no ::-webkit-scrollbar size override) since the
   default shape is already fine — only color changes. The up/down/
   left/right arrow glyphs themselves can't be recolored via CSS (no
   such pseudo-element exists), so the arrow buttons are drawn as plain
   accent-colored triangles on a transparent button box instead. */
::-webkit-scrollbar-track,::-webkit-scrollbar-corner{background:transparent}
::-webkit-scrollbar-thumb{background-color:var(--accent);border-radius:8px}
::-webkit-scrollbar-thumb:hover{background-color:var(--accent);filter:brightness(1.15)}
::-webkit-scrollbar-button{background-color:var(--accent)}
::-webkit-scrollbar-button:vertical:start{clip-path:polygon(50% 25%, 20% 75%, 80% 75%)}
::-webkit-scrollbar-button:vertical:end{clip-path:polygon(50% 75%, 20% 25%, 80% 25%)}
::-webkit-scrollbar-button:horizontal:start{clip-path:polygon(25% 50%, 75% 20%, 75% 80%)}
::-webkit-scrollbar-button:horizontal:end{clip-path:polygon(75% 50%, 25% 20%, 25% 80%)}

/* ---------- header ---------- */
/* header + footer share the same bar shell — see the grouped rule below */
header,footer{display:flex;align-items:center;justify-content:space-between;
  position:relative;z-index:2;background:var(--panel)}
header{padding:0 16px}
.brand{display:flex;align-items:baseline;gap:10px}
.brand .mark{color:var(--accent);flex:none;transform:translateY(2px)}
.brand .name{font-weight:700;letter-spacing:var(--ls-hero);font-size:var(--fs-brand)}
.brand .sub{font-family:var(--mono);font-size:var(--fs-data);color:var(--muted)}
.actions{display:flex;gap:22px;align-items:center}
.actGroup{display:flex;gap:8px;align-items:center}

/* ---------- about / shortcuts modal ---------- */
.aboutOverlay{position:fixed;inset:0;z-index:50;background:rgba(10,12,15,.55);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  display:flex;align-items:center;justify-content:center}
.aboutOverlay[hidden]{display:none}
.aboutModal{width:580px;max-width:92vw;max-height:85vh;overflow-y:auto;
  background:var(--panel);border:1px solid var(--line);border-radius:10px;
  box-shadow:0 20px 60px rgba(0,0,0,.5);padding:20px 22px 18px}
.aboutHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.aboutTitle{font-weight:700;letter-spacing:var(--ls-hero);font-size:var(--fs-btn);color:var(--muted)}
.aboutLogoRow{display:flex;align-items:center;justify-content:center;gap:12px;
  margin:6px 0 22px}
.aboutLogoMark{width:34px;height:34px;flex:none}
.aboutLogoName{font-weight:700;letter-spacing:var(--ls-hero);font-size:var(--fs-hero)}
.aboutSectionTitle{text-align:center;font-family:var(--mono);font-size:var(--fs-caps-lg);
  text-transform:uppercase;letter-spacing:var(--ls-caps);color:var(--muted);margin-bottom:14px}
.aboutShortcuts{display:flex;flex-direction:column;gap:16px}
.aboutShortcutGroup h3{font-family:var(--mono);font-size:var(--fs-caps);text-transform:uppercase;
  letter-spacing:var(--ls-caps);color:var(--accent);margin-bottom:7px;font-weight:600}
.aboutShortcutRow{display:flex;align-items:center;gap:12px;font-size:var(--fs-control);
  color:var(--text);padding:3px 0}
.aboutShortcutRow .aboutKeys{flex:none;display:flex;align-items:center;gap:3px;
  flex-wrap:nowrap;width:180px;color:var(--muted);font-family:var(--mono);font-size:var(--fs-data)}
.aboutShortcutRow kbd{font-family:var(--mono);font-size:var(--fs-data);background:var(--panel2);
  border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;
  padding:2px 6px;color:var(--text)}
.aboutFooter{margin-top:20px;padding-top:14px;border-top:1px solid var(--line);
  text-align:center;font-family:var(--mono);font-size:var(--fs-data);color:var(--muted)}
.aboutFooter a{color:var(--accent);text-decoration:none}
.aboutFooter a:hover{text-decoration:underline}
/* buttons — one text button (.btn) + a family of square/round icon buttons.
   All share: panel2 fill, hairline border, muted-until-hover, flex-centred.
   The icon buttons differ only in size / radius / a couple of bg overrides. */
.btn{background:var(--panel2);border:1px solid var(--line);border-radius:50cqh;
  padding:7px 14px;cursor:pointer;font-weight:600;font-size:var(--fs-btn);letter-spacing:var(--ls-btn)}
.aboutBtn,.aboutClose,.cornerBtn,.addViewBtn,.svBtn{flex:none;display:flex;
  align-items:center;justify-content:center;padding:0;line-height:1;cursor:pointer;
  border:1px solid var(--line);background:var(--panel2);color:var(--muted)}
.btn:hover,.aboutBtn:hover,.aboutClose:hover,.cornerBtn:hover,
.addViewBtn:hover,.svBtn:hover,.vpBtn:hover{border-color:var(--accent);color:var(--accent)}
.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
.btn.primary:hover{filter:brightness(1.1);color:var(--accent-ink)}
/* per-button size / shape (the rest comes from the two rules above) */
.aboutBtn{width:26px;height:26px;border-radius:50%;font-size:var(--fs-btn);font-weight:600}
.aboutClose{width:24px;height:24px;border-radius:50%;background:transparent;font-size:var(--fs-data)}
.addViewBtn{width:24px;height:24px;border-radius:50cqh;font-size:var(--fs-brand)}
.svBtn{width:24px;height:24px;border-radius:50%;font-size:var(--fs-data)}
#clearBlocksBtn,#duplicateBlockBtn{width:auto;padding:0 10px;
  font-size:var(--fs-control);font-weight:600;letter-spacing:var(--ls-btn)}
/* one faded/inert state — native :disabled controls + JS-toggled .ctlDisabled rows */
:disabled,.ctlDisabled{opacity:var(--disabled-opacity);pointer-events:none;cursor:default}
/* pill switches — an unambiguous on/off affordance: track + sliding knob.
   'active' slides the knob right and lights the track (same class the JS
   already toggled on the old button styling). */
.pillToggle{position:relative;flex:none;width:34px;height:18px;border-radius:9px;
  border:none;background:color-mix(in srgb, var(--panel) 50%, transparent);
  cursor:pointer;padding:0;transition:background .15s}
.pillToggle::after{content:'';position:absolute;top:3px;left:3px;width:12px;height:12px;
  border-radius:50%;background:var(--muted);transition:left .15s,background .15s}
.pillToggle:hover::after{background:var(--text)}
.pillToggle.active{background:rgba(88,184,214,.28)}
.pillToggle.active::after{left:19px;background:var(--accent)}
.pillToggle.active:hover::after{background:var(--text)}
.switchWrap{display:flex;align-items:center;gap:6px;
  font-size:var(--fs-control);color:var(--text);user-select:none}
.switchWrap>span{cursor:pointer}
/* projection / overlay-order row: same segmented-toggle look as .modeToggle —
   the two flanking mode names ARE the buttons, the old slide-knob switch is
   hidden. Sliding-pill styling is shared, see ".modeToggle,.projRow" below. */
.projRow{display:flex;align-items:center;gap:4px;margin:2px 0 6px;
  padding:0 3px;background:color-mix(in srgb, var(--panel) 50%, transparent);
  border-radius:12px;height:24px}
.projRow .pillToggle{display:none}
.projLbl{flex:1;display:flex;align-items:center;justify-content:center;height:18px;
  border-radius:999px;font-size:var(--fs-data);color:var(--muted);cursor:pointer;
  transition:color .15s}
.projLbl:hover{color:var(--text)}
.projLbl.active,.projLbl.active:hover{color:var(--accent-ink)}

/* ---------- main split ---------- */
main{display:grid;grid-template-columns:1fr 1fr 322px;min-height:0;position:relative}
@media (max-width:1100px){
  main{grid-template-columns:1fr 1fr;grid-template-rows:1fr auto}
  #panel{grid-column:1 / -1;max-height:38vh}
  #panelResizeHandle{display:none}
}
/* Positioned relative to <main> (not #panel, which has overflow-y:auto) —
   inside #panel it would scroll away with the settings list whenever it's
   taller than the viewport. `right` is kept in sync with the panel's
   current width by the drag script (offset by half its own width, so the
   handle is centered ON the boundary line rather than sitting flush
   against one side of it) — 318px here just matches that same math for
   the initial 322px default, before any drag has happened. */
#panelResizeHandle{position:absolute;top:0;bottom:0;right:318px;width:8px;
  cursor:ew-resize;z-index:20;touch-action:none}
#panelResizeHandle::after{content:'';position:absolute;left:3px;top:0;bottom:0;width:2px;
  background:transparent;transition:background .1s}
#panelResizeHandle:hover::after,#panelResizeHandle.dragging::after{background:var(--accent)}
body.resizingPanel{cursor:ew-resize;user-select:none}
body.resizingPanel #viewport3d,body.resizingPanel #paperPane{pointer-events:none}
/* Layout tab: the 3D viewport shows nothing useful (generation is fully
   paused while it's active — see activeTab gating in panel-controls.js),
   so it's hidden outright and the 2D pane takes over its space too,
   spanning both of the first two grid tracks instead of just its own. */
body.layoutMode #viewport3d{display:none}
body.layoutMode #paperPane{grid-column:1 / 3}
section,aside{position:relative;min-width:0;min-height:0}
#viewport3d{border-right:1px solid var(--line);background:#12151a;position:relative}
#viewport3d canvas{display:block}
/* corner reset/recenter buttons — small square icon buttons sitting in
   each pane's bottom-left corner; same frosted treatment as the floating
   panels above them */
.cornerBtn{position:absolute;left:10px;bottom:8px;z-index:2;width:26px;height:26px;
  border-radius:6px;border-color:var(--edge-glass);
  background:color-mix(in srgb, var(--bg) 62%, transparent);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}
.cornerBtn svg{display:block}

/* Floating panels — frosted glass over a viewport (pure backdrop-filter, no
   dependency; degrades to a plain translucent panel where unsupported).
   The two panel stacks are identical — one over the 3D pane, one over the 2D;
   each stacks its floats vertically with no guessed pixel offsets. The H
   shortcut (panel-controls.js) slides both offscreen via .panelsHidden. */
#camPanelStack,#paperPanelStack{position:absolute;top:12px;left:12px;z-index:2;
  display:flex;flex-direction:column;gap:10px;width:224px;transition:transform .25s ease}
#camPanelStack.panelsHidden,#paperPanelStack.panelsHidden{transform:translateX(-100%)}
/* frosted glass — shared by every floating panel and the layer context menu;
   backdrop-filter degrades to a plain translucent panel where unsupported. */
#camFloat,#rotateFloat,#viewsFloat,#genExportFloat,#addToLayoutFloat,#blocksFloat,#layerContextMenu{
  border:1px solid var(--edge-glass);border-radius:10px;box-shadow:var(--shadow-float);
  padding:12px 12px 10px;
  -webkit-backdrop-filter:blur(14px) saturate(1.3);backdrop-filter:blur(14px) saturate(1.3)}
#camFloat,#rotateFloat,#viewsFloat,#genExportFloat,#addToLayoutFloat{width:224px;
  background:color-mix(in srgb, var(--panel3), transparent 20%)}
#blocksFloat{width:270px;background:color-mix(in srgb, var(--bg) 58%, transparent)}
/* camFloat/rotateFloat labels are all 1-3 characters (FOV, X, Y, Z) — a much
   narrower label column than a settings-style row needs (room for something
   like "Orientation") hands the freed width straight to the slider track,
   giving more physical pixels per degree of rotation range for finer
   default drag control. */
#camFloat .ctl,#rotateFloat .ctl{grid-template-columns:22px 1fr 34px;margin:6px 0}
#camFloat .ctl.ctlWideLabel{grid-template-columns:64px 1fr 34px}
#camFloat .vpLabel,#rotateFloat .vpLabel,#viewsFloat .vpLabel,#blocksFloat .vpLabel{margin:7px 0 4px}
#rotateFloat .vpLabel:first-child{margin-top:0}
#camFloat select{width:100%}
#blocksFloat .vpLabel:first-child{margin:0}

/* genExport / addToLayout float internals */
#genRow{display:flex;align-items:center;gap:8px;margin:0 0 8px}
#genRow .switchWrap{flex:none}
#genRow #genBtn{flex:1;text-align:center}
#genExportFloat #exportBtn{width:100%;text-align:center}
#addToLayoutFloat .addToLayoutBtn{width:100%;text-align:center}
/* #addToLayoutFloat is only 224px wide, so its .ctl label column is tightened
   (same reasoning as the #camFloat/#rotateFloat overrides above). */
#addToLayoutFloat .ctl{grid-template-columns:50px 1fr 34px}
#addToLayoutSaveViewWrap{margin-top:10px}
#layoutOverlayWrap{margin-top:6px}
#addToLayoutSaveViewWrap,#layoutOverlayWrap{width:100%;justify-content:space-between}
#layoutOverlayControls{margin-top:2px}
#layoutOverlayControls .projRow{margin:6px 0 2px}
#addToLayoutMsg{width:224px;text-align:center;font-family:var(--mono);font-size:var(--fs-data);
  color:var(--accent);height:14px;line-height:14px;margin-top:-4px;
  opacity:0;transition:opacity .25s;pointer-events:none}
#addToLayoutMsg.show{opacity:1}
/* Model file info — 3D viewport, under the axis gizmo. H shortcut hides it. */
#modelInfoFloat{position:absolute;top:112px;right:12px;z-index:2;
  max-width:200px;text-align:right;pointer-events:none}

/* Saved views panel — header row puts the label and add button on one line
   (vpLabel's own margin handles the vertical spacing, same as every other
   panel header), the list below is a simple scrollable stack of rows so an
   unbounded number of saved views can't push the rest of the 3D viewport's
   UI around or grow the panel past the viewport height. */
.svHeaderRow{display:flex;align-items:center;justify-content:space-between}
.svHeaderRow .vpLabel{margin:0}
.svHeaderBtns{display:flex;align-items:center;gap:6px}
#viewsList,#blocksList{max-height:500px;overflow-y:auto;margin-top:10px}
#blocksList{position:relative}
/* Empty state (Saved Views only — the Layout Layers panel hides itself
   entirely when empty instead, see syncBlocksFloatVisibility in
   layout-canvas.js): center the header row (name + button) within whatever
   height the panel has, instead of it sitting flush against the top
   padding with visible dead space below. The (empty) list itself is
   hidden outright so its own margin-top doesn't throw off the centering. */
#viewsFloat.svEmpty{display:flex;flex-direction:column;justify-content:center}
#viewsFloat.svEmpty #viewsList{display:none}
.savedView{display:grid;grid-template-columns:1fr 22px 22px;align-items:center;gap:5px;
  padding:4px 6px;border-bottom:1px dashed var(--divider)}
/* Layout's blocksList rows have one more button (Lock, between Eye and
   Delete) than the base .savedView template accounts for — Saved Views
   (#viewsList, Update+Delete) and the layer-visibility context menu both
   still only need theirs, so this widens the grid ONLY for #blocksList
   rather than changing the shared base rule out from under them. */
#blocksList .savedView{grid-template-columns:18px 1fr 22px 22px 22px}
#blocksList .svDragHandle{width:18px;height:22px;display:flex;align-items:center;justify-content:center;
  cursor:grab;color:var(--muted);touch-action:none}
#blocksList .svDragHandle:hover{color:var(--text)}
#blocksList .savedView.svDragging{opacity:.4}
#blocksList .svInsertLine{position:absolute;left:6px;right:6px;height:2px;background:var(--accent);border-radius:1px;pointer-events:none}
.savedView .svName{font-size:var(--fs-control);color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:text;width:fit-content;max-width:100%}
.savedView .svName[contenteditable="true"]{outline:none;-webkit-user-select:text;user-select:text;overflow:visible}
.svBtn.svDelete:hover{border-color:var(--warn);color:var(--warn)}
.svBtn.svLockActive{color:var(--accent)}
.svBtn svg{display:block}
.savedView.svRowHidden .svName{opacity:.45}
.savedView.svRowSelected{background:rgba(88,184,214,.1);border-radius:4px}

/* Layout canvas — margin guide (paper reference, bottom layer, same
   dashed-line treatment as the Preview tab's own margin guide), selection
   rectangle + corner handles (vector-effect:non-scaling-stroke is correct
   here, unlike on plot content: these are UI chrome, meant to stay a
   constant screen size regardless of zoom — handle SIZE itself is
   computed in mm from the current zoom in JS, but stroke width doesn't
   need to be), and snap guide lines. */
/* Selection overlay lives in its OWN unclipped, viewport-fixed SVG now
   (not nested inside #layoutPlot, which is clipped to the paper) — so a
   block positioned partly or fully off the page still shows its selection
   box/handles/gizmo, letting it be found and dragged back. Coordinates
   drawn into it are plain screen pixels (via canvasMmToScreen), not mm, so
   vector-effect:non-scaling-stroke isn't needed here — there's no zoom
   transform on this SVG to counteract in the first place. */
#layoutOverlaySvg,#previewOverlaySvg{position:fixed;inset:0;width:100vw;height:100vh;
  pointer-events:none;z-index:1}
.textureGizmo{fill:var(--accent);stroke:var(--accent-ink);stroke-width:1.5;
  vector-effect:non-scaling-stroke;pointer-events:auto;cursor:grab}
.textureGizmo:active{cursor:grabbing}
.textureGizmoCross{stroke:var(--accent-ink);stroke-width:1;vector-effect:non-scaling-stroke;pointer-events:none}
/* Debug endpoint markers. Drawn into previewOverlaySvg in SCREEN coordinates,
   so the radius below is literal screen px and stays put under zoom. The pale
   ring keeps a dot readable against both dark ink and a dark page colour. */
.pathEndpointDot{fill:#ff2d2d;fill-opacity:.85;stroke:#fff;stroke-width:.75;stroke-opacity:.9;pointer-events:none}
/* dashed reference / snap guides — all zoom-independent (non-scaling-stroke),
   differing only in stroke width + dash pattern (+ colour for the axis locks) */
.layoutMarginGuide,.layoutGridGuide,.pvMarginGuide,.pvGridGuide,.layoutSnapGuide,.layoutAxisLockGuide{
  fill:none;vector-effect:non-scaling-stroke;pointer-events:none;stroke:var(--guide-color)}
.layoutGridGuide,.pvGridGuide,.layoutSnapGuide,.layoutAxisLockGuide{opacity:.85}
.layoutMarginGuide{stroke-width:1;stroke-dasharray:3 3}
.layoutGridGuide{stroke-width:1.6;stroke-dasharray:1 3}
.pvMarginGuide{stroke-width:0.7;stroke-dasharray:2 2}
.pvGridGuide{stroke-width:0.9;stroke-dasharray:1 2}
.layoutSnapGuide{stroke:var(--accent);stroke-width:1;stroke-dasharray:2 2}
.layoutAxisLockGuide{stroke-width:1;stroke-dasharray:4 3}
.layoutAxisLockGuide-x{stroke:#e5484d}
.layoutAxisLockGuide-y{stroke:#30a46c}
/* selection box + rotate connector (outlines) and the drag handles / rotate knob (fills) */
.layoutSelRect,.layoutSelRectMember,.layoutRotateConnector{fill:none;stroke:var(--sel-color);
  stroke-width:1;pointer-events:none}
.layoutSelRect{stroke-width:1.4;stroke-dasharray:5 3}
.layoutSelRectMember{stroke-dasharray:2 2;opacity:.55}
.layoutRotateConnector{stroke-dasharray:3 2;opacity:.75}
.layoutSelHandle,.layoutRotateGizmo{fill:var(--accent);stroke:var(--accent-ink);stroke-width:1;pointer-events:none}
#rotateAngleLabel,.dimLabel{position:fixed;display:none;
  background:color-mix(in srgb, var(--bg) 92%, transparent);color:var(--accent);font-family:var(--mono);
  font-size:var(--fs-data);padding:3px 8px;border-radius:4px;pointer-events:none;
  white-space:nowrap;z-index:1;border:1px solid var(--edge-glass)}
#rotateAngleLabel{transform:translate(-50%,-140%)}
.dimLabel{transform:translate(-50%,-50%)}

/* base frost comes from the grouped rule above; these are its own overrides */
#layerContextMenu{position:fixed;width:192px;z-index:60;border-radius:8px;padding:8px 10px 6px;
  background:color-mix(in srgb, var(--bg) 94%, transparent)}
#layerContextMenu.overrideActive{width:350px}
#layerContextMenu .vpLabel{margin:0 0 4px}
/* Name column is a fixed px (matching what 1fr already resolves to at the
   base 180px menu width), not 1fr — a flexible column would change size
   whenever the OTHER columns' total width changes (i.e. whenever Override
   toggles), shifting the eye button that follows it even though nothing
   about that button itself changed. Fixed-width everywhere means toggling
   Override only ever ADDS columns, never resizes the existing ones. */
#layerContextMenuList .savedView{grid-template-columns:133px 22px}
/* The spacer column (empty <span class="ctxSpacer">, inserted in the DOM
   by openLayerContextMenu right after the eye button) shifts color/width/
   dash rightward as a group, using space that would otherwise sit unused
   after the dash dropdown — same total menu width as before, just
   redistributed, and it doubles as a visual gap between the visibility
   toggles and the rest of the per-layer settings. */
#layerContextMenu.overrideActive #layerContextMenuList .savedView{grid-template-columns:133px 22px 10px 24px 56px 50px}
#layerContextMenuList .svName{cursor:default}
/* Native color-input rendering nests an inner swatch element with its own
   default padding/border regardless of the outer input's own padding:0 —
   left alone, that reads as a smaller box floating inside a larger one
   instead of filling the whole container edge to edge. Zeroed out once,
   globally, for every input[type=color] in the app (layer rows, page
   color, and the layout-block color-override menu all share this same
   fix, on top of whatever border-radius each context sets on the input
   itself — 4px is what all three currently use, so it's safe to match
   here directly rather than juggling border-radius:inherit through a
   vendor shadow-pseudo-element's own inheritance chain). overflow:hidden
   on the wrapper is a defensive belt-and-braces clip, in case a given
   browser's own swatch corner-rounding doesn't quite line up.  */
input[type=color]{padding:0}
input[type=color]::-webkit-color-swatch-wrapper{padding:0;border-radius:4px;overflow:hidden}
input[type=color]::-webkit-color-swatch{border:none;border-radius:4px}
input[type=color]::-moz-color-swatch{border:none;border-radius:4px}
#layerContextMenuList input[type="color"]{width:100%;height:20px;padding:0;border:1px solid var(--line);border-radius:4px;background:none}
#layerContextMenuList input[type="number"]{width:100%;height:20px;padding:0 3px;border:1px solid var(--line);border-radius:4px;
  background:var(--panel2);color:var(--text);font-size:var(--fs-data)}
#layerContextMenuList select{width:100%;height:20px;padding:0 2px;border:1px solid var(--line);border-radius:4px;
  background:var(--panel2);color:var(--text);font-size:var(--fs-data)}
#layerContextOverrideRow{margin:6px 2px 2px;padding-top:6px}

/* axis gizmo — top-right of the 3D viewport, Blender-style */
#axisGizmo{position:absolute;top:12px;right:12px;z-index:2;
  font-family:var(--mono);user-select:none}
#axisGizmo .ball{cursor:pointer}
#axisGizmo .ball:hover circle{stroke:#fff;stroke-width:1.4}
#axisGizmo text{pointer-events:none;font-size:9.5px;font-weight:700}

/* light direction gizmo — bottom-right of the 3D viewport, mirroring the
   axis gizmo's top-right placement and frosted-free, canvas-native SVG
   overlay approach. Static (doesn't follow camera orbit) by design: azimuth/
   elevation are world-space, and a rotating ring would turn into an ellipse
   from most camera angles, which is exactly the "genuinely tricky, skip it
   for now" case this widget deliberately avoids. */
#lightGizmo{position:absolute;bottom:0px;right:0px;z-index:2;
  font-family:var(--mono);user-select:none}
#lightGizmo .lgRing{fill:rgba(255,255,255,.02);stroke:var(--line);stroke-width:2.25}
#lightGizmo .lgHit{fill:transparent;stroke:transparent;cursor:grab}
#lightGizmo .lgHit:active{cursor:grabbing}
#lightGizmo .lgTrackBg{stroke:var(--line);stroke-width:2.25;stroke-linecap:round}
#lightGizmo .lgTrackFill{stroke:var(--accent);stroke-width:2.25;stroke-linecap:round;opacity:.55}
#lightGizmo .lgNeedle{stroke:var(--accent);stroke-width:2.25;opacity:.5}
/* pointer-events:none — the sun icon sits visually ON TOP of its hit-target
   (drawn after it, so it isn't hidden underneath), but must be transparent
   to the pointer itself: otherwise hovering/clicking exactly on the dot hits
   the icon instead of the hit-target beneath, which has no listeners of its
   own — cursor reverts to a plain arrow and dragging silently does nothing
   right where a user's eye naturally aims. */
#lightGizmo .lgSun{pointer-events:none}
#lightGizmo .lgSun circle{fill:#ffd013}
#lightGizmo .lgLbl{font-size:12px;letter-spacing:.08em;fill:var(--muted);text-transform:uppercase}

/* ---------- paper pane (signature: screen vs paper) ---------- */
#paperPane{border-right:1px solid var(--line);background:var(--bed);
  position:relative;padding:10px;user-select:none;
  overflow:hidden;touch-action:none}
.sheet{background:var(--paper);border:1px solid var(--paper-edge);
  box-shadow:0 2px 6px rgba(30,34,42,.18),0 16px 36px rgba(30,34,42,.14);
  position:absolute;border-radius:1px}
.sheet svg{position:absolute;inset:0;width:100%;height:100%;display:block}
.sheet.stale svg{opacity:.42;filter:saturate(.4)}
#staleBadge{position:absolute;top:12px;left:50%;transform:translateX(-50%);display:none;
  font-family:var(--mono);font-size:var(--fs-data);color:var(--warn);border:1px solid var(--warn);
  background:color-mix(in srgb, var(--bg) 90%, transparent);padding:4px 10px;border-radius:20px;pointer-events:none}
#paperPane.stale #staleBadge{display:block}
#progressWrap{position:absolute;top:0;left:0;right:0;height:3px;display:none}
#paperPane.busy #progressWrap{display:block}
#progressBar{height:100%;width:0;background:var(--accent);transition:width .12s linear}

/* Preview/Layout tabs — top-right of the 2D pane, clear of genExportFloat/
   blocksFloat (top-left) and the stale badge (top-center). Styled like
   actual browser tabs: flush against the pane's top edge (top:0 — absolute
   positioning ignores the parent's own padding, so this sits right at the
   border, no gap), shaped using clip-path (with negative margin-left).
   The active tab is filled with the HEADER's own background (the pane sits
   directly under the header, so matching it reads as "this tab is attached
   to the header above") and has no visible seam where it meets the pane's
   content below. The inactive tab instead gets the pane's own background
   (var(--bed) but a bit darker (black 25%), so it recedes into the pane behind*/
#paperTabs{position:absolute;top:0;right:12px;z-index:3;display:flex}
.paperTab{background:color-mix(in srgb, var(--bed), black 25%);border:0px;border-top:none;
  padding:7px 30px;margin-left:-30px;
  font-family:var(--disp);font-size:var(--fs-control);font-weight:600;
  transition:background .12s,color .12s;
  clip-path: shape(
    from top left,
    curve to 15px 10px with 15px 0,
    vline to calc(100% - 15px),
    curve to 30px 100% with 15px 100%,
    hline to calc(100% - 30px),
    curve to calc(100% - 15px) calc(100% - 15px) with calc(100% - 15px) 100%,
    vline to 15px,
    curve to 100% 0 with calc(100% - 15px) 0);}
.paperTab:first-child{margin-left:0}
.paperTab:hover{color:var(--accent)}
.paperTab:focus,.paperTab:focus-visible{outline:none}
.paperTab.active{background:var(--panel);border-color:var(--line);color:var(--text);z-index:1}
body.dragging #paperTabs{display:none}

/* ---------- control panel ---------- */
#panel{background:var(--panel3);overflow-y:auto;padding-bottom:24px;position:relative;z-index:4}
/* Pen/Texture/Page/Cog settings-mode toggle + the Texture-tab sub-tab rows:
   a pill-shaped bar of equal buttons. The accent fill is a single pill that
   slides to the active button — shared with .projRow, see the block below. */
.modeToggle{display:flex;align-items:center;gap:4px;height:34px;margin:14px 14px 0;
  padding:0 3px;box-sizing:border-box;background:var(--panel);border-radius:17px}
.modeToggleBtn{flex:1;display:flex;align-items:center;justify-content:center;
  height:28px;background:transparent;border:none;padding:0;cursor:pointer;
  color:var(--muted);border-radius:999px;transition:color .15s}
.modeToggleBtn:hover{color:var(--text)}
.modeToggleBtn.active,.modeToggleBtn.active:hover{color:var(--accent-ink)}

/* ---------- segmented toggle: one accent pill that slides between slots ----------
   Every .modeToggle / .projRow gets a single ::before pill. js/main.js's
   positionSegPill() writes --seg-x / --seg-w from the active child's box on
   each change, and a ResizeObserver re-runs it when a row is resized or first
   revealed. .seg-anim (added right after the first placement) is what turns
   the move into an animation instead of a jump on load. */
.modeToggle,.projRow{position:relative}
.modeToggle>*,.projRow>*{position:relative;z-index:1}
.modeToggle::before,.projRow::before{content:'';position:absolute;z-index:0;left:0;
  top:3px;bottom:3px;width:var(--seg-w,0);transform:translateX(var(--seg-x,3px));
  border-radius:999px;background:var(--accent);pointer-events:none}
.modeToggle.seg-anim::before,.projRow.seg-anim::before{
  transition:transform .18s cubic-bezier(.33,1,.68,1),
             width .18s cubic-bezier(.33,1,.68,1)}
#panel h2{font-family:var(--mono);font-weight:600;font-size:var(--fs-caps-lg);letter-spacing:var(--ls-hero);
  color:var(--muted);text-transform:uppercase;padding:14px 14px 8px;border-top:1px solid var(--line);
  display:flex;justify-content:space-between;align-items:center}
#panel h2:first-child{border-top:0}
.sec{padding:0 14px}
.subBox{border:1px solid var(--line);border-radius:8px;padding:2px 10px 6px;margin:10px 0;
  background:rgba(255,255,255,.025)}
.subBox .boxLabel{font-family:var(--mono);font-size:var(--fs-caps);letter-spacing:var(--ls-caps);color:var(--muted);
  text-transform:uppercase;margin:7px 0 1px}
.stat{font-family:var(--mono);font-size:var(--fs-data);color:var(--muted);line-height:1.7}
.stat b{color:var(--text);font-weight:400}
.warnTxt{font-family:var(--mono);font-size:var(--fs-data);color:var(--warn);line-height:1.5;margin-top:4px}
/* explanatory paragraph under a section heading (Dedup, Texture mode…) */
.hint{font-size:var(--fs-data);color:var(--muted);line-height:1.4;margin:0 0 10px}

.ctl{display:grid;grid-template-columns:86px 1fr 44px;align-items:center;gap:8px;margin:7px 0}
.ctl.ctlWideLabel{grid-template-columns:104px 1fr 44px}
.ctl.ctlWideLabel label{white-space:nowrap}
.vpLabel{font-family:var(--mono);font-size:var(--fs-caps);color:var(--muted);letter-spacing:var(--ls-caps);
  text-transform:uppercase;margin:8px 0 4px}
.vpGrid{display:grid;gap:4px}
.vpGrid4{grid-template-columns:repeat(4,1fr)}
/* compact button — the one deliberate exception to --fs-btn: a 4-up micro-grid
   (NW/NE/SW/SE) that would overflow at button size, so it uses --fs-data. */
.vpBtn{background:var(--panel2);border:1px solid var(--line);border-radius:50cqh;color:var(--text);
  font-size:var(--fs-data);font-weight:600;padding:6px 2px;cursor:pointer;text-align:center}
.vpBtn:hover{border-color:var(--accent);color:var(--accent)}
.ctl label{font-size:var(--fs-control);color:var(--text)}
.ctl .val{font-family:var(--mono);font-size:var(--fs-data);text-align:right;color:var(--text);cursor:text}
.ctl .val[contenteditable="true"]{outline:none;-webkit-user-select:text;user-select:text}
input[type=range]{-webkit-appearance:none;appearance:none;height:3px;border-radius:2px;
  background:var(--slider);cursor:pointer;width:100%;min-width:0}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:10px;height:10px;
  border-radius:50%;background:var(--accent)}
input[type=range]::-moz-range-thumb{width:11px;height:11px;border-radius:50%;
  background:var(--accent);border:2px solid var(--bg)}
input[type=checkbox]{-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0;flex:none;
  border:1.8px solid var(--accent);border-radius:50%;background:transparent;
  cursor:pointer;box-sizing:border-box}
input[type=checkbox]:checked{background:radial-gradient(circle, var(--accent) 4.0px, transparent 4.5px)}
/* checkbox row — ONE class for every checkbox in the app (a <label> wrapping
   the input + its text). Modifiers:
     .chk-sub    muted secondary checkbox (e.g. "same noise field per layer")
     .chkRow     two checkboxes sharing one line, split evenly
     .rowSplit   a checkbox and an unrelated sibling control pushed to opposite ends */
.chk{display:flex;align-items:center;gap:8px;margin:7px 0;
  font-size:var(--fs-control);cursor:pointer}
.chk input{width:var(--check-size);height:var(--check-size)}
.chk-sub{margin:0;color:var(--muted)}
.chkRow{display:flex;gap:8px;margin:7px 0}
.chkRow .chk{flex:1;margin:0;min-width:0}
.rowSplit{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
.rowSplit .chk{margin:0}
select,input[type=number]{background:var(--panel2);border:1px solid var(--line);
  border-radius:5px;padding:4px 6px;font-family:var(--mono);font-size:var(--fs-data)}
input[type=number]{width:58px}

/* Dash section (cog tab) — two user-editable 6-value patterns */
.dashGroup{margin:10px 0}
.dashGroupLabel{display:flex;align-items:center;justify-content:space-between;
  font-size:var(--fs-control);color:var(--text);margin-bottom:6px}
.dashPreview{width:110px;height:10px;flex:none}
.dashPreview line{stroke:var(--muted);stroke-width:2;fill:none}
.dashFieldsRow{display:flex;gap:3px}
.dashField{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:2px}
.dashField input{width:100%;min-width:0;padding:3px 0;font-size:var(--fs-data);text-align:center;
  -moz-appearance:textfield}
.dashField input::-webkit-outer-spin-button,.dashField input::-webkit-inner-spin-button{
  -webkit-appearance:none;margin:0}
.dashField span{font-size:var(--fs-caps);color:var(--muted);text-transform:uppercase;letter-spacing:var(--ls-caps)}

/* Texture section (texture tab) — each effect is a .textureGroup: a .chk
   on/off toggle, then its .textureFieldsWrap of min/max sliders. */
.textureGroup{margin:10px 0}
.textureGroup > .chk{margin:0 0 6px}
.textureFieldsWrap{transition:opacity .15s}
.textureSectionSep{border:none;border-top:1px solid var(--line);margin:14px 0 10px}

/* layer rows — pen swatch style */
#hatchLayers{border-top:1px solid var(--line);margin-top:6px;padding-top:2px}
/* Applied to #plot itself (the shared parent of #paperContent AND
   #previewLayoutOverlay — see the "Layout overlay" toggle), not to
   #paperContent alone, so the live drawing's own strokes and the Preview
   overlay's cloned block strokes blend against EACH OTHER, not just within
   their own subtree. #previewLayoutOverlay's content is real cloned DOM
   (not a <use> reference — see renderPreviewLayoutOverlay's own comment),
   so mix-blend-mode on it behaves exactly like any other SVG content here,
   with no cross-<use>-boundary ambiguity. */
#plot.blendMultiply{isolation:isolate}
#plot.blendMultiply #paperContent > g{mix-blend-mode:multiply}
#plot.blendMultiply #previewLayoutOverlay .layoutLayerStroke{mix-blend-mode:multiply}
/* Layout mode's equivalent, but scoped to blend across ALL blocks rather
   than only within each one individually. isolation:isolate on a PER-BLOCK
   element (the old approach) creates its own separate blending boundary for
   each block, so blend-mode inside it could never see anything outside
   that one block — moving isolation up to the single shared container that
   holds every block, and marking each individual layer's stroke group
   (not the per-block wrapper) as the thing that blends, means every layer
   of every block blends against everything drawn before it, block
   boundaries included. */
#layoutBlocksLayer.blendMultiplyLayout{isolation:isolate}
#layoutBlocksLayer.blendMultiplyLayout .layoutLayerStroke{mix-blend-mode:multiply}
.layer{display:grid;grid-template-columns:16px 50px 1fr 26px 56px 58px;align-items:center;
  gap:6px;padding:5px 0;border-bottom:1px dashed var(--divider)}
.layer .nm{font-size:var(--fs-control);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.layer .nm.hid{color:var(--muted);font-size:var(--fs-data);padding-left:8px}
.layer input[type=checkbox]{width:var(--check-size);height:var(--check-size)}
.layer input[type=color]{width:24px;height:20px;border:1px solid var(--line);border-radius:4px;
  background:none;padding:0;cursor:pointer}
.layer input[type=number]{width:56px;padding:3px 4px;font-size:var(--fs-data)}
.layer select{width:58px;padding:3px 3px;font-size:var(--fs-data)}
.swatch{width:50px;height:14px;background:var(--paper);border-radius:2px;display:block}
#pageColor{width:56px;height:24px;border:1px solid var(--line);border-radius:4px;background:none;cursor:pointer}
.rowsHead{display:grid;grid-template-columns:16px 50px 1fr 26px 56px 58px;gap:6px;
  font-family:var(--mono);font-size:var(--fs-caps);color:var(--muted);letter-spacing:var(--ls-caps);
  text-transform:uppercase;padding:2px 0}

/* ---------- footer ---------- */
footer{padding:0 14px;border-top:1px solid var(--line);font-family:var(--mono);
  font-size:var(--fs-data);color:var(--muted);white-space:nowrap;overflow:hidden}
footer b{color:var(--text);font-weight:600}
#dropHint{position:absolute;inset:0;display:none;align-items:center;justify-content:center;
  background:color-mix(in srgb, var(--bg) 80%, transparent);font-family:var(--mono);color:var(--accent);z-index:5;
  border:2px dashed var(--accent);pointer-events:none}
body.dragging #dropHint{display:flex}
body.dragging #camPanelStack,body.dragging #paperPanelStack,body.dragging #axisGizmo,body.dragging #lightGizmo,body.dragging .cornerBtn{display:none}
@media (prefers-reduced-motion: reduce){*{transition:none!important}}
