/**
 * Draw Mask window — the full-viewport modal reached from the pencil beside
 * "Add a Base Img".
 *
 * Every number here is transcribed from a real capture, not chosen:
 *   tools/ui-extract/out/components/draw-window-painted.json  (computed styles)
 *   tools/ui-extract/out/components/draw-summary.json         (tools + inputs)
 *
 * Measured at a 1600x1000 viewport, which is why the capture's absolute
 * coordinates line up with the layout rules below:
 *   modal inset      10px on all four sides (1580x980 content box)
 *   top bar          87px tall, left panel 236px, right group 210x46
 *   bottom toolbar   830x50 centred, 44x44 buttons, 3px padding
 *   layers button    44x44 pinned bottom-right of the stage
 *
 * The icon buttons use the same mask technique as the rest of the clone — see
 * the .icon block in panel.css, and naibot-icon-sizing for why these are 16x16
 * measured rather than eyeballed.
 */

/* The modal owns the viewport. NAI paints the page background over everything
   rather than dimming it — this is a full window, not an overlay on the rail. */
.draw-window {
  position: fixed;
  inset: 0;
  z-index: 100;
  background-color: var(--nai-bg-input);
  color: var(--nai-text);
  font-family: var(--nai-font);
}
.draw-window[hidden] {
  display: none;
}

/* The chrome sits 10px inside the viewport as one column: top bar, stage,
   bottom toolbar. The stage takes whatever the two bars leave. */
.draw-window__chrome {
  position: absolute;
  inset: 10px;
  z-index: 1;
  display: flex;
  flex-direction: column;
  /* The stage underneath must stay clickable, so the chrome column itself is
     transparent to the pointer and only its controls take events back. */
  pointer-events: none;
}
.draw-window__chrome > * {
  pointer-events: auto;
}

/* ---- top bar ------------------------------------------------------------ */

/* 1580x87, its two groups pushed to the edges and bottom-aligned. */
.draw-window__top {
  display: flex;
  flex: 0 0 auto;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--nai-space-2);
}

/* Left: the title cell, a 1px rule, then the pen controls. 236x87 measured. */
.draw-tools {
  display: flex;
  align-items: center;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
}

/*
 * The glyph stacked over the label, both centred. 62px tall in both modes.
 *
 * The WIDTH is label-driven but floored, and both halves of that are measured:
 *   mask  "Draw Mask"  63px label + 10px padding = 73px cell
 *   draw  "Draw"       30px label + 10px padding = 40px … but NAI renders 62px
 * So the short label does not shrink the cell to fit — there is a floor at
 * 52px of content box. Letting it size purely to content renders the draw-mode
 * cell 22px narrow and drags the whole strip left, which is exactly what the
 * first pass did (panel 515px against the measured 537px).
 */
.draw-tools__title {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 62px;
  /* The floor is on the BORDER box: NAI's draw-mode cell is 62px INCLUDING its
     2x5px padding, so a content-box minimum of 52 renders 62+10=72 and leaves
     the panel 10px wide. box-sizing makes the 62 mean what it measures. */
  box-sizing: border-box;
  min-width: 62px;
  padding: 0 var(--nai-space-1);
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
}
.draw-tools__title .icon {
  margin-bottom: 4px;
}

/* Same filled-div separator the button groups use, not a border — see
   .button-group__rule in panel.css. 1x85 here. */
.draw-tools__rule {
  align-self: stretch;
  width: 1px;
  margin: 1px 0;
  background-color: var(--nai-bg-raised);
}

/*
 * The controls beside the title. Measured 160x85 in mask mode and 472x60 in
 * draw mode, both with 8px/10px padding and a 10px gap.
 *
 * NEITHER width is set here, on purpose. The row holds a different set of
 * controls per mode, so its width is the RESULT of what is showing — pinning
 * 160px (as this did) clips draw mode's swatch and brush-shape group, and
 * pinning 472px pads mask mode out with dead space. `width: auto` reproduces
 * both measurements from one rule.
 */
.draw-tools__fields {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--nai-space-2);
  padding: 8px var(--nai-space-2);
}
/* Same reasoning for the stack: 140px is the pen-size group's own width and it
   is the widest child in both modes, so it comes out of the content. */
.draw-tools__stack {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  gap: 4px;
  width: 140px;
}

/* The 140x44 group: the label row and its range, with NO gap between them.
   Only this group and the checkbox label are spaced by the stack's 4px. */
.pen-size-group {
  width: 140px;
}

/* "Pen Size:" and the number share one 140x29 row; the range sits under it.
   The 29px is measured, not the natural height of a 21px line — the row is
   taller than its text in the capture, which is what spaces the stack out to
   its 69px. Leaving it to flow renders 24px and shortens the whole panel. */
