/* ============================================================================
 * css/gizmo.css — the spatial gizmo editor's chrome (spec: docs/gizmo-wysiwyg-
 * spec-2026-07-12.md; ported from docs/gizmo-wysiwyg-mockup.html).
 *
 * STRUCTURE ONLY, like css/style.css: every color is a var(--token) injected
 * from data/theme.js. The black/white handle casings are the one deliberate
 * exception — the double outline that keeps a handle legible on any wedge
 * color, any theme (spec §4).
 *
 * Scope: everything here is inert until <body class="editing"> or a gizmo
 * floater (#pop/.pairpop/.cpick) exists. View mode is untouched.
 * ========================================================================== */

/* ---- edit mode ground rules ---------------------------------------------- */
/* A drag across real DOM is a TEXT SELECTION unless we say otherwise. Kill it
   in edit mode — but only on the stage; the card's inputs stay selectable. */
body.editing .rollline-panel,
body.editing .rollline-panel * { user-select: none; -webkit-user-select: none; -webkit-user-drag: none; }
body.editing #pop input, body.editing #pop select, body.editing #pop textarea,
.pairpop input, .cpick input { user-select: text; -webkit-user-select: text; }
body.dragging { cursor: inherit; }
body.dragging * { cursor: inherit !important; }
body.editing .rollline-panel { outline: 1px dashed var(--line); }

/* ---- the elements -------------------------------------------------------- */
/* .gz-el is a TAG the gizmo puts on the real nodes at edit-mode entry; z-order
   comes from the registry (data/elements.js), never hand-written here. */
/* THE TAG DOES NOT POSITION THE PIECE. `position: relative` was here for the z-order below —
   z-index needs a positioned box — and it was applied to EVERY piece, including the ones the
   canvas has already positioned (`position: absolute`, style.css: that is how a coordinate
   becomes a place). It overrode them, and a relative box reads `top` as an offset from the flow
   AND leaves its margin in play, where an absolute box folds the margin into the coordinate that
   roll-area placeCanvas so carefully subtracted back out. Every piece carrying a margin sat one
   margin too high in edit mode, and nowhere else. The Result's margin is .9rem: 16px, exactly the
   drop you saw when you left the editor.
   The pieces that have no position of their own are told so at tagging time (js/gizmo.js
   tagElements: .gz-unplaced), and only they get one. */
/* A CURSOR IS A PROMISE — and since §3 a press on a piece only SELECTS it (moving is the ✥
   chip's job, and the ✥ carries its own `cursor: move`). The four-way arrow here was promising
   a drag the body no longer performs (review 2026-07-14). Pointer: "you can pick this". */
body.editing .gz-el { cursor: pointer; }
body.editing .gz-el.gz-unplaced { position: relative; }   /* ...only where there is nothing to override */
.gz-el.cur { z-index: 20 !important; }          /* the thing you are editing is on top… */
.gz-el[data-sub] { z-index: 25 !important; }    /* …but a sub-object is always above its parent */

/* the texture of "this is the bit that will respond": transparent, so the
   paint below still tells the truth; diagonal, so it is never the thing itself */
/* THE HATCH IS DECLARED ONCE, HERE, AND EVERYTHING ELSE READS IT.
   --hatch-rgb is set beside every fill in the app (js/util.js inkOn) from the SAME function that
   decides the text colour — util.textOn. Ink and stripes therefore cannot disagree: whatever the
   text does on a colour, the hatch does. The default is white, for the dark chrome that has no
   fill of its own. (A custom property nested in another resolves at the point of USE, so each
   element gets its own answer out of one declaration.)
   The GEOMETRY and the ALPHA live here too, and js/gizmo.js builds its SVG patterns out of these
   very tokens — SVG cannot use a gradient the way CSS can, so it needs its own patterns, but it
   does NOT need its own numbers. (It used to have them: .34 alpha where CSS said .30. Small, and
   exactly the kind of drift that makes two things that should be one thing look like two.) */
:root {
  /* EVERY DOM HANDLE IS THE SAME HEIGHT AND CARRIES THE SAME SIZE OF ICON. The corner, the ▬ edge,
     the ✥ mover and the card's grip are four of ONE thing — a handle — and they had drifted into
     four sizes (1.9rem, 1.6rem, 1.15rem, and an icon each). The ▬ is WIDER, because a bar is what
     it is; it is not SHORTER, because that is not what a bar is. One number, four readers. */
  --gz-handle: 1.9rem;
  --gz-edge-w: 4.6rem;                       /* the ▬ edge's width — the ✥ stands off its shoulder */
  /* THE CHIPS ARE PRESSED, THE HANDLES ARE DRAGGED, AND THEY STAND SIDE BY SIDE — so they are
     measured against each other. The point chips were 26px, a number from when a handle was 13px
     across; the handles have since doubled (data/theme.js editor.handleR) and the chips did not,
     so the things you PRESS ended up smaller than the things you DRAG. */
  --gz-chip: 2.75rem;   /* the stage chips (point delete / snappify) match the card's chips, not a size of their own */
  --gz-gap: .55rem;                          /* the air between two handles standing side by side */

  /* THE CASING — a handle's double outline, the trick that keeps it legible on ANY wedge
     colour (see the note at the top of this file). It is a documented exception to "no
     hardcoded colours", but its GEOMETRY was still being re-typed at six sites. */
  --casing:    0 0 0 1.5px #fff, 0 0 0 3px #000;     /* on a handle */
  --casing-lg: 0 0 0 2px   #fff, 0 0 0 4.5px #000;   /* on a bigger target: a sub-part, a pill */
  --hatch-rgb: 255,255,255;
  --hatch-a: .30;         /* how strongly the stripes sit on the paint */
  --hatch-stripe: 3;      /* stripe width, in px */
  --hatch-period: 8;      /* stripe + gap */
  --hatch: repeating-linear-gradient(45deg,
    rgba(var(--hatch-rgb), var(--hatch-a)) 0 calc(var(--hatch-stripe) * 1px),
    rgba(0,0,0,0) calc(var(--hatch-stripe) * 1px) calc(var(--hatch-period) * 1px));
}
/* the sub-part ring: black casing + white ring, exactly as hdl() draws it. Never a tint.
   A part answers for itself whether or not its owner is selected — the hatch is the promise
   that a press here lands HERE (js/gizmo.js: subpartClick on the first press), so it must not
   wait for the owner to be chosen before it dares to show up. A ghost is a picture of a piece,
   not a piece: its parts promise nothing. */
body.editing .gz-el:not(.ghost) .subpart:hover,
body.editing .gz-el:not(.ghost) .pill:hover { box-shadow: var(--casing-lg);
  background-image: var(--hatch); }
/* (the fillable-body hover hatch lived here until 2026-07-14 §7. It was the promise "click the
   paint and you get the fill picker" — and §6 moved that doorway to the ◉ fill chip, so the body
   no longer makes the promise and must not wear its texture. Hatch now means exactly one thing:
   the hovered element / sub-part that will respond — spec §11, the wedges' own rule.) */
/* .subpart is the APP's class (js/roll-area.js). Its one job HERE is the editing hit-target,
   so the selector says so. Outside edit mode the padding and the negative margin cancel and
   nothing is painted, so scoping it costs nothing but gains the honesty. */
body.editing .subpart { border-radius: var(--radius-sm); padding: 0 3px; margin: 0 -3px; }
body.editing .gz-el.cur .pill:hover { position: relative; z-index: 1; }

/* pulse: breathes at all times, holds its breath under the cursor */
.gz-el.pulsing { animation: gz-breathe 2.4s ease-in-out infinite; }
body.editing .gz-el.pulsing:hover { animation-play-state: paused; }
@keyframes gz-breathe {
  0%, 100% { transform: translate(var(--tx,0), var(--ty,0)) scale(var(--s,1)); }
  50% { transform: translate(var(--tx,0), var(--ty,0)) scale(calc(var(--s,1) * (1 + var(--amp,0)))); }
}

/* A ghost is an OUTLINE, not a dimmed copy (its dashed box + caption live in
   the unscaled #boxes overlay, so dash weight never scales with the object). */
/* A GHOST DOES NOT ARRIVE, IT IS SIMPLY THERE. The pieces are real controls and real
   controls have manners — .roll-btn transitions its background. So un-hiding a piece as a
   ghost handed the browser a change to animate (its real fill -> transparent), and the
   button flashed at full colour for a frame and then faded out. A ghost is a DRAWING of a
   piece, not a piece being dressed: it has no animation and no transition, in or out. */
/* A GHOST NEEDS SOMETHING TO HOLD. An outline is a picture of where a piece WOULD be, and a
   picture has no handle: the dashed box lives in the unscaled overlay, so the only thing under
   your pointer was the piece's own transparent box — you were grabbing at a shape you could see
   but not feel. A 10% black backing gives it a surface. It is faint enough to stay a drawing
   (you are not looking at a button; you are looking at a space reserved for one) and solid
   enough to be a target. */
