/* ============================================================================
   GRAPHITE — the Abney design language, v3.2 "one kit, one grammar".
   Modern greyscale on thick liquid glass: near-black ground, heavy-blur
   saturated glass islands floating over the photographs. Controls are
   capsules; related actions share one glass group; panels carry a light
   rim on the top edge (the refraction line). Geist Sans speaks, Geist Mono
   records. Photographs are the only color; the UI never competes.
   Selection, guides, and primary actions are WHITE — decision reads as light,
   not as a hue. (v2 "print-flat" retired; the crop tool keeps its flat,
   sharp-cornered marks on purpose — it is an instrument, not chrome.)

   v3.2 is the same look, finished as a system. Every primitive is defined ONCE,
   in the SYSTEM RECIPES section, and each component below only says how it
   differs. The shared primitives:
     · surface ladder   island bar → menu → card → dialog → sheet
     · option row       hairline-topped 34px row: label left, control right
     · field            an inset well (--well, hairline, 8px corners)
     · range            2px track, 11px round thumb, everywhere identical
     · label            mono 10.5px uppercase, faint
     · small button     .btn-sm inside rows and card heads
     · slider row       label + value over a full-width track  (THE CONTROL KIT)
     · picker           pick-one-of-N: capsule | tiles | list   (THE CONTROL KIT)
   New components must reuse these; a new one-off variant is a bug.

   TWO RULES v3.2 exists to enforce, each one a bug that shipped:
     · --well is the FIELD material. A control never wears it. Interaction always
       answers by getting LIGHTER (rest → --hover → --on). The Fit tiles and the
       active channel chip were darker than the card they sat on, and read as
       holes — this is what "the buttons are black" meant.
     · "Pick one of N" is ONE component (src/controls.js). It was six, each with
       its own active state, each chosen by hand against the card width. The
       options decide the shape now, not the author.
   ========================================================================== */

