/**
 * Settings panel — the left rail.
 *
 * Geometry and colour come from tools/ui-extract/out/components/settings-panel.json
 * (measured at 1600x1000). Class names are ours; NAI's own are styled-components
 * hashes that churn on deploy, so nothing here depends on them.
 */

.settings-panel {
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;
  width: var(--nai-panel-width);
  height: 100%;
  box-sizing: border-box;
  background-color: var(--nai-bg-panel);
  color: var(--nai-text);
  font-family: var(--nai-font);
  font-size: var(--nai-text-base);
  line-height: var(--nai-line-base);
  /* The rail's own right edge, separating it from the canvas. */
  border-right: var(--nai-border);
  overflow: hidden; /* the inner scroller handles overflow, not the rail */
}

/* Content between the navbar and the footer. Scrolls on its own so the footer
   below can stay pinned rather than being pushed off the bottom. */
.panel-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* Pinned foot of the rail: the AI-settings glance bar suspended directly above
   Generate, 15px apart, as measured (the pair occupy y=834..1000 in a 1000px
   panel). Neither scrolls away with the settings. */
.panel-footer {
  display: flex;
  flex-direction: column;
  /* 15px between the bar and Generate (bar ends 921, button starts 936), and
     20px of clearance below the button (ends 980 in a 1000px panel). */
  gap: var(--nai-space-3);
  flex: 0 0 auto;
  padding-bottom: var(--nai-space-4);
  background-color: var(--nai-bg-panel);
}

/* Header strip — 449x50 at the top of the rail. Brand mark sits hard left in
   its own cell, the hamburger hard right, with the Anlas pill centred between. */
.panel-header {
  display: flex;
  align-items: center;
  /* No gap — the cells are flush to their dividers and the Anlas group centres
     itself with auto margins. A gap here would offset that centring. */
  height: 50px;
  flex: 0 0 auto;
  padding: 0;
  box-sizing: border-box;
  background-color: var(--nai-bg-panel);
  border-bottom: var(--nai-border);
}

/* The logo and hamburger sit in full-height cells divided off from the middle
   of the bar. NAI draws those divisions as cell borders spanning the navbar's
   full 50px, not as inset rules — hence border-right/left here rather than a
   separate element like the segmented button group uses. */
.brand-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 57px;
  height: 100%;
  flex: 0 0 auto;
  border-right: var(--nai-border);
}
.brand-mark::before {
  content: '';
  width: 18px;
  height: 21px;
  background-color: currentColor;
  -webkit-mask: var(--icon) center / contain no-repeat;
  mask: var(--icon) center / contain no-repeat;
}

.hamburger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 49px;
  height: 100%;
  padding: 0;
  background: none;
  border: 0;
  border-left: var(--nai-border);
  /* Full white at opacity 1 — the capture reads rgb(255,255,255)/op=1 on the
     glyph, so the 0.9 that was here rendered it slightly grey. */
  color: var(--nai-text);
  cursor: pointer;
}
.hamburger:hover {
  background-color: var(--nai-bg-raised);
}

/* Anlas pill: a bordered readout joined to a "+" button, hence the split radii
   (3px 0 0 3px / 0 3px 3px 0) measured on the live control. 124x38 + 46x38.
   The 180x38 group is centred in the bar — `margin: auto` on both sides rather
   than a gap, so it stays centred regardless of the flanking cell widths. */
.anlas-group {
  display: flex;
  align-items: center;
  height: 38px;
  margin: 0 auto;
  flex: 0 0 auto;
}
.anlas-readout {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--nai-space-1);
  width: 124px;
  height: 38px;
  padding: 0 var(--nai-space-3);
  /* Bordered on all four sides — the capture shows Top,Right,Bottom,Left with
     radius `3px 0 0 3px`. The right edge stays: the "+" button is a filled
     block butted against it, so that edge reads as the seam between them. */
  border: var(--nai-border);
  border-radius: var(--nai-radius) 0 0 var(--nai-radius);
  /* 14px/600 throughout the pill, per the capture. */
  font-size: var(--nai-text-sm);
  font-weight: 600;
  line-height: var(--nai-line-sm);
  box-sizing: border-box;
}
/* White. Only the value and its glyph are accent — the pill container measures
   rgb(255,255,255) and just the inner group (grFpki) is rgb(245,243,194). */
.anlas-readout__label {
  color: var(--nai-text);
}
/* Balance and glyph are both accent — the whole readout group is `grFpki`,
   coloured rgb(245,243,194) at 14px/600. */
.anlas-readout strong,
.anlas-readout .icon {
  color: var(--nai-accent);
  font-weight: 600;
}
.anlas-add {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 38px;
  padding: 0;
  background-color: var(--nai-bg-raised);
  border: 0;
  border-radius: 0 var(--nai-radius) var(--nai-radius) 0;
  color: var(--nai-text);
  cursor: pointer;
}

/* Full-bleed band holding the model card, closed by a rule across the whole
   panel. Measured at 435x135 @0,50 with border-bottom — note the 435px width
   at x=0, i.e. NOT the 395px content inset, which is why an inset border on the
   card itself never matched. This value only became visible after the extractor
   was fixed to record per-side borders; before that it was absent from the
   capture entirely, and four rounds of guessing at it all missed. */
/* 135 tall: 20 top + a 20px empty spacer NAI renders above the card + 74 card
   + 20 bottom. The spacer carries no content, so it's folded into the padding
   here rather than reproduced as an element. */
.model-section {
  /* Anchors the absolutely-positioned menu, which is a sibling of the card so it
     can overhang this section's border-bottom instead of being clipped by it.

     The z-index is load-bearing, not decoration. `position: relative` alone
     leaves this section at the default level, so the sections BELOW it — which
     come later in document order — paint over the open menu: "Customize separate
     characters" showed through the menu's lower rows. Raising the section (not
     just the menu) is what puts the whole subtree above its later siblings. */
  position: relative;
  z-index: 5;
  padding: 40px var(--nai-panel-inset) var(--nai-panel-inset);
  border-bottom: var(--nai-border);
}

/* Model card — 395x74, laid out as a 358px text column plus a 37px indicator.
   Text starts at x=42 inside a card at x=20, so the inset is 22px, not 15. */
/* Content is a 40px block centred in a 74px box — 18px above, 16px below, as
   measured. An earlier version fixed the height and let the two grid rows
   stretch to fill it, which pinned the title to the top and the description to
   the bottom while the box height still measured "correct". */
.model-card {
  position: relative;
  /* The card IS the combobox button. Focus styling is the browser default ring
     suppressed in favour of the border treatment below, so the ring is restored
     explicitly for keyboard users. */
  cursor: pointer;
  user-select: none;
  display: grid;
  grid-template-columns: 1fr 37px;
  grid-template-rows: 21px 18px;
  /* 40px of content centred in 74 — 18 above, 16 below. The 1px row gap matches
     the capture (title ends at 129, description starts at 130). */
  row-gap: 1px;
  align-content: center;
  /* No margin — .model-section supplies the 20px inset around it. */
  width: 100%;
  height: 74px;
  padding: 0 0 0 22px;
  box-sizing: border-box;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
}

/* 37px column holding the 1x58 rule and the chevron, both at the card's right. */
.model-card__indicator {
  /* Purely decorative, and it paints ON TOP of the stretched select — a click
     on the chevron hit the icon and never reached the control. The select owns
     all interaction, so take this column out of hit-testing entirely. */
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--nai-space-2);
  grid-row: 1 / span 2;
  grid-column: 2;
  align-self: stretch;
  padding-right: var(--nai-space-2);
  opacity: 0.8;
}
/* A filled 1x58 rule in the page tone, not a border — same trick NAI uses for
   the react-select indicator separators throughout. */
.model-card__rule {
  width: 1px;
  height: 58px;
  flex: 0 0 auto;
  background-color: var(--nai-bg-page);
}

/* The visible title — Eczar 14px/700/21px, per the capture (div.fdaUFf). */
.model-card__title {
  grid-column: 1;
  grid-row: 1;
  height: 21px;
  color: var(--nai-text);
  font-family: var(--nai-font-display);
  font-size: var(--nai-text-sm);
  font-weight: 700;
  line-height: var(--nai-line-sm);
}

/* No hover or focus ring. NAI draws none: across the whole captured panel every
   node computes `outline: ... none ...`, and the card's own edge is its resting
   1px border in the divider tone — there is no second state layered on top. An
   earlier version invented a muted border on hover and a 2px focus ring, which
   made the card light up on approach in a UI that never does that.

   Suppressed explicitly rather than just left unstyled. This is a tabindex div,
   not a button, so Chrome matches :focus-visible even on a plain mouse click —
   removing the custom rule alone left the UA default (a near-black 1px ring)
   painted around the card for the whole time the menu was open.

   The open state is signalled by the chevron flip and the menu itself, which is
   what NAI relies on too. */
.model-card:focus,
.model-card:focus-visible {
  outline: none;
}

/* The chevron points down when shut and flips up while the menu is open, matching
   the direction the card is about to move. .icon--flipped supplies the shut
   state in markup, so this cancels it. */
.model-card[aria-expanded="true"] .model-card__indicator .icon {
  transform: none;
}

/* The remaining native <select>s (resolution, sampler, …) still get an OS-drawn
   popup, so their options need a readable surface. */
.select-control option {
  background-color: var(--nai-bg-panel);
  color: var(--nai-text);
}

/* Source Sans Pro 14px/400/21px at FULL white — the capture reads opacity 1 and
   rgb(255,255,255) here, not the dimmed treatment used for section headings. */
.model-card__desc {
  grid-column: 1;
  grid-row: 2;
  /* The row is 18px tall but the type keeps its measured 21px leading — the
     glyphs overflow the row box slightly, which is exactly what NAI does (its
     description element is 18 tall carrying 21px line-height). Forcing
     line-height to 18 instead would have been the wrong lever: it changes the
     type scale to fix a layout number. */
  height: 18px;
  overflow: visible;
  font-size: var(--nai-text-sm);
  font-weight: 400;
  line-height: var(--nai-line-sm);
}

/* Prompt block — input wells sit on the darkest surface.
   It and .base-img are one continuous outlined shape: this is the top (rounded
   top corners, no bottom edge) and .base-img is the foot (rounded bottom
   corners), so the border runs unbroken down both and the shared edge appears
   once. The base-img radius of `0 0 3px 3px` in the capture is what gives this
   away — a standalone card would be rounded on all four. */
.prompt-block {
  margin: var(--nai-space-4) var(--nai-panel-inset) 0;
  background-color: var(--nai-bg-input);
  border: var(--nai-border);
  border-bottom: 0;
  border-radius: var(--nai-radius) var(--nai-radius) 0 0;
}