.gz-el.ghost { z-index: 1 !important;
  opacity: 1 !important; background: rgba(0,0,0,.10) !important; border: 0 !important;
  color: transparent !important; box-shadow: none !important; outline: 0 !important;
  animation: none !important; transition: none !important; text-shadow: none !important; }
.gz-el.ghost > * { visibility: hidden; }

/* A WEDGE IS PICKED UP, NOT PUSHED. While one is being dragged the cursor closes around it —
   the same hand every list in the world uses to say "you have hold of this". */
body[data-drag="grabbing"], body[data-drag="grabbing"] * { cursor: grabbing !important; }

/* headline buttons are CHROME, not composition: scenery while editing */
body.editing #roll-headline .share-link,
body.editing #roll-headline .fav-star,
body.editing #roll-headline .nom-trophy { pointer-events: none; opacity: .75; }
body.editing #roll-headline { cursor: default; }
/* ...AND THE BACKGROUND SAYS NOTHING AT ALL. The panel is a .gz-el, and it is the one every
   other piece is drawn inside — so the rule above would put its cursor on the ENTIRE stage,
   every empty pixel. A press there selects the panel, but a whole screen of "pick me" is
   noise, not a promise. */
body.editing .gz-el[data-el="panel"] { cursor: default; }

/* ---- SVG overlay layers --------------------------------------------------- */
#gz-boxes { position: absolute; inset: 0; z-index: 58; pointer-events: none; overflow: visible; }
#gz-ov { position: absolute; inset: 0; z-index: 64; pointer-events: none; overflow: visible; }
/* (.rollline-svg's `overflow: visible` — what lets a handle hang outside the ink — is
   css/style.css's, and always was. This file was saying it a second time.) */

/* EVERY handle, in every layer. A handle is a thing you GRAB: one class, one
   cursor, and the layer it happens to be drawn in has no say in the matter. */
/* A HANDLE ALWAYS WINS THE PRESS. The vibe rail's knobs are drawn INSIDE the engine's SVG —
   i.e. inside the line's box, which sits below the buttons in the canvas's stacking order — so
   a knob that happened to lie over the Roll button could not be grabbed: the button took the
   press. A handle is chrome, not composition; it is on top of everything it is drawn over. */
.gz-hdl, .gz-hdl * { pointer-events: auto; cursor: grab; }
body.editing .gz-layers, body.editing .gz-underlay { pointer-events: none; }   /* a layer is not a target... */
body.editing .gz-layers .gz-hdl, body.editing .gz-layers .gz-hdl * { pointer-events: auto; }   /* ...its handles are */
body.editing .gz-el.cur .gz-layers, body.editing .rollline-svg { overflow: visible; }
body.wmode.editing #rollline-stage, body.editing .gz-el.cur { z-index: 30 !important; }
.gz-hdl:active, .gz-hdl:active * { cursor: grabbing; }
/* NO transition on a tether's knob either: while it is being dragged, js/forms.js's spring
   owns its transform (see --pull in style.css), and a transition would smear every frame of
   that into the next one. */
.gz-hdl { transition: none; }
body:not(.dragging) .gz-hdl:hover { filter: drop-shadow(0 0 4px var(--accent)); }
/* EVERY HANDLE SAYS WHAT IT DOES — when you point at it. It grows to make room, and the glyph
   for its VERB fades up (data/icons.js handleIcons; js/gizmo.js hdl()).
   The grow is on .gz-hart, the inner art group — NEVER on .gz-hdl, whose `transform` ATTRIBUTE
   carries translate(x y) plus inContent()'s counter-scale. A CSS transform there would replace
   that attribute and fling the handle to the SVG origin.
   The HIT circle is a sibling of the art and does not grow: hovering changes what a handle
   SHOWS, never what it catches ("A HANDLE ALWAYS WINS THE PRESS" — and it wins the same press). */
/* A HANDLE IN YOUR HAND DOES NOT FLINCH. Every drag move repaints the chrome, which REBUILDS the
   handle under the pointer — a brand-new node, arriving already hovered, starting the hover
   transition from scratch. Sixty times a second that is not a hover, it is a tremor: the knob
   pulsed between its two sizes for the whole length of the pull. So the hover answer is only for
   a hand that is NOT already holding something (body.dragging is set for the length of any drag —
   js/gizmo.js drag()), and the transition is off while it is. */
.gz-hart { transition: transform var(--t-fast) var(--bounce); transform-origin: 0 0; }
body:not(.dragging) .gz-hdl:hover .gz-hart { transform: scale(1.3); }
body:not(.dragging) .gz-hdl:hover .gz-hart > *:nth-child(1) { stroke-width: 7; }
body:not(.dragging) .gz-hdl:hover .gz-hart > *:nth-child(2) { stroke-width: 4.5; }
body.dragging .gz-hart { transition: none; }
/* A HANDLE SAYS WHAT IT DOES, ALWAYS. The glyph used to fade up on hover — which meant you had to
   already be pointing at a handle to find out what it was for, and the one thing you cannot do
   while hunting for the right handle is point at all of them. It is part of the handle now; the
   handle is big enough to hold it (data/theme.js editor.handleR), and hovering still grows it. */