@font-face { font-family: 'Geist Sans'; src: url('../fonts/geist-sans-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Geist Sans'; src: url('../fonts/geist-sans-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }
@font-face { font-family: 'Geist Sans'; src: url('../fonts/geist-sans-600.woff2') format('woff2'); font-weight: 600; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('../fonts/geist-mono-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('../fonts/geist-mono-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }
/* Redaction — the book/zine caption face (files shipped since v1 "Proof") */
@font-face { font-family: 'Redaction'; src: url('../fonts/Redaction-Regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Redaction'; src: url('../fonts/Redaction-Italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'Redaction'; src: url('../fonts/Redaction-Bold.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; }
/* The wordmark is not a font. It is five glyphs that never change, so it ships
   as outlines (design/wordmark.svg) rather than as a face nobody else uses:
   no font file, no FOUT, and it cannot be re-set by accident. Drawn from Jost*
   ExtraBold Italic (OFL, a Futura revival), sheared to a 12deg oblique. */

:root {
  /* greyscale ramp */
  --ground: #0a0a0a;
  /* Glass stays light and translucent (the v3 look). backdrop-filter blurs and
     saturates but never DARKENS, so legibility over a BRIGHT photo is handled
     two other ways, not by making the panel opaque: a brightness() clamp in
     --blur tames blown-out highlights, and --glass-ink-shadow gives glass text
     a dark halo that only shows through where the panel goes light. */
  --panel: rgba(20, 20, 22, 0.6);           /* floating glass: bar, menus, HUDs */
  --panel-thick: rgba(24, 24, 26, 0.8);     /* thick glass: cards, dialogs, the gallery */
  --panel-solid: #141414;
  --well: #050505;                          /* inset fields sunk INTO a panel */
  --ink: #ededed;
  --ink-dim: #a1a1a1;
  --ink-faint: #858585;     /* lifted from #6f6f6f: small mono labels/counts need
                               to clear AA on light glass, not whisper */
  --line: rgba(255, 255, 255, 0.1);
  --line-strong: rgba(255, 255, 255, 0.22);

  /* interaction ramp: three deliberate steps of white, nothing in between.
     rest → hover (a breath) → hover on a lifted surface / pressed intent →
     active (the selected state). Every .active in the UI is --on. */
  --hover: rgba(255, 255, 255, 0.07);
  --hover-2: rgba(255, 255, 255, 0.11);
  --on: rgba(255, 255, 255, 0.12);

  /* decision = light */
  --accent: #ededed;        /* selection borders, guides, active markers */
  --accent-ink: #0a0a0a;    /* text on accent */
  /* status hues. Each is a triple — the hue, a wash to lay over glass, a rim —
     so a status surface never has to unroll an rgba() of its own. */
  --warn: #d9b13b;          /* system warnings only (throttling, battery) */
  --warn-wash: rgba(217, 177, 59, 0.12);
  --warn-line: rgba(217, 177, 59, 0.45);
  --danger: #dd7873;        /* the one permitted red: destructive actions, and failures */
  --danger-wash: rgba(221, 120, 115, 0.12);
  --danger-line: rgba(221, 120, 115, 0.45);
  --focus-ring: 2px solid rgba(237, 237, 237, 0.4);   /* keyboard focus, all chrome */

  --sans: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* type scale — six sizes, no more. Mono records, sans speaks.
     micro  10.5 mono   labels, section titles, chips, counts
     mono   11   mono   values, filenames, metadata
     note   11.5 sans   hints, secondary copy, small buttons
     ui     12.5 sans   buttons, row labels, body copy in chrome
     body   13   sans   menu items, dialogs, empty states
     head   15   sans   dialog titles */
  --fs-micro: 10.5px;
  --fs-mono: 11px;
  --fs-note: 11.5px;
  --fs-ui: 12.5px;
  --fs-body: 13px;
  --fs-head: 15px;

  /* glass recipe: heavy blur + saturation behind, light rim on the top edge.
     brightness(0.75) only meaningfully darkens near-white backdrops — mid-tone
     and dark photos pass through almost untouched, so the glass keeps its light,
     translucent character while a blown-out highlight can't wash the panel out. */
  --blur: blur(36px) saturate(1.8) brightness(0.75);
  --rim: inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 0 0.5px rgba(255, 255, 255, 0.04);
  --shadow: var(--rim), 0 2px 8px rgba(0, 0, 0, 0.28), 0 16px 48px rgba(0, 0, 0, 0.45);
  /* dark halo for text on translucent glass: near-invisible over dark content,
     but keeps light ink readable where a bright photo shows through the panel.
     Applied to the glass containers below — NOT to book pages (literal paper). */
  --glass-ink-shadow: 0 1px 2px rgba(0, 0, 0, 0.55), 0 0 5px rgba(0, 0, 0, 0.4);

  /* radius ladder — each size is a ROLE, not a choice:
     xs 6   kbd keycaps, tiny wells, marquee
     sm 8   inset fields, thumbs, histograms — anything sunk into a panel
     md 10  interior items: menu rows, tiles, chips that aren't pills
     lg 16  floating panels: menus, cards
     xl 22  dialogs, the gallery sheet
     pill   capsule controls and bars */
  --radius-xs: 6px;
  --radius-sm: 8px;
  --radius: 10px;
  --radius-lg: 16px;
  --radius-xl: 22px;
  --pill: 999px;

  /* chrome metrics */
  --edge: 10px;             /* inset of floating chrome from the viewport */
  --bar-h: 46px;
  --row-h: 34px;            /* one option row (label + control) */
  --card-w: 316px;          /* the right-column cards */
  --card-w-sm: 264px;       /* the narrow text card */
  --card-pad: 14px 16px 16px;
  --drawer-h: 150px;          /* set to --drawer-collapsed by JS when tray is hidden */
  --drawer-collapsed: 50px;

  /* z ladder (documentation, not tokens — values live on the components):
     5–9    on-desk marks: beta note, book layer, guides, handles, hint tags
     10–16  floating chrome: arrange bar, gallery sheet, bar islands, crop bar
     21–27  cards, rail, HUDs, toasts
     28–30  transient popups: menus, drag ghosts
     31–35  lightbox, flip preview, save banner, dialogs
     40+    whole-screen states: dropzone, fatal, GPU loss */
}

/* ============================================================================
   SYSTEM RECIPES — every shared primitive, defined once.
   ========================================================================== */

/* Glass legibility: every translucent glass panel gives its text a soft dark
   halo (inherited by descendants). Over dark content it's invisible; over a
   bright photo showing through the light glass it's what keeps the ink readable.
   The book layer is deliberately absent — its pages are literal paper with dark
   ink, where a dark halo would only muddy the type. */
.island, .menu, .arrange-bar, .lb-actions, .cropbar, #railMenu, #drawer,
#infocard, #bookcard, #booktextcard, #editcard, #tourcard, .dialog, #keyhud,
.warnchip, .toast, #bkHint, .tour-tag {
  text-shadow: var(--glass-ink-shadow);
}

/* Keyboard focus: one ring for every piece of chrome. Pointer clicks don't
   show it (focus-visible); the sliders' thumb glow lives with the range recipe. */
:is(.btn, .seg-btn, .menu-item, .menubtn, .checkfilter, .rail-btn, .arrange-btn,
.tour-navbtn, .drawer-place, .ctl-opt, .ctl-val, .ctl-swap, .paper-opt,
.bk-acc-head, .helpbtn, .bk-swatch, .menu-swatch, .pj-main, .pj-icon, .toast-x,
.lb-star, .lb-nav, .bk-tool, .ft-x):focus-visible {
  outline: var(--focus-ring);
  outline-offset: 1px;
}

/* ---- the label: mono, micro, uppercase — what the system says quietly ----
   One voice for section titles, menu group headings, roll dates, and tags. */
.label, .menu-label, .ic-title, .ctl-title, .ps-title, .pj-side-title, .roll-label,
.tour-kicker, .ft-tag {
  font: 500 var(--fs-micro)/1 var(--mono);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.label { font-size: var(--fs-mono); color: var(--ink-dim); }
/* Section title inside a card or dialog. It needs real air above it — a new
   section must read as a new thought, not as one more row — and it is itself
   the section's separator, so the first row under it does NOT draw a hairline
   too (see below). Hairlines divide rows WITHIN a section; titles divide
   sections. */
/* THE TWO LABEL ROLES. This is the whole of the card's typographic hierarchy,
   and getting it wrong is what made the book card read as one long list:

     .ctl-title   MONO, UPPERCASE   a GROUP of settings — a section, or a
                                    collapsible group head (Margins, Print)
     .ctl-label   sentence sans     ONE setting, even when its control is too
                                    wide to share the row (Fit, Binding, Trim)

   Margins and Print are groups of five and four settings; they were dressed as
   rows, which is why the capitalization looked arbitrary. A group is uppercase
   whether or not it collapses.

   A section title is also the SEPARATOR: it carries real air above it (a new
   section is a new thought), and the first row under it drops its hairline. */
.ic-title, .ctl-title { padding: 26px 0 9px; }
.ic-title:first-child, .ctl-title:first-child { padding-top: 0; }
.menu-label { padding: 6px 10px 5px; }
.ps-title { margin: 22px 0 2px; }
.pj-side-title { margin: 0; }

/* The title is the separator: whatever opens the section drops its top hairline
   (a row, an accordion, a note, a tile grid's wrapper). */
:is(.ic-title, .ctl-title, .ps-title) + :is(.ic-row, .ctl-row, .ps-row, .bk-acc, .bk-note) {
  border-top: 0;
}

/* ---- buttons ---- */
.btn {
  appearance: none; border: 1px solid var(--line); border-radius: var(--pill);
  padding: 6px 14px; font: 500 var(--fs-ui)/1.2 var(--sans);
  background: var(--hover); color: var(--ink); cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.btn:hover { background: var(--hover-2); border-color: var(--line-strong); }
.btn:disabled { opacity: 0.35; cursor: default; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.btn-primary:hover { background: #fff; border-color: #fff; }
.btn-quiet { background: transparent; border-color: transparent; color: var(--ink-dim); }
.btn-quiet:hover { color: var(--ink); background: var(--hover); border-color: transparent; }
/* the small button: the only button allowed inside an option row or card head.
   The contextual selectors keep existing markup on the recipe.

   Weight, by rule: a row's real verb is a bordered .btn (+ Photo, + Text, move a
   page, Crop…). .btn-quiet is for dismissals and nothing else — it had drifted
   onto half the verbs in the book card, which is why a card of actions read as a
   card of hints. Destructive keeps .bk-danger and its distance. */
.btn-sm,
.bk-inline .btn, .ed-inline .btn, .ctl-row > .btn,
.bk-card-head .bk-close, .ed-head .ed-close, .ft-x {
  padding: 3px 10px; font-size: var(--fs-note);
}

kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 19px; height: 19px; padding: 0 5px;
  font: 500 var(--fs-mono)/1 var(--mono); color: var(--ink-dim);
  background: rgba(255, 255, 255, 0.08); border: 1px solid var(--line);
  border-radius: var(--radius-xs);
}

.warnchip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: var(--pill);
  border: 1px solid rgba(217, 177, 59, 0.35); background: rgba(217, 177, 59, 0.08);
  font: 500 var(--fs-mono)/1.2 var(--mono); color: var(--warn);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.warnchip[hidden] { display: none; }

/* ---- the option row: label left, control right, hairline above -------------
   THE unit of every card and settings surface. One height, one padding,
   whatever the surface (info card, book card, edit card, dialogs, fonts). */
.ic-row, .ctl-row, .ps-row, .ft-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 0; min-height: var(--row-h); border-top: 1px solid var(--line);
}
.ft-row:first-child { border-top: 0; }
/* our display:flex beats the UA's [hidden]{display:none} — restore it */
.ic-row[hidden], .ctl-row[hidden], .ps-row[hidden], .ft-row[hidden] { display: none; }
.ic-label, .ctl-label { font: 400 var(--fs-ui)/1.3 var(--sans); color: var(--ink-dim); }
.ic-value { font: 500 var(--fs-note)/1.3 var(--mono); color: var(--ink); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---- fields: inset wells sunk into a panel ----------------------------------
   Text inputs, selects, number cells, the feedback textarea. One material
   (--well), one hairline, one focus answer (the hairline strengthens). */
.ps-input, .ps-select, .ed-select, .bk-num-in, .bk-text-in, .pj-rename, .share-url, .fb-text {
  background: var(--well); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 7px 10px; font: 400 var(--fs-ui)/1.2 var(--sans);
}
.ps-input:focus, .ps-select:focus, .ed-select:focus, .bk-num-in:focus, .bk-text-in:focus,
.pj-rename:focus, .share-url:focus, .fb-text:focus {
  outline: none; border-color: var(--line-strong);
}
.ps-input, .ps-select { min-width: 200px; }
.ed-select { flex: 1; max-width: 150px; padding: 4px 8px; font-size: 12px; }
.bk-num-in { width: 52px; padding: 4px 7px; font: 500 var(--fs-note)/1.2 var(--mono); }
.bk-text-in { flex: 1; min-width: 0; padding: 4px 8px; font-size: 12px; }
.pj-rename { width: 160px; padding: 3px 7px; font-weight: 500; border-color: var(--line-strong); }
.share-url { flex: 1; min-width: 0; font: 400 12px/1.2 var(--mono); color: var(--ink-dim); padding: 7px 11px; }
.fb-text { width: 100%; min-height: 110px; margin-top: 12px; resize: vertical; font: 400 var(--fs-body)/1.5 var(--sans); padding: 9px 11px; }

/* ---- the range slider: 2px track, 11px round thumb — identical everywhere
   (tray zoom, control kit, project settings) ---- */
.thumb-size, .ctl-range, .ps-range {
  -webkit-appearance: none; appearance: none;
  height: 14px; background: transparent; cursor: pointer;
}
:is(.thumb-size, .ctl-range, .ps-range)::-webkit-slider-runnable-track {
  height: 2px; background: var(--line-strong); border-radius: 1px;
}
:is(.thumb-size, .ctl-range, .ps-range)::-webkit-slider-thumb {
  -webkit-appearance: none; width: 11px; height: 11px; margin-top: -4.5px;
  background: var(--ink); border: 0; border-radius: 50%;
}
:is(.thumb-size, .ctl-range, .ps-range):hover::-webkit-slider-thumb { background: #fff; }
:is(.thumb-size, .ctl-range, .ps-range):focus-visible { outline: none; }
:is(.thumb-size, .ctl-range, .ps-range):focus-visible::-webkit-slider-thumb {
  background: #fff; box-shadow: 0 0 0 3px rgba(237, 237, 237, 0.28);
}
.thumb-size { width: 120px; }
.ps-range { flex: 1; max-width: 180px; }
.ctl-range { width: 100%; display: block; }

/* ============================================================================
   THE CONTROL KIT (src/controls.js) — the slider row and the picker.
   Every adjustable value in every card is one of these two shapes. A new
   bespoke control is a bug; a new bespoke ACTIVE STATE is a worse one.
   ========================================================================== */

/* ---- the slider row: label + value above, a full-width track below ----------
   The track is the thing you scrub, so it gets the width. It used to be a fixed
   132px stub sharing a 34px line with an ellipsized label and a 10.5px number —
   40% of the row spent on the one control being manipulated, which is most of
   why the cards read as cramped.

   Sliders draw NO hairline: a run of them in one section is one group, and a
   rule between every pair turned the card into a data table. Hairlines divide
   rows within a section; titles divide sections; sliders divide nothing. */
.ctl-slider {
  flex-direction: column; align-items: stretch; justify-content: center; gap: 3px;
  border-top: 0; padding: 5px 0 8px; min-height: 0;
}
.ctl-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.ctl-slider .ctl-label {
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* the value: mono, tabular, and big enough to read while you are changing it
   (it was --fs-micro, the smallest size in the scale, for the one number the
   user is actively adjusting). Click it to type an exact one. */
.ctl-val {
  flex: 0 0 auto; min-width: 46px; text-align: right;
  font: 500 var(--fs-mono)/1.3 var(--mono); font-variant-numeric: tabular-nums;
  color: var(--ink-faint); cursor: text; border-radius: var(--radius-xs); padding: 1px 4px;
  transition: color 0.1s, background 0.1s;
}
/* The row under the pointer (or holding focus) is the row the arrows reach, so
   it says so: the value lights up. This is the ONLY hint the keyboard target
   gives, and without it the grammar is invisible. */
.ctl-slider.hot .ctl-val, .ctl-slider:focus-within .ctl-val { color: var(--ink); background: var(--hover); }
.ctl-val-edit {
  width: 100%; min-width: 46px; text-align: right;
  font: 500 var(--fs-mono)/1.3 var(--mono); color: var(--ink);
  background: var(--well); border: 1px solid var(--line-strong); border-radius: var(--radius-xs);
  padding: 0 3px;
}
.ctl-val-edit:focus { outline: none; border-color: var(--ink-faint); }
/* A PAIRED row: two parameters share one slot and the label swaps between them
   (Contrast ⇄ Negative, Print exposure ⇄ Preflash). The mark is quiet at rest —
   it is an offer, not a control competing with the value beside it — and lights
   with the label on hover. Clicking swaps; ⌥ is NOT the trigger, because ⌥ is
   already the fine tier and one key cannot be both a mode and a tier. */
.ctl-swap {
  appearance: none; background: none; border: 0; padding: 0; margin: 0;
  display: inline-flex; align-items: baseline; gap: 5px;
  cursor: pointer; text-align: left; border-radius: var(--radius-xs);
  transition: color 0.1s;
}
.ctl-swap:hover { color: var(--ink); }
.ctl-swap-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ctl-swap-mark {
  flex: 0 0 auto; font: 400 11px/1 var(--sans); color: var(--ink-faint);
  opacity: 0.55; transition: opacity 0.1s, color 0.1s;
}
.ctl-swap:hover .ctl-swap-mark, .ctl-slider.hot .ctl-swap-mark { opacity: 1; color: var(--ink-dim); }
.ctl-track { position: relative; display: flex; align-items: center; width: 100%; }
/* threshold ghost thumb (⌥-drag): a dimmer marker on the same track */
.ctl-thumb2 {
  position: absolute; top: 50%; width: 9px; height: 9px; margin-top: -4.5px; margin-left: -4.5px;
  border-radius: 50%; background: rgba(237, 237, 237, 0.38);
  border: 1px solid rgba(237, 237, 237, 0.55); pointer-events: none;
}
.ctl-thumb2[hidden] { display: none; }
/* asleep: the print sliders while no paper is chosen. Dimmed, and inert. */
.ctl-slider.off, .ctl-slider.disabled { opacity: 0.35; }
.ctl-slider.off .ctl-range, .ctl-slider.disabled .ctl-range,
.ctl-slider.off .ctl-val, .ctl-slider.disabled .ctl-val { pointer-events: none; }

/* ---- the picker: one control, three densities ------------------------------
   "Pick one of N" is ONE component. The shape follows from the options (hints →
   list, >3 with glyphs → tiles, else a capsule), and all three share one active
   state: the interaction ramp's --on.

   No picker uses --well. That material means "an inset field you type into"
   (see FIELDS above), and the Fit tiles and the active channel chip had borrowed
   it — making them the only controls in the app that answer interaction by going
   DARKER than the surface they sit on. They read as holes, which is exactly what
   "the buttons are black" meant. Every control here gets LIGHTER: rest → hover →
   on, and nothing else. */
.ctl-opt {
  appearance: none; cursor: pointer; text-align: left;
  transition: color 0.1s, background 0.1s, border-color 0.1s;
}
.ctl-opt:disabled { opacity: 0.38; cursor: default; }
.ctl-opt-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ctl-opt-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ctl-opt-hint { font: 400 var(--fs-micro)/1.35 var(--sans); color: var(--ink-faint); }
.ctl-opt .bk-glyph { width: 15px; height: 15px; margin-right: 0; flex: 0 0 auto; }
.ctl-opt.active .bk-glyph { color: inherit; }

/* seg — one glass capsule, up to three words (or four bare glyphs) */
.ctl-seg {
  display: inline-flex; gap: 2px; padding: 3px; flex-wrap: wrap; justify-content: flex-end;
  background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line); border-radius: var(--pill);
}
.ctl-seg .ctl-opt {
  display: inline-flex; align-items: center; gap: 6px;
  border: 0; border-radius: var(--pill); padding: 4px 11px;
  background: transparent; color: var(--ink-faint); font: 500 var(--fs-note)/1.2 var(--sans);
}
.ctl-seg .ctl-opt-text { flex-direction: row; align-items: center; gap: 6px; }
.ctl-seg .ctl-opt:hover:not(:disabled) { color: var(--ink-dim); }
.ctl-seg .ctl-opt.active { background: var(--on); color: var(--ink); }

/* icons — the same capsule, bare glyphs. For a group whose glyphs say it better
   than words (alignment). The word stays in the DOM as the button's accessible
   name; it is only the EYE that gets the glyph alone. Carrying both is what
   wraps a three-option pill into a cramped oval. */
.ctl-seg.ctl-icons .ctl-opt { padding: 5px 10px; }
.ctl-seg.ctl-icons .ctl-opt-text { display: none; }
.ctl-seg.ctl-icons .ctl-opt .bk-glyph { width: 14px; height: 14px; }

/* tiles — a grid, glyph beside the word, for four to six real choices */
.ctl-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; width: 100%; }
.ctl-tiles .ctl-opt {
  display: flex; align-items: center; gap: 8px; padding: 8px 10px; min-height: var(--row-h);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: transparent; color: var(--ink-dim); font: 500 var(--fs-ui)/1.2 var(--sans);
}
.ctl-tiles .ctl-opt:hover:not(:disabled) { background: var(--hover); color: var(--ink); border-color: var(--line-strong); }
.ctl-tiles .ctl-opt.active { background: var(--on); color: var(--ink); border-color: var(--line-strong); }

/* list — stacked, each option saying what you get */
.ctl-list { display: flex; flex-direction: column; gap: 2px; width: 100%; }
.ctl-list .ctl-opt {
  display: flex; align-items: center; gap: 9px; padding: 7px 9px;
  border: 1px solid transparent; border-radius: var(--radius);
  background: transparent; color: var(--ink-dim); font: 500 var(--fs-ui)/1.2 var(--sans);
}
.ctl-list .ctl-opt:hover:not(:disabled) { background: var(--hover); color: var(--ink); }
.ctl-list .ctl-opt.active { background: var(--on); color: var(--ink); border-color: var(--line); }

/* a row whose control needs the full width stacks it under the label */
.ctl-row.ctl-col { flex-direction: column; align-items: stretch; gap: 5px; }
.ctl-row.ctl-col > .ctl-label { display: flex; align-items: center; }
/* A mount point for a kit control inside a template. display: contents means the
   slot lays out as if it were not there, so a control reads as a direct child of
   the row it belongs to — and the card can still hide the whole thing by id. */
.ctl-slot { display: contents; }

/* ---- context menu (Figma-style): also the app menu, the tray thumb menu,
   and the RA-4 paper picker's shell ---- */
.menu {
  position: fixed; z-index: 28; min-width: 236px; padding: 6px;
  background: var(--panel); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}
.menu-item {
  display: flex; align-items: center; justify-content: space-between; gap: 28px;
  width: 100%; padding: 7px 10px; border: 0; border-radius: var(--radius);
  font: 400 var(--fs-body)/1.2 var(--sans); color: var(--ink); text-align: left;
  background: transparent; cursor: pointer;
}
.menu-item:hover { background: var(--hover-2); }
.menu-item:disabled { opacity: 0.35; cursor: default; }
.menu-item:disabled:hover { background: transparent; }
.menu-item kbd { background: transparent; border-color: transparent; color: var(--ink-faint); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover { background: var(--danger-wash); }
.menu-sep { height: 1px; background: var(--line); margin: 5px 8px; }
.menu-swatches { display: flex; gap: 6px; padding: 3px 10px 8px; }
.menu-swatch {
  appearance: none; width: 22px; height: 22px; border-radius: 50%;
  border: 1px solid var(--line-strong); cursor: pointer; padding: 0;
}
.menu-swatch:hover { border-color: var(--ink-dim); }
.menu-swatch.active { outline: 2px solid var(--ink); outline-offset: 1px; }

/* ---- segmented control: one glass capsule, several actions — the
   grouped-control shape used everywhere in v3. Inside the CARDS this shape is
   reached through the control kit's picker (.ctl-seg); what remains here serves
   the gallery tabs and the export dialog, which are not param surfaces. ---- */
.seg {
  display: inline-flex; gap: 2px; padding: 3px;
  background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line); border-radius: var(--pill);
}
.seg-btn {
  appearance: none; border: 0; border-radius: var(--pill); padding: 4px 12px;
  display: inline-flex; align-items: center; gap: 7px;
  font: 500 12px/1.2 var(--sans); background: transparent; color: var(--ink-faint); cursor: pointer;
  transition: color 0.1s, background 0.1s;
}
.seg-btn:hover { color: var(--ink-dim); }
.seg-btn.active { background: var(--on); color: var(--ink); }
.seg-btn:disabled { opacity: 0.35; cursor: default; }
.seg-btn:disabled:hover { color: var(--ink-faint); background: transparent; }
/* Count = a small legible pill, not a faint whisper next to the label. */
.seg-btn .count {
  font: 500 var(--fs-micro)/1 var(--mono); color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  padding: 2px 5px; border-radius: var(--pill); background: rgba(255, 255, 255, 0.07);
}
.seg-btn.active .count { color: var(--ink); background: rgba(255, 255, 255, 0.16); }

/* ---- checkbox filter (Tray: reveal the omitted photos) ----
   A labelled checkbox, not an icon toggle: the term "omitted" is spelled out,
   and the filled/empty box says unambiguously whether they're included. */
.checkfilter {
  appearance: none; display: inline-flex; align-items: center; gap: 8px;
  margin-left: 14px; padding: 5px 12px 5px 8px;
  border: 1px solid var(--line); border-radius: var(--pill);
  background: transparent; color: var(--ink-dim); font: 500 var(--fs-note)/1.2 var(--sans); cursor: pointer;
  transition: color 0.1s, border-color 0.1s, background 0.1s;
}
.checkfilter:hover { border-color: var(--line-strong); color: var(--ink); background: var(--hover); }
.checkfilter .box {
  width: 15px; height: 15px; flex: 0 0 auto; border-radius: 5px;
  border: 1.5px solid var(--line-strong); color: var(--accent-ink);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background 0.1s, border-color 0.1s;
}
.checkfilter .box svg { width: 11px; height: 11px; opacity: 0; transition: opacity 0.1s; }
.checkfilter.on { color: var(--ink); }
.checkfilter.on .box { background: var(--accent); border-color: var(--accent); }
.checkfilter.on .box svg { opacity: 1; }
.checkfilter .count { font: 500 var(--fs-micro)/1 var(--mono); color: var(--ink-dim); font-variant-numeric: tabular-nums; }

/* ---- the glass card: every right-column card and the tour card --------------
   One material (thick glass), one radius, one padding, one width. The cards
   dock top-right left of the rail (layoutRightCards packs the open ones),
   scroll inside themselves on short windows, and can be torn off by their
   header. The tour card lives bottom-left instead. */
#infocard, #bookcard, #booktextcard, #editcard, #tourcard {
  position: fixed; z-index: 23;
  width: var(--card-w); padding: var(--card-pad);
  max-height: calc(100vh - var(--bar-h) - var(--drawer-collapsed) - 44px);
  overflow-y: auto;
  background: var(--panel-thick); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow);
}
/* The rail owns the right edge — cards dock left of it with a breathing gap.
   These `right` values are only the first-paint fallback: layoutRightCards()
   (ui.js) packs whichever cards are open side by side. */
#infocard, #bookcard, #booktextcard, #editcard {
  top: calc(var(--bar-h) + var(--edge) + 10px);
  right: calc(var(--edge) + 46px + 12px);
}
#booktextcard { width: var(--card-w-sm); }
#booktextcard #btFont { flex: 1; min-width: 0; }
#infocard[hidden], #bookcard[hidden], #booktextcard[hidden], #editcard[hidden] { display: none; }
/* card head: title · summary · close. The header is also the tear-off grip. */
.bk-card-head, .ed-head { display: flex; align-items: center; gap: 10px; padding-bottom: 4px; }
.bk-card-head .ic-title, .ed-head .ic-title { padding: 0; }
.bk-summary, .ed-summary { font: 400 var(--fs-mono)/1 var(--mono); color: var(--ink-faint); margin-left: auto; }
.ed-summary { max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* torn-off cards: the header is the grip */
.card-grip { cursor: grab; }
.card-dragging { opacity: 0.94; }
.card-dragging .card-grip { cursor: grabbing; }

/* ---- drag ghosts: the travelling thumbnail + count badge ---- */
.bk-ghost, .drag-ghost {
  position: fixed; z-index: 30; pointer-events: none; opacity: 0.85;
  transform: translate(-50%, -50%); border-radius: 3px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.drag-badge {
  position: fixed; z-index: 31; pointer-events: none;
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: var(--pill);
  background: var(--accent); color: var(--accent-ink);
  font: 600 12px/20px var(--sans); text-align: center;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5); transform: translate(6px, -50%);
}

/* ============================================================================
   COMPONENTS
   ========================================================================== */

/* selection resize handles (single selection, desk) */
.sel-handle {
  position: fixed; z-index: 9; width: 10px; height: 10px;
  background: var(--accent); border: 1px solid rgba(0, 0, 0, 0.55); border-radius: 50%;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  display: none;
}
.sel-handle[data-c="nw"], .sel-handle[data-c="se"] { cursor: nwse-resize; }
.sel-handle[data-c="ne"], .sel-handle[data-c="sw"] { cursor: nesw-resize; }

/* ---- swap handles: one pill per selected photo; grab and pull onto another
   selected photo to trade places (Figma smart-selection style) ---- */
.swap-handle {
  position: fixed; z-index: 9; width: 8px; height: 30px; padding: 0;
  appearance: none; border-radius: var(--pill);
  background: rgba(255, 255, 255, 0.26); border: 1px solid rgba(255, 255, 255, 0.4);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  box-shadow: none;
  cursor: grab;
  opacity: 0; pointer-events: none; /* revealed only while hovering the selection */
  transition: opacity 0.15s, width 0.08s, background 0.08s;
}
.swap-handle.show { opacity: 1; pointer-events: auto; }
.swap-handle:hover { width: 10px; background: rgba(255, 255, 255, 0.5); }
.swap-handle.dragging { cursor: grabbing; width: 10px; background: rgba(255, 255, 255, 0.7); }

/* ---- arrange toolbar: floats above a 2+ selection, one glass capsule ---- */
.arrange-bar {
  position: fixed; z-index: 10; display: flex; align-items: center; gap: 1px; padding: 4px;
  background: var(--panel); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--pill); box-shadow: var(--shadow);
  opacity: 0; pointer-events: none; transition: opacity 0.12s;
}
.arrange-bar.show { opacity: 1; pointer-events: auto; }
.arrange-bar .sep { width: 1px; height: 16px; background: var(--line); margin: 0 3px; }
.arrange-btn {
  appearance: none; border: 0; border-radius: 50%; padding: 0;
  width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
  background: transparent; color: var(--ink-dim); cursor: pointer;
}
.arrange-btn:hover { background: var(--hover); color: var(--ink); }
.arrange-btn svg { width: 14px; height: 14px; stroke-width: 1.4; }

/* ---- lightbox: look, don't move ---- */
.lb-backdrop { position: fixed; inset: 0; z-index: 31; background: rgba(5, 5, 5, 0.78); backdrop-filter: blur(22px) saturate(1.3); -webkit-backdrop-filter: blur(22px) saturate(1.3); }
.lightbox { position: fixed; inset: 0; z-index: 32; display: flex; flex-direction: column; padding: 14px 20px 18px; pointer-events: none; }
/* our display:flex beats the UA's [hidden]{display:none} — restore it, same as
   #infocard / #importOverlay above */
.lightbox[hidden], .lb-backdrop[hidden] { display: none; }
.lightbox > * { pointer-events: auto; }
.lb-top { display: flex; align-items: center; gap: 12px; }
.lb-name { font: 400 12px/1.2 var(--mono); color: var(--ink-dim); }
.lb-chip { font: 500 var(--fs-micro)/1 var(--mono); letter-spacing: 0.06em; text-transform: uppercase; padding: 4px 9px; border-radius: var(--pill); border: 1px solid var(--line-strong); color: var(--ink-dim); }
.lb-chip.desk { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.lb-chip.omitted { opacity: 0.6; }
.lb-counter { margin-left: auto; font: 400 var(--fs-mono)/1 var(--mono); color: var(--ink-faint); }
/* rating stars: clickable in the lightbox top row; keys 1–5 mirror them */
.lb-stars { display: inline-flex; gap: 1px; }
.lb-star {
  appearance: none; border: 0; background: transparent; padding: 2px 3px;
  font: 400 15px/1 var(--sans); color: var(--ink-faint); cursor: pointer;
  transition: color 0.1s, transform 0.06s;
}
.lb-star:hover { color: var(--ink); transform: scale(1.2); }
.lb-star.filled { color: var(--ink); }
.lb-stage { flex: 1; display: flex; align-items: center; justify-content: center; gap: 14px; min-height: 0; pointer-events: none; }
.lb-stage canvas { pointer-events: auto; background: var(--well); }
.lb-nav {
  pointer-events: auto; appearance: none; border: 1px solid var(--line); border-radius: var(--pill);
  width: 32px; height: 56px; background: var(--hover); color: var(--ink-dim);
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  font: 300 22px/1 var(--sans); cursor: pointer;
}
.lb-nav:hover { color: var(--ink); background: var(--hover-2); }
/* action row = one glass capsule; the buttons inside go borderless */
.lb-actions {
  display: flex; justify-content: center; gap: 2px;
  width: max-content; margin: 14px auto 0; padding: 4px;
  background: var(--panel); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--pill); box-shadow: var(--shadow);
}
.lb-actions .btn { border-color: transparent; background: transparent; }
.lb-actions .btn:hover { background: var(--hover); }
.lb-actions .btn-primary { background: var(--accent); color: var(--accent-ink); }
.lb-actions .btn-primary:hover { background: #fff; }
.lb-danger { color: var(--danger); }
.lb-danger:hover { border-color: rgba(221, 120, 115, 0.5); }

/* ---- drag-drop import target: lights up while OS files hover the window.
   pointer-events: none so the drop event still lands on the window. ---- */
#dropzone {
  position: fixed; inset: 10px; z-index: 45; pointer-events: none;
  display: flex; align-items: center; justify-content: center; text-align: center; padding: 40px;
  border: 2px dashed var(--line-strong); border-radius: var(--radius-xl);
  background: rgba(8, 8, 8, 0.62); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  font: 500 15px/1.5 var(--sans); color: var(--ink);
}
#dropzone[hidden] { display: none; }

/* ---- project settings form (inside the shared .dialog shell) ----
   Rows are the shared option row; only the dialog-specific bits live here. */
.ps-row > label:first-child { font: 500 var(--fs-ui)/1.3 var(--sans); color: var(--ink-dim); flex: 0 0 auto; }
.ps-hint { font: 400 12px/1.4 var(--sans); color: var(--ink-dim); }
.ps-check { width: 15px; height: 15px; accent-color: var(--accent); }
.ps-value { font: 400 var(--fs-mono)/1 var(--mono); color: var(--ink-faint); min-width: 52px; text-align: right; }

/* ---- projects & desks dialog (inside the shared .dialog shell) ----
   Two columns while the concept guide is showing (first run, or the "?"
   toggled on): guide left, lists and actions right. The everyday state is
   pj-compact: guide folded away, one column, and the dialog gives the freed
   width back instead of stretching the lists across it. */
.dialog.pj { width: 680px; max-height: 80vh; overflow-y: auto; }
.dialog.pj.pj-compact { width: 460px; }
.pj-cols { display: grid; grid-template-columns: 216px 1fr; gap: 22px; margin-top: 14px; }
.pj-cols.pj-one { grid-template-columns: 1fr; }
.pj-side { border-right: 1px solid var(--line); padding-right: 20px; }
.pj-fact { font: 400 12px/1.55 var(--sans); color: var(--ink-dim); margin-top: 11px; }
.pj-fact strong { color: var(--ink); font-weight: 600; }
.pj-fact.pj-how { color: var(--ink-faint); border-top: 1px solid var(--line); padding-top: 11px; margin-top: 14px; }
.pj-maincol { min-width: 0; }
.pj-maincol .ps-title:first-child { margin-top: 0; }
.pj-list { display: flex; flex-direction: column; margin-top: 2px; }
.pj-item { display: flex; align-items: center; gap: 2px; border-top: 1px solid var(--line); }
.pj-item:first-child { border-top: 0; }
.pj-main {
  flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 10px;
  appearance: none; background: none; border: 0; padding: 10px 4px; margin: 0;
  text-align: left; cursor: pointer; border-radius: var(--radius);
}
.pj-main:hover:not(:disabled):not(.is-current) { background: var(--hover); }
.pj-main:disabled, .pj-main.is-current { cursor: default; }
.pj-name {
  font: 500 var(--fs-body)/1.3 var(--sans); color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pj-meta { margin-left: auto; font: 400 var(--fs-mono)/1.4 var(--mono); color: var(--ink-faint); white-space: nowrap; }
.pj-badge {
  flex: 0 0 auto; font: 500 9.5px/1 var(--mono); letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--accent); border: 1px solid currentColor; border-radius: var(--pill); padding: 3px 7px;
}
.pj-icon {
  flex: 0 0 auto; appearance: none; background: none; border: 0; cursor: pointer;
  color: var(--ink-faint); font: 400 12px/1 var(--sans); padding: 7px 8px; border-radius: var(--radius);
}
.pj-icon:hover:not(:disabled) { color: var(--ink); background: var(--hover); }
.pj-icon.danger:hover:not(:disabled) { color: var(--danger); background: var(--danger-wash); }
.pj-icon:disabled { opacity: 0.3; cursor: default; }
.pj-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
.pj-hintline { font: 400 var(--fs-note)/1.4 var(--sans); color: var(--ink-faint); }
.pj-foot { justify-content: flex-end; align-items: center; gap: 12px; }

/* ---- app menu button (wordmark dropdown) ---- */
.menubtn {
  appearance: none; border: 0; border-radius: var(--pill); padding: 6px 12px;
  display: flex; align-items: center; gap: 7px;
  background: transparent; cursor: pointer;
}
.menubtn:hover { background: var(--hover); }
/* Wordmark: lowercase "abney" cut in heavy geometric oblique — the one place
   the UI speaks in a voice. The element keeps its text for screen readers and
   copy/paste (font-size 0 hides it); the mark itself is the mask, painted in
   currentColor so it inherits ink everywhere it is used. Size it by height. */
.wordmark {
  display: inline-block; font-size: 0; user-select: none;
  aspect-ratio: 2.8696;
  background: currentColor;
  -webkit-mask: url('wordmark.svg') no-repeat center / contain;
  mask: url('wordmark.svg') no-repeat center / contain;
}
.menubtn .wordmark { height: 16px; color: var(--ink); }
.menubtn .chev { font-size: 11px; color: var(--ink-dim); margin-left: 1px; }

/* ---- dialogs (share) ---- */
.dialog-backdrop { position: fixed; inset: 0; z-index: 34; background: rgba(0, 0, 0, 0.45); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
.dialog {
  position: fixed; top: 44%; left: 50%; transform: translate(-50%, -50%); z-index: 35;
  width: 420px; max-width: calc(100vw - 48px); padding: 20px 22px;
  background: var(--panel-thick); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--radius-xl); box-shadow: var(--shadow);
}
.dialog h3 { font: 600 var(--fs-head)/1.2 var(--sans); color: var(--ink); }
.dialog-sub { font: 400 var(--fs-note)/1.4 var(--mono); color: var(--ink-faint); margin-top: 3px; }
.share-row { display: flex; gap: 8px; margin-top: 14px; }
.dialog-note { font: 400 var(--fs-ui)/1.55 var(--sans); color: var(--ink-dim); margin-top: 12px; }
.dialog-note.dim { color: var(--ink-faint); margin-top: 6px; }
.dialog-note strong { color: var(--ink); font-weight: 600; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }

/* ---- welcome / about card (first run + app menu "About Abney") ----
   The one dialog headed by the wordmark voice instead of a plain h3. */
.about-title { height: 26px; color: var(--ink); }
.about-steps { margin-top: 14px; }
.about-step {
  display: flex; gap: 11px; padding: 8px 0; border-top: 1px solid var(--line);
  font: 400 var(--fs-ui)/1.55 var(--sans); color: var(--ink-dim);
}
.about-step strong { color: var(--ink); font-weight: 600; }
.about-num { flex: 0 0 auto; font: 500 var(--fs-mono)/1.8 var(--mono); color: var(--ink-faint); }

/* ---- gallery status badges: place is a status, not a hidden bucket ---- */
.thumb.on-desk::after {
  content: ''; position: absolute; top: 6px; right: 6px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--accent);
  border: 1px solid rgba(0, 0, 0, 0.55); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}
.thumb.omitted { opacity: 0.35; }
.thumb.omitted:hover { opacity: 0.8; }
.thumb.sel { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.thumb.omitted.sel { opacity: 0.6; }

/* ---- info card: session & library stats, labeled ---- */
.ic-row { align-items: baseline; }

/* ---- key HUD: shows the shortcut just pressed + what it did ---- */
#keyhud {
  position: fixed; bottom: calc(var(--drawer-h) + 16px); left: 50%; z-index: 26;
  transform: translate(-50%, 6px); display: flex; align-items: center; gap: 10px;
  padding: 8px 15px 8px 12px;
  background: var(--panel); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--pill); box-shadow: var(--shadow);
  font: 400 var(--fs-ui)/1.2 var(--sans); color: var(--ink-dim);
  opacity: 0; pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
#keyhud.show { opacity: 1; transform: translate(-50%, 0); }
#keyhud .keys { display: flex; gap: 3px; }

/* ---- smart guides + marquee ---- */
.guide { position: fixed; z-index: 8; background: var(--accent); opacity: 0.85; pointer-events: none; display: none; }
.guide.v { width: 1px; top: calc(var(--bar-h) + var(--edge)); bottom: var(--drawer-h); }
.guide.h { height: 1px; left: 0; right: 0; }
#marquee, .drawer-marquee {
  position: fixed; z-index: 8; pointer-events: none; display: none;
  border: 1px solid rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.07);
  border-radius: var(--radius-xs);
}
.drawer-marquee { z-index: 30; } /* above the gallery glass */

/* ---- the gallery: a resizable bottom sheet (tray / shelf) ---- */
#drawer {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 12;
  display: flex; flex-direction: column;
  transition: height 0.22s ease;
  /* height set inline by JS — user-resizable split */
}
#drawer.resizing { transition: none; }
/* The glass lives on a pseudo-element BEHIND the content: a scroller full of
   canvases inside a backdrop-filtered element triggers Chromium compositing
   bugs (unpainted tiles, content painted at stray offsets). The sheet's
   rounded top corners + border live here too, so the resize handle (which
   sticks out above the sheet) is never clipped by an overflow:hidden. */
#drawer::before {
  content: ''; position: absolute; inset: 0;
  background: var(--panel-thick); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: var(--rim), 0 -12px 40px rgba(0, 0, 0, 0.35);
  pointer-events: none;
}
.drawer-tabs, .drawer-body { position: relative; }
#drawer.collapsed .drawer-body,
#drawer.collapsed .drawer-resize,
#drawer.collapsed .drawer-foot { display: none; }
/* Minimized: the tab row is the whole bar. Let it fill the collapsed height and
   centre vertically (its own align-items: center) so the buttons sit with even
   spacing above and below instead of glued to the screen edge. The bottom
   border is meaningless with no body beneath it. */