.pen-size {
  display: flex;
  align-items: center;
  height: 29px;
  gap: var(--nai-space-1);
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
}
/* The number input is 16px wide in the capture — it hugs its digits rather than
   reserving a column, the same trick the AI-settings readouts use. Sized from
   --digits so "50" doesn't clip where "4" fits. */
.pen-size__number {
  width: calc(var(--digits, 1) * 1ch + 2px);
  padding: 0;
  color: inherit;
  background: none;
  border: none;
  font-size: var(--nai-text-sm);
  font-weight: 700;
  text-align: left;
}
/* Spinner arrows suppressed, as everywhere else in this panel. */
.pen-size__number::-webkit-outer-spin-button,
.pen-size__number::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.pen-size__number {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* 140x12 track on the darkest fill. */
/* The range sits DIRECTLY under the label row with no gap between them — see
   .pen-size-group. The capture's 3.2px margin-bottom is the UA default and it
   is load-bearing: the wrapper measures 44px = 29 (row) + 12 (track) + 3, and
   that 3px is what sets the checkbox row's y67. Zeroing it shortens the group
   and lifts the checkbox; the stack's 4px gap sits below this group, not
   inside it. */
/* The thumb is 12x18 and OVERFLOWS its 12px track — pixel-scanned off
   draw-window-painted.png at the default value of 4:
     x95..106  rgb(34,37,63)  thumb, spanning y45..62 (18 tall)
     x107..234 rgb(14,15,33)  track, spanning y48..59 (12 tall)
   So the thumb is --nai-bg-raised on a --nai-bg-input track: the raised fill
   reads *lighter* than the groove it rides in. There is no accent on this
   control and no filled/progress portion before the thumb. */
.pen-size__range {
  display: block;
  width: 140px;
  height: 12px;
  margin: 0 0 3px;
  background-color: var(--nai-bg-input);
  border-radius: var(--nai-radius);
  -webkit-appearance: none;
  appearance: none;
  overflow: visible;
  cursor: default;
}
.pen-size__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 18px;
  /* Centres the 18px thumb on the 12px track without growing the input. */
  margin-top: -3px;
  background-color: var(--nai-bg-raised);
  border: none;
  border-radius: 2px;
  cursor: pointer;
}
.pen-size__range::-moz-range-thumb {
  width: 12px;
  height: 18px;
  background-color: var(--nai-bg-raised);
  border: none;
  border-radius: 2px;
  cursor: pointer;
}

/* 140x21 row: a 16x16 painted box, then the label 10px to its right. The real
   checkbox is collapsed to 1x13 in the capture — NAI hides it under the painted
   square, the same pattern positions.js hit with the AI's Choice box. */
.square-brush {
  display: flex;
  align-items: center;
  width: 140px;
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  cursor: pointer;
}
.square-brush input {
  position: absolute;
  width: 1px;
  height: 13px;
  opacity: 0;
  pointer-events: none;
}
.square-brush__box {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  background-color: var(--nai-bg-input);
  border: var(--nai-border);
  border-radius: 2px;
}
.square-brush__box .icon {
  width: 12px;
  height: 12px;
  opacity: 0;
}
/* Checked INVERTS the box: accent fill, page-dark tick, no border — a solid
   yellow square with a dark check, not an outlined box with a yellow one.
   No capture has this state (every checkbox in out/components is unchecked, and
   the one checked box in draw-summary.json sits at y1518, below every 1000px
   screenshot), so this follows .position-global__pill in panel.css, which is
   the measured precedent for the same accent-fill/bg-page-foreground pattern. */
.square-brush input:checked + .square-brush__box {
  background-color: var(--nai-accent);
  border-color: transparent;
}
.square-brush input:checked + .square-brush__box .icon {
  opacity: 1;
  color: var(--nai-bg-page);
}
.square-brush__label {
  flex: 1 1 auto;
  margin-left: var(--nai-space-2);
}

/* ---- draw mode: colour + brush shape ------------------------------------ */

/*
 * Everything in this block belongs to DRAW mode only and is measured from
 * out/components/canvas-draw-window.json (npm run ui:canvas-draw).
 *
 * The two modes are one window with two control sets, and the left panel's
 * width is the RESULT of which set is showing — 537px in draw mode against
 * 236px in mask mode. Neither number is written down anywhere below: the panel
 * is sized by its contents, so hiding the wrong-mode controls produces the
 * measured width on its own. Hardcoding either would break the moment a
 * control changed.
 */

/* Mask-only and ink-only controls, switched by a class on the window. */
.draw-window--mask .draw-tool--ink,
.draw-window--mask .pen-color-group,
.draw-window--mask .brush-shape-row {
  display: none;
}
.draw-window--ink .draw-tool--mask,
.draw-window--ink .square-brush {
  display: none;
}