/* 46px tall: 15px 15px 0 around a 26px control row. */
.prompt-tabs {
  display: flex;
  align-items: center;
  gap: var(--nai-space-2);
  height: 46px;
  padding: var(--nai-space-3) var(--nai-space-3) 0;
  box-sizing: border-box;
}
.prompt-tab {
  padding: 2px var(--nai-space-1);
  background: none;
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-sm);
  font-weight: 600;
  line-height: var(--nai-line-sm);
  white-space: nowrap;
  cursor: pointer;
}
.prompt-tab[aria-selected="true"] {
  background-color: var(--nai-bg-raised);
}
/* The inactive tab drops to half opacity — measured on the wrapper (op=0.5)
   while the active one keeps its raised pill at full strength. */
.prompt-tab[aria-selected="false"] {
  opacity: 0.5;
}

/* 26x26 anime/furry dataset toggle, sitting left of the tabs. */
.dataset-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
  padding: var(--nai-space-1);
  box-sizing: border-box;
  background: none;
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  cursor: pointer;
}
/* The glyph itself swaps — sakura for anime, paw for furry. An earlier version
   kept one icon and recoloured it, which reads as "selected" rather than as a
   mode switch and gives no clue what the other mode is. */
.dataset-toggle .icon {
  --icon: url('../icons/sakura.svg');
}
.dataset-toggle[aria-pressed="true"] .icon {
  --icon: url('../icons/paw.svg');
}
.dataset-toggle:hover {
  background-color: var(--nai-bg-raised);
}

/* The live UI uses ProseMirror here for weight-syntax highlighting. A textarea
   matches the styling but not that behaviour — see README. */
/* `[hidden]` must win over this rule's `display: block`, or the inactive tab's
   textarea stays painted — the tab switch sets .hidden, not a class. */
.prompt-input[hidden] {
  display: none;
}
/* Grows with its content instead of scrolling, matching ProseMirror. The height
   is driven from JS (autoGrow) up to --prompt-max, after which it scrolls.
   `resize: none` because NAI has no drag handle — the box sizes itself. */
.prompt-input {
  display: block;
  width: 100%;
  /* 106px editor area: NAI's .relative wrapper is 106 tall around a 96px
     ProseMirror with 5px of breathing room top and bottom. */
  min-height: 106px;
  max-height: var(--prompt-max, 420px);
  padding: var(--nai-space-1) var(--nai-space-3);
  box-sizing: border-box;
  background-color: var(--nai-bg-input);
  border: 0;
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  line-height: var(--nai-line-base);
  overflow-y: auto;
  resize: none;
}
.prompt-input:focus {
  outline: none;
}

/* Two rows below the editor, sharing the prompt well's fill.
 *
 * Both were rebuilt from settings-panel.json after guesswork got them wrong
 * twice: first as a labelled button in the wrong place, then stripped to a bare
 * die. The capture shows a LABELLED 110x32 button preceded by a small "or", and
 * a separate row carrying the length bar plus the Quality Tags status. */
.prompt-actions {
  display: flex;
  align-items: center;
  /* 10px, not 5: the capture puts "or" at x=41 ending at 55 and the button at
     x=65. A 5px gap pulled the button ~5px left of where NAI draws it. */
  gap: var(--nai-space-2);
  height: 32px;
  padding: 0 var(--nai-panel-inset);
  background-color: var(--nai-bg-input);
}
/* 16px/400/24px at full opacity — measured; it is not dimmed. */
.prompt-actions__or {
  font-size: var(--nai-text-base);
  font-weight: 400;
  line-height: var(--nai-line-base);
}

/* Once the prompt has text the offer to fill it is redundant, so NAI drops the
   "or" and the label and leaves a bare die. Toggled by syncRandomize(). */
.prompt-actions--compact .prompt-actions__or,
.prompt-actions--compact .randomize span:not(.icon) {
  display: none;
}
/* The die does not just shrink in place — it moves ONTO the length-bar row and
   sits inline to the bar's left, which is what the filled state actually looks
   like. So this row collapses to nothing rather than keeping a 32px band with a
   lone die floating in it, and .prompt-meter reclaims the space.
 *
 * `position: absolute` + a matching left pad on the bar rather than moving the
 * node in JS: the button belongs to the actions row semantically (it is the
 * "randomize the prompt" control, not a meter readout), and reparenting on every
 * keystroke would drop focus mid-interaction. */
.prompt-actions--compact {
  position: relative;
  height: 0;
  overflow: visible;
  padding: 0 var(--nai-panel-inset);
}
/* `space-between` is meaningless once the label is hidden — with a single child
   it pins the die to the box's left edge instead of centring it. The labelled
   state needs it (that is what holds the die and label apart at the measured
   110px), so it is undone here rather than dropped above. */
.prompt-actions--compact .randomize {
  position: absolute;
  z-index: 1;
  left: var(--nai-panel-inset);
  justify-content: center;
  /* Centred on the length bar, not merely dropped near it. This row is
     zero-height and sits directly above .prompt-meter, so the bar's centre is
     the meter's 10px top padding plus the 5px margin above the 4px track, plus
     half the track: 10 + 5 + 2 = 17. Half the 26px box is 13, so the box starts
     4px BELOW this row's baseline. */
  top: 4px;
  width: 26px;
  height: 26px;
  padding: 0;
  background: none;
}
.prompt-actions--compact .randomize:hover {
  background-color: var(--nai-bg-panel);
}
/* The bar row yields the die's 26px plus a 10px gap; the status line beneath is
   full width, matching the filled state where only the bar is indented. */
.prompt-actions--compact + .prompt-meter .prompt-length {
  width: calc(100% - 36px);
  margin-left: 36px;
}

/* 110x32 on the panel fill — a real button, not a bare glyph.
   `padding: 3px 5px` and `justify-content: space-between` are the measured
   values; 0/10px centred made the button 118px and spread the die away from its
   label, which is what pushed the whole control out of position. */
.randomize {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--nai-space-1);
  flex: 0 0 auto;
  width: 110px;
  height: 32px;
  padding: 3px 5px;
  box-sizing: border-box;
  background-color: var(--nai-bg-panel);
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  /* 16px/600/24px — the label is emphasised, unlike the "or" beside it. */
  font-size: var(--nai-text-base);
  font-weight: 600;
  line-height: var(--nai-line-base);
  cursor: pointer;
}
.randomize:hover {
  background-color: var(--nai-bg-raised);
}

/* 53px: 10px 20px padding around a 14px bar row and a 19px status row. */
.prompt-meter {
  display: flex;
  flex-direction: column;
  height: 53px;
  padding: var(--nai-space-2) var(--nai-panel-inset);
  box-sizing: border-box;
  background-color: var(--nai-bg-input);
}
/* 12.8px/400/19.2px, and the ROW is dimmed to 0.7 as a whole — label and glyph
   together. An earlier pass read the label span (opacity 1) and concluded the
   dimming was invented, but the 0.7 lives on this flex row one level up:
   settings-panel.json has `41,413 353x19 opacity: 0.7` wrapping a full-opacity
   'Quality Tags Enabled'. Dimming here rather than on the text so the help glyph
   fades with it. */
.prompt-meter__status {
  display: flex;
  align-items: center;
  gap: var(--nai-space-1);
  opacity: 0.7;
  font-size: var(--nai-text-xs);
  font-weight: 400;
  line-height: 19.2px;
}

/* Icons.
 *
 * NAI serves these as standalone SVGs referenced through CSS `mask-image`, not
 * as inline markup — the element paints `currentColor` through the SVG as a
 * stencil. That is why every icon inherits its container's text colour instead
 * of carrying a fill of its own, and it's worth preserving: one icon file works
 * on any surface, and hover/disabled states come for free.
 *
 * Downloaded by `npm run ui:assets` into src/web/public/icons/.
 */
.icon {
  display: inline-block;
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  /* Nudges the glyph onto the text baseline; inline-block otherwise sits it
     on the line box bottom and it reads as misaligned next to a label. */
  vertical-align: -0.15em;
  background-color: currentColor;
  mask: var(--icon) center / contain no-repeat;
  -webkit-mask: var(--icon) center / contain no-repeat;
}

/* Inside a flex container the baseline nudge is wrong — the parent already
   centres the glyph, so the -0.15em shifts it off-centre by ~2px. Every icon
   button in this panel is a flex box, which is why they all read slightly low. */
button .icon,
.icon-button .icon,
.button-group__button .icon,
.dimension-box__swap .icon,
.model-card__indicator .icon {
  vertical-align: baseline;
}

/* An icon immediately followed by label text needs breathing room — otherwise
   it renders flush against the first character. */
.prompt-tab .icon,
.add-character .icon,
.anlas-readout .icon {
  margin-right: 6px;
}
.anlas-readout .icon {
  margin-right: 0;
  margin-left: 6px;
}
/* Sizes below are measured, not chosen. `npm run ui:icon-sweep` records the
   rendered box of every mask icon on the live page to out/icons/masks.json;
   these classes and the per-icon overrides after them are transcribed from it.
   NAI paints icons the same way this clone does — a mask box tinted with
   background-color — so the box size IS the icon size. Several glyphs are
   deliberately non-square or appear at two sizes in different places; that is
   NAI's doing, not drift, and it's why a single uniform scale can't work. */
.icon--sm {
  width: 16px;
  height: 16px;
}
.icon--xs {
  width: 14px;
  height: 14px;
}
.icon--md {
  width: 20px;
  height: 20px;
}
.icon--lg {
  width: 24px;
  height: 24px;
}
/* The Anlas glyph is 10x10 on the live header — smaller than the plus beside it. */
.icon--anla {
  width: 10px;
  height: 10px;
}

/* Per-icon boxes that don't fit the square scale, straight from masks.json.
   These win over the size classes above, so a glyph can't be sized wrong by
   picking the wrong --sm/--xs at the call site. */
.icon-settings  { width: 20px; height: 16px; }  /* 20x16 in the navbar */
.icon-cross     { width: 16px; height: 12px; }  /* 16x12 dimension separator */
.icon-easel     { width: 24px; height: 24px; }  /* 24x24, not 16 */
.icon-help      { width: 20px; height: 20px; }  /* 20x20, not 14 */
.icon-vibe-transfer { width: 20px; height: 20px; }
.icon-precise-reference { width: 24px; height: 24px; }  /* 24x24 in the rail */
/* vibe-transfer and precise-reference appear at a SECOND, smaller size in the
   upload modal — both are 16x16 there (upload-summary.json, modalControls) even
   though the rail draws them at 20 and 24. An explicit .icon--sm wins back over
   the per-icon boxes above, which would otherwise silently oversize them. */
.icon--sm.icon-vibe-transfer,
.icon--sm.icon-precise-reference { width: 16px; height: 16px; }
/* Same story for the gear: 20x16 in the navbar, 16x16 on the prompt block's
   settings trigger. masks.json records both boxes. */