#drawer.collapsed .drawer-tabs {
  flex: 1; padding-top: 0; padding-bottom: 0; border-bottom: 0;
}
.drawer-resize {
  position: absolute; top: -4px; left: 0; right: 0; height: 8px; z-index: 2;
  cursor: ns-resize;
}
.drawer-resize:hover {
  background: linear-gradient(to bottom, transparent 2px, var(--line-strong) 3px, var(--line-strong) 4px, transparent 5px);
}
/* ---- gallery footer: presentation controls (placement + preview zoom) ----
   One row shared by both docks: a placement toggle on the left, the zoom on
   the right. Hidden when the bottom sheet is collapsed. */
.drawer-foot {
  position: relative; flex: 0 0 auto;
  display: flex; align-items: center; gap: 12px;
  padding: 7px 16px; border-top: 1px solid var(--line);
}
.drawer-place {
  appearance: none; border: 0; border-radius: var(--radius-sm); padding: 0;
  width: 30px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--ink-dim); cursor: pointer;
  transition: color 0.1s, background 0.1s;
}
.drawer-place:hover { color: var(--ink); background: var(--hover); }
.drawer-place svg { width: 16px; height: 16px; }
.drawer-zoom {
  margin-left: auto; display: inline-flex; align-items: center; gap: 9px;
  color: var(--ink-faint);
}
.drawer-zoom svg { width: 13px; height: 13px; flex: 0 0 auto; }
/* view-menu group: sort + omitted filter, kept together. On the card they flow
   left after the tabs so a width resize reflows predictably (right-aligning them
   made the group jump against the edge). The bottom sheet is full-width and
   never resizes horizontally, so there it's pushed to the far right. */
