/* ===========================================================
   Shitzengiggles Atelier OS — styles (V21 "Acrylic Gallery")
   Plain CSS, no frameworks. Friendly, readable.

   Design system: an editorial fashion-house UI on a bright ribbed
   near-white canvas — a thin rainbow bar across the top of every page,
   oversized Big Shoulders Display headlines, Space Grotesk body, Space
   Mono micro-labels, and a category colour system used as a solid tab +
   matching label. Squared, architectural radii (2-4px). Neutral shadows
   only — never coloured pastel ones.

   Spec: ATELIER_REDESIGN_HANDOVER.md. Step log: REDESIGN_LOG.md.
   The class/id names are unchanged — the JavaScript contract is intact.
   =========================================================== */

/* ===========================================================
   Fonts — SELF-HOSTED (v21.3)
   Downloaded from Google Fonts, the exact families + versions the
   approved mockups use: Big Shoulders Display v24, Space Grotesk v22,
   Space Mono v17. Self-hosted (not a <link>) so an iOS webview that
   blocks third-party font hosts can't silently fall back to the system
   font on Josh's iPhone/iPad.

   Big Shoulders Display and Space Grotesk are VARIABLE — one file each
   covers the whole weight range the mockups ask for. Space Mono has no
   variable version, so 400 and 700 ship separately.
   =========================================================== */
@font-face {
  font-family: "Big Shoulders Display";
  font-style: normal;
  font-weight: 600 900;               /* mockups use 600/700/800/900 */
  font-display: swap;
  src: url("../assets/fonts/big-shoulders-display-latin-var.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Big Shoulders Display";
  font-style: normal;
  font-weight: 600 900;
  font-display: swap;
  src: url("../assets/fonts/big-shoulders-display-latin-ext-var.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 400 700;               /* mockups use 400/500/600/700 */
  font-display: swap;
  src: url("../assets/fonts/space-grotesk-latin-var.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../assets/fonts/space-grotesk-latin-ext-var.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Space Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/space-mono-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Space Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/space-mono-400-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Space Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/space-mono-700-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Space Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/space-mono-700-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ===========================================================
   V21 DESIGN TOKENS — "Acrylic Gallery"
   Values come straight from ATELIER_REDESIGN_HANDOVER.md §1.
   =========================================================== */
:root {
  /* ---- Type ---- */
  --disp: "Big Shoulders Display", "Arial Narrow", sans-serif;   /* condensed poster display */
  --ui:   "Space Grotesk", system-ui, -apple-system, sans-serif;  /* body / UI */
  --mono: "Space Mono", ui-monospace, "SFMono-Regular", monospace; /* kickers, meta, labels */

  /* ---- Neutrals (editorial near-white) ---- */
  --ink:   #14131a;   /* primary text */
  --mut:   #6e6a7a;   /* secondary text — darkened from the spec's #7a7686 so it
                         clears WCAG AA on the canvas (4.3:1 -> 5.2:1) */
  /* --faint is 2.56:1 on white. That is BELOW AA for any text, so it is not a
     text colour: it is for decorative marks, empty-slot graphics, supplementary
     placeholders, and disabled controls (which WCAG 1.4.3 exempts).
     v21.13 first darkened this token instead, which worked but collapsed the
     grey tier — at an AA-passing lightness it is indistinguishable from --mut.
     Josh's ruling: keep the three-step tier, move the LIVE labels to --mut and
     leave --faint only where nothing readable depends on it. If you are about
     to use --faint on something a person reads, use --mut. */
  --faint: #a3a0ad;   /* tertiary / decorative / disabled — NOT for live text */
  --line:  #e4e3dd;   /* hairline borders */
  --line2: #eceae4;   /* lighter dividers */
  --panel: #ffffff;   /* card / panel surface */

  /* The ribbed "acrylic" canvas. --bg-flat is the same colour without the
     ribbing, for small surfaces and for the reduced-texture path. */
  --bg-flat: #f5f5f2;
  --bg: repeating-linear-gradient(90deg, #f7f7f5 0 7px, #f0f0ed 7px 8px), #f5f5f2;

  /* ---- Brand spectrum ---- */
  --pink:   #ff2e88;
  /* CONTRAST — ACCEPTED DELIBERATELY, do not "fix" it again.
     v21.13 recorded this as "4.4999, one ten-thousandth under". That figure is
     for a WHITE panel, and it is the wrong surface for .cat-text-outfit, which
     is painted on the ribbed canvas like every other category label. The real
     numbers:
                     white   flat canvas   dark rib
       --red        4.4999      4.1197       3.9411
     So it is not a rounding artifact where it matters — it is a genuine miss on
     the canvas. It stays anyway: this hex is welded into --rainbow and
     --rainbow-v below, and splitting the brand red into two values in one
     stylesheet is the worse problem. Ruled v21.13a, re-ruled with the correct
     figures in v21.15. Also affects white-on-red (Generate, "Create new") and
     red-on-white ("Restore it", the mobile tab-bar label). */
  --red:    #e0322f;
  --orange: #ff7a2e;
  --yellow: #ffca2e;
  --green:  #12b58f;
  --blue:   #2f7dff;
  --purple: #8a5cff;

  /* ---- Category coding, TEXT-ONLY tier (v21.15) -------------------------
     A deep twin for each bright category colour. **Never use these for a
     surface, a border or a gradient** — the bright spectrum above owns every
     tab, border, rail, progress fill and the rainbow bar, and the brights are
     written literally into --rainbow / --rainbow-v, so the two tiers must not
     be swapped for one another.

     These exist because the same colour cannot do both jobs. On a thumbnail
     the bright colour is a 4px block of pure signal; as 9px mono TEXT it is
     unreadable (white-on-Fabric-yellow measures 1.53:1). Only .cat-text-*
     uses this tier.

     DERIVED AGAINST THE DARKEST RIB STRIPE (#f0f0ed) — not white, and not the
     flat canvas. These labels have NO opaque ancestor: measured in the running
     app, every parent up to <body> is transparent, so they are painted on the
     ribbed canvas (--bg, stripes of #f7f7f5 and #f0f0ed over #f5f5f2). The
     darkest stripe is the worst case, so clearing 4.5:1 there means clearing it
     everywhere the label can land. The first cut of this tier was calibrated
     against #ffffff and only reached ~3.94 on the rib — a palette that passed
     its own test and failed on screen.

                    rib #f0f0ed   canvas #f5f5f2   white
       --blue-ink       4.5069        4.7111       5.1459   (was --blue   3.94 rib)
       --green-ink      4.5133        4.7179       5.1533   (was --green  2.28 rib)
       --orange-ink     4.5228        4.7278       5.1641   (was --orange 2.27 rib)
       --gold-ink       4.5101        4.7146       5.1497   (was #a8820e  3.13 rib)
       --pink-ink       4.5102        4.7146       5.1498   (was --pink   3.06 rib)
       --purple-ink     4.5036        4.7077       5.1422   (was --purple 3.63 rib)
       --teal-ink       4.5449        4.7509       5.1894   (was #0e8f93  3.42 rib)
     Check 6 of tools/audit-a11y.py GATES on the rib and prints the other two
     columns beside it. */
  --blue-ink:   #005fff;
  --green-ink:  #0c7c62;
  --orange-ink: #bf4500;
  --gold-ink:   #8b6800;
  --pink-ink:   #d8005d;
  --purple-ink: #7944ff;
  --teal-ink:   #0c797d;

  /* ---- Category coding ---- */
  --cat-outfit:  var(--red);
  --cat-concept: var(--blue);
  --cat-patch:   var(--green);

  /* ---- Signature rainbow bar ---- */
  --rainbow: linear-gradient(90deg, #ff2e88, #e0322f, #ff7a2e, #ffca2e, #12b58f, #2f7dff, #8a5cff);
  /* Vertical cut, for rails running DOWN the page. A horizontal gradient on a
     3px-wide vertical border samples to a single colour — this does not. */
  --rainbow-v: linear-gradient(180deg, #ff2e88, #e0322f, #ff7a2e, #ffca2e, #12b58f, #2f7dff, #8a5cff);

  /* ---- Radii — deliberately tight/architectural. This look is squared. ---- */
  --r-xs: 2px;
  --r-sm: 3px;
  --r-md: 4px;
  --r-pill: 999px;

  /* ---- Spacing (4px base) ---- */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 34px; --s7: 44px;

  /* ---- Elevation — soft, NEUTRAL. Never coloured pastel shadows. ---- */
  --sh-1: 0 8px 16px rgba(20, 19, 26, 0.12);
  --sh-2: 0 12px 24px rgba(20, 19, 26, 0.16);
  --sh-3: 0 30px 54px rgba(20, 19, 26, 0.22);

  /* =========================================================
     COMPATIBILITY ALIASES  (v21.3)
     The ~1,900 rules below still use the V4 token names, and js/settings.js
     overrides some of them by name. Rather than rewrite every rule in one
     unreviewable commit, every old name is kept here and re-pointed at a V21
     value. The whole app therefore adopts the new palette and type at once,
     and each screen gets properly restyled in its own later step
     (see REDESIGN_LOG.md).

     All 39 names the stylesheet references are covered — verified by grep,
     not by eye. See the v21.3 commit message.
     ========================================================= */
  --font-display: var(--disp);
  --font-ui: var(--ui);

  --soft: var(--mut);              /* --soft was renamed --mut in V21 */
  --cream: var(--bg-flat);
  --card: var(--panel);
  --canvas: var(--bg-flat);
  --rail: var(--panel);
  --rail-border: var(--line);
  --panel-2: #faf9f6;              /* inputs / pills — a hair off --panel */
  --thumb-bg: #ecebe5;             /* image placeholder backdrop */
  --image-stage: #14131a;          /* dark stage behind a previewed image */

  --mint: var(--green);
  --gold: var(--yellow);
  --sky:  var(--blue);
  --cyan: #12b0b5;

  /* Accent is RED in V21 (was violet). This one line is what turns the old
     "AI purple" chrome — active nav, focus rings, links — editorial. */
  --accent: var(--red);
  --accent-2: var(--pink);
  --accent-ink: #b8231f;           /* readable red for text on --accent-soft */
  --accent-soft: #fbeceb;
  --heart-bg: rgba(255, 255, 255, 0.92);

  --primary-bg: var(--ink);
  --primary-fg: #ffffff;
  --primary-glow: var(--sh-2);     /* neutral — no coloured glow in V21 */

  --shadow-1: var(--sh-1);
  --shadow-2: var(--sh-2);
  --shadow-lg: var(--sh-3);
  --ring: 0 0 0 3px rgba(224, 50, 47, 0.25);

  /* The holographic aura is retired — V21 takes its colour from the rainbow
     bar and category tabs, not a lavender wash. Neutralised rather than
     deleted so every `background-image: var(--holo)` rule stays valid. */
  --holo: none;

  --grad-brand: var(--rainbow);
  --grad-sidebar: var(--rainbow);
  --grad-holo: var(--rainbow);
  --grad-camp: var(--rainbow);

  /* Old radius scale, squared off. */
  --r-lg: var(--r-md);
  --r-xl: 6px;

  /* Chrome metrics (v21.4) */
  --rainbow-h: 7px;
  --tabbar-h: 58px;
  /* Vertical band at the top-left of a thumbnail reserved for the Pinterest
     browser extension's injected "Save" button. Our overlays start below it.
     One number to change if that button ever resizes. (v21.7.5) */
  --pin-clear: 44px;
}

* { box-sizing: border-box; }

/* v22.18a — no automatic text inflation. iPhone Safari enlarges the text of
   any block WIDER than the screen (WebKit RenderBlockFlow::adjustFontSizes
   returns early only when the block fits), but only while this property is
   `auto`. The saved-prompt list's rows were 607px wide on a 390px phone, so
   some titles were inflated and some were not: two sizes in one list. Any
   value but `auto` turns it off (WebKit tests textSizeAdjust().isAuto()). */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--ui);
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  /* v21.3: the lavender aura is gone. The canvas is the ribbed "acrylic"
     near-white; colour now comes from the rainbow bar and category tabs. */
  background: var(--bg);
  background-attachment: fixed;
  padding-top: var(--rainbow-h);   /* clear the fixed rainbow bar (v21.4) */
  display: flex;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---------- The signature rainbow bar (v21.4) ----------
   Fixed at the very top so it spans the sidebar and the content in one
   unbroken sweep, without touching the body flex layout. Everything else
   is pushed down by --rainbow-h. */
.atelier-rainbow {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--rainbow-h);
  background: var(--rainbow);
  z-index: 60;
}

/* ---------- Sidebar navigation ---------- */
.sidebar {
  width: 236px;
  flex-shrink: 0;
  background: var(--panel);
  color: var(--ink);
  padding: 26px 14px 18px;
  position: sticky;
  top: var(--rainbow-h);
  height: calc(100vh - var(--rainbow-h));
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 40;
}
.nav-settings { margin-top: auto; }   /* pin Settings to the bottom */

/* v21.7.9: the five accent "vibe" themes and the full Dark Atelier token set
   are RETIRED (locked decision 6). They repainted --purple / --accent-ink
   while V21 drives everything from --red, so every one of them produced a
   half-applied palette. V21 is one committed identity. The login screen is
   still a dark brand moment — that is a designed screen, not a theme. */

/* Settings screen */
.settings-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
/* v21.18: was `style="max-width:340px;margin-bottom:0"` on each of the two
   inputs. .search-box is width:100% with a 16px bottom margin — sensible for a
   search field at the top of a screen, wrong for a control sitting in a flex
   row next to its Save button. */
.settings-row .search-box { max-width: 340px; margin-bottom: 0; }

/* ---------- Settings panels (v21.10b) ----------
   Handover §5.6: stacked panels, ~760px measure. The screen was a flat run of
   headings and controls with nothing grouping them, so every setting looked
   equally important and the destructive ones sat next to the cosmetic ones. */
#screen-settings { max-width: 780px; }
.set-panel {
  display: block;
  border: 1px solid var(--line);
  background: var(--panel);
  padding: 20px 22px 22px;
  margin: 0 0 14px;
}
/* The heading is the panel's own; it shouldn't carry the page-level top margin. */
.set-panel > .section-head { margin-top: 0; }
.set-panel > .hint:last-child { margin-bottom: 0; }
.set-panel .button-row:last-child { margin-bottom: 0; }
.settings-toggle { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; color: var(--ink); }

/* v21.61 — Settings → Manage tags. Flexbox, not a grid (unlike .subject-row
   above) — nothing here needed adding to tools/audit-a11y.py's "Shrink
   safety" list, since that check only scans for grid-template-columns.
   min-width:0 on the name is still the right defensive move for a flex
   item carrying an ellipsis — without it, a long tag name can push out
   past the row before the ellipsis has anything to clip. */
.tag-manage-list { display: flex; flex-direction: column; gap: 8px; }
.tag-manage-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel-2);
}
.tag-manage-name {
  flex: 1; min-width: 0; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tag-manage-count { flex: 0 0 auto; font-size: 12px; color: var(--mut); }

/* ---------- Brand lockup ----------
   WORDMARK ONLY — handover §7 forbids a graphic charm standing in for the
   logo. The old gradient charm (.lock-mark), gradient rule (.lock-rule) and
   the pre-V4 .brand-title/.brand-sub fallbacks were all deleted in v21.7.3;
   nothing in index.html or js/ referenced them. */
/* v21.7.1: the lockup is a button back to Home — reset the browser's button
   chrome so it still reads as a wordmark, not a control. */
.brand {
  display: block; width: 100%; text-align: left;
  margin: 0 0 26px; padding: 6px 10px; position: relative;
  background: none; border: none; border-radius: var(--r-xs);
  font: inherit; color: inherit; cursor: pointer;
}
.brand:hover .lock-name { color: var(--red); }
.brand:focus-visible { outline: none; box-shadow: var(--ring); }
.lock-name { transition: color 0.15s ease; }
.lock-edition {
  font-family: var(--mono);
  font-size: 9px; font-weight: 400; letter-spacing: 0.26em; text-transform: uppercase;
  color: var(--mut);
}
.lock-row { display: flex; align-items: center; margin-top: 9px; }
.lock-name {
  font-family: var(--disp); font-weight: 800; font-size: 20px;
  text-transform: none; letter-spacing: -0.01em; line-height: 1; color: var(--ink);
}
.lock-os {
  font-family: var(--mono); font-weight: 400; font-size: 9px;
  letter-spacing: 0.3em; text-transform: uppercase; color: var(--mut); margin: 6px 0 0;
}


/* v21.4: architectural nav — a square marker, not a sparkle; a flat tinted
   active row; red for the active label. No gradients in the chrome. */
.nav-button {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  text-align: left;
  background: transparent;
  color: var(--ink);
  border: none;
  border-radius: var(--r-sm);
  padding: 11px 12px;
  margin-bottom: 1px;
  font-family: var(--ui);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
/* A 7px square dot — grey at rest, red when active. */
.nav-button::before {
  content: "";
  width: 7px; height: 7px; flex: none;
  background: #d5d3cb;
  transition: background 0.15s ease;
}
.nav-button:hover { background: #f4f0f0; }
.nav-button.active { background: #f4f0f0; color: var(--accent-ink); font-weight: 600; }
.nav-button.active::before { background: var(--red); }

/* ---------- Main content ---------- */
.content { flex: 1; min-width: 0; padding: 34px 44px; max-width: none; }

.screen { display: none; }       /* hidden by default */
.screen.active { display: block; } /* app.js adds .active to the current screen */

/* V21 type scale (handover §1). Big Shoulders Display is a CONDENSED poster
   face — it only works uppercase, heavy, and tight. Sizes are fluid so the
   same rule serves iPhone, iPad, and desktop. */
h1 {
  margin: 0 0 6px;
  font-family: var(--disp);
  font-size: clamp(38px, 5vw, 56px);
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  line-height: 0.88;
  color: var(--ink);
}
h2 {
  margin: var(--s6) 0 var(--s3);
  font-family: var(--disp);
  font-size: clamp(22px, 2.4vw, 30px);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  line-height: 0.95;
  color: var(--ink);
}
.lead { color: var(--mut); margin: 0 0 26px; font-size: 15px; max-width: 720px; }
.hint { color: var(--mut); font-size: 13px; margin: 0 0 12px; }

/* ===========================================================
   CATEGORY COLOUR SYSTEM (v21.6)
   Handover §4 "Option A": a solid tab in the category colour at the top-left
   of an image, the label under it in the same colour, a neutral shadow for
   depth. Never a coloured pastel shadow.

   The handover fixes three (Outfit=red, Concept=blue, Patch=green); the rest
   map onto the brand spectrum per REDESIGN_LOG.md locked decision 4, because
   the real data model has 14 asset types, not 3.
   =========================================================== */
.cat-outfit   { --cat: var(--red); }
.cat-concept  { --cat: var(--blue); }
.cat-patch    { --cat: var(--green); }
.cat-garment  { --cat: var(--orange); }
.cat-fabric   { --cat: var(--yellow); }
.cat-trim     { --cat: var(--pink); }
.cat-model    { --cat: var(--purple); }
/* v21.7.1: Finished Piece was ink. Now that the category colour is a BORDER
   and selection is an ink outline, an ink-framed thumbnail read as "selected".
   Moved to teal — still distinct from the other eight, no longer ambiguous.
   (Revises REDESIGN_LOG.md locked decision 4.) */
.cat-finished { --cat: #0e8f93; }
.cat-unsorted { --cat: #b9b6c0; }

/* The solid tab that sits on an image. */
.cat-tab {
  position: absolute; top: 8px; left: 0; z-index: 2;
  background: var(--cat, var(--ink)); color: #fff;
  font-family: var(--mono); font-weight: 700; font-size: 9px;
  letter-spacing: 0.08em; text-transform: uppercase;
  padding: 4px 10px;
}
/* The matching label under it. */
/* These take the TEXT-ONLY colour tier declared in :root — never the bright
   token of the same name. The brights stay on .cat-tab, which is ruled
   unchanged (see REDESIGN_LOG.md, v21.15). */
.cat-text-outfit   { color: var(--red); }          /* 4.4999 — accepted, see :root */
.cat-text-concept  { color: var(--blue-ink); }
.cat-text-patch    { color: var(--green-ink); }
.cat-text-garment  { color: var(--orange-ink); }
.cat-text-fabric   { color: var(--gold-ink); }
.cat-text-trim     { color: var(--pink-ink); }
.cat-text-model    { color: var(--purple-ink); }
.cat-text-finished { color: var(--teal-ink); }
.cat-text-unsorted { color: var(--mut); }

/* ===========================================================
   HOME — the landing that replaced the Dashboard (v21.6)
   =========================================================== */
.kicker {
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.24em; text-transform: uppercase; color: var(--red);
  margin-bottom: 14px;
}

/* v21.9.5 — the hero is a flex row: type left, PLATE right. Both columns may
   compress, but each has a floor; below the point where both floors no longer
   fit, the plate LEAVES rather than shrinking to a stamp. That is the whole
   lesson of this step — a flex child with no floor squashes instead of
   stepping out of the way. */
.home-hero {
  display: flex; gap: 56px; align-items: center;
  /* Not 100vh: the stats strip, section cards, recents and footer live below
     this, and a full-height first screen buries all of them on a 1440x900
     laptop. Just enough height that the type reads as centred against the
     plate. */
  min-height: 620px;
  max-width: 1180px;        /* uncapped, the two halves drift apart on a wide monitor */
  padding: var(--s5) 0 var(--s6);
}
.home-hero-copy { width: 560px; min-width: 430px; flex: 0 1 auto; }
.home-hero-copy h1 {
  /* vw, not cqw: the lookbook's query container is the whole app frame
     (sidebar included), which is the viewport here. */
  font-size: clamp(78px, 7.4vw, 112px);
  line-height: 0.86;
  letter-spacing: -0.015em;
  margin: 0 0 18px;
}
.home-hero-copy .lead { max-width: 430px; margin-bottom: 20px; }

/* The seven-swatch spectrum rule under the lead. */
.spectrum-rule { display: flex; gap: 5px; margin-bottom: 26px; }
.spectrum-rule i { width: 34px; height: 7px; }
.spectrum-rule i:nth-child(1) { background: var(--pink); }
.spectrum-rule i:nth-child(2) { background: var(--red); }
.spectrum-rule i:nth-child(3) { background: var(--orange); }
.spectrum-rule i:nth-child(4) { background: var(--yellow); }
.spectrum-rule i:nth-child(5) { background: var(--green); }
.spectrum-rule i:nth-child(6) { background: var(--blue); }
.spectrum-rule i:nth-child(7) { background: var(--purple); }

.home-cta { display: flex; gap: 10px; flex-wrap: wrap; }
.home-cta .primary { grid-column: auto; }

/* ----- THE PLATE -----
   Rainbow rule as the top edge, then a bordered white panel with the picture
   at 4:5 inside it. Taller than the type block on purpose, so it runs above
   and below it. Framed things get a rainbow rule — that is now the system,
   shared with the sign-in card. */
.home-figure {
  margin: 0; padding: 0; cursor: pointer;
  width: 520px; min-width: 400px; flex: 0 1 auto;
}
.home-plate-rule { height: 5px; background: var(--rainbow); }
.home-plate-frame {
  background: var(--panel);
  border: 1px solid var(--line); border-top: 0;
  padding: 16px;
  box-shadow: var(--sh-2);
}
.home-plate-img {
  display: block; width: 100%; aspect-ratio: 4 / 5;
  object-fit: cover; object-position: 50% 12%;
  background: var(--thumb-bg);
}
/* Waiting on the cloud. Deliberately NOT the empty state — at boot we do not
   yet know whether the vault is empty (see renderHomeHero in js/dashboard.js). */
.home-plate-img.is-loading { animation: home-plate-wait 1.4s ease-in-out infinite; }
@keyframes home-plate-wait { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
@media (prefers-reduced-motion: reduce) { .home-plate-img.is-loading { animation: none; } }
.home-plate-empty {
  display: grid; gap: 6px; place-content: center; text-align: center;
  aspect-ratio: 4 / 5; background: var(--bg-flat); color: var(--mut);
}
.home-plate-empty b { font-family: var(--disp); font-size: 20px; text-transform: uppercase; color: var(--ink); }
.home-plate-empty span { font-size: 13px; }

/* Below 1300 the plate goes entirely and the headline takes the width.
   1300, not 1240: at 1240 the content column is ~892px and the two floors plus
   the gap need 886 — add a scrollbar and the plate squashes instead of
   leaving, which is exactly the failure this rule exists to prevent. */
@media (max-width: 1300px) {
  .home-figure { display: none; }
  .home-hero { max-width: none; min-height: 0; }
  .home-hero-copy { width: 100%; min-width: 0; max-width: 720px; }
}

/* Stats strip */
.home-stats {
  display: grid; grid-template-columns: repeat(5, 1fr);
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  margin-bottom: var(--s6);
}
.home-stat { padding: 16px 4px 16px 18px; border-left: 1px solid var(--line); }
.home-stat:first-child { border-left: none; padding-left: 0; }
.home-stat b {
  display: block; font-family: var(--disp); font-weight: 800;
  font-size: 30px; line-height: 1; color: var(--ink);
}
.home-stat span {
  display: block; margin-top: 7px;
  font-family: var(--mono); font-size: 9px; font-weight: 400;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--mut);
}

/* Section headers with a right-hand note or link */
.section-head { justify-content: space-between; }
.section-note {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--mut);
}
.section-link {
  border: none; background: none; cursor: pointer;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--mut);
}
.section-link:hover { color: var(--red); }

/* "Jump back in" cards — a 5px colour bar down the left edge */
.home-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.home-card {
  position: relative; display: flex; flex-direction: column; align-items: flex-start;
  text-align: left; cursor: pointer; font: inherit;
  background: var(--panel); border: 1px solid var(--line); border-left: 5px solid var(--tone, var(--ink));
  border-radius: 0; padding: 18px 18px 14px; min-height: 152px;
  transition: box-shadow 0.15s ease, transform 0.12s ease;
}
.home-card:hover { box-shadow: var(--sh-2); transform: translateY(-2px); }
/* v21.18: TWO variables per tone, because --tone does two jobs.
   --tone paints the card's 5px left border — a SURFACE, which must keep the
   bright token; the *-ink tier is explicitly never for borders.
   --tone-ink paints .home-card-cta, which is 10px bold TEXT on the card's
   opaque white. Measured there, the bright tokens gave Plan 2.61, Open 3.50,
   Create 3.82, Browse 4.15 against a 4.5 bar; the ink twins give 5.15.
   Repointing --tone itself would have darkened the border too. */
.home-card.tone-pink   { --tone: var(--pink);   --tone-ink: var(--pink-ink); }
.home-card.tone-blue   { --tone: var(--blue);   --tone-ink: var(--blue-ink); }
.home-card.tone-green  { --tone: var(--green);  --tone-ink: var(--green-ink); }
.home-card.tone-purple { --tone: var(--purple); --tone-ink: var(--purple-ink); }
.home-card-title {
  font-family: var(--disp); font-weight: 800; font-size: 21px; line-height: 0.95;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
}
.home-card-copy { margin-top: 10px; font-size: 13px; line-height: 1.45; color: var(--mut); flex: 1; }
.home-card-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  width: 100%; margin-top: 12px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
}
.home-card-cta { color: var(--tone-ink, var(--ink)); font-weight: 700; }
.home-card-count { color: var(--mut); }

/* Editorial footer line */
.home-foot {
  display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  margin-top: var(--s6); padding-top: 16px; border-top: 1px solid var(--line);
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--mut);
}

/* Home responsive */
@media (max-width: 1100px) {
  .home-hero { gap: var(--s6); }
  .home-cards { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 820px) {
  /* v21.9.5: the plate is already gone by here (it leaves at 1300); all that
     is left for the phone is tightening the type. */
  .home-hero { gap: var(--s5); padding-top: 0; }
  .home-hero-copy h1 { font-size: clamp(46px, 15vw, 64px); }
  .home-cta .primary { flex: 1; }

  /* Stats stay on ONE line. Five columns of small mono numbers read fine and
     cost ~60px instead of ~200px stacked. */
  .home-stats { grid-template-columns: repeat(5, 1fr); }
  .home-stat { padding: 12px 2px 12px 8px; }
  .home-stat b { font-size: 19px; }
  .home-stat span { font-size: 7.5px; letter-spacing: 0.08em; margin-top: 5px; }

  /* Recently added is sized in the late mobile block — .recent-strip's base
     rule is defined further down this file, so a rule here would lose. */
}
@media (max-width: 480px) {
  .home-cards { grid-template-columns: 1fr; }
  .home-card { min-height: 0; }
  .home-stat { padding: 10px 2px 10px 6px; }
  .home-stat b { font-size: 16px; }
  .home-stat span { font-size: 7px; }
}

/* The Dashboard's stat cards (.stat-grid/.stat-card/.stat-number/.stat-label)
   were deleted in v21.7.3 — Home replaced that screen in v21.6 and nothing in
   index.html or js/ referenced them any more. */

/* ---------- Forms ---------- */
.card-form {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 20px;
  margin-bottom: 24px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  box-shadow: var(--shadow-1);
}
.field { display: flex; flex-direction: column; }
.field.wide { grid-column: 1 / -1; }
.field label { font-size: 12.5px; font-weight: 600; margin-bottom: 5px; color: var(--soft); }

input, select, textarea {
  font: inherit;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--panel-2);
  color: var(--ink);
}
textarea { min-height: 64px; resize: vertical; }
input::placeholder, textarea::placeholder { color: var(--faint); }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}

/* ---------- Buttons (v21.4 primitives) ----------
   Space Mono, uppercase, 2px radius — the handover's .btn language, applied to
   the class names the app already uses so nothing in the JS has to change.
   .primary = the ink anchor, .secondary = the ghost/outline. */
button { font: inherit; }
.primary {
  grid-column: 1 / -1;
  background: var(--primary-bg);
  color: var(--primary-fg);
  border: 1px solid var(--primary-bg);
  border-radius: var(--r-xs);
  padding: 13px 20px;
  cursor: pointer;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.25;              /* Big-Shoulders-adjacent faces clip at 1 */
  transition: background 0.15s ease, border-color 0.15s ease;
}
.primary:hover { background: #000; border-color: #000; }
.primary:focus-visible { outline: none; box-shadow: var(--ring); }

/* The red "create / new" action. Opt-in: .primary.create */
.primary.create { background: var(--red); border-color: var(--red); }
.primary.create:hover { background: #c72a27; border-color: #c72a27; }

.secondary {
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  padding: 11px 16px;
  cursor: pointer;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.25;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.secondary:hover { border-color: var(--ink); color: var(--ink); }
.secondary:focus-visible { outline: none; box-shadow: var(--ring); }

.button-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }

/* `.file-button` was deleted in v21.10 — its only user was the Settings
   "Import backup" label, and that button is gone (it restored into
   localStorage, so it would have reported success and restored nothing). */

/* ---------- Item cards (typed catalog views) ---------- */
.item-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 18px;
}
.item-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-1);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.item-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-2); }
/* v21.14b: keyboard focus must lift the card exactly as hover does, or the
   focus ring sits on a picture with no other sign that it is the active card.
   Same reasoning as .vault-card:focus-within. */
.item-card:focus-within { transform: translateY(-4px); box-shadow: var(--shadow-2); }

/* The keyboard route into a catalog card — modelled on .vault-open. Only the
   PICTURE is inside it; .item-body and the Delete button stay siblings, because
   a <button> may hold only phrasing content and you cannot nest a button in a
   button. The card's own frame is the visual, so this only has to be the right
   size and carry the ring.
   No `display` risk here: .item-open, .item-photo and .placeholder are never
   rendered with the `hidden` attribute — verified across js/ and index.html. */
.item-open {
  display: block; width: 100%; padding: 0; margin: 0;
  border: 0; background: none; cursor: pointer;
  color: inherit; text-align: inherit;
}
.item-open:focus-visible { outline: none; box-shadow: var(--ring); }
.item-photo {
  /* `display: block` is load-bearing. As a direct flex item of .item-card this
     img was blockified automatically; inside .item-open (a block container) it
     went back to `inline`, sat on the text baseline, and the descender gap
     under it made every card 6px taller — measured in Chrome AND Safari, at
     390 and 1440. Never rendered with `hidden`, so no partner rule.
     .item-photo.placeholder overrides this with `display: flex` below. */
  display: block;
  width: 100%;
  height: 210px;
  object-fit: cover;
  background: var(--thumb-bg);
}
.item-photo.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, 0.92);
  font-weight: 600;
  font-size: 13px;
}
.item-body { padding: 14px 15px; flex: 1; }
.item-title { font-weight: 600; margin-bottom: 4px; color: var(--ink); }
.item-meta { font-size: 12.5px; color: var(--soft); line-height: 1.5; }
.tag {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: 11px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  margin: 3px 3px 0 0;
}
.delete-button {
  background: none;
  border: none;
  color: #c0356b;
  cursor: pointer;
  font-size: 12px;
  padding: 9px 15px;
  text-align: left;
  border-top: 1px solid var(--line);
}
.delete-button:hover { background: rgba(192, 53, 107, 0.08); }

.empty {
  color: var(--soft);
  font-style: italic;
  padding: 14px 2px;
}

/* ---------- Toast (v21.9.1) ----------
   Was a rounded maroon block of raw error text. Now the same floating-surface
   language as the bulk-action bar and the dialogs: squared, ink, spectrum along
   the top edge. Errors are marked by a red rule and a headline, not by turning
   the whole thing maroon. */
.toast {
  position: fixed;
  right: 20px;
  bottom: 20px;
  /* v22.7b — 10091: ten above the highest layer in this stylesheet (10081,
     the phone folder sheet and the Prompt Studio thread rail). It was 10000,
     which drew every message UNDER the asset lightbox (10002), the Build
     Sheet editor (10080) and the phone sheets (10081) — so a message fired
     while one of those was open was never seen. Appended to <body> by
     js/toast.js, so this one number is the whole fix. */
  z-index: 10091;
  width: min(360px, calc(100vw - 40px));
  overflow: hidden;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 15px 16px 14px;
  background: var(--ink);
  color: #fff;
  border: none;
  box-shadow: var(--sh-3);
  opacity: 0;
  transform: translateY(12px);
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--rainbow);
}
.toast.toast-show {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
/* Errors: a red rule down the side, and the spectrum gives way to it. */
.toast.toast-error { cursor: pointer; border-left: 3px solid var(--red); }
.toast.toast-error::before { background: var(--red); }

.toast-body { flex: 1; min-width: 0; }
.toast-title {
  display: none;
  font-family: var(--mono); font-weight: 700; font-size: 9.5px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: #ff8b86; margin-bottom: 5px;
}
.toast.toast-error .toast-title { display: block; }
.toast-msg { font-size: 13.5px; line-height: 1.5; }
/* The second line — what to do about it. Only errors set one. */
.toast-hint { margin-top: 6px; font-size: 12.5px; line-height: 1.5; color: rgba(255, 255, 255, 0.66); }
.toast-hint:empty { display: none; }

.toast-close {
  display: none;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  margin-top: -1px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: none;
  color: rgba(255, 255, 255, 0.75);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  align-items: center;
  justify-content: center;
}
.toast-close:hover { border-color: #fff; color: #fff; }
.toast.toast-has-close .toast-close { display: inline-flex; }

/* ---------- Build checklists ---------- */
.checklist {
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 8px 10px;
  max-height: 180px;
  overflow-y: auto;
  background: var(--panel-2);
}
.checklist label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 400;
  padding: 3px 0;
}
.checklist input { width: auto; }

/* ---------- Packet + run sheet output ---------- */
.packet-output {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 18px 20px;
  white-space: pre-wrap;
  line-height: 1.6;
  color: var(--ink);
}
.packet-output ol { padding-left: 22px; }
.packet-output h3 { margin: 14px 0 6px; }

/* ===========================================================
   Visual atelier building blocks
   =========================================================== */

/* ---------- Section header ---------- */
.section-head { display: flex; align-items: center; gap: 10px; margin: 34px 0 14px; }
/* `display: flex` beats the `hidden` ATTRIBUTE, so a hidden section head still
   showed — with nothing under it. THIRD time this shape has bitten the project
   (.card-form, .section-head, and .review-details / .detail-concept below).
   Any class that sets `display` on an element that is ever rendered with the
   `hidden` attribute needs this partner rule. tools/audit-a11y.py checks. */
.section-head[hidden] { display: none !important; }
.section-head h2 { margin: 0; }

/* ---------- Connected-asset thumbnails (Projects board) ---------- */
.thumb {
  width: 46px;
  height: 46px;
  border-radius: var(--r-sm);
  object-fit: cover;
  border: 2px solid var(--panel);
  box-shadow: var(--shadow-1);
  background: var(--thumb-bg);
}
.thumb.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  color: var(--mut);
  text-align: center;
}

/* ===========================================================
   Project Workroom (v21.9) — project rows + full-page workspace
   -----------------------------------------------------------
   Handover §5.4. The project card is a ROW (thumbnail + name +
   "Category · N steps · %" + progress bar), not an image tile: on this
   screen you are picking up work, not browsing pictures. The old
   `.project-card` / `.wr-card-*` tile styles are deleted with it.
   =========================================================== */
.wr-plist { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 12px; }
/* v21.10d: the row's thumbnail + body are one button; the delete button is
   its sibling. Same shape as .vault-open on a Vault card. */
.wr-pj-open {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 13px;
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  border-radius: var(--r-xs);
}
.wr-pj-open:focus-visible { outline: none; box-shadow: var(--ring); }
/* Keyboard focus should light the row the way hover does. */
.wr-pj:focus-within { border-color: var(--ink); box-shadow: var(--sh-2); }

.wr-pj {
  position: relative;
  display: flex; align-items: center; gap: 13px;
  background: var(--panel); border: 1px solid var(--line);
  padding: 12px; cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.12s ease;
}
.wr-pj:hover { border-color: var(--ink); box-shadow: var(--sh-2); transform: translateY(-2px); }
/* The category colour arrives as an edge stripe down the thumbnail — the same
   "colour lives in the frame" rule the Vault follows (handover §7). */
.wr-pj-thumb {
  position: relative; flex: none;
  width: 58px; height: 58px; overflow: hidden;
  background: var(--thumb-bg);
  border-left: 3px solid var(--cat, var(--ink));
}
.wr-pj-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wr-pj-ph { width: 100%; height: 100%; background: var(--thumb-bg); }
.wr-pj-body { flex: 1; min-width: 0; }
.wr-pj-name {
  font-family: var(--disp); font-weight: 800; font-size: 16px; line-height: 1.05;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wr-pj-meta {
  margin-top: 4px;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--mut);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wr-pj-prog { height: 5px; background: var(--line); margin-top: 9px; overflow: hidden; }
.wr-pj-prog i { display: block; height: 100%; background: var(--green); }
.wr-pj-next {
  margin-top: 7px; font-size: 12px; color: var(--mut); line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wr-pj-next span {
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--mut);
}
/* Quiet until you go near the card — deleting a project should take intent. */
.wr-pj-del {
  flex: none; align-self: flex-start;
  width: 24px; height: 24px; padding: 0; line-height: 1;
  border: 1px solid transparent; background: none; color: var(--mut);
  font-size: 15px; cursor: pointer; opacity: 0;
  transition: opacity 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.wr-pj:hover .wr-pj-del, .wr-pj-del:focus-visible { opacity: 1; }
.wr-pj-del:hover { color: var(--red); border-color: var(--red); }

/* Manual project creation disclosure — v21.1 put it ABOVE the project cards as
   a prominent bar, deliberately matching the "Full Maker Plan" bar inside a
   project (.wr-details-summary) so the two read as one family. v21.9 restyles
   both into the V21 language at once.
   The +/− marker comes from builds.js's button text, not a pseudo-element. */
.wr-manual { margin: 2px 0 20px; }
.wr-manual-toggle {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  cursor: pointer; font: inherit;
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink); background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: 15px 18px;
}
.wr-manual-toggle:hover { border-color: var(--ink); }
.wr-manual-toggle:focus-visible { outline: none; box-shadow: var(--ring); }
.wr-manual .card-form { margin-top: 12px; }
/* .card-form sets display:grid, which overrides the [hidden] attribute's
   display:none — so make the hidden state win for the disclosure form. */
.card-form[hidden] { display: none !important; }

/* --- full-page workspace --- */
.wr-loading { color: var(--mut); padding: 24px 2px; }
.wr-topbar { margin-bottom: 16px; }
.wr-back {
  background: var(--panel); border: 1px solid var(--line); color: var(--mut);
  border-radius: var(--r-xs); padding: 9px 15px; cursor: pointer;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.wr-back:hover { border-color: var(--ink); color: var(--ink); }

.wr-grid { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 28px; align-items: start; }

/* left rail */
/* v21.9.2: the rail does NOT scroll on its own any more. It used to be sticky
   with `max-height` + `overflow-y: auto`, which meant its contents were cut off
   mid-panel with a second scrollbar inside the page — Josh's screenshot. It is
   now a plain column at its natural height that scrolls with the page. */
.wr-rail {
  background: var(--panel); border: 1px solid var(--line);
  box-shadow: var(--sh-1);
}
.wr-hero { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; background: var(--thumb-bg); }
.wr-hero.placeholder {
  display: flex; align-items: center; justify-content: center; text-align: center;
  background: var(--thumb-bg); color: var(--mut);
  font-family: var(--disp); font-weight: 800; font-size: 18px;
  text-transform: uppercase; padding: 14px;
}
.wr-rail-block { padding: 14px 16px; border-top: 1px solid var(--line); }
.wr-rail-block:first-of-type { border-top: none; }
.wr-kicker {
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--red);
}
.wr-name {
  font-family: var(--disp); font-weight: 800; font-size: 24px; line-height: 0.95;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink); margin: 8px 0 0;
}
/* v22.25a — the project name with ✎ Rename beside it (in the text block,
   never on the picture), and the inline box that replaces them. */
.wr-name-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-top: 8px; }
.wr-name-row .wr-name { margin: 0; flex: 1; min-width: 0; overflow-wrap: anywhere; }
.wr-rename {
  flex: none; background: none; border: 1px solid var(--line); border-radius: var(--r-xs); cursor: pointer;
  font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--mut); padding: 6px 9px; min-height: 32px; white-space: nowrap;
}
.wr-rename:hover { color: var(--ink); border-color: var(--ink); }
.wr-rename:focus-visible { outline: none; box-shadow: var(--ring); }
.wr-rename-form { margin-top: 8px; }
.wr-rename-label {
  display: block; margin-bottom: 5px; font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut);
}
.wr-rename-input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 15px; color: var(--ink);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px;
}
.wr-rename-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.wr-rename-msg { font-size: 12.5px; line-height: 1.45; color: var(--mut); margin: 6px 0 0; }
.wr-rename-msg:empty { display: none; }
.wr-rename-msg.is-error { color: var(--accent-ink); }
.wr-rename-actions { display: flex; gap: 8px; margin-top: 10px; }
.wr-rename-actions button { flex: 1 1 0; min-width: 0; min-height: 40px; }
.wr-rename-actions button:disabled { opacity: 0.55; cursor: default; }
@media (max-width: 820px) {
  .wr-rename { min-height: 44px; padding: 0 12px; }
  .wr-rename-actions button { min-height: 44px; }
}
.wr-name-row[hidden], .wr-rename-form[hidden], .wr-rename-actions[hidden], .wr-rename-label[hidden] { display: none !important; }
.wr-rail-label {
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut); margin-bottom: 7px;
}
.wr-rail-head { margin-bottom: 12px; }
.wr-prompt { font-size: 13px; color: var(--soft); line-height: 1.6; white-space: pre-wrap; }
.wr-engine { display: flex; flex-wrap: wrap; gap: 6px; }
.wr-muted { font-size: 12.5px; color: var(--mut); margin: 0; }
.wr-src-group { margin-top: 12px; }
.wr-src-group:first-child { margin-top: 0; }
.wr-src-row { display: flex; flex-wrap: wrap; gap: 8px; }
/* v22.1b — the "add a moodboard" row in the rail. Flex, not grid, so the
   audit's grid-item shrink rule does not apply; the select carries its own
   min-width: 0 so a long board name cannot push the button out of the rail. */
.wr-boards-add { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.wr-boards-add select { flex: 1 1 auto; min-width: 0; }
.wr-src {
  width: 56px; height: 66px; padding: 0; border: 1px solid var(--line);
  overflow: hidden; cursor: pointer; background: var(--thumb-bg); flex: none;
}
.wr-src:hover { border-color: var(--ink); }
.wr-src img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wr-src-ph { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: var(--mut); }

/* main / Maker Plan */
.wr-main {
  background: var(--panel); border: 1px solid var(--line);
  box-shadow: var(--sh-1); padding: 22px 26px 26px;
}
.wr-main-head { margin-bottom: 16px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
.wr-main-head .wr-lead { color: var(--mut); font-size: 14px; margin: 8px 0 0; max-width: 62ch; }
/* The Maker Plan heading in the display face, matching every other page head. */
.wr-main-head .wr-kicker + .wr-lead { margin-top: 6px; }
.wr-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.wr-field { margin-top: 16px; }
.wr-field > label {
  display: block; margin-bottom: 5px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut);
}
.wr-field-hint { font-size: 12px; color: var(--mut); margin: 0 0 6px; }
.wr-field input[type="text"], .wr-field textarea, .wr-field select {
  width: 100%; box-sizing: border-box; font: inherit; color: var(--ink);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px;
}
.wr-field textarea { resize: vertical; line-height: 1.55; }
.wr-field input:focus, .wr-field textarea:focus, .wr-field select:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.wr-field textarea:disabled { opacity: 0.6; cursor: not-allowed; }
.wr-actions { margin-top: 22px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.wr-actions .primary { grid-column: auto; }
/* v21.9.2: the same row, repeated under the page header, so Regenerate and
   Download are reachable without scrolling a long plan to the bottom. */
.wr-actions-top {
  margin: 0 0 18px; padding-bottom: 16px; gap: 14px;
  border-bottom: 1px solid var(--line);
}
.wr-plan-date { font-size: 12.5px; color: var(--mut); margin: 0 0 10px; }

/* Maker Plan empty state (before a plan exists). An empty state is a brand
   moment (design system §"Brand moments"), so the spectrum shows up here. */
.wr-empty-plan {
  position: relative; overflow: hidden;
  text-align: center; padding: 36px 22px 34px;
  border: 1px solid var(--line); background: var(--panel);
}
.wr-empty-plan::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
  background: var(--rainbow);
}
.wr-empty-emoji { font-size: 30px; line-height: 1; margin-bottom: 12px; color: var(--red); }
.wr-empty-title {
  font-family: var(--disp); font-weight: 800; font-size: 28px; line-height: 1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink); margin: 0 0 10px;
}
.wr-empty-copy { color: var(--mut); font-size: 14px; line-height: 1.6; max-width: 460px; margin: 0 auto 22px; }
.wr-empty-actions { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.wr-empty-actions .primary { grid-column: auto; }

/* text-style secondary button (Start manually / Regenerate) */
.wr-textbtn {
  background: none; border: none; cursor: pointer; padding: 6px 4px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut);
}
.wr-textbtn:hover:not(:disabled) { color: var(--ink); text-decoration: underline; }
/* v22.4 — this stylesheet has no global `button:disabled` rule on purpose
   (see the v21.69 note at .shopping-foot .primary:disabled), so every
   disabled control carries its own line. Without this, v22.1b's
   "+ Add moodboard" set the disabled ATTRIBUTE — correctly — and still
   looked fully live: same colour, still `cursor: pointer`, still
   underlining on hover. Same shape and same numbers as .studio-saveall,
   the app's other text-style button that greys out. The :not(:disabled)
   on the hover rule above is what stops the underline. */
.wr-textbtn:disabled { color: var(--faint); cursor: default; text-decoration: none; }

/* The app's shared "AI is working" card. Started life in the Workroom (first
   pass + Unsure regeneration) and keeps the `wr-` prefix; since v21.9.2 the
   Design Studio uses it too while an image generates. Kept expressive on
   purpose — a loading state is a brand moment, and the spectrum spark is the
   app's one piece of motion. Squared to match V21. */
.wr-generating { position: relative; overflow: hidden; padding: 34px 26px; text-align: center; border: 1px solid var(--line); background: var(--panel); box-shadow: var(--sh-1); }
.wr-generating::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--rainbow); }
.wr-gen-spark { width: 60px; height: 60px; border-radius: 50%; margin: 0 auto 18px; display: flex; align-items: center; justify-content: center; background: var(--rainbow); color: #fff; font-size: 28px; box-shadow: var(--sh-2); animation: wr-gen-spin 3.2s linear infinite; }
.wr-gen-spark span { display: block; animation: wr-gen-pulse 1.4s ease-in-out infinite; }
.wr-gen-title { font-family: var(--disp); font-weight: 800; font-size: 26px; line-height: 1; text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink); margin: 0 0 10px; }
.wr-gen-sub { font-size: 14px; line-height: 1.6; color: var(--mut); margin: 0 auto 18px; max-width: 420px; }
.wr-gen-bar { height: 5px; background: var(--line); overflow: hidden; max-width: 320px; margin: 0 auto 18px; }
.wr-gen-bar i { display: block; height: 100%; width: 40%; background: var(--rainbow); animation: wr-gen-slide 1.6s ease-in-out infinite; }
.wr-gen-note { display: inline-flex; align-items: center; gap: 7px; font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut); }
@keyframes wr-gen-spin { to { transform: rotate(360deg); } }
@keyframes wr-gen-pulse { 0%, 100% { opacity: 0.65; transform: scale(0.9); } 50% { opacity: 1; transform: scale(1.12); } }
@keyframes wr-gen-slide { 0% { transform: translateX(-60%); } 100% { transform: translateX(260%); } }
@media (prefers-reduced-motion: reduce) { .wr-gen-spark, .wr-gen-spark span, .wr-gen-bar i { animation: none; } }

/* v21.9: the `.wr-cf-*` confirm dialog is retired. workroom.js's wrConfirm is
   now a thin adapter over the shared window.atelierConfirm (js/ask.js), so the
   app has ONE dialog language — see the `.ask-*` block further down. */

/* Auto-height edit textareas (no cramped internal scrollbars) */
.wr-field textarea.wr-grow { overflow: hidden; resize: vertical; min-height: 78px; }

/* Readable Maker Plan view */
.wr-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.wr-chip {
  display: inline-flex; align-items: baseline; gap: 7px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: 6px 11px; font-size: 12.5px; color: var(--ink);
}
.wr-chip-k { font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut); }

.wr-read-card { background: var(--panel); border: 1px solid var(--line); padding: 16px 18px; margin-bottom: 12px; }
/* The hero section (Recommended Build Strategy) is marked with a red rule, not
   a tinted fill — colour lives in the frame. */
.wr-read-hero { border-left: 3px solid var(--red); }
.wr-read-label {
  font-family: var(--disp); font-weight: 800; font-size: 17px; line-height: 1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink); margin: 0 0 10px;
}
.wr-read-p { margin: 0 0 7px; color: var(--ink); font-size: 14px; line-height: 1.6; }
.wr-read-p:last-child { margin-bottom: 0; }
.wr-read-list { margin: 0; padding-left: 20px; }
.wr-read-list li { color: var(--ink); font-size: 14px; line-height: 1.6; margin-bottom: 5px; }
.wr-read-list li:last-child { margin-bottom: 0; }
.wr-read-note { font-size: 12.5px; color: var(--mut); margin: 0 0 8px; }
.wr-read-empty { color: var(--mut); margin: 0; }

/* ----- Construction steps you tick off (v21.9) -----
   The Workroom is a making tool, so the steps are checkboxes, not a numbered
   list you read. Done state persists per step; see js/workroom.js planSteps. */
.wr-progress { display: flex; align-items: center; gap: 12px; margin: 0 0 12px; }
.wr-progress-num {
  flex: none; font-family: var(--mono); font-weight: 700; font-size: 9.5px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut);
}
.wr-progress-bar { flex: 1; height: 5px; background: var(--line); overflow: hidden; }
.wr-progress-bar i { display: block; height: 100%; background: var(--green); transition: width 0.2s ease; }
.wr-tasks { display: block; }
.wr-task {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 10px 0; border-bottom: 1px solid var(--line);
  font-size: 13.5px; line-height: 1.5; color: var(--ink); cursor: pointer;
}
.wr-task:last-child { border-bottom: none; }
.wr-task-box { flex: none; width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--green); cursor: pointer; }
.wr-task-n {
  flex: none; min-width: 18px; margin-top: 1px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; color: var(--mut);
}
.wr-task.done .wr-task-text { color: var(--mut); text-decoration: line-through; }
 /* Measured, not assumed: this number sits on section.wr-read-card, which is
    an opaque rgb(255,255,255) — a real white panel, unlike the category labels
    on the canvas. --green was 2.61:1 there; --green-ink is 5.15:1. */
.wr-task.done .wr-task-n { color: var(--green-ink); }

/* ----- The two named plan sections (v21.9.2) -----
   These are the page's real headings. They used to be a 10px red kicker and a
   mono bar, which is why you couldn't tell they WERE the headings. Now: a
   display-face title with a line under it saying what's inside, and a big
   click target that spans the whole bar. */
.wr-sec { margin: 0 0 20px; border: 1px solid var(--line); background: var(--panel); }
.wr-sec-head {
  cursor: pointer; padding: 18px 20px; list-style: none; user-select: none;
  display: flex; align-items: flex-start; gap: 13px;
}
.wr-sec-head::-webkit-details-marker { display: none; }
.wr-sec-head:hover { background: var(--panel-2); }
.wr-sec-mark {
  flex: none; margin-top: 2px; font-size: 17px; line-height: 1;
  color: var(--mut); transition: transform 0.15s ease, color 0.15s ease;
}
.wr-sec[open] .wr-sec-mark { transform: rotate(90deg); color: var(--red); }
.wr-sec-text { min-width: 0; }
.wr-sec-title {
  display: block;
  font-family: var(--disp); font-weight: 800; font-size: 26px; line-height: 1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
}
.wr-sec-sub { display: block; margin-top: 6px; font-size: 13px; line-height: 1.45; color: var(--mut); }
.wr-sec[open] .wr-sec-head { border-bottom: 1px solid var(--line); }
.wr-sec-body { padding: 16px 18px; }
.wr-sec-body .wr-read-card:last-child { margin-bottom: 0; }
/* Cards inside the closed-by-default section sit on the tinted ground, so the
   two sections don't read as one flat wall of white boxes. */
.wr-sec[data-sec="bench"] .wr-sec-body .wr-read-card { background: var(--panel-2); }

/* Notes for the next pass */
.wr-nextnotes { margin: 18px 0 4px; padding: 14px 16px; border: 1px solid var(--line); background: var(--panel-2); }
.wr-nextnotes > label {
  display: block; margin-bottom: 5px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut);
}
.wr-nextnotes textarea { width: 100%; box-sizing: border-box; font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px; line-height: 1.55; overflow: hidden; resize: vertical; min-height: 56px; }
.wr-nextnotes textarea:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.wr-nextnotes-actions { margin-top: 8px; }

/* Future Vault-aware placeholder (non-functional) */
.wr-scan-stub { border: 1px dashed var(--line); padding: 14px 16px; margin: 16px 0; background: var(--panel-2); }
.wr-scan-head { font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut); }
.wr-scan-copy { font-size: 12.5px; color: var(--mut); margin: 6px 0 0; line-height: 1.5; }

/* ---------- v12.1: "Use from Vault" grouped clickable asset chips ---------- */
.wr-vgroup { margin: 0 0 12px; }
.wr-vgroup:last-child { margin-bottom: 0; }
.wr-vgroup-label { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut); margin: 0 0 6px; }
.wr-vlist { list-style: none; margin: 0; padding: 0; }
.wr-vline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 8px; }
.wr-vline:last-child { margin-bottom: 0; }
.wr-vchip {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 4px 12px 4px 4px; font: inherit; font-size: 13px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xs);
  cursor: pointer; transition: box-shadow 0.15s ease, border-color 0.15s ease;
  /* v22.25a — never wider than its line: on a 390px phone a long title
     pushed the chip past the card's edge. The title shortens instead. */
  max-width: 100%; box-sizing: border-box; min-width: 0;
}
.wr-vchip:hover { border-color: var(--ink); box-shadow: var(--sh-1); }
.wr-vchip-thumb { width: 28px; height: 28px; overflow: hidden; background: var(--thumb-bg); flex: none; }
.wr-vchip-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wr-vchip-title { font-weight: 600; color: var(--ink); max-width: 220px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wr-vchip-type { font-size: 11px; color: var(--mut); flex: none; }
.wr-vline-note { font-size: 13px; color: var(--soft); line-height: 1.5; }

/* ---------- v21.34 (Phase C2): "Materials" ideal + paired match ---------- */
.wr-mat-list { list-style: none; margin: 0; padding: 0; }
.wr-mat { margin: 0 0 14px; padding: 0 0 14px; border-bottom: 1px solid var(--line); }
.wr-mat:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
.wr-mat-ideal { font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.5; margin: 0 0 8px; }
.wr-mat-have { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 4px; }
.wr-mat-have:last-child { margin-bottom: 0; }
.wr-mat-need { font-size: 13px; color: var(--soft); line-height: 1.5; }
.wr-mat-need-label { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut); margin-right: 6px; }
.wr-mat-note { font-size: 13px; color: var(--soft); line-height: 1.5; }

/* v21.9: was 900px. Same arithmetic as the Design Studio (locked decision 15):
   a 340px rail plus the sidebar and gutters leaves the plan column unusable
   below ~1100, and 900 let it break at iPad portrait. Stack at 1100. */
@media (max-width: 1100px) {
  .wr-grid { grid-template-columns: 1fr; gap: 18px; }
  .wr-row2 { grid-template-columns: 1fr; }
  /* Stacked, the rail comes FIRST — and a 4:5 hero across the full width is
     487px of picture on a phone before you reach the project name, let alone
     the plan. It becomes a banner instead. (Same call as hiding the Home hero
     on mobile in v21.7.5.) */
  .wr-hero { aspect-ratio: 3 / 1; object-position: 50% 22%; }
}

/* ---------- Color swatch chip (Materials) ---------- */
.color-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--soft); }
.color-dot {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.14);
  background: #eee;
  display: inline-block;
}

/* ---------- Search / filter box ---------- */
.search-box {
  width: 100%;
  padding: 11px 16px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  margin-bottom: 16px;
  font-size: 15px;
  background: var(--panel-2);
  color: var(--ink);
}
.search-box::placeholder { color: var(--mut); }
.search-box:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

/* ---------- Recently-added photo strip (Home) ----------
   v21.6: images sit directly on the canvas with a category tab and a caption
   beneath — plates in a lookbook, not boxed cards. Depth comes from a neutral
   shadow on the image only. */
.recent-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 20px; padding: 2px 0 4px; }
/* v21.10d: a real <button> now. Strip the UA chrome and keep the tile look. */
.recent-item {
  cursor: pointer; display: block; width: 100%;
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left;
}
.recent-item:focus-visible { outline: none; }
.recent-item:focus-visible .recent-photo-wrap { box-shadow: var(--ring), var(--sh-2); }
.recent-photo-wrap { position: relative; overflow: hidden; box-shadow: var(--sh-1); transition: box-shadow 0.15s ease, transform 0.12s ease; }
.recent-item:hover .recent-photo-wrap { box-shadow: var(--sh-2); transform: translateY(-3px); }
.recent-item:focus-visible .recent-photo-wrap { transform: translateY(-3px); }
.recent-photo { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; background: var(--thumb-bg); }
.recent-photo.placeholder { background: var(--thumb-bg); }
.recent-body { padding: 11px 0 0; }
.recent-name {
  font-family: var(--disp); font-weight: 800; font-size: 15px; line-height: 1.05;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.recent-cat {
  margin-top: 5px; font-family: var(--mono); font-weight: 700; font-size: 9px;
  letter-spacing: 0.1em; text-transform: uppercase;
}

/* ---------- Badge ---------- */
.badge {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: 11px;
  font-weight: 600;
  padding: 3px 11px;
  border-radius: var(--r-pill);
}

/* ===========================================================
   Sparkle Vault — image-first library
   =========================================================== */
/* v21.7: the dropzone was a 26px-padded panel eating a third of the fold on a
   gallery screen. It is now a slim strip — drag-and-drop is unchanged, and the
   primary upload action moved up to the "+ New piece" button in the header. */
.vault-dropzone {
  display: block;
  border: 1px dashed var(--line);
  border-radius: var(--r-xs);
  background: var(--panel);
  padding: 11px 14px;
  text-align: center;
  cursor: pointer;
  margin-bottom: 12px;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.vault-dropzone:hover { border-color: var(--ink); }
.vault-dropzone.drag { border-color: var(--red); background: var(--accent-soft); }
/* The big emoji and the third line only earn their space while dragging. */
.vault-dropzone-emoji { display: none; }
.vault-dropzone-inner { color: var(--mut); display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 12.5px; }
.vault-dropzone-inner .hint { display: none; }
.vault-dropzone-inner strong { color: var(--ink); font-weight: 600; }
.vault-dropzone.drag .vault-dropzone-emoji { display: block; font-size: 22px; }

.vault-toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 18px; }
.vault-toolbar .search-box { flex: 1; min-width: 220px; margin-bottom: 0; }
.vault-filter { padding: 11px 14px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel-2); color: var(--ink); font: inherit; }
.vault-filter:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
/* v22.16 — on a desktop the filters sit on ONE row beside the search box.
   The global `select { width: 100% }` (the min-width section, ~4667) made
   every filter its own full-width line: 373px of toolbar at 1440px wide.
   Scoped to .vault-toolbar, so no other select or input changes size. The
   phone half of this fix is in the ≤820px block (`input.search-box`).

   ★ THE 140px CAP IS MEASURED, NOT TASTE. A <select> is as wide as its
   LONGEST option, not the one showing: one long tag name made "All tags"
   310px wide, and the colour counter made "All colors" 249px. Uncapped, the
   four could not share a row with the search box in the 791px toolbar a
   1440px window gives (the mock's selects held one short option each, so
   it never showed). The widest label that must read whole, "All statuses",
   is 135px; a longer choice is cut off in the closed box and shown whole in
   the open list. 180px + 4 × 140px + the gaps fits 791px, and the search
   box grows into whatever is left. The sort keeps its own width (138px),
   which is what sends it to the second row with Select all. */
@media (min-width: 821px) {
  .vault-toolbar .vault-filter { width: auto; flex: 0 1 auto; max-width: 140px; }
  .vault-toolbar #vault-sort { max-width: none; }
  .vault-toolbar .search-box { flex: 1 1 180px; }
}
/* v22.17 — 821–1280px, where the folder rail sits beside the toolbar and
   leaves it 452–709px wide (measured in v22.16): too narrow for search plus
   four filters, which wrapped one to three of them. Here the search box gets
   its own full row and the four filters share the row below in equal parts;
   sort, Select all and density follow on the next.
   ★ HOW THE BREAK IS MADE: the toolbar's ::after is a zero-height,
   full-width flex item, and `order` puts it after the four filters (DOM
   order is untouched, so reading and tab order stay as they are). Its own
   line would add an empty row-gap, so spacing here is a top margin on each
   row's items instead (row-gap 0). ≥1281px is exactly v22.16's layout. */
@media (min-width: 821px) and (max-width: 1280px) {
  .vault-toolbar { row-gap: 0; }
  .vault-toolbar .search-box { flex: 1 1 100%; }
  .vault-toolbar .vault-filter { flex: 1 1 0; min-width: 0; max-width: none; order: 1; margin-top: 10px; }
  .vault-toolbar::after { content: ""; flex: 0 0 100%; height: 0; order: 2; }
  .vault-toolbar #vault-sort { flex: 0 1 auto; order: 3; }
  .vault-toolbar #vault-select-all,
  .vault-toolbar #vault-density { order: 3; margin-top: 10px; }
}

/* v22.16 — the quiet line under the grid while more of the list is still to
   be drawn (#vault-more, js/vault.js syncMoreRow). It sets `display`, so it
   needs the [hidden] partner rule or `hidden` would never hide it. */
.vault-more-row {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--mut); padding: 22px 0 6px;
}
.vault-more-row[hidden] { display: none; }
.vault-more-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--rainbow); flex: none; }

/* v21.7: two densities. Compact is the default and the point of the redesign —
   ~5-up at desktop instead of 3-up, so roughly twice as much of the vault is
   visible before you scroll. The choice is remembered per device. */
.vault-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 22px; }
.vault-gallery.compact { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
/* v21.7.1 "Sheet" — a contact sheet. Small SQUARE thumbnails, no captions:
   built for scrolling a lot of work quickly rather than reading about it. */
.vault-gallery.sheet { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; }
.vault-gallery.sheet .vault-photo { aspect-ratio: 1 / 1; }
.vault-gallery.sheet .vault-card-body { display: none; }
.vault-gallery.sheet .vault-dl { display: none; }     /* no room, and not the job here */
.vault-gallery.sheet .vault-heart { width: 26px; height: 26px; font-size: 13px; bottom: 5px; right: 5px; }
.vault-gallery.sheet .vault-check { width: 17px; height: 17px; top: 5px; right: 5px; }
.vault-gallery.sheet .cat-tab { font-size: 7.5px; padding: 2px 5px; top: 5px; }

/* Segmented control (density here; engine lanes later). */
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r-xs); background: var(--panel); overflow: hidden; }
.seg-btn {
  border: none; background: none; cursor: pointer;
  padding: 10px 14px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut);
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.active { background: var(--ink); color: #fff; }

/* Scope header actions */
.vault-scope-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.vault-scope-actions .primary { grid-column: auto; white-space: nowrap; }

/* v21.8.0: "+ Click to upload" sits at the top of the folder rail, which is
   already sticky — so the action stays reachable at any scroll depth WITHOUT
   floating over the gallery, which is what looked wrong pinned to the right.
   Rendered by renderSidebar() in js/vault.js. */
#vault-new-piece {
  display: block; width: 100%; margin-bottom: 14px;
  grid-column: auto; text-align: center;
}

/* Multi-select checkbox + selection bar */
/* Top-RIGHT so a browser "Save to Pinterest" overlay (top-left) can't cover it. */
.vault-check {
  position: absolute; top: 9px; right: 9px; z-index: 3;
  width: 21px; height: 21px; cursor: pointer; accent-color: var(--red);
}
/* NOTE: the pre-V21 `.vault-card.selected` rule that used to live here
   (a pink outline round the WHOLE card plus a pink glow) is deleted. It was
   still firing alongside the v21.7 rule, boxing the caption in as well as the
   image and making a selected card read as narrower than its neighbours —
   Josh's "the view becomes squished". Selection is now expressed once, on the
   image only: see `.vault-card.selected .vault-photo-wrap`. */

/* ---------- Bulk-action bar (v21.7.2) ----------
   Was a rounded dark pill with translucent default selects. Rebuilt in the
   V21 language: squared, ink, mono labels, real controls. */
/* v21.7.4: the bar earns a bit of drama. It is the only floating surface in
   the app, so it gets the rainbow along its top edge — the same signature as
   the page header — and the count is set in the display face. */
/* ★ v21.87 — the bar is a COLUMN now: one compact row, plus a panel that is
   only there when asked for. It used to be a single wrapping flex row of
   eleven controls, which measured 297px tall and sat over the grid the whole
   time anything was selected. The reserve mechanism (--vault-sel-reserve,
   below) needs no change: it measures whatever height this ends up being. */
.vault-selection {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  z-index: 10050; max-width: min(94vw, 940px);
  /* ★ v22.5a — WITHOUT THIS THE BAR IS CAPPED AT HALF THE VIEWPORT. It is
     `position: fixed` with `left: 50%` and no width, so its shrink-to-fit
     available space is (viewport − left) = 50vw, and the transform only
     recentres it afterwards. Until v22.5 the select grid's intrinsic
     min-content held it wider than that cap; v22.5's `min-width: 0` on grid
     items removed that floor, and at ~850px the bar collapsed to ~398px and
     wrapped "Clear" onto a second line. max-content sizes it to its real
     content and lets the max-width above do the clamping it was always
     meant to do. The two breakpoint rules set their own width, so only the
     821–1100px floating range is affected. */
  width: max-content;
  display: flex; flex-direction: column; gap: 10px; align-items: stretch;
  padding: 7px 14px 8px;
  background: var(--ink); border: none; border-radius: var(--r-md);
  box-shadow: var(--sh-3);
  color: #fff;
  overflow: hidden;                 /* keeps the rainbow inside the corners */
  animation: vault-sel-in 0.18s ease-out;
}
/* The collapsed row: N selected · Actions · Moodboard ......... Clear.
   Its buttons are compact on purpose — the whole point is a bar that reads as
   a strip rather than a panel, and the app's default .secondary padding
   (11px 16px) put the closed bar at 59px. 8px keeps it under 48. */
.vault-sel-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.vault-sel-row .secondary { padding: 8px 13px; }
.vault-sel-spacer { flex: 1; }

/* The Actions button, and the chevron that says which way this will go. */
.vault-sel-toggle { position: relative; display: inline-flex; align-items: center; gap: 6px; }
.vault-sel-chev { display: inline-block; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid currentColor; transition: transform .15s ease; }
.vault-sel-toggle[aria-expanded="true"] .vault-sel-chev { transform: rotate(180deg); }

/* ★ A staged change must not be invisible behind a collapsed panel — that is
   the trap this whole change introduces. The badge carries the COUNT of
   staged edits and rides on the very button you would press to see them. */
.vault-staged-badge {
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
  background: var(--red); color: #fff;
  font-family: var(--mono); font-size: 9px; font-weight: 700; line-height: 16px;
  text-align: center;
}

/* The panel. Opened by the .open CLASS — it sets `display`, so pairing it
   with the `hidden` attribute is the trap CLAUDE.md documents four times. */
.vault-sel-panel { display: none; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, .16); }
.vault-sel-panel.open { display: block; }
/* Two across, not four stacked — half the height for the same controls. */
.vault-sel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.vault-sel-panel #vault-pending-summary { display: block; margin-top: 8px; }
.vault-sel-foot { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }

@media (max-width: 760px) {
  /* One column when there is no room for two; the bar still must not grow
     wider than the viewport. */
  .vault-sel-grid { grid-template-columns: 1fr; }
}

/* ★ v22.5 — on wide screens the bar docks over the left rail (dead space
   while selecting) so it never sits on the images. Josh's own suggestion,
   third time raised.

   left/width are MEASURED from the rail by syncSelectionReserve()
   (js/vault.js) and arrive as custom properties; the fallbacks are only
   what to draw in the one frame before the first measurement lands. 1101px
   matches the JS constant and the rail's own desktop breakpoint — below it
   the rail is a chip strip, then a bottom sheet, and docking to either
   would put the bar somewhere meaningless. The base rule is untouched, so
   everything narrower keeps the centred floating bar exactly as it was.
   `width` with `max-width: none` because the base rule's
   max-width: min(94vw, 940px) would otherwise still win on a wide window. */
@media (min-width: 1101px) {
  .vault-selection {
    left: var(--vault-sel-left, 280px);
    transform: none;
    width: var(--vault-sel-width, 268px);
    max-width: none;
    /* ★ v22.5a — ITS OWN KEYFRAMES, and this is not cosmetic. An animation's
       keyframe value beats a normal declaration for the whole of its run, so
       the base `vault-sel-in` (which animates translate(-50%, …), still
       correct below 1101px) was overriding the untransformed value declared
       two lines above and drawing the docked bar ~134px to its left for
       180ms before it snapped into place. Same motion, no X translation. */
    animation-name: vault-sel-in-docked;
  }
  /* 268px has no room for two select columns. */
  .vault-selection .vault-sel-grid { grid-template-columns: 1fr; }
  /* ★ The collapsed row WRAPS to two lines at this width (estimated: count
     ~119px + Actions ~92px on line 1, Moodboard + Clear on line 2), which
     flex-wrap already handles and which takes the closed bar from 48px to
     ~81px. #vault-selection-count KEEPS its border-right deliberately: it
     still shares line 1 with Actions, so the divider is still dividing. If
     a three-digit count ever pushes Actions onto line 2 the border would
     then point at empty space, and dropping it here is the one-line fix. */
}

.vault-selection::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
  background: var(--rainbow);
}
@keyframes vault-sel-in {
  from { opacity: 0; transform: translate(-50%, 10px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
/* v22.5a — the docked bar's entrance. Identical rise and fade; the X
   translation is gone because the docked bar is positioned by `left`, not
   by recentring. Used only inside the >=1101px block above. */
@keyframes vault-sel-in-docked {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .vault-selection { animation: none; } }

#vault-selection-count {
  font-family: var(--disp); font-weight: 800; font-size: 19px;
  letter-spacing: -0.01em; text-transform: uppercase;
  padding: 0 14px 0 4px; white-space: nowrap;
  border-right: 1px solid rgba(255, 255, 255, 0.18);
  margin-right: 6px;
}
.vault-selection select {
  background: rgba(255, 255, 255, 0.1); color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.22); border-radius: var(--r-xs);
  padding: 9px 11px; font-family: var(--ui); font-size: 13px;
}
/* v21.10b: this killed the outline and replaced it with a border tint only —
   on the ink bulk bar that is nearly invisible. The ring is white here because
   the red one does not read against ink. */
.vault-selection select:focus-visible {
  outline: none; border-color: #fff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.45);
}
.vault-selection select:focus { outline: none; border-color: #fff; }
.vault-selection select option { color: var(--ink); background: #fff; }
.vault-selection .secondary {
  background: transparent; color: #fff; border-color: rgba(255, 255, 255, 0.3);
}
.vault-selection .secondary:hover { border-color: #fff; color: #fff; background: rgba(255, 255, 255, 0.08); }
.vault-selection .delete-button {
  border: 1px solid rgba(255, 122, 118, 0.5); border-radius: var(--r-xs);
  color: #ff8b86; background: transparent;
  font-family: var(--mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.06em; text-transform: uppercase; padding: 11px 14px;
}
.vault-selection .delete-button:hover { background: rgba(224, 50, 47, 0.22); color: #fff; border-color: #ff8b86; }
.vault-selection[hidden] { display: none; }
/* v21.36: the staged-changes summary. No new color — it inherits
   .vault-selection's own `color: #fff`, the same white every other label in
   this bar already reads against ink. Empty (nothing staged) renders as an
   empty string, so it takes no visible space either way. */
#vault-pending-summary { font-size: 12px; opacity: 0.85; }
#vault-pending-summary:empty { display: none; }
/* v21.18: the floating bar covers the bottom of the gallery, and it cannot be
   moved or dismissed — so a selected piece under it was unreachable. The bar
   keeps floating; the gallery reserves room instead. The value is measured in
   js/vault.js (syncSelectionReserve) because the bar's height changes with
   wrapping and its offset changes at the mobile breakpoint; it is 0px whenever
   nothing is selected, so nothing moves in the normal case.
   NOTE: no `display` rule here. .vault-selection is rendered with the `hidden`
   attribute and its partner rule is directly above — adding one is the trap
   that has bitten this project three times. */
.vault-gallery { padding-bottom: var(--vault-sel-reserve, 0px); }

/* ----- Unified explorer: folder sidebar + main grid ----- */
/* v22.0a — 256px → 284px. The rail carries emoji + name + a count pill, and
   at 256 the longest real labels ("Patches / Appliqués", a dated Moodboard
   collection) had nothing left to give. The 28px buys the ellipsis somewhere
   to land rather than hiding the problem. */
.vault-layout { display: grid; grid-template-columns: 284px 1fr; gap: 26px; align-items: start; }

/* v21.7: the rail loses its card. It is navigation, not content — a bordered,
   shadowed panel made it compete with the images. Now a plain column with a
   hairline on the right, matching the app sidebar's language. */
.vault-sidebar {
  background: transparent; border: none; border-right: 1px solid var(--line);
  border-radius: 0; padding: 0 16px 0 0; position: sticky; top: 18px; box-shadow: none;
}
.vault-folders-head {
  font-family: var(--mono); font-size: 9px; font-weight: 400;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--mut); padding: 14px 2px 8px;
}
.vault-folder {
  position: relative; display: flex; align-items: center; gap: 9px;
  padding: 9px 10px; border-radius: var(--r-xs); cursor: pointer; color: var(--ink);
  font-weight: 500; font-size: 13.5px;
}
.vault-folder:hover { background: #f4f0f0; }
.vault-folder.active { background: #f4f0f0; color: var(--accent-ink); font-weight: 600; }
/* v21.7.2: disclosure twisty for collections that have sub-collections. */
/* ---------- Folder disclosure (v21.8.0) ----------
   Two earlier attempts made the ARROW bigger and it still read as obscure.
   The research explains why: a disclosure icon is a weak affordance no matter
   its size, and users are slower when they believe they must hit the icon
   itself.

   So this stops asking a 12px glyph to carry the job:

     1. The TARGET is a full-height 28px column at the left of the row, with
        its own hover fill — you aim at a band, not a glyph.
     2. The STATE is structural, not iconographic. An open folder gets a
        rainbow rail that runs down the left edge of its children, visually
        BRACKETING the group. You see the open folder because you can see what
        belongs to it — the arrow is confirmation, not the message.

   That also survives touch, where there is no hover to reveal anything. */
.vault-twisty {
  flex: none; align-self: stretch;
  width: 28px; min-height: 34px; padding: 0; margin: -9px 2px -9px -10px;
  display: flex; align-items: center; justify-content: center;
  border: none; background: none; cursor: pointer;
  color: var(--mut); font-size: 13px; line-height: 1;
  border-radius: var(--r-xs);
  transition: background 0.15s ease, color 0.15s ease, transform 0.18s ease;
}
.vault-twisty:hover { background: rgba(20, 19, 26, 0.09); color: var(--ink); }
.vault-twisty.open { transform: rotate(90deg); color: var(--ink); }
.vault-twisty:focus-visible { outline: none; box-shadow: var(--ring); }
.vault-twisty-spacer { flex: none; width: 20px; }

/* A parent whose children are showing reads as a heading for the group. */
.vault-folder.has-kids.is-open .vault-folder-label { font-weight: 700; }

/* THE BRACKET. A rainbow rail down the children of an open folder — this is
   what actually communicates "expanded", at any glance distance. */
.vault-subtree {
  margin-left: 18px;
  padding-left: 8px;
  border-left: 3px solid transparent;
  border-image: var(--rainbow-v) 1;
}
/* The bracket IS the indent — so cancel the per-depth padding js/vault.js
   writes inline, or nested rows get indented twice and their names clip in a
   232px rail. */
.vault-subtree .vault-folder { padding-left: 8px !important; }
/* v21.10c: this is a real <button> now. Strip the UA chrome, keep it looking
   exactly as it did, and give it the row's whole width so the click target is
   no smaller than before. */
.vault-folder-label {
  flex: 1; min-width: 0;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  border-radius: var(--r-xs);
}
/* ★ v22.0a — A BLOCK CONTAINER, NOT A FLEX ONE, and that is the whole
   ellipsis fix. `text-overflow` only applies to block containers; a flex
   container ignores it, and it is not inherited, so it never reached the
   text inside either. The declaration was here all along and had simply
   never done anything — long names were being CHOPPED, not ellipsised.
   There is nothing to put `flex: none` on: js/vault.js:480 builds this as
   `iconHtml + " " + label`, where the icon is a bare emoji (no span) for
   every row except a collection with a thumbnail. As inline content that
   emoji now just flows with the text, which is what makes one ellipsis at
   the end possible. */
.vault-folder-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  display: block;
}
.vault-folder-label:focus-visible { outline: none; box-shadow: var(--ring); }
/* v22.0a — `vertical-align` and `margin-right` replace the alignment and the
   8px gap that .vault-folder-name's flex layout used to provide, now that it
   is a block container. This element is only ever rendered inside that name
   (js/vault.js:286, a collection with a cover), so `flex: none` is inert
   today — kept because it costs nothing and is correct the moment this
   thumbnail is reused anywhere that IS a flex row. */
.vault-folder-thumb { width: 22px; height: 22px; border-radius: 6px; object-fit: cover; flex: none; background: var(--thumb-bg); vertical-align: middle; margin-right: 4px; }
.vault-folder-count {
  font-size: 11.5px; font-weight: 600; color: var(--mut);
  background: var(--panel-2); border-radius: var(--r-pill); padding: 1px 8px; min-width: 20px; text-align: center;
  /* v22.0a — a flex child shrinks by default, and a shrunken pill breaks its
     digits across two lines: "62" read as 6 over 2. `flex: none` stops it
     giving up width and `nowrap` stops the digits ever breaking. */
  flex: none; white-space: nowrap;
}
.vault-folder.active .vault-folder-count { background: var(--panel); color: var(--accent-ink); }
.vault-folder-menu-btn {
  border: none; background: transparent; color: var(--mut); cursor: pointer;
  font-size: 18px; line-height: 1; padding: 0 2px; border-radius: 6px; opacity: 0;
}
/* v21.10c: :focus-visible added. The ⋯ is a real button, so Tab has always
   landed on it — but it was opacity:0 unless hovered or active, so keyboard
   focus disappeared into an invisible control. That was the "highlight
   vanishes" Josh saw. */
.vault-folder:hover .vault-folder-menu-btn,
.vault-folder.active .vault-folder-menu-btn,
.vault-folder-menu-btn:focus-visible { opacity: 1; }
.vault-folder-menu-btn:focus-visible { outline: none; box-shadow: var(--ring); }
.vault-folder-action:focus-visible { outline: none; box-shadow: var(--ring); }
.vault-folder-menu-btn:hover { background: rgba(20, 19, 26, 0.08); color: var(--ink); }
.vault-folder-menu {
  display: none; position: absolute; right: 8px; top: 34px; z-index: 20;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow-2); padding: 6px; min-width: 150px;
}
.vault-folder-menu.open { display: block; }
.vault-folder-action {
  display: block; width: 100%; text-align: left; border: none; background: transparent;
  padding: 8px 10px; border-radius: 8px; cursor: pointer; font: inherit; color: var(--ink);
}
.vault-folder-action:hover { background: var(--accent-soft); color: var(--accent-ink); }
.vault-folder-action.danger { color: #d6336c; }
.vault-folder-action.danger:hover { background: rgba(214, 51, 108, 0.1); }
.vault-folder-empty { color: var(--mut); font-size: 13px; padding: 4px 12px 10px; }
.vault-folder-new {
  display: block; width: calc(100% - 8px); margin: 8px 4px 4px; text-align: left;
  border: 1px dashed var(--line); background: transparent; color: var(--accent-ink);
  font-weight: 600; padding: 9px 12px; border-radius: var(--r-sm); cursor: pointer;
}
.vault-folder-new:hover { border-color: var(--accent); background: var(--accent-soft); }
/* The scope title is the page's real H1 in the new design — set it like one. */
.vault-scope-title {
  margin: 4px 0 0;
  font-family: var(--disp); font-weight: 900;
  font-size: clamp(30px, 3.4vw, 42px); line-height: 0.9;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
}
.vault-breadcrumb {
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--red); font-weight: 700;
}
/* The screen H1 + lead are redundant once the scope title is doing the work,
   and they were pushing the grid a third of a screen down. */
#screen-vault > h1, #screen-vault > .lead { display: none; }
/* v21.7: one slim line — drop target stretches, import sits beside it. */
.vault-utility-row { display: flex; align-items: stretch; gap: 10px; margin-bottom: 16px; }
.vault-utility-row .vault-dropzone { flex: 1; margin-bottom: 0; display: flex; align-items: center; justify-content: center; }
.vault-import-row { display: flex; align-items: center; flex: none; }
.vault-import-row .secondary { white-space: nowrap; height: 100%; }
.vault-import-row .hint { display: none; }
/* Quieter "new collection" — it is a minor action in a navigation rail. */
.vault-folder-new {
  border: 1px dashed var(--line); background: transparent; color: var(--mut);
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase;
  padding: 10px; border-radius: var(--r-xs); margin: 12px 0 4px; width: 100%;
}
.vault-folder-new:hover { border-color: var(--ink); color: var(--ink); background: transparent; }

/* Folder highlights as a drop target while dragging images onto it. */
.vault-folder.drag-over { background: var(--accent-soft); outline: 2px dashed var(--accent-2); outline-offset: -2px; }

/* Scope header: breadcrumb + title on the left, "Add images" on the right. */
.vault-scope-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; flex-wrap: wrap; margin: 4px 0 18px; }
#vault-add-images { white-space: nowrap; padding-left: 20px; padding-right: 20px; }
.vault-scope-heading { min-width: 0; }
.vault-breadcrumb { font-size: 13px; color: var(--soft); font-weight: 500; }
.vault-breadcrumb .crumb { cursor: pointer; }
.vault-breadcrumb .crumb:hover { color: var(--accent); text-decoration: underline; }
.vault-breadcrumb .sep { margin: 0 6px; color: var(--line); }

/* Draggable thumbnails */
.vault-card { user-select: none; }
.vault-card.dragging { opacity: 0.45; }
.vault-card.drop-target { outline: 2.5px dashed var(--accent); outline-offset: -2.5px; }

.vault-drag-ghost {
  position: absolute; top: -9999px; left: -9999px;
  background: var(--accent-2); color: #fff; font-weight: 600; font-size: 13px;
  padding: 9px 16px; border-radius: var(--r-xs); box-shadow: var(--sh-2);
}

/* Download button on each thumbnail (bottom-left, appears on hover). */
.vault-dl {
  position: absolute; bottom: 9px; left: 9px; z-index: 2;
  width: 34px; height: 34px; border-radius: 50%; border: none; cursor: pointer;
  background: var(--heart-bg); color: var(--accent); font-size: 16px; line-height: 1;
  box-shadow: var(--shadow-1);
  opacity: 0; transition: opacity 0.12s ease;
}
.vault-card:hover .vault-dl { opacity: 1; }
.vault-dl:hover { background: var(--accent); color: #fff; }

/* "Add images from the Vault" picker modal */
.vault-picker-overlay {
  position: fixed; inset: 0; z-index: 10050; display: none;
  align-items: center; justify-content: center; padding: 24px;
  background: rgba(20, 12, 32, 0.55);
}
.vault-picker-overlay.open { display: flex; }
.vault-picker {
  background: var(--panel); border-radius: var(--r-xl); width: min(900px, 100%);
  max-height: 86vh; display: flex; flex-direction: column; overflow: hidden;
  box-shadow: var(--shadow-2);
}
.vault-picker-head { padding: 20px 24px 12px; border-bottom: 1px solid var(--line); }
.vault-picker-head h2 { margin: 0 0 12px; }
.vault-picker-head .search-box { margin-bottom: 0; }
.vault-picker-body { padding: 18px 24px; overflow-y: auto; }
.vault-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
.vault-pick {
  position: relative; border-radius: var(--r-md); overflow: hidden; cursor: pointer;
  border: 3px solid transparent; background: var(--thumb-bg);
}
.vault-pick img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; }
/* v21.14a: was an inline style on a <div> inside the <button>. Now a <span>
   with a real class; `display: block` restores the box it used to be. Never
   rendered with `hidden`, so no partner rule. */
/* `height: 100%` is load-bearing, not tidiness. A <button> vertically CENTRES
   its content, and a <div> does not — so when .vault-pick became a button in
   v21.14 this text silently dropped from 3px below the tile's top edge to
   35.8px, in Chrome and Safari alike. Filling the height puts it back exactly.
   `align-items` does not reach a button's anonymous content box, and neither
   does `display: block` on the button — both were measured and both failed. */
.pick-noimg { display: block; height: 100%; padding: 20px; color: var(--soft); font-size: 12px; }
.vault-pick.picked { border-color: var(--accent-2); }
.vault-pick.in-folder { opacity: 0.5; cursor: default; }
.vault-pick .pick-tick {
  position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; border-radius: 50%;
  background: var(--accent-2); color: #fff; display: none; align-items: center; justify-content: center;
  font-size: 15px; font-weight: 700;
}
.vault-pick.picked .pick-tick, .vault-pick.in-folder .pick-tick { display: flex; }
.vault-pick.in-folder .pick-tick { background: var(--faint); }
.vault-picker-foot {
  padding: 16px 24px; border-top: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.vault-picker-foot .count { color: var(--soft); font-weight: 600; }

/* ----- Vault cards (v21.7) -----
   Plates, not boxes: the image sits on the canvas with a category tab, and the
   caption reads beneath it. Depth is a neutral shadow on the image only — the
   old camp-gradient hover halo and pink glow are gone (handover §7: colour
   lives in the frame, never as a coloured drop-shadow). */
.vault-card {
  position: relative;
  background: transparent;
  border: none;
  cursor: pointer;
}
/* v21.7.1: the CATEGORY COLOUR IS THE FRAME.
   The solid tab was covering a corner of every thumbnail, which on a screen
   whose whole job is looking at pictures is the wrong trade. The colour now
   arrives as a border around the image — legible at a glance, covering none of
   the artwork — and the word only appears on hover.
   (--cat comes from the cat-* class, which v21.7.1 moved onto .vault-card.) */
/* v21.7.5: THE FRAME INVERTS.
   At rest every piece is framed in the full spectrum — the wall reads as one
   gallery, and the brand is present on every screen without a single tag
   covering artwork. The CATEGORY colour is the *engaged* state: hover or
   select a piece and its frame resolves to that piece's colour. Information
   on demand, brand at rest.
   border-image is used rather than outline because outline cannot take a
   gradient; the frames are square, so border-image ignoring border-radius
   costs nothing. */
.vault-photo-wrap {
  position: relative; overflow: hidden;
  border: 3px solid transparent;
  border-image: var(--rainbow) 1;
  box-shadow: var(--sh-1);
  transition: box-shadow 0.15s ease, transform 0.12s ease;
}
.vault-card:hover .vault-photo-wrap,
.vault-card:focus-within .vault-photo-wrap {
  border-image: none;
  border-color: var(--cat, var(--ink));
  box-shadow: var(--sh-2);
  transform: translateY(-3px);
}
/* Selected = the category colour, doubled. Distinct from both the rainbow
   (unselected) and the plain single border (hover), and it survives being
   the same hue as its neighbours. */
.vault-card.selected .vault-photo-wrap {
  border-image: none;
  border-color: var(--cat, var(--ink));
  box-shadow: 0 0 0 2px var(--cat, var(--ink)), var(--sh-2);
}

/* The tab is hover-only inside the Vault. On touch there is no hover, but the
   border carries the colour and the label still reads beneath the card, so
   nothing is lost.
   v21.7.5: back to the TOP-LEFT, but pushed down clear of the Pinterest
   extension's "Save" button, which it injects at the top-left of every image
   it finds (~28px tall from the top edge). --pin-clear is that reserved band,
   so if Pinterest ever changes size there is one number to adjust. */
.vault-card .cat-tab {
  top: var(--pin-clear);
  bottom: auto;
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}
.vault-card:hover .cat-tab,
.vault-card:focus-within .cat-tab { opacity: 1; }
.vault-gallery.sheet .vault-card .cat-tab { top: calc(var(--pin-clear) - 6px); }

.vault-photo { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; background: var(--thumb-bg); display: block; }
/* v21.10c: the button that wraps the picture. It has no look of its own — the
   card's existing frame is the visual — so it only has to be the right size
   and carry the focus ring. */
.vault-open {
  display: block; width: 100%; padding: 0; margin: 0;
  border: 0; background: none; cursor: pointer;
}
.vault-open:focus-visible { outline: none; box-shadow: var(--ring); }
/* Keyboard focus should light the card the same way hover does, or the ring
   sits on a picture with no other sign that it is the active one. */
.vault-card:focus-within .vault-photo-wrap {
  border-image: none;
  border-color: var(--cat, var(--ink));
  box-shadow: var(--sh-2);
}
.vault-card:focus-within .cat-tab { opacity: 1; }
.vault-photo.placeholder {
  display: flex; align-items: center; justify-content: center;
  color: var(--mut); font-size: 11px; aspect-ratio: 4 / 5; background: var(--thumb-bg);
}

.vault-card-body { padding: 10px 0 0; }
.vault-card-title {
  font-family: var(--disp); font-weight: 800; font-size: 14.5px; line-height: 1.05;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vault-card-meta {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin-top: 5px;
}
/* The category label, in the category colour, under a short rule. */
.vault-cat {
  font-family: var(--mono); font-weight: 700; font-size: 9px;
  letter-spacing: 0.1em; text-transform: uppercase;
  display: inline-flex; align-items: center; gap: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vault-cat::before { content: ""; width: 14px; height: 2px; background: currentColor; flex: none; }

/* Status stays available but recedes to a dot — it is operational, not
   editorial, and must not compete with the category colour. */
.vault-status.badge, .vault-status {
  margin: 0; background: transparent; color: transparent; padding: 0;
  font-size: 0; width: 8px; height: 8px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
}
.vault-status::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--faint); display: inline-block;
}
.vault-gallery.compact .vault-card-title { font-size: 13px; }
.vault-gallery.compact .vault-card-body { padding-top: 8px; }

.vault-heart {
  position: absolute; bottom: 9px; right: 9px; z-index: 2;
  width: 34px; height: 34px; border-radius: 50%;
  border: none; cursor: pointer;
  background: var(--heart-bg);
  color: var(--accent-2); font-size: 18px; line-height: 1;
  box-shadow: var(--shadow-1);
  transition: transform 0.12s ease, background 0.12s ease;
}
.vault-heart:hover { transform: scale(1.08); }
.vault-heart.on { background: var(--accent-2); color: #fff; }
.vault-photo-wrap { position: relative; }

/* ===========================================================
   Design Studio (v21.8) — controls left, concepts right
   -----------------------------------------------------------
   Handover §5.3. The panel and the canvas swapped sides in v21.8: the
   generated concepts are the payoff of this screen and now get the wide
   pane, instead of appearing in a pop-up over an empty page.
   =========================================================== */
.studio-layout { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 26px; align-items: start; }

/* The controls panel. Sticky AND internally scrollable: sticky alone would
   pin its top and push its bottom (where Generate lives) off-screen on a
   short laptop viewport once the Body-Forward rows are showing.
   The rainbow rail is a BACKGROUND rather than a ::before, because an
   absolutely-positioned pseudo-element inside a scroll container scrolls with
   the content; a background with the default `scroll` attachment stays put. */
.studio-panel {
  position: sticky; top: calc(var(--rainbow-h) + 12px);
  /* NOT 100vh. Before you scroll, the panel starts ~145px down the page (under
     the h1 and the lead), so a full-viewport panel puts its own bottom — and
     therefore Generate — below the fold until you scroll. The allowance keeps
     the whole panel, sticky or not, inside the first screen. */
  max-height: calc(100vh - 190px); min-height: 320px;
  overflow: hidden;                /* the inner box does the scrolling */
  display: flex; flex-direction: column;
  border: 1px solid var(--line);
  padding: 18px 18px 0 21px;
  box-shadow: var(--sh-1);
  background-color: var(--panel);
  background-image: var(--rainbow-v);
  background-repeat: no-repeat;
  background-size: 3px 100%;
  background-position: left top;
}
/* Section heading inside the panel — the mockup's `.panel h3`. */
.studio-h {
  font-family: var(--mono); font-weight: 700; font-size: 10.5px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--mut);
  margin: 0 0 10px;
}
.studio-h + .studio-h, .studio-field + .studio-h, .studio-wide + .studio-h { margin-top: 18px; }

.studio-field { margin-bottom: 14px; }
.studio-flabel {
  display: block; margin-bottom: 6px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut);
}
.studio-selects { display: flex; gap: 8px; }
.studio-selects select, #studio-bf2-model { flex: 1; min-width: 0; width: 100%; font-size: 13px; }
.studio-check { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; font-size: 12.5px; color: var(--soft); }
.studio-check input { width: auto; margin: 2px 0 0; flex: none; }
.studio-check em { font-style: normal; opacity: 0.7; }
/* v22.11 — Qwen's careful-mode box sits under its note, in the same field. */
.studio-qwen-careful { margin-top: 10px; }
/* v22.13 — Qwen's Add-on picker, between the note and the careful box. */
.studio-qwen-addon { margin-top: 10px; }
.studio-wide { width: 100%; }

/* Engine — the seg picks the lane, the chips pick the engine within it. */
.studio-lanes { display: flex; width: 100%; margin-bottom: 10px; }
.studio-lanes .seg-btn { flex: 1; }
.studio-engines { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.studio-engines[hidden] { display: none; }
.provider-opt {
  background: var(--panel); border: 1px solid var(--line); cursor: pointer;
  padding: 7px 12px; border-radius: var(--r-xs);
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--mut);
}
.provider-opt:hover { border-color: var(--ink); color: var(--ink); }
.provider-opt.active { background: var(--ink); border-color: var(--ink); color: #fff; }
/* v21.8.2: `.provider-note` (the Body-Forward disclaimer panel) is retired —
   see the comment in index.html. What is left is this: a one-line operational
   note under the control it describes. */
.studio-hint { margin: 6px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--mut); }

/* Inside the panel the saved-prompt list expands INLINE. Absolutely positioned
   it would be clipped by the panel's own scrollbox — and the panel scrolls, so
   pushing the textarea down costs nothing. */
.studio-panel .prompt-picker-panel {
  position: static; margin-top: 6px;
  box-shadow: none; border-radius: var(--r-sm);
}

#studio-prompt {
  width: 100%; min-height: 118px; box-sizing: border-box;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 12px; font: inherit; font-size: 13px;
  background: var(--panel-2); color: var(--ink);
  margin-bottom: 8px;
}
#studio-prompt:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
#studio-save-prompt { margin-bottom: 18px; }

/* Reference images — the handover's `.refs` strip: picked thumbnails then a
   "+" tile that walks you to the vault grid on the canvas. */
.studio-refs { display: flex; flex-wrap: wrap; gap: 8px; }
.studio-ref { position: relative; }
.studio-ref img, .studio-ref .placeholder {
  width: 52px; height: 52px; object-fit: cover; display: block;
  border: 1px solid var(--line); background: var(--thumb-bg);
}
.studio-ref .placeholder { display: flex; align-items: center; justify-content: center; color: var(--faint); }
/* v22.8 — INSIDE the thumbnail's top-right corner. It used to sit at
   -6px/-6px, hanging off the 52px thumb — the repo's no-overhanging-controls
   rule. Never top-LEFT (the standing Pinterest rule). .studio-ref is
   position: relative and does not clip, so this lands on the picture.
   ★ min-height: 0 exempts it from the <=500px 44px tap floor (v21.13,
   "Touch targets" below), which made it a 22x44 bar down most of a 52px
   thumbnail. 22x22 meets WCAG 2.2 AA 2.5.8 by its spacing exception: a
   24px circle on it stays inside its own thumbnail — a plain div, not a
   target — and touches no other control. */
.studio-ref-remove {
  position: absolute; top: 4px; right: 4px; width: 22px; height: 22px; min-height: 0;
  display: flex; align-items: center; justify-content: center;
  border: none; background: var(--ink); color: #fff; cursor: pointer;
  font-size: 13px; line-height: 1; padding: 0;
}
/* v22.18a — the same line as "Save this prompt" (.secondary: 1px solid
   --line, --r-xs corners), not the old 1.5px dashed tile. `appearance: none`
   hands the drawing to these rules in WebKit too: both engines computed
   `appearance: auto` for this button before. */
.studio-ref-add {
  -webkit-appearance: none; appearance: none;
  width: 52px; height: 52px; border: 1px solid var(--line); border-radius: var(--r-xs); background: none;
  color: var(--mut); font-size: 20px; line-height: 1; cursor: pointer; padding: 0;
}
.studio-ref-add:hover { border-color: var(--ink); color: var(--ink); }
/* Body-Forward engines are text-to-image; studioGenerate does not send
   references on them. Say so rather than dropping them silently. */
.studio-panel.refs-ignored .studio-refs { opacity: 0.45; }
.studio-panel.refs-ignored .studio-field-refs .studio-flabel::after {
  content: " — not used by this engine"; color: var(--mut);
}
/* v22.9 — FLUX EDITS the first reference (api/generate-image.js
   runFluxEdit), so it gets its own two labels, set by js/studio.js
   syncRefsHint(): one with a full tray, one inviting a reference when it
   is empty. With a full tray, every reference after the first is dimmed
   the same way an ignored tray is — those are not sent. */
.studio-panel.refs-edit .studio-field-refs .studio-flabel::after {
  content: " — the first one is the picture being edited; the rest are ignored"; color: var(--mut);
}
.studio-panel.refs-edit .studio-ref ~ .studio-ref { opacity: 0.45; }
.studio-panel.refs-edit-empty .studio-field-refs .studio-flabel::after {
  content: " — add one to edit it with FLUX"; color: var(--mut);
}
/* v22.9c — the aspect control while a FLUX edit keeps the reference's shape
   (js/studio.js syncRefsHint). The base `select` rule sets its own colour,
   which overrides the browser's greyed-out look, and this sheet has no
   global :disabled rule — so this line is what makes "disabled" visible.
   Same dimming as an ignored reference tray. */
.studio-selects select:disabled { opacity: 0.45; cursor: not-allowed; }

/* The fields scroll; Generate does not. A separate box rather than a sticky
   child, so no field can ever peek out from under the button. */
.studio-panel-scroll { flex: 1; min-height: 0; overflow-y: auto; }
.studio-generate-bar {
  flex: none;
  padding: 12px 0 18px; background: var(--panel);
  border-top: 1px solid var(--line);
}
.studio-status {
  margin: 0 0 10px; min-height: 0;
  font-family: var(--mono); font-size: 11px; line-height: 1.5;
  letter-spacing: 0.02em; color: var(--soft);
}
.studio-status:empty { display: none; }

/* ===========================================================
   v22.18 — Design Studio: outside pictures, the ⓘ, the phone order
   =========================================================== */

/* The tray's two ways in, where the one "+" tile was: "From Vault" (the old
   tile's job) and "Upload" (a picture that never joins the Vault). Same
   tile, a little wider for its two-line label.
   ★ v22.18a: 62 → 66px. "FROM VAULT" measured 57.1px inside 59px — under
   1px from each side line, so any wider rendering of it (a fallback mono
   face, iPhone text inflation) sat ON the left and right borders. */
.studio-ref-add {
  width: 66px; display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  font-family: var(--mono); font-size: 8.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; line-height: 1.15;
}
.studio-ref-add-ic { font-size: 16px; line-height: 1; font-family: var(--ui); font-weight: 400; }
.studio-ref-add:disabled { opacity: 0.6; cursor: default; }
/* An outside picture says so ON the picture: a strip along its foot (never
   the top-left — the standing Pinterest rule; the × keeps the top-right). */
.studio-ref-out {
  position: absolute; left: 1px; right: 1px; bottom: 1px; padding: 2px 1px;
  background: rgba(20, 19, 26, 0.82); color: #fff; text-align: center;
  font-family: var(--mono); font-size: 7px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; line-height: 1.15;
  pointer-events: none;
}

/* The engine's description behind an ⓘ (js/studio.js syncEngineInfo). The
   chip row is the tip's positioning box. No `display` on either, so the
   `hidden` attribute hides both with no partner rule. */
.studio-engines { position: relative; }
.studio-engine-info {
  width: 28px; height: 28px; padding: 0; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--line); background: var(--panel); color: var(--mut);
  font-size: 15px; line-height: 1;
}
.studio-engine-info:hover, .studio-engine-info[aria-expanded="true"] { border-color: var(--ink); color: var(--ink); }
.studio-engine-tip {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 6;
  padding: 9px 11px; border-radius: var(--r-sm);
  background: var(--ink); color: #fff; box-shadow: var(--sh-1);
  font-size: 12px; line-height: 1.45;
}

/* ★ THE PHONE / iPad ORDER (the stacked layout, ≤1100px). Desktop keeps its
   order and its pinned Generate bar; here Generate follows the prompt:
     Engine → Add-on + Careful → Prompt → ✦ Generate (+ status) →
     Save this prompt → Reference images → Count & aspect.
   HOW: js/studio.js placeGenerateBar() MOVES the bar to just after the
   prompt at this width, and back to the foot of the panel above it. A real
   move, not CSS `order`, so what is read out and tabbed through is the
   order that is seen. Here the bar is one more row, not a pinned footer. */
@media (max-width: 1100px) {
  .studio-generate-bar { border-top: 0; padding: 2px 0 16px; }
  /* Add-on and Careful mode on ONE row: the picker on the left, the box on
     the right; they wrap only when the row truly cannot hold both. */
  /* Measured at 390px: the row is 321px and "Careful mode (slower)" 164px,
     so the picker's starting width (120px) + the gap (12px) + 164px = 296px
     fits, and the picker grows into the rest. At 150px + 16px it wrapped. */
  .studio-qwen-row { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 8px 12px; }
  .studio-qwen-row .studio-qwen-addon { flex: 1 1 120px; min-width: 0; margin-top: 0; }
  .studio-qwen-row .studio-qwen-careful { flex: 0 0 auto; margin: 0 0 10px; }
}
/* On a phone every button is held to 44px tall (the ≤500px tap-target block
   below), which made the ⓘ a 44×44 tile that no longer fit beside the
   engine buttons — measured at 390px: the row is 320.5px, the three
   Body-Forward chips and their gaps 285px, leaving ~29px. So it is DRAWN at
   28px, centred on the 44px chips, and its TAP AREA is widened by an
   invisible ::before to 34×44 — kept inside the 6px gaps either side so it
   never covers a chip (WCAG 2.2 AA 2.5.8 asks for 24px). The same idea as
   .studio-ref-remove's 22px ×. */
@media (max-width: 500px) {
  .studio-engine-info { min-height: 0; align-self: center; position: relative; }
  .studio-engine-info::before { content: ""; position: absolute; inset: -8px -3px; }
}

/* ----- The canvas: latest concepts, then the vault picker ----- */
.studio-canvas { min-width: 0; }
.studio-canvas-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding-bottom: 8px; margin-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.studio-vault-head { margin-top: 34px; }
.studio-canvas-title {
  margin: 0; font-family: var(--disp); font-weight: 800;
  font-size: clamp(20px, 2vw, 26px); line-height: 1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
}
.studio-canvas-note, .studio-saveall {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--mut); flex: none;
}
.studio-saveall {
  background: none; border: none; padding: 0; cursor: pointer;
  font-weight: 700; color: var(--red);
}
.studio-saveall:hover { text-decoration: underline; }
.studio-saveall[hidden] { display: none; }
.studio-saveall:disabled { color: var(--faint); cursor: default; text-decoration: none; }
.studio-canvas-empty { color: var(--mut); font-size: 13.5px; margin: 0; }
.studio-canvas-empty[hidden] { display: none; }

.studio-controls { display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.studio-controls .search-box { flex: 1; min-width: 160px; margin-bottom: 0; }

.studio-picker {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 14px;
  max-height: 74vh; overflow-y: auto; padding: 3px;
}
/* Same frame language as the Vault (locked decision 10): the full spectrum at
   rest, this piece's category colour on hover and when it is picked. Picked =
   the category colour doubled, plus a squared check. */
.studio-pick { position: relative; cursor: pointer; background: none; border: none; }
.studio-pick-frame {
  /* v21.14a: these are <span>s now — a <button> may only contain phrasing
     content — so they need display: block to lay out as they did as <div>s.
     No [hidden] partner needed: neither class is ever rendered with the
     `hidden` attribute nor toggled via .hidden. Verified, not assumed. */
  display: block;
  position: relative; overflow: hidden;
  border: 3px solid transparent; border-image: var(--rainbow) 1;
  box-shadow: var(--sh-1);
  transition: box-shadow 0.15s ease, transform 0.12s ease;
}
.studio-pick:hover .studio-pick-frame {
  border-image: none; border-color: var(--cat, var(--ink));
  box-shadow: var(--sh-2); transform: translateY(-3px);
}
.studio-pick.selected .studio-pick-frame {
  border-image: none; border-color: var(--cat, var(--ink));
  box-shadow: 0 0 0 2px var(--cat, var(--ink)), var(--sh-2);
}
.studio-pick img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; background: var(--thumb-bg); }
.studio-pick .placeholder {
  aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center;
  background: var(--thumb-bg); color: var(--faint); font-size: 10.5px;
}
/* Top-RIGHT: the top-left of any thumbnail belongs to the Pinterest extension
   (locked decision 11). Squared, not a circle — this look is not rounded. */
.studio-pick.selected .studio-pick-frame::after {
  content: "\2713"; position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 20px; height: 20px;
  background: var(--cat, var(--ink)); color: #fff;
  font-size: 12px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.studio-pick-label {
  display: block;              /* see .studio-pick-frame above */
  font-family: var(--disp); font-weight: 800; font-size: 12.5px; line-height: 1.1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  padding: 7px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Saved-prompts dropdown picker */
.prompt-picker { position: relative; margin-bottom: 10px; }
.prompt-picker-toggle { width: 100%; text-align: left; }
.prompt-picker-panel {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 20;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow-2); padding: 10px;
}
.prompt-picker-panel[hidden] { display: none; }
.prompt-picker-panel .search-box { margin-bottom: 8px; }
/* v22.18a — one column that may NOT grow past the list. Without a column
   rule the grid's one auto track sized itself to the longest title (565px,
   nowrap), so every row was 607px wide inside a 299px list on a phone: the
   titles ran past the edge and the "…" was never reached. The explicit
   column also puts this grid in the audit's shrink check ("Shrink safety"
   lists it). The title is the flex item that gives way. */
.prompt-picker-list { max-height: 220px; overflow-y: auto; display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; }
.prompt-pick-item {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  border: none; background: none; padding: 8px 10px; border-radius: 8px; cursor: pointer; font: inherit; color: var(--ink);
}
.prompt-pick-item:hover { background: var(--accent-soft); }
.prompt-pick-star { width: 14px; flex-shrink: 0; color: var(--gold); }
.prompt-pick-title { flex: 1 1 auto; min-width: 0; font-weight: 500; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Favorite star button (shared: Prompt Library) */
.prompt-fav { border: none; background: none; color: var(--gold); font-size: 18px; line-height: 1; cursor: pointer; flex-shrink: 0; }

/* ===========================================================
   Prompt Library (v21.12) — card grid over the v21.11 taxonomy
   =========================================================== */
.prompts-toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 14px; }
/* ★ v21.88 — the search field IS the way into a long library, so it wears the
   house rainbow. Same technique .tryon-img uses: a transparent border filled
   by the --rainbow token, never a retyped set of stops. */
.prompts-toolbar .search-box {
  flex: 1; min-width: 200px; margin-bottom: 0;
  border: 2px solid transparent; border-image: var(--rainbow) 1;
}
.prompts-count {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--mut);
}

/* ----- Filter chip rows ----- */
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.chip-row-tags { margin-bottom: 20px; }
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line); background: var(--panel); color: var(--ink);
  border-radius: var(--r-xs); padding: 8px 12px; cursor: pointer;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.chip:hover { border-color: var(--ink); }
.chip.on { background: var(--ink); border-color: var(--ink); color: #fff; }
.chip-n { font-weight: 400; opacity: 0.6; }
.chip-sm { padding: 6px 10px; font-size: 9px; }
/* A style chip carries its colour as a leading rule, so the row reads as the
   palette rather than twelve identical grey pills. */
.chip-style::before { content: ""; width: 10px; height: 10px; background: var(--st, var(--mut)); flex: none; }
.chip-unstyled { border-style: dashed; color: var(--mut); }

/* ----- The 12 locked styles -----
   These MUST stay in step with PROMPT_STYLES in js/vault-data.js and the
   prompts_style_check constraint in supabase/prompts_taxonomy.sql. */
.st-club-nightlife   { --st: var(--pink); }
.st-street-everyday  { --st: var(--orange); }
.st-formal-tailored  { --st: var(--ink); }
.st-runway-couture   { --st: var(--red); }
.st-casual           { --st: var(--green); }
.st-sensual          { --st: var(--purple); }
.st-nude-figure      { --st: #12b5c8; }
.st-resort-swim      { --st: var(--yellow); }
.st-athleisure       { --st: #3fd1a5; }
.st-evening          { --st: #5b2fd6; }
.st-drag-performance { --st: #ff2ecb; }
.st-utility          { --st: #9a97a5; }
.st-none             { --st: var(--line); }

/* ----- Cards ----- */
/* ★ v21.88 — ONE COLUMN. Thin rows in a three-across grid read as a wall of
   fragments; a list of rows reads as a list. Both #prompts-favs and
   #prompts-list use this class, so both become lists together. */
.prompt-grid { display: grid; grid-template-columns: 1fr; gap: 6px; margin-bottom: 22px; }
/* The colour bar is the LEFT border now, carrying the same --st the chip's
   dot already uses — one colour source, not two. */
.pcard {
  position: relative;
  background: var(--panel); border: 1px solid var(--line);
  border-left: 4px solid var(--st, var(--line));
  padding: 0;
}
/* THE COLLAPSED ROW — the whole point of v21.88. ~44px, one line, no wrap. */
.pcard-row {
  display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 0 12px; cursor: pointer;
}
.pcard-row:hover { background: var(--panel-2); }
.pcard-row:focus-visible { outline: none; box-shadow: var(--ring); }
.pcard-chev {
  flex: none; width: 0; height: 0; margin-left: 2px;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 5px solid var(--mut); transition: transform .15s ease;
}
.pcard[data-open] .pcard-chev { transform: rotate(180deg); }
/* Folded, not removed: everything the card carried is in here. */
.pcard-body { display: none; padding: 2px 14px 12px; }
.pcard[data-open] .pcard-body { display: block; }
.pcard-top { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
/* The style chip IS the assign control — two clicks and no dialog. */
.pcard-style { flex: none; }
.pcard-style {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--line); background: var(--panel); cursor: pointer;
  border-radius: var(--r-xs); padding: 5px 10px;
  font-family: var(--mono); font-weight: 700; font-size: 9.5px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink);
}
.pcard-style::before { content: ""; width: 9px; height: 9px; background: var(--st, var(--line)); flex: none; }
.pcard-style:hover { border-color: var(--ink); }
.pcard.st-none .pcard-style { border-style: dashed; color: var(--mut); }
.pcard-json {
  font-family: var(--mono); font-weight: 700; font-size: 8.5px;
  letter-spacing: 0.1em; text-transform: uppercase;
  border: 1px solid var(--line); padding: 4px 7px; color: var(--mut);
}
.pcard-fav { flex: none; font-size: 17px; line-height: 1; background: none; border: 0; cursor: pointer; padding: 0 2px; }
.prompt-fav { border: none; background: none; color: var(--gold); font-size: 18px; line-height: 1; cursor: pointer; flex-shrink: 0; }
/* ONE line, ellipsised — a title that wrapped to three lines is exactly what
   made the old card tall. */
.pcard-title {
  flex: 1; min-width: 0; margin: 0;
  font-family: var(--disp); font-weight: 800; font-size: 14px; line-height: 1.2;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pcard-preview {
  margin: 0; font-size: 13px; line-height: 1.5; color: var(--mut);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Raw JSON reads as code, not prose. */
.pcard-code {
  margin: 0; padding: 9px 11px; max-height: 92px; overflow: hidden;
  background: var(--ink); color: #fff;
  font-family: var(--mono); font-size: 10.5px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
}
.pcard-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
.ptag {
  font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.08em; text-transform: uppercase;
  border: 1px solid var(--line); padding: 3px 7px; color: var(--mut);
}
/* The meta moved up into the row (one source, no duplicate), so the foot is
   the actions alone. */
.pcard-foot { display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding-top: 12px; }
.pcard-meta { flex: none; font-family: var(--mono); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut); white-space: nowrap; }
.pcard-acts { display: flex; align-items: center; gap: 6px; flex: none; }
.pcard-acts .secondary { padding: 8px 12px; font-size: 10px; }
/* ★ v21.89 — the red Edit sits on the same baseline as Use in Studio beside
   it. `.primary` is padded 13px/20px for a full-width form action, which
   would make this button visibly taller than its neighbour; the padding and
   size here are .prompt-use's own, so the pair reads as one control group.
   The RED comes from .primary.create — the app's existing red action — and is
   not redeclared here. */
.pcard-acts .primary { padding: 8px 12px; font-size: 10px; grid-column: auto; }
.pcard-more {
  border: 1px solid var(--line); background: var(--panel); cursor: pointer;
  width: 32px; height: 32px; padding: 0; line-height: 1;
  border-radius: var(--r-xs); color: var(--mut); font-size: 15px;
}
.pcard-more:hover { border-color: var(--ink); color: var(--ink); }

/* ----- Popovers ----- */
.pcard-menu, .pstyle-menu {
  position: absolute; z-index: 30;
  background: var(--panel); border: 1px solid var(--line);
  box-shadow: var(--sh-2); padding: 4px; min-width: 168px;
}
.pcard-menu { right: 14px; bottom: 50px; }
.pcard-menu[hidden] { display: none; }
.pstyle-menu { left: 0; top: calc(100% + 5px); max-height: 280px; overflow-y: auto; }
.pcard-act, .pstyle-opt {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  border: 0; background: none; cursor: pointer; padding: 9px 10px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink);
}
.pcard-act:hover, .pstyle-opt:hover { background: var(--panel-2); }
.pcard-act.danger { color: var(--red); }
.pstyle-opt::before { content: ""; width: 10px; height: 10px; background: var(--st, var(--line)); flex: none; }
.pstyle-opt.on { background: var(--panel-2); }
.pstyle-opt.on::after { content: "✓"; margin-left: auto; color: var(--red); }
.pstyle-clear::before { background: none; border: 1px dashed var(--line); }

/* ----- Inline editor ----- */
/* ★ v21.89a — the editor is a SIBLING of .pcard-body, not a child, so it never
   inherited the body's 14px sides: TITLE, PROMPT and Save sat flush against the
   card's coloured left bar while the preview text above them did not. The 14px
   here is .pcard-body's own, so the two line up exactly. border-top stays
   outside the padding and full-bleed across the card. */
.pcard-editor { display: grid; gap: 6px; margin-top: 12px; padding: 12px 14px 14px; border-top: 1px solid var(--line); }
.pcard-editor[hidden] { display: none; }
.prompt-edit-title, .prompt-edit-body {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 13px;
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px 11px;
  background: var(--panel-2); color: var(--ink);
}
.prompt-edit-body { min-height: 130px; resize: vertical; line-height: 1.5; }
/* ★ v21.89 — the Save/Cancel row sat hard against the textarea above and the
   next card below; 14px on top of the grid's own 6px gap gives it room.
   v21.89a dropped its bottom margin back to 0: the editor now carries 14px of
   bottom padding of its own, and two of them stacked read as a gap nobody
   asked for. */
.pcard-editor .button-row { margin-top: 14px; margin-bottom: 0; }
.pcard-editor .primary { grid-column: auto; }
/* ★ v21.89 — louder field labels, SCOPED TO THE EDITOR. .studio-flabel is
   shared with the Design Studio (three labels in index.html) and restyling it
   there would be an accident, so this never touches the base rule. */
.pcard-editor .studio-flabel { font-size: 11.5px; font-weight: 800; color: var(--ink); letter-spacing: 0.1em; }

/* v21.33 (Fix B) — a recipe's pending reference chips, inside the editor.
   .pcard-ref-chip borrows .wr-chip's look (border + panel fill); .pcard-ref-x
   borrows .ps-ref-x's small round remove button, inline instead of overlaid
   on an image since these are text chips, not thumbnails. */
.pcard-refs { display: flex; flex-wrap: wrap; gap: 6px; }
.pcard-refs-empty { font-size: 12.5px; color: var(--mut); margin: 0; }
.pcard-ref-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xs);
  padding: 5px 6px 5px 11px; font-size: 12.5px; color: var(--ink);
}
.pcard-ref-x {
  width: 16px; height: 16px; padding: 0; flex: none;
  border: none; border-radius: 50%; cursor: pointer; line-height: 1; font-size: 11px;
  background: var(--mut); color: #fff;
}
.pcard-ref-x:hover { background: var(--ink); }
.pcard-ref-x:focus-visible { outline: none; box-shadow: var(--ring); }
.pcard-refs-edit { justify-self: start; }

@media (max-width: 820px) {
  .prompt-grid { grid-template-columns: 1fr; }
  /* Chip rows scroll sideways rather than stacking into a wall. */
  .chip-row { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; }
  .chip { flex: none; }
  .pcard-menu { right: 16px; }
}

/* ===========================================================
   THE PROMPT STUDIO (v21.20) — stage 2 of PROMPT_STUDIO_SPEC.md

   A conversation panel at the top of the Prompt Library, with a thread
   rail on the left. The rail deliberately MIRRORS the Sparkle Vault's
   folder rail (spec §8) rather than inventing a second navigation
   language: the same 256px track, the same 26px gutter, the same
   hairline on the right, the same sticky column, and the same
   collapse-to-a-bottom-sheet on a phone. The numbers below are copied
   from .vault-layout / .vault-sidebar — if one moves, move both.

   NO `display` IS SET ON ANY CLASS THAT CARRIES THE `hidden` ATTRIBUTE
   in this block. The rail's ⋯ menu opens by CLASS (.open), like the
   Vault's, precisely so nothing here can become the fourth instance of
   the `[hidden]`-vs-`display` trap.
   =========================================================== */
.ps-panel {
  border: 1px solid var(--line); background: var(--panel);
  border-top: 4px solid transparent; border-image: var(--rainbow) 1;
  padding: 20px 22px 22px; margin-bottom: 30px;
}
.ps-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; }
.ps-kicker {
  margin: 0 0 4px; font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--red);
}
.ps-title {
  margin: 0; font-family: var(--disp); font-weight: 900;
  font-size: clamp(22px, 2.4vw, 30px); line-height: 0.95;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
}

/* ----- The two columns. Same GUTTER as .vault-layout; the track is
   deliberately narrower. v22.0a widened the Vault's rail to 284px because it
   carries emoji + name + a count pill and its labels run long; a thread name
   here is short and has no pill beside it, so 256px still fits and matching
   the Vault would only steal width from the conversation. ----- */
.ps-layout { display: grid; grid-template-columns: 256px 1fr; gap: 26px; align-items: start; }
.ps-rail {
  background: transparent; border: none; border-right: 1px solid var(--line);
  border-radius: 0; padding: 0 16px 0 0; position: sticky; top: 18px;
}
.ps-rail-head {
  margin: 0; font-family: var(--mono); font-size: 9px; font-weight: 400;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--mut);
  padding: 0 2px 8px;
}
.ps-rail .search-box { margin-bottom: 10px; }
.ps-thread-list { display: flex; flex-direction: column; gap: 2px; }
.ps-rail-empty { color: var(--mut); font-size: 13px; margin: 0; padding: 4px 2px; line-height: 1.5; }

/* A row is a container with its own buttons inside — the same shape as
   .vault-folder, and for the same reason: a button may not nest in a button,
   so the row is a <div> and the OPEN action is the button. */
.ps-thread {
  position: relative; display: flex; align-items: center; gap: 4px;
  padding: 7px 8px; border-radius: var(--r-xs); color: var(--ink);
}
.ps-thread:hover { background: #f4f0f0; }
.ps-thread.active { background: #f4f0f0; }
.ps-thread-open {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px;
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  border-radius: var(--r-xs);
}
.ps-thread-open:focus-visible { outline: none; box-shadow: var(--ring); }
.ps-thread-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13.5px; font-weight: 500;
}
.ps-thread.active .ps-thread-name { font-weight: 700; color: var(--accent-ink); }
.ps-thread-when { font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut); }
.ps-thread-star { font-size: 15px; }
.ps-thread-menu-btn {
  border: none; background: transparent; color: var(--mut); cursor: pointer;
  font-size: 18px; line-height: 1; padding: 0 2px; border-radius: 6px; opacity: 0;
}
/* Same :focus-visible reveal as the Vault's ⋯ (v21.10c): without it Tab lands
   on an invisible control and the highlight appears to vanish. */
.ps-thread:hover .ps-thread-menu-btn,
.ps-thread.active .ps-thread-menu-btn,
.ps-thread-menu-btn:focus-visible { opacity: 1; }
.ps-thread-menu-btn:focus-visible { outline: none; box-shadow: var(--ring); }
.ps-thread-menu {
  display: none; position: absolute; right: 6px; top: 34px; z-index: 20;
  background: var(--panel); border: 1px solid var(--line);
  box-shadow: var(--sh-2); padding: 4px; min-width: 150px;
}
.ps-thread-menu.open { display: block; }
.ps-thread-act {
  display: block; width: 100%; text-align: left; border: none; background: transparent;
  padding: 8px 10px; cursor: pointer; color: var(--ink);
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.06em; text-transform: uppercase;
}
.ps-thread-act:hover { background: var(--panel-2); }
.ps-thread-act.danger { color: var(--red); }
.ps-thread-act:focus-visible { outline: none; box-shadow: var(--ring); }

/* ----- The conversation ----- */
.ps-main { min-width: 0; }

/* ===========================================================
   v21.25 — THE BENCH
   -----------------------------------------------------------
   Conversation + composer on the left, the prompt as a permanent plate on
   the right. The same shape as the Design Studio (locked decision 15: the
   concepts are the payoff of that screen and belong on a canvas, not in a
   pop-up) — the prompt is the payoff of this one.

   THE SPLIT, and why it is not 50/50. Inside .ps-layout the rail already
   takes a fixed 256px plus a 26px gutter, so the bench gets whatever is
   left. The plate holds a paragraph of prompt that people READ and edit;
   the conversation holds one- or two-line replies now that the prompt has
   moved out of it. So the plate takes the larger share — 1fr / 1.15fr —
   which at 1440 measures out to roughly 380 / 437 (see the v21.25 entry in
   REDESIGN_LOG.md for the numbers).

   Both children carry min-width: 0 via the "Shrink safety" block, and the
   talk column carries min-height: 0 here so its message list can actually
   scroll instead of pushing the composer off the bottom — the block-axis
   version of the same trap, which has no visible symptom (check 7).
   =========================================================== */
.ps-bench { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 22px; align-items: start; }
.ps-bench-talk { display: flex; flex-direction: column; min-width: 0; min-height: 0; }

/* v21.25d — THE INNER SCROLLBOX IS GONE. `.ps-messages` was
   `max-height: 60vh; overflow-y: auto` from v21.20, which put a second
   scrollbar inside a page that already scrolls — measured at 1440 as 859px of
   content in a 540px box. Two nested scroll contexts is the "scrolls weird"
   complaint at the root, and REDESIGN_LOG already carries the ruling from
   v21.16: "Two stacked scrollers inside one clipping box is the bug, not the
   fix." Collapsing the history is what makes removing it safe — a long thread
   is now a short list of one-line summaries. THE PAGE IS THE ONLY SCROLLER.
   Do not put a height cap back on this element. */
.ps-messages { display: flex; flex-direction: column; gap: 16px; min-height: 0; }
.ps-messages:empty { display: none; }

/* ----- v21.25d: exchanges, and the collapsed summary row -----
   LOCKED DECISION 13 shaped this. The Vault's folder rail proved twice that a
   disclosure ICON is a weak affordance at any size, so the arrow here is
   decoration on a big target and never the target: the toggle is a real
   <button> filling the row, 44px tall, with a hover fill. The version pill is
   its SIBLING — a button cannot nest inside a button, the same constraint that
   shaped .vault-folder / .vault-folder-label. */
.ps-exchange { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.ps-ex-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ps-ex-toggle {
  flex: 1; min-width: 0; min-height: 44px;
  display: flex; align-items: center; gap: 10px; text-align: left;
  border: none; background: none; cursor: pointer; padding: 8px 10px;
  border-radius: var(--r-xs); font: inherit; color: var(--ink);
}
.ps-ex-toggle:hover { background: var(--panel-2); }
.ps-ex-toggle:focus-visible { outline: none; box-shadow: var(--ring); }
/* v21.25e — Josh: "the little arrow that toggles the convos open needs to be at
   least twice as big and a color." 11px -> 24px (the 2x floor is 22; 24 sits
   better against a 44px row) and off --mut onto the accent, so it speaks the
   app's touchable language instead of reading as chrome.

   It carries OPEN/CLOSED state, so it is measured as non-text UI against BOTH
   surfaces it can sit on: --accent #e0322f is 4.4999:1 on the resting row
   (--panel #ffffff) and 4.2740:1 on the hover fill (--panel-2 #faf9f6), against
   the 3:1 WCAG 1.4.11 asks. LOCKED DECISION 13 is untouched — this stays an
   aria-hidden decoration inside the real 44px <button>; the arrow is never the
   target. Fixed width so swapping ▸ for ▾ cannot shift the text beside it. */
.ps-ex-arrow {
  flex: none; width: 20px; text-align: center;
  color: var(--accent); font-size: 24px; line-height: 1;
}
.ps-ex-when {
  flex: none; font-family: var(--mono); font-size: 9px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut);
}
.ps-ex-text {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13px; color: var(--ink);
}
.ps-ex-body { display: flex; flex-direction: column; gap: 14px; }

/* ===========================================================
   v21.25a — HISTORY IS A TRANSCRIPT, NOT BOXES
   -----------------------------------------------------------
   Josh on the shipped v21.25: "I have no idea where to type… it completely
   blends in." Every object weighed the same — his messages, the replies and
   the input were all the same bordered box with the same tiny label, so
   nothing read as touchable.

   The rule the whole re-weighting follows: BORDERS AND BOXES NOW MEAN
   "TOUCHABLE". Exactly one object is heavy — the dark composer below. So
   everything already said gives up its border and its fill: your turns keep a
   thin left rule, the Studio's are plain text. Nothing in the transcript may
   look like an input.

   v21.25a housekeeping: .ps-empty, .ps-prompt-label and .ps-prompt-edit are
   DELETED here. The first was the empty-state paragraph the dark composer
   replaces; the other two were orphaned by v21.25 when the prompt moved to the
   plate, and a scripted sweep of every .ps-* class against index.html + js/
   confirmed all three were referenced by nothing.
   =========================================================== */
/* ===========================================================
   v21.25c — WHO SAID WHAT, WITHOUT READING THE LABELS
   -----------------------------------------------------------
   Josh: "let's create more distinction between what I said and the prompt
   studio said. It's hard to tell the difference."

   The v21.25a constraint still holds — boxes and borders mean TOUCHABLE, so
   the answer cannot be to put the messages back in boxes. Distinction comes
   from THREE signals at once, none of which is a box:

     1. POSITION   your turns sit flush left; the Studio's are indented.
     2. COLOUR     yours are muted; the Studio's are full ink.
     3. THE RAIL   only the Studio's replies carry an accent rail, so the
                   answers form one visible column down the transcript.

   Three signals means it still reads if any one of them is weakened — and
   each is a single value here, so Josh can dial any of them without
   unpicking the others.
   =========================================================== */
.ps-role {
  margin: 0 0 5px; font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent-ink);
}
.ps-role-when { font-weight: 400; color: var(--mut); }
.ps-bubble {
  background: none; border: none; padding: 0;
  font-size: 13.5px; line-height: 1.6; color: var(--ink);
  white-space: pre-wrap;
}

/* YOU — v21.25d: full-strength ink on a soft neutral BAND.
   Josh: "make the 'you' parts more readable and not in that lighter gray. They
   should be JUST AS LEGIBLE as the other parts. Maybe try a different shade of
   gray behind them." So the muted text is gone: your words are now the same
   ink and the same size as the Studio's, and the distinction moves onto a
   background tint instead.

   A BAND IS NOT A BOX. The v21.25a rule sharpens to "BORDERS mean touchable" —
   this band has no border and no shadow, it is a grouping tint, and nothing in
   the app uses a bare fill as a control. #f1f0ea is the strongest tint that
   still clears 4.5:1 for the 9px mono label (--mut measures 4.59 on it; the
   next step down, #eeece4, drops it to 4.43). Both pairings are gated in
   tools/contrast-check.py so a future nudge to this value fails loudly. */
.ps-msg-you {
  padding: 11px 14px; border-radius: var(--r-sm);
  background: #f1f0ea;
}
/* v21.25e — Josh: "make YOU a dark purple instead of gray." #463366 measures
   9.5504:1 on the band, far past the 4.5 a 9px mono label needs. Deliberately
   NOT --accent-ink: that is the STUDIO's label colour, and the two voices
   staying distinct is the entire point of v21.25c/d. Raw hex rather than a new
   token, matching how the band tint itself was introduced in v21.25d — both
   are single-use values on this one surface, and both are gated in
   tools/contrast-check.py so neither can drift silently.
   .ps-role-when (the timestamp) stays --mut, which is why that pairing is
   still in the gate too. */
.ps-msg-you .ps-role { color: #463366; }
.ps-msg-you .ps-bubble { color: var(--ink); font-size: 13.5px; }

/* THE STUDIO — indented under an accent rail, full ink. The answers line up
   as one column, so a glance down the transcript separates the two voices
   before a single label is read. */
.ps-msg-ai { padding-left: 13px; border-left: 2px solid var(--accent); }
.ps-msg-ai .ps-role { color: var(--accent-ink); }
.ps-msg-ai .ps-bubble { color: var(--ink); }

.ps-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
/* v21.25: .ps-actions and .ps-measures are now rendered with the `hidden`
   attribute (empty plate = no buttons, no size chips), and both set `display`.
   Their [hidden] partners go in THIS edit — eight occurrences of that trap so
   far, and check 2 of tools/audit-a11y.py fails without these two lines. */
.ps-actions[hidden] { display: none !important; }
.ps-actions .primary, .ps-actions .secondary { padding: 9px 14px; font-size: 10px; }
/* v21.25a — THE PLATE'S PRIMARY IS OUTLINED, NOT FILLED.
   `.primary` is an ink slab app-wide (the V4 "ink anchor" button), so Copy was
   a SECOND dark mass on this screen and the one-loud-object rule only works if
   there is one. Outlined keeps it first among the four actions without
   competing with the composer for the eye. Scoped to .ps-plate so the ink
   anchor is untouched everywhere else in the app — measured ink-on-white at
   18.46:1 for the label, and a 2px ink edge is far past the 3:1 WCAG 1.4.11
   asks of a control boundary. */
.ps-plate .ps-actions .primary {
  background: transparent; color: var(--ink); border: 2px solid var(--ink);
}
.ps-plate .ps-actions .primary:hover { background: var(--ink); color: #fff; }

/* v21.32: the Midjourney jump-link is a real <a>, not a <button> — every
   other rule above it (.secondary, .ps-act) already carries the right
   background/border/color for it for free, since none of that is
   button-specific. The one thing an <a> needs that a <button> never did is
   its browser-default underline removed (checked via computed styles, not
   assumed — nothing above sets text-decoration at all, because buttons
   don't have one to remove). */
.ps-act-mj { text-decoration: none; }

/* ----- v21.23: the preset bar ----- */
.ps-presets { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
.ps-preset { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 160px; }
/* .ps-preset carries the `hidden` attribute on the engine picker, and it sets
   `display` — so it gets its [hidden] partner in the SAME edit. Five
   occurrences of that trap so far (.card-form v5, .section-head v21.12,
   .review-details + .detail-concept v21.12b, .detail-homes v21.22); check 2 of
   tools/audit-a11y.py fails without the next line. */
.ps-preset[hidden] { display: none !important; }
.ps-preset-label {
  font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--mut);
}
.ps-preset select { width: 100%; }
/* Which presets a saved prompt was written under. Sits in the role line, so a
   thread that switched target half way can be read at a glance. */
.ps-badge {
  margin-left: 8px; padding: 2px 7px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  font-family: var(--mono); font-size: 8.5px; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut);
}

/* ----- v21.23: the Settings mapping editor ----- */
.subject-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.subject-row { display: grid; grid-template-columns: 1.2fr 1fr 1.4fr auto; gap: 8px; align-items: center; }
.subject-row .search-box { margin-bottom: 0; }
.subject-del { padding: 8px 12px; line-height: 1; }
@media (max-width: 640px) {
  .subject-row { grid-template-columns: 1fr 1fr; }
  .subject-del { grid-column: 2; justify-self: end; }
}

/* ----- v21.24: the image tray -----
   .ps-tray IS rendered with the `hidden` attribute (js/promptstudio.js sets
   trayEl.hidden when nothing is attached) and it DOES set `display`, so it gets
   its [hidden] partner in the SAME edit. Seven occurrences of that trap now —
   .card-form (v5), .section-head (v21.12), .review-details + .detail-concept
   (v21.12b), .detail-homes (v21.22), .ps-preset (v21.23), and this. */
/* v21.25a: the tray sits INSIDE the dark control row now, beside the attach
   button, so the thumbnails travel with the control that made them. */
.ps-tray { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 0; }
.ps-tray[hidden] { display: none !important; }
.ps-ref {
  position: relative; width: 44px; height: 44px; flex: none;
  border: 1px solid var(--mut); background: var(--thumb-bg); overflow: hidden;
}
.ps-ref img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ps-ref-none { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: var(--mut); }
/* The measured size, on the piece it belongs to. */
.ps-ref-size {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: rgba(20, 19, 26, 0.72); color: #fff;
  font-family: var(--mono); font-size: 7.5px; letter-spacing: 0.04em;
  text-align: center; padding: 2px 1px;
}
.ps-ref-x {
  position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; padding: 0;
  border: none; border-radius: 50%; cursor: pointer; line-height: 1; font-size: 12px;
  background: rgba(20, 19, 26, 0.66); color: #fff;
}
.ps-ref-x:focus-visible { outline: none; box-shadow: var(--ring); }
.ps-attach-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 0; }
/* v21.27 — the inline collection picker under the attach row. Reuses
   .ps-compose's dark select styling and the existing primary/secondary
   button classes, so nothing new is needed beyond layout. */
.ps-collection-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.ps-collection-row[hidden] { display: none !important; }
.ps-attach-hint {
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.1em;
  text-transform: uppercase; color: #b9b5c6;
}

/* ----- v21.24: sizes read off the pictures, and correctable ----- */
.ps-measures { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 10px; }
.ps-measures[hidden] { display: none !important; }   /* v21.25 — see .ps-actions above */
.ps-measures-head {
  font-family: var(--mono); font-size: 8.5px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--mut);
  width: 100%;
}
.ps-measure {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line); background: var(--panel); cursor: pointer;
  padding: 5px 10px; border-radius: var(--r-pill);
  font-size: 12px; font-weight: 600; color: var(--ink);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ps-measure:hover { border-color: var(--ink); }
.ps-measure:focus-visible { outline: none; box-shadow: var(--ring); }
.ps-measure-val { font-family: var(--mono); font-size: 9.5px; color: var(--mut); }

/* ----- v21.24b: the working card, inline in the conversation -----
   The card itself is `.wr-generating`, the app's shared "AI is working" card.
   This class does what `.studio-progress-card` does for the Design Studio's
   copy — SIZING ONLY. No new card language, no third treatment.

   It is smaller than the Studio's because it sits in a message list rather
   than centred over the page: a 26px display title and 34px of padding are
   right for a full-screen moment and shouting inside a conversation. There is
   no `.wr-gen-bar` in this one on purpose — see the note in js/promptstudio.js
   for why a fake bar loses to honest elapsed time.

   NOTE: no `display` rule here, and none wanted. This card is created and
   removed, never toggled with the `hidden` attribute, so it cannot become the
   eighth occurrence of that trap. */
.ps-working { padding: 20px 18px; box-shadow: none; }
.ps-working .wr-gen-spark { width: 40px; height: 40px; font-size: 19px; margin-bottom: 12px; }
.ps-working .wr-gen-title { font-size: 17px; margin-bottom: 7px; }
.ps-working .wr-gen-sub { font-size: 12.5px; margin-bottom: 12px; }
.ps-working .wr-gen-note { font-size: 8.5px; }

/* ===========================================================
   v21.25 — THE PLATE: the prompt, permanently on screen
   -----------------------------------------------------------
   Sticky on the wide tier for the same reason the thread rail is: the page
   scrolls on past this panel to the saved-prompt grid, and the workpiece
   should still be there when it does.
   =========================================================== */
.ps-plate {
  min-width: 0;
  border: 1px solid var(--line); background: var(--panel);
  border-top: 3px solid transparent; border-image: var(--rainbow) 1;
  padding: 16px 18px 18px;
  position: sticky; top: 18px;
}
.ps-plate-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.ps-plate-kicker {
  margin: 0; font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--red);
}
.ps-plate-when {
  margin: 0; font-family: var(--mono); font-size: 9px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut);
}
.ps-plate-empty { margin: 0; font-size: 13.5px; line-height: 1.6; color: var(--mut); }
.ps-plate-empty[hidden] { display: none !important; }

/* The workpiece itself. Tall on purpose — this is the thing being read. */
.ps-plate-text {
  display: block; width: 100%; box-sizing: border-box;
  font: inherit; font-size: 13.5px; line-height: 1.6;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--ink);
  border-radius: var(--r-sm); padding: 13px 15px; resize: vertical;
  min-height: 300px;
}
.ps-plate-text[hidden] { display: none !important; }

/* ----- v21.25e: the humanized Body-Forward view -----
   .ps-plate-human and .ps-plate-code are BOTH rendered with the `hidden`
   attribute and BOTH set `display`, so their [hidden] partners go in THIS
   edit. That trap has bitten this project eight times; check 2 of
   tools/audit-a11y.py fails without the two lines below. */
.ps-plate-human { display: block; }
.ps-plate-human[hidden] { display: none !important; }
.ps-human-body {
  font-size: 13.5px; line-height: 1.6; color: var(--ink);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.ps-human-head {
  margin: 14px 0 5px; font-family: var(--mono); font-size: 8.5px; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--mut);
}
.ps-human-neg {
  font-size: 12.5px; line-height: 1.55; color: var(--mut);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
/* The settings, as quiet mono rows — what the engine is being told, at a
   glance, without reading a payload. */
.ps-sets { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.ps-set { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
.ps-set-k {
  font-family: var(--mono); font-size: 8.5px; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut);
}
.ps-set-v { font-family: var(--mono); font-size: 11px; color: var(--ink); }

.ps-plate-code {
  display: inline-block; margin-top: 12px;
  border: none; background: none; cursor: pointer; padding: 4px 0;
  font-family: var(--mono); font-weight: 700; font-size: 9px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut);
}
.ps-plate-code[hidden] { display: none !important; }
.ps-plate-code:hover { color: var(--ink); text-decoration: underline; }
.ps-plate-code:focus-visible { outline: none; box-shadow: var(--ring); }
.ps-plate-text:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

/* "You are looking at an earlier version" — a state you can only reach on
   purpose, and can leave in one click. */
/* v21.25a: accent-pink, per the approved lookbook — this is a state you chose
   and can leave, so it should read as a live notice rather than grey chrome. */
.ps-plate-older {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap;
  margin-bottom: 10px; padding: 8px 10px;
  border: 1px dashed var(--pink-ink); background: rgba(255, 46, 136, 0.06);
  font-size: 12px; color: var(--pink-ink);
}
.ps-plate-older[hidden] { display: none !important; }
.ps-plate-latest {
  border: none; background: none; cursor: pointer; padding: 0;
  font-family: var(--mono); font-weight: 700; font-size: 9.5px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--pink-ink);
}
.ps-plate-latest:hover { text-decoration: underline; }
.ps-plate-latest:focus-visible { outline: none; box-shadow: var(--ring); }

/* The stream's marker for a turn that produced a prompt. */
/* v21.25a: rounded PILLS. Outlined for a past version, one filled ink pill for
   the version on the bench — and exactly one can ever be filled, because
   `activePlateId` is a single value. In a transcript that has given up its
   boxes, these pills are the only touchable thing left, which is the point. */
.ps-version {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 9px;
  border: 1px solid var(--mut); background: transparent; cursor: pointer;
  padding: 5px 12px; border-radius: var(--r-pill);
  font-family: var(--mono); font-weight: 700; font-size: 9px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink);
}
.ps-version:hover { border-color: var(--ink); background: var(--panel-2); }
.ps-version:focus-visible { outline: none; box-shadow: var(--ring); }
.ps-version-mark { color: var(--red); }
.ps-version-when { color: var(--mut); font-weight: 400; }
.ps-version.on { background: var(--ink); border-color: var(--ink); color: #fff; }
.ps-version.on .ps-version-mark, .ps-version.on .ps-version-when { color: #fff; }

/* ===========================================================
   v21.25a — THE COMPOSER: the one dark object on a light canvas
   -----------------------------------------------------------
   The place you act is ONE object: headline, textarea and every control in a
   single ink card. Nothing else on this screen is dark, so the eye has exactly
   one landing place.

   CONTRAST IS MEASURED, NOT EYEBALLED. This is the app's first dark surface,
   and tools/audit-a11y.py's check 6 gates the LIGHT ones — so every pairing in
   here is proved by `python3 tools/contrast-check.py`, which fails the run if
   any of them drops below target. The values below are the ones it passes
   with; changing a colour here means re-running it. Measured on #14131a:
     #ff7ab3 headline  7.63    #b9b5c6 hints/labels  9.22
     #f2f0f6 on #211f2a select text  14.35
     --ink on #ffffff button + field  18.46
     #6e6a7a control border  3.52 (WCAG 1.4.11 wants 3:1 to IDENTIFY a control,
       and the sunk fill alone is only 1.60 against the card, so the border
       carries that job)
   =========================================================== */
.ps-compose {
  background: var(--ink); color: #f2f0f6;
  border: 1px solid var(--ink);
  border-top: 3px solid transparent; border-image: var(--rainbow) 1;
  padding: 16px 16px 14px; margin-top: 4px;
  /* v21.25d — BREATHING ROOM. Josh: "the top of the conversation is way too
     close to the bottom of the input box." Measured before this line: the gap
     between the composer's bottom edge and the transcript's top was 0px at
     both 1440 and 390. */
  margin-bottom: 24px;
}

/* The coached headline — it changes with the moment (see COACH in
   js/promptstudio.js). Pink-tinted, per the approved lookbook. */
.ps-coach { margin: 0 0 11px; display: flex; flex-direction: column; gap: 3px; }
.ps-coach-head {
  font-family: var(--mono); font-weight: 700; font-size: 11.5px;
  letter-spacing: 0.14em; text-transform: uppercase; color: #ff7ab3;
}
.ps-coach-hint { font-size: 12.5px; line-height: 1.45; color: #b9b5c6; }

/* THE FIELD. White on ink with a 2px accent ring — the universal "type here".
   This is the only white block inside the dark card, so it cannot be missed. */
.ps-compose textarea {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 13.5px; line-height: 1.55;
  border: 2px solid var(--accent); background: #ffffff; color: var(--ink);
  border-radius: var(--r-sm); padding: 11px 13px; resize: vertical;
}
.ps-compose textarea::placeholder { color: var(--mut); }
.ps-compose textarea:focus { outline: none; box-shadow: 0 0 0 3px rgba(224, 50, 47, 0.28); }

/* One row: presets, attach + thumbnails, submit. Wraps on narrow. */
.ps-controls { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin-top: 11px; }
.ps-controls > * { min-width: 0; }
.ps-controls .ps-presets { flex: 1 1 260px; margin-bottom: 0; }
.ps-preset-label { color: #b9b5c6; }
.ps-compose select {
  background: #211f2a; color: #f2f0f6;
  border: 1px solid var(--mut); border-radius: var(--r-xs);
  padding: 7px 9px; font: inherit; font-size: 12.5px;
}
.ps-compose select:focus { outline: none; box-shadow: 0 0 0 3px rgba(224, 50, 47, 0.28); }

.ps-attach {
  border: 1px solid var(--mut); background: none; color: #f2f0f6; cursor: pointer;
  padding: 8px 12px; border-radius: var(--r-xs);
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase;
}
.ps-attach:hover { border-color: #f2f0f6; }
.ps-attach:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(224, 50, 47, 0.28); }
.ps-attach:disabled { opacity: 0.5; cursor: default; }

/* THE SUBMIT. White on ink is the brightest thing available on this card, so
   it wins the eye without importing a second brand colour — and it avoids
   re-using the brand red at its known 4.4999 white-on-red pairing. */
.ps-send {
  margin-left: auto; border: none; cursor: pointer;
  background: #ffffff; color: var(--ink);
  padding: 11px 18px; border-radius: var(--r-xs);
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase;
}
.ps-send:hover { background: #ffe6f1; }
.ps-send:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(224, 50, 47, 0.45); }
.ps-send:disabled { opacity: 0.55; cursor: default; }

/* While writing: the card dims and locks. The working card in the stream
   stays the primary signal (v21.24b) — this is only "not now". */
.ps-compose.is-busy { opacity: 0.72; }
.ps-compose.is-busy .ps-coach-head { color: #b9b5c6; }

/* "Using the last N of M messages" — spec §8 asks for what is in context to
   be shown rather than silently truncated. Inside the dark card now. */
.ps-context {
  margin: 10px 0 0; font-family: var(--mono); font-size: 9px;
  letter-spacing: 0.12em; text-transform: uppercase; color: #b9b5c6;
}

/* ----- iPad and below: the rail stacks, and THE BENCH STACKS PLATE-FIRST -----
   One breakpoint, not two. The rail already collapses at 1100 (v21.20), and
   the bench runs out of room at the same place for the same reason — below it
   two text columns are both too narrow to read. 1100 is above the 1024 tier
   the brief names, so it satisfies "1024 and below" while keeping this screen
   on a single breakpoint instead of inventing a second one to remember.

   PLATE FIRST when stacked, which is the lookbook Josh approved: the prompt is
   what you came for, so it should not sit under a conversation you have to
   scroll past. Order is plate -> conversation -> composer. */
@media (max-width: 1100px) {
  .ps-bench { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .ps-plate { order: -1; position: static; }
  .ps-plate-text { min-height: 220px; }
  .ps-layout { grid-template-columns: 1fr; gap: 14px; }
  .ps-rail {
    position: static; border-right: none; border-bottom: 1px solid var(--line);
    padding: 0 0 10px;
  }
  .ps-thread-list { flex-direction: row; gap: 6px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
  .ps-thread-list::-webkit-scrollbar { display: none; }
  .ps-thread { flex: none; border: 1px solid var(--line); border-radius: var(--r-xs); max-width: 240px; }
  .ps-thread.active { border-color: var(--red); }
  .ps-thread-menu-btn { display: none; }
}

/* ===========================================================
   Threads on a PHONE — the same bottom sheet as the Vault's folders
   (v21.7.5). The sheet IS #ps-rail, re-laid-out by CSS: same markup,
   same handlers, no duplicate DOM.
   =========================================================== */
.ps-thread-btn { display: none; }
.ps-sheet-backdrop { display: none; }

@media (max-width: 820px) {
  .ps-thread-btn {
    display: flex; align-items: center; gap: 10px; width: 100%;
    margin-bottom: 12px; padding: 13px 14px;
    background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xs);
    cursor: pointer; font: inherit; color: var(--ink);
  }
  .ps-thread-btn-icon { color: var(--red); font-size: 14px; line-height: 1; }
  .ps-thread-btn-label {
    flex: 1; text-align: left;
    font-family: var(--disp); font-weight: 800; font-size: 17px;
    text-transform: uppercase; letter-spacing: -0.01em;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .ps-thread-btn-chev { color: var(--mut); font-size: 12px; }

  .ps-sheet-backdrop {
    display: block; position: fixed; inset: 0; z-index: 10080;
    background: rgba(20, 19, 26, 0.5);
    opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
  }
  body.ps-threads-open .ps-sheet-backdrop { opacity: 1; pointer-events: auto; }

  .ps-rail {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    z-index: 10081;
    display: block;                       /* undo the iPad chip row */
    max-height: 76vh; overflow-y: auto; overflow-x: hidden;
    padding: 18px 16px calc(20px + env(safe-area-inset-bottom, 0px));
    background: var(--panel);
    border: none; border-top: 4px solid transparent; border-image: var(--rainbow) 1;
    box-shadow: 0 -18px 50px rgba(20, 19, 26, 0.3);
    transform: translateY(101%);
    transition: transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  body.ps-threads-open .ps-rail { transform: translateY(0); }

  /* Rows go back to being rows — the sheet is a list, not a chip strip. */
  .ps-rail .ps-thread-list { flex-direction: column; overflow-x: visible; }
  .ps-rail .ps-thread { border: none; max-width: none; min-height: 46px; }
  .ps-rail .ps-thread-menu-btn { display: block; opacity: 1; }
  .ps-panel { padding: 16px 14px 18px; }
  .ps-messages { max-height: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ps-rail, .ps-sheet-backdrop { transition: none; }
}

.studio-results { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 18px; }
.studio-results:empty { display: none; }

/* ONE result: the concept gets the FULL width of the canvas and its fields go
   underneath in two columns.
   The first attempt at this put the fields in a column beside the image. That
   only works for tall images: the field stack is ~850px, so a landscape
   concept — the 3-view sheets Josh actually generates — left a half-page of
   dead white under a small picture. Width is the thing that makes an image
   big here, so the image takes all of it and the form gets short and wide.
   `contain` + a mat rather than `cover`, because this is the one screen in the
   app where cropping the artwork to fit would be the wrong trade. */
.studio-results.one { grid-template-columns: minmax(0, 1fr); }
.studio-results.one .review-img {
  width: 100%; max-height: 72vh; object-fit: contain;
  background: var(--panel-2);
}
.studio-results.one .review-body {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
  gap: 10px 14px;
  padding: 16px 18px 18px;
}
/* `.primary` carries `grid-column: 1 / -1` for the two-column .card-form, which
   made "Save changes" span here and knocked the action row out of alignment. */
.studio-results.one .review-body .primary { grid-column: auto; }
/* Keep / Delete is the decision, so it gets the full width; the prompt and the
   saved-state line read across the card too. */
.studio-results.one .review-decisions,
.studio-results.one .review-toggle,
.studio-results.one .review-details,
.studio-results.one .review-status-line { grid-column: 1 / -1; }

/* ---------- Review card (generated concepts) ----------
   v21.8: squared and neutral. The image carries the spectrum frame the rest of
   the app uses, and the pastel mint/gold/pink decision fills are gone — Keep is
   the ink anchor, Delete is a red outline. (Maybe/Reject were removed from the
   markup back in v15; their styles went with them.) */
.review-card {
  background: var(--panel);
  border: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  box-shadow: var(--sh-1);
}
.review-img {
  width: 100%; display: block; background: var(--thumb-bg);
  border-bottom: 3px solid transparent; border-image: var(--rainbow) 1;
}
.review-body { padding: 14px; display: grid; gap: 8px; }
.review-decisions { display: flex; gap: 6px; flex-wrap: wrap; }
.review-btn {
  flex: 1; min-width: 58px;
  border: 1px solid var(--line); background: var(--panel); color: var(--mut);
  border-radius: var(--r-xs); padding: 9px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer;
}
.review-btn:hover { border-color: var(--ink); color: var(--ink); }
.review-btn.keep.active { background: var(--ink); border-color: var(--ink); color: #fff; }
.review-btn.del { color: var(--red); border-color: rgba(224, 50, 47, 0.35); }
.review-btn.del:hover { background: var(--red); border-color: var(--red); color: #fff; }
.review-notes {
  width: 100%; min-height: 48px; box-sizing: border-box; resize: vertical;
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px; font: inherit; background: var(--panel-2);
}
.review-assign { display: flex; gap: 6px; flex-wrap: wrap; }
.review-assign select {
  flex: 1; min-width: 120px; padding: 8px; font: inherit; font-size: 12.5px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel-2);
}
.review-toggle {
  background: none; border: none; cursor: pointer; text-align: left; padding: 0;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut);
}
.review-toggle:hover { color: var(--ink); }
/* v21.12b: js/studio.js toggles this with setAttribute("hidden"), but
   `display: grid` won, so "View prompt & sources" changed its own label while
   the block stayed on screen. Live bug since the card was built. */
.review-details { font-size: 12.5px; color: var(--soft); display: grid; gap: 8px; }
.review-details[hidden] { display: none !important; }
.review-prompt { line-height: 1.45; color: var(--ink); }
.review-sources { display: flex; gap: 6px; flex-wrap: wrap; }
.review-sources img { width: 40px; height: 40px; object-fit: cover; border: 1px solid var(--line); }
.review-status-line {
  min-height: 14px;
  font-family: var(--mono); font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut);
}
.review-title { width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px; font: inherit; background: var(--panel-2); }
.review-tags-row { display: flex; gap: 6px; }
.review-tags-row input { flex: 1; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 8px; font: inherit; background: var(--panel-2); }
.review-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.review-typerow { display: flex; gap: 6px; }
.review-typerow select { flex: 1; padding: 8px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel-2); font: inherit; font-size: 12.5px; }
.review-save { width: 100%; }
.review-download { width: 100%; }

/* ===========================================================
   Branded "type something" dialog (v21.8.2) — js/ask.js
   Replaces window.prompt(), which showed the raw Vercel URL in system
   chrome and read like an error. Squared, ink, mono labels, and the
   rainbow along the top edge — the same signature the bulk-action bar
   uses, because these are the app's only two floating surfaces.
   =========================================================== */
/* v22.22 — 10082: above the phone sheets (the Vault's folders and the
   Prompt Studio's threads, both 10081), which open boxes from inside them —
   at 10080 "+ New collection" on a phone opened its box underneath. Still
   below the toasts (10091). */
.ask-overlay {
  position: fixed; inset: 0; z-index: 10082;
  display: flex; align-items: center; justify-content: center; padding: 24px;
  background: rgba(20, 19, 26, 0.55);
  animation: ask-fade 0.14s ease-out;
}
.ask-modal {
  position: relative; overflow: hidden;
  width: 100%; max-width: 460px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-sm); box-shadow: var(--sh-3);
  padding: 26px 26px 22px;
  animation: ask-rise 0.16s ease-out;
}
.ask-modal::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
  background: var(--rainbow);
}
.ask-title {
  margin: 0 0 6px;
  font-family: var(--disp); font-weight: 800; font-size: 26px; line-height: 1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
}
.ask-body { margin: 0 0 4px; font-size: 13.5px; line-height: 1.55; color: var(--mut); }
/* The "nothing is lost" line. Quiet, but it earns a rule so it reads as
   reassurance rather than more warning text. */
.ask-reassure {
  margin: 12px 0 0; padding: 9px 12px;
  border-left: 3px solid var(--green); background: var(--panel-2);
  font-size: 13px; line-height: 1.5; color: var(--soft);
}
.ask-label {
  display: block; margin: 14px 0 6px;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut);
}
.ask-input {
  width: 100%; box-sizing: border-box;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 11px 12px; font: inherit; font-size: 14px;
  background: var(--panel-2); color: var(--ink);
}
.ask-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.ask-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; margin-top: 18px; }
.ask-actions .primary { grid-column: auto; }
/* v22.8 — a button label never breaks mid-word. body carries
   `overflow-wrap: anywhere`, so when a three-button confirm (Discard it /
   Cancel / Save and close) ran out of room on a phone, flex shrank the
   buttons and "Cancel" broke to "CANCE / L". nowrap keeps every label
   whole; the row wraps instead (above) and, on a phone, stacks (below). */
.ask-actions > button { white-space: nowrap; }
@media (max-width: 480px) {
  /* Full-width and stacked, in the same DOM order — every label on one
     line however many buttons the dialog carries. */
  .ask-actions { flex-direction: column; align-items: stretch; }
  .ask-actions > button { width: 100%; }
}
/* Destructive confirm: red anchor, so "Delete project" never looks like "Save". */
.ask-danger { background: var(--red); border-color: var(--red); }
.ask-danger:hover { background: #c72a27; border-color: #c72a27; }

@keyframes ask-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ask-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .ask-overlay, .ask-modal { animation: none; }
}

/* ---------- Loading skeletons (v21.10b) ----------
   A grey shape that says "this is coming" is not the same message as "there is
   nothing here", and the app had been using the second to mean the first. Used
   wherever a list waits on the cloud. */
.skeleton {
  background: var(--thumb-bg);
  animation: skeleton-pulse 1.4s ease-in-out infinite;
}
@keyframes skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }
.skeleton-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
.skeleton-tile { aspect-ratio: 4 / 5; }
.skeleton-rows { display: grid; gap: 10px; }
.skeleton-row { height: 58px; }

/* ---------- Focus rings (v21.10b) ----------
   Every interactive control gets the same visible ring. The audit found
   buttons, links and file labels that showed nothing at all on keyboard focus,
   which on a screen with a permanent-delete button is not a small thing.
   :focus-visible only, so a mouse click never paints a ring. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: none;
  box-shadow: var(--ring);
  border-radius: var(--r-xs);
}
/* Inputs already carry a border colour change on :focus; keep both. */
input:focus-visible, select:focus-visible, textarea:focus-visible { border-color: var(--accent); }

/* The Trash is a list, not a thumbnail grid. */
.vault-gallery.is-trash { display: block; }
/* Upload chrome and the density switch have no meaning in the Trash — you are
   not adding pieces here and there are no thumbnails to resize. */
body.vault-in-trash .vault-utility-row,
body.vault-in-trash #vault-density,
body.vault-in-trash #vault-select-all { display: none; }

/* ---------- THE TRASH (v21.11) ----------
   Rows, not a gallery: these pieces need Restore / Delete forever, and the one
   permanently destructive button in the app should not look like a card. */
.trash-head {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  border: 1px solid var(--line); background: var(--panel);
  padding: 16px 18px; margin-bottom: 14px;
}
.trash-count {
  font-family: var(--disp); font-weight: 800; font-size: 22px; line-height: 1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
}
.trash-note { flex: 1; min-width: 220px; font-size: 12.5px; line-height: 1.5; color: var(--mut); }
.trash-empty { flex: none; }

.trash-list { display: grid; gap: 10px; }
.trash-row {
  display: flex; align-items: center; gap: 13px;
  border: 1px solid var(--line); background: var(--panel); padding: 12px;
}
.trash-thumb { flex: none; width: 52px; height: 62px; overflow: hidden; background: var(--thumb-bg); }
.trash-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.trash-main { flex: 1; min-width: 0; }
.trash-name {
  font-family: var(--disp); font-weight: 800; font-size: 15px; line-height: 1.05;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trash-meta {
  margin-top: 4px;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--mut);
}
.trash-restore, .trash-forever { flex: none; }

/* A project whose concept is in the Trash — a placeholder with a way back,
   never a broken image. */
.wr-pj-ph.is-trashed {
  display: grid; place-content: center; font-size: 20px;
  /* --mut, not --faint: this glyph IS the thumbnail for a trashed project —
     it is the only thing on the tile. At --faint it measured 2.35:1, which
     misses even the 3:1 bar WCAG 1.4.11 sets for non-text. */
  background: var(--bg-flat); color: var(--mut);
}
.wr-pj-trashed {
  flex-basis: 100%; margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--line);
  font-size: 12px; color: var(--mut);
}
.wr-pj-restore {
  border: 0; background: none; padding: 0 0 0 2px; cursor: pointer;
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--red);
  border-bottom: 1px solid currentColor;
}
.wr-pj-restore:hover { color: var(--ink); }
.wr-pj { flex-wrap: wrap; }

/* ---------- Settings: Export (v21.10) ---------- */
.export-status {
  margin: 10px 0 0; font-size: 13px; line-height: 1.5; color: var(--mut);
  max-width: 620px;
}
.export-status:empty { display: none; }
.export-status.is-ok { color: #0b7a5f; font-weight: 600; }
.export-status.is-warn { color: #a8620e; font-weight: 600; }
.export-status.is-error { color: #b32421; font-weight: 600; }

.export-files { margin: 16px 0 0; border: 1px solid var(--line); background: var(--panel); max-width: 720px; }
.export-files > summary {
  cursor: pointer; padding: 14px 16px; list-style: none; user-select: none;
  display: flex; align-items: center; gap: 10px;
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink);
}
.export-files > summary::-webkit-details-marker { display: none; }
.export-files > summary::before {
  content: "▸"; font-size: 16px; line-height: 1; color: var(--mut);
  display: inline-block; transition: transform 0.15s ease;
}
.export-files[open] > summary::before { transform: rotate(90deg); color: var(--red); }
.export-files > summary:hover { background: var(--panel-2); }
.export-files[open] > summary { border-bottom: 1px solid var(--line); }
.export-files-body { padding: 14px 16px 16px; }
.export-files-body .hint { max-width: none; }
/* Default browser blue has no business on this canvas. */
.export-files-body a {
  color: var(--ink); font-weight: 600;
  text-decoration: none; border-bottom: 1px solid var(--red);
}
.export-files-body a:hover { color: var(--red); }
.export-steps { margin: 12px 0; padding-left: 20px; }
.export-steps li { font-size: 13px; line-height: 1.55; color: var(--ink); margin-bottom: 12px; }
.export-steps code, .export-files-body code {
  display: block; margin-top: 6px; padding: 9px 11px;
  background: var(--ink); color: #fff;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
  overflow-x: auto; white-space: pre;
  -webkit-user-select: all; user-select: all;   /* one click selects the line */
}
/* The inline mentions in the closing note are not commands — keep them small. */
.export-files-body > .hint code {
  display: inline; margin: 0; padding: 1px 5px;
  background: var(--panel-2); color: var(--ink); white-space: normal;
}

/* ---------- Settings: the pinned Home hero (v21.9.5) ---------- */
.hero-pin {
  display: flex; align-items: center; gap: 14px;
  border: 1px solid var(--line); background: var(--panel);
  padding: 12px; max-width: 520px;
}
.hero-pin-thumb { flex: none; width: 56px; height: 70px; overflow: hidden; background: var(--thumb-bg); }
.hero-pin-thumb.is-empty { border: 1px dashed var(--line); background: none; }
.hero-pin-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hero-pin-main { min-width: 0; }
.hero-pin-name {
  font-family: var(--disp); font-weight: 800; font-size: 16px; line-height: 1.05;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hero-pin-actions { display: flex; align-items: center; gap: 10px; margin-top: 9px; }

/* ---------- Migration log (Settings import) ---------- */
.migrate-log {
  font-size: 12.5px;
  color: var(--soft);
  margin-top: 10px;
  display: grid;
  gap: 2px;
  max-height: 220px;
  overflow-y: auto;
}
.migrate-log div { padding: 1px 0; }

/* ===========================================================
   Shared detail lightbox (detail.js) — editorial review panel
   =========================================================== */
.detail-overlay {
  position: fixed; inset: 0; z-index: 10002;
  display: none; align-items: center; justify-content: center;
  background: rgba(12, 8, 18, 0.74); padding: 24px;
  backdrop-filter: blur(3px);
}
.detail-overlay.open { display: flex; }
.detail-shell {
  position: relative;
  background: var(--panel); border-radius: var(--r-xl); overflow: hidden;
  width: min(1120px, 96vw); max-height: 92vh;
  display: grid; grid-template-columns: 1.55fr 1fr;
  box-shadow: 0 40px 100px rgba(0, 0, 0, 0.5);
}
.detail-shell-project { grid-template-columns: 1fr; width: min(640px, 96vw); }
.detail-close {
  position: absolute; top: 12px; right: 14px; z-index: 3;
  width: 38px; height: 38px; border-radius: 50%; border: none;
  background: rgba(255, 255, 255, 0.92); color: var(--ink); font-size: 22px; cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}
.detail-close:hover { background: #fff; }
.detail-image {
  background-color: var(--image-stage);
  /* Cinematic: a prism vignette pools behind the previewed image. */
  /* v21.7.3: the prism vignette is gone. A photograph should sit on a neutral
     stage — coloured light behind it changes how the garment colour reads. */
  display: flex; align-items: center; justify-content: center; min-height: 320px; max-height: 92vh; padding: 18px;
  /* v21.66 — HOLDS IN the crop tool's veil. .detail-crop-select's box-shadow
     (below, ~line 3668) spreads 9999px to darken everything outside the
     selection; .detail-shell's own overflow:hidden only stops it at the
     WHOLE MODAL's edge, which left it darkening the side panel too (Josh
     reported "Apply crop" as greyed out — it was never disabled, just under
     60% black). This clips it at the image stage specifically. Do not
     remove this as "just tidying" — it is load-bearing for the crop veil.
     Verified it doesn't clip anything else: the normal (non-crop) lightbox
     image, and the Upscaled-versions thumbnails, never draw outside this
     box in the first place (object-fit:contain within the flex-centred
     padding), so nothing here changes for either. */
  overflow: hidden;
}
.detail-image img { max-width: 100%; max-height: 92vh; object-fit: contain; display: block; border-radius: 6px; }
.detail-side { padding: 28px 26px; display: grid; gap: 4px; align-content: start; overflow-y: auto; max-height: 92vh; }
.detail-side label { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--mut); margin-top: 18px; }
.detail-meta { display: grid; gap: 4px; align-content: start; }
.detail-side input, .detail-side select, .detail-side textarea {
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px; background: var(--panel-2); font: inherit; color: var(--ink);
}
.detail-side input:focus, .detail-side select:focus, .detail-side textarea:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.detail-side textarea { min-height: 64px; resize: vertical; }
/* ★ v21.91 — ONE ROW, MEASURED. The shell is min(1120px, 96vw) split
   1.55fr/1fr, so the side column is 439px and its content box 387.2px after
   .detail-side's 26px padding. At the previous padding the four buttons summed
   to 376px + 30px of gaps — three rows. At the padding below they sum to
   334px + 24px of gaps = 358px: one row with 29px spare, icons kept. */
.detail-actions { display: flex; gap: 8px; align-items: center; margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.detail-actions .primary   { padding: 11px 14px; }
.detail-actions .secondary { padding: 11px 11px; }
/* v21.92a — two lines were being drawn above Delete: this row's own full-width
   border-top, and .delete-button's, which is only as wide as the button.
   ★ SCOPED, not a change to .delete-button itself. That class is shared by
   seven places — Delete selected, Empty the Trash, Delete forever on every
   trash row, Builds project rows, catalog list rows, and both lightboxes —
   and in the stacked lists its border is the only thing separating rows. */
.detail-actions .delete-button { border-top: 0; }

/* Below 1120px the modal shrinks with the viewport and the row would wrap
   3+1. Force 2+2 instead.
   ★ .primary carries `grid-column: 1 / -1` globally (css/styles.css:759).
   That is inert inside a flex row but makes Save span the FULL width the
   moment this container becomes a grid. It must be overridden here. */
@media (max-width: 1120px) {
  .detail-actions { display: grid; grid-template-columns: 1fr 1fr; }
  .detail-actions .primary { grid-column: auto; }
  .detail-actions button { width: 100%; }
}
/* Shrink safety for the grid above. Scoped nowhere — a grid item's default
   `min-width: auto` refuses to go below its content, which is what makes a
   2-column grid of buttons overflow instead of fitting. Flagged by
   tools/audit-a11y.py the moment .detail-actions became a grid. */
.detail-actions > * { min-width: 0; }
/* v21.98 — .bs-bar became a grid in the same way and tripped the same check.
   Not cosmetic: without it the sub-line ("Updated … · 12 pieces, 4 patches")
   cannot shrink below its min-content width and would push the arrow out of
   the bar on a narrow panel. */
.bs-bar > * { min-width: 0; }

/* ★ v21.91 — SAVE IS THE SAVE-STATE INDICATOR: red while anything is
   unsaved, green the moment it lands. Same tokens and the same two-class
   shape as the moodboard's own Save (js/moodboard.js), and the specificity
   is load-bearing: .primary.create is (0,2,0), so a bare one-class green
   rule would lose to red whenever both classes are on the element. */
.primary.detail-clean, .primary.detail-clean:hover,
.detail-clean, .detail-clean:hover {
  background: var(--green); border-color: var(--green); color: #fff;
}

.detail-tags { display: flex; flex-wrap: wrap; gap: 7px; min-height: 22px; margin-top: 6px; }
.detail-tags-empty { color: var(--mut); font-size: 12.5px; }
/* Collections list gets a little more room between chips and the picker. */
#detail-collections { margin-bottom: 2px; }
.tag-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--accent-soft); color: var(--accent-ink); font-size: 12px; font-weight: 600;
  padding: 4px 6px 4px 10px; border-radius: var(--r-pill);
}
/* .tag-x / .tag-edit stay — js/studio.js renders its own tag-editor with
   these same two classes (grepped the whole codebase before touching
   either rule); only js/detail.js's asset-tags block stopped using them
   in v21.61, replaced by the .tag-pick picker below. */
.tag-x { border: none; background: rgba(20, 19, 26, 0.12); color: var(--accent-ink); border-radius: 50%; width: 16px; height: 16px; line-height: 1; cursor: pointer; font-size: 12px; }
.tag-edit { border: none; background: rgba(20, 19, 26, 0.08); color: var(--accent-ink); border-radius: 50%; width: 16px; height: 16px; line-height: 1; cursor: pointer; font-size: 10px; }

/* ===========================================================
   v21.61 — the tag picker (js/detail.js's asset-tags block)
   =========================================================== */
.tag-pick-cloud { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

.tag-pick {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line);
  font-size: 12px; font-weight: 600; font-family: var(--ui);
  padding: 4px 11px; border-radius: var(--r-pill); cursor: pointer;
}
.tag-pick:hover { border-color: var(--ink); }
.tag-pick:focus-visible { outline: none; box-shadow: var(--ring); }
.tag-pick .tag-pick-n { color: var(--mut); font-weight: 400; font-size: 11px; }

.tag-pick.on {
  background: var(--accent-soft); color: var(--accent-ink);
  border-color: transparent;
}
.tag-pick.on .tag-pick-n { color: var(--accent-ink); opacity: .6; }
.tag-pick.on::before { content: "✓"; font-size: 10px; }

.tag-pick-rule {
  flex-basis: 100%; height: 0;
  border-top: 1px solid var(--line); margin: 3px 0 1px;
}

.tag-pick-toggle {
  justify-self: start; margin-top: 9px;
  background: none; border: 1px dashed var(--line); color: var(--mut);
  font-family: var(--ui); font-size: 12px; font-weight: 600;
  padding: 5px 12px; border-radius: var(--r-pill); cursor: pointer;
}
.tag-pick-toggle:hover { border-color: var(--ink); color: var(--ink); }
.tag-pick-toggle:focus-visible { outline: none; box-shadow: var(--ring); }
.detail-tag-add { display: flex; gap: 8px; margin-top: 8px; }
.detail-tag-add input { flex: 1; }
.detail-tag-add select { flex: 1; width: 100%; }
/* v21.42 — .detail-tag-add sets `display: flex` unconditionally above, with
   no `:not([hidden])` guard. The Finish/Upscale lane's #detail-upscale-row
   reuses this class AND is toggled via the `hidden` attribute — without this
   rule the class wins and `hidden` silently does nothing (the exact trap
   CLAUDE.md documents four prior occurrences of; tools/audit-a11y.py caught
   this as the fifth before it shipped). Scoped to the CLASS, not the one id
   — .detail-tag-add's other two uses (tag-add, collection-add) never carry
   `hidden` today, so this is purely additive protection, and it's the exact
   fix tools/audit-a11y.py itself names. */
.detail-tag-add[hidden] { display: none !important; }

/* v21.44 — the Finish/Upscale lane's OWN inline status region: progress/
   error/success in place, beside the Upscale controls, while the lightbox
   stays open (toasts draw above the lightbox since v22.7b, so this is
   placement, not visibility) — not a second visual language: the
   spinner is `.wr-gen-spark` reused verbatim (just resized here); the error
   colour is the same `var(--red)` `.wr-read-hero`/`.toast-error` already use
   for "something's wrong" (text AND border, since a border tint alone would
   be easy to miss); success reuses `var(--accent)` — already this app's
   general interactive-accent token (focus rings, etc.), not a new "success
   green" invented for this one region — as a border tint only, text stays
   plain ink, matching how a plain toast (no red) already IS what "it
   worked" looks like everywhere else in this app. Gets its own `[hidden]`
   partner from the start, unlike .detail-tag-add above, which only got one
   after tools/audit-a11y.py caught the gap live. */
.detail-upscale-status {
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: 13px; line-height: 1.5; color: var(--ink);
}
.detail-upscale-status[hidden] { display: none; }
/* The same spinner showStudioProgress() shows (index.html's #studio-progress
   uses this exact class), sized down for this compact inline row instead of
   the Studio's page-centred 460px card — same shape/animation, not a second
   spinner style. */
.detail-upscale-status .wr-gen-spark {
  width: 26px; height: 26px; margin: 0; font-size: 13px; flex: none; box-shadow: none;
}
.detail-upscale-status-error { border-color: var(--red); color: var(--red); }
.detail-upscale-status-success { border-color: var(--accent); }

/* ===========================================================
   v21.65 — Crop tool
   -----------------------------------------------------------
   .detail-image needs a positioning context: the crop overlay below is
   sized/positioned by JS (computeCropGeometry(), js/detail.js) to exactly
   match #detail-img's rendered box every time crop mode opens — never a
   fixed CSS inset, since the stage centres the image via flex and that box
   changes with every image's own aspect ratio.

   The veil + selection outline + handles + size readout all live ON TOP OF
   the always-dark --image-stage background, not the app's light/dark theme
   surfaces — so their colours are hardcoded white, not tokens, per the ask.
   =========================================================== */
.detail-image { position: relative; }

/* v21.93 — the manual turn button, riding on the image stage rather than in
   .detail-actions (v21.91 measured that row to fit exactly four buttons).
   ★ BOTTOM-RIGHT, never top-left: Pinterest's extension badges an image's
   top-left corner, which is the standing rule here.
   It sets no `display`, so it needs no [hidden] partner rule — confirmed
   against tools/audit-a11y.py, not assumed. */
.detail-rotate {
  position: absolute; right: 12px; bottom: 12px; z-index: 3;
  width: 36px; height: 36px; border-radius: 50%; border: none;
  background: rgba(255,255,255,.92); color: var(--ink);
  font-size: 17px; line-height: 1; cursor: pointer;
  box-shadow: 0 4px 12px rgba(0,0,0,.25);
}
.detail-rotate:hover:not(:disabled) { background: #fff; }
.detail-rotate:disabled { opacity: .5; cursor: default; }

/* v21.95 — "read this again", sitting immediately left of the turn button
   (36px wide + 12px inset + 8px gap = 56px). Same treatment, deliberately:
   they are a pair of stage tools, not two unrelated controls.
   ★ It sets no `display`, so the `hidden` attribute works unaided and no
   [hidden] partner rule is needed — confirmed with tools/audit-a11y.py. */
.detail-rescan {
  position: absolute; right: 56px; bottom: 12px; z-index: 3;
  width: 36px; height: 36px; border-radius: 50%; border: none;
  background: rgba(255,255,255,.92); color: var(--ink);
  font-size: 16px; line-height: 1; cursor: pointer;
  box-shadow: 0 4px 12px rgba(0,0,0,.25);
}
.detail-rescan:hover:not(:disabled) { background: #fff; }
.detail-rescan:disabled { opacity: .5; cursor: default; }

/* v21.96 — the Build Sheet bar in the asset lightbox: a full-width door
   under the actions row, the shape the Workroom's own disclosure bars use.
   ★ v21.98 — it is a GRID now (icon · text · arrow), and the border is
   SOLID. Dashed-on-panel-2 read as an empty-state placeholder rather than
   something you could press, which is part of why it went unused. */
.bs-bar {
  width: 100%; text-align: left; margin-top: 12px;
  padding: 12px 14px; cursor: pointer;
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-md);
  font: inherit;
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: 12px;
}
/* ★★ LOAD-BEARING. `display: grid` above beats the `hidden` attribute, so
   without this the bar would show on every asset — the exact trap CLAUDE.md
   documents. It was already required before v21.98 (the child rules below
   set `display`, which tools/audit-a11y.py rightly flags); the grid makes
   it indispensable. Do not remove it. */
.bs-bar[hidden] { display: none !important; }
.bs-bar:hover { border-color: var(--ink); background: var(--panel); }
.bs-bar-icon { font-size: 18px; line-height: 1; }
/* The arrow says "this goes somewhere", and leans on hover. */
.bs-bar-go { font-size: 16px; color: var(--mut); transition: transform .12s ease; }
.bs-bar:hover .bs-bar-go { transform: translateX(3px); color: var(--ink); }
.bs-bar .bs-bar-k {
  display: block;
  font-family: var(--mono); font-size: 9px; letter-spacing: .11em;
  text-transform: uppercase; color: var(--mut);
}
.bs-bar .bs-bar-s { display: block; font-size: 12px; margin-top: 3px; color: var(--ink); }
/* An existing sheet gets a live accent on the kicker — it is a thing that
   exists now, not an invitation to make one.
   v21.98: `.bs-bar.has { border-style: solid }` was deleted here, not
   forgotten. Every bar is solid now, so it set the value it already had. */
.bs-bar.has .bs-bar-k { color: var(--accent-ink); }

.detail-crop-overlay { position: absolute; touch-action: none; }
.detail-crop-overlay[hidden] { display: none !important; }

/* The veil: a huge box-shadow spread on the selection itself darkens
   everything OUTSIDE it, so no separate 4-panel veil element is needed. */
.detail-crop-select {
  position: absolute;
  box-shadow: 0 0 0 9999px rgba(12, 8, 18, 0.6);
  border: 1px solid #fff;
  cursor: move;
  touch-action: none;
}
.detail-crop-guide { position: absolute; background: rgba(255, 255, 255, 0.35); }
.detail-crop-guide-v1, .detail-crop-guide-v2 { top: 0; bottom: 0; width: 1px; }
.detail-crop-guide-v1 { left: 33.333%; }
.detail-crop-guide-v2 { left: 66.666%; }
.detail-crop-guide-h1, .detail-crop-guide-h2 { left: 0; right: 0; height: 1px; }
.detail-crop-guide-h1 { top: 33.333%; }
.detail-crop-guide-h2 { top: 66.666%; }

/* ~13px, square, 2px radius, half overhanging the edge — per the ask. */
.detail-crop-handle {
  position: absolute; width: 13px; height: 13px;
  background: #fff; border-radius: 2px; touch-action: none;
}
.detail-crop-handle[data-h="nw"] { top: -7px; left: -7px; cursor: nwse-resize; }
.detail-crop-handle[data-h="ne"] { top: -7px; right: -7px; cursor: nesw-resize; }
.detail-crop-handle[data-h="sw"] { bottom: -7px; left: -7px; cursor: nesw-resize; }
.detail-crop-handle[data-h="se"] { bottom: -7px; right: -7px; cursor: nwse-resize; }

.detail-crop-size {
  position: absolute; bottom: 6px; right: 6px; pointer-events: none;
  font-family: var(--mono); font-size: 10px; color: #fff;
  background: rgba(12, 8, 18, 0.72); padding: 3px 7px; border-radius: var(--r-pill);
  white-space: nowrap;
}

/* The side-panel swap. #detail-side-fields carries .detail-side's own
   former `display:grid; gap:4px; align-content:start` (line ~3540) so
   every row inside it lays out exactly as before — .detail-side itself now
   only ever has these two direct children, and only one is visible at a
   time. Needs its own [hidden] partner for the same reason every other
   class-plus-hidden element in this section already does. */
#detail-side-fields { display: grid; gap: 4px; align-content: start; }
#detail-side-fields[hidden] { display: none !important; }

.detail-crop-panel { display: flex; flex-direction: column; gap: 10px; }
.detail-crop-panel[hidden] { display: none !important; }
.detail-crop-panel-head { font-family: var(--font-display); font-weight: 700; font-size: 18px; color: var(--ink); }
.detail-crop-ratios { margin: 0; }

/* Reuses .detail-upscale-status's own shape verbatim (same reason it exists
   — a toast fires behind the open lightbox) rather than a second status
   language for a second flow. */
.detail-crop-status {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: 13px; line-height: 1.5; color: var(--ink);
}
.detail-crop-status[hidden] { display: none !important; }
.detail-crop-status-error { border-color: var(--red); color: var(--red); }

.detail-crop-footer { display: flex; gap: 10px; margin-top: auto; padding-top: 16px; border-top: 1px solid var(--line); }
.detail-crop-footer button { flex: 1; }
.detail-crop-hint { margin: 8px 0 0; font-family: var(--mono); font-size: 10px; color: var(--mut); text-align: center; }

/* v21.65 — the revert row. Below Status, above Delete. Contrast hand-
   checked (tools/contrast-check.py only tests its own fixed pairing list):
   var(--accent-ink) #b8231f on var(--panel-2) #faf9f6 measures 6.05:1,
   comfortably over the 4.5:1 text floor — see this batch's commit message. */
.detail-crop-revert-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 14px 0 2px; padding: 10px 12px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: 12.5px; color: var(--mut);
}
.detail-crop-revert-row[hidden] { display: none !important; }
.detail-crop-revert-row p { margin: 0; }
.detail-crop-revert-btn {
  flex: none; background: none; border: none; padding: 0; cursor: pointer;
  color: var(--accent-ink); font: inherit; font-weight: 700; text-decoration: underline;
}

/* ===========================================================
   v21.22 (A3) — "also appears in" on the detail view
   -----------------------------------------------------------
   .detail-homes IS rendered with the `hidden` attribute (js/detail.js sets
   box.hidden = true when a piece has no other homes), and it DOES set
   `display` — so it gets its [hidden] partner in this same edit. That trap
   has broken this app four times: .card-form (v5), .section-head (v21.12),
   .review-details + .detail-concept (v21.12b). Check 2 of
   tools/audit-a11y.py fails without the line below.
   =========================================================== */
.detail-homes { display: block; margin: 14px 0 2px; padding-top: 12px; border-top: 1px solid var(--line); }
.detail-homes[hidden] { display: none !important; }
.detail-homes-line {
  margin: 0 0 7px; font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--mut);
}
.detail-homes-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.home-chip {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 600; color: var(--ink);
  border: 1px solid var(--line); padding: 4px 9px; border-radius: var(--r-pill);
}
/* A left edge rather than a fill, so a project and a collection are
   distinguishable without inventing two more brand colours. */
.home-chip-coll { border-left: 3px solid var(--blue-ink); }
.home-chip-proj { border-left: 3px solid var(--green-ink); }
.home-chip-flag { color: var(--orange-ink); font-weight: 900; }
.detail-homes-note { margin: 7px 0 0; font-size: 12px; line-height: 1.45; color: var(--mut); }

/* ===========================================================
   v21.22 (A5) — the duplicate warning's list, inside the shared dialog
   =========================================================== */
.dupe-list { display: flex; flex-direction: column; gap: 10px; max-height: 46vh; overflow-y: auto; margin: 4px 0 10px; }
.dupe-row { display: flex; align-items: center; gap: 10px; min-width: 0; }
.dupe-thumb { width: 44px; height: 44px; object-fit: cover; flex: none; background: var(--thumb-bg); border: 1px solid var(--line); }
.dupe-thumb-none { display: block; }
.dupe-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; font-size: 13px; line-height: 1.35; }
.dupe-text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dupe-text span { color: var(--mut); font-size: 12px; }

/* v21.12b: js/detail.js sets box.hidden = true for any piece that was not
   AI-generated, but `display: grid` won — so the "Generated concept" panel
   kept its padding, background and border on every asset in the lightbox.
   Live bug. */
.detail-concept { margin-top: 14px; padding: 14px; border-radius: var(--r-md); background: var(--accent-soft); border: 1px solid var(--line); display: grid; gap: 6px; }
.detail-concept[hidden] { display: none !important; }
.detail-concept-head { font-family: var(--font-display); font-weight: 600; font-size: 15px; color: var(--accent-ink); }
.concept-meta { font-size: 12.5px; color: var(--soft); line-height: 1.45; }
.concept-meta strong { color: var(--ink); }
.concept-prompt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 3px; }
.concept-copy {
  flex: none; border: 1px solid var(--line); background: var(--panel); color: var(--accent-ink);
  font: inherit; font-size: 11.5px; font-weight: 600; padding: 4px 10px; border-radius: var(--r-pill); cursor: pointer;
}
.concept-copy:hover { background: var(--accent-soft); border-color: var(--accent); }

.clamp-wrap { display: block; }
.clamp-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.clamp-text.expanded { -webkit-line-clamp: unset; display: block; overflow: visible; }
.clamp-toggle { background: none; border: none; color: var(--accent-ink); font-weight: 600; cursor: pointer; padding: 2px 0 0; font-size: 12px; }
.concept-sources { display: flex; gap: 6px; flex-wrap: wrap; }

/* ===========================================================
   v21.59 — four small Vault/detail fixes
   =========================================================== */

/* FIX 2: the upscale engine picker used to reveal into the same borderless
   flow every other row in this panel uses, so it read as "just another
   field" instead of "the next step". flex-wrap so .detail-upscale-cue
   (real text, not ::before content) can force itself onto its own line
   above the select+Go row — scoped to the id, not .detail-tag-add itself,
   since that class's other two uses (tag-add, collection-add) never wrap.
   Border contrast measured 3.92:1 on the accent-soft fill (WCAG 1.4.11
   needs 3.0 for a non-text UI boundary); the cue text 5.55:1 (needs 4.5). */
#detail-upscale-row {
  flex-wrap: wrap;
  gap: 12px;
  background: var(--accent-soft);
  border: 1.5px solid var(--red);
  border-radius: var(--r-md);
  padding: 12px 14px;
}
/* v21.60 — the select was stretching flush against Go (.detail-tag-add
   select{flex:1;width:100%} + the shared 8px .detail-tag-add gap left no
   real room), and its own native chevron sat jammed against its own right
   edge with no reserved space. flex:1 1 auto keeps it filling the row
   (same effective sizing as the shared rule, just explicit here); the
   padding gives the chevron somewhere to sit that isn't the option text. */
#detail-upscale-engine {
  flex: 1 1 auto;
  padding-right: 30px;
}
#detail-upscale-go { flex: 0 0 auto; }
.detail-upscale-cue {
  flex-basis: 100%;
  margin: 0 0 8px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
/* A one-time pulse on reveal (optional per the ask) — purely decorative, so
   the blanket prefers-reduced-motion rule elsewhere in this file already
   neutralizes it for anyone who's asked for less motion; nothing extra
   needed here. */
@keyframes detailUpscalePulse {
  0% { box-shadow: 0 0 0 0 rgba(224, 50, 47, 0.35); }
  100% { box-shadow: 0 0 0 10px rgba(224, 50, 47, 0); }
}
#detail-upscale-row.pulse-once { animation: detailUpscalePulse 0.6s ease-out; }

/* FIX 3: "Upscaled versions" + the symmetric "Upscaled from →" back-link.
   Reuses .detail-concept's card verbatim for the container (same
   accent-soft "related context" language as Generated concept, right
   above it) — only these inner pieces are new. */
.upscale-versions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; }
.upscale-version {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  width: 64px; border: none; background: none; padding: 0; cursor: pointer;
}
.upscale-version img {
  width: 64px; height: 64px; object-fit: cover; border-radius: var(--r-sm);
  box-shadow: var(--shadow-1); border: 2px solid var(--panel);
}
.upscale-version:focus-visible { outline: none; }
.upscale-version:focus-visible img { outline: 2px solid var(--accent); outline-offset: 2px; }
.upscale-version-label {
  font-size: 10px; font-weight: 600; color: var(--accent-ink);
  text-align: center; line-height: 1.25;
}
.upscale-back-link {
  display: inline-flex; align-items: center; margin-top: 10px;
  border: none; background: none; padding: 0;
  color: var(--accent-ink); font-size: 12.5px; font-weight: 600;
  cursor: pointer; text-decoration: underline;
}
.upscale-back-link:hover { color: var(--red); }

/* v21.60: the v21.59 "Keep" chip is gone — Josh wants the space back
   entirely, not a chip standing in its place. #detail-status-row (a plain
   <div>, no class) and #detail-status (a plain <select>) both need no
   [hidden] partner rule: neither carries a class that sets an unconditional
   `display`, so the native `hidden` behavior applies untouched — the
   standing trap this codebase keeps hitting only bites when a class
   already forces a `display` onto the element. .badge[hidden] (added in
   v21.59 only for the now-removed chip — confirmed no other .badge element
   anywhere in this app is ever rendered with `hidden`) was removed with it,
   above; .badge itself is unchanged. */

/* v22.1 — the "add a moodboard" row above the linked assets. Flex, not grid,
   so the audit's grid-item shrink rule does not apply to it. */
.pdetail-add-board { display: flex; gap: 8px; align-items: center; margin: 0 0 12px; }
.pdetail-assets { display: flex; flex-wrap: wrap; gap: 8px; }
.pdetail-asset { border: none; background: none; padding: 0; cursor: pointer; }
.pdetail-asset img, .pdetail-asset .thumb { width: 56px; height: 56px; object-fit: cover; border-radius: var(--r-sm); box-shadow: var(--shadow-1); }

/* ===========================================================
   Your machines (production engine) — Settings screen
   =========================================================== */
.machines-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 14px; }

.machine-card {
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--panel);
  padding: 14px 16px;
  box-shadow: var(--shadow-1);
}
.machine-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.machine-card-actions { display: flex; gap: 12px; }
.machine-title { font-weight: 700; font-size: 16px; color: var(--ink); }
.machine-line { color: var(--soft); font-size: 13px; margin-top: 3px; }
.machine-notes { font-style: italic; }

.machine-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.machine-chip {
  font-size: 11px; font-weight: 600; color: var(--accent-ink);
  background: var(--accent-soft);
  border-radius: var(--r-pill); padding: 3px 10px;
}

.link-btn {
  background: none; border: none; padding: 0; cursor: pointer;
  font-size: 13px; font-weight: 600; color: var(--accent-ink);
}
.link-btn:hover { text-decoration: underline; }
.link-btn.danger { color: var(--red); }

.machines-empty { color: var(--soft); }
.machines-empty p { margin: 0 0 10px; }

.machine-form {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px;
  border: 1px dashed var(--line); border-radius: var(--r-md);
  padding: 16px; margin: 4px 0 14px;
  background: var(--panel-2);
}
.machine-field { display: flex; flex-direction: column; gap: 4px; }
.machine-field-wide { grid-column: 1 / -1; }
.machine-field label { font-size: 12px; font-weight: 700; color: var(--soft); }
.machine-form .button-row { grid-column: 1 / -1; margin-bottom: 0; }

.machine-caps { display: flex; flex-wrap: wrap; gap: 6px 16px; }
.machine-cap { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink); }

/* ===========================================================
   Build Sheet — the entry control inside the asset detail view.
   (The spacious editor lives in the Pass 2 ".bsx-*" / ".pw-*" styles
   near the end of this file. The old in-panel popup styles were
   removed once breakdown.js started opening the full editor.)
   =========================================================== */
.detail-buildsheet {
  margin-bottom: 10px; padding-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.bs-head { font-family: var(--font-display); font-weight: 600; font-size: 16px; color: var(--ink); margin-bottom: 4px; }
.bs-controls { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 4px; }

/* v21.8: the `.engine-lane*` boxes are retired. The engine picker is now the
   seg + chip pair styled in the Design Studio section above. */

/* v21.8: the generated-concepts pop-up is retired. Concepts render on the
   Studio canvas instead, so there is no modal to style (and the modal's
   sub-line, which still promised "Keep, Maybe, or Reject", is gone with it —
   the cards have offered Keep / Delete since v15). */

/* ===========================================================
   Bulk import — progress overlay + review pop-up
   =========================================================== */
.import-progress-overlay {
  position: fixed; inset: 0; z-index: 10070; display: none;
  align-items: center; justify-content: center;
  background: rgba(12, 8, 18, 0.6);
}
.import-progress-overlay.open { display: flex; }
.import-progress-box {
  background: var(--panel); border-radius: var(--r-lg); padding: 28px 36px;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  box-shadow: var(--shadow-2);
}
.import-spinner {
  width: 34px; height: 34px; border-radius: 50%;
  border: 3px solid var(--line); border-top-color: var(--accent);
  animation: import-spin 0.8s linear infinite;
}
@keyframes import-spin { to { transform: rotate(360deg); } }
.import-progress-text { font-weight: 600; color: var(--ink); font-size: 14px; }

/* v16: floating generation-progress card — larger, centered on the page
   (was bottom-center in v15), visible at any scroll depth. */
.studio-progress {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 10050; pointer-events: none;
}
/* v21.9.2: the card itself is `.wr-generating` (the shared "AI is working"
   card). All this class does now is size it — the old flex row, spinner and
   two grey lines are gone. */
.studio-progress-card { width: min(460px, calc(100vw - 40px)); box-shadow: var(--sh-3); }

.import-review-overlay {
  position: fixed; inset: 0; z-index: 10060; display: none;
  align-items: flex-start; justify-content: center;
  background: rgba(12, 8, 18, 0.6); padding: 30px 20px; overflow: auto;
}
.import-review-overlay.open { display: flex; }
.import-review-modal {
  position: relative; background: var(--panel); margin: auto;
  width: min(840px, 96vw); max-height: 90vh; overflow: auto;
  border-radius: var(--r-xl); padding: 28px 30px;
  box-shadow: var(--shadow-2);
}
.import-review-close {
  position: absolute; top: 14px; right: 18px; z-index: 3;
  background: none; border: none; cursor: pointer;
  font-size: 26px; line-height: 1; color: var(--soft);
}
.import-review-close:hover { color: var(--ink); }
.import-review-head { font-family: var(--font-display); font-weight: 600; font-size: 22px; color: var(--ink); padding-right: 28px; }

.import-review-controls { display: flex; flex-wrap: wrap; gap: 14px; margin: 12px 0; }
.import-review-controls label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 600; color: var(--soft); }
.import-review-controls select { min-width: 200px; }
.import-review-tagrow { display: flex; gap: 8px; margin-bottom: 16px; }
.import-review-tagrow .search-box { margin-bottom: 0; }

.import-review-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px; margin-bottom: 18px;
}
.import-review-thumb {
  aspect-ratio: 1 / 1; border-radius: var(--r-sm); overflow: hidden;
  background: var(--thumb-bg); border: 1px solid var(--line);
}
.import-review-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.import-review-noimg { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 10px; color: var(--mut); }

/* ===========================================================
   v21.62 — Wardrobe intake overlay (js/wardrobe-intake.js)
   Mirrors .import-review-overlay's geometry immediately above (same
   fixed/flex/backdrop shape, same modal treatment) so the two read as one
   app. Every button/input here already gets a focus ring for free from
   the universal button/input/select:focus-visible rule further down this
   file — nothing new needed per class for that.
   =========================================================== */
.wardrobe-overlay {
  position: fixed; inset: 0; z-index: 10065; display: none;
  align-items: flex-start; justify-content: center;
  background: rgba(12, 8, 18, 0.6); padding: 30px 20px; overflow: auto;
}
.wardrobe-overlay.open { display: flex; }
.wardrobe-modal {
  position: relative; background: var(--panel); margin: auto;
  width: min(760px, 96vw); max-height: 90vh; overflow: auto;
  border-radius: var(--r-xl); padding: 28px 30px;
  box-shadow: var(--shadow-2);
}
.wardrobe-close {
  position: absolute; top: 14px; right: 18px; z-index: 3;
  background: none; border: none; cursor: pointer;
  font-size: 26px; line-height: 1; color: var(--soft);
}
.wardrobe-close:hover { color: var(--ink); }

.wardrobe-head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; padding-right: 28px; margin-bottom: 16px; }
.wardrobe-title { font-family: var(--disp); font-weight: 800; font-size: 30px; line-height: 1.05; text-transform: uppercase; color: var(--ink); }
.wardrobe-prog { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut); white-space: nowrap; }

/* The right column needs its own min-width:0 — a long chip row (e.g. every
   category on one line before wrapping starts) can otherwise push this
   grid wider than the modal. Registered in the "Shrink safety" block
   below, same as every other grid container in this file — tools/audit-a11y.py's
   check 3 scans for grid-template-columns and expects exactly that. */
.wardrobe-body { display: grid; grid-template-columns: 200px 1fr; gap: 20px; }
/* v21.63 — wraps .wardrobe-photo + the filename caption below it, so
   .wardrobe-body's 200px column stays ONE grid item (still just the two
   columns the Shrink-safety comment above describes) rather than gaining a
   third that would need its own track. */
.wardrobe-photo-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.wardrobe-photo {
  aspect-ratio: 3 / 4; background: var(--panel-2);
  border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.wardrobe-photo img { width: 100%; height: 100%; object-fit: contain; }
/* v21.63 — the filename, so two similar garment photos in one batch are
   tellable apart. Ellipsizes rather than widening the 200px column it
   lives in — min-width:0 above is what lets that ellipsis actually engage. */
.wardrobe-filename {
  font-family: var(--mono); font-size: 10px; color: var(--mut);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.wardrobe-fields { display: flex; flex-direction: column; gap: 4px; }
.wardrobe-fields label { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--mut); }
.wardrobe-mt { margin-top: 14px; }
.wardrobe-fields input[type="text"] {
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 10px 12px; background: var(--panel-2); font: inherit; color: var(--ink);
}
.wardrobe-fields input[type="text"]:focus-visible { border-color: var(--accent); }
.wardrobe-fields .hint { margin: 6px 0 0; }

.wardrobe-strip { display: flex; gap: 6px; overflow-x: auto; margin-top: 18px; padding-bottom: 4px; }
.wardrobe-thumb {
  flex: 0 0 auto; width: 34px; height: 44px; padding: 0; cursor: pointer;
  border: 2px solid var(--line); border-radius: var(--r-xs); overflow: hidden;
  background: var(--thumb-bg);
}
.wardrobe-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wardrobe-thumb.done { border-color: var(--green); }
.wardrobe-thumb.current { border-color: var(--red); }

.wardrobe-foot { display: flex; align-items: center; gap: 12px; margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.wardrobe-spacer { flex: 1; }
.wardrobe-skip {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--mut); font: inherit; font-size: 13px; text-decoration: underline;
}
.wardrobe-skip:hover { color: var(--ink); }

/* ===========================================================
   Mobile top bar + drawer (additive — shows only on small screens)
   =========================================================== */
.atelier-topbar {
  display: none;
  align-items: center; gap: 12px;
  position: sticky; top: var(--rainbow-h); z-index: 45;
  padding: 13px 18px;
  background: var(--panel); border-bottom: 1px solid var(--line);
}
.topbar-brand { font-family: var(--disp); font-weight: 800; font-size: 19px; color: var(--ink); }
.topbar-brand em { font-style: normal; color: var(--red); }
.nav-backdrop {
  display: none; position: fixed; inset: 0; z-index: 39;
  background: rgba(20, 19, 26, 0.5);
}

/* ---------- Bottom tab bar — phones only (v21.4) ----------
   Hidden by default; the media query below turns it on at <=820px so iPad
   portrait (834pt) keeps the sidebar rail, exactly as the mockups show. */
.tabbar { display: none; }

.tabbar-btn {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: 48px;                 /* comfortable touch target */
  padding: 6px 2px;
  background: none; border: none; cursor: pointer;
  color: var(--mut);
}
/* Inline SVG, not text glyphs — iOS turns several geometric characters into
   colour emoji, which would break the monochrome tab bar. */
.tabbar-icon { width: 19px; height: 19px; display: block; fill: currentColor; }
.tabbar-label {
  font-family: var(--mono); font-size: 9px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
}
/* app.js adds .active to whichever .nav-button matches the open screen, so the
   tab lights up for free — no extra navigation JS.
   v22.8a — the red + is excluded. It is a nav-button for the Studio too, so
   on the Studio screen this rule (0,3,0) out-ranked the + button's own
   rules and its `background: none` stripped the red circle, leaving a red +
   on the white bar — true since v21.4, noticed once v22.8 put a Studio tab
   beside it. The + keeps its own darker-red active state below. */
.tabbar .nav-button.active:not(.tabbar-fab) { background: none; color: var(--red); }
.tabbar .nav-button::before { display: none; }   /* no square dot down here */

.tabbar-fab {
  flex: none;
  width: 56px; height: 56px; margin: 0 4px;
  display: flex; align-items: center; justify-content: center;
  background: var(--red); color: #fff;
  border: none; border-radius: 50%;
  box-shadow: 0 6px 16px rgba(224, 50, 47, 0.4);
  cursor: pointer;
}
.tabbar-fab svg { width: 26px; height: 26px; fill: currentColor; }
/* v22.8a — the same out-ranking on HOVER: the sidebar's .nav-button:hover
   (0,2,0) painted the + near-white (#f4f0f0) behind its white glyph. On an
   iPhone a tapped button keeps :hover until the next tap, so tapping + to
   upload on the Vault left it pale. Declared before the active rule, so on
   the Studio screen the darker active red still wins. */
.tabbar-fab:hover { background: var(--red); }
/* v22.8a — `color: #fff` is needed here too: once the + is out of the tab
   rule above, the sidebar's .nav-button.active (0,2,0) out-ranks the +
   button's own white (0,1,0) and turned the glyph dark red on dark red. */
.tabbar-fab.active { background: #c72a27; color: #fff; }

/* ===========================================================
   Shrink safety — why `1fr` was not sharing the space (v21.13)
   -----------------------------------------------------------
   Found by the v21.13 device-QA sweep: horizontal scroll on Home and the
   Studio at 390/430, and on the four typed-catalog forms at 834.

   All of it was ONE cause. A grid item defaults to `min-width: auto`, which
   means it can never be narrower than its own min-content. So:

     - `.recent-name` and `.studio-pick-label` set `white-space: nowrap` with
       `text-overflow: ellipsis`. The ellipsis was never reached: nowrap makes
       min-content the FULL width of the title, that became the track's floor,
       and `1fr` stopped meaning "share the space". Home's 3-up strip resolved
       to `350px 350px 283px` inside a 362px column — 629px of horizontal
       scroll on a phone.
     - An `<input>` has an intrinsic width (~20 characters) and no `width:100%`
       anywhere in this stylesheet, so `.card-form`'s `1fr 1fr` resolved to
       `353px 212px` inside a 494px form at 834.

   Two rules fix every case, and prevent the next one:

   1. Grid items may shrink. The list is every container in this file that
      declares `grid-template-columns` — regenerate it with
      `python3 tools/audit-a11y.py`, which fails if a new grid is missing here.
   2. Long words may break. Titles are real filenames — a piece uploaded as
      gold_lame_trim_ROLL_2026_batch_07_final_FINAL_v3.jpg has no space to
      break on, and underscores are not break opportunities. Without this,
      rule 1 alone let 429px of text spill out of a 143px card on Outfits and
      Materials. `anywhere` (not `break-word`) is required: only `anywhere`
      reduces the intrinsic min-content size, which is the thing at fault.
   =========================================================== */
.ask-pics > *,
.card-form > *,
.detail-shell > *,
.detail-shell-asset > *,
.detail-shell-project > *,
.detail-side.tight #detail-meta > *,
.home-cards > *,
.home-stats > *,
.import-review-grid > *,
.item-grid > *,
.machine-form > *,
.mg-grid > *,
.pf-row2 > *,
.prompt-grid > *,
.prompt-picker-list > *,
.ps-bench > *,
.ps-layout > *,
.recent-strip > *,
.shop-make > *,
.shop-piece > *,
.skeleton-grid > *,
.subject-row > *,
.tryon-pick > *,
.tryon-result > *,
.studio-layout > *,
.studio-picker > *,
.studio-results > *,
.studio-results.one > *,
.studio-results.one .review-body > *,
.vault-gallery > *,
.vault-gallery.compact > *,
.vault-gallery.sheet > *,
.vault-layout > *,
.vault-picker-grid > *,
/* v22.5 — both spellings of the bulk bar's select grid. Not cosmetic: the
   docked bar is 268px wide and these are <select>s whose longest option
   ("Add to collection…", or a long collection name) sets a min-content
   width larger than that. Without this the grid item refuses to shrink and
   the bar either overflows or is pushed wider than the rail it is docked
   over. The base .vault-sel-grid was already flagged by the audit before
   this batch; the scoped one is new in v22.5 and needed the same line. */
.vault-sel-grid > *,
.vault-selection .vault-sel-grid > *,
.wardrobe-body > *,
/* v22.21 — the Wardrobe's header, its numbers, and a pair of small rails. */
.wd-mast > *,
.wd-mast-stats > *,
.wd-pair > *,
/* v22.22 — the add-a-type box's 16 icons. */
.ask-icons > *,
/* v22.25 — Workroom Mode's look | step columns (tablet and computer). */
.wm-split > *,
/* v22.26 — the before → after pair (the look, the arrow, what was made). */
.fp-ba > *,
.wr-grid > *,
.wr-plist > *,
.wr-row2 > * { min-width: 0; }

/* An input with no width fills its field instead of setting the field's floor.
   Checkboxes and radios are excluded: they are fixed-size controls, and the
   first version of this rule stretched #setting-sound into a 155px-wide
   checkbox. Caught by the tap-target pass in the same sweep. */
input:not([type="checkbox"]):not([type="radio"]),
select,
textarea { min-width: 0; width: 100%; }
input[type="checkbox"], input[type="radio"] { min-width: 0; }

body { overflow-wrap: anywhere; }

/* ===========================================================
   Button resets for the v21.14 keyboard conversions
   -----------------------------------------------------------
   Five clickable areas were <div>/<span>/<img> with click listeners, so Tab
   skipped all of them. They are real <button type="button"> now, and a button
   brings its own font, colour, background, border, padding and centred text.
   These rules hand all of that back so the pixels do not move.

   NOTE ON `display` — deliberately absent. Setting `display` on a class that
   might carry the `hidden` attribute is the trap that has broken this app
   three times (.card-form v5, .section-head v21.12, .review-details +
   .detail-concept v21.12b). It is not needed here:
     - .studio-pick and .vault-pick are grid items, and a grid container
       blockifies its children's display automatically;
     - .crumb wants inline-block, which is a button's default;
     - .concept-src is a flex item of .concept-sources, also blockified.
   If you ever DO need `display` on one of these, add the matching
   `[hidden] { display: none !important; }` in the same edit. Check 2 of
   tools/audit-a11y.py fails if you forget.
   =========================================================== */
.studio-pick,
.vault-pick,
.crumb,
.concept-src {
  margin: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: inherit;
  -webkit-appearance: none;
  appearance: none;
  /* The UA stylesheet gives FORM CONTROLS their own reset for these five, and
     none of them are part of the `font` shorthand — so `font: inherit` does
     not bring them back. Measured: without this the breadcrumb lost
     `text-transform: uppercase` and `letter-spacing: 2.6px`, shrinking "All
     sparkle" from 116.1px to 87.5px and shifting the separator with it. */
  text-transform: inherit;
  letter-spacing: inherit;
  word-spacing: inherit;
  text-indent: inherit;
  text-shadow: inherit;
}

/* .studio-pick already declared `background: none; border: none`. It fills its
   grid track the way the old <div> did. */
.studio-pick { width: 100%; }

/* .vault-pick already declared its own border and background; only the width
   needs restating, because a button does not stretch on its own. */
.vault-pick { width: 100%; }

/* The breadcrumb is a line of running text. A button is inline-block, which
   sits on the baseline like the <span> did — but it needs the underline
   behaviour and the inherited 13px/500/--soft from .vault-breadcrumb. */
.vault-breadcrumb .crumb {
  background: none;
  border: 0;
  cursor: pointer;
  vertical-align: baseline;
}

/* .concept-src moved from the <img> to the <button> that wraps it. The button
   carries the 44x44 frame; the image fills it. */
.concept-src {
  width: 44px; height: 44px;
  border-radius: 8px;
  cursor: pointer;
  border: 2px solid var(--panel);
  box-shadow: var(--shadow-1);
  background: none;          /* the <img> it replaced had none */
  overflow: hidden;
  flex: none;
}
.concept-src img { width: 100%; height: 100%; object-fit: cover; }
/* v22.18 — a source that was an OUTSIDE picture (uploaded straight into the
   Design Studio, never kept): a plain labelled tile the height of the
   others. A <span>, not a button: there is nothing to open. */
.concept-src-out {
  display: inline-flex; align-items: center; height: 44px; padding: 0 10px; flex: none;
  border: 1px dashed var(--line); border-radius: 8px; background: var(--panel);
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--mut);
}

/* Requirement (e): 44px minimum on touch. The ≤500px block already sets
   min-height on every button; these also need a min-WIDTH, and the breadcrumb
   needs the height honoured without stretching the line box. */
@media (max-width: 500px) {
  .studio-pick, .vault-pick, .crumb, .concept-src { min-width: 44px; min-height: 44px; }
  /* A 44px-tall inline-block would otherwise sit on the baseline and drop its
     text to the bottom of the line box. Centring the crumb and its separator
     keeps the breadcrumb reading as one line. Done WITHOUT touching
     .vault-breadcrumb's `display` — see the note at the top of this block. */
  .vault-breadcrumb .crumb { vertical-align: middle; }
  .vault-breadcrumb .sep { vertical-align: middle; }
}

/* Focus rings. The old divs could not be focused, so none of these had one. */
.studio-pick:focus-visible,
.vault-pick:focus-visible,
.crumb:focus-visible,
.concept-src:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ===========================================================
   Touch targets — 44px on phones (v21.13)
   -----------------------------------------------------------
   The v21.13 sweep measured every interactive element at 390px: 43 of them
   were under 44px. Almost all were near-misses — the nav rows, the primary
   buttons and Generate were all 43px, one pixel short — so this is mostly a
   floor, not a redesign. Chips and segmented buttons (26-35px) do get
   visibly taller; on a phone that is the point.

   Measured after: 43 under-size targets -> 3, with no new horizontal
   overflow at any of 390/430/834/1024/1440.

   The 4 left are deliberate, and are Josh's call on the device:
     - .vault-check (24x24) is a native checkbox with accent-color. Sized to
       44 the tick itself renders huge. 24x24 meets WCAG 2.2 AA (2.5.8).
     - the inline "Supabase CLI" link in Settings prose (84x18) — an inline
       link inside a sentence cannot be 44px tall without breaking the line.
     - .studio-ref-remove (22x22, v22.8) — the Studio reference x, inside its
       52px thumbnail; at 44 tall it became a bar down most of the picture.
       Meets 2.5.8 by the spacing exception.
   (v21.13 counted 3 but named only the first two above; the third was
   never identified here.)
   =========================================================== */
@media (max-width: 500px) {
  button, select, .chip, .seg-btn { min-height: 44px; }
  .vault-heart, .vault-dl, .pcard-more, .pcard-fav { min-width: 44px; min-height: 44px; }
  input[type="checkbox"], input[type="radio"] { min-width: 24px; min-height: 24px; }
}

/* ===========================================================
   Responsive — a real pass, not a squeezed desktop view
   =========================================================== */

/* Tablet / small laptop: tighten gutters, narrow rails. */
@media (max-width: 1100px) {
  .content { padding: 30px 32px; }
  /* v21.8: the Studio STACKS here, not at 820 like the rest of the shell.
     Two columns need ~340 for the panel plus ~520 for the canvas, and the
     sidebar rail and gutters eat ~330 — so below ~1100 the canvas was being
     squeezed to a couple of hundred pixels (measured 199px at iPad portrait).
     Stacked, the panel and the concepts each get the full width. */
  .studio-layout { grid-template-columns: 1fr; gap: 22px; }
  .studio-panel { position: static; max-height: none; min-height: 0; overflow: visible; }
  .studio-panel-scroll { overflow: visible; }
  .studio-picker { max-height: none; overflow: visible; }
  /* Stacked, the canvas is too narrow to pair the fields up — a single result
     is just a normal card that happens to be full width. */
  .studio-results.one .review-body { grid-template-columns: minmax(0, 1fr); }

  /* v21.7: on iPad the rail beside the grid leaves ~400px (2-up), and stacking
     it vertically ate the entire first screen before you reached an image.
     Instead it becomes a horizontal, scrollable strip of folder chips — the
     grid gets the full width, and collections stay one tap away (locked
     decision 3: the rail is kept, not replaced by filter chips).
     NOTE: below 820px this is superseded by the bottom-sheet rules further
     down; a flat chip strip cannot express nesting, which is the whole point
     on a phone where Josh has 14 collections and 7 nested model folders. */
  .vault-layout { grid-template-columns: 1fr; gap: 14px; }
  .vault-sidebar {
    position: static; border-right: none; border-bottom: 1px solid var(--line);
    padding: 0 0 10px;
    display: flex; gap: 6px; align-items: center;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .vault-sidebar::-webkit-scrollbar { display: none; }
  .vault-sidebar > * { flex: none; }
  /* Section headings become inline dividers rather than stacked labels. */
  .vault-folders-head { padding: 0 6px 0 2px; }
  .vault-folders-head:not(:first-child) { border-left: 1px solid var(--line); padding-left: 12px; margin-left: 6px; }
  .vault-folder {
    /* JS writes an inline padding-left for nesting depth; override it so chips
       do not inherit a deep indent when laid out horizontally. */
    padding-left: 12px !important;
    padding-right: 12px;
    border: 1px solid var(--line); border-radius: var(--r-xs);
    white-space: nowrap;
  }
  .vault-folder.active { border-color: var(--red); }
  .vault-folder-menu-btn { display: none; }
  .vault-folder-new { width: auto; margin: 0; white-space: nowrap; }
}

/* ★ v22.5a — THE iPAD FOLDER STRIP NEVER ACTUALLY BECAME A STRIP.
   Between 821px and 1100px the rail rendered as a ~630px-tall empty band
   above ALL SPARKLE. The v21.7 rule above makes .vault-sidebar a flex row
   and assumes the folder ROWS are its children — they are not, and have not
   been since June: js/vault.js:252 wraps every row in a single
   <div class="vault-folders">, which has no CSS rule anywhere in this file
   (verified: `grep -c '\.vault-folders[ {]'` is 0; only the -head and
   body.vault-folders-open spellings exist). So the strip held exactly two
   flex items — the upload button and one tall block — and
   `.vault-sidebar > * { flex: none }` kept that block at its 230px
   intrinsic width, pushing it past the strip's right edge and out of sight
   in the strip's own overflow. THIS PREDATES v22.5; the dock change only
   made it easy to notice.

   The fix moves the row layout one level down, onto the element that
   actually holds the rows.

   ★ Scoped to EXACTLY 821–1100, not `max-width: 1100px`: below 821 the rail
   is a bottom sheet that needs .vault-folders to stay a stacked block —
   nesting is the entire point there — and a plain max-width would reach in
   and flatten it.

   ★ OVERRIDE, NOT MOVE, and deliberately. `overflow-x: auto` on
   .vault-sidebar is genuinely dead once the scrolling lives on
   .vault-folders, so moving it would be tidier — but it travels with
   `scrollbar-width: none`, `-webkit-overflow-scrolling` and the
   `::-webkit-scrollbar` rule, and the ≤820 bottom sheet sets NO scrollbar
   hiding of its own (checked): it scrolls vertically on `max-height: 76vh;
   overflow-y: auto` and relies on those three inherited from the block
   above. Moving the group would put a visible scrollbar on the phone sheet.
   One `overflow-x: visible` here costs nothing and breaks nothing.
   `.vault-sidebar > * { flex: none }` also STAYS — #vault-new-piece still
   needs it; only .vault-folders opts out, at higher specificity. */
@media (min-width: 821px) and (max-width: 1100px) {
  .vault-sidebar { overflow-x: visible; }
  /* #vault-new-piece is `width: 100%` (css ~1621), which in a flex row
     resolves against the whole strip — 700px of it at a 1000px viewport. */
  .vault-sidebar > #vault-new-piece { width: auto; }
  .vault-sidebar > .vault-folders {
    display: flex; gap: 6px; align-items: center;
    flex: 1 1 auto; min-width: 0;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .vault-sidebar > .vault-folders::-webkit-scrollbar { display: none; }
  .vault-sidebar > .vault-folders > * { flex: none; }
  /* v22.22a — the ⋯ is back here, as on a desktop (it shows on hover and on
     the chosen chip), so a collection or a type can be renamed or removed
     at these widths. Its menu floats on the screen under it
     (js/vault.js placeFolderMenu): the strip would clip a dropped menu. */
  .vault-folder-menu-btn { display: block; }
  .vault-folder-menu.is-floating { position: fixed; right: auto; z-index: 60; }
}
/* A touch screen at these widths has no hover to reveal the ⋯, so it
   always shows there. */
@media (min-width: 821px) and (max-width: 1100px) and (hover: none) {
  .vault-folder-menu-btn { opacity: 1; }
}

/* ===========================================================
   Vault folders on a PHONE — a bottom sheet (v21.7.5)

   The horizontal chip strip broke down here: 9 type views + 14 collections is
   a long blind sideways scroll, and a flat strip cannot express nesting at all
   — so the seven folders inside "Models" were simply unreachable.

   Instead the toolbar carries ONE row saying where you are, and tapping it
   raises the full tree from the bottom of the screen: thumb-reachable (the
   same edge as the tab bar), scrollable, expandable, dismissed by tapping a
   folder, the backdrop, or Escape.

   The sheet IS #vault-sidebar — the same element, same markup, same handlers,
   re-laid-out by CSS. No duplicate DOM, nothing to keep in sync.
   =========================================================== */
.vault-folder-btn { display: none; }
.vault-sheet-backdrop { display: none; }

@media (max-width: 820px) {
  .vault-folder-btn {
    display: flex; align-items: center; gap: 10px; width: 100%;
    margin-bottom: 12px; padding: 13px 14px;
    background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xs);
    cursor: pointer; font: inherit; color: var(--ink);
  }
  .vault-folder-btn-icon { color: var(--red); font-size: 14px; line-height: 1; }
  .vault-folder-btn-label {
    flex: 1; text-align: left;
    font-family: var(--disp); font-weight: 800; font-size: 17px;
    text-transform: uppercase; letter-spacing: -0.01em;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .vault-folder-btn-chev { color: var(--mut); font-size: 12px; }

  .vault-sheet-backdrop {
    display: block; position: fixed; inset: 0; z-index: 10080;
    background: rgba(20, 19, 26, 0.5);
    opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
  }
  body.vault-folders-open .vault-sheet-backdrop { opacity: 1; pointer-events: auto; }

  /* The rail becomes the sheet. */
  .vault-sidebar {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto;
    z-index: 10081;
    display: block;                       /* undo the chip-strip flex row */
    max-height: 76vh; overflow-y: auto; overflow-x: hidden;
    padding: 18px 16px calc(20px + env(safe-area-inset-bottom, 0px));
    background: var(--panel);
    border: none; border-top: 4px solid transparent; border-image: var(--rainbow) 1;
    box-shadow: 0 -18px 50px rgba(20, 19, 26, 0.3);
    transform: translateY(101%);
    transition: transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  body.vault-folders-open .vault-sidebar { transform: translateY(0); }

  /* Rows go back to being rows, not chips — nesting is the point here. */
  .vault-sidebar .vault-folder {
    border: none; border-radius: var(--r-xs);
    padding-right: 10px; white-space: nowrap;
    min-height: 46px;                     /* comfortable touch target */
  }
  .vault-sidebar .vault-folders-head {
    border-left: none; margin-left: 0; padding: 16px 4px 6px;
  }
  .vault-sidebar .vault-folder-menu-btn { display: block; }
  .vault-sidebar .vault-folder-new { width: 100%; margin-top: 14px; }
  .vault-sidebar > * { flex: initial; }
}
@media (prefers-reduced-motion: reduce) {
  .vault-sidebar, .vault-sheet-backdrop { transition: none; }
}

/* v21.8: the old 900px Studio breakpoint is gone — stacking now happens at
   1100 (above), where the canvas actually runs out of room. All that is left
   for phones is dropping a hover hint that does not apply to touch, and which
   was pushing "Your vault" onto two lines to say it.
   No `order` juggling any more: the panel is the FIRST child in the DOM, so it
   leads naturally once the grid is one column. */
@media (max-width: 820px) {
  .studio-canvas-note { display: none; }
}

/* Phones: bottom tab bar + the sidebar as a "More" drawer.
   v21.4: breakpoint moved 860 -> 820 so iPad portrait (834pt) keeps the
   permanent sidebar rail, as the approved iPad mockups show. */
/* Desktop never shows the composer's disclosure — the panel is simply open,
   and always has been. */
.ps-toggle { display: none; }

/* ===========================================================
   v21.88 — the Prompt Library on a phone.
   =========================================================== */
@media (max-width: 820px) {
  /* The collapsed row keeps ONLY the colour bar, the title, the star and the
     chevron. The chip, the used-count and the JSON badge are what buys the
     title its width back — all of them return when the row is opened. */
  .pcard-row .pcard-style,
  .pcard-row .pcard-meta,
  .pcard-row .pcard-json { display: none; }
  .pcard[data-open] .pcard-row { flex-wrap: wrap; padding-top: 6px; padding-bottom: 6px; }
  .pcard[data-open] .pcard-row .pcard-style,
  .pcard[data-open] .pcard-row .pcard-meta,
  .pcard[data-open] .pcard-row .pcard-json { display: inline-flex; }

  /* Search gets a row of its own; sort and the count share the next one. */
  .prompts-toolbar { flex-wrap: wrap; }
  .prompts-toolbar .search-box { flex: 1 0 100%; min-width: 0; }
  .prompts-toolbar #prompts-sort { flex: 1; }
  .prompts-toolbar #prompts-count { flex: none; }

  /* ★ THE COMPOSER STARTS CLOSED, every load, so a phone lands on the
     library rather than on the input. Class-driven — pairing `display` with
     the `hidden` attribute is the trap CLAUDE.md documents four times. */
  .ps-panel .ps-head { cursor: pointer; }
  .ps-panel .ps-toggle { display: inline-flex; }
  .ps-toggle {
    margin-left: auto; flex: none;
    font-family: var(--mono); font-size: 10px; font-weight: 700;
    letter-spacing: .08em; text-transform: uppercase;
    background: none; border: 1px solid var(--line); border-radius: var(--r-xs);
    padding: 7px 11px; cursor: pointer; color: var(--mut);
  }
  .ps-panel.ps-collapsed > *:not(.ps-head) { display: none; }
  .ps-panel.ps-collapsed { padding-bottom: 10px; }
  .ps-panel.ps-collapsed .ps-title { display: none; }
  .ps-panel.ps-collapsed #ps-new { display: none; }
}

@media (max-width: 820px) {
  body { display: block; }
  .atelier-topbar { display: flex; }
  .sidebar {
    position: fixed; top: var(--rainbow-h); left: 0;
    height: calc(100vh - var(--rainbow-h)); width: 270px;
    transform: translateX(-100%); transition: transform 0.22s ease;
    box-shadow: 0 24px 60px rgba(20, 19, 26, 0.4);
    overflow-y: auto;
    /* Above the sticky top bar (z-index 45), or the drawer's wordmark is
       clipped by it. Bottom padding keeps Settings — pinned by
       `.nav-settings { margin-top:auto }` — clear of the tab bar. */
    z-index: 46;
    padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 20px);
  }
  body.nav-open .sidebar { transform: translateX(0); }
  body.nav-open .nav-backdrop { display: block; }

  /* Show the tab bar and reserve room for it (plus the home indicator). */
  .tabbar {
    display: flex; align-items: center;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    padding: 0 6px calc(env(safe-area-inset-bottom, 0px));
    min-height: var(--tabbar-h);
    background: var(--panel);
    border-top: 1px solid var(--line);
  }
  .content {
    padding: 20px 18px calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 28px);
  }
  /* Lift the two bottom-anchored overlays clear of the tab bar. */
  /* ★ v21.88 — FULL WIDTH ON A PHONE. v21.87's bar is centred with a
     max-width, which left margins the card grid behind it does not have.
     Edge to edge here so it matches the images it sits over; the tab-bar and
     safe-area offset below is v21.87's and is deliberately unchanged.
     Desktop is finished and is not touched — this lives inside the 820px
     block only. */
  .vault-selection {
    left: 0; right: 0; transform: none;
    max-width: none; width: auto;
    border-radius: 0;
    bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 12px);
  }
  .toast { bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 12px); }
  /* The horizontal folder strip from the <=1100 rule already applies here. */
  .vault-layout { grid-template-columns: 1fr; gap: 14px; }

  /* v21.7: strip the phone toolbar back to what earns its space. Five rows of
     chrome before the first image defeats the point of the screen.
       * density toggle — the phone grid is 2-up either way
       * "+ New piece"  — the red FAB in the tab bar does this (v22.2: it
                          genuinely does now; when this was written it did not)
       * upload strip   — drag-and-drop is meaningless on touch
     All three still exist for larger screens; nothing is removed from the DOM,
     so every handler stays wired. */
  /* v21.7.1: the density toggle EARNS its space on the phone now — Sheet is
     most useful exactly here, where scrolling a lot is the main activity. */
  /* The rail's upload button is hidden on a phone — inside the folder sheet
     it would read as a folder action — and the drag-and-drop strip is
     meaningless on touch.
     ★ v22.2 — THIS IS ONLY SAFE BECAUSE THE FAB NOW UPLOADS HERE. The two
     comments above used to assert that the FAB opened this same picker.
     It did not. index.html:1173 gives it data-screen="studio" and
     js/app.js:99 navigates on that, so hiding these three left the phone
     with no way at all to add a photo. js/vault.js's wireVaultFab() makes
     the FAB open #vault-upload while the Vault is the active screen. If
     that ever goes, un-hide one of these in the same edit. */
  #vault-new-piece,
  .vault-utility-row { display: none; }
  #vault-density { width: 100%; }
  #vault-density .seg-btn { flex: 1; padding: 10px 6px; }
  .vault-toolbar { gap: 8px; margin-bottom: 12px; }
  .vault-toolbar .search-box { margin-bottom: 0; }
  .vault-scope-head { margin-bottom: 12px; }
  .vault-gallery { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  /* v22.16 — `input.search-box`, not `.search-box`. The global
     `input:not([type=checkbox]):not([type=radio]) { min-width: 0 }` scores
     (0,2,1) and beat the old (0,2,0) selector here, so the box that should
     have its own full-width row was squeezed to 106px beside two filters. */
  .vault-toolbar input.search-box { min-width: 100%; }
  .vault-filter { flex: 1; min-width: 120px; }
  .item-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  /* Recently added: 3-up and small, so several read at once instead of one
     enormous plate per screen. Lives HERE, after .recent-strip's base rule —
     the same declaration up in the Home block lost on source order. */
  .recent-strip { grid-template-columns: repeat(3, 1fr); gap: 10px; }
  .recent-name { font-size: 12px; }
  .recent-cat { font-size: 8px; margin-top: 3px; }
  .recent-body { padding-top: 7px; }
  .card-form, .machine-form { grid-template-columns: 1fr; }
  /* v21.3: the old fixed h1 sizes are gone — the V21 scale is a clamp() that
     already resolves to 38px at this width. */
  /* ---- The detail overlay is ONE scrolling column on mobile (v21.16) ----
     It used to be two independent scrollers stacked inside a clipping box, and
     it produced two separate faults. Measured at 390x900:

       .detail-shell  max-height 846 (94vh), overflow hidden,
                      grid rows "414px 828px" = 1242px
       .detail-side   max-height 828 (92vh), min-height auto, overflow-y auto
       #detail-img    max-height 828 (92vh — never overridden here)

     1. 1242px of rows in an 846px box: 396px of the panel was clipped and
        UNREACHABLE, including Save / Download / Delete at the bottom.
     2. The <img> kept the desktop 92vh cap while its stage was capped at 46vh,
        and .detail-image does not clip — so an 828px image centred in a 414px
        stage spilled 180px above and 207px BELOW, painting over the form. That
        is the overlap in the screenshots, and it is a different fault from 1.

     The fix removes the second scroller instead of enlarging the first: the
     shell scrolls, its children size to content, and the image cap moves onto
     the <img> so it can never exceed its stage. */
  .detail-shell { grid-template-columns: 1fr; max-height: 94vh; overflow-y: auto; }
  /* The cap lives on the IMG, not the stage: a stage cap does not constrain a
     child that has its own larger max-height, and this stage does not clip. */
  .detail-image { max-height: none; }
  .detail-image img { max-height: 46vh; }
  /* No second scroller, and no 92vh floor fighting the shell's 94vh. */
  .detail-side { max-height: none; overflow-y: visible; }
  /* Once the SHELL is the scroller, an absolutely-positioned close button
     scrolls away with the content — measured at -1000px once you reach Save.
     Sticky pins it to the top of the scrollport instead. The negative bottom
     margin cancels the button's own outer height so it still contributes no
     row to the grid, exactly as `position: absolute` did.

     THE BUTTON IS TWO DIFFERENT HEIGHTS, which is why there are two values:
       501-820px  height: 38px (declared above)     ->  12 + 38 - 50 = 0
       <= 500px   min-height: 44px (touch-target rule) ->  12 + 44 - 56 = 0
     Measured, not derived: at 390 the computed height is 44px, not 38px. A
     single -50px here resolves to 12 + 44 - 50 = +6 and opens a 6px gap above
     the image on exactly the phone widths this block exists for. */
  .detail-close {
    position: sticky;
    top: 12px;
    justify-self: end;
    margin: 12px 14px -50px 0;
  }
  /* Bigger, finger-friendly touch targets. */
  .vault-heart, .vault-dl { width: 38px; height: 38px; opacity: 1; }
  .vault-check { width: 24px; height: 24px; }
}

/* The 44px touch-target rule (max-width: 500px, further up this file) raises
   .detail-close from its declared 38px to 44px, so the row-cancelling margin
   needs 6px more than the 820px block sets. It has to live HERE, after that
   block: both selectors are one class deep, so source order decides, and the
   820px `margin` shorthand would otherwise win. 12 + 44 - 56 = 0. */
@media (max-width: 500px) {
  .detail-close { margin-bottom: -56px; }
}

/* Hide the mobile chrome whenever the app is locked (auth gate). */
body.atelier-auth-checking .atelier-topbar,
body.atelier-auth-signed-out .atelier-topbar,
body.atelier-auth-checking .tabbar,
body.atelier-auth-signed-out .tabbar,
body.atelier-auth-checking .nav-backdrop,
body.atelier-auth-signed-out .nav-backdrop { display: none !important; }

@media (max-width: 480px) {
  /* Tighter gutters, but keep the tab-bar + safe-area allowance from above. */
  .content {
    padding: 18px 14px calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 28px);
  }
  .vault-gallery { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  /* v22.18a — 4 across, not 2: a tile is 83px at 390 (was 173px). Wider
     screens keep their auto-fill counts exactly. */
  .studio-picker { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
  .studio-results { grid-template-columns: 1fr; }
  .detail-side, .bsx-modal, .import-review-modal { padding: 20px 18px; }
  /* …and at that size the title goes, from SIGHT only. Clipped, not
     display:none, so it stays the button's accessible name (its picture's
     alt is empty, so the title is what a screen reader says). The picked
     frame and its square check are untouched. */
  .studio-pick-label {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
}

/* v22.18a — iPhone Safari zooms the page into any focused field whose text
   is under 16px (WebKit zooms it to an effective 16: WKWebViewStandardFontSize,
   changeset r230171), and does not zoom back out. 60 of the 62 fields on the
   page were 12.5–15px. So at phone and iPad widths every typeable field is
   16px. Pinch-zoom is NOT blocked: the viewport tag has no maximum-scale and
   no user-scalable=no. Not covered: the moodboard note (contenteditable) —
   its card height is measured from its text and saved with the board. */
@media (max-width: 820px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]):not([type="hidden"]),
  select,
  textarea { font-size: 16px !important; }
}
/* …and the only two hints that 16px pushed past their box at 390 (measured
   on every screen): the Studio's vault search, 3px over, and the new-password
   box in Settings, 5px over. 4px less side padding each, phone only. */
@media (max-width: 480px) {
  .studio-controls .search-box,
  #setting-new-password { padding-left: 12px; padding-right: 12px; }
}

/* ===========================================================
   Pass 2 — Project workspace (tabs) + spacious Build Sheet editor
   =========================================================== */

/* ----- Project workspace: a wide, tabbed overlay ----- */
.pw-shell {
  display: flex; flex-direction: column;
  width: min(900px, 96vw); max-height: 92vh; overflow: hidden;
}
.pw-head {
  padding: 26px 28px 14px;
  background-color: var(--panel);
  border-bottom: 1px solid var(--line);
}
.pw-kicker { font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent-ink); margin-bottom: 8px; }
.pw-name {
  width: 100%; border: none; background: transparent; padding: 0;
  font-family: var(--font-display); font-weight: 600; font-size: 26px; letter-spacing: -0.025em; color: var(--ink);
}
.pw-name:focus { outline: none; box-shadow: none; }
.pw-tabs { display: flex; gap: 4px; padding: 0 22px; border-bottom: 1px solid var(--line); background: var(--panel); }
.pw-tab {
  position: relative; border: none; background: none; cursor: pointer;
  padding: 13px 14px; font: inherit; font-size: 14px; font-weight: 600; color: var(--soft);
}
.pw-tab:hover { color: var(--ink); }
.pw-tab.active { color: var(--ink); }
.pw-tab.active::after { content: ""; position: absolute; left: 10px; right: 10px; bottom: -1px; height: 3px; background: var(--red); }
.pw-panels { flex: 1; overflow-y: auto; padding: 22px 28px 28px; background: var(--panel); }
.pw-panel { display: none; }
.pw-panel.active { display: block; }
.pw-panel > label { display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--mut); margin: 14px 0 6px; }
.pw-panel input[type="text"], .pw-panel textarea {
  width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 10px 12px; background: var(--panel-2); font: inherit; color: var(--ink);
}
.pw-panel input:focus, .pw-panel textarea:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

.pw-looks { display: grid; gap: 12px; }
.pw-look {
  display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
  border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel-2);
  padding: 12px 14px; cursor: pointer; transition: border-color 0.15s ease, transform 0.12s ease;
}
.pw-look:hover { border-color: var(--accent); transform: translateY(-2px); }
.pw-look-thumb { width: 54px; height: 64px; border-radius: var(--r-sm); object-fit: cover; flex: none; background: var(--thumb-bg); }
.pw-look-thumb.placeholder { display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.92); font-size: 11px; font-weight: 600; }
.pw-look-title { flex: 1; font-weight: 600; font-size: 15px; color: var(--ink); }
.pw-look-go { font-size: 13px; font-weight: 600; color: var(--accent-ink); flex: none; }

/* ----- The spacious, editable Build Sheet editor ----- */
.bsx-overlay {
  position: fixed; inset: 0; z-index: 10080; display: none;
  align-items: center; justify-content: center; padding: 24px;
  background: rgba(12, 8, 18, 0.72); backdrop-filter: blur(3px);
}
.bsx-overlay.open { display: flex; }
.bsx-modal {
  position: relative; display: flex; flex-direction: column;
  width: min(980px, 96vw); max-height: 92vh; overflow: hidden;
  background-color: var(--panel); border-radius: var(--r-xl); box-shadow: var(--shadow-2);
}
/* v21.99b — THE BUTTON GROUP IS ONE UNIT. .bsx-head-actions used to wrap
   internally, so when the row ran out of room the LAST items broke off
   alone — Save and × stranded on a second row under the other three.
   Now the head itself wraps and the group does not: the group sits beside
   the title while it fits (the title is what gives ground, via flex-basis
   below), and when it cannot fit, the WHOLE group drops to its own
   right-aligned row. Save can never split off by itself.
   Deterministic rather than tuned — no pixel widths to re-measure the next
   time a label changes, which is exactly what went wrong in v21.99. */
.bsx-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
  padding: 24px 28px 18px;
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
}
.bsx-head-main { flex: 1 1 260px; min-width: 0; }
.bsx-kicker { font-size: 10px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent-ink); margin-bottom: 6px; }
.bsx-title { font-family: var(--font-display); font-weight: 600; font-size: 24px; letter-spacing: -0.025em; margin: 0; color: var(--ink); }
.bsx-sub { font-size: 12.5px; color: var(--soft); margin-top: 5px; }
.bsx-head-actions { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; justify-content: flex-end; flex: 0 0 auto; margin-left: auto; }
.bsx-btn { grid-column: auto; white-space: nowrap; }

/* v21.98b — the save state, ON the Save button. Same green and red as
   .wr-review-status (css/styles.css ~5147), so the app keeps one of each.
   .primary already carries `transition: background 0.15s ease,
   border-color 0.15s ease`, so the colour eases in for free.
   ★★ THE :hover PAIRS ARE REQUIRED, NOT DECORATION. `.primary:hover` forces
   `background: #000` (css/styles.css:774), and the cursor is sitting on the
   button at the exact moment it turns green — without these the colour would
   flash and immediately snap back to black. */
#bsx-save.is-saving { opacity: 0.65; cursor: default; }
#bsx-save.is-saved,
#bsx-save.is-saved:hover {
  background: #2e9e6b; border-color: #2e9e6b; color: #fff;
}
#bsx-save.is-failed,
#bsx-save.is-failed:hover {
  background: #c0356b; border-color: #c0356b; color: #fff;
}

.bsx-proj { padding: 9px 12px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel-2); font: inherit; font-size: 13px; color: var(--ink); }
.bsx-close { width: 38px; height: 38px; border-radius: 50%; border: none; background: var(--panel); color: var(--ink); font-size: 22px; cursor: pointer; box-shadow: var(--shadow-1); }
.bsx-close:hover { background: var(--panel-2); }
.bsx-status { padding: 10px 28px 0; color: var(--accent-ink); font-weight: 600; font-size: 13px; min-height: 0; }
.bsx-status:empty { padding: 0; }


/* v21.96 — the spec sheet's inline options. An inline reveal, not a modal:
   the same pattern #detail-coll-select uses.
   ★ .bsx-spec-panel sets `display`, so it needs its own [hidden] partner
   rule — the trap CLAUDE.md documents. The chips are .tag-pick /
   .tag-pick-cloud reused verbatim, so no new chip styling exists here. */
.bsx-spec-panel {
  display: flex; flex-direction: column; gap: 12px;
  margin: 12px 28px 0; padding: 14px 16px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-md);
}
.bsx-spec-panel[hidden] { display: none; }
.bsx-spec-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.bsx-spec-label {
  font-family: var(--mono); font-size: 9px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--mut); min-width: 78px;
}
.bsx-spec-row select { flex: 1; min-width: 0; }
.bsx-spec-acts { display: flex; gap: 8px; }
.bsx-body { flex: 1; overflow-y: auto; padding: 18px 28px 28px; display: grid; gap: 14px; }
/* v21.99a — the "translating" wait. The card itself is `.wr-generating`, the
   app's shared "AI is working" card; this is SIZING ONLY, the same way
   `.ps-generating` handles it for the Prompt Studio (see the note at ~2948).
   No new card language, no third treatment. Centred because .bsx-body is a
   grid and the card is its only child while a translation is running. */
.bsx-body .bsx-deriving { margin: 12px auto; max-width: 560px; width: 100%; }

.bsx-section { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel); padding: 16px 18px; box-shadow: var(--shadow-1); }
.bsx-section-head { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; }
.bsx-section-icon { font-size: 16px; }
.bsx-section-title { font-family: var(--font-display); font-weight: 600; font-size: 16px; letter-spacing: -0.01em; margin: 0; flex: 1; color: var(--ink); }
.bsx-ai-tag, .bsx-manual-tag { font-size: 9.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; padding: 2px 8px; border-radius: var(--r-pill); }
.bsx-ai-tag { background: var(--accent-soft); color: var(--accent-ink); }
.bsx-manual-tag { background: var(--panel-2); color: var(--mut); }

.bsx-text { width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px; background: var(--panel-2); font: inherit; color: var(--ink); resize: vertical; min-height: 48px; }
.bsx-text:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

.bsx-rows { display: grid; gap: 10px; }
.bsx-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel-2); }
.bsx-num { width: 24px; height: 24px; flex: none; border-radius: 50%; background: var(--accent-soft); color: var(--accent-ink); font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 4px; }
.bsx-row-fields { flex: 1; display: flex; flex-wrap: wrap; gap: 8px 10px; min-width: 0; }
.bsx-field { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 120px; }
.bsx-field-wide { flex-basis: 100%; }
.bsx-field span { font-size: 10.5px; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase; color: var(--mut); }
.bsx-field input, .bsx-field select, .bsx-field textarea {
  width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 8px 10px; background: var(--panel); font: inherit; font-size: 13.5px; color: var(--ink); resize: vertical;
}
.bsx-field input:focus, .bsx-field select:focus, .bsx-field textarea:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.bsx-check { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 500; color: var(--ink); align-self: center; flex: none; }
.bsx-check input { width: auto; }
.bsx-row-del { width: 26px; height: 26px; flex: none; border: none; border-radius: 50%; background: var(--panel); color: #c0356b; font-size: 16px; line-height: 1; cursor: pointer; }
.bsx-row-del:hover { background: rgba(192, 53, 107, 0.1); }
.bsx-add { margin-top: 10px; border: 1px dashed var(--line); background: transparent; color: var(--accent-ink); font-weight: 600; font-size: 13px; padding: 9px 12px; border-radius: var(--r-sm); cursor: pointer; }
.bsx-add:hover { border-color: var(--accent); background: var(--accent-soft); }

@media (max-width: 640px) {
  .bsx-head { flex-direction: column; }
  /* v21.99b — the phone path is unchanged apart from these two, which undo
     the desktop rule above: on a column head there is no row to keep the
     group out of, so the buttons wrap normally rather than overflowing, and
     `margin-left: auto` would push them off a left-aligned stack. */
  .bsx-head-actions { justify-content: flex-start; flex-wrap: wrap; margin-left: 0; }
  .bsx-body, .pw-panels { padding-left: 16px; padding-right: 16px; }
  .bsx-close { position: absolute; top: 14px; right: 14px; }
  .pw-tabs { padding: 0 12px; }
}

/* ===========================================================
   E-2 — "Not Safe For Public" (NSFP) thumbnail blur
   A thumbnail <img> gets the .nsfp class when its asset is marked
   metadata.nsfp === true. It stays softly blurred wherever it appears.
   Desktop (real hover): sharpen while hovering, re-blur on leave.
   Touch: no hover, so it stays blurred until opened in the lightbox.
   The full-size lightbox image (#detail-img) never gets .nsfp, so
   clicking into an image always shows it clear.
   =========================================================== */
img.nsfp {
  filter: blur(14px);
  transition: filter 0.18s ease;
}
@media (hover: hover) {
  img.nsfp:hover { filter: none; }
}
/* The small, understated "nsfp" checkbox that sits under the Title in the
   detail lightbox. Deliberately quiet — not a flashy part of the UI. */
.detail-nsfp {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 6px 0 2px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--soft);
  cursor: pointer;
  user-select: none;
}
.detail-nsfp input { margin: 0; cursor: pointer; }

/* ===========================================================
   v22.17 — Picture details, the tightened layout
   -----------------------------------------------------------
   The approved mock (mocks/v22.16-vault-shelves/3b-detail-ai-closed.png):
   at a 1440×900 window a Garment with three photos, six fields, five tags
   and Status Keep fits the panel with NO scrolling. Values are the mock's
   (shelves.css "ROUND 2" / "2b"), renamed where the real code needed it.
   The first half applies at every width (new pieces of markup); the
   tightening itself is ≥821px only — the phone keeps its stacked,
   scrolling layout (the ≤820px block further up).
   =========================================================== */

/* The drawn Download / Crop icons (js/detail.js). Inline beside the word
   on a phone; the desktop row lays them out with a flex gap instead. */
.btn-ico { width: 15px; height: 15px; flex: none; vertical-align: -3px; margin-right: 6px; }

/* Color — read-only chips from metadata.colors (js/detail.js renderColorRow).
   ★ The group sets `display`, so it needs the [hidden] partner rule. */
.detail-color-group { display: grid; gap: 4px; }
.detail-color-group[hidden] { display: none; }
.detail-color-group > * { min-width: 0; }
.color-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.color-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px 3px 4px;
  border: 1px solid var(--line); border-radius: var(--r-pill); background: var(--panel-2);
  font-size: 12px; font-weight: 600; color: var(--ink);
}
.color-dot { width: 16px; height: 16px; border-radius: 50%; border: 1px solid rgba(20, 19, 26, 0.15); flex: none; }
.color-src { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut); margin-left: 4px; }

/* Collections — the chips and the "Add to collection…" picker on one line
   where there is room (the desktop block below), each on its own line on a
   phone.
   ★ The picker keeps a PERCENTAGE width here, measured: a <select> with
   `width: auto` is as wide as its longest folder name, and that widened the
   whole phone panel to 444px inside a 342px screen. A percentage width lets
   it shrink to the column instead. */
.detail-inline { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.detail-inline > .detail-tags { flex: 1 1 160px; min-width: 0; margin-top: 0; }
.detail-inline select { flex: 1 1 180px; width: 100%; min-width: 0; }

/* Tags — the "＋ Pick from your N tags" toggle shares the chips' line. The
   cloud is `display: contents`, so its chips (and the .tag-pick-rule
   divider, which still breaks the line) flow in this row beside it. */
.detail-tag-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
.detail-tag-line .tag-pick-cloud { display: contents; }
.detail-tag-line .tag-pick-toggle { margin-top: 0; }

/* Per-type fields — each a label above its input (js/detail.js
   renderTypeFields). One column here; three across on a desktop. */
.detail-meta-f { display: grid; gap: 3px; align-content: start; }
.detail-meta-f > * { min-width: 0; }
.detail-meta-f > label { margin-top: 10px; }

@media (min-width: 821px) {
  /* The image / panel split, for THIS overlay only — the project workspace
     (.detail-shell-project) shares .detail-shell and keeps its own. */
  .detail-shell-asset { grid-template-columns: 1.4fr 1fr; }

  .detail-side.tight { padding: 20px 22px 18px; }
  .detail-side.tight #detail-side-fields { gap: 3px; }
  .detail-side.tight label { margin-top: 7px; margin-bottom: 1px; font-size: 10px; letter-spacing: 0.1em; }
  /* About 30px tall: 6px of padding round a 13px line. */
  .detail-side.tight input:not([type="checkbox"]),
  .detail-side.tight select { padding: 6px 9px; font-size: 13px; min-height: 0; }
  /* Notes: two lines. */
  .detail-side.tight textarea { min-height: 0; height: 46px; padding: 6px 9px; font-size: 13px; }
  /* The Title clears the popup's own × (38px wide, 14px in from the
     shell's edge, so it reaches 30px into this panel's 22px padding):
     narrower by 46px rather than given a margin, because the global
     `width: 100%` plus a margin would push it past the panel.
     ★ v22.19 — it is the Title's WRAPPER that narrows (so the ✨ AI mark
     sits inside the box), and only while the AI note is not showing: with
     the note up, the note is the first row and clears the × itself. */
  .detail-side.tight #detail-side-fields:not(.has-ai-note) #detail-title-field { width: calc(100% - 46px); }

  .detail-side.tight .detail-actions { margin-top: 10px; padding-top: 10px; gap: 6px; flex-wrap: nowrap; }
  /* ★ `:not([hidden])` IS LOAD-BEARING. This gives the buttons a `display`,
     and Upscale / Crop are hidden with the `hidden` attribute on a Moodboard
     (v22.4) — a rule that matched them would show them again. Matching only
     the visible ones leaves `hidden` alone, with no partner rule needed.
     The Delete row (.detail-bottom) keeps its own padding, below. */
  .detail-side.tight .detail-actions:not(.detail-bottom) > button:not([hidden]) {
    padding: 9px 11px; display: inline-flex; align-items: center; gap: 6px; grid-column: auto;
  }
  .detail-side.tight .btn-ico { margin-right: 0; }
  /* The Project Workroom door for a look, now beside the actions rather
     than above the Title — less air, its divider kept. */
  .detail-side.tight .detail-buildsheet { margin: 8px 0 0; padding-bottom: 10px; }
  .detail-side.tight .detail-buildsheet .bs-head { font-size: 14px; margin-bottom: 2px; }
  .detail-side.tight .detail-buildsheet .hint { font-size: 12px; margin: 0 0 6px; }
  .detail-side.tight .detail-nsfp { margin-top: 8px; text-transform: none; letter-spacing: 0; font-size: 12px; }

  .detail-side.tight .gp-mini { width: 34px; height: 42px; }
  .detail-side.tight .detail-tags { min-height: 0; }
  /* One line, measured: a <select> is as wide as its LONGEST option, so one
     long folder name pushed the picker onto a second line (62px tall). The
     cap keeps it beside the chips; the open list still shows names whole. */
  .detail-side.tight .detail-inline { flex-wrap: nowrap; }
  .detail-side.tight .detail-inline select { flex: 0 1 auto; width: auto; max-width: 220px; }
  .detail-side.tight .detail-tag-line { margin-top: 2px; gap: 5px; }
  .detail-side.tight .tag-pick { padding: 2px 9px; font-size: 11.5px; }
  .detail-side.tight .tag-pick-toggle { padding: 2px 9px; font-size: 11.5px; }

  /* Three across: a Garment's six fields are two rows of three. The
     heading and any textarea field take the whole row. */
  .detail-side.tight #detail-meta { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px 8px; }
  .detail-side.tight .detail-meta-head,
  .detail-side.tight .detail-meta-f.wide { grid-column: 1 / -1; }
  .detail-side.tight .detail-meta-f > label { margin-top: 2px; font-size: 10px; font-weight: 600; letter-spacing: 0; text-transform: none; }

  .detail-side.tight .detail-bottom { margin-top: 10px; padding-top: 8px; justify-content: flex-end; }
  .detail-side.tight .detail-bottom .delete-button { padding: 6px 12px; min-height: 0; }
}

/* ===========================================================
   v22.19 — AI labels (js/ai-label.js), built to
   mocks/v22.16-vault-shelves/shelves.css. Existing tokens only.
   =========================================================== */

/* Read by a screen reader, never seen: the description a ✨ field or tag
   points at (aria-describedby="detail-ai-desc"). Absolutely positioned, so
   inside #detail-side-fields' grid it takes no row and adds no gap. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* The banner at the top of the Vault home: before the pass, while it runs,
   paused, and done. ★ It sets display: flex, so it carries a [hidden]
   partner rule — the trap this file documents again and again. */
.vault-ai-banner {
  position: relative; display: flex; align-items: center; gap: 18px;
  background: var(--panel); border: 1px solid var(--line);
  padding: 16px 20px 16px 24px; margin: 0 0 22px; box-shadow: var(--sh-1);
}
.vault-ai-banner[hidden] { display: none; }
.vault-ai-banner::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--rainbow-v);
}
.vault-ai-spark { font-size: 24px; line-height: 1; flex: none; }
.vault-ai-text { flex: 1; min-width: 0; }
.vault-ai-kicker {
  font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink);
}
.vault-ai-line { font-size: 13px; color: var(--mut); margin-top: 5px; line-height: 1.4; }
.vault-ai-line strong { color: var(--ink); font-weight: 600; }
.vault-ai-bar { height: 5px; background: var(--line2); margin-top: 11px; position: relative; overflow: hidden; }
.vault-ai-bar > span { position: absolute; left: 0; top: 0; bottom: 0; background: var(--rainbow); }
.vault-ai-num {
  font-family: var(--disp); font-weight: 800; font-size: 34px; line-height: 0.9;
  text-transform: uppercase; flex: none; text-align: right;
}
.vault-ai-num small {
  display: block; font-family: var(--mono); font-weight: 400; font-size: 9px;
  letter-spacing: 0.16em; color: var(--mut); margin-top: 6px;
}
.vault-ai-actions { display: flex; gap: 8px; flex: none; }
.vault-ai-banner .secondary, .vault-ai-banner .primary { padding: 10px 14px; grid-column: auto; }

/* v22.24 — the to-read list's banner (js/read-queue.js): the AI banner's
   look, first in the Vault column. The bar MOVES while the server reads —
   a light sweeping across it — so it is plainly working even between the
   10-second looks; reduced motion keeps it still. */
.vault-rq { margin-bottom: 18px; }
.vault-rq[hidden] { display: none !important; }
.vault-rq.has-x { padding-right: 52px; }
.vault-rq .vault-ai-bar.is-moving::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 40%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.75), transparent);
  animation: vault-rq-sweep 1.6s linear infinite;
}
.vault-rq .vault-ai-bar > span { transition: width 0.6s ease; }
@keyframes vault-rq-sweep { from { transform: translateX(-100%); } to { transform: translateX(250%); } }
@media (prefers-reduced-motion: reduce) {
  .vault-rq .vault-ai-bar.is-moving::after { animation: none; display: none; }
  .vault-rq .vault-ai-bar > span { transition: none; }
}
/* The finished line's ✕, top-right like the AI note's. */
.vault-rq-x {
  position: absolute; top: 8px; right: 8px; width: 36px; height: 36px; min-height: 0;
  border: 0; background: none; color: var(--mut); font-size: 16px; line-height: 1; cursor: pointer;
  border-radius: var(--r-xs); padding: 0; grid-column: auto;
}
.vault-rq-x:hover { background: var(--line2); color: var(--ink); }

/* A card whose AI-filled title, type or tag is still untouched: a small
   spark beside the status dot. */
.vault-ai-mark { font-size: 11px; line-height: 1; margin-left: auto; margin-right: 6px; flex: none; }

/* Picture details: the note, and the ✨ on what the AI filled in. */
.detail-ai-note {
  position: relative; display: flex; gap: 10px; align-items: flex-start;
  background: var(--panel-2); border: 1px solid var(--line); border-left: 3px solid var(--purple);
  padding: 10px 30px 10px 12px; margin-top: 4px;
  font-size: 12.5px; color: var(--ink); line-height: 1.4;
}
.detail-ai-note[hidden] { display: none; }
.detail-ai-note .vault-ai-kicker { display: block; margin-bottom: 3px; font-size: 9px; }
.detail-ai-spark { flex: none; }
.detail-ai-copy { min-width: 0; }
.detail-ai-x {
  position: absolute; top: 4px; right: 4px; width: 24px; height: 24px; min-height: 0;
  border: 0; background: none; color: var(--mut); font-size: 17px; line-height: 1; cursor: pointer;
  border-radius: var(--r-xs); padding: 0;
}
.detail-ai-x:hover { background: var(--line2); color: var(--ink); }
.detail-ai-field { position: relative; }
.detail-ai-field.is-ai::after {
  content: "✨ AI"; position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  font-family: var(--mono); font-size: 9px; letter-spacing: 0.12em; color: var(--mut); pointer-events: none;
}
.detail-ai-field.is-select.is-ai::after { right: 40px; }
/* Room for the mark, so a long title or type never runs under it. After
   the v22.17 tight rules on purpose: same weight, so the later one wins. */
.detail-side .detail-ai-field.is-ai > input { padding-right: 52px; }
.detail-side .detail-ai-field.is-select.is-ai > select { padding-right: 78px; }
.tag-pick.ai::after { content: "✨"; font-size: 10px; }

@media (min-width: 821px) {
  /* The note is the first row: it clears the popup's × (the Title's
     46px narrowing, v22.17, moves onto it — see #detail-title-field). */
  .detail-side.tight .detail-ai-note { margin-top: 0; margin-right: 40px; padding: 7px 30px 7px 10px; font-size: 12px; }
  /* ★ THE FIT. The note is 65px, and v22.17's Garment (front + 2 photos,
     six fields, five tags, Keep) then measured 863 / 828 — it scrolled.
     While the note shows, the section labels close up 7 -> 4px and the
     rows 3 -> 2px: 819 / 819, measured, with the note at the mock's own
     size. With the note closed nothing here applies and v22.17's 795 / 795
     is untouched. The per-type labels keep their own 2px. */
  .detail-side.tight #detail-side-fields.has-ai-note { gap: 2px; }
  .detail-side.tight #detail-side-fields.has-ai-note label { margin-top: 4px; }
  .detail-side.tight #detail-side-fields.has-ai-note .detail-meta-f > label { margin-top: 2px; }
}

@media (max-width: 820px) {
  .vault-ai-banner { flex-wrap: wrap; gap: 12px; padding: 14px 14px 14px 18px; margin-bottom: 18px; }
  .vault-ai-actions { width: 100%; }
  .vault-ai-actions .primary, .vault-ai-actions .secondary { flex: 1; }
  /* v22.24 — compact on a phone: no spark, one tight block under the top bar. */
  .vault-rq { gap: 8px 10px; padding: 10px 14px; margin-bottom: 12px; }
  .vault-rq.has-x { padding-right: 46px; }
  .vault-rq .vault-ai-spark { display: none; }
  .vault-rq .vault-ai-line { margin-top: 3px; font-size: 12.5px; }
  .vault-rq .vault-ai-bar { margin-top: 8px; }
  .vault-rq-x { top: 4px; right: 4px; width: 40px; height: 40px; }
  /* A finger-sized ×. min-height stays 0: the ≤500px 44px floor would
     stretch it into a bar down the note. */
  .detail-ai-x { width: 32px; height: 32px; top: 2px; right: 2px; }
  .detail-ai-note { padding-right: 38px; }
}

/* ===========================================================
   v22.20 — the Vault home's SHELVES (js/vault.js drawVaultShelves),
   built to mocks/v22.16-vault-shelves/shelves.css. Existing tokens only.
   The shelves live INSIDE #vault-gallery, so the density switch's classes
   (.compact / .sheet on the gallery) reach their cards exactly as they
   reach the grid's — and the gallery stops being a grid while they show.
   =========================================================== */
.vault-gallery.is-shelves { display: block; }
.vault-shelf { margin: 0 0 34px; min-width: 0; }
.vault-shelf-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 8px; min-width: 0; }
.vault-shelf-title {
  font-family: var(--disp); font-weight: 800; font-size: 26px; line-height: 1;
  text-transform: uppercase; letter-spacing: -0.01em; color: var(--ink);
  display: inline-flex; align-items: center; gap: 10px; margin: 0; min-width: 0;
}
.vault-shelf-title::before { content: ""; width: 18px; height: 3px; background: var(--cat, var(--ink)); flex: none; }
/* A long name gives way (an ellipsis) before the heading wraps: the heading
   and its See all stay on one line, on a phone too. */
.vault-shelf-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vault-shelf-count { font-family: var(--mono); font-size: 11px; color: var(--mut); letter-spacing: 0.08em; flex: none; white-space: nowrap; }
.vault-shelf-note { font-size: 12.5px; color: var(--mut); margin: -4px 0 12px; }
.vault-shelf-all {
  margin-left: auto; flex: none; background: none; border: 0; border-bottom: 2px solid var(--cat, var(--ink));
  /* 8px above the words: a 24px-tall target (WCAG 2.5.8) without the
     ≤500px 44px floor turning the underline into a tall block. */
  padding: 8px 0 2px; cursor: pointer; min-height: 0;
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink); white-space: nowrap;
}
.vault-shelf-strip { position: relative; }
/* ONE row that scrolls sideways — inside itself, never the page. Comfort
   (the bare gallery) ~206px cards; Compact 128px; Sheet two rows of 96px
   squares (its captions are hidden by the gallery's own .sheet rules). */
.vault-shelf-row {
  display: grid; grid-auto-flow: column; grid-auto-columns: 206px; gap: 22px;
  overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
  scrollbar-width: none; padding: 6px 4px 4px;
}
.vault-shelf-row::-webkit-scrollbar { display: none; }
.vault-gallery.is-shelves.compact .vault-shelf-row { grid-auto-columns: 128px; gap: 14px; }
.vault-gallery.is-shelves.sheet .vault-shelf-row { grid-template-rows: repeat(2, auto); grid-auto-columns: 96px; gap: 8px; }
.vault-gallery.is-shelves.compact .vault-card-title { font-size: 13px; }
.vault-gallery.is-shelves.sheet .vault-shelf { margin-bottom: 26px; }
/* "There's more this way": a soft fade on the right edge while there is. */
.vault-shelf-strip.has-more::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 70px; pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--canvas) 85%);
}
/* ‹ › for a mouse. ★ They set `display`, so [hidden] has its partner rule;
   js/vault.js syncShelfNav hides ‹ at the start and › at the end. */
.vault-shelf-nav {
  position: absolute; top: 34%; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; min-height: 0; padding: 0; border-radius: 50%;
  border: 1px solid var(--line); background: var(--panel); color: var(--ink);
  font-size: 22px; line-height: 1; cursor: pointer; box-shadow: var(--sh-1);
}
.vault-shelf-nav[hidden] { display: none; }
.vault-shelf-nav.prev { left: 6px; }
.vault-shelf-nav.next { right: 6px; }
.vault-shelf-nav:hover { border-color: var(--ink); }
.vault-gallery.is-shelves.sheet .vault-shelf-nav { top: 42%; }
/* A finger swipes: no buttons on a touch screen. */
@media (pointer: coarse) {
  .vault-shelf-nav { display: none; }
}
/* The handful of types too small for a shelf of their own. */
.vault-shelf-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.vault-shelf-chips .tag-pick { padding: 7px 13px; font-size: 12.5px; }

@media (max-width: 820px) {
  .vault-shelf { margin-bottom: 26px; }
  .vault-shelf-head { gap: 8px; }
  .vault-shelf-title { font-size: 22px; gap: 8px; }
  .vault-gallery.is-shelves.compact .vault-shelf-row { grid-auto-columns: 132px; gap: 12px; }
  .vault-gallery.is-shelves .vault-shelf-row { gap: 14px; }
}

/* ===========================================================
   v22.21 — THE WARDROBE (js/wardrobe.js), built to mocks/v22.21-wardrobe:
   wardrobe.css + wardrobe-pizazz.css, the bold pass. Existing tokens only.
   Each category's colour arrives in a style attribute — --wc (the block,
   the rod, the chip ring) and --wct (the text ON that block) — from
   window.atelierWardrobe.colourOf (js/wardrobe-intake.js), so no category
   name is written in CSS. The rails are the Vault's own shelf classes
   (v22.20, above); only what is new is here.
   ★ Every class here that sets `display` has its [hidden] partner.
   =========================================================== */

/* ---- 1. The poster header ---- */
.wd-mast {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0 48px;
  align-items: end; margin: 2px 0 22px;
}
.wd-mast[hidden] { display: none !important; }
.wd-mast-kicker {
  grid-column: 1 / -1; display: flex; align-items: center; gap: 10px;
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.24em; text-transform: uppercase; color: var(--ink);
}
.wd-mast-kicker[hidden] { display: none !important; }
.wd-mast-kicker::before { content: ""; width: 30px; height: 4px; background: var(--rainbow); flex: none; }
.wd-mast-titlerow { display: flex; align-items: flex-start; gap: 20px; }
.wd-mast-titlerow[hidden] { display: none !important; }
#screen-wardrobe .wd-mast-title {
  font-family: var(--disp); font-weight: 900; font-size: clamp(72px, 9vw, 138px);
  line-height: 0.8; letter-spacing: -0.02em; text-transform: uppercase;
  margin: 12px 0 0; color: var(--ink); white-space: nowrap;
}
.wd-mast-lead { font-size: 16px; color: var(--mut); margin: 14px 0 0; max-width: 540px; }
.wd-mast-stats { display: grid; grid-template-columns: repeat(2, auto); gap: 16px 34px; padding-bottom: 4px; }
.wd-mast-stats[hidden] { display: none !important; }
.wd-stat b {
  display: block; font-family: var(--disp); font-weight: 900; font-size: 46px;
  line-height: 0.82; color: var(--ink);
}
.wd-stat span {
  display: block; margin-top: 5px; font-family: var(--mono); font-size: 10px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--mut);
}
/* The closet as one bar: each block a category, as wide as its count. */
.wd-spectrum { grid-column: 1 / -1; display: flex; gap: 3px; height: 16px; margin-top: 22px; }
.wd-spectrum[hidden] { display: none !important; }
.wd-spectrum i { flex: var(--n) 1 0; min-width: 8px; background: var(--wc); }
/* "N new this week" — only while something arrived this week (js decides).
   Ink on --yellow: 12.1:1. */
.wd-sticker {
  flex: none; width: 114px; height: 114px; margin-top: 2px; transform: rotate(10deg);
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
  background: var(--yellow); color: var(--ink);
  clip-path: polygon(50% 0, 60.6% 10.4%, 75% 6.7%, 79% 21%, 93.3% 25%, 89.6% 39.4%, 100% 50%,
    89.6% 60.6%, 93.3% 75%, 79% 79%, 75% 93.3%, 60.6% 89.6%, 50% 100%, 39.4% 89.6%, 25% 93.3%,
    21% 79%, 6.7% 75%, 10.4% 60.6%, 0 50%, 10.4% 39.4%, 6.7% 25%, 21% 21%, 25% 6.7%, 39.4% 10.4%);
}
.wd-sticker[hidden] { display: none !important; }
.wd-sticker b { font-family: var(--disp); font-weight: 900; font-size: 34px; line-height: 0.82; text-transform: uppercase; }
.wd-sticker span { font-family: var(--mono); font-weight: 700; font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase; margin-top: 4px; }

/* ---- the top row: Add clothes · Sort my old photos · search · density ---- */
.wd-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 4px 0 12px; }
.wd-toolbar[hidden] { display: none !important; }
.wd-toolbar .search-box { flex: 1 1 240px; width: auto; margin: 0; }
.wd-toolbar > button { width: auto; flex: none; }

/* ---- chips: each wears its category colour as a 2px ring (border + inset
   shadow, so no chip gets wider); the chosen one goes solid ink. ---- */
.wd-chips { margin: 0 0 18px; }
.wd-chip-sep { width: 1px; align-self: stretch; background: var(--line); margin: 2px 4px; }
.wd-chip-back { border-style: dashed; }
#screen-wardrobe .wd-chips .tag-pick.wd-cc { border-color: var(--wc); box-shadow: inset 0 0 0 1px var(--wc); }
#screen-wardrobe .wd-chips .tag-pick.wd-cc:hover { background: var(--panel); }
#screen-wardrobe .wd-chips .tag-pick.on { background: var(--ink); color: #fff; }
#screen-wardrobe .wd-chips .tag-pick.on::before { display: none; }
#screen-wardrobe .wd-chips .tag-pick.on .tag-pick-n { color: #fff; opacity: 0.75; }

/* ---- the one-time photo tip: an ink "call sheet" with dressing-room bulbs ---- */
.wd-tip { margin-bottom: 26px; background: var(--ink); border-color: var(--ink); padding-top: 32px; box-shadow: var(--sh-2); }
.wd-tip[hidden] { display: none !important; }
.wd-tip::after {
  content: ""; position: absolute; left: 24px; right: 20px; top: 11px; height: 10px;
  background: radial-gradient(circle, #fff7d6 0 1.6px, var(--yellow) 2.2px 4px, transparent 4.6px) 0 0 / 24px 10px repeat-x;
}
.wd-tip .vault-ai-kicker { color: var(--yellow); }
.wd-tip .vault-ai-line { color: rgba(255, 255, 255, 0.8); }
.wd-tip .vault-ai-line strong { color: #fff; }
.wd-tip .secondary { background: #fff; color: var(--ink); border-color: #fff; }

/* ---- "← All rails" over a grid ---- */
.wd-crumb { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 0 0 14px; }
.wd-crumb[hidden] { display: none !important; }
.wd-crumb-back {
  background: none; border: 0; border-bottom: 2px solid var(--ink); padding: 6px 0 2px; min-height: 0;
  width: auto; cursor: pointer; font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink);
}
.wd-crumb-text { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; color: var(--mut); }

/* ---- 3. Rails: a rod in the category colour, every piece on a hanger ---- */
#screen-wardrobe .vault-shelf { margin-bottom: 44px; }
#screen-wardrobe .is-shelves .vault-shelf-strip::before {
  content: ""; position: absolute; left: 0; right: 0; top: 6px; height: 6px;
  background: var(--wrod, var(--wc, var(--ink)));
}
.wd-rail-lost { --wrod: repeating-linear-gradient(90deg, #b9b6c0 0 14px, transparent 14px 22px); }
#screen-wardrobe .is-shelves .vault-shelf-row { position: relative; padding-top: 34px; }
#screen-wardrobe .is-shelves.compact .vault-shelf-row { padding-top: 30px; }
#screen-wardrobe .is-shelves.sheet .vault-shelf-row { padding-top: 22px; }
#screen-wardrobe .is-shelves .wd-card { transform-origin: 50% -26px; }
#screen-wardrobe .is-shelves .wd-card::before {
  content: ""; position: absolute; left: 50%; top: -30px; width: 76px; height: 30px; margin-left: -38px;
  pointer-events: none;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 76 30'%3E%3Cpath d='M38 16V11C38 9 42 8.6 42 5C42 2.6 40.2 1 38 1C35.8 1 34 2.6 34 4.6M38 16L4 28.5M38 16L72 28.5' fill='none' stroke='%2314131a' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
#screen-wardrobe .is-shelves.compact .wd-card::before { width: 60px; margin-left: -30px; height: 26px; top: -26px; }
#screen-wardrobe .is-shelves.sheet .wd-card::before { display: none; }
@keyframes wd-swing {
  0% { transform: rotate(0); } 25% { transform: rotate(2.4deg); } 55% { transform: rotate(-1.6deg); }
  80% { transform: rotate(0.7deg); } 100% { transform: rotate(0); }
}
/* A hanging piece swings from its hook; it does not also lift. Mouse only,
   and never under reduced motion (the blanket rule shortens it to nothing;
   this takes it away outright). */
@media (hover: hover) {
  #screen-wardrobe .is-shelves .wd-card:hover { animation: wd-swing 0.9s ease-out; }
}
#screen-wardrobe .is-shelves .wd-card:hover .vault-photo-wrap,
#screen-wardrobe .is-shelves .wd-card:focus-within .vault-photo-wrap { transform: none; }
@media (prefers-reduced-motion: reduce) {
  #screen-wardrobe .wd-card:hover { animation: none !important; }
}

/* Two small categories share a row ONLY while every card of both fits —
   js/wardrobe.js fitPairs() measures it and adds .is-stacked otherwise. */
.wd-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0 36px; min-width: 0; }
.wd-pair[hidden] { display: none !important; }
.wd-pair.is-stacked { display: block; }

/* ---- 4. Showroom headings: kicker, bigger name, the count in a block ---- */
#screen-wardrobe .vault-shelf-head { flex-wrap: wrap; align-items: center; row-gap: 4px; margin-bottom: 0; }
.wd-rail-kicker {
  flex: 0 0 100%; font-family: var(--mono); font-weight: 700; font-size: 10px;
  letter-spacing: 0.24em; text-transform: uppercase; color: var(--mut);
}
#screen-wardrobe .vault-shelf-title { font-size: 42px; font-weight: 900; }
#screen-wardrobe .vault-shelf-title::before { display: none; }
/* Large text on purpose (22px, weight 800 — WCAG's large-text bar is
   18.66px bold): ink on the brights measured 4.10–12.08:1 against the 3:1
   that applies, white on the two dark neutrals 5.24 and 18.46:1, ink on the
   grey 9.24:1. REDESIGN_LOG v22.21. */
#screen-wardrobe .vault-shelf-count {
  font-family: var(--disp); font-weight: 800; font-size: 22px; line-height: 1;
  letter-spacing: 0.01em; text-transform: uppercase;
  color: var(--wct, var(--ink)); background: var(--wc, var(--line)); padding: 4px 9px 3px;
}
#screen-wardrobe .vault-shelf-all { border-bottom: 3px solid var(--wc, var(--ink)); }
#screen-wardrobe .vault-shelf-note { margin: 4px 0 0; }

/* ---- the garment card: the Vault card plus size, sides and Model it ---- */
/* The hover-only type tab: ink with a stripe of the category colour, so its
   white words never sit on a light bright (white on --yellow is 1.5:1). */
#screen-wardrobe .wd-cat-tab { background: var(--ink); color: #fff; border-left: 4px solid var(--cat, var(--ink)); }
.wd-card .vault-open { position: relative; }
.wd-card .wd-back { position: absolute; inset: 0; opacity: 0; transition: opacity 0.18s ease; }
.wd-card.show-back .wd-back { opacity: 1; }
@media (hover: hover) {
  .wd-card.has-back:hover .wd-back { opacity: 1; }
}
/* 5. Size as a paper luggage tag: pointed end, punched hole. */
.wd-size {
  position: absolute; left: 10px; bottom: 12px; z-index: 2; max-width: calc(100% - 64px);
  background: #fff; color: var(--ink); padding: 5px 10px 5px 19px;
  font-family: var(--mono); font-weight: 700; font-size: 11px; letter-spacing: 0.06em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transform: rotate(-5deg); transform-origin: left center;
  clip-path: polygon(9px 0, 100% 0, 100% 100%, 9px 100%, 0 50%);
}
.wd-size::before {
  content: ""; position: absolute; left: 6px; top: 50%; width: 4px; height: 4px; margin-top: -2px;
  border-radius: 50%; background: rgba(20, 19, 26, 0.35);
}
.wd-size.empty { color: var(--mut); font-weight: 400; font-style: italic; }
/* Compact: the tag may run up to the heart, so "Add size" reads whole on a
   128px card (it was "Add…"). */
.vault-gallery.compact .wd-size { left: 6px; bottom: 8px; font-size: 10px; padding: 4px 6px 4px 15px; max-width: calc(100% - 48px); }
.vault-gallery.compact .wd-size.empty { font-size: 9px; letter-spacing: 0.02em; }
.vault-gallery.sheet .wd-size { left: 4px; bottom: 6px; font-size: 9px; padding: 3px 6px 3px 15px; max-width: calc(100% - 40px); }
/* Sheet is the contact sheet (its captions are hidden too): a 96px square has
   no room for "Add size" beside the heart — measured, it showed as "Add…" —
   so only a real size shows there. */
.vault-gallery.sheet .wd-size.empty { display: none; }
/* On a phone the heart grows to the 44px touch floor, which leaves a 96px
   square no room for any tag (measured: 3–9px under the heart). */
@media (max-width: 500px) {
  .vault-gallery.sheet .wd-size { display: none; }
}
/* Front / back: two dots at the TOP-RIGHT, inside the card (never the
   top-left, which belongs to the Pinterest button). A real button: 34×26,
   above WCAG's 24px — min-height: 0 keeps the phones' 44px floor off it. */
.wd-sides {
  position: absolute; top: 4px; right: 4px; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 26px; min-height: 0; padding: 0; border: 0; background: none; cursor: pointer;
}
.wd-sides[hidden] { display: none !important; }
.wd-sides:focus-visible { outline: none; box-shadow: var(--ring); }
.wd-sides-pill { display: flex; gap: 4px; background: rgba(20, 19, 26, 0.62); padding: 4px 6px; border-radius: 10px; }
.wd-sides-pill[hidden] { display: none !important; }
.wd-sides-pill i { display: block; width: 6px; height: 6px; border-radius: 50%; background: rgba(255, 255, 255, 0.45); }
.wd-sides-pill i.on { background: #fff; }
.wd-card.show-back .wd-sides-pill i { background: #fff; }
.wd-card.show-back .wd-sides-pill i.on { background: rgba(255, 255, 255, 0.45); }
@media (hover: hover) {
  .wd-card.has-back:hover .wd-sides-pill i { background: #fff; }
  .wd-card.has-back:hover .wd-sides-pill i.on { background: rgba(255, 255, 255, 0.45); }
}
/* v22.22a — a phone: the dots get the 44×44 touch floor (still TOP-RIGHT,
   flush to the corner) and grow from 6px to 8px. The heart is bottom-right,
   so even a Sheet card (96×120) keeps the two apart. */
@media (max-width: 820px) {
  .wd-sides { top: 0; right: 0; width: 44px; height: 44px; }
  .wd-sides-pill { gap: 5px; padding: 5px 7px; border-radius: 12px; }
  .wd-sides-pill i { width: 8px; height: 8px; }
}
/* v22.22a — a sideways swipe on a picture with a back flips it
   (js/wardrobe.js wireSwipe). The browser keeps up-and-down scrolling and
   pinch-zoom; a sideways move starting on the picture is left to the page.
   ★ v22.22b — in a GRID only (a chip's category view, See all, search: the
   cards sit straight in #wardrobe-gallery, which then isn't .is-shelves). A
   card in a rail keeps the default, so a sideways swipe scrolls its rail.
   Touch only — a mouse ignores touch-action. */
#wardrobe-gallery:not(.is-shelves) > .wd-card.has-back .vault-photo-wrap { touch-action: pan-y pinch-zoom; }
.wd-meta { gap: 8px; justify-content: space-between; }
.wd-photos {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.06em; color: var(--mut);
  text-transform: uppercase; white-space: nowrap;
}
/* The card's two small buttons: 26px tall (WCAG's floor is 24), so the
   phones' 44px button floor does not stretch every card. */
.wd-add-back, .wd-model {
  display: inline-flex; align-items: center; min-height: 26px; width: auto; flex: none;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.05em; text-transform: uppercase;
  white-space: nowrap; cursor: pointer;
}
.wd-add-back[hidden], .wd-model[hidden] { display: none !important; }
.wd-add-back { border: 0; background: none; padding: 0; color: var(--accent-ink); }
.wd-add-back:hover { text-decoration: underline; }
.wd-model { border: 1px solid var(--line); background: var(--panel); color: var(--ink); padding: 0 8px; }
.wd-model:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.vault-gallery.compact .wd-meta { flex-wrap: wrap; row-gap: 2px; }

/* ---- 7. The Vault's doorway, in the Garments shelf's place ---- */
.wd-door-inner {
  position: relative; display: flex; align-items: center; gap: 20px;
  background: var(--panel); border: 1px solid var(--line); box-shadow: var(--sh-1);
  padding: 16px 20px 16px 24px;
}
.wd-door-inner[hidden] { display: none !important; }
.wd-door-inner::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--orange); }
.wd-door-text { flex: 1; min-width: 0; }
.wd-door-kicker { margin: 0; }
.wd-door-line { font-size: 13.5px; color: var(--mut); margin: 6px 0 0; line-height: 1.4; }
.wd-door-line strong { color: var(--ink); }
.wd-door-thumbs { display: flex; gap: 6px; flex: none; }
.wd-door-thumbs[hidden] { display: none !important; }
.wd-door-thumbs img {
  width: 44px; height: 55px; object-fit: cover; background: var(--thumb-bg);
  border: 2px solid transparent; border-image: var(--rainbow) 1;
}
.wd-door-inner > button { width: auto; flex: none; }
/* The rail's "👔 Wardrobe ↗": a door, so it reads as one. */
.wd-rail-door .vault-folder-name { color: var(--accent-ink); }

/* ---- 8. The Fitting Room's doorway ---- */
.wd-fit-door {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 34px; padding: 14px 16px; border: 1px solid var(--line); background: var(--panel);
  border-left: 4px solid var(--orange);
}
.wd-fit-door[hidden] { display: none !important; }
.wd-fit-door-ic { font-size: 20px; line-height: 1; flex: none; }
.wd-fit-door-line { flex: 1 1 260px; min-width: 0; font-size: 14px; color: var(--mut); }
.wd-fit-door-line strong { color: var(--ink); }
.wd-fit-door > button { width: auto; flex: none; }

/* The try-on's "Who's wearing it?" (js/tryon.js, opened from the Wardrobe). */
.tryon-people { margin: 6px 0 14px; }

/* ---- phone (Josh's fix 2): the header shrinks to the title and its sticker
   on one line, ONE row of the four numbers, and the bar. No kicker, no
   subtitle. The first rail's cards must start inside a 390×664 screen. ---- */
@media (max-width: 820px) {
  .wd-mast { grid-template-columns: minmax(0, 1fr); margin-bottom: 10px; }
  .wd-mast-kicker, .wd-mast-lead { display: none; }
  .wd-mast-titlerow { gap: 10px; align-items: center; }
  #screen-wardrobe .wd-mast-title { font-size: 50px; margin-top: 2px; }
  .wd-mast-stats { grid-template-columns: repeat(4, auto); justify-content: start; gap: 6px 18px; margin-top: 8px; padding-bottom: 0; }
  .wd-stat b { font-size: 26px; }
  .wd-stat span { font-size: 8.5px; letter-spacing: 0.08em; margin-top: 2px; }
  .wd-sticker { width: 54px; height: 54px; margin-top: 0; }
  .wd-sticker b { font-size: 15px; }
  .wd-sticker span { font-size: 4.5px; letter-spacing: 0.1em; margin-top: 2px; }
  .wd-spectrum { height: 10px; margin-top: 8px; }
  /* Two rows, not three: the two intake buttons, then search beside the
     density switch — measured, what lets the first rail's cards start
     inside a 390×664 screen (REDESIGN_LOG v22.21). The switch's words are
     set small enough that "Search your wardrobe…" still reads whole. */
  .wd-toolbar { gap: 8px; margin-bottom: 8px; }
  .wd-toolbar > button { flex: 1 1 calc(50% - 4px); padding-left: 8px; padding-right: 8px; font-size: 11px; letter-spacing: 0.04em; }
  .wd-toolbar .search-box { flex: 1 1 0; min-width: 0; order: 3; padding-left: 10px; padding-right: 10px; }
  /* The HINT text only, at 13px, so "Search your wardrobe…" reads whole in
     the room beside the switch (measured: 182px at 16px, 166px of box, less
     the room WebKit keeps for the search box's own × — at 14px the "…" was
     still cut, seen in a WebKit screenshot). What Josh types stays 16px —
     the size that stops iPhone zooming in. */
  #wardrobe-search::placeholder { font-size: 13px; }
  .wd-toolbar .seg { order: 4; flex: none; }
  .wd-toolbar .seg-btn { padding: 0 6px; font-size: 9px; letter-spacing: 0.04em; }
  .wd-chips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; margin-bottom: 10px; }
  .wd-chips::-webkit-scrollbar { display: none; }
  .wd-chips .tag-pick { flex: none; }
  /* The tip: its words take the full width (the 📸 steps aside, and the
     last sentence too), and Got it sits up beside the heading — 26px tall,
     above WCAG's 24. */
  .wd-tip { display: block; padding: 20px 14px 10px 18px; margin-bottom: 12px; }
  .wd-tip::after { top: 6px; left: 18px; right: 14px; }
  .wd-tip .vault-ai-spark, .wd-tip-more { display: none; }
  .wd-tip .vault-ai-kicker { padding-right: 84px; line-height: 26px; }
  .wd-tip .vault-ai-line { margin-top: 0; font-size: 13px; }
  .wd-tip .vault-ai-actions { position: absolute; top: 20px; right: 12px; width: auto; }
  .wd-tip .vault-ai-actions .secondary { flex: none; min-height: 26px; padding: 0 12px; font-size: 10px; }
  #screen-wardrobe .vault-shelf { margin-bottom: 30px; }
  #screen-wardrobe .vault-shelf-head { gap: 8px; row-gap: 2px; }
  .wd-rail-kicker { font-size: 9px; }
  #screen-wardrobe .vault-shelf-title { font-size: 28px; }
  /* 19px weight 800 stays LARGE text (18.66px bold), so the 3:1 bar holds. */
  #screen-wardrobe .vault-shelf-count { font-size: 19px; padding: 3px 7px 2px; }
  .wd-pair { grid-template-columns: minmax(0, 1fr); }
  .wd-door-inner { flex-wrap: wrap; }
  .wd-door-thumbs { order: 3; width: 100%; }
}

/* ===========================================================
   v22.22 — JOSH'S OWN TYPES (js/custom-types.js), built to
   mocks/v22.22-custom-types/types.css. Existing tokens only. Each type's
   card colours are NOT here: js/custom-types.js writes one .cat-ct-<id> /
   .cat-text-<ct-id> pair per type into <style id="atelier-type-colours">.
   ★ Every class here that sets `display` has its [hidden] partner.
   =========================================================== */

/* ---- the form dialog: js/ask.js atelierAskForm (the app's one dialog) ---- */
.ask-modal.ask-form {
  max-width: 500px; max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px);
  overflow-y: auto; padding-bottom: 0;
}
/* The buttons stay at the bottom of a box that has to scroll (a phone), so
   the confirm button is never hidden below the fold. */
.ask-form .ask-actions {
  position: sticky; bottom: 0; z-index: 1; background: var(--panel);
  margin-top: 16px; padding: 12px 0 20px;
}
.ask-hint { margin: 6px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--mut); }
/* The one plain line that says why (a duplicate name, a built-in name). */
.ask-error { margin: 12px 0 0; font-size: 13px; line-height: 1.45; font-weight: 600; color: var(--accent-ink); }
.ask-icons { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px; }
.ask-icons[hidden] { display: none !important; }
.ask-icon {
  height: 40px; min-height: 0; width: auto; padding: 0; font-size: 19px; line-height: 1;
  border: 1px solid var(--line); background: var(--panel-2); border-radius: var(--r-xs); cursor: pointer;
}
.ask-icon.on { border: 2px solid var(--ink); background: var(--panel); }
.ask-icon:focus-visible { outline: none; box-shadow: var(--ring); }
.ask-swatches { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.ask-swatches[hidden] { display: none !important; }
.ask-swatch {
  width: 28px; height: 28px; min-height: 0; padding: 0; border-radius: 50%; cursor: pointer;
  border: 2px solid var(--panel); box-shadow: 0 0 0 1px var(--line); background: var(--c);
}
.ask-swatch.on { box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--ink); }
.ask-swatch:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--accent); }
/* "how its pictures are marked": the card tab, drawn in the type's colour. */
.ask-preview { display: flex; align-items: center; gap: 10px; margin-top: 14px; }
.ask-preview[hidden] { display: none !important; }
.ask-preview .cat-tab { position: static; display: inline-block; }
.ask-preview-note { font-size: 12.5px; color: var(--mut); }
.ask-check {
  display: flex; gap: 10px; align-items: flex-start; margin-top: 16px; padding: 12px 14px;
  border: 1px solid var(--line); background: var(--panel-2); border-radius: var(--r-xs);
  font-size: 13.5px; line-height: 1.45; color: var(--ink); cursor: pointer;
}
.ask-check[hidden] { display: none !important; }
.ask-check input { width: 18px; height: 18px; min-width: 18px; margin: 1px 0 0; accent-color: var(--red); flex: none; }
.ask-check small { display: block; color: var(--mut); font-size: 12.5px; margin-top: 2px; }
/* v22.24a — a "pictures" field (Choose its front): small pictures, one tap
   is the answer. Three across on a phone, four on a desktop. */
.ask-pics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-top: 6px; }
.ask-pics[hidden] { display: none !important; }
.ask-pic {
  position: relative; display: flex; flex-direction: column; gap: 4px; min-width: 0; min-height: 0;
  padding: 4px 4px 6px; border: 1px solid var(--line); background: var(--panel-2); border-radius: var(--r-xs);
  cursor: pointer; text-align: left; color: var(--ink); grid-column: auto;
}
.ask-pic img, .ask-pic-none { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; background: var(--line2); }
.ask-pic-label {
  font-size: 11px; line-height: 1.3; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
/* Under the picture, never over it. */
.ask-pic-note {
  align-self: flex-start; max-width: 100%; padding: 2px 5px; background: var(--ink); color: #fff;
  font-family: var(--mono); font-size: 8.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ask-pic.on { border: 2px solid var(--ink); background: var(--panel); }
.ask-pic:hover { border-color: var(--ink); }
.ask-pic:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---- the rail: a type of Josh's shows its ⋯ faintly at rest (it has
   one; a built-in never does), fully on hover / when it is the view ---- */
.ty-row .vault-folder-menu-btn { opacity: 0.55; }

/* ---- "✨ Find pictures for it": the banner, the ticked grid ---- */
.ty-suggest { margin: 0 0 4px; }
.ty-suggest[hidden] { display: none !important; }
.ty-suggest-banner { margin-bottom: 16px; }
.ty-section-label {
  font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--mut); margin: 4px 0 10px;
}
.ty-suggest-grid { margin-bottom: 22px; }
/* Each card is a <label> round its tick: the whole card toggles it. */
.ty-sg-card { display: block; }
.ty-sg-card[hidden] { display: none !important; }
.ty-sg-card .vault-photo-wrap, .ty-sg-card .vault-card-body, .ty-sg-card .vault-card-title { display: block; }
.ty-sg-card:not(.selected) .vault-photo { opacity: 0.55; }
.ty-from {
  display: block; margin-top: 4px; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--mut);
}
/* A type of Josh's with nothing in it yet — across the whole grid. */
.ty-empty {
  grid-column: 1 / -1; border: 1px dashed var(--line); padding: 26px; text-align: center;
  color: var(--mut); font-size: 13.5px; background: var(--panel);
}

/* ---- phone: the add box (picture 7b) fits a 390px screen ---- */
@media (max-width: 820px) {
  .ask-modal.ask-form { padding: 20px 16px 0; }
  .ask-form .ask-label { margin-top: 12px; }
  .ask-icons { gap: 4px; }
  .ask-icon { height: 34px; font-size: 16px; }
  .ask-swatches { gap: 8px; }
  .ask-swatch { width: 26px; height: 26px; }
  .ask-check { margin-top: 12px; padding: 10px 12px; }
  .ask-pics { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
}

/* ===========================================================
   v22.23 — A PATTERN'S BACK in the Vault (js/vault.js vaultCardHtml).
   The Wardrobe's own dots (.wd-sides / .wd-sides-pill, above), TOP-RIGHT
   just left of the select box — which owns the corner. A click or tap
   flips the card; a desktop hover shows the back. No swipe here. The
   back's picture is fetched only when first asked for.
   =========================================================== */
#vault-gallery .vault-card.has-back .vault-open { position: relative; }
#vault-gallery .vault-card .wd-back { position: absolute; inset: 0; opacity: 0; transition: opacity 0.18s ease; }
#vault-gallery .vault-card.show-back .wd-back { opacity: 1; }
#vault-gallery .vault-card.show-back .wd-sides-pill i { background: #fff; }
#vault-gallery .vault-card.show-back .wd-sides-pill i.on { background: rgba(255, 255, 255, 0.45); }
@media (hover: hover) {
  #vault-gallery .vault-card.has-back:hover .wd-back { opacity: 1; }
  #vault-gallery .vault-card.has-back:hover .wd-sides-pill i { background: #fff; }
  #vault-gallery .vault-card.has-back:hover .wd-sides-pill i.on { background: rgba(255, 255, 255, 0.45); }
}
/* The select box is 21px at 9px in (17px at 5px in Sheet); the dots sit 6px
   to its left, their middle level with its middle. */
#vault-gallery .wd-sides { top: 6px; right: 36px; }
#vault-gallery.sheet .wd-sides { top: 0; right: 24px; }
/* A phone: the dots' 44×44 touch box (the ≤820 rule above), flush to the
   top, beside a 24px select box (still 17px in Sheet). */
@media (max-width: 820px) {
  #vault-gallery .wd-sides { top: 0; right: 38px; }
  #vault-gallery.sheet .wd-sides { top: 0; right: 24px; }
}
@media (prefers-reduced-motion: reduce) {
  #vault-gallery .vault-card .wd-back { transition: none; }
}
/* A back that found no front: one quiet line under its title. */
.vault-back-note {
  font-family: var(--ui); font-size: 11px; line-height: 1.35;
  color: var(--mut); margin: 5px 0 0;
}
/* v22.24c — a Pattern card shows the WHOLE envelope: envelopes are about
   2:3, the card 4:5 (1:1 in Sheet), and a crop cut off the brand and number
   printed at the top. Fit, on the picture's own --thumb-bg, every size and
   the shelves (the same card). Other types keep their crop. */
.vault-card.is-pattern .vault-photo { object-fit: contain; object-position: 50% 50%; }
/* "<brand> · <number>" under the title: the number never cut, the brand
   shortens first. */
.vault-pattern-code {
  display: flex; align-items: baseline; gap: 5px; min-width: 0; margin: 3px 0 0;
  font-family: var(--mono); font-size: 10.5px; line-height: 1.35; letter-spacing: 0.03em; color: var(--ink);
}
.vpc-brand { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--mut); }
.vpc-sep { flex: none; color: var(--mut); }
.vpc-num { flex: none; white-space: nowrap; font-weight: 700; overflow-wrap: normal; }
/* "N patterns photographed twice · Show them" — the Patterns view and the
   home's Patterns shelf. */
.vault-doubles-line {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 6px;
  margin: 0 0 14px; font-size: 13px; line-height: 1.4; color: var(--ink);
}
.vault-doubles-line[hidden] { display: none !important; }
.vault-doubles-line.in-shelf { margin: 0 0 10px; }
.vault-doubles-show {
  display: inline-flex; align-items: center; width: auto; min-height: 30px; padding: 2px 10px; grid-column: auto;
  border: 1px solid var(--ink); background: var(--panel); color: var(--ink); border-radius: var(--r-xs);
  font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer;
}
.vault-doubles-show:hover { background: var(--ink); color: #fff; }
.vault-doubles-show:focus-visible { outline: none; box-shadow: var(--ring); }
/* "Photographed twice": each pair starts its own row, so the two sit side by side. */
.vault-gallery .vault-card.pair-start { grid-column-start: 1; }
@media (max-width: 820px) {
  .vault-doubles-show { min-height: 44px; }
}
/* v22.24a — its "Choose its front", right under that line; a finger-sized
   44px on a phone. Card body only, never over the picture. */
.vault-choose-front, .gp-choose-front {
  display: inline-flex; align-items: center; justify-content: center; width: auto; grid-column: auto;
  min-height: 30px; margin: 6px 0 0; padding: 4px 10px;
  border: 1px solid var(--ink); background: var(--panel); color: var(--ink); border-radius: var(--r-xs);
  font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  cursor: pointer;
}
.vault-choose-front:hover, .gp-choose-front:hover { background: var(--ink); color: #fff; }
.vault-choose-front:focus-visible, .gp-choose-front:focus-visible { outline: none; box-shadow: var(--ring); }
.gp-choose-front { margin: 0 0 0 auto; }
/* v22.24b — a double: its line, then Merge / Keep both (same buttons). */
.vault-double-note {
  font-family: var(--ui); font-size: 11px; line-height: 1.35;
  color: var(--ink); margin: 5px 0 0;
}
.dbl-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.dbl-merge, .dbl-keep {
  display: inline-flex; align-items: center; justify-content: center; width: auto; grid-column: auto;
  min-height: 30px; margin: 0; padding: 4px 10px;
  border: 1px solid var(--ink); background: var(--panel); color: var(--ink); border-radius: var(--r-xs);
  font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  cursor: pointer;
}
.dbl-merge { background: var(--ink); color: #fff; }
.dbl-merge:hover { background: var(--panel); color: var(--ink); }
.dbl-keep:hover { background: var(--ink); color: #fff; }
.dbl-merge:focus-visible, .dbl-keep:focus-visible { outline: none; box-shadow: var(--ring); }
@media (max-width: 820px) {
  .vault-choose-front, .gp-choose-front, .dbl-merge, .dbl-keep { min-height: 44px; }
  .gp-turn { width: 44px; height: 44px; font-size: 17px; }
}

/* ===========================================================
   C-2 — collaborative capture on the Maker Plan (Workroom)
   A quiet per-section row: "Unsure" checkbox + a note field.
   =========================================================== */
.wr-review {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px dashed var(--line);
}
.wr-review-chk {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--soft);
  cursor: pointer;
  user-select: none;
}
.wr-review-chk input { margin: 0; cursor: pointer; }
.wr-review-note {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  padding: 6px 9px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--panel);
  color: var(--ink);
}
/* v21.10b: was outline:none with only a border tint. */
.wr-review-note:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.wr-review-note:focus { outline: none; border-color: var(--accent); }
/* When a section is flagged "Unsure", give it a soft accent edge. */
.wr-read-card.wr-read-unsure {
  border-left: 3px solid var(--accent);
  padding-left: 12px;
}
/* Per-section save feedback for the Unsure/note fields. */
.wr-review-status { flex: none; min-width: 62px; font-size: 11px; color: var(--soft); }
.wr-review-status.is-saved { color: #2e9e6b; }
.wr-review-status.is-failed { color: #c0356b; }

/* ===========================================================
   Maker Plan version snapshots (Workroom rail list + read-only modal)
   =========================================================== */
.wr-version-hint { margin: 2px 0 8px; }
.wr-version-list { display: flex; flex-direction: column; gap: 6px; }
.wr-version-row {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel); color: var(--ink);
}
.wr-version-row:hover { border-color: var(--accent); }
.wr-version-n { font-weight: 600; font-size: 13px; }
.wr-version-t { font-size: 11px; color: var(--soft); }

.wr-vm-overlay {
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(20, 18, 28, 0.55);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.wr-vm-modal {
  background: var(--canvas); color: var(--ink);
  width: min(760px, 100%); max-height: 86vh; overflow: auto;
  border: 1px solid var(--line); border-radius: var(--r-lg, 16px);
  box-shadow: var(--shadow-lg, 0 20px 60px rgba(0, 0, 0, 0.3));
}
.wr-vm-head {
  position: sticky; top: 0; background: var(--canvas);
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 18px 22px; border-bottom: 1px solid var(--line);
}
.wr-vm-title { margin: 2px 0 0; font-size: 18px; }
.wr-vm-close {
  flex: none; width: 32px; height: 32px; border-radius: 50%;
  border: none; background: var(--panel); color: var(--ink);
  font-size: 20px; line-height: 1; cursor: pointer;
}
.wr-vm-close:hover { background: var(--line); }
.wr-vm-body { padding: 18px 22px 24px; }
.wr-vm-foot {
  position: sticky; bottom: 0; background: var(--canvas);
  display: flex; align-items: center; justify-content: flex-end; gap: 10px;
  padding: 14px 22px; border-top: 1px solid var(--line);
}
.wr-vm-section { margin-bottom: 16px; }
.wr-vm-label {
  margin: 0 0 4px; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--soft);
}

/* ===========================================================
   Reduced motion — the catch-all (v21.13)
   -----------------------------------------------------------
   Before this, reduced motion was handled selector by selector: five blocks
   covering .home-plate-img, .wr-gen-*, .vault-selection, .ask-*, .skeleton.
   The v21.13 sweep found the gap that approach guarantees — .import-spinner
   (import-spin, an infinite rotation) had simply never been added, and of the
   28 `transition` declarations only .vault-sidebar / .vault-sheet-backdrop
   were covered, leaving 14 transform transitions running.

   A per-selector list is only ever as current as the last person to remember
   it, so this is a blanket. The five specific blocks above are left in place:
   they are harmless, and they document intent at the point of use.

   Animations are not cancelled but collapsed to ~0s, so anything that
   animates INTO its visible state (.ask-modal rising into view) still ends up
   visible. Every keyframe in this file ends on its visible state, so there is
   nothing that would be left hidden by this.

   CSS cannot reach `element.scrollIntoView({ behavior: "smooth" })` — the
   option beats the property. That is handled in js/shell.js via
   window.atelierScrollIntoView, which js/studio.js and js/machines.js call.
   =========================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ===========================================================
   Fitting Room — People roster + add/edit-person form (D1a)
   -----------------------------------------------------------
   No photo UI yet (D1b) — .person-photo is a fixed placeholder square,
   reserving the spot D1b will fill with a real <img>. The roster grid
   reuses .item-grid verbatim (already shrink-safe, already in the
   "Shrink safety" list below); the form reuses .card-form verbatim
   (already grid + already collapses to one column at 820px), so this
   section adds no new CSS grid container — every layout below is plain
   flexbox, which is why nothing here needed adding to that list.
   =========================================================== */
.person-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 12px 14px 14px;
  box-shadow: var(--shadow-1);
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.person-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-2); }
/* Keyboard focus must lift the card exactly as hover does — same reasoning
   as .item-card:focus-within / .vault-card:focus-within. */
.person-card:focus-within { transform: translateY(-4px); box-shadow: var(--shadow-2); }

.person-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.person-card-actions { display: flex; gap: 12px; }

/* "Profile" reuses .badge's default accent pairing verbatim (already
   proven-readable, used everywhere else in this app). "Guest" needs its
   own pairing so the two are visibly different, never just a label swap —
   --ink on --line rather than a new brand colour, so it needs no new
   contrast check of its own. */
.person-pill.guest { background: var(--line); color: var(--ink); }

/* The keyboard route into a person — modelled on .item-open, but unlike
   that one this button holds ALL the card's text (name/relationship/sub),
   not just a picture. That is a deliberate choice: .person-card itself
   never gets a click handler (only .person-open does), so this needs no
   entry in tools/audit-a11y.py's REVIEWED list at all — there is nothing
   for CHECK 1 to flag. */
.person-open {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  width: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: inherit;
  text-align: left;
}
.person-open:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--r-xs); }

.person-photo.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r-md);
  background: var(--thumb-bg);
  font-size: 20px;
}
.person-name { font-weight: 700; font-size: 15px; color: var(--ink); }
.person-relationship { font-size: 12.5px; color: var(--soft); }
.person-sub { font-size: 11.5px; color: var(--mut); }

/* ---------- Add/edit-person form ---------- */
/* .person-measurements is a direct .card-form grid child (grid-column: 1/-1,
   the same spanning technique .field.wide uses) but is NOT itself a .field —
   it holds several groups, not one label+input pair, so it needs its own
   flex layout rather than .field's flex-column rule. */
.person-guest-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}
.person-measurements {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* The guest-toggle hides this whole block via the `hidden` ATTRIBUTE, so it
   needs the `hidden`-vs-`display` partner rule — the standing trap noted in
   ATELIER_ROADMAP.md's "Standing notes" section. */
.person-measurements[hidden] { display: none !important; }

.person-group-title { font-family: var(--font-display); font-weight: 600; font-size: 14px; color: var(--ink); }
.person-group-fields { display: flex; flex-wrap: wrap; gap: 12px 16px; margin-top: 6px; }
.person-group-fields .field { flex: 1 1 110px; max-width: 170px; }

/* The "More measurements" disclosure is a native <details>/<summary> on
   purpose — no JS, no `hidden` attribute, no partner rule to remember. */
.person-extended { border-top: 1px solid var(--line); padding-top: 10px; }
.person-extended summary { cursor: pointer; font-weight: 600; color: var(--accent-ink); font-size: 13px; }
.person-extended-fields { margin-top: 10px; }

.person-form-missing { color: var(--red); font-size: 12.5px; min-height: 16px; }

/* ===== Height feet+inches split + How-to-measure trigger (Fitting Room) ===== */
.height-split { display: flex; align-items: center; gap: 6px; }
.height-split input { width: 56px; flex: 0 0 auto; }
.height-unit { font-size: 12.5px; color: var(--soft); white-space: nowrap; }
.person-measure-guide-row { margin: 2px 0 4px; }
.person-group-fields .field.field-height { max-width: 250px; flex: 1 1 210px; }
.req { color: var(--red, #b3402f); margin-left: 2px; font-weight: 700; }

/* ===== How to measure — measurement guide overlay (measure-guide.js) ===== */
.mg-overlay{position:fixed;inset:0;z-index:1000;display:none;align-items:flex-start;justify-content:center;background:rgba(34,28,26,.55);padding:32px 16px;overflow-y:auto}
.mg-overlay.open{display:flex}
.mg-modal{position:relative;background:var(--cream,#F4EFE6);color:var(--ink,#221C1A);border-radius:16px;max-width:1040px;width:100%;padding:28px 28px 36px;box-shadow:0 24px 60px rgba(0,0,0,.35)}
.mg-close{position:absolute;top:14px;right:16px;border:0;background:transparent;font-size:30px;line-height:1;color:var(--muted,#8A7F72);cursor:pointer;padding:4px 8px}
.mg-kicker{font-size:12px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted,#8A7F72);margin:0 0 6px}
.mg-modal h2{font-family:Georgia,serif;font-weight:700;font-size:34px;line-height:1.05;margin:0 0 10px}
.mg-lede{max-width:620px;color:#4A423C;margin:0 0 6px}
.mg-hint{font-size:13px;color:var(--muted,#8A7F72);margin:0 0 22px}
.mg-hint b{color:var(--plum,#463366)}
.mg-grid{display:grid;grid-template-columns:440px 1fr;gap:32px;align-items:start}
@media(max-width:820px){.mg-grid{grid-template-columns:1fr}}
.mg-figwrap{position:sticky;top:0;background:var(--paper,#FBF8F2);border:1px solid var(--line,#E0D8C9);border-radius:14px;padding:14px}
@media(max-width:820px){.mg-figwrap{position:static}}
.mg-toggle{display:flex;gap:4px;background:#EDE5D6;border-radius:999px;padding:4px;width:max-content;margin:0 auto 12px}
.mg-toggle button{border:0;background:transparent;font:inherit;font-size:13px;font-weight:600;color:var(--muted,#8A7F72);padding:6px 18px;border-radius:999px;cursor:pointer}
.mg-toggle button.active{background:var(--plum,#463366);color:#fff}
.mg-figbox{position:relative;width:100%;border-radius:10px;overflow:hidden}
.mg-figbox img{display:block;width:100%;height:auto}
.mg-cap{text-align:center;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted,#8A7F72);margin-top:8px}
.mg-dot{position:absolute;transform:translate(-50%,-50%);width:26px;height:26px;border-radius:50%;background:rgba(70,51,102,.92);color:#fff;border:2px solid #fff;font-weight:700;font-size:12.5px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;box-shadow:0 1px 4px rgba(0,0,0,.35);transition:transform .1s}
.mg-dot:hover,.mg-dot.on{transform:translate(-50%,-50%) scale(1.18);background:var(--plum,#463366);z-index:6}
.mg-figtip{position:absolute;z-index:20;max-width:250px;background:var(--ink,#221C1A);color:#F6F1E7;font-size:12.5px;line-height:1.4;padding:9px 11px;border-radius:9px;box-shadow:0 8px 22px rgba(0,0,0,.25);display:none;pointer-events:none}
.mg-figtip .mg-t{font-weight:700;display:block;margin-bottom:2px}
.mg-htrack{position:absolute;left:3%;top:3%;bottom:4%;width:1px;background:rgba(70,51,102,.55)}
.mg-htrack::before,.mg-htrack::after{content:"";position:absolute;left:-4px;width:9px;height:9px;border-left:1.5px solid var(--plum,#463366);border-top:1.5px solid var(--plum,#463366)}
.mg-htrack::before{top:0;transform:rotate(45deg)} .mg-htrack::after{bottom:0;transform:rotate(-135deg)}
.mg-group{margin:0 0 18px}
.mg-group h3{font-family:Georgia,serif;font-size:12.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--plum,#463366);border-bottom:1px solid var(--line,#E0D8C9);padding-bottom:6px;margin:0 0 6px}
.mg-row{display:flex;align-items:center;gap:8px;padding:8px 2px;border-bottom:1px dotted var(--line,#E0D8C9);position:relative}
.mg-row:last-child{border-bottom:none}
.mg-num{flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:var(--plum,#463366);color:#fff;font-weight:700;font-size:12.5px;display:flex;align-items:center;justify-content:center}
.mg-name{font-weight:600;font-size:15px;flex:0 1 auto}
.mg-back{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#fff;background:var(--muted,#8A7F72);border-radius:4px;padding:1px 6px;margin-left:8px;vertical-align:1px}
.mg-info{flex:0 0 auto;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--plum,#463366);color:var(--plum,#463366);background:transparent;font-size:12px;font-weight:700;font-style:italic;font-family:Georgia,serif;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.15s}
.mg-info:hover,.mg-info.open{background:var(--plum,#463366);color:#fff}
.mg-tip{position:absolute;left:34px;right:auto;top:calc(100% - 2px);z-index:5;width:min(340px,80%);background:var(--ink,#221C1A);color:#F6F1E7;font-size:13px;line-height:1.45;padding:11px 13px;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);display:none}
.mg-tip::before{content:"";position:absolute;left:16px;top:-6px;width:12px;height:12px;background:var(--ink,#221C1A);transform:rotate(45deg)}
.mg-row:hover .mg-tip,.mg-info.open + .mg-tip{display:block}

/* ============ Fitting Room person form — v21.56 restyle (scoped, pf-*) ============ */
.pform{position:relative;background:var(--panel,#fff);border:1px solid var(--line,#e4e3dd);border-radius:14px;padding:24px 24px 22px;box-shadow:0 1px 2px rgba(20,19,26,.04),0 10px 30px rgba(20,19,26,.05);overflow:hidden;margin:0 0 24px}
.pform::before{content:"";position:absolute;top:0;left:0;right:0;height:4px;background:var(--rainbow,linear-gradient(90deg,#ff2e88,#e0322f,#ff7a2e,#ffca2e,#12b58f,#2f7dff,#8a5cff))}
.pform input[type=number]{-moz-appearance:textfield}
.pform input[type=number]::-webkit-outer-spin-button,.pform input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.pform-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin:6px 0 18px}
.pform-kicker{font-family:var(--mono);font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--mut,#6e6a7a)}
.pform-title{font-family:var(--disp);font-weight:800;font-size:26px;line-height:1;text-transform:uppercase;letter-spacing:.01em;margin:2px 0 0;color:var(--ink,#14131a)}
.pf-guest{display:flex;align-items:center;gap:9px;font-family:var(--mono);font-size:12px;color:var(--ink,#14131a);background:var(--panel-2,#faf9f6);border:1px solid var(--line,#e4e3dd);border-radius:8px;padding:9px 12px;width:max-content;margin:0 0 18px;cursor:pointer}
.pf-guest input{accent-color:var(--red,#e0322f)}
.pf-row2{display:grid;grid-template-columns:1fr 1fr;gap:14px 18px;margin:0 0 4px}
@media(max-width:640px){.pf-row2{grid-template-columns:1fr}}
.pf-field{display:flex;flex-direction:column}
.pf-lab{font-family:var(--mono);font-size:9.5px;font-weight:700;letter-spacing:.13em;text-transform:uppercase;color:var(--mut,#6e6a7a);margin:0 0 5px}
.pf-req{color:var(--red,#e0322f);margin-left:3px}
.pf-in{font-family:var(--ui);font-size:14px;color:var(--ink,#14131a);background:var(--panel-2,#faf9f6);border:1px solid var(--line,#e4e3dd);border-radius:8px;padding:9px 12px;width:100%}
.pf-in::placeholder{color:var(--faint,#a3a0ad)}
.pf-in:focus,.pf-hcombo:focus-within,.pf-suffix-wrap:focus-within{outline:none;border-color:var(--red,#e0322f);box-shadow:0 0 0 3px var(--accent-soft,#fbeceb)}
.pf-guide{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--accent-ink,#b8231f);background:var(--accent-soft,#fbeceb);border:1px solid #f2d5d3;border-radius:7px;padding:7px 11px;margin:18px 0 4px;cursor:pointer}
.pf-group{margin:16px 0 0}
.pf-ghead{display:flex;align-items:center;gap:10px;margin:0 0 10px}
.pf-ghead h4{font-family:var(--disp);font-weight:700;font-size:15px;letter-spacing:.05em;text-transform:uppercase;margin:0;color:var(--ink,#14131a)}
.pf-rule{flex:1;height:1px;background:var(--line,#e4e3dd)}
.pf-gfields{display:flex;flex-wrap:wrap;gap:12px 14px}
.pf-gfields .pf-field{width:150px}
.pf-hcombo{display:inline-flex;align-items:center;background:var(--panel-2,#faf9f6);border:1px solid var(--line,#e4e3dd);border-radius:8px;padding:0 10px;height:38px;width:150px}
/* flex:0 0 38px, not just width:38px — a pre-existing global rule
   (input:not([type="checkbox"]):not([type="radio"]), select, textarea {
   min-width:0; width:100% }) out-specifies a plain `.pf-hcombo input{width}`
   (two :not() arguments count as attribute selectors, so it beats a single
   class+tag selector) and silently stretches these inputs to fill the box,
   squeezing "ft"/"in" until they wrap onto two lines. Setting flex-basis
   directly sidesteps the width property (and its specificity fight)
   entirely — verified live: without this, .pf-u measured 6.7px wide and
   wrapped; with it, every piece renders at its natural size. */
.pf-hcombo input{border:0;background:transparent;flex:0 0 38px;width:38px;font-family:var(--ui);font-size:14px;color:var(--ink,#14131a);text-align:right;padding:0;outline:none}
.pf-hcombo .pf-u{font-family:var(--mono);font-size:11px;color:var(--mut,#6e6a7a);margin:0 2px 0 3px;flex:0 0 auto;white-space:nowrap}
.pf-hcombo .pf-hdiv{width:1px;height:18px;background:var(--line,#e4e3dd);margin:0 6px;flex:0 0 auto}
.pf-suffix-wrap{position:relative;width:100%}
.pf-suffix-wrap input{padding-right:28px}
.pf-suffix-wrap .pf-sfx{position:absolute;right:11px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:11px;color:var(--mut,#6e6a7a);pointer-events:none}
.pf-measurements[hidden]{display:none!important}
.pf-more{margin:20px 0 0;border-top:1px solid var(--line,#e4e3dd);padding-top:12px}
.pf-more summary{font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--accent-ink,#b8231f);cursor:pointer;list-style:none}
.pf-more summary::-webkit-details-marker{display:none}
.pf-more summary::before{content:"\25B8  ";color:var(--red,#e0322f)}
.pf-more[open] summary::before{content:"\25BE  "}
.pf-more .pf-gfields{margin-top:12px}
.pf-notes{margin:18px 0 0}
.pf-missing{font-family:var(--mono);font-size:11px;color:var(--red,#e0322f);margin:14px 0 0;min-height:14px}
.pf-brow{display:flex;gap:10px;margin:16px 0 0}
.pf-save{flex:1;font-family:var(--mono);font-weight:700;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:var(--ink,#14131a);color:#fff;border:0;border-radius:9px;padding:13px;cursor:pointer}
.pf-save:disabled{opacity:.5;cursor:not-allowed}
.pf-cancel{font-family:var(--mono);font-weight:700;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:transparent;color:var(--mut,#6e6a7a);border:1px solid var(--line,#e4e3dd);border-radius:9px;padding:13px 22px;cursor:pointer}

/* ===== Fitting Room roster cards — v21.57 photo-forward restyle (pc-*) ===== */
.person-card{position:relative;background:var(--panel,#fff);border:1px solid var(--line,#e4e3dd);border-radius:12px;overflow:hidden;padding:0;gap:0;box-shadow:0 1px 2px rgba(20,19,26,.05);transition:transform .15s ease,box-shadow .15s ease;display:flex;flex-direction:column}
.person-card:hover,.person-card:focus-within{transform:translateY(-4px);box-shadow:0 12px 30px rgba(20,19,26,.12)}
.person-card::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;background:var(--rainbow,linear-gradient(90deg,#ff2e88,#e0322f,#ff7a2e,#ffca2e,#12b58f,#2f7dff,#8a5cff));opacity:0;transition:opacity .15s;z-index:2}
.person-card:hover::before,.person-card:focus-within::before{opacity:1}
.pc-open{display:block;width:100%;padding:0;margin:0;border:0;background:none;cursor:pointer;color:inherit;text-align:left;font:inherit}
.pc-open:focus-visible{outline:none;box-shadow:var(--ring)}
.pc-photo{position:relative;display:flex;align-items:center;justify-content:center;width:100%;aspect-ratio:4/5;background:var(--panel-2,#faf9f6);overflow:hidden}
.pc-photo img{width:100%;height:100%;object-fit:cover;object-position:top center}
.pc-ph{font-size:34px;opacity:.4}
.pc-pill{position:absolute;top:10px;left:10px;font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;padding:3px 8px;border-radius:999px;background:rgba(255,255,255,.92);color:var(--ink,#14131a);z-index:2}
.pc-pill.profile{background:var(--red,#e0322f);color:#fff}
/* .55 alpha measured 4.24:1 white-on-scrim against the actual placeholder
   background that renders today (below the 4.5:1 AA text threshold — 9px
   bold mono is far under the "large text" size exemption). Raised to .7:
   7.06:1 against the placeholder, and still clears 4.5:1 with margin (~7:1)
   against a bright future photo, so this stays safe once D1b wires real
   thumbnails behind it rather than only passing today's flat background. */
.pc-del{position:absolute;top:8px;right:8px;font-family:var(--mono);font-size:9px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#fff;background:rgba(20,19,26,.7);border:0;border-radius:6px;padding:4px 8px;cursor:pointer;opacity:0;transition:opacity .15s;z-index:3}
.person-card:hover .pc-del,.pc-del:focus-visible{opacity:1}
.pc-body{display:block;padding:11px 13px 13px}
.pc-name{display:block;font-family:var(--disp);font-weight:700;font-size:20px;line-height:1.05;text-transform:uppercase;letter-spacing:.01em;color:var(--ink,#14131a)}
.pc-rel{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--mut,#6e6a7a);margin-top:3px}
.pc-sub{display:flex;align-items:center;gap:6px;font-family:var(--ui);font-size:12px;color:var(--mut,#6e6a7a);margin-top:6px}
.pc-dot{width:5px;height:5px;border-radius:50%;background:var(--red,#e0322f);display:inline-block;flex:0 0 auto}
.pc-dot.done{background:var(--green,#12b58f)}

/* ===========================================================
   v21.80 — "Model it" (renamed in v21.81) leaves the card.
   Two faults from v21.78 at once: the button had no CSS at all (it was
   rendering as a raw browser default), and it lived inside .person-card so
   the card and its action read as one object. It is now its own thing
   below the card, in the app's red — the same red the overlay's primary
   wears, so the whole try-on path reads as one action.
   =========================================================== */
.person-slot { display: flex; flex-direction: column; gap: 10px; }
/* The card takes the slack so every button in the row lines up, whatever
   the name length does to card height. */
.person-slot > .person-card { flex: 1; }
.pc-tryon {
  width: 100%;
  font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.08em; text-transform: uppercase;
  padding: 12px 14px;
  background: var(--red); color: #fff; border: 1px solid var(--red);
  border-radius: var(--r-xs); cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.pc-tryon:hover { background: #c72a27; border-color: #c72a27; }
.pc-tryon:focus-visible { outline: none; box-shadow: var(--ring); }

/* ===== Fitting Room photos (D1b) ===== */
.pf-photos-group{margin:16px 0 0}
.pf-photos{display:flex;flex-wrap:wrap;gap:10px}
.pf-thumb{position:relative;width:92px;height:115px;border-radius:9px;overflow:hidden;background:var(--panel-2,#faf9f6);border:1px solid var(--line,#e4e3dd)}
.pf-thumb img{width:100%;height:100%;object-fit:cover;object-position:top center}
/* --mut, not --faint: --faint is this file's own tertiary/decorative token
   ("NOT for live text", see its definition) and measured 2.44:1 on
   --panel-2 — this "…" is live status (a photo is uploading), not chrome.
   --mut measures 4.98:1 on the same surface. */
.pf-thumb-load{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--mut,#6e6a7a)}
.pf-thumb-x{position:absolute;top:4px;right:4px;width:20px;height:20px;border-radius:50%;border:0;background:rgba(20,19,26,.7);color:#fff;font-size:13px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
.pf-add{width:92px;height:115px;border-radius:9px;border:1.5px dashed var(--line,#e4e3dd);background:var(--panel-2,#faf9f6);color:var(--mut,#6e6a7a);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;font-family:var(--mono);font-size:9px;letter-spacing:.08em;text-transform:uppercase}
.pf-add:disabled{opacity:.6;cursor:default}
.pf-add-ic{font-size:22px;line-height:1;opacity:.6}
.pf-photos-hint{font-family:var(--ui);font-size:12px;color:var(--mut,#6e6a7a);margin:10px 0 0}

/* ===== v21.90 — garment photos, in the asset lightbox =====
   Deliberately alongside the Fitting Room's .pf-photos strip above so the two
   read as one family. Entry 0 is the FRONT and is built from the asset row
   itself, never from a garment_photos row — see js/detail.js. */
/* v21.91 — 4px, matching the panel grid's own label-to-control gap. */
.gp-photos-group { display: grid; gap: 4px; }
/* ★ REQUIRED. The rule above sets `display`, which beats the `hidden`
   attribute — the exact trap CLAUDE.md documents six times over. Removing
   this line makes the strip show on every asset type. */
.gp-photos-group[hidden] { display: none; }
/* ★ v22.17 — THE COMPACT STRIP (js/detail.js renderGarmentPhotos). The
   104×130 cards that carried their own ×, Make front and ◀ ▶ (v21.90–v22.7)
   became small thumbnails — 48×60 here, 34×42 in the desktop panel (the
   ≥821px block with .detail-side.tight) — and a tapped photo's controls
   show on ONE line under the strip (.gp-mini-actions). The front keeps an
   ink outline; the chosen photo an accent one. The old card rules
   (.gp-item, .gp-thumb, .gp-cap, .gp-front-pill, .gp-add) went with the
   markup: nothing renders them any more. */
.gp-photos { margin-top: 0; }
.gp-mini-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.gp-mini {
  width: 48px; height: 60px; padding: 0; flex: none; cursor: pointer;
  border: 1px solid var(--line); border-radius: 3px; overflow: hidden; background: var(--panel-2);
}
.gp-mini img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
.gp-mini.front { outline: 2px solid var(--ink); outline-offset: 1px; }
.gp-mini.selected { outline: 2px solid var(--accent); outline-offset: 1px; }
.gp-mini:focus-visible { box-shadow: var(--ring); }
.gp-mini.add { display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--mut); border-style: dashed; }
.gp-mini.add:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); }
.gp-mini.add:disabled { opacity: .6; cursor: default; }
.gp-mini-hint { font-family: var(--mono); font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--mut); margin-left: 4px; }
.gp-mini-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
/* ★ REQUIRED — the rule above sets `display`, and every row but the chosen
   photo's carries `hidden`. Without this, every photo's controls show at
   once. */
.gp-mini-actions[hidden] { display: none; }
.gp-make-front,
.gp-x {
  height: 26px; padding: 0 10px; border-radius: var(--r-xs); cursor: pointer;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--mut);
  font-family: var(--mono); font-size: 9px; letter-spacing: .08em; text-transform: uppercase;
}
.gp-make-front:hover { border-color: var(--ink); color: var(--ink); }
.gp-x:hover { border-color: var(--accent); color: var(--accent-ink); }
/* v22.7 — compact square secondary buttons, Make front's own border and
   panel so the three read as one control group. */
.gp-move { flex: 0 0 auto; width: 26px; height: 26px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-xs); cursor: pointer; border: 1px solid var(--line); background: var(--panel-2); color: var(--mut); font-family: var(--ui); font-size: 10px; line-height: 1; }
.gp-move:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); }
/* No global button:disabled rule exists on purpose (the v21.69 note at
   .shopping-foot .primary:disabled), so this line is required — and it is
   .wr-textbtn:disabled's look from v22.4: --faint, and a plain cursor. */
.gp-move:disabled { color: var(--faint); cursor: default; }
/* v22.24b — the chosen photo's ⟳ (a back or another angle), the stage's
   own glyph and words; square like ◀ ▶. */
.gp-turn { flex: 0 0 auto; width: 26px; height: 26px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-xs); cursor: pointer; border: 1px solid var(--line); background: var(--panel-2); color: var(--mut); font-family: var(--ui); font-size: 14px; line-height: 1; }
.gp-turn:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); }
.gp-turn:disabled, .gp-turn.is-busy { color: var(--faint); cursor: progress; }
/* v22.24b — "Looks like a second copy of …" in picture details. */
.gp-double { margin-top: 10px; padding: 8px 10px; border: 1px solid var(--line); background: var(--panel-2); border-radius: var(--r-xs); }
.gp-double-line { margin: 0; font-size: 12px; line-height: 1.4; color: var(--ink); }
/* v22.17 — shown only while the FRONT is the chosen photo; one line beside
   nothing else, so no top margin. */
.gp-locked { font-family: var(--ui); font-size: 11px; line-height: 1.35; color: var(--mut); margin: 0; max-width: 340px; }

/* The intake overlay's "another angle of ←" row. NOTE: body carries
   `overflow-wrap: anywhere`, inherited everywhere, which is why -txt needs
   min-width: 0 and the name needs nowrap + ellipsis. Do not remove either. */
.wardrobe-attach { display: flex; align-items: center; gap: 12px; width: 100%; margin-top: 18px; padding: 10px 12px; cursor: pointer; text-align: left; border: 1.5px solid var(--line); border-radius: var(--r-md); background: var(--panel-2); color: var(--ink); font: inherit; }
.wardrobe-attach:hover:not(:disabled) { border-color: var(--ink); }
.wardrobe-attach:disabled { cursor: default; opacity: .55; }
.wardrobe-attach-thumb { flex: 0 0 auto; width: 34px; height: 44px; border-radius: var(--r-xs); overflow: hidden; border: 1px solid var(--line); background: var(--thumb-bg); }
.wardrobe-attach-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wardrobe-attach-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.wardrobe-attach-k { font-family: var(--mono); font-size: 9px; letter-spacing: .09em; text-transform: uppercase; color: var(--mut); }
.wardrobe-attach-n { font-size: 14px; font-weight: 600; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* v22.7 — the "A" shortcut, shown on the button itself. Inherits the
   button's colour, so it dims with the rest when the button is disabled. */
.wardrobe-attach kbd { align-self: flex-start; margin-top: 2px; padding: 1px 5px; font-family: var(--mono); font-size: 10px; line-height: 1.3; color: inherit; border: 1px solid var(--line); border-radius: 3px; }

/* v21.69 — the ONE line the approved shop.css does not carry, kept OUTSIDE
   that verbatim block on purpose. "Go Shopping!" is disabled until a person
   is picked and the need has some text, and this stylesheet has no global
   `button:disabled` rule — every disabled control gets its own line
   (.ps-send:disabled, .pf-save:disabled, .tag-pick[disabled]). Without it
   the primary reads as fully live while doing nothing when clicked, which
   is the one state the mock never had to depict because its example had
   both a person and a sentence. Same shape and same numbers as .ps-send. */
.shopping-foot .primary:disabled { opacity: 0.55; cursor: default; }

/* ===========================================================
   v21.69 — the Shopping Brain overlay ("Go Shopping!").
   Follows .wardrobe-overlay's fixed/flex/backdrop shape exactly so the
   two read as one app. Opened/closed by the .open CLASS — never the
   `hidden` attribute. Chips reuse .tag-pick-cloud/.tag-pick verbatim.
   =========================================================== */
.shopping-overlay {
  position: fixed; inset: 0; z-index: 10065; display: none;
  align-items: flex-start; justify-content: center;
  background: rgba(12, 8, 18, 0.6); padding: 30px 20px; overflow: auto;
}
.shopping-overlay.open { display: flex; }
.shopping-modal {
  position: relative; background: var(--panel); margin: auto;
  width: min(860px, 96vw); max-height: 90vh; overflow: auto;
  border-radius: var(--r-xl); padding: 28px 30px;
  box-shadow: var(--shadow-2);
}
.shopping-close {
  position: absolute; top: 14px; right: 18px; z-index: 3;
  background: none; border: none; cursor: pointer;
  font-size: 26px; line-height: 1; color: var(--soft);
}
.shopping-close:hover { color: var(--ink); }

.shopping-head { padding-right: 28px; margin-bottom: 4px; }
.shopping-title { font-family: var(--disp); font-weight: 800; font-size: 34px; line-height: 1.02; text-transform: uppercase; color: var(--ink); }
.shopping-sub { font-size: 13.5px; color: var(--mut); margin: 6px 0 22px; }

/* ---- Screen 1: the ask ---- */
.shop-label { font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut); display: block; margin-bottom: 8px; }
.shop-sect { margin-bottom: 22px; }
.shop-badge { font-family: var(--mono); font-size: 10px; opacity: .75; }
.tag-pick.is-guest .shop-badge { color: var(--orange-ink); opacity: 1; }
.tag-pick[disabled] { opacity: .4; cursor: not-allowed; }

.shop-need {
  width: 100%; box-sizing: border-box; resize: vertical;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--panel-2); color: var(--ink);
  font-family: var(--ui); font-size: 19px; line-height: 1.45;
  padding: 16px 18px; min-height: 96px;
}
.shop-need::placeholder { color: var(--faint); }
.shop-need:focus-visible { outline: none; border-color: var(--ink); box-shadow: var(--ring); }
.shop-count { font-family: var(--mono); font-size: 11px; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); margin-top: 10px; }

.shopping-foot { display: flex; align-items: center; gap: 12px; margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.shopping-spacer { flex: 1; }

/* ---- Screen 2: the look ---- */
.shop-look + .shop-look { margin-top: 26px; padding-top: 24px; border-top: 1px solid var(--line); }
.shop-look-who { font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--red); margin-bottom: 4px; }
.shop-look-title { font-family: var(--disp); font-weight: 800; font-size: 27px; line-height: 1.05; color: var(--ink); }
.shop-look-summary { font-size: 14px; line-height: 1.55; color: var(--mut); margin: 8px 0 18px; max-width: 62ch; }

.shop-group { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); display: flex; align-items: center; gap: 10px; margin: 0 0 8px; }
.shop-group::after { content: ""; flex: 1; height: 1px; background: var(--line2); }
.shop-group.tailor { color: var(--orange-ink); margin-top: 18px; }
.shop-group.check { color: var(--faint); margin-top: 18px; }

.shop-piece { display: grid; grid-template-columns: 62px 1fr; gap: 14px; align-items: start; padding: 10px 0; border-top: 1px solid var(--line2); cursor: pointer; min-width: 0; }
.shop-piece:first-of-type { border-top: none; }
.shop-piece:hover .shop-piece-name { text-decoration: underline; }
.shop-piece-thumb { width: 62px; aspect-ratio: 3 / 4; border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--thumb-bg); }
.shop-piece-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shop-piece-main { min-width: 0; }
.shop-piece-top { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.shop-piece-name { font-weight: 700; font-size: 14.5px; color: var(--ink); }
.shop-piece-role { font-family: var(--mono); font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: var(--faint); }
.shop-piece-note { font-size: 13px; line-height: 1.5; color: var(--mut); margin-top: 4px; }
.shop-piece-alt { font-size: 13px; line-height: 1.5; color: var(--orange-ink); font-style: italic; margin-top: 3px; }

.shop-fit { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; padding: 3px 9px; border-radius: var(--r-pill); border: 1px solid transparent; white-space: nowrap; }
.fit-fits     { background: rgba(18,181,143,.14); color: var(--green-ink); }
.fit-wearable { background: rgba(47,125,255,.12); color: var(--blue-ink); }
.fit-tailor   { background: rgba(255,122,46,.16); color: var(--orange-ink); border-color: rgba(191,69,0,.35); }
.fit-nofit    { background: rgba(224,50,47,.13); color: var(--red); border-color: rgba(224,50,47,.4); }
.fit-unknown  { background: var(--panel-2); color: var(--mut); border-color: var(--line); }

/* ---- Make before buy — its own band, equal weight to the look ---- */
.shop-mbb { margin-top: 26px; padding: 22px 22px 20px; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel-2); }
.shop-mbb-head { font-family: var(--disp); font-weight: 800; font-size: 22px; letter-spacing: .01em; text-transform: uppercase; color: var(--ink); margin-bottom: 3px; }
.shop-mbb-rule { height: 3px; border-radius: 2px; background: var(--rainbow); margin: 0 0 16px; }
.shop-make { display: grid; grid-template-columns: 62px 1fr; gap: 14px; align-items: start; cursor: pointer; min-width: 0; }
.shop-make + .shop-make { margin-top: 14px; }
.shop-make-thumb { width: 62px; aspect-ratio: 3 / 4; border: 1px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--thumb-bg); }
.shop-make-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shop-make-name { font-weight: 700; font-size: 14.5px; color: var(--ink); }
.shop-make-flag { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--purple-ink); margin-top: 2px; }
.shop-make-why { font-size: 13px; line-height: 1.5; color: var(--mut); margin-top: 4px; }
.shop-buys-wrap { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
.shop-buys { margin: 0; padding: 0; list-style: none; }
.shop-buys li { font-size: 13px; line-height: 1.5; color: var(--mut); padding-left: 16px; position: relative; }
.shop-buys li + li { margin-top: 6px; }
.shop-buys li::before { content: "·"; position: absolute; left: 4px; color: var(--faint); }
.shop-buys-lab { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--faint); margin-bottom: 8px; }

/* ---- What it could not judge ---- */
.shop-honesty { margin-top: 20px; padding: 14px 16px; border-left: 3px solid var(--line); background: transparent; }
.shop-honesty-lab { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--faint); margin-bottom: 5px; }
.shop-honesty p { font-size: 13px; line-height: 1.55; color: var(--mut); margin: 0; }

/* ---- v21.84 (D3) — the Workroom handoff line ----
   The footer is already a wrapping flex row, so the button needs nothing of
   its own; only the quiet count beside it does. */
.shop-handoff-note { font-family: var(--mono); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); }

/* ===========================================================
   v21.70 — Design Studio result-card controls + the Vault rail scroll.
   Three groups, all cosmetic or wiring. No schema change.
   =========================================================== */

/* ---- 1. Dismissing a card ---------------------------------
   An ✕ floats top-right ON the image, and a Close button joins Download.
   Both do the same thing; the ✕ is discoverable, the button is reachable
   after you have scrolled down the card. Neither is Delete — Delete
   destroys the asset and its Storage files, so it stays visually apart. */
.review-imgwrap { position: relative; }
.review-close {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  width: 28px; height: 28px; padding: 0; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%; cursor: pointer;
  background: rgba(20, 19, 26, 0.55); color: #fff;
  font-size: 15px; backdrop-filter: blur(3px);
}
.review-close:hover { background: rgba(20, 19, 26, 0.8); }
.review-close:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---- 2. Saved state ---------------------------------------
   Save changes goes green on a successful save and returns to ink the
   moment any field changes — it is a repeatable ACTION, so it needs a
   state. Keep/Delete is a DECISION, so once the card is saved the whole
   row disappears instead, mirroring v21.60's Status row in the Vault
   detail popup. Delete then lives in the Vault, where destructive things
   belong. */
.review-save.saved {
  background: var(--green-ink); border-color: var(--green-ink); color: #fff;
}
.review-save.saved:hover { background: var(--green-ink); border-color: var(--green-ink); }
.review-decisions[hidden] { display: none !important; }

/* ---- 3. The Vault rail scrolls on its own ------------------
   .vault-sidebar is position:sticky with no height bound, so once the
   collection list outgrows the window its bottom is unreachable. Scoped
   to desktop with min-width, NOT added to the base rule: below 1100px the
   same element is a horizontal chip strip and below 820px it is a bottom
   sheet, and a max-height added to the base rule would break both. */
@media (min-width: 1101px) {
  .vault-sidebar {
    max-height: calc(100vh - 36px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* ===========================================================
   C4 · Q1 — the quantity stepper.
   Additive. `metadata.quantity` stays a free-text string; the stepper is
   a way to nudge the number in it, never a replacement for typing.
   =========================================================== */

/* ---- Detail panel: the field grows two buttons -------------
   renderTypeFields() gains a fourth branch (`f.stepper`) beside options /
   textarea / suggestions. The input keeps its id, its value and its
   free-text nature — a stepper is decoration on a text field, not a
   number input, because "2 packs" has to survive being typed. */
.qty-row { display: flex; align-items: stretch; gap: 6px; width: 100%; }
.qty-row input[type="text"] { flex: 1; min-width: 0; }
.qty-step {
  flex: 0 0 auto; width: 34px; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--panel-2); color: var(--ink);
  font-family: var(--mono); font-size: 15px; font-weight: 700; line-height: 1;
}
.qty-step:hover { border-color: var(--ink); }
.qty-step:focus-visible { outline: none; box-shadow: var(--ring); }
.qty-step[disabled] { opacity: .35; cursor: default; }
/* Hidden — NOT disabled — on a type with no quantity field, and on a value
   the parser could not read. Both partner rules live here so neither can
   become the display-vs-hidden trap. */
.qty-row[hidden] { display: none !important; }
.qty-step[hidden] { display: none !important; }

/* The quiet line under an unreadable value. It is not an error: the entry
   is kept exactly as typed and the field still edits by hand. */
.qty-freeform {
  font-family: var(--mono); font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint); margin-top: 4px;
}

/* ---- Vault card: the same control, small -------------------
   Sits under the frame with the title, never over the artwork — the
   category-frame rule (v21.7.5) exists because nothing covers the picture. */
.vault-qty {
  display: flex; align-items: center; gap: 5px; margin-top: 5px;
}
.vault-qty-n {
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  color: var(--ink); min-width: 2ch; text-align: center;
}
.vault-qty-raw {
  font-family: var(--mono); font-size: 10px; color: var(--mut);
}
.vault-qty .qty-step { width: 22px; height: 22px; font-size: 12px; }
.vault-qty[hidden] { display: none !important; }

/* ===========================================================
   D4 · try-on. One garment, one person, one render.
   Overlay follows .wardrobe-overlay's shape; opened by the .open
   CLASS, never the `hidden` attribute.
   =========================================================== */
.tryon-overlay {
  position: fixed; inset: 0; z-index: 10065; display: none;
  align-items: flex-start; justify-content: center;
  background: rgba(12, 8, 18, 0.6); padding: 30px 20px; overflow: auto;
}
.tryon-overlay.open { display: flex; }
.tryon-modal {
  position: relative; background: var(--panel); margin: auto;
  width: min(820px, 96vw); max-height: 90vh; overflow: auto;
  border-radius: var(--r-xl); padding: 28px 30px; box-shadow: var(--shadow-2);
}
.tryon-close {
  position: absolute; top: 14px; right: 18px; z-index: 3;
  background: none; border: none; cursor: pointer;
  font-size: 26px; line-height: 1; color: var(--soft);
}
.tryon-title { font-family: var(--disp); font-weight: 800; font-size: 32px; line-height: 1.02; text-transform: uppercase; }
.tryon-sub { font-size: 13.5px; color: var(--mut); margin: 6px 0 22px; }
.tryon-lab { font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--mut); display: block; margin-bottom: 8px; }
.tryon-sect { margin-bottom: 22px; }
/* v21.79 — the pickers need air under them; the hint was sitting on top of
   whatever came next and the whole panel read as one dense block. */
.tryon-pick { margin-bottom: 12px; }

/* The two inputs, side by side — a photo of the person, a garment. */
.tryon-pick { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.tryon-strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.tryon-thumb {
  flex: 0 0 auto; width: 62px; aspect-ratio: 3/4; padding: 0; cursor: pointer;
  border: 2px solid var(--line); border-radius: var(--r-sm); overflow: hidden;
  background: var(--thumb-bg);
}
.tryon-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tryon-thumb.on { border-color: var(--red); }
.tryon-hint { font-size: 12.5px; color: var(--mut); margin: 10px 0 0; }

/* The result. Before and after, same size, so the comparison is honest. */
.tryon-result { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-top: 4px; }
.tryon-pane { }
.tryon-pane-lab { font-family: var(--mono); font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); margin-bottom: 7px; }
.tryon-img {
  width: 100%; aspect-ratio: 2/3; object-fit: cover; display: block;
  border: 3px solid transparent; border-image: var(--rainbow) 1; background: var(--thumb-bg);
}
.tryon-cost { font-family: var(--mono); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); margin-top: 12px; }
.tryon-foot { display: flex; align-items: center; gap: 12px; margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.tryon-spacer { flex: 1; }


/* ===========================================================
   v21.79 — three fixes after the first real render.
   =========================================================== */

/* 1. The primary was ordinary-sized and Josh did not see it. It is the one
      thing this overlay exists to have you press, and it spends money, so
      it gets weight the way .primary.create already does for Generate. */
.tryon-foot .primary {
  font-size: 14px; padding: 15px 34px; letter-spacing: 0.06em;
  background: var(--red); border-color: var(--red);
}
.tryon-foot .primary:hover:not(:disabled) { background: #c72a27; border-color: #c72a27; }
.tryon-foot .primary:disabled { background: var(--faint); border-color: var(--faint); }

/* 2. Room under the pickers before the result or the working card. */
.tryon-sect + .tryon-sect,
#tryon-result-host > .tryon-sect { margin-top: 26px; }

/* 3. The waiting state is now the app's SHARED "AI is working" card,
      .wr-generating — the rainbow-topped panel the Workroom and the Design
      Studio already use. No second spelling of the same idea. */
.tryon-working { margin: 4px 0 2px; }
.tryon-working .tryon-stage { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut); }


/* ===========================================================
   v21.82 — the picker greys out what the engine cannot wear.
   =========================================================== */
.tryon-thumb:disabled { opacity: 0.34; cursor: not-allowed; filter: grayscale(1); }
.tryon-thumb:disabled:hover { border-color: var(--line); }
.tryon-note {
  font-size: 12.5px; color: var(--mut); margin: 10px 0 0;
  padding-left: 11px; border-left: 2px solid var(--line);
}


/* ===========================================================
   v21.83 — layering. The badge says WHICH ORDER, because the
   endpoint wears pieces one at a time and the order changes
   the render.
   =========================================================== */
/* MEASURED, not assumed. The badge sits at -6px, and three things were
   eating it — verified by hit-testing the painted pixels, because
   getBoundingClientRect() reports layout boxes and cannot see clipping:
     1. .tryon-thumb had `overflow: hidden`, which clipped the badge to the
        button and left only its inner quarter visible. That hidden was there
        to round the <img>'s square corners, so the rounding moves ONTO the
        img (3px radius minus the 2px border = 1px) and the button goes
        visible. The img fills the padding box exactly, so nothing else can
        escape now that it may.
     2. .tryon-strip's `overflow-x: auto` forces the Y axis to `auto` too,
        which clipped the 4px the badge rises above the strip — hence
        padding-top.
     3. Scrolled to the end, the last thumb's badge overhangs the strip's
        right edge — hence padding-right.
   position: relative itself changes NO layout: the thumb is `flex: 0 0 auto`
   with a fixed width and aspect-ratio, and measured 62x83 both before and
   after, with the img unchanged. */
.tryon-thumb { position: relative; overflow: visible; }
.tryon-thumb img { border-radius: 1px; }
.tryon-strip { padding-top: 8px; padding-right: 8px; }
.tryon-ord {
  position: absolute; top: -6px; right: -6px; z-index: 2;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--red); color: #fff;
  font-family: var(--mono); font-size: 10px; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 4px rgba(20, 19, 26, 0.35);
}
.tryon-cost-pre {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--faint); margin-top: 14px;
}


/* ===========================================================
   v21.85 (F1) — the moodboard pinboard.
   A FREEFORM canvas of typed cards, not a grid. Cards are positioned in
   PERCENT so the markup is literally the normalised model the export and
   (in v21.86) persistence both key to — no pixel conversion anywhere.
   =========================================================== */
.mb-overlay {
  position: fixed; inset: 0; z-index: 10070; display: none;
  align-items: flex-start; justify-content: center;
  background: rgba(12, 8, 18, 0.66); padding: 24px 18px; overflow: auto;
}
.mb-overlay.open { display: flex; }
.mb-modal {
  position: relative; background: var(--panel); margin: auto;
  width: min(1180px, 97vw); border-radius: var(--r-xl);
  padding: 24px 26px 26px; box-shadow: var(--shadow-2);
}
.mb-close {
  position: absolute; top: 14px; right: 18px; z-index: 3;
  background: none; border: none; cursor: pointer;
  font-size: 26px; line-height: 1; color: var(--soft);
}
.mb-head { margin-bottom: 14px; }
.mb-title { font-family: var(--disp); font-weight: 800; font-size: 30px; line-height: 1.02; text-transform: uppercase; }
/* Nothing is saved yet, and that is said at the top rather than discovered. */
.mb-warn { font-family: var(--mono); font-size: 11px; letter-spacing: .05em; color: var(--red); margin: 7px 0 0; }

.mb-bar { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
.mb-bar-sp { flex: 1; }
.mb-btn {
  font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; padding: 9px 13px; cursor: pointer;
  background: var(--panel); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r-xs);
}
.mb-btn:hover { border-color: var(--ink); }
/* ★ v21.86a — the Save button IS the indicator: red while anything is
   unsaved, green once the board is safely in the Vault. The green is the
   app's own --green token, not a new colour. */
/* .primary.create is TWO classes (0,2,0); a bare .mb-saved is one, so it
   would lose the background to red in any moment both are on the element.
   Matched specificity, and the plain .mb-saved kept for any non-.primary use. */
.primary.mb-saved, .primary.mb-saved:hover, .mb-saved, .mb-saved:hover {
  background: var(--green); border-color: var(--green); color: #fff;
}
.mb-btn:disabled { opacity: .45; cursor: not-allowed; }
/* ★ v21.86a — "SIZE" was rendering as "SI / ZE" on two lines. The cause is
   not local: `body { overflow-wrap: anywhere }` (line ~4281) is inherited by
   everything, and it drops a text run's min-content width to ZERO — so
   `min-width: auto` no longer protects a flex item, and the label's default
   flex-shrink: 1 let the <select> beside it squeeze the word apart
   mid-syllable. Measured at 18.6px wide against a natural 29.27px. The label
   is a two-word-max mono kicker, so it simply may not wrap or shrink. */
.mb-ctl { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--mut); }
.mb-ctl > span { white-space: nowrap; flex: none; }
.mb-sel { font-family: var(--mono); font-size: 11px; padding: 7px 9px; border: 1px solid var(--line); border-radius: var(--r-xs); background: var(--panel); color: var(--ink); }
.mb-export { font-size: 12px; padding: 11px 22px; letter-spacing: .06em; }

/* The "+ From Vault" tray — closed until asked for. */
.mb-tray { display: none; gap: 7px; overflow-x: auto; padding: 12px 0 4px; }
.mb-tray.open { display: flex; }
.mb-tray-thumb { flex: 0 0 auto; width: 52px; aspect-ratio: 3/4; padding: 0; cursor: pointer; border: 2px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--thumb-bg); }
.mb-tray-thumb:hover { border-color: var(--red); }
.mb-tray-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mb-tray-empty { font-family: var(--mono); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); }

/* ★ A FIXED 3:2 working area — no pan, no zoom. Against an infinite canvas a
   normalised coordinate means nothing. */
.mb-canvas-wrap { margin-top: 14px; }
.mb-canvas {
  position: relative; width: 100%; aspect-ratio: 3 / 2; overflow: hidden;
  background: var(--panel-2); border-radius: var(--r-sm);
  /* An OUTLINE, not a border. A border would sit outside the padding box that
     percentage-positioned children resolve against, so every normalised
     coordinate would land ~1px off — which the export magnifies to ~5px at
     2048. Measured; see the v21.85 log entry. outline costs no layout. */
  outline: 1px solid var(--line); outline-offset: -1px;
  touch-action: none; user-select: none;
}

.mb-card { position: absolute; cursor: grab; }
.mb-card:active { cursor: grabbing; }
/* ★ v21.85c — SELECTION IS UI STATE, NOT CONTENT.
   A rainbow ring rather than a red outline (`outline` cannot take a gradient,
   so this is a border-image on a pseudo-element, the same technique
   .tryon-img already uses). It reads the --rainbow token, never a retyped
   copy of the stops.

   It lives at inset 0 — ON the card's own edge, not outside it — so it can
   never be clipped by .mb-palette's overflow or by the canvas at a board
   edge, the same reasoning as the controls.

   ★ It CANNOT reach the export: renderExport() draws from the item model and
   has no idea which card is selected. That is exactly why this is simpler
   than the toggle it replaces. */
.mb-card.sel::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 5;
  border: 2px solid transparent; border-image: var(--rainbow) 1;
}
/* ★ v21.85b — THE MAT IS OUTSIDE THE PICTURE.
   The padding is still interior to the ELEMENT, but the element is now the
   mat box: js/moodboard.js's box() grows an image item's stored rect by MAT
   on every side, so the content box is exactly the stored rect and
   `object-fit: cover` has nothing left to crop. Percentage padding resolves
   against the container's WIDTH on all four sides, which is what keeps the
   mat visually even on a 3:2 board. 0.9% matches MAT in the JS. */
.mb-image { background: #fff; padding: 0.9%; box-shadow: 0 6px 18px rgba(20, 19, 26, 0.22); }
.mb-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mb-img-missing { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: var(--thumb-bg); font-family: var(--mono); font-size: 9px; text-transform: uppercase; color: var(--faint); }
.mb-note { background: #fdf6df; padding: 13px; border-radius: 3px; box-shadow: 0 5px 14px rgba(20, 19, 26, 0.18); }
.mb-note-text { width: 100%; font-size: 13px; line-height: 1.4; color: var(--ink); outline: none; cursor: text; overflow-wrap: anywhere; }
/* v21.85a — say what the card is FOR, not what to do to it. */
.mb-note-text:empty::before { content: "a note to yourself"; color: var(--faint); }
.mb-palette { background: var(--panel); padding: 13px; border-radius: 3px; box-shadow: 0 5px 14px rgba(20, 19, 26, 0.18); border: 1px solid var(--line); overflow: hidden; }
.mb-pal-lab { font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); margin-bottom: 9px; }
.mb-pal-row { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; }
.mb-pal-chip { width: 15px; height: 15px; border-radius: 3px; flex: 0 0 auto; box-shadow: inset 0 0 0 1px rgba(20, 19, 26, .12); }
.mb-pal-name { flex: 1; font-size: 11px; color: var(--ink); text-transform: capitalize; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mb-pal-pct { font-family: var(--mono); font-size: 10px; color: var(--mut); }
.mb-pal-empty { font-size: 11px; line-height: 1.45; color: var(--mut); margin: 0; }

/* ★ v21.85a — INSIDE the card, never overhanging it.
   The × was at top:-8px;left:-8px and the grip at -5px, so .mb-palette's own
   `overflow: hidden` clipped the × away entirely (hit-tested: every sample
   point returned the canvas behind it) and .mb-canvas clipped whatever
   crossed the board's edge. No choice of overflow fixes an overhanging
   control, because a card can always be dragged into a corner. */
/* ★ v21.85b — THREE CONTROLS, ONE FAMILY. One disc, one white stroke weight,
   one arrowhead shape; only the glyph differs. All at POSITIVE offsets inside
   the card — the v21.85a rule, which is what stops any of them being clipped
   when a card is dragged to a board edge. */
.mb-ctrl {
  position: absolute; width: 22px; height: 22px; padding: 0; z-index: 6;
  border: none; border-radius: 50%; cursor: pointer;
  background: rgba(20, 19, 26, .72); backdrop-filter: blur(2px);
  display: none; align-items: center; justify-content: center;
  box-shadow: 0 1px 4px rgba(20, 19, 26, .3);
}
.mb-ctrl svg { width: 14px; height: 14px; display: block; fill: none; stroke: #fff; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.mb-ctrl:hover { background: rgba(20, 19, 26, .92); }
/* ★ v21.85c — TOP-LEFT IS RESERVED AND MUST STAY EMPTY. Pinterest's browser
   extension paints its own Save button over the top-left of every image on the
   page; it sits above everything and no z-index reaches it, so anything we put
   there is simply covered. Rotate moved here, to the bottom-left. */
.mb-rot  { bottom: 5px; left: 5px; cursor: grab; }
.mb-rot:active { cursor: grabbing; }
.mb-x    { top: 5px; right: 5px; }
.mb-grip { bottom: 5px; right: 5px; cursor: nwse-resize; }

/* Reveal behaviour is UNCHANGED from v21.85a, which Josh asked to keep:
   × and resize on hover-or-selected, rotate on selected only. */
.mb-card:hover .mb-x, .mb-card.sel .mb-x,
.mb-card:hover .mb-grip, .mb-card.sel .mb-grip { display: flex; }
.mb-card.sel .mb-rot { display: flex; }

/* v21.85a — the dashed export frame is GONE. It cropped what the Vault's own
   crop tool already crops, and it sat over the cards eating their clicks.
   Export is the whole board; this line just says how big that will be. */
.mb-dims { font-family: var(--mono); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); margin: 9px 0 0; }

@media (max-width: 820px) {
  .mb-modal { padding: 18px 14px 20px; }
  .mb-bar-sp { display: none; }
}

/* ── v21.92 · reading a pattern envelope ──────────────────────────────
   The card's own progress report while the vision call is out. Applied
   IN PLACE by js/pattern-scan.js — never by a grid re-render, which
   would rebuild every card from `vaultSelected` and lose the selection
   Josh just made (the same reason js/quantity.js syncs in place).
   ★ Nothing here sets `display`, so no [hidden] partner rule is needed
   — confirmed with tools/audit-a11y.py, not assumed. */
.vault-scanning .vault-photo { filter: saturate(.35) brightness(.94); }

.vault-scan-bar {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: rgba(255,255,255,.35); overflow: hidden;
}
.vault-scan-bar::after {
  content: ""; position: absolute; inset: 0; width: 45%;
  background: var(--rainbow);
  animation: vault-scan-slide 1.1s linear infinite;
}
@keyframes vault-scan-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(320%); }
}

/* The title's placeholder while there is no title to show yet. */
.vault-shim {
  height: 12px; border-radius: 3px;
  background: linear-gradient(90deg, #e8e5de, #f3f1ec, #e8e5de);
}
.vault-shim + .vault-shim { width: 45%; margin-top: 6px; }
.vault-scan-reading {
  font-family: var(--mono); font-size: 8.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--mut); margin-top: 7px;
}

/* Failure is a finished state, not a stuck card: a red edge and one
   plain line saying what to do. The piece still saved as a Pattern. */
.vault-scan-failed .vault-card-body { border-top: 2px solid var(--red); }
.vault-scan-warn {
  font-family: var(--ui); font-size: 11px; line-height: 1.35;
  color: var(--mut); margin: 5px 0 0;
}

/* The bar is decoration; the "Reading envelope…" line carries the
   meaning, so stilling it loses nothing. */
@media (prefers-reduced-motion: reduce) {
  .vault-scan-bar::after { animation: none; }
}

/* ── v21.94 · the pattern-lookup banner + its card states ─────────────
   Offers to fill in the patterns whose description is blank but whose
   brand and number are both known. */
.pl-banner {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 13px 16px; margin-bottom: 14px;
  box-shadow: var(--shadow-1, 0 6px 18px rgba(0,0,0,.10));
}
.pl-banner::before {
  content: ""; width: 4px; align-self: stretch;
  border-radius: 2px; background: var(--rainbow);
}
/* ★ REQUIRED. .pl-banner sets display:flex, which beats the `hidden`
   attribute — the trap CLAUDE.md documents repeatedly, and the reason
   tools/audit-a11y.py gates it. */
.pl-banner[hidden] { display: none; }

.pl-txt { font-family: var(--ui); font-size: 13px; color: var(--ink); line-height: 1.4; }
.pl-sub { font-family: var(--ui); font-size: 11.5px; color: var(--mut); margin-top: 2px; }
.pl-sp  { flex: 1; }

/* The "Unverified" chip on a looked-up description.
   ★ #8a6d3b was specified and MEASURED AT 4.161:1 on #f3ede2 — below AA's
   4.5 for small text, and this is 8px uppercase, the smallest type in the
   app. Darkened one step to #7a5f33 = 5.131:1. Same warm brown, passes. */
.vault-unver {
  font-family: var(--mono); font-size: 8px; letter-spacing: .09em;
  text-transform: uppercase; background: #f3ede2; color: #7a5f33;
  padding: 2px 6px; border-radius: 3px;
}
.vault-lookup-note {
  font-family: var(--ui); font-size: 11px; line-height: 1.35;
  color: var(--mut); margin: 5px 0 0;
}
/* Nothing found is a quiet, finished state — a grey rule, not the red
   one .vault-scan-failed uses. Nothing went wrong; there was just no page. */
.vault-lookup-none .vault-card-body { border-top: 2px solid var(--line); }

/* ===========================================================
   v22.25 — Workroom Mode (js/workroom-mode.js)
   One construction step at a time, full screen, for the phone at the
   sewing table. Ported from mocks/v22.25-workroom-mode/wm.css (approved
   2026-10-18) with existing tokens only. Layers: .wm 10090 sits above the
   phone tab bar and below toasts (10091); its two sheets are 10095/10096.
   The picture details are never layered over it — the mode steps aside
   while they are open (see openPicture() in the JS).
   =========================================================== */

/* ---------- the door, in the Workroom page's rail ---------- */
.wm-door {
  position: relative; background: var(--panel); border: 1px solid var(--ink); box-shadow: var(--sh-2);
  padding: 16px 18px 18px; overflow: hidden;
}
.wm-door::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--rainbow); }
/* "NEW" until Workroom Mode is first opened (localStorage atelier-wm-seen-v1). */
.wm-door.wm-new::after {
  content: "NEW"; position: absolute; top: 10px; right: 10px; font-family: var(--mono); font-size: 8.5px;
  font-weight: 700; letter-spacing: 0.12em; color: #fff; background: var(--accent); padding: 2px 5px;
}
.wm-door-kicker { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut); }
.wm-door-title {
  font-family: var(--disp); font-weight: 800; font-size: 30px; line-height: 1; text-transform: uppercase;
  letter-spacing: -0.01em; margin: 6px 0 4px; color: var(--ink);
}
.wm-door-sub { font-size: 13.5px; color: var(--mut); margin: 0 0 12px; }
.wm-door .wr-progress { margin: 0 0 14px; }
.wm-door .primary.wm-go {
  width: 100%; min-height: 54px; display: flex; align-items: center; justify-content: center; gap: 10px;
  white-space: nowrap; letter-spacing: 0.04em; font-size: 15px;
}

/* ---------- the mode: a full-screen layer over the app ---------- */
body.wm-on { overflow: hidden; }
.wm {
  position: fixed; inset: 0; z-index: 10090; background: var(--panel); color: var(--ink);
  display: flex; flex-direction: column; padding-top: env(safe-area-inset-top, 0px);
}
.wm:focus { outline: none; }
.wm::before { content: ""; height: 4px; background: var(--rainbow); flex: none; }
.wm-top { flex: none; display: flex; align-items: center; gap: 10px; padding: 10px 12px 8px 14px; }
.wm-look { flex: none; width: 40px; height: 50px; object-fit: cover; border: 1px solid var(--line); background: var(--thumb-bg); }
.wm-look-text { min-width: 0; flex: 1; }
.wm-kicker { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut); }
.wm-lookname {
  font-family: var(--disp); font-weight: 800; font-size: 19px; line-height: 1.02; text-transform: uppercase;
  letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; color: var(--ink);
}
.wm-close {
  flex: none; width: 44px; height: 44px; min-height: 0; padding: 0; border: 1px solid var(--line);
  background: var(--panel-2); font-size: 20px; line-height: 1; color: var(--ink); border-radius: var(--r-sm); cursor: pointer;
}
.wm-close:hover { border-color: var(--ink); }
.wm-close:focus-visible { outline: none; box-shadow: var(--ring); }
/* Shown ONLY while a screen wake lock is actually held. */
.wm-awake {
  display: inline-flex; align-items: center; gap: 5px; font-family: var(--mono); font-size: 9.5px;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--green-ink);
}
.wm-awake i { width: 7px; height: 7px; border-radius: 50%; background: var(--green); display: inline-block; }
.wm-stage { flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* where you are: "Step 13 of 19 ▾" + one segment per step */
.wm-where { flex: none; padding: 0 14px; }
.wm-stepbtn {
  min-height: 44px; padding: 0; border: 0; background: none; font-family: var(--mono); font-weight: 700;
  font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--red); cursor: pointer;
}
.wm-stepbtn:focus-visible { outline: none; box-shadow: var(--ring); }
.wm-segs { display: flex; gap: 3px; }
.wm-segs i { flex: 1; height: 6px; background: var(--line); }
.wm-segs i.done { background: var(--green); }
.wm-segs i.now { background: var(--red); }

/* phone: one column, the look stays in the top bar */
.wm-split { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.wm-left { display: none; }
.wm-right { flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* the step */
.wm-body {
  flex: 1; min-height: 0; overflow-y: auto; padding: 18px 18px 12px;
  touch-action: pan-y pinch-zoom; overscroll-behavior: contain;
}
.wm-numrow { display: flex; align-items: flex-end; gap: 12px; }
.wm-num { font-family: var(--disp); font-weight: 900; font-size: 64px; line-height: 0.85; color: var(--red); letter-spacing: -0.02em; }
.wm-isdone {
  display: inline-flex; align-items: center; font-family: var(--mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--green-ink); padding-bottom: 4px;
}
.wm-text { font-size: 23px; line-height: 1.38; font-weight: 500; margin: 12px 0 0; letter-spacing: -0.005em; overflow-wrap: anywhere; }
.wm-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.wm-chip {
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 6px 12px 6px 8px; box-sizing: border-box;
  border: 1px solid var(--line); background: var(--panel-2); font: inherit; font-size: 13.5px; font-weight: 600; color: var(--ink);
  border-radius: var(--r-sm); text-align: left; max-width: 100%;
}
.wm-chip > span:last-child { min-width: 0; overflow-wrap: anywhere; }
button.wm-chip { cursor: pointer; }
button.wm-chip:hover { border-color: var(--ink); }
button.wm-chip:focus-visible { outline: none; box-shadow: var(--ring); }
.wm-chip .ic {
  flex: none; width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center; font-size: 15px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xs);
}
.wm-chip img { flex: none; width: 28px; height: 28px; object-fit: cover; border: 1px solid var(--line); background: var(--thumb-bg); }
.wm-chip small { display: block; font-family: var(--mono); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--mut); font-weight: 400; }

/* a note per step (Josh's call B) */
.wm-note {
  margin-top: 18px; padding: 6px 6px 6px 12px; border-left: 3px solid var(--ink); background: var(--panel-2);
  display: flex; align-items: center; gap: 10px;
}
.wm-note-text { flex: 1; min-width: 0; font-size: 14px; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; padding: 6px 0; }
.wm-textbtn {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 10px; background: none; border: 0; cursor: pointer;
  font-family: var(--mono); font-weight: 700; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink);
}
.wm-textbtn:hover { text-decoration: underline; }
.wm-textbtn:focus-visible { outline: none; box-shadow: var(--ring); }
.wm-note .wm-textbtn { flex: none; }
.wm-addnote { margin: 12px 0 0 -10px; }
.wm-noteedit { margin-top: 18px; }
.wm-note-input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 15px; color: var(--ink); line-height: 1.5;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px; resize: vertical;
}
.wm-note-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.wm-note-help { font-size: 12px; color: var(--mut); margin: 6px 0 0; }
.wm-noteedit-row { display: flex; gap: 8px; margin-top: 10px; }
.wm-noteedit-row button { flex: 1 1 0; min-width: 0; min-height: 46px; }
.wm-noteedit-row button:disabled { opacity: 0.55; cursor: default; }
.wm-hint { margin-top: 18px; font-size: 12.5px; color: var(--mut); display: flex; align-items: center; gap: 8px; }

/* the bottom: Done is the big one */
.wm-foot { flex: none; padding: 10px 14px calc(14px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--line); background: var(--panel); }
.wm-btns { display: flex; flex-wrap: wrap; gap: 8px; }
.wm-btns .wm-done {
  flex: 1 1 100%; min-height: 58px; font-size: 16px; display: flex; align-items: center; justify-content: center; gap: 10px;
  white-space: nowrap; letter-spacing: 0.04em;
}
.wm-btns .secondary {
  flex: 1 1 0; min-width: 0; min-height: 46px; padding: 0 6px; display: flex; align-items: center; justify-content: center; gap: 6px;
  white-space: nowrap; letter-spacing: 0.03em; font-size: 13px;
}
.wm-btns .secondary:disabled { opacity: 0.45; cursor: default; }
.wm-btns .secondary:disabled:hover { border-color: var(--line); }
/* v22.25b — Read it removed (Josh, 2026-10-19): on a phone Done is full
   width and ‹ Back + ☰ What I need share the row under it, half each. */

/* sheets: every step, and what I need */
.wm-scrim { position: fixed; inset: 0; z-index: 10095; background: rgba(20, 19, 26, 0.42); }
.wm-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 10096; max-height: 82vh; overflow-y: auto; overscroll-behavior: contain;
  background: var(--panel); border-top: 1px solid var(--ink); box-shadow: var(--sh-3);
  padding: 8px 16px calc(18px + env(safe-area-inset-bottom, 0px));
}
.wm-sheet:focus { outline: none; }
.wm-grab { width: 42px; height: 4px; background: var(--line); margin: 4px auto 10px; border-radius: 2px; }
/* Pinned while the list scrolls, so ✕ is always in reach. */
.wm-sheet-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px;
  position: sticky; top: -8px; z-index: 1; background: var(--panel); padding: 6px 0;
}
.wm-sheet-title {
  font-family: var(--disp); font-weight: 800; font-size: 26px; line-height: 1; text-transform: uppercase;
  letter-spacing: -0.01em; margin: 0; color: var(--ink);
}
.wm-list { list-style: none; margin: 0; padding: 0; }
.wm-list li { border-bottom: 1px solid var(--line); font-size: 14px; line-height: 1.4; color: var(--ink); }
.wm-jump {
  display: flex; gap: 10px; align-items: flex-start; width: 100%; min-height: 44px; box-sizing: border-box; padding: 10px 2px;
  background: none; border: 0; font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.wm-jump:focus-visible { outline: none; box-shadow: var(--ring); }
.wm-list .n { flex: none; width: 26px; font-family: var(--mono); font-size: 12px; color: var(--mut); padding-top: 1px; }
.wm-list .t {
  flex: 1; min-width: 0; overflow: hidden; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.wm-list li.done .t { color: var(--mut); text-decoration: line-through; }
.wm-list li.done .n { color: var(--green-ink); }
.wm-list li.now { background: var(--accent-soft); margin: 0 -16px; border-left: 3px solid var(--red); }
.wm-list li.now .wm-jump { padding-left: 15px; padding-right: 18px; }
.wm-list li.now .n { color: var(--accent-ink); font-weight: 700; }
.wm-tick { flex: none; color: var(--green-ink); font-weight: 700; }
.wm-grp { display: block; font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut); margin: 16px 0 4px; }
.wm-noteedit .wm-grp { margin-top: 0; margin-bottom: 6px; }
.wm-plain li { padding: 8px 2px; font-size: 13.5px; overflow-wrap: anywhere; }
.wm-vchips { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
.wm-vchips .wm-chip { justify-content: flex-start; width: 100%; }
.wm-empty { font-size: 14px; color: var(--mut); margin: 14px 0 4px; }

/* the finish */
.wm-finish { flex: 1; min-height: 0; overflow-y: auto; padding: 26px 22px calc(26px + env(safe-area-inset-bottom, 0px)); text-align: center; }
.wm-burst {
  width: 92px; height: 92px; border-radius: 50%; background: var(--rainbow); margin: 16px auto 18px;
  display: flex; align-items: center; justify-content: center; color: #fff; font-size: 42px; box-shadow: var(--sh-2);
}
.wm-big { font-family: var(--disp); font-weight: 900; font-size: 46px; line-height: 0.95; text-transform: uppercase; letter-spacing: -0.015em; color: var(--ink); }
.wm-finish p { color: var(--mut); font-size: 15px; margin: 12px auto 0; max-width: 30ch; }
.wm-finish-hero { width: 150px; aspect-ratio: 4 / 5; object-fit: cover; border: 1px solid var(--line); margin: 20px auto 0; display: block; background: var(--thumb-bg); }
.wm-finish-actions { display: flex; flex-direction: column; gap: 10px; margin: 24px auto 0; max-width: 340px; }
.wm-finish-actions .primary, .wm-finish-actions .secondary { min-height: 52px; font-size: 15px; }
.wm-finish-actions .primary:disabled { opacity: 0.6; cursor: default; }
.wm-finish-actions .wm-mark.is-done,
.wm-finish-actions .wm-mark.is-done:hover { background: var(--green-ink); border-color: var(--green-ink); color: #fff; opacity: 1; }
.wm-later { margin-top: 16px; font-size: 12.5px; color: var(--mut); }

/* ---------- tablet / computer: the look on the left, the step on the right ---------- */
@media (min-width: 821px) {
  .wm-top { padding: 12px 22px 10px; }
  .wm-where { padding: 0 22px; }
  .wm-split { display: grid; grid-template-columns: minmax(0, 42%) minmax(0, 1fr); }
  .wm-split.no-look { display: flex; }
  .wm-left { border-right: 1px solid var(--line); padding: 22px; display: flex; flex-direction: column; gap: 12px; min-height: 0; }
  .wm-lookbtn {
    flex: 1; min-height: 0; display: block; padding: 0; border: 1px solid var(--line); background: var(--thumb-bg); cursor: zoom-in;
  }
  .wm-lookbtn:focus-visible { outline: none; box-shadow: var(--ring); }
  .wm-lookbtn img { width: 100%; height: 100%; object-fit: contain; display: block; }
  .wm-right .wm-body { padding: 26px 34px 16px; }
  .wm-right .wm-text { font-size: 28px; }
  .wm-right .wm-num { font-size: 84px; }
  .wm-right .wm-foot { padding: 14px 34px 20px; }
  .wm-btns { flex-wrap: nowrap; gap: 10px; }
  /* v22.25b — three in one row now: Done, Back, What I need. */
  .wm-btns .wm-done { flex: 2 1 0; min-width: 0; }
  .wm-btns .secondary { min-height: 58px; font-size: 14px; }
  .wm-btns .wm-back { flex: 1 1 0; }
  .wm-btns .wm-need { flex: 1.3 1 0; }
  .wm-sheet { left: 50%; right: auto; width: min(640px, 100%); transform: translateX(-50%); }
}

/* ★ Every rule above that sets `display` loses to nothing — so each one
   gets its [hidden] partner, the trap this file keeps documenting. */
.wm[hidden], .wm-awake[hidden], .wm-awake i[hidden], .wm-stage[hidden], .wm-segs[hidden],
.wm-split[hidden], .wm-left[hidden], .wm-right[hidden], .wm-lookbtn[hidden], .wm-numrow[hidden],
.wm-isdone[hidden], .wm-chips[hidden], .wm-chip[hidden], .wm-chip .ic[hidden], .wm-note[hidden],
.wm-textbtn[hidden], .wm-noteedit-row[hidden], .wm-hint[hidden], .wm-btns[hidden], .wm-btns .wm-done[hidden],
.wm-btns .secondary[hidden], .wm-sheet-head[hidden], .wm-jump[hidden], .wm-list .t[hidden], .wm-grp[hidden],
.wm-vchips[hidden], .wm-burst[hidden], .wm-finish-hero[hidden], .wm-finish-actions[hidden],
.wm-door .primary.wm-go[hidden], .wm-chip small[hidden], .wm-lookbtn img[hidden] { display: none !important; }

/* The Workroom page's Construction Steps: a step's note, in small type under
   it. In .wr-task-main beside .wr-task-text, so a ticked step's line-through
   never reaches the note. */
.wr-task-main { flex: 1; min-width: 0; }
.wr-task-note {
  display: block; margin-top: 4px; font-size: 12px; line-height: 1.45; color: var(--mut);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.wr-task-note[hidden] { display: none !important; }

/* v22.25b — "Notes from earlier steps", under the Construction Steps: notes
   whose step a refresh reworded. Quiet and dashed — something to tidy, not
   an alarm. */
.wr-oldnotes { margin-top: 14px; padding: 12px 14px; border: 1px dashed var(--line); background: var(--panel-2); }
.wr-oldnotes-head {
  margin: 0; font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink);
}
.wr-oldnotes-sub { margin: 4px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--mut); }
.wr-oldnote { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.wr-oldnote-was { margin: 0; font-size: 12px; line-height: 1.45; color: var(--mut); overflow-wrap: anywhere; }
.wr-oldnote-text { margin: 4px 0 0; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.wr-oldnote-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.wr-oldnote-move, .wr-oldnote-remove {
  min-height: 36px; padding: 0 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-xs);
  font-family: var(--mono); font-weight: 700; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink); cursor: pointer;
}
.wr-oldnote-move:hover, .wr-oldnote-remove:hover { border-color: var(--ink); }
.wr-oldnote-move:focus-visible, .wr-oldnote-remove:focus-visible, .wr-oldnote-to:focus-visible { outline: none; box-shadow: var(--ring); }
.wr-oldnote-move:disabled, .wr-oldnote-remove:disabled, .wr-oldnote-to:disabled { opacity: 0.55; cursor: default; }
.wr-oldnote-steps {
  list-style: none; margin: 8px 0 0; padding: 0; max-height: 280px; overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--line); background: var(--panel);
}
.wr-oldnote-steps li + li { border-top: 1px solid var(--line); }
.wr-oldnote-to {
  display: flex; align-items: flex-start; gap: 8px; width: 100%; min-height: 40px; box-sizing: border-box;
  padding: 9px 10px; background: none; border: 0; font: inherit; font-size: 13px; line-height: 1.4;
  color: var(--ink); text-align: left; cursor: pointer;
}
.wr-oldnote-to:hover { background: var(--accent-soft); }
.wr-oldnote-n { flex: none; width: 22px; font-family: var(--mono); font-size: 11px; color: var(--mut); padding-top: 1px; }
.wr-oldnote-t { flex: 1; min-width: 0; overflow-wrap: anywhere; }
@media (max-width: 820px) {
  .wr-oldnote-move, .wr-oldnote-remove, .wr-oldnote-to { min-height: 44px; }
}
.wr-oldnote-actions[hidden], .wr-oldnote-steps[hidden], .wr-oldnote-to[hidden] { display: none !important; }

/* ===========================================================
   v22.26 — Finished piece (js/finished-piece.js)
   Ported from mocks/v22.26-finished-piece/fp.css (approved 2026-10-19),
   existing tokens only. The arrow is --accent-ink (text never takes a
   bright brand token — the audit's check 6b).
   =========================================================== */

/* the camera button */
.fp-snap { display: flex; align-items: center; justify-content: center; gap: 10px; white-space: nowrap; }

/* before -> after */
.fp-kicker { font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mut); margin: 4px 0 12px; }
.fp-ba { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; max-width: 360px; margin: 0 auto; }
.fp-ba figure { margin: 0; min-width: 0; }
.fp-ba img, .fp-ba .fp-ph {
  width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; box-sizing: border-box;
  border: 1px solid var(--line); background: var(--thumb-bg);
}
.fp-ba .fp-ba-made img, .fp-ba .fp-ba-made .fp-ph { border: 2px solid var(--ink); }
.fp-ba figcaption {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--mut); margin-top: 6px; text-align: center;
}
.fp-ba .fp-ba-made figcaption { color: var(--ink); font-weight: 700; }
.fp-arrow { font-family: var(--disp); font-weight: 800; font-size: 22px; color: var(--accent-ink); }
.fp-ba.small { max-width: none; gap: 6px; }
.fp-ba.small .fp-arrow { font-size: 16px; }
.fp-ba.small figcaption { font-size: 8.5px; margin-top: 4px; }

/* Workroom Mode: the before -> after panel (picture 2) */
.fp-after { padding-top: 18px; }
.wm-finish p.fp-saved { color: var(--green-ink); font-weight: 700; font-size: 14px; margin: 16px auto 0; max-width: none; }
.fp-name { display: flex; align-items: center; justify-content: center; gap: 10px; margin: 8px auto 0; max-width: 340px; }
.fp-name-text {
  font-family: var(--disp); font-weight: 800; font-size: 22px; text-transform: uppercase; line-height: 1;
  letter-spacing: -0.005em; color: var(--ink); min-width: 0; overflow-wrap: anywhere;
}
.fp-name .wr-rename { flex: none; }
.wm-fp-rename-form { max-width: 340px; margin: 10px auto 0; text-align: left; }
.wm-fp-input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 15px; color: var(--ink);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px;
}
.wm-fp-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.wm-fp-rename-row { display: flex; gap: 8px; margin-top: 8px; }
.wm-fp-rename-row button { flex: 1 1 0; min-width: 0; min-height: 44px; }
.wm-fp-rename-row button:disabled { opacity: 0.55; cursor: default; }
.wm-fp-msg { max-width: 340px; margin: 10px auto 0; font-size: 13px; line-height: 1.45; color: var(--mut); }
.wm-fp-msg:empty { display: none; }
.wm-finish-actions .secondary:disabled { opacity: 0.6; cursor: default; }

/* the Workroom page's Finished piece card (pictures 3, 5, 5b) */
.fp-card { position: relative; border: 1px solid var(--ink); background: var(--panel); box-shadow: var(--sh-2); padding: 14px 16px; }
.fp-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--green-ink); }
.fp-card.fp-empty::before { background: var(--rainbow); }
.fp-card-kicker { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--mut); margin: 4px 0 10px; }
.fp-card-sub { font-size: 13.5px; line-height: 1.5; color: var(--mut); margin: 0 0 12px; }
.fp-card-meta { font-size: 12.5px; color: var(--mut); margin: 10px 0 2px; }
.fp-card-actions { display: flex; gap: 18px; flex-wrap: wrap; }
.fp-card-actions .wr-textbtn { min-height: 44px; }
.fp-card.fp-empty .fp-snap { width: 100%; min-height: 52px; font-size: 15px; }
.fp-card .fp-snap:disabled { opacity: 0.7; cursor: default; }
.fp-card-msg { font-size: 12.5px; line-height: 1.45; color: var(--mut); margin: 8px 0 0; }
.fp-card-msg:empty { display: none; }
.fp-card-msg.is-error { color: var(--accent-ink); }

/* the picture details: "Made from" / "Made into" (picture 4) */
.fp-made { display: grid; gap: 6px; }
.fp-from { margin: 0 0 6px; display: grid; gap: 8px; }
.fp-from-row {
  display: flex; align-items: center; gap: 10px; width: 100%; box-sizing: border-box; text-align: left;
  background: var(--panel); border: 1px solid var(--line); padding: 8px 10px; min-height: 44px;
  cursor: pointer; font: inherit; color: var(--ink);
}
.fp-from-row:hover { border-color: var(--ink); }
.fp-from-row:focus-visible { outline: none; box-shadow: var(--ring); }
.fp-from-row img, .fp-from-row .fp-ph { width: 40px; height: 50px; object-fit: cover; border: 1px solid var(--line); flex: none; background: var(--thumb-bg); display: block; }
.fp-from-text { display: grid; gap: 2px; flex: 1; min-width: 0; }
.fp-from-text b { font-weight: 700; font-size: 14px; overflow-wrap: anywhere; }
.fp-from-text small { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mut); }
.fp-go { flex: none; font-size: 20px; color: var(--mut); }
.fp-from .fp-ba.small { max-width: 300px; margin: 0; }

.fp-snap[hidden], .fp-ba[hidden], .fp-ba img[hidden], .fp-ph[hidden], .fp-name[hidden], .wm-fp-rename-row[hidden],
.fp-card-actions[hidden], .fp-made[hidden], .fp-from[hidden], .fp-from-row[hidden], .fp-from-text[hidden] { display: none !important; }