.icon--sm.icon-settings { width: 16px; height: 16px; }
/* And for the help glyph: 20x20 where it stands alone, but 16x16 trailing the
   Quality Tags line — settings-panel.json measures that svg at 158,415 16x16
   inside a 21x16 wrapper. The blanket 20x20 above made it visibly outgrow the
   12.8px text it annotates. */
.icon--xs.icon-help { width: 16px; height: 16px; }
/* The section-collapse arrow is the one glyph NAI draws at a second, larger
   size: 14x14 inline, 16x15 as a panel header control. Not 16x16 — the extra
   pixel of height would letterbox the 14x8 artwork differently than NAI does. */
.icon--sm.icon-arrow-up,
.icon--sm.icon-arrow-down { width: 16px; height: 15px; }
/* pen-tip is the 18x21 brand mark, sized on .brand-mark::before — not here. */

.icon-pen-tip { --icon: url('../icons/pen-tip-light.svg'); }
.icon-hamburger { --icon: url('../icons/hamburger_alt.svg'); }
.icon-settings { --icon: url('../icons/settings.svg'); }
.icon-help { --icon: url('../icons/help.svg'); }
.icon-vibe-transfer { --icon: url('../icons/vibe_transfer.svg'); }
/* The upload flow's three glyphs. img2img/inpaint/fold postdate the last icon
   sweep, so they are absent from out/icons/masks.json; all three are measured at
   16x16 in the upload capture instead (upload-summary.json). The SVGs are NAI's
   own assets, fetched from the hashed URLs recorded in that same capture. */
.icon-precise-reference { --icon: url('../icons/precise_reference.svg'); }
.icon-img2img { --icon: url('../icons/img2img.svg'); }
.icon-inpaint { --icon: url('../icons/inpaint.svg'); }
.icon-fold { --icon: url('../icons/fold.svg'); }
.icon-import { --icon: url('../icons/import.svg'); }
.icon-plus { --icon: url('../icons/plus.svg'); }
.icon-dice { --icon: url('../icons/dice.svg'); }
.icon-anla { --icon: url('../icons/anla.svg'); }
.icon-history { --icon: url('../icons/history.svg'); }
.icon-image-count { --icon: url('../icons/image_count.svg'); }
.icon-pen { --icon: url('../icons/pen.svg'); }
.icon-sakura { --icon: url('../icons/sakura.svg'); }
.icon-arrow-up { --icon: url('../icons/directional_arrow_up.svg'); }
.icon-arrow-down { --icon: url('../icons/directional_arrow_down.svg'); }
.icon-arrow-right { --icon: url('../icons/directional_arrow_right.svg'); }
.icon-easel { --icon: url('../icons/easel.svg'); }
/* Character prompts. The three gender glyphs are the live SVG paths from NAI's
   picker flyout, copied verbatim; the four card-header icons were only ever
   captured as 14x14 mask boxes, so they are drawn to match the panel's stroke
   weight rather than guessed at pixel level. */
.icon-female { --icon: url('../icons/female.svg'); }
.icon-male { --icon: url('../icons/male.svg'); }
.icon-other { --icon: url('../icons/other.svg'); }
.icon-trash { --icon: url('../icons/trash.svg'); }
.icon-check { --icon: url('../icons/check.svg'); }
/* No caret-up/-down classes: those were hand-drawn triangles spanning 12x8 of a
   24x24 viewBox, so `contain` rendered them at about a third of their box. NAI
   uses its own directional_arrow_* assets (ink fills the viewBox) at 14x14 —
   see .icon-arrow-up/-down above. Delete the two SVGs once nothing wants them. */
.icon-close-x { --icon: url('../icons/close-x.svg'); }
/* The separator between width and height is a thin × — NAI's own asset, not a
   swap arrow. An earlier version invented a two-arrow glyph for this. */
.icon-cross { --icon: url('../icons/thin_cross.svg'); }

/* Draw Mask toolbar. Every one of these is a 16x16 mask box in the capture
   (draw-summary.json), so no per-icon size override is needed — .icon--sm is
   already the measured box.

   ALL OF THESE ARE NOW NAI'S REAL ARTWORK. The eraser/select/resize/undo/redo/
   export/trash files were previously drawn by hand to the panel's 2px stroke
   weight, because `ui:assets` sweeps the cold page and this toolbar only exists
   once the Draw window is open. The draw-mode capture recorded their hashed
   URLs, so they were fetched from _next/static/media directly and the hash
   stripped — the same naming `ui:assets` would have produced.

   The substitution was not cosmetic: the hand-drawn `resize` was a box with a
   diagonal arrow, where NAI's is a dashed crop frame around an image. */
.icon-eraser { --icon: url('../icons/eraser.svg'); }
.icon-select { --icon: url('../icons/select.svg'); }
.icon-fill { --icon: url('../icons/fill.svg'); }
.icon-palette { --icon: url('../icons/palette.svg'); }
.icon-resize { --icon: url('../icons/resize.svg'); }
.icon-undo { --icon: url('../icons/undo.svg'); }
.icon-redo { --icon: url('../icons/redo.svg'); }
.icon-export { --icon: url('../icons/export.svg'); }
.icon-layers { --icon: url('../icons/layers.svg'); }

/* Draw-mode-only glyphs — the tools MASK mode does not have.
 * Source: out/components/canvas-draw-window.json (npm run ui:canvas-draw).
 *
 *   dropper, penwriting  DOWNLOADED from NAI, not drawn: the capture recorded
 *                        their real mask URLs (dropper.78b85b64.svg,
 *                        penwriting.3c214126.svg) so they were fetched from
 *                        _next/static/media directly. `ui:assets` still cannot
 *                        find them — it sweeps the cold page and this toolbar
 *                        only exists once the Draw window is open.
 *   lasso, blur,         transcribed verbatim from the inline <svg> the capture
 *   clone-stamp,         recorded — these are inline in NAI too, so there is no
 *   sliders              asset to download and the markup IS the artwork.
 *
 * `sliders` is deliberately not icons/settings.svg: NAI uses vertical faders
 * for the canvas-settings button and a gear in the rail, so sharing one file
 * would be a substitution rather than a match. */
.icon-dropper { --icon: url('../icons/dropper.svg'); }
.icon-penwriting { --icon: url('../icons/penwriting.svg'); }
.icon-lasso { --icon: url('../icons/lasso.svg'); }
.icon-blur { --icon: url('../icons/blur.svg'); }
.icon-clone-stamp { --icon: url('../icons/clone-stamp.svg'); }
.icon-sliders { --icon: url('../icons/sliders.svg'); }

/* Prompt-settings popover — opened by the gear in the prompt header.
 * Measured 380x520 with 20px padding, anchored right-start beside the 450px
 * rail. The height is FIXED, not content-driven: both tabs are 520px and the
 * Prompt Chunks tab uses the slack to push "Delete All" to the bottom.
 * Source: out/components/prompt-settings/ (npm run ui:prompt-settings)
 */
.prompt-settings {
  position: absolute;
  z-index: 20;
  width: 380px;
  height: 520px;
  box-sizing: border-box;
  padding: var(--nai-space-4);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
}
.prompt-settings[hidden] {
  display: none;
}

/* Tab strip: 338x39 = a 25px chip row + 8px padding-bottom + a 1px rule,
   then 12px of margin before the body. The rule is easy to miss on a dark
   screenshot but it is there in the capture. */
.prompt-settings__tabs {
  display: flex;
  flex: 0 0 auto;
  padding-bottom: 8px;
  margin-bottom: 12px;
  border-bottom: var(--nai-border);
}
/* The selected tab is a FILLED CHIP, not bolder text — 58x25 / 101x25 with
   rgb(34,37,63) behind it, 3px radius, 2px/5px padding, weight 400 at 14px.
   The unselected one is the same box at opacity 0.5 with no fill.

   `align-self: flex-start` is load-bearing: without it the chip stretches to
   the full strip height and reads as a tall slab instead of hugging its text.
   The width is text-driven too — never set one.

   The first tab carries margin-left:-5px so its TEXT lines up with the panel's
   20px inset while the chip's own 5px padding bleeds into the gutter. Without
   it "Settings" sits 5px right of everything below it. */
.prompt-settings__tab {
  align-self: flex-start;
  padding: 2px 5px;
  /* The 5px below each chip is part of the strip's 39px: 25 + 5 + 8 + 1. */
  margin-right: var(--nai-space-1);
  margin-bottom: var(--nai-space-1);
  background: none;
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-sm);
  font-weight: 400;
  line-height: 21px;
  cursor: pointer;
  opacity: 0.5;
}
.prompt-settings__tab:first-child {
  margin-left: -5px;
}
.prompt-settings__tab[aria-selected="true"] {
  opacity: 1;
  background-color: var(--nai-bg-raised);
}

/* Setting row: label left, checkbox right, description underneath.
   Row offsets inside the panel body, measured: Add Quality Tags +72,
   Undesired Content Preset +158, Disable Tag Suggestions +240,
   Highlight Emphasis +285 — i.e. a 45px pitch between the two bare rows and a
   larger gap after the ones carrying a description or a select. */
.setting {
  display: block;
  cursor: pointer;
}
/* 338x24 with 6px under it — and the 6px is there even on rows with no
   description, which is why a bare row measures 30px and the Add Quality Tags
   block 51px (24 + 6 + 21). The blocks themselves are then spaced by the
   column's 15px gap, not by margins. */
.setting__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--nai-space-2);
  height: 24px;
  margin-bottom: 6px;
}
/* 16px/600 at line-height 24 — these read as headings, not body text. The
   clone had them at weight 400, which is the single most visible error in the
   old build after the checkbox shape. */
.setting__label {
  font-size: var(--nai-text-base);
  font-weight: 600;
  line-height: var(--nai-line-base);
}
/* Both descriptions occupy ONE 338x21 grid cell, stacked at the same position
   and swapped by opacity (0.7 vs 0) — NAI never removes either from flow. That
   is what keeps the block at exactly 51px: hiding one instead collapses the
   wrapper to a single line box and every row below shifts up. */
.setting__descs {
  display: grid;
  grid-template-columns: 100%;
}
.setting__desc {
  grid-area: 1 / 1;
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  opacity: 0.7;
}
.setting__desc[data-hidden='true'] {
  opacity: 0;
}

/* NAI's checkbox is NOT an iOS track-and-thumb switch, which is what this used
   to be. Measured in out/components/prompt-settings/settings.json:

     47x20 track
       26x16 slab   absolute, inset 2px; left:2px unchecked -> left:19px checked
                    bg rgb(34,37,63) unchecked -> rgb(14,15,33) checked
       8x8 glyph    absolute, rides inside the slab, painted with mask-image
                    small_cross.svg @ rgba(255,255,255,0.5) unchecked
                    check.svg       @ rgb(245,243,194)      checked

   The slab is a rounded rectangle that SLIDES, and the tick/cross sits inside
   it — there is no circular knob. The old capture recorded the boxes but not
   the mask, so this was rebuilt as a switch; ui:prompt-settings records the
   paint mechanism specifically to stop that recurring. */