.drawer-viewops { display: inline-flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.drawer-viewops .checkfilter { margin-left: 0; }
#drawer:not(.cardmode) .drawer-viewops { margin-left: auto; }
.drawer-tabs { display: flex; align-items: center; padding: 8px 16px 7px; border-bottom: 1px solid var(--line); flex: 0 0 auto; }
.drawer-body {
  flex: 1; overflow-y: auto; overflow-x: hidden;
  /* classic (non-overlay) scrollbars: reserve the gutter up front, so a
     scrollbar appearing can't shrink the row width the thumb fit is based on */
  scrollbar-gutter: stable;
  display: flex; flex-direction: column; gap: 16px;
  padding: 10px 16px 14px;
}
.roll { display: flex; flex-direction: column; gap: 7px; }
.thumb-strip { display: flex; flex-wrap: wrap; gap: 6px; }
.thumb {
  position: relative; overflow: hidden; flex: 0 0 auto; contain: paint;
  width: var(--tw, 120px); height: var(--th, 90px);
  border-radius: var(--radius-sm); background: var(--well);
  border: 1px solid var(--line); cursor: grab;
  transition: border-color 0.12s;
}
.thumb:hover { border-color: var(--line-strong); }
/* star rating badge: bottom-left of the thumb, readable over any image */
.thumb-stars {
  position: absolute; left: 6px; bottom: 5px; pointer-events: none;
  font: 500 10px/1 var(--sans); letter-spacing: 1.5px;
  color: #fff; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}
/* translateZ(0): own compositing layer per canvas — sidesteps the
   unpainted-until-hover Chromium glitch under blur/scroll combinations. */
.thumb canvas { width: var(--tw, 120px); height: var(--th, 90px); display: block; transform: translateZ(0); }
.drawer-empty { padding: 22px 10px 0; color: var(--ink-faint); font: 400 13.5px/1.5 var(--sans); }

/* ---- crop mode (the marks below stay flat and sharp: instrument, not chrome) ---- */
.cropbar {
  position: fixed; top: calc(var(--bar-h) + var(--edge) + 12px); left: 50%; transform: translateX(-50%); z-index: 16;
  display: flex; gap: 3px; align-items: center; padding: 5px;
  background: var(--panel); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--pill); box-shadow: var(--shadow);
}
.cropbar .sep { width: 1px; height: 18px; background: var(--line); margin: 0 5px; }
.crop-rect {
  position: fixed; box-sizing: border-box; cursor: move;
  border: 1px solid var(--ink);
  box-shadow: 0 0 0 9999px rgba(5, 5, 5, 0.72);
}
.crop-handle { position: absolute; width: 16px; height: 16px; }
.crop-handle[data-h="nw"] { top: -8px; left: -8px; border-top: 2px solid var(--ink); border-left: 2px solid var(--ink); cursor: nwse-resize; }
.crop-handle[data-h="ne"] { top: -8px; right: -8px; border-top: 2px solid var(--ink); border-right: 2px solid var(--ink); cursor: nesw-resize; }
.crop-handle[data-h="se"] { bottom: -8px; right: -8px; border-bottom: 2px solid var(--ink); border-right: 2px solid var(--ink); cursor: nwse-resize; }
.crop-handle[data-h="sw"] { bottom: -8px; left: -8px; border-bottom: 2px solid var(--ink); border-left: 2px solid var(--ink); cursor: nesw-resize; }
.crop-thirds {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to right, transparent calc(33.33% - 0.5px), var(--line) calc(33.33% - 0.5px), var(--line) calc(33.33% + 0.5px), transparent calc(33.33% + 0.5px), transparent calc(66.66% - 0.5px), var(--line) calc(66.66% - 0.5px), var(--line) calc(66.66% + 0.5px), transparent calc(66.66% + 0.5px)),
    linear-gradient(to bottom, transparent calc(33.33% - 0.5px), var(--line) calc(33.33% - 0.5px), var(--line) calc(33.33% + 0.5px), transparent calc(33.33% + 0.5px), transparent calc(66.66% - 0.5px), var(--line) calc(66.66% - 0.5px), var(--line) calc(66.66% + 0.5px), transparent calc(66.66% + 0.5px));
}