.gz-hglyph { pointer-events: none; color: #000; }

/* the outline's band lights while you are on its knob — same hatch as the fill */
/* (.oring — the outline's hover band — retired 2026-07-14: the outline is not interactable;
   the ● knob is its whole presence.) */

/* wedges (overlay hit-paths drawn by gizmo-stage over the engine's ink) */
body.editing .gz-wedgehits path { cursor: pointer; pointer-events: stroke; }
.gz-wedgehits path { fill: none; stroke: transparent; }
#gz-spdghost { fill: #fff; opacity: .9;
  filter: drop-shadow(0 0 3px #000) drop-shadow(0 0 6px var(--scrim-strong)); }
/* the key's pills are display-only in the game (style.css turns their pointer
   events off) — in EDIT mode they are the outcome's name, and the name is a
   target (spec §1.1). The evicted list stays scenery: it is play state. */
/* THE PASSIVE PIECES TAKE THE POINTER WHILE EDITING. In play they are
   `pointer-events: none` (style.css) so a Result or a Key parked over the Roll button can
   never eat a tap. In EDIT mode that made them unreachable: the press fell through to
   whatever was behind (usually the line), so the Result could not be selected — and the
   corner handles you thought were resizing it were resizing the LINE. */
body.editing .result-readout,
body.editing #outcomes-table,
body.editing #evicted-wrap { pointer-events: auto; }
body.editing #outcomes-table tbody tr, body.editing #outcomes-table tbody tr * {
  pointer-events: auto; cursor: pointer; }
body.editing .gz-el.ghost tbody tr, body.editing .gz-el.ghost tbody tr * { pointer-events: none; }

/* THE MARKER IS AS BIG AS ITS BOX, NOT AS BIG AS ITS INK. An SVG <text> is hit-tested on the
   GLYPH — the actual strokes of the character — so a triangle marker could only be grabbed on the
   triangle itself, and the gaps beside it (which are inside its box, inside its selection frame,
   and inside the dashed box the editor draws to say "this is the thing") answered to nothing. You
   were being asked to hit a shape with a mouse. `bounding-box` makes the press mean what the frame
   promises: the whole box is the marker. */
body.editing .rollline-marker { pointer-events: bounding-box; }

/* on-wedge names respond in the two editors that have something to say about them: WEDGES (the
   words) and LOOK (their size) — and nowhere else */
body:not(.wmode):not(.lmode) .rollline-svg .ow-pill { pointer-events: none; }
body.wmode.editing .rollline-svg .ow-pill,
body.lmode.editing .rollline-svg .ow-pill { pointer-events: auto; cursor: pointer; }
/* THE HALO BELONGS TO THE KEY, AND ONLY WHILE YOU ARE ON IT — the sub-part hover, said the only
   way an SVG name can say it. TWO BUGS LIVED IN THIS ONE LINE:
     the colour was hardcoded #fff, so a white name on a dark wedge got a white edge and vanished
       into it — precisely the thing an edge exists to prevent. It is the inverse of the text now
       (--ink-inv, declared by the engine from util.inkInverse: the one colour the letters cannot
       be), so black text always gets a white edge and white text always gets a black one.
     the width was 3.5 USER UNITS on type only ~8 units tall — 43% of the glyph's height, which
       floods the counters and turns an 8 into a blob. The engine sets --halo-w from the pill's
       own font size, so the edge is a PROPORTION of the letters it is drawn around.
   (Hovering the WEDGE is a different question with a different answer — the wedge lights. The
   name is not part of that question.) */
body.wmode.editing .rollline-svg .ow-pill:hover text,
body.lmode.editing .rollline-svg .ow-pill:hover text {
  stroke: var(--ink-inv, #000); stroke-width: var(--halo-w, 1px); paint-order: stroke; }
/* THE SLIDE — armed only while reordering */
body.reorder .rollline-seg, body.reorder .gz-hotw path {
  transition: stroke-dashoffset var(--t-med) cubic-bezier(.22,.85,.25,1),
              stroke-dasharray var(--t-med) cubic-bezier(.22,.85,.25,1); }

/* ---- selection frame ------------------------------------------------------ */
/* THE WASH IS A BACKING — under the thing it lifts, and under every other piece with it.
   Getting it there took two goes:
     z-index: 0  put it OVER the Key and the Result (which had no z-index of their own).
     z-index: -1 put it behind the CARD ITSELF. A negative z-index does not mean "one
                 layer down"; it means "below the in-flow content of the nearest STACKING
                 CONTEXT" — and #rollline-panel, being merely `position: relative`, is not
                 one. The wash sailed past it and painted behind the panel's background.
   The fix is not on the wash at all: the CANVAS is lifted to its own layer (z-index: 1 on
   #panel-content, in style.css), so every piece rides above the wash and the wash still
   sits above the card it is drawn on. */
/* THE CENTRE OF THE BACKGROUND, WHILE YOU ARE PUTTING SOMETHING SOMEWHERE.
 *
 * Two hairlines through the middle of the canvas, shown only while a placing gesture runs
 * (body.placing — js/gizmo.js beginPlacing). They are a REFERENCE and nothing else: no pointer
 * events, no snapping, no opinion. The grid is what snaps, and it has its own switch.
 *
 * They are drawn by the CANVAS, on the canvas's own middle — so nothing has to recompute them
 * when the panel resizes, and entering full screen (which resizes it) simply moves them. */
#gz-guides { position: absolute; inset: 0; display: none; pointer-events: none; z-index: 57; }
body.placing #gz-guides { display: block; }
/* A REFERENCE YOU CANNOT SEE IS NOT A REFERENCE. These were a 1px --line hairline at .55 — the
   colour the app uses for the edge of a box that is not asking for attention, which is the exact
   opposite of this job. You are looking for them, ON TOP of a busy composition, while your eye is
   on the piece in your hand: 2px, and light enough to read against the dark panel and the ink both.
   Still quiet — a guide, not a rule — but present. */
#gz-guides::before, #gz-guides::after {
  content: ''; position: absolute; background: var(--ink-dim);   /* #9a9486 — readable grey, not the
     placeholder grey (--ink-faint, #5f5c54, which is what a guide you cannot find is made of) */
  opacity: .85; box-shadow: 0 0 0 1px var(--scrim);   /* a hair of casing: legible over the ink too */
}
/* --gx / --gy are set by js/gizmo.js placeGuides(): the middle of the canvas as it will be in FULL
   SCREEN, in canvas px. The 50% fallback is only for the case where nobody has measured yet. */
#gz-guides::before { left: 0; right: 0; top: var(--gy, 50%); height: 2px; }   /* the horizontal middle */
#gz-guides::after  { top: 0; bottom: 0; left: var(--gx, 50%); width: 2px; }   /* ...and the vertical */

/* THE WAND HAS THE FLOOR. While Auto-layout runs (js/gizmo.js autoLayout: breath, snap, soak) the
   app is rearranging the whole composition in front of you, and for those two and a half seconds
   the pointer is not yours: a press would land on a layout that is about to be rewritten, or on a
   canvas that is about to stop being full screen.
   So: a border, to say who is working — and a surface that eats every press until it is finished.
   It does NOT dim anything. The whole point of the pauses is that you are watching what is behind
   it; a scrim here would hide the very thing it is waiting for you to see. */
#gz-wand { position: fixed; inset: 0; z-index: 500; display: none; pointer-events: none; }
body.wanding #gz-wand {
  display: block; pointer-events: auto; cursor: progress;
  box-shadow: inset 0 0 0 3px var(--accent), inset 0 0 22px var(--scrim);
  animation: gz-wanding 1.1s ease-in-out infinite;
}
@keyframes gz-wanding {                      /* the border breathes: the app is working, not stuck */
  0%, 100% { box-shadow: inset 0 0 0 3px var(--accent), inset 0 0 22px var(--scrim); }
  50%      { box-shadow: inset 0 0 0 3px var(--accent-dim), inset 0 0 30px var(--scrim); }
}

#gz-selwash { position: absolute; display: none; pointer-events: none; z-index: 0;
  border-radius: var(--radius-md); background: rgba(255,255,255,.10); }
/* ...unless what it is backing lives INSIDE the line (the marker): the line's own ink is drawn
   over the whole canvas, so a wash beneath the canvas is a wash beneath the wheel — the marker's
   box disappeared under the thing it rides on. Same layer as the frame that goes with it. */
#gz-selwash.inline { z-index: 59; }
/* A NAME ON THE WHEEL STANDS ON PAINT, NOT ON THE PANEL. The wash is 10% white — plenty against
   the dark card behind every other piece, and all but invisible on a bright red wedge, which is
   where this one has to be read. It gets a stronger backing and a rim, so the box you are scaling
   is a box you can SEE. */
#gz-selwash.ow { background: rgba(255,255,255,.22);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.35), 0 0 0 1px rgba(255,255,255,.35); }
#gz-sel { position: absolute; display: none; pointer-events: none; z-index: 60; --L: 15px; --w: 1.6px;
  background-image:
    linear-gradient(var(--accent),var(--accent)) left  top   /var(--L) var(--w) no-repeat,
    linear-gradient(var(--accent),var(--accent)) left  top   /var(--w) var(--L) no-repeat,
    linear-gradient(var(--accent),var(--accent)) right top   /var(--L) var(--w) no-repeat,
    linear-gradient(var(--accent),var(--accent)) right top   /var(--w) var(--L) no-repeat,
    linear-gradient(var(--accent),var(--accent)) left  bottom/var(--L) var(--w) no-repeat,
    linear-gradient(var(--accent),var(--accent)) left  bottom/var(--w) var(--L) no-repeat,
    linear-gradient(var(--accent),var(--accent)) right bottom/var(--L) var(--w) no-repeat,
    linear-gradient(var(--accent),var(--accent)) right bottom/var(--w) var(--L) no-repeat;
  border-radius: var(--radius-sm); }
#gz-sel.on { display: block; }
/* ■ square = size. White core, double-outlined, so it reads on anything. */
/* THE HANDLES GROW WITH THE TYPE. rem, not px: the settings font slider is not a typography
   preference, it is "make things big enough for me to see and hit", and a scale box authored
   in px is exactly the thing it fails to help with. (The SVG handles do the same, from JS —
   data/elements.js uiScale().) */
/* ==========================================================================
 * THE SCALE SQUARE — ONE handle, declared ONCE, wherever a thing is resized from.
 *
 * There is one corner now, at the bottom-right, where every window in the world is resized from
 * (four corners all measured the same ratio from the centre: four ways of saying one thing, over
 * the top of the piece you were trying to look at). And the CARD is resized from the same square
 * — it always was, but it had its own copy of the rule, and the two had drifted: .7rem there,
 * 1rem here, so the card's grip looked like a smaller, shyer relative of the handle it is. Same
 * square, same size, same hover. Only WHERE it stands is said twice.
 * ======================================================================== */
/* the corner and the card's grip: one size with the SVG handles, so a handle is a handle */
#gz-sel .h, #gz-sel .th, .gz-cardgrip { position: absolute; width: var(--gz-handle); height: var(--gz-handle); background: #fff;
  border: 1.5px solid #000; border-radius: var(--radius-xs); box-shadow: var(--casing);
  pointer-events: auto; cursor: nwse-resize; }
/* the frame's corner and the CARD's corner are the same handle doing the same job (scale), so they
   are dressed by one rule — icon, ink and all. The card's used to be the only one of the four DOM
   handles with nothing written on it. */
#gz-sel .h, #gz-sel .th, .gz-cardgrip { display: grid; place-items: center; color: #000; transition: transform var(--t-fast) var(--bounce); }
#gz-sel .h:hover, .gz-cardgrip:hover { transform: scale(1.35); }
#gz-sel .h svg, #gz-sel .th svg, .gz-cardgrip svg { display: block; }
#gz-sel .h:hover, #gz-sel .th:hover { transform: scale(1.2); }
/* THE TEXT'S OWN GRIP — same handle, different subject: the corner scales the BUTTON, this scales
   the WORDS inside it. It is only there for a piece whose registry entry has a `textScale`, and
   js/gizmo.js places it on the label's own bottom-right corner (which moves as the text grows). */