/* The 47x20 track carries the STATE COLOUR — accent when checked, input-dark
   when not — and the slab that slides over it is the opposite tone. Getting
   this backwards (accent slab on a clear track) reads as a floating chip. */
.toggle {
  position: relative;
  flex: 0 0 auto;
  width: 47px;
  height: 20px;
  background-color: var(--nai-bg-input);
  border-radius: var(--nai-radius);
  transition: background-color 0.15s ease;
}
.toggle:has(input:checked) {
  background-color: var(--nai-accent);
}
.toggle input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
  z-index: 1;
}
/* The sliding slab. */
.toggle__track {
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 2px;
  width: 26px;
  height: 16px;
  background-color: var(--nai-bg-raised);
  border-radius: var(--nai-radius);
  transition: left 0.15s ease, background-color 0.15s ease;
}
/* The 8x8 glyph inside it. mask-image, not a background: the SVG carries its
   own stroke colour and we recolour it per state. */
.toggle__track::after {
  content: '';
  position: absolute;
  top: 4px;
  left: 9px;
  width: 8px;
  height: 8px;
  background-color: rgba(255, 255, 255, 0.5);
  -webkit-mask-image: url('../icons/small_cross.svg');
  mask-image: url('../icons/small_cross.svg');
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}
/* Checked: slab slides right to left:19px and darkens to the input tone, and
   its glyph becomes the accent-coloured tick. Measured, both states:
     track rgb(14,15,33)  slab rgb(34,37,63)   glyph small_cross @ white/0.5
     track rgb(245,243,194) slab rgb(14,15,33) glyph check @ rgb(245,243,194) */
.toggle input:checked + .toggle__track {
  left: 19px;
  background-color: var(--nai-bg-input);
}
.toggle input:checked + .toggle__track::after {
  background-color: var(--nai-accent);
  -webkit-mask-image: url('../icons/check-small.svg');
  mask-image: url('../icons/check-small.svg');
}

/* Tab body — 338x427 under the strip. The rhythm is ONE gap on the column,
   not per-child margins: 15px on the Settings tab, 16px on Prompt Chunks. */
.prompt-settings__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.prompt-settings__body[hidden] {
  display: none;
}
#panel-settings {
  gap: var(--nai-space-3);
}
#panel-chunks {
  gap: 16px;
}

/* The Undesired Content Preset group — 87px = 20 + 24 + 5 + 38. Its heading
   carries the only extra spacing in the tab, on top of the column gap. */
.setting-group {
  display: block;
}
.setting-group > .setting__label {
  display: block;
  margin-top: var(--nai-space-4);
  margin-bottom: var(--nai-space-1);
}
/* The preset face is 38px here, not the 44px the same control uses out in the
   rail (sampler, noise schedule). Scoped to the popover so those keep theirs.

   `width: 100%` is required, not decorative: .select-control fills its parent
   via `flex: 1 1 auto`, and this group is a block, so without an explicit width
   the select collapses to its intrinsic ~148px. */
.prompt-settings .select-control {
  width: 100%;
  height: 38px;
}

/* --- Prompt Chunks tab ---------------------------------------------------
   Measured from out/components/prompt-settings/prompt-chunks.json:
   header row 338x32 with a 32x32 import button and two 26x26 icon buttons
   (14x14 glyphs), the empty-state line, then Delete All at 338x46 pinned to
   the bottom of the 520px panel. */
.chunks__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 32px;
}
/* 93x21 at 14px/600, opacity 0.8 — NOT the 16px/600 used for the Settings
   tab's row labels, and not full opacity either. */
.chunks__heading {
  font-size: var(--nai-text-sm);
  font-weight: 600;
  line-height: var(--nai-line-sm);
  opacity: 0.8;
}
.chunks__actions {
  display: flex;
  align-items: center;
  gap: 2px;
}
.chunks__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  cursor: pointer;
}
/* The import button is the wider 32x32 one carrying an inline SVG in NAI. */
.chunks__btn--wide {
  width: 32px;
  height: 32px;
}
.chunks__btn:hover {
  background-color: var(--nai-bg-raised);
}
.icon--chunk {
  width: 14px;
  height: 14px;
}
/* 12.8px at opacity 0.6 — the smallest type in the panel. Spacing comes from
   the column's 16px gap, so no margin of its own. */
.chunks__empty {
  margin: 0;
  font-size: var(--nai-text-xs);
  line-height: 21px;
  opacity: 0.6;
}
/* NAI puts a real 338x273 spacer between the empty state and Delete All rather
   than relying on `margin-top:auto`. Reproduced as a flex-grow filler so the
   button lands on the same 453px offset at the panel's fixed height. */
.chunks__spacer {
  flex: 1 1 auto;
  min-height: 0;
}
/* 338x46, weight 700 (not 600), 10px padding. */
.chunks__delete-all {
  flex: 0 0 auto;
  width: 100%;
  height: 46px;
  padding: var(--nai-space-2);
  background-color: var(--nai-bg-raised);
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  font-weight: 700;
  cursor: pointer;
}
.chunks__delete-all:hover {
  background-color: var(--nai-bg-input);
}

/* Gear that opens the popover. */
/* 26x26 with 5px padding around a 16x16 glyph — measured. Without an explicit
   box this collapsed to 16x18, sized by the glyph plus the line box. */
.prompt-settings-trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin-left: auto;
  padding: var(--nai-space-1);
  box-sizing: border-box;
  background: none;
  border: 0;
  color: var(--nai-text);
  font-size: var(--nai-text-base);
  line-height: 1;
  cursor: pointer;
}
/* Full opacity at rest: settings-panel.json measures this button (373,223 26x26)
   and its glyph at opacity 1, the same as the dataset toggle at the other end of
   the tab row. The 0.7 that used to sit here was carried over from the dimmed
   icon-button treatment and made the gear read as disabled. Hover is a fill,
   not a fade, matching the other bare icon buttons in the rail. */
.prompt-settings-trigger:hover {
  background-color: var(--nai-bg-raised);
  border-radius: var(--nai-radius);
}

/* 353x4 track, sitting in a 14px-tall row.
   Quality Tags appears in TWO places in the live UI: the toggle in the settings
   popover, and this read-only status line restating its state. */
.prompt-length {
  width: 100%;
  height: 4px;
  margin: var(--nai-space-1) 0;
  background-color: var(--nai-bg-panel);
  overflow: hidden;
}
/* White, not accent — the accent is reserved for the Generate button. */
.prompt-length__fill {
  width: 0;
  height: 100%;
  background-color: var(--nai-text);
  transition: width 0.15s ease;
}

/* Base image drop zone — 395x66, joined to the prompt block above it, so only
   the bottom corners are rounded. */
.base-img {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Flush against the prompt block above — no top margin, and no top border,
     since the shape is continuous and a second edge would double the line. */
  margin: 0 var(--nai-panel-inset);
  padding: var(--nai-space-2) var(--nai-space-2) var(--nai-space-2) var(--nai-space-3);
  background-color: var(--nai-bg-page);
  border: var(--nai-border);
  border-top: var(--nai-border);
  border-radius: 0 0 var(--nai-radius) var(--nai-radius);
}
.base-img__label {
  opacity: 0.7;
}
.base-img[hidden] {
  display: none;
}

/*
 * The loaded Image2Image card — 395x269 at 20,442 in rail-after.json.
 *
 * This REPLACES the strip above rather than filling it in: "Add a Base Img
 * (Optional)" appears in the diff's textRemoved, and the card carries a full
 * four-sided border on --nai-bg-panel instead of joining the prompt block's
 * outline. Inner padding is 20/20/0, so the content column is 353 wide and the
 * bottom spacing comes from a trailing 20px spacer rather than the padding.
 */
.image2image {
  margin: 0 var(--nai-panel-inset);
  background-color: var(--nai-bg-page);
  border: var(--nai-border);
  position: relative;
  overflow: hidden;
}
.image2image[hidden] {
  display: none;
}
/*
 * The uploaded image behind the controls.
 *
 * Not in the element walk — the extractor does not record pseudo-elements — but
 * plainly visible in rail-image-slot.png, where the base image sits dimmed
 * behind the sliders. `contain` keeps its aspect ratio, matching how the same
 * image is previewed in the modal. app.js sets --image.
 */
/* The mask state renders the image as a real <img>, so the faded backdrop would
   be the same picture shown twice — once dimmed behind the once that replaced
   it. Only the img2img state wants it. */
.image2image--masked::before {
  display: none;
}
.image2image::before {
  content: '';
  position: absolute;
  /* Inset by the card's own padding rather than filling it edge to edge: at
     inset 0 a `contain` image reaches the border and reads as bleeding into the
     block above, which is not how the capture shows it. */
  inset: var(--nai-space-4);
  background-image: var(--image, none);
  background-position: 50% 50%;
  background-repeat: no-repeat;
  background-size: contain;
  opacity: 0.35;
  pointer-events: none;
}
/* Content sits above the backdrop; without a stacking context of its own the
   ::before would paint over the controls rather than behind them. */
.image2image__inner {
  position: relative;
  padding: var(--nai-space-4) var(--nai-space-4) 0;
}

/* 353x45: the title block on the left, the controls on the right. */
.image2image__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.image2image__text {
  display: flex;
  flex-direction: column;
}
/* 16/600/24 — the same weight the character cards use for their names. */
.image2image__title {
  font-weight: 600;
}
/* 14/400/21 at 0.7, the panel's standard muted descender. */
.image2image__desc {
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  opacity: 0.7;
}
/* 128 wide: the 97x44 group, 10px, then the 21x44 fold. */
.image2image__actions {
  display: flex;
  gap: var(--nai-space-2);
}
/* 21x44, padded 14px 0 14px 5px — outside the bordered group and unbordered,
   which is what makes it read as a separate control rather than a third
   segment. */
.image2image__fold {
  display: flex;
  align-items: center;
  width: 21px;
  height: 44px;
  padding: 14px 0 14px 5px;
  box-sizing: border-box;
  background: none;
  border: 0;
  color: var(--nai-text);
  cursor: pointer;
}
.image2image__fold[aria-expanded='false'] .icon {
  transform: rotate(180deg);
}

/* 10px below the head, per the measured margin on the controls block. */
.image2image__body {
  margin-top: var(--nai-space-2);
}
.image2image__body[hidden] {
  display: none;
}
/* The card supplies its own 20px inset, so the rail-wide padding that
   .slider-field__row carries elsewhere would double it here. */
.image2image__body .slider-field__row {
  padding: 0;
}
/* 10px above the button row; the trailing 20px is the card's bottom spacing,
   which the 20/20/0 padding deliberately leaves to the content. */