/* ---- import overlay ---- */
#importOverlay {
  position: fixed; inset: 0; z-index: 25; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px;
  background: rgba(10, 10, 10, 0.82); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  pointer-events: none;
}
#importOverlay[hidden] { display: none; }
#importTitle { font: 600 22px/1.2 var(--sans); color: var(--ink); }
#importSub { font: 400 12px/1 var(--mono); color: var(--ink-dim); font-variant-numeric: tabular-nums; }
#importBar { width: 320px; height: 4px; border-radius: var(--pill); background: rgba(255, 255, 255, 0.12); overflow: hidden; }
#importBar i { display: block; height: 100%; width: 0; background: var(--accent); transition: width 0.2s ease; }

/* ---- book pages on the desk (zine mode) ------------------------------------
   Page artboards are DOM cards camera-synced above the canvas. The paper is
   literal (white/near-black, sharp shadow, 2px corners) — print preview, not
   chrome; the tag above each card and every control stay Graphite glass. */
#bookLayer { position: fixed; inset: 0; z-index: 6; pointer-events: none; }
/* user-select: none — dragging a page must never sweep up the p.N tag or
   caption text as a browser text selection; captions opt back in below. */
/* contain: layout scopes the per-frame width/height writes (zoom) to the card's
   own subtree so a resize never reflows the rest of the book layer. Not `paint`
   or `size` — the page-number tag sits outside the box (bottom:100%) and must
   still render. */