/*
 * The pen colour swatch: three nested boxes, not one button.
 *
 *   106x42  wrapper — its BORDER is the white ring around the swatch
 *   104x40  button  — its own background IS the current pen colour
 *    34x34  well    — the dropper's darker inset at the button's right end
 *
 * The ring is a border on the wrapper, so it cannot be folded into the button
 * as an outline: an outline would sit outside the 104x40 and the group would
 * measure 108 rather than the captured 106.
 */
.pen-color-group {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
}
.pen-color__ring {
  width: 106px;
  height: 42px;
  box-sizing: border-box;
  /* White, not --nai-border: measured rgb(255,255,255) on the wrapper while
     every other border in this panel is rgb(34,37,63). It reads as a swatch
     frame precisely because it breaks the panel's border colour. */
  border: 1px solid #fff;
  border-radius: var(--nai-radius);
}
/* justify-content:flex-end is what pins the well to the right end; the colour
   fills whatever space is left, which is the swatch. */
.pen-color {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  box-sizing: border-box;
  width: 104px;
  height: 40px;
  padding: 5px;
  /* Seeded black, then repainted by script as the colour changes. */
  background-color: var(--pen-color, #000);
  border: 0;
  cursor: pointer;
}
.pen-color__well {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  background-color: var(--nai-bg-page);
  border-radius: var(--nai-radius);
}

/* 186x44 at x=350: the 136x44 shape group (three 44x44, gap 2), a 6px gap,
   then the detached fourth button. */
.brush-shape-row {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 6px;
  height: 44px;
}
.brush-shape {
  display: flex;
  gap: 2px;
}
/* Same 44x44 cell as a toolbar tool, but these carry a visible resting border
   and sit on the page fill — the toolbar's are transparent until selected. */
.brush-shape__button {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--nai-text);
  background-color: var(--nai-bg-page);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  cursor: pointer;
}
.brush-shape__button:hover {
  background-color: var(--nai-bg-raised);
}
/* Selected takes the raised fill — measured rgb(34,37,63) on the round brush
   while the other two sat at rgb(19,21,44). Same treatment as a pressed tool,
   and deliberately NOT the accent: the glyph stays white here. */
.brush-shape__button[aria-checked='true'],
.brush-shape__button[aria-pressed='true'] {
  background-color: var(--nai-bg-raised);
}

/*
 * The three shape glyphs are painted divs, not mask icons — the capture records
 * border-radius:50% and a background colour on a 12x12 box, with no mask-image
 * anywhere. Drawing them in CSS is what the source actually does.
 */
.brush-shape__dot {
  width: 12px;
  height: 12px;
  background-color: currentColor;
  border-radius: 50%;
}
/* The soft brush is the SAME disc with a feathered edge — a filled dot that
   fades outwards, measured at rgb(227,227,230) in its centre against the round
   brush's flat rgb(255,255,255).

   The fade must run from currentColor to TRANSPARENT. An earlier version faded
   through an opaque white midpoint, which paints a light ring over the dark
   button and reads as a hollow circle — the opposite of a soft brush. */
.brush-shape__dot--soft {
  background-color: transparent;
  background-image: radial-gradient(
    circle closest-side,
    currentColor 0%,
    currentColor 25%,
    rgba(255, 255, 255, 0) 100%
  );
}
.brush-shape__dot--square {
  border-radius: 0;
}

/* Right: 210x46 holding export | Save & Close | close. */
.draw-actions {
  display: flex;
  align-items: stretch;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
}

/* 44x44, on the page tone rather than the panel tone — these read as inset
   against the group behind them. */
.draw-actions__icon {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--nai-text);
  background-color: var(--nai-bg-page);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  font-weight: 600;
  cursor: pointer;
}
.draw-actions__icon:hover {
  background-color: var(--nai-bg-raised);
}

/* 120x44 in the accent, with dark ink — the same treatment as Generate. */
.draw-actions__save {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  gap: var(--nai-space-1);
  width: 120px;
  height: 44px;
  padding: var(--nai-space-1) var(--nai-space-2);
  color: var(--nai-bg-panel);
  background-color: var(--nai-accent);
  border: none;
  font-weight: 600;
  cursor: pointer;
}

/* ---- stage -------------------------------------------------------------- */

/* The canvas row: the stage, then a 44px rail holding the layers button. */
.draw-window__middle {
  display: flex;
  flex: 1 1 0;
  gap: var(--nai-space-2);
  padding-top: var(--nai-space-2);
  min-height: 0;
}

/* The canvases are centred in whatever space is left. They are positioned by
   script (see sizeStage in draw.js), because their pixel size is derived from
   the base image's aspect ratio, not from CSS. */
.draw-stage {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
}

/*
 * The layer stack.
 *
 * NAI stacks six canvases; this clone keeps the two that carry meaning plus a
 * cursor overlay (see draw.js for why the other three are not reproduced):
 *   .draw-layer--composite  base image with the mask tinted over it — what the
 *                           user sees, and the only layer that paints visibly
 *   .draw-layer--mask       the mask alone, offscreen, never displayed
 *   .draw-layer--cursor     the brush outline that follows the pointer
 *
 * All are absolutely positioned and exactly superimposed, so a click lands on
 * the top one — which is why only the cursor layer takes pointer events and it
 * forwards nothing: the gesture is bound on the stage itself.
 */