.image2image__footer {
  display: flex;
  margin-top: var(--nai-space-2);
  padding-bottom: var(--nai-space-4);
}
/* The group hugs its one labelled button (131 wide) instead of filling the row —
   a bare .button-group is a block-level flex box and would stretch. */
.image2image__footer .button-group {
  width: max-content;
}

/*
 * Segmented button group — 97x44 shell, two 44x42 buttons, 1px rule between.
 *
 * The rule is a filled element, not a border. NAI does this throughout (the
 * react-select indicator separators are the same trick), and it matters: a
 * border would span the full 44px button height, while the real separator is
 * inset to 30px so it stops short of the group's rounded corners.
 * Source: settings-panel.json, div.SLIDS at 1x30 @355,460.
 */
.button-group {
  display: flex;
  align-items: center;
  height: 44px;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
}
.button-group__button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 42px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  cursor: pointer;
}
.button-group__button:hover {
  background-color: var(--nai-bg-raised);
}
/* The labelled variant — 129x42, 12px padding, 6px to the label, 14/600. Must
   stay AFTER the base rule: it overrides the square 44px width and zero padding
   at the same specificity, so defining it earlier silently loses. */
.button-group__button--labelled {
  width: auto;
  gap: 6px;
  padding: 0 12px;
  font-family: inherit;
  font-size: var(--nai-text-sm);
  font-weight: 600;
  line-height: var(--nai-line-sm);
  /* Without this the label wraps to two lines inside the inherited 44px box. */
  white-space: nowrap;
}
.button-group__rule {
  flex: 0 0 auto;
  width: 1px;
  height: 30px;
  background-color: var(--nai-bg-raised);
}

/* Square icon buttons — 46x44 and 97x44 variants, panel-coloured on a border. */
.icon-button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--nai-space-1);
  min-width: 46px;
  height: 44px;
  padding: 0 var(--nai-space-2);
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  cursor: pointer;
}
.icon-button:hover {
  background-color: var(--nai-bg-raised);
}

/* Character Prompts — a heading row with the Add button inline at the right. */
/* The button is centred on the two-line heading, not hung from its top edge:
   settings-panel.json puts the heading at y=529 and the 28px button at y=535,
   which is the middle of the 39px text block rather than its first line. */
.character-prompts {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: var(--nai-space-4) var(--nai-panel-inset) 0;
}
/* Title and subtitle are both dimmed; the Add Character button beside them is
   not, so the opacity sits on this wrapper rather than on .character-prompts. */
.character-prompts__heading {
  opacity: 0.5;
}
/* 14px/600/21px — the 18px line-height here was invented; NAI uses the same
   21px leading as every other 14px label in the panel. */
.character-prompts__title {
  font-size: var(--nai-text-sm);
  font-weight: 600;
  line-height: var(--nai-line-sm);
}
/* 14px/400/21px. */
.character-prompts__sub {
  font-size: var(--nai-text-sm);
  font-weight: 400;
  line-height: var(--nai-line-sm);
}
/* 131x28 — measured at 284,535. Sized explicitly because the intrinsic width of
   the plus + label came out 4px short, which read as the button floating off the
   panel's right edge rather than sitting flush to it. */
.add-character {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 131px;
  height: 28px;
  padding: 2px var(--nai-space-1);
  box-sizing: border-box;
  background-color: var(--nai-bg-raised);
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  font-weight: 600;
  cursor: pointer;
}

/* ---------------------------------------------------------------------------
   Character cards.

   Two renders, not one styled two ways. Measured from
   out/components/character-prompts-2.json, which caught both at once:

                     unfocused          focused
     height          140px              254px
     tabs            absent             Prompt / Undesired Content
     prompt text     plain span         editable well + token bar
     Position        inline summary     own row, with Tokens in the bar
     delete          in summary row     in the tab row

   NAI marks this structurally — both cards carry the same classes and there is
   no aria-selected anywhere — but rebuilding it that way would mean tearing the
   textarea out of the DOM on every blur and losing scroll/caret. Here one card
   holds both and `[data-focused]` picks which parts show, which is why the
   summary and the editor are siblings below.
   --------------------------------------------------------------------------- */
.character-list {
  margin: 0 var(--nai-panel-inset);
}

/* The tab-like name plate sits ON the top border, so the card itself carries no
   top border of its own — the header row draws it. */
.character-card {
  color: var(--nai-text);
}

.character-card__head {
  display: flex;
  justify-content: space-between;
  /* flex-end, not center: the name plate is 28px against the 24px buttons and
     they sit on a common baseline, not a common midline. */
  align-items: flex-end;
}

/* 106x28 at 2px/5px padding, radius only on the top corners — it reads as a
   folder tab above the card body. */
.character-card__name {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 2px var(--nai-space-1);
  background-color: var(--nai-bg-raised);
  border-radius: var(--nai-radius) var(--nai-radius) 0 0;
  font-size: var(--nai-text-base);
  line-height: var(--nai-line-base);
}
.character-card__name .icon {
  width: 16px;
  height: 16px;
}
/* Same ink-ratio correction as the flyout — see .gender-flyout__option .icon. */
.character-card__name .icon-other {
  width: 11.7px;
  height: 11.7px;
}

.character-card__actions {
  display: flex;
  gap: 4px;
}
/* Each action is a bare button wrapping a raised span — the span is what gets
   the fill and the top-corner radius, matching the name plate opposite. */
.character-card__action {
  padding: 0;
  background: none;
  border: 0;
  color: var(--nai-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.character-card__action > span {
  display: flex;
  align-items: center;
  padding: 2px 6px;
  background-color: var(--nai-bg-raised);
  border-radius: var(--nai-radius) var(--nai-radius) 0 0;
}
/* Disabled reorder arrows dim the whole button, not the glyph — measured at
   opacity 0.5 with the colour left alone. */
.character-card__action:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Card body. Top and bottom rules in --nai-bg-raised close the box that the
   name plate opens. */
.character-card__body {
  border-top: var(--nai-border);
  border-bottom: var(--nai-border);
}

/* --- unfocused: a read-only summary ---------------------------------------- */
.character-card__summary {
  padding: var(--nai-space-3);
  cursor: pointer;
}
/* One line of the prompt, clipped. NAI shows a 40px band and lets the rest be
   cut off rather than wrapping or ellipsising. */
.character-card__excerpt {
  height: 40px;
  overflow: hidden;
  font-size: var(--nai-text-base);
  line-height: 26px;
}
.character-card__excerpt:empty::before {
  content: attr(data-placeholder);
  opacity: 0.5;
}
.character-card__meta {
  display: flex;
  justify-content: space-between;
  gap: var(--nai-space-2);
  padding-top: var(--nai-space-2);
}
.character-card__chips {
  display: flex;
  gap: var(--nai-space-2);
}
/* Pill readouts: label in 400, value in 600 with an 8px gap. Radius 3 all round
   here, unlike the name plate's top-only. */
.character-chip {
  padding: 2px var(--nai-space-1);
  background-color: var(--nai-bg-raised);
  border-radius: var(--nai-radius);
  font-size: var(--nai-text-base);
  line-height: 26px;
  cursor: default;
}
.character-chip__value {
  margin-left: 8px;
  font-weight: 600;
}

/* --- focused: tabs, editor, token bar, position row ------------------------ */
.character-card__tabs {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--nai-space-3) var(--nai-space-3) 0;
  background-color: var(--nai-bg-input);
}
.character-card__tablist {
  display: flex;
  gap: var(--nai-space-2);
}
/* The selected tab is the one with a fill; the other is the same box at 0.5.
   Negative left margin on the first pulls it flush with the 15px inset. */
.character-tab {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 2px var(--nai-space-1);
  margin: 0 var(--nai-space-1) var(--nai-space-1) 0;
  border: 0;
  border-radius: var(--nai-radius);
  background-color: transparent;
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-sm);
  font-weight: 600;
  line-height: var(--nai-line-sm);
  cursor: pointer;
}
.character-card__tablist .character-tab:first-child {
  margin-left: -5px;
}
.character-tab[aria-selected='true'] {
  background-color: var(--nai-bg-raised);
}
.character-tab[aria-selected='false'] {
  opacity: 0.5;
}

/* The tab-row delete is a bare 16x16 glyph at full white — no raised pill and
   no dimming, unlike its counterpart in the summary row, which sits in a chip.
   Measured: opacity 1, transparent button, 16x16 icon.

   `color` matters as much as the size here: .icon paints its mask in
   currentColor, and this button's glyph is NOT wrapped in the raised <span>
   that the other actions use, so without an explicit colour it inherits the
   raised panel fill and the icon renders in #22253f against the well — present,
   correct size, and effectively invisible. */
.character-card__tabs [data-action='delete'] {
  display: flex;
  color: var(--nai-text);
  opacity: 1;
}
.character-card__tabs [data-action='delete'] > .icon {
  width: 16px;
  height: 16px;
  background-color: var(--nai-text);
}

.character-card__well {
  padding: var(--nai-space-1) var(--nai-space-3);
  background-color: var(--nai-bg-input);
}
/* 96px inner height, matching the ProseMirror box. Resize is off because the
   token bar below is positioned against a fixed well. */
.character-card__input {
  display: block;
  width: 100%;
  height: 96px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  line-height: 26px;
  resize: none;
}
.character-card__input:focus {
  outline: none;
}
.character-card__input::placeholder {
  color: var(--nai-text);
  opacity: 0.5;
}

/* Token bar — a 4px track on the panel fill, with the used portion drawn at 50%
   white. Same construction as .prompt-length; kept separate because this one
   sits inside the input well rather than under it. */
/* 34px overall: a 4px track with 15px of air above and below. The measured node
   reports 10px/20px padding around a nested wrapper that adds its own 5px, which
   collapses to this once the intermediate divs are dropped. */
.character-card__tokens {
  padding: 15px var(--nai-space-4);
  background-color: var(--nai-bg-input);
}
.character-card__token-track {
  display: flex;
  height: 4px;
  overflow: hidden;
  background-color: var(--nai-bg-panel);
  border-radius: var(--nai-radius);
}
.character-card__token-fill {
  flex: 0 0 auto;
  background-color: rgba(255, 255, 255, 0.5);
}
/* The bright cap on the leading edge of the fill. */
.character-card__token-head {
  flex: 0 0 auto;
  width: 3px;
  background-color: var(--nai-text);
}