.bk-page { position: absolute; left: 0; top: 0; contain: layout; pointer-events: auto; user-select: none; -webkit-user-select: none; }
.bk-page.dragging { cursor: grabbing; }
.bk-tag {
  position: absolute; bottom: 100%; left: 2px; margin-bottom: 5px;
  display: flex; align-items: center; gap: 8px; white-space: nowrap;
  font: 500 var(--fs-mono)/1 var(--mono); color: var(--ink-faint); cursor: grab;
}
.bk-tag .bk-num { color: var(--ink-dim); }
.bk-page.sel .bk-tag, .bk-page.sel .bk-tag .bk-num { color: var(--ink); }
.bk-tools { display: none; gap: 2px; }
.bk-page.sel .bk-tools { display: inline-flex; }
.bk-tool {
  appearance: none; border: 0; border-radius: var(--pill); padding: 2px 7px;
  font: 500 var(--fs-mono)/1.4 var(--sans); background: transparent; color: var(--ink-dim); cursor: pointer;
}
.bk-tool:hover { background: var(--hover); color: var(--ink); }
/* Ink colors come from the paper color (book.js sets the vars per page). */
.bk-page { --bk-ink: #1a1a1a; --bk-faint: rgba(20, 20, 20, 0.45); --bk-paper: #ffffff; }
/* The renderer draws the paper and every photo on it as scene instances, so this
   div holds NO pixels: it is the page's shadow, its radius, its pointer target
   and the clip for chrome. Transparent on purpose — the GPU paper shows through.
   Keep overflow:hidden: it clips the DOM chrome (a slot's outline on a bleed
   frame) to the sheet, which is what the shader's clip rect does to the pixels. */
.bk-paper {
  position: absolute; inset: 0; background: transparent; border-radius: 2px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35), 0 14px 44px rgba(0, 0, 0, 0.45);
  cursor: grab; overflow: hidden;
}
.bk-page.sel .bk-paper { outline: 1.5px solid var(--accent); outline-offset: 2px; }
/* A slot is a hit target and an outline. The only canvas it can hold is the
   transient drop preview (a photo dragged over it), which the scene knows
   nothing about. */
.bk-slot { position: absolute; }
.bk-slot canvas { width: 100%; height: 100%; display: block; }
.bk-slot.empty { box-shadow: inset 0 0 0 1px var(--bk-faint); }
.bk-slot.empty::after {
  content: '+'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font: 300 22px/1 var(--sans); color: var(--bk-faint);
}
/* layout mode (page is the sole selection): frames show their bounds and a
   resize handle; drags snap to the page grid drawn by .bk-guides */
.bk-page.editing .bk-slot { cursor: move; }
.bk-page.editing .bk-slot:hover { box-shadow: inset 0 0 0 1px var(--bk-faint); }
.bk-fr-rs { position: absolute; right: -7px; bottom: -7px; width: 14px; height: 14px; cursor: nwse-resize; display: none; z-index: 3; }
.bk-page.editing .bk-fr-rs { display: block; }
.bk-slot.empty .bk-fr-rs { display: none; } /* an empty frame has no image to scale */
.bk-fr-rs::after {
  content: ''; position: absolute; inset: 3px; border-radius: 50%;
  background: var(--accent); border: 1px solid rgba(0, 0, 0, 0.55);
}
.bk-slot .bk-x {
  position: absolute; top: 3px; right: 3px; width: 18px; height: 18px; padding: 0;
  appearance: none; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(10, 10, 10, 0.55); color: #eee; font: 500 10px/1 var(--sans);
  opacity: 0; transition: opacity 0.12s;
}
.bk-slot:hover .bk-x { opacity: 1; }
.bk-slot.empty .bk-x { display: none; }
/* reset-frame button: mirrors .bk-x, top-left, only in layout mode */
.bk-slot .bk-reset {
  position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; padding: 0;
  appearance: none; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(10, 10, 10, 0.55); color: #eee; font: 500 11px/1 var(--sans);
  opacity: 0; transition: opacity 0.12s; display: none;
}
.bk-page.editing .bk-slot:hover .bk-reset { display: block; opacity: 1; }
.bk-slot.empty .bk-reset { display: none; }
.bk-slot.droptarget { outline: 2px solid var(--accent); outline-offset: -1px; }
#bookLayer.slotdrag .bk-slot { cursor: copy; }
/* Desk photo dragged over a page: the photo itself travels on the canvas BELOW
   the paper, so the slot previews the drop — the incoming image drawn into the
   slot, gently pulsing over the occupant it would replace. */
.bk-drop-preview {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  pointer-events: none; animation: bk-incoming 0.9s ease-in-out infinite alternate;
}
@keyframes bk-incoming { from { opacity: 0.92; } to { opacity: 0.68; } }
.bk-slot.empty .bk-drop-preview { display: block; } /* beats `.empty canvas { display: none }` */
.bk-slot.empty.incoming::after { content: none; }   /* the '+' hint yields to the preview */
/* Free text blocks. The WRAPPER carries the position (trim fractions), the wrap
   width and the pointer padding; the editable inside carries only the type. That
   padding is what makes a small block catchable — a double-click that missed the
   glyphs by two pixels used to land on the paper and throw the whole book open in
   preview. The negative margin cancels it exactly, so the type still sets at
   x%,y% and the width is still the width that prints. */
.bk-tx {
  position: absolute; box-sizing: content-box;
  padding: 4px 6px; margin: -4px -6px;
  cursor: grab;
}
.bk-text {
  min-width: 6px; min-height: 0.6em;
  white-space: pre; line-height: 1.35; /* keep in sync with TEXT_LH */
  color: var(--bk-ink); outline: none;
  user-select: text; -webkit-user-select: text;
}
.bk-page.editing .bk-tx:hover .bk-text { outline: 1px dashed var(--bk-faint); outline-offset: 2px; }
/* a framed block shows the measure it is set to, so a short line still reads as
   a column rather than as loose type */
.bk-page.editing .bk-tx.framed .bk-text { outline: 1px dashed var(--bk-faint); outline-offset: 2px; }
.bk-tx.tsel .bk-text { outline: 1px dashed var(--accent); outline-offset: 2px; }
.bk-text:focus { outline: 1px solid var(--accent); outline-offset: 2px; cursor: text; }
/* The width handle: drag the right edge to wrap the type, double-click for auto.
   It sits just INSIDE the block's right edge, not outside it — the paper clips
   (overflow: hidden), so a handle hanging off the edge of a block that reaches
   the trim would be clipped away and unclickable. The text card's Width slider
   is the way in for a block whose type runs off the paper entirely. */
.bk-tx-rs {
  position: absolute; right: 0; top: 0; bottom: 0; width: 12px;
  cursor: ew-resize; display: none; z-index: 3;
}
.bk-page.editing .bk-tx-rs { display: block; }
.bk-tx-rs::after {
  content: ''; position: absolute; right: 3px; top: 50%; margin-top: -5px;
  width: 4px; height: 10px; border-radius: 2px;
  background: var(--accent); border: 1px solid rgba(0, 0, 0, 0.55);
}
/* the guides overlay: margin box + centre cross, snapped lines in accent */
/* ---- the guides: grid, margins, snap lines ---------------------------------
   These are drawn ON the paper, so their ink is the PAPER's ink (--bk-ink,
   which flips with the paper's luminance exactly as the page's type does) —
   never --accent. --accent is #ededed: a near-white line, at 26% opacity, on
   white paper. It was invisible on every light paper, which is most of them,
   and the live snap line — the one thing you must see while dragging — was the
   worst of it.

   The 1px halo in the paper's own colour is what keeps a hairline readable when
   it crosses a PHOTO rather than bare paper: dark line, light rim (or the
   reverse on black paper), the way a map graticule survives any terrain. */
.bk-guides { position: absolute; inset: 0; pointer-events: none; display: none; z-index: 4; }
.bkg-margin {
  position: absolute; border: 1px dashed color-mix(in srgb, var(--bk-ink) 42%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--bk-paper) 45%, transparent);
}
/* the page grid's modules: quiet hairline cells, resting company for layout
   mode — loud enough to compose against, never louder than the work */
.bkg-cell {
  position: absolute; border: 1px solid color-mix(in srgb, var(--bk-ink) 30%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--bk-paper) 40%, transparent);
}
.bkg-line { position: absolute; box-shadow: 0 0 0 1px color-mix(in srgb, var(--bk-paper) 45%, transparent); }
.bkg-line.v { top: 0; bottom: 0; width: 1px; }
.bkg-line.h { left: 0; right: 0; height: 1px; }
.bkg-line.dim { background: color-mix(in srgb, var(--bk-ink) 26%, transparent); }
/* the line something just snapped to: full paper ink, the loudest thing here */
.bkg-line.snap { background: var(--bk-ink); }
.bkg-line[hidden] { display: none; }
.bk-rs { position: absolute; right: -8px; bottom: -8px; width: 16px; height: 16px; cursor: nwse-resize; }
.bk-page.sel .bk-rs::after {
  content: ''; position: absolute; inset: 3px; border-radius: 50%;
  background: var(--accent); border: 1px solid rgba(0, 0, 0, 0.55);
}

/* ---- book config card (right, interactive sibling of the info card) ---- */
/* a section header that carries a control (the Layout scope picker) */
#bookcard .bk-title-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
/* A COLLAPSIBLE GROUP (Margins, Print). Its head is a section title with a
   chevron and a summary of what is inside — not a row, because it is not one
   setting. It gets a section's air above it, and a hairline, so a closed group
   reads as a closed section rather than as one more row in the list. */
.bk-acc { border-top: 1px solid var(--line); margin-top: 16px; }
.bk-acc-head {
  appearance: none; width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 12px 0 10px; min-height: var(--row-h); background: transparent; border: 0;
  color: var(--ink-dim); cursor: pointer; font: inherit; text-align: left;
}
.bk-acc-head .ctl-title {
  flex: 1 1 auto; display: flex; align-items: center; padding: 0;
  transition: color 0.1s;
}
.bk-acc-head:hover .ctl-title { color: var(--ink-dim); }
/* The summary is a VALUE, not part of the title: it keeps its own case ("mixed",
   "A4 · Perfect") and it sits RIGHT, by the chevron — the same title · summary
   shape the card head has. Butted straight against the title it read as one
   run-on heading ("PRINT A4 · PERFECT"). */
.bk-acc-head .bk-val {
  margin-left: auto; padding-left: 12px; padding-right: 10px;
  text-transform: none; letter-spacing: 0;
}
.bk-acc-chev { flex: 0 0 auto; color: var(--ink-faint); font-size: 10px; transition: transform 0.15s ease; }
.bk-acc.open .bk-acc-chev { transform: rotate(180deg); }
.bk-acc-body { padding-bottom: 4px; }
.bk-acc-body > .ctl-row:first-child { border-top: 0; }
/* bold / italic toggles on the text card */
.bk-tgl { min-width: 30px; }
/* the ink well: a colour input dressed down to the size of the buttons beside it */
.bk-color {
  width: 30px; height: 22px; padding: 0; cursor: pointer;
  background: none; border: 1px solid var(--line); border-radius: var(--radius-xs);
}
.bk-color::-webkit-color-swatch-wrapper { padding: 2px; }
.bk-color::-webkit-color-swatch { border: 0; border-radius: 3px; }
.bk-tgl.on { background: var(--on); color: var(--ink); border-color: var(--line-strong); }
.bk-empty-note { font: 400 var(--fs-ui)/1.6 var(--sans); color: var(--ink-dim); padding: 10px 0; }
.ctl-row > .ctl-label { flex: 0 1 auto; }
.bk-dim { opacity: 0.45; }
/* why a control is off, said in place — a greyed button with no reason is a
   dead end (Crop: "one photo at a time", "select a photo on the desk") */