.draw-layer {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Crisp edges: the mask is a hard-edged stencil and browser smoothing on a
     scaled canvas would blur the very boundary the export thresholds on.

     This is right for a MASK and wrong for a DRAWING, so the ink mode turns it
     off — see .draw-stage--ink below. Zoom made the difference visible: the two
     were indistinguishable while the view was capped at 1:1. */
  image-rendering: pixelated;
}

/* Ink is a photograph-like layer of antialiased strokes, not a stencil, so it
   is resampled smoothly when zoomed past 1:1. Leaving it pixelated would show
   the drawing's own edge pixels as hard squares — the blocky look the
   full-resolution ink layer exists to avoid. Set on the stage so both layers
   follow one class. */
.draw-stage--ink .draw-layer {
  image-rendering: auto;
}
.draw-layer--cursor {
  pointer-events: none;
}

/* The stage is the hit target for the whole gesture, so the brush cursor is set
   here rather than on any one layer. */
.draw-stage--drawing {
  cursor: crosshair;
}

/* 44px rail, its single button pinned to the bottom. */
.draw-window__rail {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: flex-end;
  justify-content: flex-end;
  width: 44px;
}
.draw-layers {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 44px;
  height: 44px;
  color: var(--nai-text);
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  font-weight: 600;
  cursor: pointer;
}
.draw-layers .icon {
  width: 18px;
  height: 18px;
}
/* The count beside the glyph, at 12px and dimmed. */
.draw-layers__count {
  font-size: 12px;
  line-height: 18px;
  opacity: 0.6;
}

/* The renderer badge NAI prints in the corner. Ours is a 2d context, and saying
   so is more useful than copying their "WebGL2" verbatim. */
.draw-window__renderer {
  position: absolute;
  right: 4px;
  bottom: 4px;
  font-family: monospace;
  font-size: 9.6px;
  letter-spacing: 0.5px;
  line-height: 14.4px;
  opacity: 0.3;
  pointer-events: none;
}

/* ---- bottom toolbar ----------------------------------------------------- */

/* A 50px row; the 830px bar is centred in it. */
.draw-window__bottom {
  display: flex;
  flex: 0 0 auto;
  justify-content: center;
  height: 50px;
}
.draw-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  width: 830px;
  max-width: 100%;
  padding: 3px;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
}

/* The gap between the two clusters is not a fixed margin: in the capture the
   left group ends at x=565 and the right begins at x=984, which is what a
   flexible spacer between them produces at this width. */
.draw-toolbar__spacer {
  flex: 1 1 auto;
}

/* Same filled-div rule as elsewhere, 1x30 with 3px either side. */
.draw-toolbar__rule {
  width: 1px;
  height: 30px;
  margin: 0 3px;
  background-color: var(--nai-bg-raised);
}

/* 44x44 icon buttons, transparent until selected. */
.draw-tool {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--nai-text);
  background: none;
  border: none;
  border-radius: var(--nai-radius);
  font-weight: 600;
  cursor: pointer;
}
.draw-tool:hover:not(:disabled) {
  background-color: var(--nai-bg-page);
}
/* The selected tool takes the raised fill and tints its glyph with the accent —
   measured as rgb(245,243,194) on the pen while every other glyph stayed white. */
.draw-tool[aria-pressed='true'] {
  background-color: var(--nai-bg-raised);
}
.draw-tool[aria-pressed='true'] .icon {
  color: var(--nai-accent);
}
.draw-tool:disabled {
  opacity: 0.35;
  cursor: default;
}

/* ---- colour picker ------------------------------------------------------ */

/*
 * Opened from the swatch. Every number here is PIXEL-SCANNED off
 * canvas-draw-colorpicker.png, not read from the node walk — the walk stopped
 * at the smallest newly-mounted box and captured only the SV square and its
 * handle, so the two strips and the hex readout are absent from the JSON
 * entirely. Scanning for hard edges (delta > 40 between adjacent rows, to skip
 * the gradients' own steps) gave:
 *
 *   panel       x57  y74    200 wide
 *   SV square   y74..225    152 tall
 *   hue strip   y226..249    24 tall
 *   alpha strip y250..273    24 tall
 *   hex label   y290..299
 *
 * The three bands are FLUSH — the scan found no background between them — so
 * they stack with no gap. The hex sits 16px below the last strip.
 */