#gz-sel .th { display: none; }
#gz-sel.hastext .th { display: grid; }
/* WHERE THE GRIP SITS AT REST — and on a small object, the only thing that places it. Past a
   quarter of the viewport js/gizmo.js placeGrip() takes over: the grip moves to the corner nearest
   your hand and is clamped into the window, so a thing too big to see can still be shrunk. It
   writes left/top and sets these two to auto; below the threshold it clears them again and this
   rule is back in charge. One definition of the corner, in one place. */
#gz-sel .h.br { right: -.45rem; bottom: -.45rem; }
/* ...and the TOP-RIGHT corner — the cluster's static seat since the 2026-07-14 review ("put
   these on the top of the buttons, so they don't conflict with the tracks"): the right flank
   carries the ✦ pulse leash, the bottom carries the label's text grip and the ▬ edge — the
   top is the quiet edge. */
#gz-sel .h.tr { right: -.45rem; top: -.45rem; bottom: auto; }
/* THE ARROW PULLS ALONG THE DIAGONAL ITS CORNER STANDS ON (the scale glyph is a double-headed
   arrow drawn down the ↘ diagonal — see data/icons.js). ↖ and ↘ share that diagonal, so tl and br
   wear it as drawn; ↗ and ↙ are the other one, so they wear it mirrored. On the SVG, not the
   handle: the handle's own transform is the hover grow, and the two would overwrite each other. */
#gz-sel .h.tr svg, #gz-sel .h.bl svg { transform: scaleX(-1); }
#gz-sellabel { position: absolute; left: -1px; top: -25px; font-size: var(--fs-82); font-weight: 800;
  background: var(--on-bg); color: var(--on-ink); pointer-events: auto; text-shadow: 0 1px 2px var(--scrim);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0; padding: 2px 7px; white-space: nowrap;
  display: flex; gap: 5px; align-items: center; }
/* ▬ bar = an edge: how tall the selected thing's BOX is. Two things own a height —
   the canvas (panelHeight, the card) and the line (lineHeight, the room its art
   gets). The class is put on by gizmo.place() from the registry (ELEMENTS[id].height),
   so a third one would need no CSS. */
/* A BAR IS THIN, AND A GLYPH NEEDS ROOM. The edge is still a bar at rest — the shape is the code —
   but it is thick enough to hold its ↕ now, and it opens up when you point at it. */
#gz-sel .eh { position: absolute; left: 50%; width: var(--gz-edge-w); height: var(--gz-handle);
  border-radius: var(--radius-xs); background: #fff; border: 1.5px solid #000; color: #000;
  box-shadow: var(--casing); pointer-events: auto; cursor: ns-resize; display: none;
  place-items: center; transform: translateX(-50%);
  transition: transform var(--t-fast) var(--bounce); }
#gz-sel.hasedge .eh { display: grid; }
#gz-sel .eh:hover { transform: translateX(-50%) scale(1.25); }   /* the same answer the corners give */
#gz-sel .eh svg, #gz-sel .h svg { display: block; }   /* always: a handle says what it does */
#gz-sel.panel .h, #gz-sel.fixed .h, #gz-sel.noscale .h { display: none; }
#gz-sel .eh, #gz-sel .ehmove { bottom: calc(var(--gz-handle) / -2); }   /* centred on the edge — both of them */
/* THE CURSOR GETS OUT OF THE WAY (js/gizmo.js drag(), opts.lock). While a railed or a
   move-everything drag is running there is nothing to aim at — only a direction to push — and an
   arrow hovering over the thing it is not quite touching is a lie about what is being dragged. */
body.nocursor, body.nocursor * { cursor: none !important; }

/* ✥ MOVE EVERYTHING — glued to the ▬ edge's right shoulder. Same white-on-black as every other
   handle, because it IS one: the composition's position is a property like any other, and it
   now has a place you go to change it instead of a whole background you can trip over. */
/* ...and it stands BESIDE the bar, not on it: half the bar's width, plus a gap. It was a flat
   2rem, which is INSIDE a 4.6rem bar — so the two handles overlapped, and the one underneath was
   unreachable. */
#gz-sel .ehmove { position: absolute; left: 50%;
  margin-left: calc(var(--gz-edge-w) / 2 + var(--gz-gap));
  width: var(--gz-handle); height: var(--gz-handle); display: none; place-items: center;
  transition: transform var(--t-fast) var(--bounce);
  border-radius: var(--radius-xs); background: #fff; color: #000; border: 1.5px solid #000;
  box-shadow: var(--casing); pointer-events: auto; cursor: move; }
/* EVERY PIECE GETS THE ✥ (2026-07-14 §3): the body is no longer a drag surface — a press on a
   piece SELECTS it, and moving it is this chip's job, exactly as the background always worked.
   `canmove` is set by placeBox()/placePanel() from the registry (ELEMENTS[id].offset), so a piece
   that cannot move (the headline) never shows a promise it cannot keep. */
#gz-sel.canmove .ehmove { display: grid; }
/* on an ordinary piece the ✥ stands beside the ◲ in a ROW ON THE TOP EDGE, right-aligned (the
   panel keeps its place at the ▬ edge's shoulder). This is the SEATING PLAN — static (review
   2026-07-14), so the pair can never wander onto the left-flank ●/◉ chips, the label's text
   grip, or the ✦ pulse leash that runs the right flank ("so they don't conflict with the
   tracks"). placeGrip() overrides it only to clamp the pair into the viewport when a big
   object's corner runs off screen. */
#gz-sel:not(.panel) .ehmove { left: auto; margin-left: 0; bottom: auto;
  right: calc(-.45rem + var(--gz-handle) + var(--gz-gap)); top: -.45rem; }
#gz-sel .ehmove:hover { background: var(--on-bg); color: var(--on-ink); transform: scale(1.25); }
#gz-sel .ehmove svg { display: block; }

/* The panel clips its content (overflow:hidden — rounded card corners). The panel's
 * resize handle deliberately hangs OUTSIDE the panel's bottom edge (#gz-ehH is
 * bottom:-5px, as in the prototype), so the clip ate it: only a sliver was visible
 * and the clipped part isn't hit-testable — the grab bar was all but unclickable.
 * The prototype's .stage does not clip at all. Match it while editing; play mode
 * keeps its clip. */
body.editing #rollline-panel { overflow: visible; }


/* ---- the card (#pop) ------------------------------------------------------ */
/* THE CARD'S HIERARCHY — three levels, and they should read as three.
     1  TITLE       what you have selected. The loudest thing on the card.
     2  the chips   what you can DO to it. Real buttons, sized like buttons.
     3  sections    the settings, and their labels are labels — quiet on purpose.
   The prototype authored these at 8-11px against a 16px root; snapped to the app's rem
   scale they all collapsed toward the floor, so a title, a button and a footnote came out
   the same size and the card read as one flat grey block. */
#pop { position: absolute; display: none; background: var(--bg-raised); border: 1px solid var(--line);
  /* THE CARD IS A ROOM FOR TYPE, so it is measured in type. At a fixed 240px its contents grew
     with the font slider while the walls did not: headings wrapped mid-word, tiles crushed, and
     the whole thing read as jank. In rem it keeps the same proportions at every size — 15rem is
     the 240px it always was, at the app's own 16px root. (The rail is 20px, so the left padding
     that clears it is px too; nothing else here is.) */
  border-radius: var(--radius-xl); padding: .8rem 1.25rem .95rem 32px; z-index: 90;
  box-shadow: var(--shadow-pop);
  width: 15rem; overflow: visible;   /* NEVER a scrollbar — the ■ corner scales it instead */
  transition: opacity var(--t-fast), transform var(--t-base), left var(--t-med) ease, top var(--t-med) ease;
  transform-origin: top left; font-size: var(--fs-90); color: var(--ink);
  cursor: move; }   /* the card says what it is: a window you can move. Its controls say what THEY are. */
#pop.on { display: block; }
#pop input, #pop button, #pop select, #pop textarea, #pop label, #pop .pk, #pop .chip,
#pop .cycb, #pop .gz-cardgrip, #pop .gz-pinbtn { cursor: pointer; }
#pop input[type=text], #pop textarea { cursor: text; }
#pop input[type=range] { cursor: ew-resize; }
#pop.yield { opacity: .14; pointer-events: none; }
#pop.gz-dragging-card { transition: none !important; }
#pop.pinned { border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-dim), var(--shadow-pop); }
body.dragging #pop.pinned { opacity: 1; pointer-events: auto; }   /* a pin outranks a drag, not a spin */
#pop .cardbar { display: flex; flex-direction: column; align-items: center; gap: 10px;
  margin: 0 0 11px; padding: 0 0 11px; border-bottom: 1px solid var(--line-soft); background: none; }