.ctl-why { color: var(--ink-faint); font: 400 var(--fs-micro)/1 var(--mono); margin-left: 6px; }
.ctl-why:empty { display: none; }
.bk-val { color: var(--ink-faint); font: 400 var(--fs-micro)/1 var(--mono); margin-left: 5px; }
.bk-inline { display: inline-flex; align-items: center; gap: 4px; }
/* An inline group that owns the row's whole width (a ctl-col row: label above,
   control beneath). Without min-width:0 the flex item refuses to shrink below
   its content and the trailing button hangs off the card's edge. */
.bk-inline-wide { display: flex; width: 100%; min-width: 0; }
.bk-inline-wide > .btn { flex: 0 0 auto; }
/* a destructive verb never sits flush against the safe ones it lives beside */
.bk-sep-before { margin-left: 8px; }
.bk-full { display: inline-flex; align-items: center; gap: 4px; font: 400 var(--fs-mono)/1 var(--sans); color: var(--ink-dim); }
.bk-full input[type='checkbox'] { accent-color: var(--accent); }
/* Trim: the fields sit under their label, not beside it. Flush on one row, the
   two number wells plus the auto checkbox overran the card and clipped "auto"
   off the right edge. */
.bk-trim { justify-content: flex-end; flex-wrap: wrap; gap: 6px; }
/* A note is the system SPEAKING (a full sentence), so it is set in sans like
   every other sentence. Mono is for what the system RECORDS. */
.bk-note { font: 400 var(--fs-note)/1.5 var(--sans); color: var(--ink-faint); padding: 8px 0 2px; border-top: 1px solid var(--line); }
/* the card's own action row: it closes the card, so it gets section-sized air */
.bk-actions, .ed-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 18px; }
/* custom template chips carry their own delete */
.bk-tpl-x { margin-left: 6px; color: var(--ink-faint); font-size: 10px; }
.bk-tpl-x:hover { color: var(--danger); }
/* paper color: preset swatches + the free color well */
.bk-swatch { appearance: none; width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--line-strong); cursor: pointer; padding: 0; }
.bk-swatch:hover { border-color: var(--ink-dim); }
.bk-swatch.active { outline: 2px solid var(--ink); outline-offset: 1px; }
#bkPaperColor {
  width: 30px; height: 22px; padding: 1px; border: 1px solid var(--line);
  border-radius: var(--radius-xs); background: transparent; cursor: pointer;
}
/* Fit (tiles) and Binding (list) are the control kit's picker now — see THE
   CONTROL KIT above. They used to be two bespoke shapes with two bespoke active
   states, and the Fit tiles sat on --well: the field material, which made the
   one control you pick a layout with the darkest thing on the card. */
/* the active option's one-liner, tucked under the grid without a divider */
.bk-fithow { border-top: 0; padding: 7px 1px 1px; }
/* The "?" toggle is a shared recipe: binding help on the book card, the
   concept guide on the Projects dialog. .on while its explainer is showing. */
.helpbtn {
  appearance: none; width: 16px; height: 16px; margin-left: 7px; padding: 0;
  border-radius: 50%; border: 1px solid var(--line-strong);
  background: transparent; color: var(--ink-faint); font: 500 10px/1 var(--mono); cursor: pointer;
}
.helpbtn:hover, .helpbtn.on { color: var(--ink); border-color: var(--ink-dim); }
h3 .helpbtn { vertical-align: 2px; }
.bk-bindhow { background: var(--well); border: 1px solid var(--line); border-radius: var(--radius); padding: 9px 11px; margin-top: 6px; font-family: var(--sans); font-size: var(--fs-note); }
.bk-bindhow[hidden] { display: none; }

/* danger actions inside cards and rows share the menu's red */
.bk-danger { color: var(--danger); }

/* ambient bottom hint while pages are selected (the flip-preview affordance) */
#bkHint {
  position: fixed; bottom: calc(var(--drawer-h) + 16px); left: 50%; z-index: 9;
  transform: translateX(-50%); display: flex; align-items: center; gap: 8px;
  padding: 6px 12px; border-radius: var(--pill);
  background: var(--panel); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line);
  font: 400 12px/1.2 var(--sans); color: var(--ink-dim);
  pointer-events: none;
}
#bkHint[hidden] { display: none; }

/* fonts dialog rows */
.ft-list { display: flex; flex-direction: column; margin-top: 10px; max-height: 300px; overflow-y: auto; }
.ft-row { justify-content: flex-start; }
.ft-name { flex: 1; min-width: 0; font-size: 15px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- flip preview (Space on a page) ---- */
.bk-flip { position: fixed; inset: 0; z-index: 32; display: flex; flex-direction: column; padding: 14px 20px 22px; pointer-events: none; }
.bk-flip > * { pointer-events: auto; }
.bk-flip-top { display: flex; align-items: center; gap: 12px; }
.bk-flip-top .bk-flip-label { font: 400 12px/1.2 var(--mono); color: var(--ink-dim); }
.bk-flip-top button { margin-left: auto; }
.bk-flip-stage { flex: 1; display: flex; align-items: center; justify-content: center; gap: 16px; min-height: 0; pointer-events: none; }
.bk-flip-stage > * { pointer-events: auto; }
.bk-spread { display: flex; gap: 3px; align-items: center; filter: drop-shadow(0 18px 50px rgba(0, 0, 0, 0.55)); }
.bk-spread canvas { background: #fff; }

/* ---- export dialog rows (the book and photo export dialogs share these) ---- */
.bk-x-rows { display: flex; flex-direction: column; gap: 2px; margin-top: 12px; }
.bk-x-row { display: flex; align-items: center; gap: 9px; padding: 6px 2px; font: 400 var(--fs-body)/1.3 var(--sans); color: var(--ink); }
.bk-x-row .bk-x-hint { color: var(--ink-faint); font-size: var(--fs-note); margin-left: auto; text-align: right; max-width: 46%; }
.bk-x-row input[type='checkbox'] { accent-color: var(--accent); }

/* ---- right-edge action rail (ambient chrome: muted until hovered) ---- */
#railMenu {
  position: fixed; right: var(--edge); top: 50%; transform: translateY(-50%); z-index: 24;
  display: flex; flex-direction: column; gap: 2px; padding: 6px;
  background: var(--panel); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--line); border-radius: var(--pill); box-shadow: var(--shadow);
  opacity: 0.45; transition: opacity 0.22s ease;
}
#railMenu:hover, #railMenu:focus-within { opacity: 1; }
.rail-btn {
  appearance: none; border: 0; width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent; color: var(--ink-dim); cursor: pointer;
}
.rail-btn:hover { background: var(--hover); color: var(--ink); }
/* lit while the button's surface (edit card, book card, gallery card, info)
   is open, mirroring how seg-btn marks its active option */
.rail-btn.active { background: var(--on); color: var(--ink); }
.rail-btn svg { width: 17px; height: 17px; }

/* ---- gallery as a side card (rail-toggled dock) ---- */
#drawer.cardmode {
  left: var(--edge); right: auto; bottom: auto; top: calc(var(--bar-h) + var(--edge) + 10px);
  z-index: 21; transition: none;
}
#drawer.cardmode::before {
  border: 1px solid var(--line); border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}
#drawer.cardmode .drawer-resize { display: none; }
#drawer.cardmode .drawer-tabs { flex-wrap: wrap; row-gap: 6px; }
/* keep the footer's zoom clear of the bottom-right resize corner (card only) */
#drawer.cardmode .drawer-foot { padding-right: 28px; }
#drawer.cardmode .thumb-size { width: 96px; }
.drawer-corner { display: none; position: absolute; right: 0; bottom: 0; width: 24px; height: 24px; cursor: nwse-resize; z-index: 3; }
#drawer.cardmode .drawer-corner { display: block; }
.drawer-corner::after {
  content: ''; position: absolute; right: 8px; bottom: 8px; width: 7px; height: 7px;
  border-right: 2px solid var(--line-strong); border-bottom: 2px solid var(--line-strong);
  border-radius: 0 0 3px 0;
}
.drawer-corner:hover::after { border-color: var(--ink-dim); }

/* ---- edit card: per-photo edit controls (rail sibling of book) ----
   Its rows, sliders and channel picker are the control kit's — what is left
   here is the card's own furniture and the two instruments only it has: the
   histogram and the affected-area mask. */
.ed-empty { font: 400 var(--fs-ui)/1.6 var(--sans); color: var(--ink-dim); padding: 8px 0 2px; }
#editcard.empty .ed-body { opacity: 0.35; pointer-events: none; }
.ed-inline { display: inline-flex; align-items: center; gap: 4px; }
/* levels overview: live histogram of the edited photo + channel scope picker */
#edHist {
  display: block; width: 100%; height: 64px; margin: 8px 0 8px;
  background: var(--well); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
/* affected-area mask (Alt-drag threshold): overlaid on the photo on the desk */
#edMask {
  position: fixed; z-index: 22; pointer-events: none; display: none;
  border-radius: 2px; mix-blend-mode: screen;
}
/* Channel scope for the histogram above it: it belongs to the readout, so it
   hangs under it rather than taking a row of its own. It is an ordinary kit
   picker — the chips it replaced lit up by turning BLACK (they had borrowed
   --well, the inset-field material), which is the same bug the Fit tiles had. */
.ed-chanrow { display: flex; align-items: center; margin: 0 0 2px; }
.ed-chanrow .ctl-seg { justify-content: flex-start; }

/* RA-4 paper picker: the trigger button reads like the old select but nicer,
   and opens a previewing popup (.paper-menu) */
.ed-picker {
  flex: 0 0 auto; max-width: 172px; display: inline-flex; align-items: center; gap: 6px;
  background: var(--well); color: var(--ink); text-align: left; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 5px 8px 5px 10px;
  font: 400 12px/1.2 var(--sans);
}
.ed-picker:hover { border-color: var(--line-strong); }
.ed-picker:focus-visible { outline: none; border-color: var(--line-strong); box-shadow: 0 0 0 2px rgba(237, 237, 237, 0.18); }
.ed-picker[aria-expanded="true"] { border-color: var(--line-strong); background: var(--hover); }
.ed-picker-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ed-picker-caret { width: 12px; height: 12px; flex: 0 0 auto; color: var(--ink-faint); }
.ed-picker[aria-expanded="true"] .ed-picker-caret { transform: rotate(180deg); }

/* max-height + a shrinkable list: on a short window the options scroll rather
   than the popup running off the bottom edge */