.character-card__position {
  display: flex;
  align-items: center;
  gap: var(--nai-space-2);
  padding: 0 var(--nai-space-3) var(--nai-space-3);
  background-color: var(--nai-bg-input);
}
.character-card__position-label {
  font-size: var(--nai-text-base);
  line-height: var(--nai-line-base);
}
.position-trigger {
  display: inline-flex;
  align-items: center;
  gap: var(--nai-space-1);
  padding: 0;
  border: 0;
  background: none;
  color: var(--nai-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
/* 24px tall, not 28. The 2px padding sits INSIDE the 24px box on the live node —
   an inline-block that adds padding to a 24px line-height renders 28 and pushes
   the whole position row 3px over its measured height. */
.position-trigger > span {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  height: 24px;
  padding: 2px var(--nai-space-1);
  background-color: var(--nai-bg-raised);
  border-radius: var(--nai-radius);
}
/* The cell code that follows "Adjust" — bare text, no plate of its own, and
   absent entirely on AI's Choice. Measured 84px for the AI's Choice button vs
   76px for "Adjust" + a separate "C3": the code is outside the button's box. */
.position-trigger .position-trigger__cell {
  padding: 0;
  background: none;
}
.position-trigger .position-trigger__cell:empty {
  display: none;
}
/* Global AI's Choice: the control is inert on the card, so it reads as a
   disabled plate. Measured disabled=true on the live node. */
.position-trigger:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Which half of the card is showing. */
.character-card[data-focused='false'] .character-card__editor,
.character-card[data-focused='true'] .character-card__summary {
  display: none;
}

/* Disabled: the character stays in the list, keeps everything typed into it, and
   is left out of the request. Dimming the name plate and body rather than the
   whole card keeps the header controls legible — you have to be able to find the
   toggle to switch it back on, and the delete button has to stay usable. */
.character-card[data-enabled='false'] .character-card__name,
.character-card[data-enabled='false'] .character-card__body {
  opacity: 0.4;
}
.character-card[data-enabled='false'] .character-card__summary {
  cursor: default;
}

/* --- the A1-E5 position grid ------------------------------------------------
   INLINE, not a popover. NAI replaces the card body with this in place: the
   floating-layer count stays 0 across the click that opens it, unlike the
   gender flyout. An earlier version here was an always-on popover anchored to
   the trigger, which is the wrong shape — it left the card body visible
   underneath and could open on a card that was not the one being edited. */
.character-card__picker {
  display: none;
}
.character-card[data-picking='true'] .character-card__picker {
  display: block;
}
/* While the picker is open it IS the body — the editor and the summary both go,
   which is what makes the header rename legible as a mode change. */
.character-card[data-picking='true'] .character-card__editor,
.character-card[data-picking='true'] .character-card__summary {
  display: none;
}

/* The frame carries the image's own proportions so a cell maps to the region it
   will actually occupy. Measured 180x180 outer with a 123.16px grid inside —
   180 * 832/1216 = 123.16, i.e. the frame is square and the GRID is the portrait
   box. Height is what's fixed; width follows from the ratio. */
.position-frame {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  /* 195 = the measured 180px of grid plus the 15px top padding above it. The
     live node measures 196 tall against a 180 grid for exactly this reason. */
  height: 195px;
  padding: 15px 15px 0;
  background-color: var(--nai-bg-input);
}
.position-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-template-rows: repeat(5, 1fr);
  gap: 4px;
  box-sizing: border-box;
  height: 100%;
  aspect-ratio: 832 / 1216;
  /* Height drives the width via the ratio, so the grid must not be allowed to
     flex-shrink off it — shrinking narrows the tracks below their content and
     the cell labels wrap. */
  flex: 0 0 auto;
  padding: 4px;
  background-color: var(--nai-bg-page);
}
/* Every character shows at once, marked with its NUMBER — not a tick. Only the
   one being edited gets the white box, so you can place a character against
   where the others already are. */
.position-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  /* A cell is ~19px wide and its label can be several characters when they
     share a cell, so it must not wrap or stretch the track it sits in. The
     label shrinks to fit instead of being clipped — with 15 characters allowed,
     a crowded cell is legible rather than a truncated digit that reads as the
     wrong character number. */
  overflow: hidden;
  min-width: 0;
  padding: 0 1px;
  background-color: transparent;
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  color: rgba(255, 255, 255, 0.6);
  font-family: inherit;
  font-size: var(--nai-text-base);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}
/* A cell holding more than one character steps down a size rather than being
   clipped: a truncated "12" reads as the wrong character number. Set from the
   occupant count in renderCharacters, since it depends on state, not on width. */
.position-cell[data-occupants='2'] {
  font-size: var(--nai-text-xs);
}
.position-cell[data-occupants='3'],
.position-cell[data-occupants='many'] {
  font-size: 8px;
  letter-spacing: -0.02em;
}
.position-cell:hover {
  border-color: rgba(255, 255, 255, 0.5);
}
/* The edited character's own cell. */
.position-cell[aria-pressed='true'] {
  background-color: rgba(255, 255, 255, 0.3);
  border-color: rgb(255, 255, 255);
}

.character-card__picker-foot {
  display: flex;
  justify-content: center;
  padding: 10px var(--nai-space-3) var(--nai-space-3);
  background-color: var(--nai-bg-input);
}
.position-done {
  height: 28px;
  padding: 2px var(--nai-space-2);
  background-color: var(--nai-bg-raised);
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  font-weight: 600;
  cursor: pointer;
}
.position-done:hover {
  background-color: var(--nai-bg-page);
}

/* --- the global AI's Choice pill --------------------------------------------
   Only rendered at 2+ characters. Cream when on, bordered and dimmed when off;
   the leading glyph swaps check/X. The real input is visually hidden rather
   than removed so the pill stays a real checkbox for keyboard and AT. */
.position-global {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 25px;
  /* Same horizontal inset as .character-prompts above it — this row is a
     sibling of that section, not of the full-bleed card list between them. */
  margin: var(--nai-space-2) var(--nai-panel-inset) 0;
}
.position-global[hidden] {
  display: none;
}
.position-global__label {
  font-size: var(--nai-text-base);
}
.position-global__pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--nai-space-1);
  box-sizing: border-box;
  /* The measured 100x25 is what the label happens to occupy, not a clamp: at a
     fixed 100px the glyph plus "AI's Choice" overflows and wraps out of the
     pill. Min-width holds the measurement without cutting the text. */
  min-width: 100px;
  height: 25px;
  padding: 1px var(--nai-space-2);
  white-space: nowrap;
  background-color: var(--nai-bg-page);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  color: rgba(255, 255, 255, 0.5);
  font-size: var(--nai-text-sm);
  font-weight: 700;
  cursor: pointer;
}
.position-global__pill input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.position-global__pill:has(input:checked) {
  background-color: var(--nai-accent);
  border-color: transparent;
  color: var(--nai-bg-page);
}
.position-global__pill:has(input:focus-visible) {
  outline: 1px solid var(--nai-text);
  outline-offset: 1px;
}

/* The gender picker NAI opens from Add Character. Same popover shell as the
   prompt-settings panel — it is the same styled-components primitive on the
   live site (sc-3e47b8ca-*), so it should not look like a different widget. */
/* 131x107 at the Add Character button's own origin — it covers the button
   rather than dropping below it, so the first option ("Female") lands where the
   button was. Popper reports bottom-start, but the reference it measures from is
   the heading row, not the button. Source: character-gender-flyout.json. */