/* 1 — THE TITLE: the name of the thing you are holding. Nothing else on the card outranks it. */
#pop .cardbar h4 { margin: 0; background: none; padding: 0; line-height: 1.25; text-align: center;
  width: 100%; overflow-wrap: anywhere; font-size: var(--fs-115); font-weight: 700; color: var(--accent);
  text-transform: uppercase; letter-spacing: .06em; }
#pop .cbrow { display: flex; align-items: center; justify-content: center; gap: 7px; }
#pop .cbrow:empty { display: none; }
#pop .cardbar + .gz-csec { border-top: 0; padding-top: 0; margin-top: 0; }
#pop .cardchips { display: flex; gap: 4px; }
/* (the ‹ 1/3 › cycler lived here. It is gone — a crowded pixel is now ASKED about, not
   guessed at and then apologised for: see .gz-choose below.) */

/* THE CHOOSER — the names of everything that pixel could have meant, under the pointer */
/* no selection ring on the tray: the tray is not a selection, it is the QUESTION. The ring
   belongs to the chips — that is where the answer is. */
.gz-choose { position: fixed; z-index: 90; display: flex; flex-direction: column; gap: 4px;
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: var(--radius-lg);
  padding: 6px; box-shadow: var(--shadow-float); }
.gz-choose .gz-chip { display: block; width: 100%; text-align: left; white-space: nowrap;
  background: var(--bg-sunken); border: 1px solid var(--line); border-radius: var(--radius-md);
  color: var(--ink-dim); font-size: var(--fs-90); font-weight: 600; line-height: 1;
  padding: 8px 11px; cursor: pointer; transition: var(--t-fast); }
.gz-choose .gz-chip:hover { color: var(--ink); border-color: var(--accent-dim); background: var(--line-soft); }
#pop label, .gz-sidebox label { display: block; font-size: var(--gz-body); color: var(--ink-dim); margin: 7px 0 3px; }
#pop input[type=text], #pop select { width: 100%; background: var(--bg-sunken);
  border: 1px solid var(--line); color: var(--ink); border-radius: var(--radius-sm); padding: 5px 7px;
  font: inherit; font-size: var(--fs-78); }
/* a card is 224px wide: its sliders take the compact size, not the drawer's chunky one */
#pop input[type=range] { width: 100%; --track: 10px; --thumb: 16px; }
#pop .row { display: flex; gap: 6px; align-items: center; font-size: var(--gz-body); margin-top: 6px; }
#pop .pick3 { display: flex; gap: 5px; margin-top: 3px; }
/* ===== THE TILE (.pk) — one description, four rooms ==========================
   A tile is a tile: a bordered box with a picture over a word, and it lights up when
   it is the chosen one. That was written out three times (card, box, sidebox) with
   byte-identical `.on` states and three near-identical hovers — and the file already
   had the mechanism for this, in the ONE SIZING LANGUAGE block below. It just stopped
   halfway. The rooms differ in four things and only four, so those are the four knobs:
     --pk-bg  --pk-pad  --pk-gap  --pk-fs   (and --pk-radius, for the wide wedge row)
   The ART is each room's own — a 34x17 glyph in the card, 26x13 in the box — and stays
   with the room, because that is not sizing, it is drawing. */
#pop .pk, .pairpop .pk, .gz-sidebox .pk, .gz-kwedge .pk.wk {
  flex: 1; cursor: pointer; font: inherit;
  display: flex; flex-direction: column; align-items: center;
  background: var(--pk-bg, var(--bg-sunken));
  border: 1px solid var(--line);
  color: var(--ink-dim);
  border-radius: var(--pk-radius, var(--radius-md));
  padding: var(--pk-pad, var(--gz-tile-pad));
  gap: var(--pk-gap, 4px);
  font-size: var(--pk-fs, var(--gz-tile));
}
/* chosen — the app's one selected state (css/style.css :root) */
#pop .pk.on, .pairpop .pk.on, .gz-sidebox .pk.on, .gz-kwedge .pk.wk.on {
  background: var(--on-bg); border-color: var(--on-line); color: var(--on-ink); font-weight: 600; }
/* ...and pointed at */
#pop .pk:hover, .pairpop .pk:hover, .gz-sidebox .pk:hover, .gz-kwedge .pk.wk:hover {
  border-color: var(--accent-dim); color: var(--ink); }
/* a tile that is ALREADY lit answers in parchment, not in the brass it is made of
   — the app's one rule for this (css/style.css :root, --on-line-hover). */
#pop .pk.on:hover, .pairpop .pk.on:hover, .gz-sidebox .pk.on:hover, .gz-kwedge .pk.wk.on:hover,
.gz-tb.on:hover, .chip.gz-tab.on:hover {
  border-color: var(--on-line-hover); color: var(--on-ink); }
#pop .pk svg { width: 34px; height: 17px; fill: currentColor; color: inherit; }
#pop .pick3.lmodes .pk { flex-direction: column; gap: 3px; padding: 6px 2px 5px; }
#pop .pick3.lmodes .pk svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.6; }
/* THE WORLD, WHILE YOU ARE ALIGNING TO IT. The pad is a picture of THIS box — the panel, edge to
   edge — so while the pad is open the panel says where its edges are. (The coordinate ORIGIN is
   #panel-content, which is inset from here by the panel's padding; the two are not the same
   rectangle, and only this one is a place a person can see. See js/gizmo.js canvasBox.) */
body.aligning .rollline-panel {
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
  background: color-mix(in srgb, var(--accent) 4%, var(--bg-sunken));
}

/* THE ALIGN PAD IS GONE — with its grid, its stops and its goop-flung dot (js/gizmo.js ALIGN).
   A dot on a 7-column pad can only offer 7 places per axis: "centre" was reachable and everything
   else was an approximation of the place you actually wanted. It is five plain controls now
   (Center X, Center Y, Reset, Snappify, and a snap tickbox), and they wear .gz-ops and .row like
   every other button and checkbox on a card — no private styling to keep in step. */

/* a line of quiet type under a card's controls, saying why there are fewer of them than usual */
#pop .gz-note { font-size: var(--fs-66); color: var(--ink-faint); margin-top: 6px; text-align: center; }

#pop .tfbtns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
#pop .tfbtns button { background: var(--bg-sunken); border: 1px solid var(--line); color: var(--ink);
  border-radius: var(--radius-md); padding: 10px 4px; font: inherit; font-size: var(--fs-78); cursor: pointer;
  white-space: nowrap; display: flex; align-items: center; justify-content: center; gap: 6px; }
#pop .tfbtns button:hover { border-color: var(--accent-dim); background: var(--bg-raised); }
#pop .tfbtns svg.g { display: block; flex: 0 0 auto; }
/* ■ the card's resize corner — identical to every other corner handle */
/* the card's scale grip IS a scale handle — same rule as the selection frame's corners below
   (#gz-sel .h), so they cannot drift to different sizes. Its transform is owned by JS (it is
   counter-scaled against the card's own scale, so it stays constant on screen), which is why
   its hover is a glow rather than the corners' scale-up. */
/* ...and this is the only thing the card's grip says for itself: where it stands. (The square
   itself is declared with the selection's corner — see THE SCALE SQUARE above.) */
/* A CORNER HANDLE STANDS ON ITS CORNER. Both of the card's handles used to sit INSIDE it, a few
   pixels in from the edge — which is where the card's own content is, so the ■ grip covered the
   last chip in the row and the 📌 covered the first. A handle that hides the thing it is standing
   on has taken the card's job away from it.
   Half in, half out: --gz-corner is the offset, and it is half the handle, so "centred on the
   corner" is a fact rather than a number somebody eyeballed. Both handles read it, so they cannot
   drift apart again. */
#pop, .pairpop, .ocard { --gz-corner: calc(var(--gz-handle) / -2); }   /* half a handle: derived, not eyeballed */
.gz-cardgrip { right: var(--gz-corner); bottom: var(--gz-corner); z-index: 3; }
#pop .gz-cardgrip, #pop .gz-pinbtn { transform-origin: center; }   /* the counter-scale must not walk them off their corners */
.gz-cardgrip:hover { box-shadow: var(--casing), 0 0 0 6px var(--accent); }

/* collapsible card sections */
.gz-csec { border-top: 1px solid var(--line-soft); padding-top: 7px; margin-top: 7px; }
.gz-csec:first-of-type { border-top: 0; padding-top: 0; margin-top: 0; }
.gz-csec.shut .gz-cbody { display: none; }
.gz-csec.autoshut > .gz-clab { color: var(--ink-faint); }
.gz-csec.autoshut > .gz-clab .gz-tw { opacity: .5; }
.gz-csec.shut .gz-clab { margin-bottom: 0; }
/* .gz-clab / .gz-glab: see "ONE SIZING LANGUAGE" above — they are the same heading. */
.gz-tw { display: inline-block; transition: transform var(--t-fast); font-size: var(--gz-twirl); line-height: 1; }
.gz-csec.shut .gz-tw, .gz-dgroup.shut .gz-tw { transform: rotate(-90deg); }