.paper-menu {
  z-index: 40; width: 264px; display: flex; flex-direction: column; gap: 8px;
  /* border-box: .menu is content-box, so max-height would otherwise exclude the
     popup's own padding + border and still overhang by those 14px */
  box-sizing: border-box; max-height: calc(100vh - 16px);
}
.paper-preview { flex: 0 0 auto; display: flex; flex-direction: column; gap: 6px; }
.paper-preview-cv {
  display: block; width: 240px; height: auto; margin: 0 auto; border-radius: var(--radius-sm);
  background: var(--well); border: 1px solid var(--line); box-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
}
.paper-preview.empty .paper-preview-cv { display: none; }
.paper-preview-cap { padding: 0 2px; }
.paper-preview-name { display: block; font: 500 12px/1.3 var(--sans); color: var(--ink); }
.paper-preview-blurb { display: block; font: 400 var(--fs-mono)/1.4 var(--sans); color: var(--ink-faint); margin-top: 2px; }
.paper-list {
  display: flex; flex-direction: column; gap: 1px; overflow-y: auto;
  flex: 1 1 auto; min-height: 0; max-height: 240px;
}
.paper-opt {
  display: flex; align-items: center; gap: 8px; width: 100%; cursor: pointer; text-align: left;
  padding: 6px 8px; border: 0; border-radius: var(--radius);
  background: transparent; color: var(--ink-dim); font: 400 var(--fs-ui)/1.2 var(--sans);
}
.paper-opt-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.paper-opt-check { flex: 0 0 auto; opacity: 0; color: var(--ink-faint); font-size: 11px; }
.paper-opt.active { color: var(--ink); }
.paper-opt.active .paper-opt-check { opacity: 1; }
.paper-opt.hi { background: var(--hover); color: var(--ink); }

/* explainer glyphs in the book card (the option shows what it means) */
.bk-glyph { width: 13px; height: 13px; color: var(--ink-faint); vertical-align: -2px; margin-right: 6px; flex: 0 0 auto; }

/* ---- first-run tour: coach card + anchored hint tags -----------------------
   Bottom-left, riding the tray's height var, clear of every other surface.
   Not a dialog, the desk stays fully interactive. One quiet header row, a
   title, one line of context, then the tasks. A task ticks green the moment
   the state shows it happened. Green is deliberate here: success feedback,
   the same license the warn amber has. */
#tourcard {
  --ok: #93b478;
  top: auto; right: auto;
  left: var(--edge); bottom: calc(var(--drawer-h) + 18px); z-index: 24;
  padding: 13px 16px 10px;
  transition: bottom 0.22s ease;
}
.tour-head { display: flex; align-items: center; gap: 8px; }
.tour-count { font: 500 var(--fs-micro)/1 var(--mono); color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.tour-nav { margin-left: auto; display: inline-flex; gap: 1px; }
.tour-navbtn {
  appearance: none; border: 0; border-radius: 50%; padding: 0;
  width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
  font: 400 14px/1 var(--sans); background: transparent; color: var(--ink-dim); cursor: pointer;
}
.tour-navbtn:hover { background: var(--hover); color: var(--ink); }
.tour-navbtn:disabled { opacity: 0.3; cursor: default; }
.tour-title { font: 600 14px/1.3 var(--sans); color: var(--ink); margin-top: 7px; }
.tour-body { font: 400 12px/1.5 var(--sans); color: var(--ink-dim); margin-top: 3px; }
.tour-body kbd, .tour-task kbd { margin: 0 1px; }
.tour-tasks { margin-top: 9px; }
.tour-task {
  display: flex; align-items: center; gap: 10px; padding: 7px 0;
  border-top: 1px solid var(--line);
  font: 400 12px/1.4 var(--sans); color: var(--ink-dim);
}
.tour-task .tt-dot {
  flex: 0 0 auto; width: 14px; height: 14px; border-radius: 50%;
  border: 1.5px solid var(--line-strong);
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 9px/1 var(--sans); color: #0a0a0a;
  transition: background 0.18s, border-color 0.18s;
}
.tour-task.on { color: var(--ink); }
.tour-task.on .tt-dot { background: var(--ok); border-color: var(--ok); }
.tour-task.on .tt-dot::after { content: '✓'; }
.tour-actions { display: flex; justify-content: flex-end; margin-top: 12px; padding-bottom: 4px; }
@keyframes tour-pulse {
  0%, 100% { transform: scale(1); opacity: 0.35; }
  50% { transform: scale(1.45); opacity: 0.95; }
}
/* hint tag: a small glass pill the tour pins to a photo (camera-tracked) or a
   piece of chrome. pointer-events: none — it labels, it never intercepts. */
.tour-tag {
  position: fixed; z-index: 9; transform: translate(-50%, calc(-100% - 8px));
  pointer-events: none; display: none; align-items: center; gap: 7px;
  padding: 5px 11px; white-space: nowrap;
  background: var(--panel); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--line-strong); border-radius: var(--pill); box-shadow: var(--shadow);
  font: 500 var(--fs-note)/1.2 var(--sans); color: var(--ink);
}
.tour-tag.show { display: inline-flex; }
.tour-tag::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto;
  background: var(--accent); animation: tour-pulse 1.6s ease-in-out infinite;
}

/* ---- the experimental-build notice: printed on the felt ---------------------
   Not a card. No panel, no glass, no border, no close button. Its ink is taken
   from the desk tone by feedback.js (--bn-ink / --bn-strong, recomputed when
   the tone changes), so it reads as a mark ON the surface at every one of the
   greyscale desk tones instead of a floating panel competing with the photos.

   The lines are drawn by the RENDERER, as a decal under the photographs (DOM
   can only float above the canvas, which is the opposite of a mark on a
   surface). This block still owns their layout and their ink: feedback.js
   measures these boxes and reads these colours back out to raster them, so the
   notice is styled here and nowhere else. .bn-lines is invisible, not absent —
   it is the layout, and it is what a screen reader reads.

   pointer-events stay off the text so it can never intercept a desk drag, and
   come back on for the one thing that is clickable. */
#betanote {
  position: fixed; left: calc(var(--edge) + 4px); bottom: calc(var(--drawer-h) + 14px); z-index: 5;
  display: flex; flex-direction: column; max-width: 330px;
  pointer-events: none; user-select: none;
  font: 400 var(--fs-note)/1.55 var(--sans); color: var(--bn-ink, #7a7a7a);
  transition: color 0.2s ease, bottom 0.22s ease;
}
#betanote .bn-lines { display: flex; flex-direction: column; gap: 1px; opacity: 0; }
/* No wrapping: the raster draws one line per span, and a span that wrapped in
   the DOM would be set as one long line that overruns the box. Each line is
   short enough that this is a guard, not a constraint. */
#betanote .bn-line { white-space: nowrap; }
#betanote .bn-lead { font-weight: 600; color: var(--bn-strong, #9a9a9a); }
/* The link is printed on the felt with the rest of the notice, so it is invisible
   here too and the canvas owns its clicks (input.js: pointer cursor on hover, the
   dialog on click, and only where nothing is lying over it). The button stays a
   button for the keyboard — and a keyboard user gets the real DOM one back, focus
   ring and all, the moment it is focused. */
#betanote .bn-link {
  pointer-events: none; align-self: flex-start; margin-top: 5px;
  appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  font: 500 var(--fs-note)/1.4 var(--sans); color: var(--bn-strong, #9a9a9a);
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
  opacity: 0;
}
#betanote .bn-link:focus-visible {
  opacity: 1; pointer-events: auto;
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px;
}

/* ---- toasts: what just happened ---------------------------------------------
   Bottom-right, above the tray, clear of the key HUD (bottom-center) and the
   tour/beta notice (bottom-left). A receipt flicks away on its own. Anything
   the user must act on stays and carries an ✕.

   ONE recipe, three kinds. A kind sets three variables — hue, wash, rim — and
   nothing in the layout below names a colour. Two rules the kinds all obey:

   · the wash lies ON the glass, it never replaces it. A status background that
     assigns `background:` outright throws away --panel-thick and leaves tinted
     film over the photograph, which is unreadable over a bright frame.
   · the sentence stays --ink. Colour is carried by the dot and the rim. Amber
     type on an amber wash is a warning nobody can read.

     info   no dot, plain glass       a receipt
     warn   amber, the house caution  something to act on
     error  red, the permitted red    something broke */
#toasts {
  position: fixed; right: var(--edge); bottom: calc(var(--drawer-h) + 16px); z-index: 27;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
  max-width: min(420px, 46vw); pointer-events: none;
  transition: bottom 0.22s ease;
}
.toast {
  --status: transparent;
  --status-wash: transparent;
  --status-line: var(--line);
  pointer-events: auto; display: flex; align-items: center; gap: 10px;
  padding: 9px 10px 9px 14px; border-radius: var(--pill);
  background: linear-gradient(var(--status-wash), var(--status-wash)), var(--panel-thick);
  backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur);
  border: 1px solid var(--status-line); box-shadow: var(--shadow);
  font: 400 var(--fs-ui)/1.45 var(--sans); color: var(--ink);
  /* enter from the right, leave to the right: a receipt slides in and flicks away */
  opacity: 0; transform: translateX(12px);
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.toast-in { opacity: 1; transform: translateX(0); }
.toast-out { opacity: 0; transform: translateX(16px); }
.toast-warn  { --status: var(--warn);   --status-wash: var(--warn-wash);   --status-line: var(--warn-line); }
.toast-error { --status: var(--danger); --status-wash: var(--danger-wash); --status-line: var(--danger-line); }

/* the status mark: the toast's one piece of colour, haloed in its own wash so
   it reads against both the glass and whatever photo is behind it */
.toast-dot {
  flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: var(--status); box-shadow: 0 0 0 3px var(--status-wash);
}
.toast-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.toast-text { min-width: 0; }
.toast-note { font: 400 var(--fs-note)/1.4 var(--sans); color: var(--ink-dim); }
/* with a second line the toast is no longer a capsule: it becomes a small card,
   its first line the title, and the dot rises to meet it */
.toast:has(.toast-note) { border-radius: var(--radius-lg); padding: 11px 11px 12px 14px; }
.toast:has(.toast-note) .toast-text { font-weight: 500; }
.toast:has(.toast-note) .toast-dot { align-self: flex-start; margin-top: 6px; }

/* the button is the way out of the message, so it borrows the status rim — but
   its label stays ink, or it reads as one more thing that is wrong */
.toast-btn { flex: 0 0 auto; padding: 4px 11px; font-size: 12px; white-space: nowrap; }
.toast-warn .toast-btn, .toast-error .toast-btn {
  border-color: var(--status-line); background: var(--status-wash);
}
.toast-warn .toast-btn:hover, .toast-error .toast-btn:hover {
  border-color: var(--status); background: var(--hover-2);
}
.toast-x {
  flex: 0 0 auto; appearance: none; border: 0; border-radius: 50%; padding: 0;
  width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
  background: transparent; color: currentColor; opacity: 0.55; cursor: pointer;
  font: 400 11px/1 var(--sans);
}
.toast-x:hover { opacity: 1; background: var(--hover); }

/* ---- feedback dialog (inside the shared .dialog shell) ---- */
.fb-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; }
.fb-row .ps-input { flex: 1; min-width: 0; max-width: 230px; }
#fbStatus:empty { display: none; }