.gender-flyout {
  position: absolute;
  z-index: 300;
  display: flex;
  flex-direction: column;
  width: 131px;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  overflow: hidden;
}
.gender-flyout[hidden] {
  display: none;
}
/* 129x35 each, stacked to fill the 107px panel (3x35 plus the 1px borders). */
.gender-flyout__option {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 35px;
  flex: 0 0 auto;
  padding: 0 var(--nai-space-2);
  background: none;
  border: 0;
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.gender-flyout__option:hover {
  background-color: var(--nai-bg-raised);
}
/* Female/Male measure 16x16 in the flyout; Other measures 11.7x11.7. That gap
   is not a mistake to normalise away — NAI draws these as react-icons sized at
   1em, and Other is the one glyph whose ink fills its whole viewBox (512 of
   512, vs ~14 of 24 for the stroke-drawn pair). Matching the boxes to 16/16/11.7
   reproduces the optical size NAI actually shows. Source: out/icons/sweep.json. */
.gender-flyout__option .icon {
  width: 16px;
  height: 16px;
}
.gender-flyout__option .icon-other {
  width: 11.7px;
  height: 11.7px;
}

/* At the 15-character ceiling. Kept as a disabled button rather than hiding it,
   so the row doesn't reflow when the last character is added. */
.add-character:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Feature cards — Vibe Transfer and friends. Transparent fill on a border,
   with the action button pinned right. */
/* Three cells: leading glyph, text, trailing upload button — measured at
   x=41 (20x20), x=76 (202 wide) and x=348 (46x44). */
/* 71px tall, everything vertically centred, 21px of inset on BOTH sides — the
   glyph sits at leftInset=21 and the upload button at rightInset=21.
   `align-items: flex-start` here previously threw the button to the top-right
   corner while the card's measured height still looked right. */
.feature-card {
  display: flex;
  align-items: center;
  gap: var(--nai-space-3);
  height: 71px;
  margin: var(--nai-space-3) var(--nai-panel-inset) 0;
  padding: 0 21px;
  box-sizing: border-box;
  background-color: transparent;
  border: var(--nai-border);
  border-radius: var(--nai-radius);
}
/* 20x20 at x=41, with the text starting at x=76 — a 15px gap after the glyph. */
.feature-card__glyph {
  width: 20px;
  height: 20px;
}
/* Takes the slack so the button is pushed to the right edge. */
.feature-card__text {
  flex: 1 1 auto;
  min-width: 0;
}
.feature-card__title {
  font-size: var(--nai-text-base);
  font-weight: 600;
  line-height: var(--nai-line-base);
}
.feature-card__desc {
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  opacity: 0.7;
}

/* Section heading — 14px/400/21px, full white dimmed to 0.5. Weight 400, not
   bold: "Image Settings" reads as a quiet label, not a heading. */
.section-heading {
  margin: var(--nai-space-4) var(--nai-panel-inset) var(--nai-space-1);
  font-size: var(--nai-text-sm);
  font-weight: 400;
  line-height: var(--nai-line-sm);
  opacity: var(--nai-heading-opacity);
}

/* Image settings.
   Inset with padding rather than margin, as NAI does — the row then spans the
   full panel width, so any background or divider it carries reaches the edge
   instead of stopping 20px short. */
.settings-row {
  display: flex;
  gap: var(--nai-space-2);
  padding: 0 var(--nai-panel-inset);
}
/* The native dropdown arrow is replaced by NAI's indicator: a 1x26 separator
   with the SAME directional_arrow_up glyph the model card uses, flipped to point
   down. Both are painted as background layers because a bare <select> can't host
   child elements.
 *
 * The arrow was two CSS gradient triangles — a hand-drawn 5x5 wedge that matched
 * nothing else in the panel and read as a different icon set from the model
 * card's. NAI draws one 20x20 svg in a 36x36 8px-padded container, so on a 44px
 * control it centres 18px in from the right edge; mask-image lets the real asset
 * do the work and inherit currentColor like every other icon here.
 *
 * The separator is the PAGE tone (rgb(19,21,44)) — react-select's
 * indicatorSeparator at x=221, which against the input fill is all but
 * invisible. It was drawn in the raised tone "so it reads", which is exactly the
 * visible line NAI does not have. */
.select-control {
  flex: 1 1 auto;
  min-width: 0;
  height: 44px;
  padding: 0 44px 0 var(--nai-space-2);
  box-sizing: border-box;
  background-color: var(--nai-bg-page);
  background-image:
    /* chevron: a STROKED v with rounded caps, not a filled wedge. react-select
       renders its own inline <svg> here, so this glyph never appeared in the
       mask sweep and both earlier attempts — CSS gradient triangles, then the
       solid directional_arrow_down — drew the wrong shape. Traced from
       settings-panel.png at 12x rather than reused from the arrow set, because
       NAI genuinely uses two different chevrons: this one on dropdown faces and
       the solid wedge on the model card's indicator. */
    url('../icons/chevron-down.svg'),
    /* 1x26 separator, page tone — present but near-invisible, as measured */
    linear-gradient(var(--nai-bg-page), var(--nai-bg-page));
  background-position:
    calc(100% - 18px) 50%,
    calc(100% - 37px) 50%;
  background-size: 20px 20px, 1px 26px;
  background-repeat: no-repeat;
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  appearance: none;
}

/* The positioning context for a scripted picker's pop-out. It takes the flex
   slot the bare <select> used to hold, so the row's proportions are unchanged;
   the control inside then fills it. Not `overflow: hidden` — the menu is
   deliberately allowed to escape this box upward. */
.select-field {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
}

/* The div-based face of a scripted picker. Identical to the <select> above
   except for the bits a div doesn't get for free: vertical centring of the
   label, and text that doesn't turn into a selection drag on click. */
.select-control--button {
  display: flex;
  align-items: center;
  width: 100%;
  cursor: pointer;
  user-select: none;
}
/* Long labels ellipsize rather than sliding under the chevron — the padding on
   .select-control already reserves the indicator's 44px. */
.select-control__label {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* Same reasoning as .model-card: NAI paints no focus ring anywhere in the
   captured panel, and a tabindex div otherwise picks up the UA default. */
.select-control--button:focus,
.select-control--button:focus-visible {
  outline: none;
}
/* Three segments carrying their own edges rather than one bordered wrapper:
   65x44 input (rounded left, no right edge), 16x44 swap button (top/bottom
   only), 65x44 input (rounded right). The outline runs through all three as one
   shape — measured at x=269, 334 and 350. */
.dimension-box {
  display: flex;
  align-items: stretch;
  flex: 0 0 auto;
}
/* Room for 4 digits. `ch` is too narrow in Source Sans Pro once the spinner
   arrows take their cut, so the arrows go and the width is explicit. */
.dimension-box__input {
  width: 65px;
  height: 44px;
  min-width: 0;
  padding: 0;
  appearance: textfield;
  -moz-appearance: textfield;
  background-color: var(--nai-bg-input);
  border: var(--nai-border);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  text-align: center;
}
/* First segment: rounded left; its right edge belongs to the swap button. */
.dimension-box__input:first-child {
  border-right: 0;
  border-radius: var(--nai-radius) 0 0 var(--nai-radius);
}
/* Last segment: rounded right; its left edge is already drawn. */
.dimension-box__input:last-child {
  border-left: 0;
  border-radius: 0 var(--nai-radius) var(--nai-radius) 0;
}
/* Top and bottom only, so the shared outline passes through uninterrupted.
   0.9 to match the inputs on either side — measured on the button in
   dropdown-resolution.json, which reads `opacity: 0.9` on all three segments. */
.dimension-box__swap {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  padding: 0;
  background-color: var(--nai-bg-input);
  border: 0;
  border-top: var(--nai-border);
  border-bottom: var(--nai-border);
  color: var(--nai-text);
  opacity: 0.9;
  cursor: pointer;
}
.dimension-box__swap:hover {
  background-color: var(--nai-bg-raised);
  opacity: 1;
}
/* 16x12 box; `contain` fits the square 14x14 asset as a 12x12 X, which is
   exactly the ink extent measured off dropdown-resolution.png (336..347 x
   732..743). The glyph itself is full-strength white — the 0.9 dimming NAI
   applies lives on the BUTTON, alongside the two number inputs which carry the
   same 0.9. Dimming the icon to 0.7 here instead made the × noticeably fainter
   than the digits it sits between. */
.dimension-box__swap .icon {
  width: 16px;
  height: 12px;
}

/* Dropdown menus.
 *
 * NAI uses react-select for the model, resolution and sampler pickers — all
 * three are the same widget, so these values (measured from the open resolution
 * menu) apply to every one of them. Options are 225x40 with 8px 12px padding;
 * the selected row is solid page-blue, hover the same at 0.6 alpha.
 *
 * The model and resolution pickers are scripted menus using the listbox below,
 * because their rows carry group headers (and, for model, a description line)
 * that an <option> cannot render. Sampler remains a native <select>, which the
 * browser draws with OS chrome.
 */
.listbox {
  position: absolute;
  z-index: 10;
  min-width: 225px;
  max-height: 320px;
  overflow-y: auto;
  padding: 0;
  margin: 0;
  background-color: var(--nai-bg-panel);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  list-style: none;
}

/* Model menu. Opens flush under the card, matching the card's width rather than
   the 225px minimum the narrower pickers use, and overhanging the section border
   below.

   Width is inherited from the section's content box instead of a literal, so it
   tracks the card: hardcoding the measured 395px left the menu 14px narrower
   than the card at this panel width, with the mismatch visible down the right
   edge. The card is `width: 100%` of the same box, so this is the same number by
   construction. */
.listbox--model {
  top: calc(40px + 74px);
  left: var(--nai-panel-inset);
  right: var(--nai-panel-inset);
  width: auto;
  min-width: 0;
  /* The capture shows the list clipped mid-row rather than ending cleanly — it
     scrolls, and the cut row is the affordance saying so. */
  max-height: 400px;
}

.listbox__group-label {
  padding: var(--nai-space-1) 12px;
  color: var(--nai-text-muted);
  font-size: var(--nai-text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.listbox__option {
  padding: 8px 12px;
  font-size: var(--nai-text-base);
  line-height: var(--nai-line-base);
  cursor: pointer;
}
.listbox__option:hover {
  background-color: rgba(19, 21, 44, 0.6);
}
.listbox__option[aria-selected="true"] {
  background-color: var(--nai-bg-page);
}
/* Keyboard cursor. Distinct from :hover so arrowing through the list is visible
   without a mouse, and distinct from aria-selected so "focused" and "current
   model" can be different rows — which they are the moment you open the menu and
   press Down. */
.listbox__option--active {
  background-color: rgba(19, 21, 44, 0.6);
}

/* Model rows. Two lines — the name in display type over a description in body
   type — inset to x=30 in the capture against the list's own edge at x=20, i.e.
   10px more than the 12px the single-line pickers use. */
.listbox--model .listbox__option {
  padding: 10px 12px 10px 22px;
}
/* The selected row keeps its title in the accent tone, which is how the capture
   distinguishes it beyond the fill alone. */
.listbox--model .listbox__option[aria-selected="true"] .name {
  color: var(--nai-accent);
}
/* Model options carry a description line under the name. */
.listbox__option .name {
  display: block;
  font-family: var(--nai-font-display);
  font-size: var(--nai-text-sm);
  font-weight: 700;
  line-height: var(--nai-line-sm);
}
.listbox__option .desc {
  display: block;
  color: var(--nai-text-muted);
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
}

/* Resolution menu. Measured from out/components/resolution.json.
 *
 * It opens UPWARD. The capture reads `top: -270px; bottom: 44px` against a 44px
 * control, i.e. the panel's foot sits on the control's top edge — anchoring it
 * below instead puts every row 270px off and, at this control's position near
 * the bottom of a rail that never scrolls, off-screen entirely.
 *
 * Expressed as `bottom: 100%` rather than the literal -270px so it stays pinned
 * if the height below ever changes; against a 44px control inside .select-field
 * the two are the same number.
 *
 * 239.44px wide against the control's own width: the menu is NARROWER than the
 * 44px-padded control it hangs off, so it takes its measured width rather than
 * inheriting.
 *
 * The height is a hard 270px while the content measures 665px — the panel
 * scrolls internally and must NOT be sized to fit. `max-height` alone would
 * collapse it to content when a future edit shortens the list, so both are set.
 */
.listbox--resolution {
  top: auto;
  bottom: 100%;
  left: 0;
  width: 239.438px;
  min-width: 0;
  height: 270px;
  max-height: 270px;
  /* No border: the capture draws the edge as the first of two shadows — a 1px
     spread ring in the divider tone — over an 11px ambient drop. A real border
     would sit inside the 270px and cost two rows of a pixel each. */
  border: 0;
  box-shadow:
    rgba(59, 64, 109, 0.6) 0 0 0 1px,
    rgba(0, 0, 0, 0.2) 0 4px 11px 0;
}

/* 18px headers with a 3px gap to the first row beneath, inset 12px to share the
   options' left edge. Sentence case, unlike the model menu's uppercase NEW /
   LEGACY — "Wallpaper" reads as written in the capture. */
.listbox--resolution .listbox__group-label {
  height: 18px;
  padding: 0 12px;
  margin: 0 0 3px;
  box-sizing: border-box;
  color: rgba(255, 255, 255, 0.6);
  font-size: 12px;
  font-weight: 500;
  line-height: 18px;
  text-transform: none;
  letter-spacing: normal;
  cursor: default;
}

/* Uniform 40px rows — 8px 12px around a 24px line. Fixed rather than implied by
   the padding so "Custom", which carries no dimensions, is the same height as
   every other row instead of shrinking. */
.listbox--resolution .listbox__option {
  height: 40px;
  padding: 8px 12px;
  box-sizing: border-box;
  line-height: var(--nai-line-base);
}

/* Row states, read from the three emotion classes the capture happened to catch
   in one shot: the selected row (css-aaqgm9) is the TRANSLUCENT page tone and
   the hovered row (css-54aenv) is the SOLID one — the opposite pairing to the
   shared rules above, which were inferred from the model menu. Scoped here
   rather than flipped globally, since the model capture wasn't re-checked.
   Hover comes after selected so pointing at the current row still lights it. */
.listbox--resolution .listbox__option[aria-selected="true"] {
  background-color: rgba(19, 21, 44, 0.6);
}
.listbox--resolution .listbox__option--active,
.listbox--resolution .listbox__option:hover {
  background-color: rgb(19, 21, 44);
}

/* AI Settings subpanel.
 *
 * NAI pairs each numeric with a range slider — the number input is a compact
 * inline readout, not the primary control, and it never shows spinner arrows.
 */
.ai-settings {
  display: flex;
  flex-direction: column;
  gap: var(--nai-space-2);
  /* Inset only; the footer supplies the gap above Generate. */
  margin: 0 var(--nai-panel-inset);
}

/* 409x67 bordered bar. It is the collapse control AND the summary readout, so
   the whole thing is one button rather than a heading with a chevron beside it. */
.ai-settings__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--nai-space-2) var(--nai-space-3);
  background: none;
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  text-align: left;
  cursor: pointer;
}
.ai-settings__head:hover {
  background-color: var(--nai-bg-panel);
}
/* While the panel is open its contents show every value in full, so the summary
   bar removes itself instead of restating them. The collapse control moves into
   the expanded body (.ai-settings__close). */
.ai-settings__head--expanded {
  display: none;
}

/* Four label-over-value pairs: 14px at 0.7 above 16px bold. */
.ai-glance {
  display: flex;
  align-items: flex-start;
  gap: var(--nai-space-3);
  min-width: 0;
}
.ai-glance__item {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.ai-glance__label {
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
  opacity: 0.7;
}
.ai-glance__value {
  font-weight: 600;
  line-height: var(--nai-line-base);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.section-heading--inline {
  margin: 0;
}

.icon-button--bare {
  min-width: 0;
  height: auto;
  padding: 4px;
  background: none;
  border: 0;
  opacity: 0.7;
}
.icon-button--bare:hover {
  background: none;
  opacity: 1;
}

.ai-settings__body {
  display: flex;
  flex-direction: column;
  gap: var(--nai-space-2);
}
/* Heading row inside the expanded panel, carrying the collapse control. */
.ai-settings__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.ai-settings__body[hidden] {
  display: none;
}

.icon--flipped {
  transform: rotate(180deg);
}

/* Sub-collapse inside AI Settings — a plain text button, not a card. */
.advanced-toggle {
  display: flex;
  align-items: center;
  gap: var(--nai-space-2);
  padding: 0;
  background: none;
  border: 0;
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-sm);
  font-weight: 400;
  cursor: pointer;
}
.advanced-body {
  display: flex;
  flex-direction: column;
  gap: var(--nai-space-2);
}
.advanced-body[hidden] {
  display: none;
}

/* Label and value share one line — "Steps: 28" — with the slider beneath.
   An earlier version stacked the number above the track, which read wrong. */
.slider-field {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* No gap — the 0.5ch lives on the label's margin-right, as in the live markup.
   Both would double the spacing. */
.slider-field__row {
  display: flex;
  align-items: baseline;
  padding: 0 var(--nai-panel-inset);
}
/* 16px/600/24px at full opacity, 0.5ch to the readout — measured from
   ai-settings.json. Was 14px at 0.7, which made the whole row read too quiet
   and too short (29px row height in NAI, not 24). */
.slider-field__label {
  margin-right: 0.5ch;
  font-size: var(--nai-text-base);
  font-weight: 600;
  line-height: var(--nai-line-base);
}
/* Sized to its content — NAI sets width inline per value (2.3ch for "28",
   wider for "5.8"), so the readout hugs the digits instead of reserving a fixed
   column. `--digits` is kept in sync from app.js. */
.slider-field__number {
  /* NAI sets this inline per value: 2.3ch for "23", 1.3ch for "5" — i.e. one
     ch per digit plus 0.3ch of slack. 16px/700, matching the measurement. */
  width: calc(var(--digits, 2) * 1ch + 0.3ch);
  padding: 0;
  appearance: textfield;
  -moz-appearance: textfield;
  background: none;
  border: 0;
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  font-weight: 700;
  line-height: var(--nai-line-base);
}
.slider-field__number::-webkit-outer-spin-button,
.slider-field__number::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}

/* Spinner arrows are browser chrome NAI never shows — strip them everywhere
   rather than per-field, or they reappear on each new numeric input. */
input[type='number'] {
  appearance: textfield;
  -moz-appearance: textfield;
}
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

/* Range slider ("thick-slider" in NAI's markup): a chunky recessed track with
   a slim upright thumb, not the hairline-plus-dot the browser default gives. */
/* The filled portion is a gradient driven by --fill (a percentage set from JS
   on input). WebKit has no ::-moz-range-progress equivalent, so a background
   gradient is the only approach that looks identical in both engines. */
/* 22px bordered track on the input fill, filled left-to-right with the accent.
   NAI draws NO thumb — the boundary between filled and unfilled IS the handle,
   which is why the whole control is tall enough to read as a bar rather than a
   line. An earlier version was 14px with a visible 8x12 thumb.
   Source: the live rule for `.slider.thick-slider`. */
/* 377x22 bordered track with a FLAT dark fill and a raised-tone thumb.
 *
 * There is no accent fill: pixel-sampling the captured panel across the whole
 * track returns rgb(14,15,33) either side of a single rgb(34,37,63) block, and
 * no element anywhere in the panel carries the accent as a background. Earlier
 * versions painted a bright yellow gradient, which was invented — that is why
 * the sliders kept reading wrong no matter how the height was adjusted. */
.slider-field__range {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 22px;
  padding: 0;
  margin: 0 0 8px;
  box-sizing: border-box;
  background: var(--nai-bg-input);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  outline: none;
  cursor: pointer;
}
/* The thumb is a solid block in the raised tone, full track height, square. */
.slider-field__range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 12px;
  height: 20px;
  background: var(--nai-bg-raised);
  border: 0;
  border-radius: 0;
  cursor: pointer;
}
.slider-field__range::-moz-range-thumb {
  width: 12px;
  height: 20px;
  background: var(--nai-bg-raised);
  border: 0;
  border-radius: 0;
  cursor: pointer;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}
.field--grow {
  flex: 1 1 auto;
}
.text-control {
  width: 100%;
  padding: var(--nai-space-2) var(--nai-space-3);
  box-sizing: border-box;
  background-color: var(--nai-bg-input);
  border: var(--nai-border);
  border-radius: var(--nai-radius);
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
}

/* Steps / Guidance / Seed / Sampler readout row — labels are muted 0.7. */
.param-row {
  display: flex;
  align-items: flex-start;
  gap: var(--nai-space-4);
  margin: var(--nai-space-3) var(--nai-panel-inset) 0;
  padding: var(--nai-space-2) var(--nai-space-3);
  background-color: var(--nai-bg-input);
  border-radius: var(--nai-radius);
}
.param {
  display: flex;
  flex-direction: column;
}
.param__label {
  color: var(--nai-text-muted);
  font-size: var(--nai-text-sm);
  line-height: var(--nai-line-sm);
}
.param__value {
  font-size: var(--nai-text-base);
  font-weight: 600;
  line-height: var(--nai-line-base);
}
/* Seed can be 10 digits, guidance "4.6", steps 2 — 4ch clipped "4.6" to "4.(".
   Sized to content so each field only takes what it needs. */
.param input {
  width: auto;
  min-width: 3ch;
  field-sizing: content;
  background: none;
  border: 0;
  color: var(--nai-text);
  font-family: inherit;
  font-size: var(--nai-text-base);
  font-weight: 600;
}

/* Generate button — 409x44, the one strong accent in the UI.
   `margin-top: auto` pins it to the bottom of the rail's flex column, so it
   stays put whether the panel is sparse or scrolled full. */
.generate-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: 0 0 auto;
  /* Inset only — the footer owns the vertical rhythm. `margin-top: auto` here
     was left over from when this was pinned directly by the panel's flex
     column; combined with the footer gap it pushed the whole group upward. */
  margin: 0 var(--nai-panel-inset);
  height: 44px;
  /* Asymmetric, as measured: the label sits 20px in, the cost badge 10px from
     the right edge. Even padding pushed the badge too far inboard. */
  padding: var(--nai-space-2) var(--nai-space-2) var(--nai-space-2) var(--nai-space-4);
  box-sizing: border-box;
  background-color: var(--nai-accent);
  border: 0;
  border-radius: var(--nai-radius);
  color: var(--nai-bg-page);
  font-family: inherit;
  /* 14px/700/21px — measured. This was 16px/600/24px, so the button's whole
     line box was 3px taller than NAI's and pushed the footer out of place. */
  font-size: var(--nai-text-sm);
  font-weight: 700;
  line-height: var(--nai-line-sm);
  cursor: pointer;
  /* Copied verbatim from the live button's computed style. `opacity` is
     deliberately NOT in this list — the busy dim snaps rather than fades, which
     a rAF sample through a real click confirms (opacity holds 1 for the whole
     transform run, then flips). Only `transform` is actually exercised here;
     colour is carried along because NAI declares it. */
  transition: color 0.1s ease, background-color 0.1s ease, transform 0.15s ease;
}
.generate-button:hover:not(:disabled) {
  filter: brightness(1.05);
}
/* The click bounce — shrink in, then back out.
 *
 * Two phases, and the second is the easy one to get wrong. On click the button
 * eases down to scale(0.98); once it actually enters the disabled state it
 * eases back to 1. It does NOT stay shrunk for the generation.
 *
 * So the shrink cannot live on :disabled — that is the state it returns to
 * full size in. It rides a transient class that app.js adds on click and drops
 * when the job is confirmed, at which point this rule stops matching and the
 * base transform (none) takes over, easing back up over the same 0.15s.
 *
 * Scale measured off the live button by rAF sampling a real click:
 * 1 -> 0.999 -> 0.996 -> 0.991 -> 0.987 -> 0.984 across the first 74ms, a
 * decelerating curve landing on ~0.98 — about 8px off a 409px width. */
.generate-button--pressed {
  transform: scale(0.98);
}

/* Busy state — measured off a real generation, `frames[]` in
   out/components/generate-state.json (busy frame at t+14ms vs idle at t+5988ms).
   The button keeps its label, its 409x44 box, its accent fill, its 14px/700
   text and its full size; what changes is the dim.

   `cursor` deliberately stays `pointer` — NAI does NOT switch it to `default`.

   NAI also sets `pointer-events: none` here. It is deliberately not copied:
   with the button unhoverable the transform springs back mid-transition, and
   `disabled` already blocks activation on its own. The hover brighten is scoped
   with :not(:disabled) above instead.

   The dim is on the button itself here, not on a wrapper row the way NAI dims
   settings rows elsewhere — worth stating because that convention is common
   enough in this UI to assume by mistake. */
.generate-button:disabled {
  opacity: 0.5;
}
/* 50x27 dark badge on the accent button. It sets its own colour so the anla
   glyph inside picks up accent via currentColor, not the button's dark text. */
.generate-button__cost {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-width: 50px;
  height: 27px;
  padding: 0 var(--nai-space-1);
  box-sizing: border-box;
  background-color: var(--nai-bg-page);
  border-radius: var(--nai-radius);
  color: var(--nai-accent);
  /* 14px/700/21px, matching the button label beside it. */
  font-size: var(--nai-text-sm);
  font-weight: 700;
  line-height: var(--nai-line-sm);
}