/* ---- chips ---------------------------------------------------------------
   THE CHIP IS THE APP'S. It used to be redefined here — same bare `.chip`, same
   specificity, later file — so this block silently won EVERY drawer chip in the
   product (44px grid box, blue hover) and the hybrid that came out was authored
   by nobody. The chip and every modifier it wears live in css/style.css now.
   Nothing about a chip is decided in this file. */
.gz-ptchips { position: absolute; display: none; gap: 5px; z-index: 75; }
.gz-ptchips.on { display: flex; }
/* the chips themselves are .chip (css/style.css) — the app's one chip, the one the card's rail
   wears. All this row says is where they stand and how big they are out here on the artwork, where
   they sit beside 44px handles rather than inside a card. */
.gz-ptchips .chip { width: var(--gz-chip); height: var(--gz-chip); min-width: 0; padding: 0;
  box-shadow: var(--shadow-chip); }
.gz-ptchips .chip svg { width: 62%; height: 62%; }

/* ---- floating boxes (.pairpop) and their shared rail ---------------------- */
.pairpop { position: fixed; z-index: 320; width: 252px; background: var(--bg-sunken);
  border: 1.5px solid var(--accent-dim); border-radius: var(--radius-md); padding: 8px 24px;
  box-shadow: 0 12px 34px var(--scrim-strong); color: var(--ink); font-size: var(--fs-78); }
.pairpop.pinned { border-color: var(--accent); }
/* THE TWO SIDE GRIPS — drag the card by the left one, pin it with the right. They were
   --ink-faint on a dark card: invisible until you already knew they were there, which is no
   use to the person who does not. They are CONTROLS, and a control says so without being
   hovered first: a lit rail in the accent, brightening under the pointer. */
/* ONE RAIL, on the left. The card had a grip down each edge — drag on the left, pin on the
   right — which cost it a margin on both sides and gave a person two things to learn instead
   of one. They are the same KIND of thing (what you do to the card, not what the card does),
   so they live together: the ⠿ rail runs down the left, and the 📌 sits at the foot of it. */
/* THE PIN, AND NOTHING ELSE. The card used to carry a ⠿ rail down its edge whose only job was
   dragging — and the card's whole BODY does that now (js/gizmo.js wireGrip), so the rail was a
   signpost pointing at a road that had moved. The pin is a real choice ("stop the app moving
   this one") and nothing else offers it, so it stays: a small button in the corner, not a wall
   down the side. */
.gz-pinbtn { position: absolute; left: var(--gz-corner); bottom: var(--gz-corner);
  width: 1.9rem; height: 1.9rem;                 /* the grip's size — they are two handles on one card */
  display: grid; place-items: center; color: var(--accent-dim); z-index: 3;
  background: var(--bg-raised);
  border: 1px solid var(--line); padding: 0; cursor: pointer;
  border-radius: var(--radius-md);
  /* NO CASING. The black/white double outline exists for handles drawn ON THE ARTWORK, where the
     surface behind them could be any colour (see the note at the top of this file) — the ■ grip
     is a white square lying over the spinner, so it needs one. The 📌 is chrome ON THE CARD: a
     known, dark, one-colour surface. A casing there is a solution wearing no problem, and it read
     as a permanent white halo. */
  transition: background var(--t-fast), color var(--t-fast); }
.gz-pinbtn:hover { color: var(--accent-ink); background: var(--on-bg); }
.gz-pinbtn svg { fill: none; stroke: currentColor; stroke-width: 1.6;
  transition: fill var(--t-fast), stroke var(--t-fast), transform var(--t-fast); }
.gz-pinbtn:hover svg { transform: rotate(-12deg); }
.gz-pinbtn.on svg { fill: var(--accent); stroke: var(--accent); filter: drop-shadow(0 0 5px var(--accent-dim)); }
.gz-dragging-card { cursor: grabbing; }