.color-picker {
  position: absolute;
  /* Under the swatch: the swatch's ring starts at x=84 and the picker at x=57,
     i.e. 27px further left, and 10px below the 66px top bar. Anchored to the
     chrome rather than the swatch so it can overhang the panel, which it does. */
  top: 64px;
  left: 47px;
  z-index: 10;
  width: 200px;
  /* Rounded at the TOP only — measured 5px 5px 0 0. The strips square off the
     bottom because the alpha band runs to the panel's edge. */
  border-radius: var(--nai-radius-lg) var(--nai-radius-lg) 0 0;
  /* The bands paint their own colour; anything behind them would never show. */
  background-color: transparent;
  /* The handles overhang their bands (the SV thumb is 28px on a 152px field and
     rides to the very edge), so they must not be clipped. */
  overflow: visible;
}
.color-picker[hidden] {
  display: none;
}

/*
 * Saturation across, value down, over the current hue.
 *
 * Two stacked gradients on one box, which is what the capture's corners
 * describe: TL white, TR the pure hue, and the whole bottom row black. The hue
 * is a variable so script repaints only that, leaving the geometry alone.
 */
.color-picker__field {
  position: relative;
  height: 152px;
  background-color: var(--picker-hue, #f00);
  background-image:
    linear-gradient(to top, #000, rgba(0, 0, 0, 0)),
    linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
  cursor: crosshair;
}

/* 28x28 white ring with a 24x24 core showing the current colour, and a soft
   drop shadow — all measured on the node walk, which did capture this one. */
.color-picker__thumb {
  position: absolute;
  /* Centred on its coordinate rather than hung from the corner. */
  transform: translate(-50%, -50%);
  width: 28px;
  height: 28px;
  box-sizing: border-box;
  background-color: var(--pen-color, #000);
  border: 2px solid #fff;
  border-radius: 50%;
  box-shadow: rgba(0, 0, 0, 0.2) 0 2px 4px 0;
  pointer-events: none;
}

/* Full spectrum, left to right. The scan read pure red at both ends
   (255,0,0 at x=58 and 255,0,3 at x=256), i.e. the ramp wraps a full 360. */
.color-picker__hue {
  position: relative;
  height: 24px;
  background-image: linear-gradient(
    to right,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%
  );
  cursor: ew-resize;
}

/*
 * BRIGHTNESS, not alpha — and this is worth stating plainly because the shape
 * of the control invites the opposite reading.
 *
 * Scanning the reference across two rows 12px apart gives an IDENTICAL ramp on
 * both, white(239) at the left to black(0) at the right. A transparency strip
 * would show a checkerboard alternating between those rows; there is none
 * anywhere along it. The handle also sits near the BLACK end while the pen is
 * #000000, which fits a value scale and contradicts an opacity one (the colour
 * is fully opaque, so an alpha handle would be at the far side).
 *
 * So: one flat gradient, no checker, no second layer.
 */
.color-picker__alpha {
  position: relative;
  height: 24px;
  /* PURE GREYSCALE — no hue in it. Sampled across the reference strip every
     channel is equal at every step (240,240,240 … 0,0,0), so routing the ramp
     through the current hue (as a "brightness of this colour" reading would)
     paints a red band the source does not have. */
  background-image: linear-gradient(to right, #fff 0%, #000 100%);
  cursor: ew-resize;
}

/* Both strips share one handle: a 12px puck spanning the band's full height. */
.color-picker__handle {
  position: absolute;
  top: -2px;
  bottom: -2px;
  width: 12px;
  transform: translateX(-50%);
  box-sizing: border-box;
  background-color: transparent;
  border: 2px solid #fff;
  border-radius: 2px;
  box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px 0;
  pointer-events: none;
}

/* 16px below the alpha strip (y274 -> y290). Dimmed and small; the capture
   shows no '#', so the markup carries the bare digits. */
.color-picker__hex {
  margin-top: 16px;
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  text-align: center;
  opacity: 0.7;
  user-select: text;
}

/* ---- entry flow --------------------------------------------------------- */

/*
 * "What do you want to do with this image?" — the interstitial between picking
 * a file and the editor. Uploading does NOT drop you into the canvas; NAI
 * intercepts the file with this three-way choice and mounts nothing until one
 * is picked.
 *
 * Every number below is from out/components/upload-modal.json, measured at
 * 1600x1000. Vibe Transfer and Precise Reference are offered because that is the
 * real flow, but both are out of scope (see PROJECT_NOTES §7); they keep NAI's
 * styling and are disabled, rather than being silently dropped.
 */
.image-intent[hidden] {
  display: none;
}

/*
 * Backdrop and panel are siblings, not nested: the capture records the overlay
 * at z-index 301 and the dialog at 305, as two separate fixed layers.
 *
 * Flat rgb(14,15,33) at opacity 0.7 with backdropFilter: none — deliberately NOT
 * a blur. Every other clone of this dialog reaches for backdrop-filter; the
 * measurement says NAI does not.
 */
.image-intent__backdrop {
  position: fixed;
  inset: 0;
  z-index: 301;
  background-color: var(--nai-bg-input);
  opacity: 0.7;
}
.image-intent__panel {
  position: fixed;
  inset: 0;
  z-index: 305;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The panel layer only centres; the box below carries the measured size. */
  pointer-events: none;
}

/* 418x487, padded 50/20/30 — the 50px top is what clears the close button. */
.image-intent__box {
  position: relative;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  width: 418px;
  max-width: calc(100vw - 40px);
  height: 487px;
  max-height: calc(100vh - 40px);
  padding: 50px var(--nai-space-4) 30px;
  background-color: var(--nai-bg-page);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  overflow: hidden;
  pointer-events: auto;
}

/* 32x32 at top:20 left:366 — measured from the box's own padding box, so it is
   pinned by offsets rather than placed by the flex flow. */
.image-intent__close {
  position: absolute;
  top: var(--nai-space-4);
  right: var(--nai-space-4);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--nai-text);
  cursor: pointer;
}

/* The arc motif: an inset layer 1px outside the content, on --nai-bg-raised. It
   is decorative and sits behind everything, which is why the body below needs a
   stacking context of its own to stay legible. */
.image-intent__lines {
  position: absolute;
  inset: -1px;
  background-color: var(--nai-bg-raised);
  opacity: 0.35;
}

.image-intent__body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1;
  min-height: 0;
}

/* Eczar 20/30 in the accent colour, with 15px of lead-in above it. The title
   wraps to two lines at this width on the live page, which is why the measured
   box is 75 tall rather than 30. */
.image-intent__title {
  width: 100%;
  margin: 0;
  padding: var(--nai-space-3) 0 0;
  font-family: var(--nai-font-display);
  font-size: 20px;
  font-weight: 600;
  line-height: 30px;
  color: var(--nai-accent);
}

/* The preview well: 376x222 on the darkest surface, 10px of inset padding,
   5px radius, 15px of margin above and below. */
.image-intent__preview {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 100%;
  flex: 1;
  min-height: 0;
  margin: var(--nai-space-3) 0;
  padding: var(--nai-space-2);
  background-color: var(--nai-bg-input);
  border: var(--nai-border);
  border-radius: var(--nai-radius-lg);
}
/* `contain`, so the image letterboxes and keeps its aspect ratio. The rendered
   354x200 in the capture is the well's inner box, not the image's own size. */
.image-intent__preview-image {
  width: 100%;
  height: 100%;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  background-size: contain;
}

/* Wrapping row, space-around: that is what puts two buttons on the first line
   and centres the third under them, rather than any explicit grid. */
.image-intent__options {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  gap: var(--nai-space-2);
  width: 100%;
  max-width: 385px;
}
/* Accent-filled, 34 tall, 4/6 padding, 10px between glyph and label. */
.image-intent__option {
  display: flex;
  align-items: center;
  gap: var(--nai-space-2);
  height: 34px;
  padding: 4px 6px;
  background-color: var(--nai-accent);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  color: var(--nai-bg-input);
  font-family: inherit;
  font-size: var(--nai-text-base);
  line-height: var(--nai-line-base);
  cursor: pointer;
}
.image-intent__option:hover:not(:disabled) {
  filter: brightness(1.08);
}
.image-intent__option:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ---- the saved-mask state of the Image2Image card ------------------------ */

/*
 * Committing a mask REPLACES the card's contents rather than adding a panel
 * below it, keeping the head and swapping its copy to Inpaint / Change part of
 * an image. The card grows 395x269 -> 395x441.
 *
 * Every offset below is card-relative, from the re-capture that anchors both
 * states on the card root (states.saved.slot + subBoxes.*.offsetInCard). The
 * first capture anchored this state on the "Mask" sub-box while the img2img
 * state anchored on the card, so the two were never comparable and the head
 * above the sub-box was outside the frame entirely.
 *
 *   22  back / pen / trash / fold      (44 tall; fold 31 wide, rest 44)
 *   21  the text block                 (24 title + 21 descender = 45)
 *   81  maskBox starts, 353x339
 *        +38  "Mask" label row
 *        +38  preview 351x233
 *        +38  to the slider
 *         22  slider
 *          8  tail
 *   21  card bottom below maskBox
 */
.mask-state[hidden] {
  display: none;
}
/*
 * Leads the head — the one control that leaves this state without discarding the
 * image. Measured at card-relative l:7 w:44, with the text block starting at
 * l:51, i.e. flush against the arrow with no gap between them.
 *
 * The card's own 20px inset already places content at l:20, so this pulls back
 * 13px to reach 7. Centred on the 45px two-line text block rather than aligned
 * to its first line, which is how rail-saved.png shows it.
 *
 * No left-arrow asset exists, so the right one is rotated, as the fold does.
 */
.mask-state__back {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 44px;
  height: 44px;
  margin-left: -13px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--nai-text);
  cursor: pointer;
}
.mask-state__back .icon {
  transform: rotate(180deg);
}
/*
 * The shared head is `justify-content: space-between`, which with a third child
 * spreads all three and pushes the copy into the middle of the card. Measured,
 * the text starts at l:51 — flush against the arrow — so the gap belongs
 * entirely to the right: the text block takes the slack instead.
 */
.mask-state .image2image__text {
  margin-right: auto;
}
.mask-state__body[hidden] {
  display: none;
}
/*
 * maskBox sits at card-relative top:81. The head above it ends at 21+45 = 66,
 * so this is the remaining 15 — NOT the 10px .image2image__body uses, which is
 * what left the "Mask" label crowding the header.
 */
.mask-state__body {
  margin-top: 15px;
}
/*
 * The label and preview as one bordered panel, 353x339 overall.
 *
 * The 1px border is why a 353-wide box holds a 351-wide image: the layers are
 * inset exactly 1px on each side. The capture's own node walk reports maskBox as
 * borderless because the border lives on this inner wrapper, which it did not
 * enumerate — rail-saved.png shows it plainly.
 */
.mask-box {
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  overflow: hidden;
}
/*
 * The titled bar above the image — 38px from the box's top edge to the preview,
 * on the page fill so it reads as a header strip rather than a floating label.
 */
.mask-box__head {
  display: flex;
  align-items: center;
  height: 38px;
  padding: 0 var(--nai-space-2);
  background-color: var(--nai-bg-page);
  box-sizing: border-box;
}
/* 16/600/24, the same treatment the card titles use. */
.mask-box__title {
  font-weight: 600;
}
/*
 * 351x233. Two stacked layers share this rect in the capture — the base image
 * beneath, and the mask over it at natural 96x64 with object-fit:fill and an
 * effective opacity of 0.7. app.js composites both into one source, so the
 * element itself only has to letterbox the base: 768x512 is 1.5:1 against this
 * box's 1.505:1, and all four corner markers survive.
 *
 * Sits on the input fill because that composite letterboxes, and bare page
 * background in the bars would read as a hole in the card.
 */
.mask-state__preview {
  display: block;
  /* Fills the bordered box rather than sitting at a fixed 351: the rail is 395
     wide in the capture and 409 here, and a hardcoded width would leave a strip
     of card showing beside the image. 351x233 is the measured RESULT of that
     fill, not an input. */
  width: 100%;
  height: 233px;
  background-color: var(--nai-bg-input);
  object-fit: contain;
}
/* The measured 38px runs from the preview's bottom edge to the SLIDER, and the
   "Strength" label row sits inside that span: 12 here + the 24px row + the
   field's own 2px gap. Setting 38 directly would put the label where the slider
   belongs and leave the slider 26px low. */
.mask-state .slider-field {
  margin-top: 12px;
}
.mask-state .slider-field__row {
  padding: 0;
}

/* The standing notice beside the Vibe Transfer card it disables. */
.vibe-notice[hidden] {
  display: none;
}
.vibe-notice {
  margin: 0 var(--nai-panel-inset) var(--nai-space-3);
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  opacity: 0.7;
}
/* Unavailable rather than merely unstyled: the endpoint rejects the pairing, so
   the control is actually disabled in app.js and this matches it visually. */
.feature-card--disabled {
  opacity: 0.45;
}

/* ---- HSV Adjustment ------------------------------------------------------
 *
 * The panel behind the faders button in the bottom toolbar, captured for the
 * first time by `npm run ui:canvas-settings` (canvas-settings.json). The button
 * had been rendered with no handler because nothing had ever opened it — the
 * tool sweeps deliberately skipped the action cluster, since clicking `trash`
 * or `resize` mid-run poisons every capture after it.
 *
 * It is a colour-grading panel over the whole drawing, not the generic "canvas
 * settings" the placeholder aria-label guessed at.
 *
 * Measured 260x239 at 670,689 — i.e. ABOVE the toolbar and horizontally near
 * its own button (984). Anchored to the window chrome here, bottom-left of the
 * button, so it sits over the stage rather than inside the toolbar's flow.
 */
.hsv-panel[hidden] {
  display: none;
}
.hsv-panel {
  position: absolute;
  /*
   * CENTRED ON THE WINDOW, not on the button that opens it.
   *
   * That is what the capture shows and it is worth stating, because the button
   * sits far to the right (x=984, centre 1006) and "flyout above its trigger"
   * is the obvious reading. NAI's panel centre is 800 — the centre of the 1600
   * viewport, and of the chrome — so it is a centred dialog that happens to be
   * opened from a toolbar button.
   *
   * Derived rather than hardcoded. An earlier version set `left: 674px`, taken
   * straight from the capture's x=670, which was wrong twice over: it ignored
   * the chrome's own 10px inset (the same correction the colour picker above
   * makes, 57 -> 47), and being an absolute it stayed put while the button
   * moved with the viewport — measured 192px left of its button at 1600 wide
   * and 32px at 1280, i.e. anchored to nothing.
   */
  left: 50%;
  transform: translateX(-50%);
  /*
   * Clear of the toolbar ROW, which is 50px — not the 44px button inside it.
   * The row is flush to the chrome's bottom edge, so `bottom` counts up from
   * there and this is row height plus the measured 12px gap (NAI's panel ends
   * at 928 against the bar's top at 940). Using the button's 44 leaves it 6px
   * low and reads as the panel resting on the bar.
   */
  bottom: calc(50px + 12px);
  z-index: 10;
  display: flex;
  justify-content: center;
  width: 260px;
}
/* The frame is the inner box; the outer is a plain positioning wrapper, which
   is how the capture has it (outer transparent, inner carrying the fill). */
.hsv-panel__body {
  display: flex;
  flex-direction: column;
  /* 8px, measured. Deliberately not --nai-space-2 (10px) — the scale has no 8,
     and rounding it to the nearest token stacks 2px of error across four gaps
     and renders the panel 8px taller than the 239 it measures. */
  gap: 8px;
  width: 100%;
  padding: 10px;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  box-sizing: border-box;
}
.hsv-panel__title {
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  font-weight: 600;
}

/* 238x44: a 29px label row over the 12px range, with no gap between them —
   the 44 is 29 + 12 + the range's own 3px UA margin, the same arithmetic the
   pen-size group uses. */
.hsv-field {
  display: flex;
  flex-direction: column;
}
.hsv-field__row {
  height: 29px;
}
.hsv-field__label {
  display: flex;
  align-items: center;
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  font-weight: 600;
}
/* 16x21 with no visible box — the value reads as bold text inline with the
   label, and only the caret shows it is editable. */
/*
 * Sized from its content, not the captured 16px.
 *
 * 16px is what the field measures showing "0", its default — NAI's hugs its
 * digits rather than reserving a column, so freezing that one measurement
 * clipped every value with more than one character. These fields are SIGNED
 * (hue -180..180, the other two -100..100), so the common case is four
 * characters wide and the minus was the first thing to be cut off.
 *
 * Same --digits mechanism as .pen-size__number above, kept in sync from app.js.
 * The +2px is that rule's allowance for the caret; `ch` is the width of "0", so
 * a minus sign is narrower than the unit it is counted as and never overflows.
 */
.hsv-field__number {
  width: calc(var(--digits, 1) * 1ch + 2px);
  height: 21px;
  /* 3px, measured: "Hue:" ends at 709 and the field starts at 712. */
  margin-left: 3px;
  padding: 0 0 0 2px;
  background-color: transparent;
  border: none;
  border-radius: 1px;
  color: inherit;
  font: inherit;
  font-weight: 700;
  -moz-appearance: textfield;
  appearance: textfield;
}
.hsv-field__number::-webkit-outer-spin-button,
.hsv-field__number::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
/* 2px, measured on all three rows (field ends 728/767/768, unit starts
   730/769/770). The earlier 1px read as "100%" with no gap at all once the
   field grew past its captured single-digit width. */
.hsv-field__unit {
  margin-left: 2px;
}

/* Same track as every other range in the window, at the panel's full width. */
.hsv-field__range {
  display: block;
  width: 238px;
  height: 12px;
  margin: 0 0 3px;
  background-color: var(--nai-bg-input);
  border-radius: var(--nai-radius);
  -webkit-appearance: none;
  appearance: none;
  overflow: visible;
  cursor: default;
}
.hsv-field__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 18px;
  margin-top: -3px;
  background-color: var(--nai-bg-raised);
  border: none;
  border-radius: 2px;
  cursor: pointer;
}
.hsv-field__range::-moz-range-thumb {
  width: 12px;
  height: 18px;
  background-color: var(--nai-bg-raised);
  border: none;
  border-radius: 2px;
  cursor: pointer;
}

/* Reset hard left, Cancel and Apply hard right — the 91px gap between them in
   the capture is a flexible spacer, not a fixed margin, so the row survives
   different label widths. */
.hsv-panel__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 27px;
}
.hsv-panel__spacer {
  flex: 1;
}
/* Reset and Cancel are unadorned text; only Apply gets a fill. */
.hsv-panel__text {
  padding: 0;
  background: none;
  border: none;
  color: inherit;
  font-family: inherit;
  font-size: 12.8px;
  font-weight: 600;
  line-height: 19px;
  cursor: pointer;
}
.hsv-panel__text:disabled {
  opacity: 0.45;
  cursor: default;
}
.hsv-panel__apply {
  padding: 4px 12px;
  background-color: var(--nai-bg-raised);
  border: none;
  border-radius: 4px;
  color: inherit;
  font-family: inherit;
  font-size: 12.8px;
  font-weight: 600;
  cursor: pointer;
}