/* rows shared by the pairpop and the outcome card */
.pairpop .f, .ocard .f { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.pairpop .f:first-child, .ocard .f:first-child { margin-top: 0; }
.pairpop .f label, .ocard .f label { flex: 0 0 62px; font-size: var(--gz-body); color: var(--ink-dim); white-space: nowrap; }
.pairpop .f input, .ocard .f input { flex: 1; min-width: 0; }
.pairpop input, .ocard input { background: var(--bg-raised); border: 1px solid var(--line);
  color: var(--ink); border-radius: var(--radius-sm); padding: 5px 7px; font: inherit; font-size: var(--fs-78);
  outline: none; }
.pairpop input:focus, .ocard input:focus { border-color: var(--accent-dim); }
.pairpop label.chk { display: flex; align-items: center; gap: 6px; font-size: var(--gz-body);
  color: var(--ink); margin-top: 7px; }
.pairpop .pick3 { display: flex; gap: 5px; }
.pairpop .pk { --pk-bg: var(--bg-raised); --pk-pad: 5px 4px; --pk-gap: 2px; --pk-fs: var(--fs-66); }
.pairpop .pk svg { width: 26px; height: 13px; }
.pairpop .chiprow, .ocard .chiprow { display: flex; gap: 6px; margin-top: 8px; align-items: center; flex-wrap: wrap; }
.pairpop .ochips, .ocard .ochips { display: flex; gap: 5px; }
.pairpop .gz-ocol, .ocard .gz-ocol { width: 30px; height: 30px; flex: 0 0 auto; }
/* the card's sliders are the app's slider, in miniature — the geometry follows from
   the two vars (see "ONE range, parameterised" in style.css), so the knob stays on
   the bar. --track-bg: the fill is the vibe's own colour here, not the brass accent. */
.pairpop input[type=range], .ocard input[type=range] { flex: 1; min-width: 0; padding: 0;
  --track: 10px; --thumb: 16px; --accent: var(--handle-vibe); cursor: pointer; }
.pairpop .vg, .ocard .vg { flex: 0 0 auto; font-size: var(--fs-95); min-width: 2.4ch; text-align: center;
  line-height: 1; font-variant-numeric: tabular-nums; }
/* NO `margin-left: auto`. It shoved the weight stepper to the far right of its row, which is
   tidy while it shares that row with the swatches — and reads as a hole the moment it wraps onto
   a row of its own. A control does not change where it belongs because of what fits beside it:
   it lines up on the left, with everything else in the card. */
.pairpop .step, .ocard .step { display: flex; align-items: center; gap: 3px; background: var(--bg-raised);
  border: 1px solid var(--line); border-radius: var(--radius-md); padding: 2px 4px; font-size: var(--fs-78); }
.pairpop .step button, .ocard .step button { background: none; border: 0; color: var(--ink);
  cursor: pointer; font-size: var(--fs-90); padding: 0 3px; line-height: 1; }
.pairpop .step button:hover, .ocard .step button:hover { color: var(--accent-dim); }
.pairpop .step b, .ocard .step b { min-width: 14px; text-align: center; font-variant-numeric: tabular-nums; }
.pairpop .step i, .ocard .step i { color: var(--ink-faint); font-style: normal; font-size: var(--fs-66);
  min-width: 24px; text-align: right; }
.ocard { min-width: 0; }
.ocard .step { flex: 0 0 auto; }
/* the picker's title, wherever the picker is standing */
.pairpop .cptitle, .cpick .cptitle { font-size: var(--fs-90); font-weight: 700; color: var(--ink); margin-bottom: 7px; }

/* gradient stops: the tick sits ON the swatch */
.pairpop .pc { width: 100%; height: 26px; flex: 1; }
.pairpop .gstops { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 7px; }
.pairpop .gstop { position: relative; display: block; }
.pairpop .gstop .gz-ocol { width: 100%; aspect-ratio: 1; height: auto; position: relative; }
/* AN OFF STOP WEARS THE HATCH — the same hatch, not a hand-drawn imitation of it. This rule used
   to re-spell the gradient from scratch, and had already drifted: period 7 where the token says 8,
   alpha .55 where the token says --hatch-a. The two things it genuinely needs to say differently
   are now said as VALUES, not as a second gradient: black stripes (a swatch can be any colour, and
   these have to read on a pale one) and louder than the paint hatch, because this one means
   "switched off" rather than "you can grab this". */
.pairpop .gstop.off .gz-ocol {
  --hatch-rgb: 0,0,0;
  --hatch-a: .55;
  background-image: var(--hatch) !important; opacity: .45; }
.pairpop .gstop .gsx { position: absolute; right: 3px; bottom: 2px; font-size: var(--fs-60); color: #fff;
  text-shadow: 0 1px 2px #000; pointer-events: none; }
.pairpop .gstop .gsk { position: absolute; left: 3px; top: 3px; width: 14px; height: 14px; margin: 0;
  accent-color: var(--accent); cursor: pointer; z-index: 1; }

/* ---- the colour picker ----------------------------------------------------- */
#gz-scrim { position: fixed; inset: 0; background: #000; opacity: 0; pointer-events: none;
  z-index: 300; transition: opacity var(--t-fast); }
#gz-scrim.on { opacity: .5; pointer-events: auto; }
.cpick { position: fixed; z-index: 360; width: 196px; background: var(--bg-sunken);
  border: 1.5px solid var(--accent-dim); border-radius: var(--radius-lg); padding: 9px;
  box-shadow: var(--shadow-float); color: var(--ink); }
.cpick .cptitle { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }   /* it names a colour, and a name can be long */
/* the picker's own warning: this colour is being overruled by a switch elsewhere */
.cpick .cpwarn { font-size: var(--fs-80); line-height: 1.35; color: var(--ink-dim);
  background: var(--bg-sunken); border: 1px solid var(--line); border-left: 3px solid var(--warn, #d9a441);
  border-radius: var(--radius-md); padding: 6px 8px; margin-bottom: 8px; max-width: 240px; white-space: normal; }
.cpick .cpwarn b { color: var(--ink); }
.cpick .cpwarn.off { border-left-color: var(--line); }
.cpick .cpwarnrow { display: flex; align-items: flex-start; gap: 7px; cursor: pointer; }
.cpick .cpwarnrow input { margin: 2px 0 0; flex: 0 0 auto; cursor: pointer; accent-color: var(--accent-dim); }
.cpick .cpfield { position: relative; height: 96px; border-radius: var(--radius-md); border: 1px solid var(--line);
  margin-bottom: 8px; cursor: crosshair; }
.cpick .cpdot { position: absolute; width: 12px; height: 12px; border-radius: 50%;
  transform: translate(-50%, -50%); border: 2px solid #fff;
  box-shadow: 0 0 0 1px #000, 0 1px 3px var(--scrim-strong); pointer-events: none; }
.cpick .cprow { display: flex; gap: 6px; align-items: center; }
.cpick .cpsw { width: 30px; height: 28px; border-radius: var(--radius-md); border: 1px solid var(--line); flex: 0 0 auto; }
.cpick .cphex { flex: 1; min-width: 0; background: var(--bg-raised); border: 1px solid var(--line);
  color: var(--ink); border-radius: var(--radius-sm); padding: 5px 7px; font: inherit; font-size: var(--fs-78);
  outline: none; font-variant-numeric: tabular-nums; text-transform: lowercase; }
.cpick .cphex:focus { border-color: var(--accent-dim); }
.cpick .cpax { display: flex; align-items: center; gap: 7px; margin-top: 7px; }
/* one letter, in a fixed column, beside a bar: centre it — left-aligned, the three
   letters drifted against their own gutter and read as unaligned with the sliders. */
.cpick .cpax label { flex: 0 0 22px; font-size: var(--fs-82); color: var(--ink-dim);
  text-align: center; line-height: 1; }
/* THE COLOUR AXES. Each bar IS its own scale — the hue rainbow, the saturation and
   lightness ramps (js/gizmo.js sets those two, they depend on the current colour) —
   so it paints the TRACK (--track-bg), not the input's background: the track would
   simply cover that. No progress fill on these: the bar already says everything, and
   a brass wash across it would be a lie. */
.cpick .cpr { flex: 1; min-width: 0; padding: 0; --track: 14px; --thumb: 12px;
  cursor: pointer; }
.cpick .cpr::-webkit-slider-runnable-track,
.cpick .cpr::-moz-range-track { border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: none; }
.cpick .cpr::-webkit-slider-thumb { background: #fff; border: 2px solid #000;
  box-shadow: 0 0 0 1px #fff; cursor: grab; }
.cpick .cpr::-moz-range-thumb { background: #fff; border: 2px solid #000;
  box-shadow: 0 0 0 1px #fff; cursor: grab; }
.cpick .cpr.cph { --track-bg: linear-gradient(90deg,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00); }
/* all three numbers are the same box — they are the same KIND of thing (0..360, 0..100) */
.cpick .cpn { flex: 0 0 3.6ch; text-align: right; font-size: var(--fs-66); font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; cursor: ew-resize; padding: 2px 3px; border-radius: var(--radius-sm);
  background: var(--bg-raised); border: 1px solid var(--line); user-select: none; }
.cpick .cpn:hover { border-color: var(--accent-dim); }
.cpick .cppal { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 9px; }
.cpick .cpp { width: 20px; height: 20px; border-radius: var(--radius-sm); border: 1px solid var(--line);
  cursor: pointer; padding: 0; }
.cpick .cpp:hover { border-color: #fff; transform: scale(1.1); }
.cpick .cpwiz { width: 100%; margin-top: 9px; background: var(--bg-raised);
  border: 1px solid var(--handle-curve); color: var(--handle-curve); border-radius: var(--radius-md); padding: 6px;
  font: inherit; font-size: var(--fs-72); font-weight: 600; cursor: pointer; }
.cpick .cpwiz:hover { color: var(--ink); border-color: var(--accent-dim); }
.gz-ocol { border: 1px solid var(--line); border-radius: var(--radius-md); cursor: pointer; padding: 0; }
.gz-ocol:hover { border-color: var(--accent-dim); }

/* ---- readout + gizmo tooltip ----------------------------------------------- */
#gz-readout { position: fixed; display: none; background: var(--accent-ink); color: var(--ink);
  border: 1px solid var(--accent); font-size: var(--fs-72); padding: 3px 8px; border-radius: var(--radius-sm);
  z-index: 390; pointer-events: none; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* THE EDIT-MODE TIP WEARS THE DATATIP'S CLOTHES (2026-07-14 §10). Two tooltip systems remain —
   this one holds HTML and live functions; .app-tip (css/style.css) is text-only by design — but
   they are indistinguishable at a glance now: same tokens, same type, same slide-up, same arrow.
   Only the z-index differs, because the editor's tips must ride above its own chrome. The
   MANNERS moved with the look (js/gizmo.js tipOn): instant, keyboard-aware, and answering to
   the same showTooltips switch every data-tip honors. */
#gz-tip { position: fixed; display: none; z-index: 400; pointer-events: none;
  background: var(--bg-raised); color: var(--ink);
  border: 1px solid var(--accent-dim); border-radius: var(--radius-md);
  padding: .5em .8em; font: 500 var(--fs-85)/1.45 var(--font-ui);
  letter-spacing: normal; text-transform: none; text-align: center; white-space: normal;
  max-width: min(92vw, 20em); box-shadow: 0 10px 28px var(--scrim-strong);
  opacity: 0; transform: translateY(4px); transition: opacity .12s ease, transform .12s ease; }
#gz-tip.on { opacity: 1; transform: translateY(0); }
#gz-tip::after { content: ''; position: absolute; left: 50%; transform: translateX(-50%); border: 6px solid transparent; }
#gz-tip:not(.below)::after { top: 100%; border-top-color: var(--accent-dim); }
#gz-tip.below::after { bottom: 100%; border-bottom-color: var(--accent-dim); }
#gz-tip b { color: var(--ink); font-weight: 600; }

/* ---- the sidebox (drawer contents in gizmo edit mode) ---------------------- */
.gz-dgroup { margin: 9px -7px 0; padding: 5px 7px; border-radius: var(--radius-lg); }
/* AUTO LAYOUT is an action, not a twirl group — a single full-width button carrying the wand. */
.gz-dgroup.gz-dact { padding-top: 3px; }
.gz-autolayout-btn { display: flex; align-items: center; justify-content: center; gap: .5em; width: 100%; }
.gz-autolayout-btn svg { flex: 0 0 auto; width: 15px; height: 15px; }
.gz-dgroup.flash { animation: gz-essflash .95s ease-out; border-radius: var(--radius-lg); }   /* (.dsec-flash was dead — nothing in js/ ever added it) */
@keyframes gz-essflash {
  0%   { background: color-mix(in srgb, var(--accent) 18%, transparent); box-shadow: inset 0 0 0 1.5px var(--accent); }
  70%  { background: color-mix(in srgb, var(--accent) 18%, transparent); box-shadow: inset 0 0 0 1.5px var(--accent); }
  100% { background: transparent; box-shadow: inset 0 0 0 1.5px transparent; }
}
.gz-dgroup.flash .gz-glab { color: var(--ink); }
.gz-dgroup.shut.flash .gz-glab { color: var(--accent); }
/* ===== ONE SIZING LANGUAGE ==================================================
   The card (#pop) and the sidebox (.gz-sidebox) are the SAME controls in two rooms — a
   section heading is a section heading, a tile is a tile. They had drifted into two
   scales (the card's chrome had snapped to the smallest tokens), so the same control read
   as two different things depending on where you met it. One set of variables, both rooms.
   The card is the smaller room, so it takes the smaller end of the same scale, not a
   different scale. */
#pop, .gz-sidebox, .pairpop, .ocard {
  --gz-head: var(--fs-90);     /* a section heading (▾ PRESET SHAPE) */
  --gz-twirl: var(--fs-105);   /* its twirl */
  --gz-body: var(--fs-90);     /* labels, rows, checkbox text */
  --gz-tile: var(--fs-90);     /* the caption under a tile (Wide / Tall / Roll) */
  --gz-tile-pad: 7px 3px;
}
.gz-glab, .gz-clab { font-size: var(--gz-head); color: var(--ink-dim); text-transform: uppercase;
  letter-spacing: .07em; font-weight: 700; margin-bottom: 7px; }
.gz-glab.twirl, .gz-clab.twirl { cursor: pointer; display: flex; align-items: center; gap: 7px;
  user-select: none; padding: 3px 0; }
.gz-glab.twirl:hover, .gz-clab.twirl:hover { color: var(--ink); }
.gz-glab .gz-tw, .gz-clab .gz-tw { transition: transform var(--t-fast); display: inline-block;
  font-size: var(--gz-twirl); line-height: 1; }
/* CLOSED IS CLOSED — outranks any per-id layout rule below */
.gz-dgroup.shut > *:not(.gz-glab) { display: none !important; }
.gz-sidebox .pick3 { display: flex; flex-wrap: wrap; gap: 5px; }
/* Preset shape has more chips than fit one row. The prototype used a 5-up grid
   for exactly this (mockup: `.drawer #dShape{display:grid;...repeat(5,1fr)}`). */
.gz-sidebox #gz-dShape { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; }
.gz-sidebox #gz-dShape .pk { padding: 3px 1px; }
.gz-sidebox #gz-dShape .pk svg { width: 100%; height: 26px; color: currentColor; }
.gz-sidebox .pick3 > .pick3 { flex: 1; min-width: 0; }
/* (the sidebox's tile IS the default — it sets no knobs at all) */
.gz-sidebox .pk svg { width: 32px; height: 16px; fill: none; stroke: currentColor; stroke-width: 3; }
/* THE KEY ART IS SOLID. KEY_ART draws Wide and Tall as a bare <rect rx> with no
   fill/stroke attributes of its own, so without this it falls through to the generic
   `.pk svg` rule above — fill: none, stroke: currentColor, 3px — and the tiles render as
   3px outlines instead of the solid pills the tooltip promises. (On-wedge is unaffected:
   ONWEDGE_ART carries its own fill="none" presentation attribute, which beats an inherited
   CSS fill.) The rule was dead — written against a #dKey that js/gizmo.js never creates. */
.gz-sidebox #gz-dKey .pk svg { fill: currentColor; stroke: none; }
/* the on-wedge key line (kwedge) — one description, wherever it renders */
/* THE ROW IS THREE THINGS IN A LINE — art, name, eye — and each gets the room it needs and the
   same room as its neighbours: one padding, one gap, everything centred on the same axis. (It
   used to squeeze a 26×13 sliver of art and a bespoke 24×22 eye against a label that then wrapped
   onto two lines, and it read exactly as cramped as it was.) The eye is the app's eye — .chip.vis
   — so its breathing room is not decided here at all. */
.gz-kwedge { display: flex; flex: 1 0 100%; }
.gz-kwedge .pk.wk {
  --pk-radius: var(--radius-lg); --pk-pad: 8px 8px 8px 10px; --pk-gap: 10px; --pk-fs: var(--gz-body);
  flex-direction: row !important; align-items: center; font-weight: 600; min-height: 0; }
.gz-kwedge .pk.wk > svg { width: 36px; height: 18px; flex: 0 0 auto; }
/* the name sits BETWEEN its two fixed neighbours (the art, the eye) — so it centres in the room
   they leave it, not against the left edge of one of them */
.gz-kwedge .pk.wk > span { flex: 1; min-width: 0; text-align: center; line-height: 1.25; }
/* the eye is a .chip.vis and dresses itself; the row only says where it stands */
.gz-kwedge .pk.wk .chip.vis { margin-left: auto; flex: 0 0 auto; pointer-events: none; box-shadow: none; }
/* outcome rows */
.gz-orow { display: flex; gap: 6px; align-items: center; margin-bottom: 7px; background: var(--bg-sunken);
  border: 1px solid var(--line-soft); border-radius: var(--radius-lg); padding: 7px 8px 7px 4px; }
.gz-orow .ocard { flex: 1; min-width: 0; }
.gz-sidebox .grip { color: var(--ink-faint); cursor: grab; font-size: var(--fs-82); flex: 0 0 auto; }
.gz-sidebox .grip:hover { color: var(--ink); }
.gz-sidebox .grip:active { cursor: grabbing; }
.gz-sidebox .gz-ops { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 9px; }
/* an ops button is an ops button — it only stands on a different floor in each room */
/* AN OPS BUTTON IS A BUTTON, not a footnote. These were set at --fs-66 — the size the gizmo
   writes a readout or a handle's caption at — with 3px of padding, so Copy and Paste came out as
   two little chips you had to aim at. They are the things you came to the section to PRESS. Card
   body size, room around the word, and the glyph is an icon beside it rather than a ⧉ typed into
   the label. */
#pop .gz-ops button, .gz-sidebox .gz-ops button {
  background: var(--ops-bg, var(--bg-sunken)); border: 1px solid var(--line); color: var(--ink);
  border-radius: var(--radius-md); padding: .5em .85em; font: inherit; font-size: var(--gz-body);
  display: inline-flex; align-items: center; gap: .45em; line-height: 1; cursor: pointer;
  transition: var(--t-fast); }
#pop .gz-ops button svg, .gz-sidebox .gz-ops button svg { display: block; flex: 0 0 auto; opacity: .8; }
#pop .gz-ops button:hover svg, .gz-sidebox .gz-ops button:hover svg { opacity: 1; }
/* AN ICON-ONLY CARD BUTTON — square, so a row of them reads as a TOOLBAR rather than as a ragged
   stack of settings (which is what labelled buttons wrapped to in a card this narrow). The word has
   not been dropped, it has moved: each one carries its sentence on hover, and an aria-label for
   anyone not looking at it.
   IT IS THE SIZE OF A CHIP, because it is standing next to chips. Sized in `em` of the card's BODY
   text (2.2em) it came out visibly smaller than the rail chips above it and than the checkbox
   below — three square controls in one card at three different sizes, which reads as a mistake
   because it is one. It borrows the chip's metrics wholesale (.chip in css/style.css: the same
   font-size, the same padding, the same 1.6rem glyph) rather than restating a number that would
   then have to be kept in step with them by hand. */
#pop .gz-ops button.ico {
  padding: .5em; gap: 0; justify-content: center;
  min-width: 2.6em; font-size: var(--fs-110); line-height: 1; }
#pop .gz-ops button.ico svg { width: 1.6rem; height: 1.6rem; opacity: .9; }
#pop .gz-ops button:hover, .gz-sidebox .gz-ops button:hover { border-color: var(--accent); color: var(--accent); }
.gz-sidebox .gz-ops button { --ops-bg: var(--bg-raised); }
.gz-sidebox .gz-ops button.wiz { color: var(--accent); border-color: var(--accent-dim); }
.gz-ops { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }

/* ---- toolbar additions (#head-actions) ------------------------------------- */
.gz-tb { background: var(--bg-raised); border: 1px solid var(--line); color: var(--ink);
  border-radius: var(--radius-md); padding: 5px 9px; font: inherit; font-size: var(--fs-78); cursor: pointer; }
.gz-tb:hover { border-color: var(--accent-dim); }
.gz-tb.on { background: var(--on-bg); border-color: var(--on-line); color: var(--on-ink); font-weight: 600; }
.gz-tb:disabled { opacity: .4; cursor: default; }
.gz-tb:disabled:hover { border-color: var(--line); }
.gz-edit-only { display: none; gap: 6px; align-items: center; }
body.editing .gz-edit-only { display: flex; }
.gz-sep { width: 1px; height: 20px; background: var(--line); margin: 0 2px; }
#gz-tbGhost { display: inline-flex; align-items: center; gap: 5px; }
#gz-tbGhost svg { fill: currentColor; display: block; }

/* mini curve editors on the marker card */
.gz-cpt { cursor: grab; transition: r var(--t-fast), stroke var(--t-fast); }
.gz-cpt:hover { r: 7.5; stroke: var(--accent); }
.gz-cpt:active { cursor: grabbing; }
