/* =============================================================
   Winterspell — Lorcana-themed stylesheet
   ============================================================= */

/* ── Custom Fonts ── */

/* HF Farmiles — titles, nav links, section labels, buttons, login display text.
   DO NOT remove or rename this @font-face. The font file lives at
   src/main/webapp/fonts/HF Farmiles Regular.woff2/.woff.
   Without this declaration the browser falls back to sans-serif on every
   device that has not previously cached the file (incl. all mobile browsers). */
@font-face {
  font-family: 'HF Farmiles';
  src: url('../fonts/HF Farmiles Regular.woff2') format('woff2'),
       url('../fonts/HF Farmiles Regular.woff')  format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}


/* Bitter — body/serif text (deck titles, card names, stats, the Save-image export).
   SELF-HOSTED (2026-07-17), previously loaded from fonts.googleapis.com. Reasons:
     1. The Save-image export could not embed it. modern-screenshot finds @font-face by
        reading document.styleSheets[].cssRules and SILENTLY DROPS any sheet whose
        cssRules throws; a cross-origin Google sheet always throws SecurityError, so
        Bitter was never embedded and every export fell back to the OS serif (a Windows
        serif under Edge, Noto Serif under Android — visibly different text per device).
        A same-origin @font-face is readable, so the woff2 now embeds properly.
     2. Consistency — HF Farmiles above was already self-hosted; Bitter was the outlier.
     3. No visitor IP disclosed to a third party, and no third-party origin on the
        render path. Works where Google Fonts is blocked. Pinned = deterministic export.
   Blocks below are Google's own CSS for v42 (latin + latin-ext only — the four
   supported languages need no cyrillic/vietnamese), with src repointed to local files.
   Licence: SIL OFL 1.1 — see fonts/Bitter-OFL.txt. Font files: fonts/Bitter-*.woff2.
   ⚠ These urls are NOT ?v=-stamped: to REPLACE a font file, rename it (…-v2.woff2)
      rather than overwrite, or caches will serve the old face. */
/* latin-ext */
@font-face {
  font-family: 'Bitter';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/Bitter-Italic-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;
}
/* latin */
@font-face {
  font-family: 'Bitter';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/Bitter-Italic-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;
}
/* latin-ext */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}
/* latin */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}
/* latin-ext */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}
/* latin */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}
/* latin-ext */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}
/* latin */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}
/* latin-ext */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}
/* latin */
@font-face {
  font-family: 'Bitter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../fonts/Bitter-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;
}

/* ── CSS Custom Properties ── */
:root {
  /* Backgrounds */
  --bg-void:      #04040f;
  --bg-deep:      #07071a;
  --bg-surface:   #0d0d24;
  --bg-elevated:  #141432;
  --bg-panel:     #1b1b3e;
  --bg-panel-alt: #20203e;
  /* Background used by filter buttons, dropdowns and search inputs across the
     card pages (My Cards / Missing / Card Sets / My Decks / Builder). Sits
     one step brighter than the panel so the controls visibly stand out.
     Currently matches --bg-panel (#1b1b3e); kept as a separate variable so
     the filter-control tone can be tuned independently of other panels. */
  --bg-filter-control: #1b1b3e;

  /* Gold accent */
  --gold:         #d4a93b;
  --gold-light:   #f0c864;
  --gold-dim:     rgba(212,169,59,0.14);
  --gold-glow:    rgba(212,169,59,0.38);

  /* ── Z-INDEX SCALE — the single source of truth for stacking layers ──────
     Every global/overlay layer references one of these named tokens instead
     of a bare literal, so the whole stack is documented in one place and a
     re-band is a one-line edit.  Ordered low → high.

       --z-nav        top navigation bar
       --z-nav-pop    menus/dropdowns anchored to the nav (must clear it)
       --z-dropdown   in-page filter multiselect dropdowns
       --z-drawer     mobile nav drawer + its scrim
       --z-overlay    slide-in panels / privacy notice
       --z-modal      BASE of the managed dialog band — the modal-stack manager
                      (_openModal/_closeModal in app.js) assigns each open
                      dialog  var(--z-modal) + depth × 10  at open time, so
                      NESTED dialogs stack correctly with NO per-dialog z-index.
                      Dialog CSS keeps this only as a static fallback.
       --z-toast      transient toasts — above everything.

     Local in-component stacking (card art vs badges, ::before fills, etc.)
     keeps small literal z-index:1/2/5 — those are intra-component tree order,
     not global layers, and intentionally stay out of this scale. */
  --z-nav:        100;
  --z-nav-pop:    110;
  --z-dropdown:   200;
  --z-drawer:     300;
  --z-overlay:    450;
  --z-modal:     1000;
  --z-toast:     9000;

  /* Purple accent */
  --purple:       #8b5cf6;
  --purple-light: #a78bfa;
  --purple-dim:   rgba(139,92,246,0.14);

  /* Text — deliberately readable on dark backgrounds */
  --text-bright:  #f0ebe0;
  --text-main:    #d4cfc4;   /* was #c8c0b0 — slightly brighter */
  --text-muted:   #c0c0d8;   /* raised for readability — always visible on dark panels */
  --text-dim:     #9898b8;   /* raised for readability — was #6a6a8a (failed contrast) */

  /* Typography — letter-spacing (tracking) for the HF Farmiles display font.
     Farmiles is the all-caps display face (nav, breadcrumbs, buttons, section
     labels, login chrome). Reference --ls-farmiles from EVERY Farmiles selector
     so tracking stays consistent app-wide — never hardcode a value on a
     Farmiles rule. Expressed in em (relative to font-size) so the tracking
     SCALES with the text instead of being a fixed pixel count:
        0.05em = 1px on the 20px nav links, ~0.9px on the 18px breadcrumb,
                 0.8px on the 16px disclaimer button, and ~2.45px on the
                 clamp(29-49px) .login-welcome hero (at its 49px desktop size).
     One value covers the whole range including the hero: in em the old 1px UI
     tracking and 3px hero tracking are nearly the same ratio, so the hero's
     wider look falls out of its larger font-size automatically (no separate
     wide constant). Tune this single number to taste — lower = tighter. */
  --ls-farmiles: 0.05em;

  /* Borders */
  --border:       #252548;
  --border-hover: #3a3a6a;

  /* Ink colors — Tailwind palette.  Use for ink-section headers, glows,
     card-thumb tints, and any rendering that shows ONE ink at a time. */
  --amber:        #f59e0b;
  --amethyst:     #8b5cf6;
  --emerald:      #10b981;
  --ruby:         #ef4444;
  --sapphire:     #3b82f6;
  --steel:        #94a3b8;
  --inkless:      #a8a8a8;

  /* Canonical ink chart colours — extracted from the ink-icon SVGs.
     Use ONLY for stacked-segment renderers (Ink Cost Curve, Ink Balance,
     compact-list pill backgrounds) where Tailwind Emerald (#10b981) and
     Tailwind Steel (#94a3b8) are visually indistinguishable when
     stacked.  The icon-derived hues are deeper and more saturated, so
     Emerald/Steel separate cleanly.  Mirror of INK_CHART_COLORS in
     app.js — keep the two in sync.  See SPEC.md → Ink chart palette. */
  --amber-chart:    #F3B514;
  --amethyst-chart: #82367A;
  --emerald-chart:  #1C8A37;
  --ruby-chart:     #D40F33;
  --sapphire-chart: #1789C3;
  --steel-chart:    #9EACB4;

  /* Destructive-affordance accent — used for delete / wipe-collection /
     other irreversible action buttons. Separate from --ruby (the Ruby
     ink colour) so the two can be tuned independently. The previous
     pure #ef4444 was too bright and harsh against the dark theme;
     #A00000 is a darker tinted red that still reads as "destructive"
     without burning the eye. --danger-hover is the active/hover shade. */
  --danger:       #A00000;
  --danger-hover: #7a0000;

  /* Rarity colors */
  --r-common:     #94a3b8;
  --r-uncommon:   #4ade80;
  --r-rare:       #60a5fa;
  --r-super:      #c084fc;
  --r-legendary:  #fbbf24;
  --r-enchanted:  #f472b6;
  --r-special:    #fb923c;
  --r-epic:       #f87171;
  --r-iconic:     #22d3ee;

  /* Layout */
  --nav-h:        60px;
  --bottom-bar-h: 60px;
  /* App-wide horizontal gutters — the SINGLE source of truth for left/right
     padding. --gutter-chrome: the thin fixed bars (top-nav, bottom-bar).
     --gutter-content: the page-content gutter, applied to every full-width
     content container (home, breadcrumb bar, set selector, filter bar,
     #ill-tab-content, deck-detail head/body, …). Unified 2026-06-18 (was a
     scatter of 20/22/24px). Rare, deliberate exceptions stay hardcoded. */
  --gutter-chrome:  10px;
  --gutter-content: 15px;
  /* Height of the .breadcrumb-bar (the My Collection location hint that
     replaced the old sub-tab strip). Surfaces that embed inside
     #ill-tab-content and need their own fixed height (deck builder
     two-pane scroll) subtract this so they fit within the visible area.
     Keep in sync with .breadcrumb-bar's padding + line box. */
  --tabs-h:       35px;
  --panel-w:      400px;

  /* Caps the max horizontal size of every centred filter-bar surface
     (My Cards / Missing Cards / Card Sets / My Decks). On viewports
     narrower than this the bar is full-width via its `width: 100%`; on
     wider viewports it stops growing and centres via `margin: 0 auto`.
     Card grids below the filter bars stay intentionally full-width
     (cards should use available space).
     The Home welcome banner does NOT use this token — it has its own
     narrower max-width (860 px) so the greeting card doesn't feel
     stretched on widescreens. See .home-welcome for the rationale. */
  --max-content-width: 1536px;     /* was 1280px — +20% so the bar fills better on ~1440-px laptop viewports */

  --radius-s:     6px;
  --radius-m:     10px;
  --radius-l:     16px;
  --radius-xl:    22px;

  /* ── Design-system foundation (theming) — added 2026-06-18 ───────────────
     ADDITIVE: defined for components to adopt during the page-by-page polish
     (see THEME_TOKENS.md). Nothing references these YET, so adding them is a
     zero-visual-change foundation; values mirror today's real usage so a
     component that later switches to a token looks identical. Alternate themes
     override these under [data-theme="…"]; :root is the default (dark). */

  /* Font families */
  --font-display: 'HF Farmiles', sans-serif;   /* nav / headings / display */
  --font-ui:      'Bitter', serif;             /* body / UI / dropdown items */

  /* Type scale (px) — map each component to the nearest step during polish */
  --fz-xs:   12px;   /* labels, counts */
  --fz-sm:   13px;
  --fz-base: 14px;   /* body / UI default, dropdown items */
  --fz-md:   16px;
  --fz-lg:   19px;   /* top-nav links */
  --fz-xl:   24px;
  --fz-2xl:  30px;   /* big stat numbers */

  /* Font weights */
  --fw-normal:   400;
  --fw-medium:   500;
  --fw-semibold: 600;
  --fw-bold:     700;

  /* Elevation / shadows (mirror current usage) */
  --shadow-card:     0 2px 10px rgba(0,0,0,0.30);
  --shadow-dropdown: 0 14px 36px rgba(0,0,0,0.60);
  --shadow-dialog:   0 24px 64px rgba(0,0,0,0.75);

  /* Component padding classes — the "classify the exceptions" tokens. Each
     class carries ONE value; a component adopts the matching class during
     polish instead of an ad-hoc literal. (Starting values; tune per class.) */
  --pad-control: 8px;    /* small controls: chips, pills, menu items */
  --pad-card:    18px;   /* cards / tiles / page panels */
  --pad-panel:   16px;   /* inner panels / sections */
  --pad-dialog:  24px;   /* modal / dialog body */

  /* Fantasy window frame
     border-width controls padding-box size (blue panel edge).
     border-image-width controls the visual stone frame thickness.
     Keep border-width ≥ border-image-width; the difference is the
     transparent gap between the stone inner edge and the blue panel.
     To add a new framed dialog: add its selector to the grouped rule
     below the comment block and give it its own background colour. */
  --frame-border:      30px; /* layout border zone — must equal --frame-image-width for zero gap */
  --frame-image-width: 30px; /* visual frame thickness — do not change lightly */
  --frame-image:       url('../images/borders/inkcannon-window-border2-v1-small.png');
  --frame-slice:       92 83 81 85; /* top right bottom left — calibrated for inkcannon-window-border2-v1-small.png */
}

/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; overscroll-behavior: none; }

body {
  font-family: 'Bitter', serif;
  background-color: var(--bg-void);   /* solid fallback / canvas colour under the fixed bg layer */
  color: var(--text-main);
  min-height: 100vh;
  overflow-x: hidden;
  overscroll-behavior: none;
}

/* Full-viewport background image.
   Painted on a position:fixed pseudo-element (NOT `background-attachment: fixed`
   on body). iOS Safari has a long-standing bug where a fixed-attachment
   background is sized to the whole scrollable DOCUMENT, not the viewport — which
   makes it look zoomed-out and jitter as you scroll. A position:fixed layer is
   pinned to the viewport on every browser (incl. iOS), so `cover` sizes to the
   screen and the image stays perfectly still. z-index:-1 keeps it behind all
   content; translucent panels/bars show it through.
   The JPEG is the SOLE visual treatment — no gradients, filters, overlays,
   opacity, blend modes, or backdrop-filters. See SPEC.md → "Background image —
   no effects" before adding anything here. */
html::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: url('../images/inkcannon/inkcannon-background-v3.jpg') center / cover no-repeat;
  pointer-events: none;
}

a  { color: inherit; text-decoration: none; }
button { cursor: pointer; border: none; background: none; font-family: inherit; }
input, select, textarea { font-family: inherit; color: inherit; }
img { display: block; max-width: 100%; }

/* ── Scrollbar — overlay/floating, takes no layout space ── */
/* scrollbar-gutter: stable reserves a fixed gutter so the scrollbar never shifts layout */
html {
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.18) transparent;
  scrollbar-gutter: stable;
}
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.18); border-radius: 3px; }
@media (any-hover: hover) {
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.32); }
}


/* ═══════════════════════════════════════
   TOP NAVIGATION
   ═══════════════════════════════════════ */
.top-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--nav-h);
  background: rgba(8, 6, 18, 0.72);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-bottom: 1px solid rgba(255, 255, 255, 0.10);
  display: flex;
  align-items: center;
  padding: 0 var(--gutter-chrome);
  gap: 28px;
  z-index: var(--z-nav);
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  text-decoration: none;
}

.nav-logo {
  height: 45px;
  width: auto;
  object-fit: contain;
}

.nav-links {
  display: flex;
  gap: 2px;
  flex: 1;
  flex-wrap: nowrap;
  min-width: 0;
}

.nav-link {
  padding: 6px 10px;
  border-radius: var(--radius-s);
  font-family: 'HF Farmiles', sans-serif;
  font-size: 19px;
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  color: var(--text-muted);
  transition: color 0.2s, background 0.2s;
  white-space: nowrap;
  flex-shrink: 0;
}

@media (any-hover: hover) {
.nav-link:hover   { color: var(--text-bright); background: rgba(255,255,255,0.05); }
}
.nav-link.active  { color: var(--gold-light);  background: var(--gold-dim); }

.nav-end {
  flex-shrink: 0;
  font-size: 14px;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 8px;
}


.nav-user-badge {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--purple-dim);
  border: 1px solid rgba(139,92,246,0.3);
  border-radius: 20px;
  padding: 4px 8px;
  font-family: 'HF Farmiles', sans-serif;
  font-size: 19px;
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  color: var(--purple-light);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

@media (any-hover: hover) {
.nav-user-badge:hover {
  background: rgba(139,92,246,0.18);
  border-color: rgba(139,92,246,0.55);
}
}

.nav-user-avatar {
  width: 40px;
  height: 40px;
  object-fit: contain;
  border-radius: 6px;
  cursor: pointer;
  flex-shrink: 0;
}

/* .nav-user-avatar-fallback removed 2026-06-01 — the emoji fallback (🧙) is
   gone. New users get the seeded default avatar via DB; older users with
   user_icon_id = NULL fall back to images/users/user_default_icon.png (a
   real <img>, not a <span> with an emoji). See SPEC.md → Default avatar. */

/* ── User nav dropdown ── */
.nav-ill-wrap {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ── Language indicator pill ── */
.nav-lang-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  padding: 4px 8px;
  border-radius: 11px;
  font-family: 'HF Farmiles', sans-serif;
  font-size: 19px;
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  color: #0f172a;
  user-select: none;
  flex-shrink: 0;
}
.nav-lang-pill--en { background: #60a5fa; }
.nav-lang-pill--fr { background: #f1f5f9; }
.nav-lang-pill--de { background: #fb923c; }
.nav-lang-pill--it { background: #4ade80; }

/* Dropdown caret for the top-nav triggers (My Collection + username badge).
   An inline SVG chevron — replaced the old ▾ character (which at 30px carried
   huge font side-bearing whitespace). currentColor so it inherits each
   trigger's text colour (muted / active gold / purple). 2026-06-18 top-nav
   compaction. */
.nav-caret-svg {
  width: 10px;
  height: 10px;
  margin-left: 2px;
  opacity: 0.7;
  flex-shrink: 0;
}

/* User menu (Account Settings / Log Out) — styled to match the My Collection
   dropdown below (.nav-coll-dropdown / .nav-coll-item): same surface colour,
   shadow, radius, padding and Bitter 14px/500 muted items.  Stays right-
   aligned (it hangs off the right-hand user badge) and Log Out keeps red. */
.nav-ill-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 190px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  box-shadow: 0 14px 36px rgba(0,0,0,0.6);
  padding: 6px;
  display: flex;
  flex-direction: column;
  z-index: 500;
}

.nav-ill-menu-item {
  /* Farmiles display face to match the top-nav links + the My Collection
     dropdown (all-caps + --ls-farmiles tracking, 17px). Log Out keeps ruby
     via .nav-ill-menu-logout below. */
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  color: var(--text-muted);
  padding: 8px 14px;
  border-radius: 4px;
  background: none;
  border: none;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
@media (any-hover: hover) {
.nav-ill-menu-item:hover {
  color: var(--text-bright);
  background: rgba(255,255,255,0.05);
}
}

/* Log Out keeps its red text (with a subtle red-tinted hover). */
.nav-ill-menu-logout { color: var(--ruby); }
@media (any-hover: hover) {
.nav-ill-menu-logout:hover { color: var(--ruby); background: rgba(220,38,38,0.08); }
}

/* ── My Collection dropdown (top nav) ──
   The section switcher under "My Collection". Trigger is a .nav-link
   button (inherits the Farmiles nav styling); the panel + items mimic the
   old sub-tab text (Bitter 14px/500, muted, active gold). Markup is static
   in index.jsp; toggle + item clicks wired in enterApp. */
.nav-coll-wrap { position: relative; }
.nav-coll-trigger {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  border: none;
  background: none;          /* reset the UA <button> chrome — .nav-link supplies the rest */
  cursor: pointer;
}
.nav-coll-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  left: 8px;
  min-width: 190px;
  background: var(--bg-surface);          /* same colour as the tab strip / breadcrumb bar */
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  box-shadow: 0 14px 36px rgba(0,0,0,0.6);
  padding: 6px;
  display: flex;
  flex-direction: column;
  z-index: var(--z-nav-pop);              /* above the top-nav (--z-nav) */
}
.nav-coll-item {
  /* Farmiles display face to match the top-nav links (all-caps + --ls-farmiles
     tracking). Bumped to 17px — Farmiles reads small at the old 14px. */
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  color: var(--text-muted);
  padding: 8px 14px;
  border-radius: 4px;
  background: none;
  border: none;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
@media (any-hover: hover) {
.nav-coll-item:hover { color: var(--text-bright); background: rgba(255,255,255,0.05); }
}
.nav-coll-item.active { color: var(--gold-light); }

/* ── Account Settings dialog ── */
.edit-ill-dialog {
  background: var(--bg-panel);
  border: 30px solid transparent;                                              /* [FRAME] */
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch; /* [FRAME] */
  border-image-width: 30px;                                                    /* [FRAME] */
  background-clip: padding-box;                                                /* [FRAME] */
  border-radius: 0;
  box-shadow: 0 24px 64px rgba(0,0,0,0.75);
  width: 680px;
  max-width: 96vw;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  position: relative;
  animation: slideUp 0.3s cubic-bezier(0.16,1,0.3,1) forwards;
}

.edit-ill-dialog-header {
  padding: 20px 56px 16px 28px;
  border-bottom: 1px solid var(--border);
  font-family: 'Bitter', serif;
  font-size: 18px;
  color: var(--text-bright);
  flex-shrink: 0;
}

.edit-ill-body {
  display: flex;
  flex: 1;
  overflow: hidden;
  min-height: 0;
}

/* Left nav (the "section list" screen — full-width on mobile) */
.edit-ill-nav {
  width: 210px;
  flex-shrink: 0;
  border-right: 1px solid var(--border);
  padding: 16px 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.edit-ill-nav-item {
  display: block;
  width: 100%;
  padding: 10px 20px;
  background: none;
  border: none;
  border-left: 3px solid transparent;
  text-align: left;
  font-size: 14px;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}

@media (any-hover: hover) {
.edit-ill-nav-item:hover {
  background: var(--bg-hover);
  color: var(--text-main);
}
}

.edit-ill-nav-item.active {
  background: var(--bg-elevated);
  color: var(--text-bright);
  font-weight: 600;
  border-left-color: var(--gold);
}

/* Right content — a flex column the ACTIVE panel fills. Because the dialog is a FIXED
   height, switching panels never resizes it; the active panel scrolls inside. */
.edit-ill-content {
  flex: 1;
  min-width: 0;
  min-height: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* Only one panel is shown at a time (JS toggles inline display none/''). The shown panel
   fills the content area; simple panels scroll as a unit. */
.edit-ill-panel {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 24px 28px;
}
/* FILL panel (User Icon): fixed header + self-scrolling grid + pinned footer, so the long
   icon grid scrolls by itself and Save Icon stays visible. overflow:hidden → only the
   middle band scrolls. */
.edit-ill-panel--fill {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
.edit-ill-fill-head  { flex-shrink: 0; padding: 24px 28px 12px; }
.edit-ill-fill-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 0 28px 12px; }
.edit-ill-fill-foot {
  flex-shrink: 0; padding: 14px 28px; border-top: 1px solid var(--border);
  background: var(--bg-deep); display: flex; align-items: center; gap: 14px;
}

/* Delete panel sub-tabs */
.edit-ill-delete-subtabs {
  display: flex;
  gap: 4px;
  margin-bottom: 20px;
  border-bottom: 1px solid var(--border);
}

.edit-ill-delete-subtab {
  padding: 8px 16px;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
  margin-bottom: -1px;
  transition: color 0.15s, border-color 0.15s;
}

@media (any-hover: hover) {
.edit-ill-delete-subtab:hover {
  color: var(--text-main);
}
}

.edit-ill-delete-subtab.active {
  color: var(--ruby);
  border-bottom-color: var(--ruby);
  font-weight: 600;
}

/* Delete-Decks rows (Account Settings → Collection → Delete Decks).
   A label/description block on the left, a "Delete all" danger button on the
   right, separated by a hairline rule (the last row drops it via inline style). */
.pdd-danger-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}
.pdd-danger-row .t {
  font-family: 'Bitter', serif;
  font-size: 14px;
  color: var(--text-bright);
}
.pdd-danger-row .s {
  font-family: 'Bitter', serif;
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 2px;
}
/* The " · N" count suffix appended to a row title — dimmer than the title. */
.pdd-count {
  color: var(--text-muted);
  font-weight: 600;
}

.edit-ill-panel h3 {
  font-family: 'Bitter', serif;
  font-size: 16px;
  color: var(--text-bright);
  margin: 0 0 8px;
}

.edit-ill-hint {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 8px;
  line-height: 1.6;
}

.edit-ill-pii-warn {
  font-size: 12px;
  color: var(--amber);
  margin-bottom: 20px;
  line-height: 1.5;
}

/* ── Collection Language radio options ── */
.edit-ill-lang-options {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 4px;
}

.edit-ill-lang-option {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 7px 14px;   /* tightened ~30% top/bottom (was 10px) — less wasted vertical space */
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
  user-select: none;
}

@media (any-hover: hover) {
.edit-ill-lang-option:hover {
  background: var(--bg-hover);
  border-color: var(--border-hover);
}
}

.edit-ill-lang-option.selected {
  background: var(--bg-elevated);
  border-color: var(--gold);
}

.edit-ill-lang-option input[type="radio"] {
  accent-color: var(--gold);
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.edit-ill-lang-code {
  font-weight: 700;
  font-size: 13px;
  color: var(--text-bright);
  min-width: 28px;
}

.edit-ill-lang-name {
  font-size: 13px;
  color: var(--text-muted);
}

.edit-ill-char-count {
  font-size: 12px;
  color: var(--text-muted);
  text-align: right;
  margin-top: 4px;
}

.edit-ill-char-count--limit {
  color: var(--ruby);
  font-weight: 600;
}

.edit-ill-field {
  margin-bottom: 16px;
}

.edit-ill-field label {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 5px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* General form input used in dialogs */
.form-input {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 8px 12px;
  font-size: 14px;
  font-family: inherit;
  color: var(--text-bright);
  transition: border-color 0.2s;
}

.form-input:focus {
  outline: none;
  border-color: var(--gold);
}

.form-input::placeholder {
  color: var(--text-dim);
}

.form-input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.edit-ill-danger-box {
  background: rgba(192,57,43,0.12);
  border: 1px solid rgba(192,57,43,0.4);
  border-radius: var(--radius-m);
  padding: 14px 16px;
  font-size: 14px;
  color: var(--text-main);
  margin-bottom: 20px;
  line-height: 1.65;
}

.edit-ill-confirm-phrase {
  display: block;
  font-family: monospace;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 8px 12px;
  font-size: 13px;
  color: var(--gold-light);
  margin-bottom: 16px;
  user-select: all;
  word-break: break-all;
}

/* ── Icon picker ── (flows naturally; the ONLY scroller is the panel's
   .edit-ill-fill-scroll — no nested scroll container / double scrollbar).
   Grid with 1fr columns STRETCHES the tiles to fill the full width — no wasted
   space on the right — and the wider tiles fit long localized names (e.g. German
   "Königinnenkrone"). ~4 columns on desktop, ~3 on a phone. */
.ill-icon-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(105px, 1fr));
  gap: 12px;
}

.ill-icon-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  background: var(--bg-card);
  border: 2px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  width: auto;   /* grid cell (1fr) sizes it — fills the row, no wasted space */
  min-width: 0;
}

@media (any-hover: hover) {
.ill-icon-option:hover {
  border-color: var(--purple-light);
  background: var(--purple-dim);
}
}

.ill-icon-option.selected {
  border-color: var(--gold);
  background: rgba(212,175,55,0.1);
}

.ill-icon-preview {
  width: 52px;
  height: 52px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ill-icon-preview img {
  width: 52px;
  height: 52px;
  object-fit: contain;
  border-radius: 6px;
}

.ill-icon-label {
  font-size: 11px;
  color: var(--text-muted);
  text-align: center;
  line-height: 1.3;
  overflow-wrap: break-word;   /* safety for any long single-word localized name */
  max-width: 100%;
}

.ill-icon-option.selected .ill-icon-label { color: var(--gold-light); }

.ill-icon-search {
  width: 100%;
  margin-bottom: 4px;
  box-sizing: border-box;
}

.ill-icon-no-results {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 13px;
  color: var(--text-muted);
  text-align: center;
  pointer-events: none;
}

/* ── Import panel ── */
.import-file-area {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 70px;
  border: 2px dashed var(--border);
  border-radius: var(--radius-s);
  padding: 12px 16px;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
  margin-bottom: 4px;
}
@media (any-hover: hover) {
.import-file-area:hover {
  border-color: var(--gold);
  background: rgba(255,255,255,0.03);
}
}
.import-file-prompt {
  color: var(--text-muted);
  font-size: 14px;
  pointer-events: none;
}
.import-file-chosen {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.import-file-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-bright);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.import-file-clear {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 14px;
  padding: 2px 6px;
  border-radius: var(--radius-s);
  transition: color 0.15s, background 0.15s;
  flex-shrink: 0;
}
@media (any-hover: hover) {
.import-file-clear:hover { color: var(--ruby); background: rgba(239,68,68,0.1); }
}

.import-progress-wrap { margin: 10px 0 6px; }
.import-progress-bar {
  height: 8px;
  background: var(--bg-elevated);
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 6px;
}
.import-progress-fill {
  height: 100%;
  background: var(--gold);
  border-radius: 4px;
  transition: width 0.2s ease;
}
.import-progress-label {
  font-size: 12px;
  color: var(--text-muted);
  text-align: center;
}

.import-result-summary { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; font-size: 13px; }
.import-result-row     { padding: 4px 0; }
.import-result-ok   { color: var(--emerald); font-weight: 600; }
.import-result-skip { color: var(--gold-light); }
.import-result-fail { color: var(--ruby); }

/* Expandable import-issues panel — collapsed by default; the <summary>
   line is the always-visible headline ("Show N details"). When the user
   expands, the issue list is contained in a bordered surface-tone box
   with max-height so a tampered backup that throws 200 issues doesn't
   explode the dialog vertically. line-height tightened from the default
   so the list reads as a scannable log. */
.import-issues-panel {
  font-size: 12px;
  color: var(--text-muted);
}
.import-issues-panel > summary {
  font-weight: 600;
  color: var(--text-main);
  padding: 6px 0;
}
.import-issues-list {
  margin: 4px 0 0;
  padding: 8px 12px 8px 28px;     /* 28 left so list bullets sit inside the box */
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  max-height: 280px;
  overflow-y: auto;
  line-height: 1.55;
}
.import-issues-list li { padding: 2px 0; }

/* ── Restore mode selector ── */
.restore-mode-selector {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border);
  padding-bottom: 12px;
}
.restore-mode-btn {
  padding: 6px 14px;
  font-size: 13px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-family: 'Bitter', serif;
  transition: all 0.15s;
}
@media (any-hover: hover) {
.restore-mode-btn:hover { color: var(--text); border-color: var(--text-muted); }
}
.restore-mode-btn.active { background: var(--primary); color: #fff; border-color: var(--primary); }

/* ── Dreamborn.ink import panel ── */
.dreamborn-lang-warning {
  background: rgba(251,191,36,0.12);
  border: 1px solid rgba(251,191,36,0.35);
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 13px;
  color: var(--gold-light);
  margin-bottom: 12px;
}
.dreamborn-notfound-list {
  margin: 6px 0 0 16px;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.7;
  list-style: disc;
}


/* ═══════════════════════════════════════
   APP ROOT
   ═══════════════════════════════════════ */
#app-root {
  padding-top: var(--nav-h);
  padding-bottom: var(--bottom-bar-h);
  min-height: 100vh;
}

/* When the login page is active the top nav is hidden — remove its padding */
body.login-active #app-root {
  padding-top: 0;
}

/* Hide the top nav whenever the user shouldn't see it:
   • `.login-active` — login page is rendered (unauthenticated).
   • `.auth-pending` — initial body class set in index.jsp.  Removed by the
     first call to renderLogin() / enterApp() in js/app.js.  Without this an
     unauthenticated visitor would briefly see the nav (an "empty home page"
     flash) between the initial paint and the synchronous fast-path
     renderLogin() call. */
body.login-active .top-nav,
body.auth-pending .top-nav {
  display: none;
}

.page { min-height: calc(100vh - var(--nav-h) - var(--bottom-bar-h)); }

/* ═══════════════════════════════════════
   BOTTOM BAR
   ═══════════════════════════════════════ */
.bottom-bar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  height: var(--bottom-bar-h);
  background: rgba(8, 6, 18, 0.72);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-top: 1px solid rgba(255, 255, 255, 0.10);
  display: flex;
  align-items: center;
  padding: 0 var(--gutter-chrome);
  z-index: 100;
}

.bottom-bar-left,
.bottom-bar-right {
  flex: 1;
  display: flex;
  align-items: center;
}

.bottom-bar-right { justify-content: flex-end; }

.bottom-bar-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ── Privacy notice banner ──────────────────────────────────────────────── */
/* Sits directly above the bottom bar, slides up from below on first visit.
   Hidden state uses translateY(100% + bottom-bar-h) so the banner slides
   completely off-screen on dismiss — translateY(100%) alone only clears
   the banner's own height, leaving it sitting on top of the bottom bar. */
#privacy-notice {
  position: fixed;
  bottom: var(--bottom-bar-h);
  left: 0;
  right: 0;
  background: var(--bg-void);
  border-top: 2px solid var(--gold-dim);
  padding: 12px 16px 12px 20px;
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.5;
  transform: translateY(calc(100% + var(--bottom-bar-h)));
  transition: transform 0.3s ease;
  z-index: var(--z-overlay);
}

#privacy-notice.visible { transform: translateY(0); }

#privacy-notice-text { flex: 1; }

#privacy-notice-btn {
  flex-shrink: 0;
  padding: 5px 14px;
  background: transparent;
  border: 1px solid var(--gold);
  border-radius: var(--radius-s);
  color: var(--gold);
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
  transition: background .15s, color .15s;
}

@media (any-hover: hover) {
#privacy-notice-btn:hover {
  background: var(--gold-dim);
  color: var(--gold-light);
}
}

.bottom-disclaimer-btn {
  font-family: 'HF Farmiles', sans-serif;
  font-size: 19px;
  color: #fff;
  background: #6b21a8;
  border: none;
  border-radius: var(--radius-s);
  padding: 6px 10px;   /* even with the top-nav buttons (.nav-link) */
  cursor: pointer;
  letter-spacing: var(--ls-farmiles);
  transition: background 0.2s;
}
@media (any-hover: hover) {
.bottom-disclaimer-btn:hover { background: #7c3aed; }
}


.bmc-img {
  height: 38px;
  width: auto;
  display: block;
  opacity: 0.90;
  transition: opacity 0.2s;
}
@media (any-hover: hover) {
.bmc-link:hover .bmc-img { opacity: 1; }
}

/* ═══════════════════════════════════════
   LOGIN PAGE
   ═══════════════════════════════════════ */

/* ── Login entrance animation keyframes ── */
@keyframes loginBgReveal {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes loginFadeRise {
  from { opacity: 0; transform: translateY(15px); }
  to   { opacity: 1; transform: translateY(0);    }
}
@keyframes loginFadeScale {
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: scale(1);    }
}
@keyframes loginFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Black overlay on top of everything — fades out to reveal the background */
body.login-active::before {
  content: '';
  position: fixed;
  inset: 0;
  background: #000;
  z-index: 9999;
  pointer-events: none;
  animation: loginBgReveal 400ms ease-out both;
}

/* Bottom bar fades in only on the login page */
.login-active .bottom-bar {
  animation: loginFade 350ms ease-out both;
  animation-delay: 700ms;
}

.login-page {
  position: fixed;
  inset: 0;
  bottom: var(--bottom-bar-h);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Column layout: the language pills form a reserved band at the TOP (flex:none,
     centered) and .login-content fills the remaining space and centers within it,
     shrinking the logo/text to fit. Was justify-content:center — that centered the
     whole block and let the (absolute) switcher overlap the hero on a narrow window. */
  justify-content: flex-start;
}

.login-content {
  flex: 1 1 auto;
  min-height: 0;          /* allow the logo to shrink instead of overflowing */
  width: 100%;
  box-sizing: border-box;
  padding: 0 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* `safe center`: centered while it fits; when the window is too small to fit even
     the shrunk logo + text, it falls back to top-aligned + a scroll (below) instead of
     clipping the top. So the hero shrinks first (no scroll in normal use), and only a
     genuinely tiny window shows a scroll — the browse link is always reachable, never
     hidden behind the bottom bar. */
  justify-content: safe center;
  gap: clamp(6px, 1.6vh, 12px);   /* gaps tighten a little when vertically cramped */
  text-align: center;
  overflow-y: auto;
  overflow-x: hidden;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LOGIN PAGE — HF FARMILES FONT ZONE
   The classes below use 'HF Farmiles' intentionally and permanently.
   DO NOT change font-family here without explicit instruction from the owner.
   These are NOT affected by body/UI font swaps (Lora, Bitter, etc.).
   ═══════════════════════════════════════════════════════════════════════════ */

.login-welcome {
  font-family: 'HF Farmiles', sans-serif;
  /* min(vw, vh) so the hero shrinks on a narrow OR a short window (cramped states). */
  font-size: clamp(22px, min(5vw, 5vh), 49px);
  color: #00D5FF;
  line-height: 1.15;
  letter-spacing: var(--ls-farmiles);   /* em-based: ~3px at the 49px hero render, scales down gracefully on small screens */
  animation: loginFadeRise 350ms ease-out both;
  animation-delay: 400ms;
}

.login-logo {
  /* Scales to fit: capped by width on wide screens and by viewport HEIGHT on short
     screens (max-height: 52vh) so the logo automatically shrinks instead of cramping
     the text / pushing the browse link off-screen. width/height auto + both max-*
     preserve the aspect ratio. flex-shrink lets it give up more space if truly tight. */
  max-width: min(552px, 90vw);
  max-height: min(44vh, 552px);
  width: auto;
  height: auto;
  min-height: 0;
  flex-shrink: 1;
  display: block;
  margin: 0;
  animation: loginFadeScale 350ms ease-out both;
  animation-delay: 480ms;
}

.login-tagline {
  font-family: 'HF Farmiles', sans-serif;
  font-size: clamp(22px, min(5vw, 5vh), 49px);
  color: #00D5FF;
  letter-spacing: var(--ls-farmiles);
  animation: loginFadeRise 350ms ease-out both;
  animation-delay: 560ms;
}

.login-error {
  margin-top: 14px;
  padding: 11px 22px;
  background: rgba(160, 18, 18, 0.82);
  border: 1px solid rgba(239, 68, 68, 0.9);
  border-radius: 6px;
  color: #ffffff;
  font-family: 'HF Farmiles', sans-serif;
  /* Typography matched to .bottom-disclaimer-btn (size / weight / spacing) — the
     old 14px/700 rendered too small and heavier than anything else in the app.
     Text colour stays white (the red panel is the error signal). */
  font-size: 19px;
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  max-width: 380px;
  text-align: center;
  animation: loginFadeRise 350ms ease-out both;
  animation-delay: 560ms;
}

/* Sign In button: restyled 2026-07-05 to mimic the companion app's Sign In
   button spec (shared via the companion-app chat), so the very first screen a
   user sees looks the same across web and mobile. The spec's literal colors
   (#C9A84C accent, #0A0A14 background) are NOT hardcoded here — reusing
   --gold (#d4a93b, the closest existing token to #C9A84C) and --bg-void
   (#04040f, effectively identical to #0A0A14) instead, so this button tracks
   the shared theme if those tokens ever change instead of drifting into its
   own frozen color. color-mix() derives the alpha-blended states from the
   same token (the pattern already used elsewhere in this file, e.g. the
   ink-section-header background) rather than baking in an RGB triplet.
   min-width/min-height (not just padding) create the capsule footprint; 28px
   radius on a 57px-tall button reads as a near-capsule per the companion
   app's own spec (not a full 50% pill). */
.login-sign-in-btn {
  margin-top: 12px;
  min-width: 200px;
  min-height: 57px;
  padding: 12px 52px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'HF Farmiles', sans-serif;
  font-size: 22px;
  letter-spacing: var(--ls-farmiles);
  color: var(--gold);
  background: color-mix(in srgb, var(--bg-void) 55%, transparent);
  border: 2px solid var(--gold);
  border-radius: 28px;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
  animation: loginFadeRise 350ms ease-out both;
  animation-delay: 640ms;
}
@media (any-hover: hover) {
.login-sign-in-btn:hover {
  background: color-mix(in srgb, var(--gold) 18%, transparent);
  border-color: var(--gold);
  color: #fff;
}
}
/* Disabled/inactive: per the companion app spec, ONLY border + text (+ any
   accent) drop to 45% opacity — the dark background stays exactly as-is. A
   blanket `opacity` on the element would also fade the background, so the
   45% is baked directly into the border/text color values instead. Not
   currently set by app.js, but styled for parity — future double-click
   guarding can rely on it. */
.login-sign-in-btn:disabled {
  color: color-mix(in srgb, var(--gold) 45%, transparent);
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
  cursor: default;
}
@media (any-hover: hover) {
.login-sign-in-btn:disabled:hover {
  background: color-mix(in srgb, var(--bg-void) 55%, transparent);
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
  color: color-mix(in srgb, var(--gold) 45%, transparent);
}
}

/* Anonymous "browse card sets" entry point under the Sign In button. A quiet
   text link so it never competes with the primary Sign In CTA. */
.login-browse-link {
  display: inline-block;
  margin-top: 18px;
  font-size: 14px;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  cursor: pointer;
  animation: loginFadeRise 350ms ease-out both;
  animation-delay: 760ms;
}
@media (any-hover: hover) {
.login-browse-link:hover {
  color: var(--gold-light);
  border-bottom-color: var(--gold-light);
}
}

/* ── Sign-in page language switcher (renderLogin) ───────────────────────────────
   Inline pill row (EN/FR/DE/IT). Reuses the .nav-lang-pill / .nav-lang-pill--<lc>
   colour atoms and the .acs-lang-switch active/dim treatment (opacity .45 → 1 + gold
   border on the selected pill), but stays INLINE at every width — four small pills fit
   a phone — so it does NOT inherit the nav switcher's mobile dropdown-collapse. */
.login-lang-switch {
  /* Reserved band at the TOP of the login page, centered (login-page is a centering
     flex column). flex:none so it keeps its own row — it can NEVER overlap the hero
     text (the top-right absolute version did on a narrow window). Adds a fixed, small
     amount of height; .login-content (flex:1) absorbs the rest and shrinks the logo/
     text to fit, so nothing gets clipped. */
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 14px 0 2px;
  animation: loginFadeRise 350ms ease-out both;
  animation-delay: 320ms;   /* appears just before the welcome hero (400ms) */
}
.login-lang-switch .nav-lang-pill {
  border: 2px solid transparent;
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 0.15s, border-color 0.15s, transform 0.15s;
}
.login-lang-switch .nav-lang-pill.active {
  opacity: 1;
  border-color: var(--gold);
  cursor: default;
}
@media (any-hover: hover) {
  .login-lang-switch .nav-lang-pill:hover { opacity: 0.8; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   END LOGIN PAGE — HF FARMILES FONT ZONE
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Anonymous Card Sets page (P6) — nav language switcher ───────────────────
   The reduced public nav normally shows ONE static .nav-lang-pill; on the
   anonymous /card-sets page it becomes a 4-button switcher so a logged-out
   visitor can browse the catalog in EN/FR/DE/IT. Inactive pills are dimmed and
   clickable; the active one is full-opacity with a gold ring. */
.acs-lang-switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.acs-lang-switch .nav-lang-pill {
  border: 2px solid transparent;
  cursor: pointer;
  opacity: 0.45;
  font-size: 16px;
  min-width: 30px;
  padding: 3px 9px;
  transition: opacity 0.15s, border-color 0.15s, transform 0.15s;
}
@media (any-hover: hover) {
.acs-lang-switch .nav-lang-pill:hover { opacity: 0.8; }
}
.acs-lang-switch .nav-lang-pill.active {
  opacity: 1;
  border-color: var(--gold);
  cursor: default;
}

/* ── Desktop vs. mobile shape of the switcher (one DOM, see _renderAnonymousCardSetsNav).
   DESKTOP (>945px): the trigger is hidden and the four pills render INLINE — identical
   to the original switcher. MOBILE (≤945px): the pills collapse into a dropdown opened
   by the compact trigger, so the pills + Sign In button can never overflow a narrow
   viewport (the bug this fixes). ── */
.acs-lang-trigger {
  display: none;                 /* desktop: hidden — the inline .acs-lang-options shows */
  align-items: center;
  gap: 2px;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
}
.acs-lang-switch .acs-lang-trigger .nav-lang-pill { opacity: 1; }  /* current-lang pill always full */
.acs-lang-options {
  display: inline-flex;          /* desktop: inline pills (unchanged) */
  align-items: center;
  gap: 6px;
}
@media (max-width: 945px) {
  .acs-lang-switch { position: relative; }
  .acs-lang-trigger { display: inline-flex; }
  .acs-lang-options {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;                    /* anchor to the switcher's right edge → opens on-screen */
    flex-direction: column;
    gap: 6px;
    padding: 8px;
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-s);
    box-shadow: 0 14px 36px rgba(0,0,0,0.6);
    z-index: var(--z-nav-pop);   /* above the top-nav */
    display: none;
  }
  .acs-lang-switch.open .acs-lang-options { display: flex; }
  /* In the dropdown every language is a full-opacity coloured pill (a picker, not the
     dimmed desktop switcher); the active one keeps the gold ring. Bigger tap targets. */
  .acs-lang-switch .acs-lang-options .nav-lang-pill {
    opacity: 1;
    font-size: 19px;
    min-width: 46px;
    padding: 6px 14px;
  }
}

/* ── .acs-lang-switch--menu — always-dropdown variant ────────────────────────
   Used by the AUTHENTICATED top nav (see _navLangSwitcherHTML). The base
   component shows four inline pills on desktop and only collapses to a dropdown
   at ≤945px; the authed nav has no room for an inline row beside the avatar and
   username, and testers asked for the companion app's tap-the-pill → menu at
   every width. So this modifier pins the dropdown shape unconditionally.

   ⚠ These rules intentionally MIRROR the ≤945px block above. This is plain CSS
   with no preprocessor, and a media-query wrapper cannot be folded into a
   selector, so the shape cannot be shared without either duplicating it here or
   changing the anonymous nav's desktop appearance (which is out of scope).
   Keep the two blocks in sync — if you retune one, retune the other. ── */
.acs-lang-switch--menu { position: relative; }
.acs-lang-switch--menu .acs-lang-trigger { display: inline-flex; }
.acs-lang-switch--menu .acs-lang-options {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;                      /* anchor to the switcher's right edge → opens on-screen */
  flex-direction: column;
  gap: 6px;
  padding: 8px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  box-shadow: var(--shadow-dropdown);
  z-index: var(--z-nav-pop);     /* above the top-nav */
  display: none;
}
.acs-lang-switch--menu.open .acs-lang-options { display: flex; }
/* Every language in the menu is a full-opacity coloured pill (a picker, not the
   dimmed inline switcher); the current one keeps the gold ring. Bigger tap targets. */
.acs-lang-switch--menu .acs-lang-options .nav-lang-pill {
  opacity: 1;
  font-size: 19px;
  min-width: 46px;
  padding: 6px 14px;
}

/* ── Fantasy window frame ───────────────────────────────────────────────────
   Frame values are documented in :root (--frame-*) and in SPEC.md.
   Each dialog carries its own copy of the frame properties for reliable
   cascade — do NOT rely on a shared grouped selector for these properties.
   When adding a new framed dialog, copy the four lines marked [FRAME] from
   an existing dialog rule and add its own background colour.
   ─────────────────────────────────────────────────────────────────────── */

/* Disclaimer modal */
.disclaimer-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
  /* open/close animation */
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}
.disclaimer-overlay.is-open {
  opacity: 1;
  pointer-events: all;
}
.disclaimer-dialog {
  background: var(--bg-elevated);
  border: 30px solid transparent;                                              /* [FRAME] */
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch; /* [FRAME] */
  border-image-width: 30px;                                                    /* [FRAME] */
  background-clip: padding-box;                                                /* [FRAME] */
  border-radius: 0;
  padding: 16px 20px;
  max-width: 805px;
  max-height: 80vh;
  width: 90%;
  display: flex;
  flex-direction: column;
  gap: 20px;
  overflow: hidden;
  /* open/close animation */
  transform: translateY(60px);
  transition: transform 350ms ease;
}
.disclaimer-overlay.is-open .disclaimer-dialog {
  transform: translateY(0);
}
.disclaimer-dialog-body {
  font-family: 'Bitter', serif;
  font-size: 15px; /* minimum: never go below 15px for body text */
  color: var(--text-main);
  line-height: 1.7;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.disclaimer-version-line {
  color: #ffffff;
  font-size: 16px; /* matches .disclaimer-dialog-body p */
  margin: 0;
}
.disclaimer-dialog-body h1 {
  font-family: 'HF Farmiles', sans-serif;
  font-size: 18px;
  font-weight: normal;
  color: var(--gold-light);
  letter-spacing: var(--ls-farmiles);
  margin-top: 8px;
}
.disclaimer-dialog-body p {
  color: var(--text-muted);
  font-size: 16px;
}
.disclaimer-dialog-body a {
  color: #00D5FF;
  text-decoration: underline;
  text-underline-offset: 3px;
}
@media (any-hover: hover) {
.disclaimer-dialog-body a:hover { color: var(--text-bright); }
}
/* Bug-report template box (Disclaimer dialog, "Found a bug?" section) — sits
   naturally inside .disclaimer-dialog-body (same dark-void/muted palette as
   the rest of the dialog) rather than a visually distinct "card" component.
   Monospace ONLY for the template text, so it reads as literal copy-paste
   content, distinct from the surrounding prose. */
.bug-template {
  position: relative;
  background: var(--bg-void);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 12px 40px 12px 14px;   /* right padding clears the copy icon */
  margin: 4px 0 2px;
}
.bug-template pre {
  margin: 0;
  font-family: 'Courier New', monospace;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--text-secondary, #c9c9dc);
  white-space: pre-wrap;
  word-break: break-word;
}
.bug-template pre .field { color: var(--gold-light); }
/* Copy affordance — top-right corner of the panel, signaling the whole box is
   copyable without a labeled button row. Reuses the app's existing "⧉" copy
   glyph (see pdecks.copyLink) rather than a new icon asset. */
.bug-copy-icon {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-muted, #7a7a92);
  font-size: 15px;
  line-height: 1;
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.14s, color 0.14s;
}
@media (any-hover: hover) {
.bug-copy-icon:hover { border-color: var(--gold, #d4a93b); color: var(--gold-light, #f0c864); }
}
.disclaimer-dialog-close {
  align-self: flex-end;
  font-family: 'HF Farmiles', sans-serif;
  font-size: 18px;
  letter-spacing: var(--ls-farmiles);
  padding: 8px 24px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  color: var(--text-main);
  cursor: pointer;
  transition: background 0.2s;
}
@media (any-hover: hover) {
.disclaimer-dialog-close:hover { background: var(--bg-hover); }
}

/* ═══════════════════════════════════════
   HOME PAGE
   ═══════════════════════════════════════ */
.home-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - var(--nav-h));
  padding: 32px var(--gutter-content) 60px;
  text-align: center;
  position: relative;
}

.home-page::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 50% 50% at 30% 40%, rgba(139,92,246,0.10) 0%, transparent 70%),
    radial-gradient(ellipse 40% 40% at 70% 60%, rgba(212,169,59,0.09) 0%, transparent 70%);
  pointer-events: none;
}

.home-star-field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}

.home-title {
  font-family: 'Bitter', serif;
  font-size: clamp(49px, 9vw, 97px);
  font-weight: 700;
  color: #00BDE5;
  text-shadow:
    0 0 40px rgba(0,189,229,0.50),
    0 0 100px rgba(0,189,229,0.20),
    0 4px 20px rgba(0,0,0,0.8);
  letter-spacing: 5px;
  line-height: 1;
  margin-bottom: 14px;
  position: relative;
}

.home-subtitle {
  font-size: 15px;
  font-weight: 700;
  color: var(--text-main);
  letter-spacing: 4px;
  text-transform: uppercase;
  margin-bottom: 64px;
  font-weight: 300;
  position: relative;
}

.home-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px;
  max-width: 640px;
  width: 100%;
  margin-bottom: 64px;
  position: relative;
}

.home-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 36px 28px;
  cursor: pointer;
  transition: transform 0.3s, border-color 0.3s, box-shadow 0.3s;
  position: relative;
  overflow: hidden;
  text-align: left;
}

.home-card::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.3s;
  border-radius: inherit;
}

.home-card.sets::after {
  background: linear-gradient(135deg, rgba(212,169,59,0.10), rgba(139,92,246,0.06));
}

.home-card.mycollection::after {
  background: linear-gradient(135deg, rgba(139,92,246,0.11), rgba(59,130,246,0.06));
}

@media (any-hover: hover) {
.home-card:hover {
  border-color: var(--gold);
  transform: translateY(-6px);
  box-shadow: 0 24px 64px rgba(0,0,0,0.4), 0 0 32px var(--gold-glow);
}
}

@media (any-hover: hover) {
.home-card:hover::after { opacity: 1; }
}

.home-card-icon {
  font-size: 47px;
  margin-bottom: 18px;
  line-height: 1;
}

.home-card-title {
  font-family: 'Bitter', serif;
  font-size: 20px;
  font-weight: 600;
  color: var(--text-bright);
  margin-bottom: 8px;
  letter-spacing: 0.5px;
}

.home-card-desc {
  font-size: 14px;
  color: var(--text-muted);
  line-height: 1.65;
}

.home-card-arrow {
  position: absolute;
  bottom: 20px;
  right: 20px;
  font-size: 19px;
  color: var(--text-muted);
  transition: color 0.2s, transform 0.2s;
}

@media (any-hover: hover) {
.home-card:hover .home-card-arrow {
  color: var(--gold);
  transform: translateX(4px);
}
}

.home-stats {
  display: flex;
  gap: 48px;
  position: relative;
}

.home-stat {
  text-align: center;
}

.home-stat strong {
  display: block;
  font-family: 'Bitter', serif;
  font-size: 33px;
  color: var(--gold-light);
  margin-bottom: 4px;
  text-shadow: 0 0 20px var(--gold-glow);
}

.home-stat span {
  font-size: 13px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 1px;
}

/* ── Home page illumineer picker ── */
.home-ill-section {
  width: 100%;
  max-width: 700px;
  margin-bottom: 40px;
  position: relative;
}

.home-ill-label {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-main);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin-bottom: 12px;
  text-align: center;
}

.home-ill-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}

.home-ill-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 14px 22px;
  border-radius: var(--radius-m);
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
  min-width: 120px;
  font-family: inherit;
}

@media (any-hover: hover) {
.home-ill-card:hover {
  border-color: var(--border-hover);
  background: var(--bg-hover);
}
}

.home-ill-card.active {
  border-color: var(--gold);
  background: var(--gold-dim);
  box-shadow: 0 0 14px var(--gold-glow);
}

.home-ill-avatar { font-size: 26px; line-height: 1; display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; }

.home-ill-avatar-emoji { font-size: 26px; line-height: 1; }

.home-ill-avatar-img {
  width: 44px;
  height: 44px;
  object-fit: contain;
  border-radius: 6px;
}

.home-ill-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-bright);
}

.home-ill-card.active .home-ill-name { color: var(--gold-light); }

.home-ill-since {
  font-size: 11px;
  color: var(--text-dim);
}

.home-ill-active-badge {
  font-size: 10px;
  color: var(--gold);
  letter-spacing: 1px;
  text-transform: uppercase;
}

.home-ill-new {
  border-style: dashed;
  color: var(--text-muted);
}

@media (any-hover: hover) {
.home-ill-new:hover {
  border-color: var(--gold);
  color: var(--gold-light);
}
}

.home-ill-new .home-ill-avatar {
  font-size: 20px;
  color: inherit;
}


/* ═══════════════════════════════════════
   HOME PAGE v2 (redesigned)
   ═══════════════════════════════════════ */

.home-v2 {
  padding: 18px var(--gutter-content);   /* was …80px bottom — removed the dead space below the set grid (#app-root already reserves --bottom-bar-h) */
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-height: calc(100vh - var(--nav-h) - var(--bottom-bar-h));
}

.home-section-label {
  /* Matches the top-nav Farmiles text exactly: same family, 19px, tracking,
     weight, muted colour (see .nav-link). */
  font-family: var(--font-display);
  font-size: var(--fz-lg);
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  color: var(--text-muted);
  margin-bottom: 10px;
}

.section-label-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.section-label-row .home-section-label { margin-bottom: 0; }

/* ── Welcome banner ─────────────────────────────────────────────────────── */
.home-welcome {
  /* Component token (co-located, gallery-editable): the greeting card's own
     max width. See THEME_TOKENS.md → component tokens. */
  --greeting-max-w: 860px;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 18px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-l);
  position: relative;
  overflow: hidden;
  /* Independently bounded — does NOT share --max-content-width with the
     filter bars. The welcome card is a compact greeting and reads as
     cluttered at the filter-bar width on wide monitors. 860 px is ~33%
     narrower than the filter-bar cap; tune separately if needed. */
  max-width: var(--greeting-max-w);
  margin-left: auto;
  margin-right: auto;
  width: 100%;
}

/* Gradient overlay removed 2026-06-18 — the greeting panel now matches the solid
   --bg-elevated surface of the .stat-chip panels (one consistent panel surface). */

.welcome-avatar {
  width: 52px;
  height: 52px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--bg-panel), var(--bg-surface));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}

.welcome-avatar img {
  width: 100%;
  height: 100%;
  border-radius: 8px;
  object-fit: contain;
}

.welcome-text {
  position: relative;
  z-index: 1;
  flex: 1;
  min-width: 0;
}

.welcome-greeting {
  font-family: var(--font-display);
  font-size: 20px;   /* fixed — fits the 16-char name cap at every width; do NOT make responsive (it read as the text "shrinking" on narrow windows) */
  letter-spacing: var(--ls-farmiles);
  color: var(--text-bright);
  margin-bottom: 3px;
}

.welcome-greeting strong { color: var(--gold-light); font-weight: normal; }

.welcome-sub {
  font-family: var(--font-ui);
  font-size: var(--fz-base);
  color: var(--text-muted);
}

.welcome-last-login {
  margin-left: auto;
  flex-shrink: 0;
  text-align: right;
  position: relative;
  z-index: 1;
}

.welcome-last-login-label {
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 3px;
}

.welcome-last-login-value {
  font-family: var(--font-ui);
  font-size: var(--fz-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
}

/* ── Stats chips ─────────────────────────────────────────────────────────── */
.home-v2 .stats-grid {
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 10px;
  margin-bottom: 0; /* neutralise the global .stats-grid bottom margin */
}

.stat-chip {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 14px 18px;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 16px;
  cursor: default;
  transition: border-color .2s, box-shadow .2s;
  position: relative;
  /* NO overflow:hidden — the per-rarity radial glow that needed it was removed;
     keeping it would clip the number/label on narrow tiles. */
}

.stat-chip::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .2s;
  border-radius: inherit;
}

@media (any-hover: hover) {
.stat-chip:hover::after { opacity: 1; }
}

/* Fully uniform stat tiles (2026-06-18): every tile shares the base .stat-chip
   border / surface / hover AND the same number colour (--text-bright). The
   per-rarity border tints, radial glows, hover shadows, and number colours were
   all removed — "all panels look the same from the component". The rarity
   modifier classes (.total/.foil/.enchanted/…) remain on the markup but no longer
   carry styling (kept as inert hooks for a possible future opt-in accent). */

.stat-icon { width: 48px; height: 48px; flex-shrink: 0; position: relative; z-index: 1; opacity: 0.85; }
.stat-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }

.stat-chip-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  min-width: 0;   /* allow the body to shrink/wrap so the icon never gets squashed on narrow tiles (folded Fold) */
}

.stat-val {
  font-family: var(--font-ui);
  font-size: var(--fz-xl);
  font-weight: var(--fw-bold);
  color: var(--text-bright);
  line-height: 1;
  position: relative;
  z-index: 1;
}

.stat-label {
  font-family: var(--font-ui);
  font-size: var(--fz-xs);
  font-weight: 800;   /* one-off: extra-heavy micro-label (no --fw-800 token) */
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: left;
  position: relative;
  z-index: 1;
}

/* Home stat tiles — explicit responsive column count so a cell is never too
   small (text clips) nor too large. Desktop (>945) keeps the auto-fill grid
   above. The icon holds 48px (flex-shrink:0), so the COLUMN COUNT — not a
   shrinking body — is what guarantees the icon + number + label all fit:
   ≤945 (unfolded Fold / tablet) → 3 per row, ≤600 (phones / folded) → 2 per row.
   Internals shrink slightly on mobile so 3-up still fits comfortably. */
@media (max-width: 945px) {
  .home-v2 .stats-grid { grid-template-columns: repeat(3, 1fr); }
  .home-v2 .stat-chip  { padding: 12px; gap: 12px; }
  .home-v2 .stat-icon  { width: 36px; height: 36px; }
  .home-v2 .stat-val   { font-size: var(--fz-lg); }
}
@media (max-width: 600px) {
  .home-v2 .stats-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ── ? help button ───────────────────────────────────────────────────────── */
.help-btn {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  border: 1px solid var(--gold);
  background: var(--gold-dim);
  color: var(--gold-light);
  font-size: 9px;
  font-weight: 800;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color .15s, color .15s, background .15s, box-shadow .15s;
  flex-shrink: 0;
  padding: 0;
}

@media (any-hover: hover) {
.help-btn:hover {
  border-color: var(--gold-light);
  color: var(--text-bright);
  background: rgba(212,169,59,0.25);
  box-shadow: 0 0 8px var(--gold-glow);
}
}

/* ── Set summary grid ────────────────────────────────────────────────────── */
.sets-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 12px;
}

/* ── Set summary card ────────────────────────────────────────────────────── */
.set-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  will-change: border-color, box-shadow;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  cursor: pointer;
  transition: border-color .2s, box-shadow .2s;
}

@media (any-hover: hover) {
.set-card:hover {
  border-color: var(--gold);
  box-shadow: 0 0 0 1px var(--gold), 0 0 16px var(--gold-glow);
}
}

@media (any-hover: hover) {
.set-card:hover .set-id-pill { background: rgba(0,0,0,0.88); }
}

.set-header {
  position: relative;
  min-height: 81px;
  flex-shrink: 0;
  background: var(--bg-panel);
  line-height: 0;        /* prevent gap beneath inline image */
  padding-top: 10px;     /* mirror the visual left/right gap from object-fit:contain */
}

.set-thumb {
  width: 100%;
  height: 81px;     /* matches .set-chip-img on the Card Sets page */
  display: block;
  object-fit: contain;
}

.set-fallback-name {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 6px 10px;
  font-family: var(--font-ui);
  font-size: 11px;   /* one-off micro-label (below --fz-xs) */
  font-weight: var(--fw-semibold);
  color: var(--text-bright);
  text-align: center;
  text-shadow: 0 1px 6px rgba(0,0,0,0.9);
}

.set-body {
  padding: 10px 12px 12px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  flex: 1;
}

.prog-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 4px;
}

.prog-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.3px;
  text-transform: uppercase;
  color: var(--text-muted);
}

.prog-count {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-main);
}

.prog-track {
  height: 6px;
  background: rgba(255,255,255,0.05);
  border-radius: 3px;
  overflow: hidden;
}

.prog-fill { height: 100%; border-radius: 3px; }
.prog-fill.base          { background: linear-gradient(90deg, #1a4fa8, var(--sapphire)); }
.prog-fill.add           { background: linear-gradient(90deg, #92610a, var(--gold-light)); }
.prog-fill.base.complete,
.prog-fill.add.complete  { background: linear-gradient(90deg, #047857, var(--emerald)); }

.set-divider {
  height: 1px;
  background: var(--border);
  margin: 0 -12px;
}

/* ── Rarity tiers help dialog ────────────────────────────────────────────── */
/* Overlay + open/close animation mirror the Disclaimer dialog so every
   blocking modal in the app shares the same opacity-fade + slide-up
   entry. Cannot use `display: none → flex` because CSS transitions don't
   run from display:none; use opacity + pointer-events instead. The
   .open class is toggled by app.js (#home-help-overlay click handlers). */
.help-overlay {
  display: flex;
  position: fixed;
  inset: 0;
  background: rgba(4,4,15,0.72);
  backdrop-filter: blur(4px);
  z-index: 500;
  align-items: center;
  justify-content: center;
  padding: 20px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}

.help-overlay.open { opacity: 1; pointer-events: all; }

.help-dialog {
  background: var(--bg-elevated);
  /* Fantasy window border — same asset + pattern used by .disclaimer-dialog.
     See SPEC.md → "Base Set & Additional Cards help dialog" for the rule. */
  border: 30px solid transparent;
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch;
  border-image-width: 30px;
  background-clip: padding-box;
  border-radius: 0;
  padding: 26px 26px 22px;
  max-width: 460px;
  width: 100%;
  position: relative;
  box-shadow: 0 20px 50px rgba(0,0,0,0.6);
  transform: translateY(60px);
  transition: transform 350ms ease;
}

.help-overlay.open .help-dialog { transform: translateY(0);
}

.help-dialog-close {
  position: absolute;
  top: 12px;
  right: 14px;
  font-size: 16px;
  color: var(--text-dim);
  cursor: pointer;
  line-height: 1;
  background: none;
  border: none;
  transition: color .15s;
}

@media (any-hover: hover) {
.help-dialog-close:hover { color: var(--text-bright); }
}

.help-dialog-title {
  font-family: 'Bitter', serif;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-bright);
  margin-bottom: 18px;
  padding-right: 20px;
}

.help-tier {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 12px 14px;
  margin-bottom: 10px;
}

.help-tier:last-of-type { margin-bottom: 0; }

/* Section titles ("Base Set" / "Additional Cards") match the look of the
   filter-bar group labels (.filter-label) on the My Cards page — same
   size, weight, letter-spacing, and gold-light color. Both .base and .add
   variants share the look intentionally (no per-variant color override). */
.help-tier-name {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--gold-light);
  margin-bottom: 5px;
}

.help-tier-desc {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.5;
}

.help-rarity-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 8px;
}

@media (any-hover: hover) {
/* The help-dialog pills now share the .f-chip class with the filter bar.
   .f-chip-static is a display-only modifier — disables the pointer cursor
   and gold hover/focus state since these pills aren't interactive. */
.f-chip-static,
.f-chip-static:hover,
.f-chip-static:focus {
  cursor: default;
  border-color: var(--border);
  color: var(--text-muted);
}
}

/* ── Set detail dialog (stone frame) ─────────────────────────────────────── */
.set-detail-overlay {
  display: flex;
  position: fixed;
  inset: 0;
  background: rgba(4,4,15,0.75);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  z-index: 500;
  align-items: center;
  justify-content: center;
  padding: 24px;
  /* open/close animation — matches .disclaimer-overlay */
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}

.set-detail-overlay.open {
  opacity: 1;
  pointer-events: all;
}

.set-detail-dialog {
  background: var(--bg-elevated);
  border: 30px solid transparent;                                                                      /* [FRAME] */
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch;   /* [FRAME] */
  border-image-width: 30px;                                                                            /* [FRAME] */
  background-clip: padding-box;                                                                        /* [FRAME] */
  border-radius: 0;
  max-width: 390px;
  width: 100%;
  max-height: calc(100vh - 80px);
  overflow-y: auto;
  position: relative;
  /* slide animation — matches .disclaimer-dialog */
  transform: translateY(60px);
  transition: transform 350ms ease;
}

.set-detail-overlay.open .set-detail-dialog {
  transform: translateY(0);
}

.set-detail-content {
  padding: 5px 3px 8px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.set-detail-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(0,0,0,0.55);
  border: 1px solid rgba(255,255,255,0.18);
  color: var(--text-muted);
  font-size: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
  z-index: 10;
}

@media (any-hover: hover) {
.set-detail-close:hover {
  background: rgba(255,255,255,0.14);
  color: var(--text-bright);
  border-color: rgba(255,255,255,0.35);
}
}

.sdlg-close-btn {
  width: 100%;
  padding: 9px;
  background: var(--bg-panel);
  border: 1px solid var(--border-hover);
  border-radius: var(--radius-s);
  color: var(--text-muted);
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .5px;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
  margin-top: 2px;
}

@media (any-hover: hover) {
.sdlg-close-btn:hover {
  background: var(--bg-panel-alt);
  color: var(--text-bright);
  border-color: var(--gold);
}
}

.sdlg-header {
  position: relative;
  /* negative margins counteract the 5px top / 3px side content padding
     so the image bleeds edge-to-edge inside the dialog frame */
  margin: -5px -3px 0;
  background: var(--bg-panel);
  line-height: 0;
  overflow: hidden;
}

.sdlg-header img {
  width: 100%;
  /* Every card-set image is a fixed 300x169 -- match that ratio instead of a
     fixed height so the box never crops the artwork (a forced height shorter
     than width/300*169 chopped the bottom of the banner text). object-fit is
     a no-op safety net if a future image isn't exactly 300x169. */
  aspect-ratio: 300 / 169;
  display: block;
  object-fit: cover;
}

.sdlg-header .set-fallback-name { font-size: 16px; }

.sdlg-owned-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.4px;
  color: var(--gold-light);
  text-transform: uppercase;
  text-align: center;
}

.sdlg-divider { height: 1px; background: var(--border); }

.sdlg-section-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text-muted);   /* matches .prog-label on home page; no per-section colour override */
  margin-bottom: 8px;
}

.sdlg-type-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 7px;
}

.sdlg-type-row:last-child { margin-bottom: 0; }

.sdlg-type-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  width: 116px;   /* widened slightly to accommodate larger font */
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 5px;
}

.sdlg-type-icon { font-size: 13px; line-height: 1; flex-shrink: 0; display: flex; align-items: center; }
.sdlg-type-icon img { width: 16px; height: 16px; object-fit: contain; }

.sdlg-type-bar {
  flex: 1;
  height: 6px;
  background: rgba(255,255,255,0.05);
  border-radius: 3px;
  overflow: hidden;
}

/* fill color is set inline per-rarity via JS (_BASE_RARITY_CONFIG / _ADD_RARITY_CONFIG) */
.sdlg-type-fill { height: 100%; border-radius: 3px; opacity: 0.9; }

.sdlg-type-count {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-main);
  text-align: right;
  width: 72px;
  flex-shrink: 0;
}

@media (max-width: 600px) {
  .home-v2 { padding: 14px 12px 80px; }
  .sets-grid { grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); }
  /* .home-v2 .stats-grid column count is set in the dedicated stat-tile media
     block above (≤600 → 2 per row). The old minmax(100px) rule here produced 4+
     cramped columns on a folded phone and clipped the text — removed. */
}


/* ═══════════════════════════════════════
   SETS PAGE
   ═══════════════════════════════════════ */
.sets-page {
  display: flex;
  flex-direction: column;
}

/* Set selector grid */
.set-selector {
  padding: 14px var(--gutter-content) 10px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 8px;
  flex-shrink: 0;
}

/* Sets page content padding.
   The unified card-grid surface (mountCardGridSurface) renders as:
       host (#set-cards-area)
         ├── #set-filter-bar       ← keeps its own `margin: 0 20px` inset
         └── .set-content-area     ← the "Showing N cards" line + grid.
   .sets-page has no padding of its own (unlike My Cards / Missing, which get
   a single consistent inset from #ill-tab-content), so #set-filter-bar and
   .set-content-area must each carry a MATCHING inset independently. Use the
   filter bar's own 20px (not the shared --gutter-content 15px) — using the
   mismatched token here previously left the count text sitting 5px further
   out than the filter bar directly above it. */
.set-content-area {
  /* Top gap so the "Showing N cards" line isn't flush against #set-filter-bar.
     .sets-page has no page padding and the filter bar carries no bottom margin,
     so unlike My Cards / Missing (which get breathing room from their filter
     bar's bottom padding) the count line here needs an explicit top inset. */
  padding: 10px 20px 0;
}


/* Base chip — text-only keeps the pill feel */
.set-chip {
  padding: 7px 18px;
  border-radius: 20px;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
  text-align: center;
  font-family: inherit;
}

/* Image variant — card-style, image only */
.set-chip.has-img {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  border-radius: 10px;
}

.set-chip-img {
  width: 100%;
  height: 81px;
  object-fit: contain;
  border-radius: 4px;
  pointer-events: none;
}

@media (any-hover: hover) {
.set-chip:hover { border-color: var(--border-hover); color: var(--text-bright); }
}
.set-chip.active { border-color: var(--gold); background: var(--gold-dim); color: var(--gold-light); }
.set-chip.has-img.active { box-shadow: 0 0 12px var(--gold-glow); }

/* ── Set image wrapper + set-ID pill ──────────────────────────────────────────
   .set-img-wrap is a position:relative block placed around every card-set thumbnail
   image so the pill can be anchored to its bottom-right corner regardless of context
   (set-chip, collection progress card, card-detail panel).                        */
.set-img-wrap {
  position: relative;
  display: block;
}
/* Inside the flex set-chips the wrapper must fill the chip's padded area */
.set-chip.has-img .set-img-wrap {
  width: 100%;
}

/* The pill itself — dark translucent background + bright gold text for maximum
   legibility over any card-art colour, including very bright or very dark images. */
.set-id-pill {
  position: absolute;
  bottom: 5px;
  right: 5px;
  min-width: 22px;        /* comfortably holds 3 characters, e.g. "Q12" */
  padding: 2px 5px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.78);
  color: var(--gold-light);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0.04em;
  text-align: center;
  pointer-events: none;
  user-select: none;
  z-index: 2;
}



/* ═══════════════════════════════════════
   FILTER BAR
   ═══════════════════════════════════════ */
.filter-bar {
  background: var(--bg-deep);
  border-bottom: 1px solid var(--border);
  padding: 8px var(--gutter-content);
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex-shrink: 0;
}

.filter-row {
  display: flex;
  width: 100%;              /* stretch to full container width whether parent is row or column flex */
  gap: 6px 16px;
  align-items: flex-start; /* flex-start so label pins to the top of a tall wrapping group */
  flex-wrap: wrap;
}

/* Keeps a label and its content group as an inseparable unit.
   Multiple sections in one row share equal width via flex: 1 1 0. */
.filter-section {
  display: flex;
  flex-wrap: nowrap;        /* label and group NEVER wrap onto different lines */
  align-items: flex-start;
  gap: 8px;
  flex: 1 1 0;
  min-width: 0;
}

.filter-group {
  display: flex;
  gap: 4px;
  align-items: center;
  flex-wrap: wrap;
  flex: 1 1 0;   /* fill remaining space inside section; chips wrap within it */
  min-width: 0;
}

/* ── Filter chip tooltips ──
   data-tooltip="..." on a chip shows a text bubble below the chip after 1.5 s hover.
   The tooltip disappears instantly on mouse-out (transition-delay 0 s on base state). */
[data-tooltip] { position: relative; }
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute;
  top: calc(100% + 5px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  color: var(--text-main);
  font-size: 12px;
  font-weight: 400;
  white-space: nowrap;
  padding: 4px 9px;
  pointer-events: none;
  opacity: 0;
  transition-delay: 0s;
  z-index: 200;
}
@media (any-hover: hover) {
[data-tooltip]:hover::after {
  opacity: 1;
  transition-delay: 1s;
}
}

.filter-label {
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--gold-light);
  margin-right: 2px;
  flex-shrink: 0;
  padding-top: 5px; /* optical alignment with chip text */
}

/* Shared horizontal padding for ALL filter-bar controls — the toggle chips
   (.f-chip) AND the multi-select dropdown triggers (.set-ms-trigger). Defined
   once so any future "filter pill shape" decision propagates to every filter
   bar in the app (both classes are already shared across My Cards, Missing
   Cards, Add Cards, Card Sets, Deck Detail and the Deck Builder). Edit this one
   value to change the L/R padding everywhere. */
:root { --filter-pill-pad-x: 9px; }   /* was 11px on chips / 10px on dropdowns */

.f-chip {
  padding: 3px var(--filter-pill-pad-x);
  border-radius: 13px;
  border: 1px solid var(--border);
  background: var(--bg-filter-control);
  color: var(--text-muted);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.18s;
  font-family: inherit;
  display: inline-flex;
  align-items: center;
}

@media (any-hover: hover) {
.f-chip:hover,
.f-chip:focus { outline: none; border-color: var(--gold); color: var(--text-bright); }
}
.f-chip.on {
  background:   var(--gold);
  border-color: var(--gold);
  color:        #1a1405;
  font-weight:  700;
}
/* Colour ink chips while the Multi-Ink exclusive filter is active (deck
   surfaces only) — greyed + inert. The click handler also ignores them. */
.f-chip--disabled {
  opacity: 0.4;
  pointer-events: none;
}
/* Off-ink colour chips in the HUNNY ONLY filter row (a Gather-the-Party
   exemption deck — see SPEC.md). Dashed gold border + a bee marker signal
   "reachable, but only via the exemption", distinct from the deck's own
   solid-border ink chips. The dash persists in the .on (selected) state so the
   exempt nature stays legible against the gold fill. */
.f-chip--offink {
  border-style: dashed;
  border-color: var(--gold);
}
/* hunny_icon.svg marker on an off-ink chip. display:inline-block (not the global
   img{display:block}) so the SVG's width="100%" doesn't collapse — see .rc-icon. */
.f-chip-bee {
  display: inline-block;
  width: 14px; height: 14px;
  object-fit: contain;
  margin-left: 5px;
  vertical-align: middle;
}
/* The Inkable / Not Inkable SVGs have a hard-coded fill:#c6a200 (gold) that
   disappears against the gold-selected chip background. Recolor them to a
   dark navy when the chip is in the .on state:
     invert(1)  → #c6a200 (gold) inverts to ~#395dff (blue)
     brightness(0.25) → darkens that blue to ~#0e1740 (dark navy). */
.f-chip.on[data-ink="Inkable"]     .f-chip-icon,
.f-chip.on[data-ink="Not Inkable"] .f-chip-icon {
  filter: invert(1) brightness(0.25);
}

.f-chip-icon {
  width: 15px;
  height: 15px;
  vertical-align: middle;
  margin-right: 5px;
  display: inline-block;
}
/* Inside a .set-ms-trigger the icon→text spacing comes from the trigger's flex
   `gap` (6px); the icon's own 5px margin-right would STACK on top of it, making the
   Willpower / Strength / Lore dropdowns gap noticeably wider than the .f-chip
   buttons. Zero the margin so only the flex gap applies — matches the chips. Shared
   rule → every surface (desktop filter bar, mobile dialog, deck builder, deck detail). */
.set-ms-trigger .f-chip-icon { margin-right: 0; }

.f-chip-emoji {
  margin-right: 4px;
  font-size: 12px;
  vertical-align: middle;
}

/* ── Unified pill color tokens ──────────────────────────────────────────────
   CSS custom properties set on any element with a data-ink / data-rarity /
   data-type attribute.  Referenced by both filter chip .on state and detail
   panel badges — one definition, zero duplication.
   ───────────────────────────────────────────────────────────────────────── */

/* Ink */
[data-ink="Amber"]       { --pill-c: var(--amber);    --pill-bg: rgba(245,158,11,0.12); }
[data-ink="Amethyst"]    { --pill-c: var(--amethyst); --pill-bg: rgba(139,92,246,0.12); }
[data-ink="Emerald"]     { --pill-c: var(--emerald);  --pill-bg: rgba(16,185,129,0.12); }
[data-ink="Ruby"]        { --pill-c: var(--ruby);     --pill-bg: rgba(239,68,68,0.12); }
[data-ink="Sapphire"]    { --pill-c: var(--sapphire); --pill-bg: rgba(59,130,246,0.12); }
[data-ink="Steel"]       { --pill-c: var(--steel);    --pill-bg: rgba(148,163,184,0.12); }
[data-ink="Multi-Ink"]   { --pill-c: #a78bfa;         --pill-bg: rgba(167,139,250,0.12); }
[data-ink="Inkless"]     { --pill-c: var(--inkless);  --pill-bg: rgba(168,168,168,0.12); }
[data-ink="Inkable"]     { --pill-c: var(--gold);     --pill-bg: rgba(212,169,59,0.12); }
[data-ink="Not Inkable"] { --pill-c: var(--steel);    --pill-bg: rgba(148,163,184,0.12); }

/* Rarity */
[data-rarity="Common"]     { --pill-c: var(--r-common);    --pill-bg: rgba(148,163,184,0.12); }
[data-rarity="Uncommon"]   { --pill-c: var(--r-uncommon);  --pill-bg: rgba(74,222,128,0.12); }
[data-rarity="Rare"]       { --pill-c: var(--r-rare);      --pill-bg: rgba(96,165,250,0.12); }
[data-rarity="Super Rare"] { --pill-c: var(--r-super);     --pill-bg: rgba(192,132,252,0.12); }
[data-rarity="Legendary"]  { --pill-c: var(--r-legendary); --pill-bg: rgba(251,191,36,0.12); }
[data-rarity="Enchanted"]  { --pill-c: var(--r-enchanted); --pill-bg: rgba(244,114,182,0.12); }
[data-rarity="Special"]    { --pill-c: var(--r-special);   --pill-bg: rgba(251,146,60,0.12); }
[data-rarity="Epic"]       { --pill-c: var(--r-epic);      --pill-bg: rgba(248,113,113,0.12); }
[data-rarity="Iconic"]     { --pill-c: var(--r-iconic);    --pill-bg: rgba(34,211,238,0.12); }

/* Type */
[data-type="Character"] { --pill-c: var(--amethyst); --pill-bg: rgba(139,92,246,0.15); }
[data-type="Location"]  { --pill-c: var(--emerald);  --pill-bg: rgba(16,185,129,0.15); }
[data-type="Item"]      { --pill-c: var(--sapphire); --pill-bg: rgba(59,130,246,0.15); }
[data-type="Song"]      { --pill-c: var(--gold);     --pill-bg: rgba(212,169,59,0.15); }
[data-type="Action"]    { --pill-c: var(--ruby);     --pill-bg: rgba(239,68,68,0.15); }

.filter-search {
  background: var(--bg-filter-control);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 4px 11px;
  color: var(--text-main);
  font-size: 12px;
  width: 170px;
  transition: border-color 0.2s;
}

.filter-search:focus { outline: none; border-color: var(--gold); }
.filter-search::placeholder { color: var(--text-dim); }


/* ═══════════════════════════════════════
   CARD GRID
   ═══════════════════════════════════════ */
.cards-grid {
  display: grid;
  /* 2-column FLOOR on narrow phones. min(200px, (100% - gap)/2) caps each column's
     min-width at half the container (minus the one 12px gap), so >=2 columns ALWAYS
     fit — a ~390px iPhone that used to show 1 card now shows 2. Once the container
     clears ~412px the 200px min wins again, so it behaves EXACTLY as before and grows
     to 3/4/... columns as space allows (unfolded foldable, desktop unchanged). */
  grid-template-columns: repeat(auto-fill, minmax(min(200px, (100% - 12px) / 2), 1fr));
  gap: 12px;
}

/* Location sub-grid for regular card grids (Card Sets, Collection, Missing).
   Portrait cards use minmax(200px,1fr); location cells are proportionally wider
   (200 × 10/7 ≈ 286px) to match the visual footprint of a rotated portrait card. */
.cards-grid-locations {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
  margin-bottom: 12px;
}

/* Transparent wrapper covering both sub-grids (.cards-grid-locations + .cards-grid)
   within one ink section. display:contents means it generates no box of its own,
   so the two child grids lay out exactly as if they were direct children of the
   parent. The wrapper exists purely as a single DOM node so that
   _renderInkSectionsLazy's sentinel.previousElementSibling finds one element and
   _activateCardImages covers both grids in a single call. */
.section-card-grids {
  display: contents;
}

.card-thumb {
  position: relative;
  /* contain: paint removed — paint containment clips the element's own rendering to its
     border box, which swallows the hover transform (translateY + scale) and box-shadow
     overflow at scale 1.1, making the animation look instant.
     contain: layout style is kept for layout-isolation scroll-performance benefit. */
  contain: layout style;
  border-radius: var(--radius-m);
  overflow: hidden;
  cursor: pointer;
  /* Transition defined on the base element (not gated on @media hover:hover) so it
     works reliably on hybrid touchscreen laptops that may report hover:none even when
     a mouse is connected.  Touch devices never fire :hover so the transition property
     is tracked but never activated — the cost is negligible. */
  transition: transform 0.24s, box-shadow 0.24s, border-color 0.24s;
  /* Placeholder = the Lorcana card back, full-bleed (cover), shown until the real
     card image loads over it. Portrait tiles use this element background directly;
     location tiles override it with a rotated ::before (see .card-thumb--location). */
  background: url('../images/cards/card_back.jpg') center/cover no-repeat var(--bg-elevated);
  border: 1px solid var(--border);
  aspect-ratio: 700 / 977; /* matches mid-size stored images (700 px wide, proportional from 734×1024 lorcanajson source) */
}

@media (any-hover: hover) {
.card-thumb:hover {
  transform: translateY(-20px) scale(1.1);
  border-color: var(--card-ink-color, var(--gold));
  box-shadow: 0 18px 48px rgba(0,0,0,0.5), 0 0 28px var(--card-ink-glow, var(--gold-glow));
  z-index: 5;
}
}

@media (any-hover: hover) {
/* Multi-ink cards: primary ink border stays, extra ink rings + directional glows override the shadow */
.card-thumb[data-multi-ink="true"]:hover {
  box-shadow: var(--card-multi-shadow);
}
}

.card-thumb.selected {
  transform: translateY(-20px) scale(1.1);
  border-color: var(--emerald);
  box-shadow: 0 18px 48px rgba(0,0,0,0.5), 0 0 22px rgba(16, 185, 129, 0.4);
  z-index: 5;
}

.card-thumb img {
  width: 100%;
  display: block;
  /* natural aspect ratio — no forced height, no cropping.
     src is set only after the image is already in the browser cache
     (via IntersectionObserver + Image probe), so it renders instantly
     without ever flashing the placeholder through a semi-transparent image. */
}

/* While the lazy-loader hasn't fired yet, <img data-src="…"> has no src.
   Browsers render a broken-image icon + alt text in that state, which
   overlaps the card placeholder background.
   visibility:hidden keeps the element in the layout (so the
   IntersectionObserver still detects it and fires) but makes it invisible
   until the observer removes data-src and sets src — at which point this
   rule no longer matches and the loaded image appears. */
.card-thumb img[data-src] {
  visibility: hidden;
}

/* No image placeholder */
.card-no-img {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px;
  text-align: center;
}

.card-no-img .ni-icon { font-size: 31px; color: var(--text-dim); }
.card-no-img .ni-name { font-size: 11px; color: var(--text-muted); line-height: 1.4; }

/* Badge container — positions both badges as a stacked column */
.card-badges {
  position: absolute;
  top: 6px;
  right: 6px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 5px;
}

/* Individual qty badge — bigger + bolder than before (2026-06-06) so the
   counts are notable at a glance.
   STANDARD-quality count: white pill, dark text (this base rule).
   FOIL-quality count: cyan→yellow gradient (the .card-qty-foil-badge
   override below). The single deck-count pill on the Deck Detail page
   uses this same base white style.
   No backdrop-filter — My Collection renders up to 2 badges per card and
   each blur pass is a composited GPU layer that janks scroll on mobile.
   The solid fill + small drop-shadow gives the pop without the blur cost. */
.card-qty-badge {
  background: #ffffff;
  border: 1.5px solid rgba(0, 0, 0, 0.55);
  border-radius: 13px;
  padding: 3px 11px;
  font-size: 15px;
  font-weight: 700;
  color: #0a0a0a;
  line-height: 1.25;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  /* The widest possible label is "×200" (MAX_CARD_QTY_PER_IDENTIFIER = 200,
     reachable on either the standard or foil pill). The pill has no width
     cap so it grows to fit and can't truncate; nowrap guarantees the count
     never wraps to a second line even if a future, narrower layout
     constrains the badge width. */
  white-space: nowrap;
}

.card-qty-foil-badge {
  background: linear-gradient(135deg, #00FFFB 0%, #FFCF00 100%);
  border-color: rgba(0, 0, 0, 0.55);
  color: #0a0a0a;
}

/* Hunny exemption badge — first child of .card-badges on DECK surfaces only
   (off-ink exempt cards; scoped via ctx.exemptBadge). Lives in the same corner
   column as the qty pills, so it inherits the side .card-badges uses (top-right
   portrait / top-left location) and sits ABOVE the count pills. The icon uses
   inline-block sizing (the global img{display:block} would collapse the
   width="100%" SVG — see .rc-icon). */
.card-hunny-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 8px;
  border-radius: 11px;
  background: var(--gold, #d9b76b);
  color: #1a1405;
  font-size: 12px;
  font-weight: 700;
  border: 1.5px solid rgba(0, 0, 0, 0.55);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  white-space: nowrap;
}
.card-hunny-badge .che-icon {
  display: inline-block;
  width: 13px;
  height: 13px;
  object-fit: contain;
}

/* Location cards: move badges to top-left so they don't overlap the ink cost
   (bottom-right corner of the rotated landscape card). Portrait cards keep
   their badges top-right. */
.card-thumb--location .card-badges {
  right: auto;
  left: 6px;
  align-items: flex-start;
}

/* Card type emoji badge — bottom-right corner of collection grid cards */
/* No backdrop-filter — same reasoning as .card-qty-badge: one blur layer per
   card per grid creates GPU compositing cost during scroll on mobile. Higher
   alpha solid background is visually equivalent for this small corner badge. */
.card-type-badge {
  position: absolute;
  bottom: 6px;
  right: 6px;
  font-size: 12px;
  line-height: 1;
  background: rgba(7, 7, 26, 0.88);
  border-radius: 6px;
  padding: 3px 5px;
  pointer-events: none;
}

/* Ink dots (revealed on hover) */
.card-thumb-inks {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 22px 6px 5px;
  background: linear-gradient(transparent, rgba(4,4,15,0.92));
  display: flex;
  gap: 3px;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.2s;
}

@media (any-hover: hover) {
.card-thumb:hover .card-thumb-inks { opacity: 1; }
}

.ink-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* Card count */
.cards-count {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-bright);
  /* No top padding — the surrounding filter bar's own bottom padding
     (11px) provides the visual breathing room. The previous 10px top
     padding caused the Card Sets page to show an oversized gap that
     My Cards / Missing didn't (they override to 0 via .count-export-row). */
  padding-top: 0;
  margin-bottom: 10px;
}

/* Row that pairs the export button (left) with the count text (right).
   Wraps to two lines on narrow windows. */
.count-export-row,
.count-only-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
/* Both wrappers reset .cards-count's margin-bottom to 0 so the count text
   sits at the same vertical rhythm regardless of whether the surface has
   an action button (My Cards: export; Card Sets / Missing: no button).
   Without this rule on .count-only-row, the Card Sets count text inherited
   the base .cards-count `margin-bottom: 10px` and visibly drifted below
   the equivalent My Cards line. */
.count-export-row .cards-count,
.count-only-row   .cards-count {
  padding-top: 0;
  margin-bottom: 0;
  flex: 1;  /* fill available space, pushing the export button to the right */
}

/* ── Mobile (≤945px): center every result-summary line. On surfaces that carry
   an export button (My Cards / Missing) the button drops to its OWN centered
   line below the centered count text. Desktop keeps the single inline row. ── */
@media (max-width: 945px) {
  .count-only-row .cards-count { text-align: center; }
  .count-export-row {
    flex-direction: column;
    align-items: center;
    gap: 12px;   /* breathing room between the count text and the button */
  }
  .count-export-row .cards-count {
    flex: none;
    width: 100%;
    text-align: center;
  }
  .count-export-row .btn-coll-export { align-self: center; }
}

/* No results */
.no-results {
  grid-column: 1/-1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80px 24px;
  gap: 12px;
  color: var(--text-muted);
  text-align: center;
}

.no-results .nr-icon { font-size: 49px; }
.no-results p { font-size: 14px; }

/* Ink section header */
.ink-section-header {
  display: flex;
  align-items: center;
  gap: 14px;
  /* Vertical padding halved (was 12px) per UX pass — wasted space above
     the ink name and below the count line. */
  padding: 6px 16px;
  /* Top margin reduced from 16px to 10px so the gap below `.cards-count`
     (which adds its own 10px bottom margin) matches the 10px gap above
     `.cards-count`. Inter-section gap is also now 10px — visually tighter
     and consistent with the rest of the page rhythm. */
  margin-top: 10px;
  margin-bottom: 8px;
  background-color: color-mix(in srgb, var(--ink-color, var(--gold)) 15%, transparent);
  border-radius: var(--radius-m);
  border: 1px solid color-mix(in srgb, var(--ink-color, var(--gold)) 35%, transparent);
  border-left: 5px solid var(--ink-color, var(--gold));
}

.ink-icon {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  opacity: 0.8;
}

.ink-section-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ink-section-name {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-bright);
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.ink-section-count {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-bright);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

.ink-type-counts {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* Result-line breakdown ("Showing N cards 🏰x 💎y …"): the lone 12px space char
   before the breakdown reads as flush against the first (large) icon. A small
   left margin makes the gap match the 8px between entries. Scoped to .cards-count
   so the ink-section group headers (.ink-section-count — already spaced fine) are
   untouched. */
.cards-count .ink-type-counts { margin-left: 5px; }

.itc-entry {
  font-size: 12px;
  /* Inherit weight from the count container (.cards-count / .ink-section-count
     are both 500) so the per-type breakdown is not visually heavier than the
     "Showing N cards" / "238 unique" text it sits next to. */
  color: var(--text-bright);
  white-space: nowrap;
}

.itc-sep {
  color: var(--text-bright);
  opacity: 0.4;
  margin: 0 2px;
}

/* ── Card-type SECTION header (top-level, used by the Deck Detail page when
   mountSectionGrid runs in typeSections mode) ──────────────────────────────
   Visually mirrors .ink-section-header (same shape: solid tint background,
   10 px border-radius, 1 px border around, 5 px solid border-left, large
   leading icon + stacked two-line content) but uses a NEUTRAL GRAY scheme
   instead of an ink colour — these sections are by card type (Location /
   Item / Song / Action / Character), not by ink.

   Distinct from .type-subsection-header (which is a subordinate, inset row
   tinted by the parent ink section's --ink-color custom property). */
.type-section-header {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 6px 16px;
  margin-top: 10px;
  margin-bottom: 8px;
  background-color: rgba(196, 190, 172, 0.15);
  border-radius: var(--radius-m);
  border: 1px solid rgba(196, 190, 172, 0.35);
  border-left: 5px solid rgba(196, 190, 172, 0.7);
}
/* Card-type icon (_typeIcon → <img class="type-glyph">). Sized in `em` so it scales
   with the inherited font-size in every context (section header, tile badge, result
   line, print, deck list) exactly the way the emoji it replaced did — one knob, no
   per-context rules. Filter chips use .f-chip-icon instead (fixed 15px, matches the
   ink/rarity chip icons). */
.type-glyph,
.app-glyph {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.15em;
  object-fit: contain;
}
/* Gap between a leading glyph and the button label. `.btn` isn't a flex row, so a
   bare `<img> text` renders flush; this restores the icon→text spacing on every
   icon button (Import deck, Export filtered cards, etc.). */
.btn .app-glyph { margin-right: 0.4em; }
/* Warning "alert row" (_warnAlert) — icon fixed top-left in its own column, the
   message wraps beside it (Layout B). Used for multi-line warning messages. */
.warn-alert {
  display: flex;
  gap: 10px;
  align-items: flex-start;
}
.warn-alert-ic {
  width: 20px;
  height: 20px;
  flex: none;
  margin-top: 1px;
}
.type-section-icon {
  font-size: 28px;
  line-height: 1;
  flex-shrink: 0;
  opacity: 0.85;
  /* Match .ink-icon's 32 px square footprint so the stacked text column
     starts at the same x-offset as it does on .ink-section-header. */
  width: 32px;
  display: inline-flex;
  justify-content: center;
}
.type-section-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.type-section-name {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-bright);
  letter-spacing: 0.5px;
  text-transform: uppercase;
}
.type-section-count {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-bright);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

/* ── Compact-list (pill) view, used by the Deck Detail page ───────────────
   Pills wrap into as many columns as the viewport allows; minimum width is
   tuned so most Lorcana card full names render without ellipsis.  Layout is
   independent of .cards-grid / .cards-grid-locations — pills never rotate.
   Tile renderer: tileCardPill (js/components/tiles/card-pill.js). */
.pill-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
  gap: 8px;
}

.pill {
  display: flex;
  align-items: center;
  gap: 3px;                       /* tight — ≤3px between count/cost/ink (2026-06-07 spec §3) */
  padding: 1.5px 5px;             /* L/R halved 10→5px (§5); ~20% shorter overall (§1) */
  border-radius: 16px;
  border: 1px solid rgba(0, 0, 0, 0.22);
  cursor: pointer;
  transition: border-color 140ms ease, box-shadow 140ms ease;
  font: 14px/1.15 'Bitter', Georgia, serif;
  min-height: 30px;               /* was 34 (effective ~38 with the 26px icon) → ~20% smaller */
  color: #fff;
  /* No zoom on hover (spec); only border + glow change.  An aggressive
     transform here would also force the whole pill grid into a new
     stacking context, which we don't need. */
}

/* Pill backgrounds — SINGLE SOURCE OF TRUTH, shared by BOTH the deck-detail
   "Compact list" pills (.pill--<ink>) AND the deck-builder right-pane
   "Cards in deck" pills (.dl-pill.pill--<ink>).
   • Single inks → flat solid fills (the user's chosen palette, 2026-06-07).
   • Multi-ink → 6-colour rainbow tint (same family as
     .section-card-grids--multi-ink; no single solid is meaningful for it).
   • Inkless → neutral dark gray. */
:root {
  --pill-solid-emerald:  #1C8A37;
  --pill-solid-ruby:     #99122B;
  --pill-solid-steel:    #6D6F8B;
  --pill-solid-amber:    #AB7C03;
  --pill-solid-sapphire: #126D9B;
  --pill-solid-amethyst: #912E86;

  --pill-grad-multi:    linear-gradient(120deg,
                          rgba(245, 158, 11,  0.34),
                          rgba(139, 92,  246, 0.34),
                          rgba(16,  185, 129, 0.34),
                          rgba(239, 68,  68,  0.34),
                          rgba(59,  130, 246, 0.34),
                          rgba(148, 163, 184, 0.34));
  --pill-grad-inkless:  linear-gradient(120deg, #3a3a46, #2a2a36);
}

.pill--amber    { background: var(--pill-solid-amber); }
.pill--amethyst { background: var(--pill-solid-amethyst); }
.pill--emerald  { background: var(--pill-solid-emerald); }
.pill--ruby     { background: var(--pill-solid-ruby); }
.pill--sapphire { background: var(--pill-solid-sapphire); }
.pill--steel    { background: var(--pill-solid-steel); }
/* Multi-ink keeps its rainbow tint (no single solid is meaningful for it);
   inkless keeps its neutral dark fill. */
.pill--multi-ink { background: var(--pill-grad-multi); }
.pill--inkless  { background: var(--pill-grad-inkless); }

@media (any-hover: hover) {
/* Hover — ink-colour border + matching glow.  No transform. */
.pill--amber:hover    { border-color: var(--amber);    box-shadow: 0 0 14px color-mix(in srgb, var(--amber)    45%, transparent); }
}
@media (any-hover: hover) {
.pill--amethyst:hover { border-color: var(--amethyst); box-shadow: 0 0 14px color-mix(in srgb, var(--amethyst) 45%, transparent); }
}
@media (any-hover: hover) {
.pill--emerald:hover  { border-color: var(--emerald);  box-shadow: 0 0 14px color-mix(in srgb, var(--emerald)  45%, transparent); }
}
@media (any-hover: hover) {
.pill--ruby:hover     { border-color: var(--ruby);     box-shadow: 0 0 14px color-mix(in srgb, var(--ruby)     45%, transparent); }
}
@media (any-hover: hover) {
.pill--sapphire:hover { border-color: var(--sapphire); box-shadow: 0 0 14px color-mix(in srgb, var(--sapphire) 45%, transparent); }
}
@media (any-hover: hover) {
.pill--steel:hover    { border-color: var(--steel);    box-shadow: 0 0 14px color-mix(in srgb, var(--steel)    45%, transparent); }
}
@media (any-hover: hover) {
.pill--multi-ink:hover { border-color: rgba(255,255,255,0.65); box-shadow: 0 0 14px rgba(255,255,255,0.35); }
}
@media (any-hover: hover) {
.pill--inkless:hover  { border-color: #cfd4dc; box-shadow: 0 0 14px rgba(207,212,220,0.45); }
}

/* Cost icon — reuses the deck-builder right-pane SVG family
   (images/cards/cost/{ink_cost,not_inkable_cost}_{0..12}.svg).  Each SVG
   bakes the cost number into the inkable or not-inkable rim, so we don't
   need a separate text node for the number.  Sized 26 px square — one
   step larger than the builder's .dl-cost-icon (22 px) so the pill row
   reads comfortably at this typography scale. */
.pill-cost-icon {
  height: 27px;                 /* ≈90% of the 30px pill height (§4) */
  width: auto;
  flex-shrink: 0;
  object-fit: contain;
  /* Same shadow as the count pill (§3); drop-shadow so it follows the
     rosette/hex shape rather than a square box. */
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.5));
}
/* Fallback pair for cost > 12 / missing inkCost — plain number + generic
   inkable / not-inkable rim. */
.pill-cost-num {
  min-width: 16px;
  font-weight: 700;
  font-size: 13px;
  color: var(--gold);
  flex-shrink: 0;
  text-align: center;
  line-height: 1;
}
.pill-cost-rim {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  object-fit: contain;
  margin-left: -2px;
}

/* Ink-hex icon — uses the same .svg assets the filter chips do, bumped up
   one size step from the original 16 px so it reads as comfortably as the
   cost icon next to it. */
.pill-ink-hex {
  height: 27px;                 /* ≈90% of the 30px pill height (§4) */
  width: auto;
  flex-shrink: 0;
  object-fit: contain;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.5));   /* same shadow as count pill (§3) */
}
.pill-ink-glyph {
  width: 20px;
  flex-shrink: 0;
  text-align: center;
  color: var(--text-dim);
  font-size: 15px;
  line-height: 1;
}

/* Text block — the single truncating flex item (name + identifier).
   flex: 1 1 auto + min-width: 0 is the standard "shrinkable inside flex"
   recipe; a long name ellipsises while the count/cost/ink chips stay fixed. */
.pill-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-left: 3px;
}
.pill-name  { font-weight: 700; color: #fff; }                       /* 14px (inherited), bold */
.pill-ident { font-weight: 400; font-size: 12px; color: rgba(255, 255, 255, 0.9); }  /* §2 — 12px regular */

/* Count — first element now (leftmost). flex-shrink: 0 keeps it intact.
   White pill + border + shadow come from the shared .card-qty-badge class
   applied alongside in card-pill.js; this rule trims that badge down to the
   smaller pill's scale and provides layout only. */
.pill-count {
  flex-shrink: 0;
  font-size: 13px;
  padding: 2px 8px;
  border-radius: 11px;
}

/* ── Card-type subsection header (Option A from
   PROPOSAL-card-type-subsections.html) ─────────────────────────────────────
   Sits inside an ink section beneath its ink header, one per non-empty card
   type (Location → Item → Song → Action → Character).  Inherits the parent
   section's --ink-color via the CSS custom property so the same markup
   re-tints itself per ink without per-call style overrides.

   Visual weight is deliberately subordinate to .ink-section-header:
     – ~8 % ink tint (vs 15 % on the ink header)
     – 3 px ink-color border-left (vs 5 px)
     – font-size 12 px medium (vs the ink header's 12 px on a taller box)
     – left-indented 8 px so the hierarchy reads top-down (ink → type). */
.type-subsection-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 12px;
  margin: 10px 0 6px 8px;
  background-color: color-mix(in srgb, var(--ink-color, var(--gold)) 8%, transparent);
  border-radius: var(--radius-s);
  border: 1px solid color-mix(in srgb, var(--ink-color, var(--gold)) 22%, transparent);
  border-left: 3px solid var(--ink-color, var(--gold));
  font-size: 12px;
}
/* Tighten the gap when the first subsection follows an ink header — the ink
   header already adds 8 px margin-bottom, so the subsection's default 10 px
   margin-top stacks visibly.  Adjacent-sibling selector targets that case. */
.ink-section-header + .type-subsection-header {
  margin-top: 4px;
}
.type-subsection-header .ts-emoji { font-size: 14px; line-height: 1; }
.type-subsection-header .ts-name {
  color: var(--text-bright);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.type-subsection-header .ts-count {
  color: var(--text-muted);
}

/* Multi-ink section header */
.multi-ink-header {
  background: linear-gradient(
    135deg,
    rgba(245, 158, 11,  0.15),
    rgba(139, 92,  246, 0.15),
    rgba(16,  185, 129, 0.15),
    rgba(239, 68,  68,  0.15),
    rgba(59,  130, 246, 0.15),
    rgba(148, 163, 184, 0.15)
  );
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-left: none;
  padding-left: 21px; /* 16px original + 5px for the pseudo-element border */
  position: relative;
}
.multi-ink-header::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 5px;
  border-radius: var(--radius-m) 0 0 var(--radius-m);
  background: linear-gradient(to bottom, #f59e0b, #8b5cf6, #10b981, #ef4444, #3b82f6, #94a3b8);
}

/* Multi-ink subsection headers (Option A — see PROPOSAL-card-type-subsections.html).
   Mirrors .multi-ink-header above: a 6-colour rainbow background gradient at
   half the alpha used on the parent header (0.08 vs 0.15, matching the single-
   ink subsection's 8% tint) plus a 3px vertical-rainbow left-edge pseudo-
   element (the header uses 5px to match its border-left convention).
   --ink-color isn't set on the wrapper in the Multi-Ink case — these rules
   own the entire visual treatment instead. */
.section-card-grids--multi-ink .type-subsection-header {
  background: linear-gradient(
    135deg,
    rgba(245, 158, 11,  0.08),
    rgba(139, 92,  246, 0.08),
    rgba(16,  185, 129, 0.08),
    rgba(239, 68,  68,  0.08),
    rgba(59,  130, 246, 0.08),
    rgba(148, 163, 184, 0.08)
  );
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-left: none;
  padding-left: 15px; /* 12px original + 3px for the pseudo-element border */
  position: relative;
}
.section-card-grids--multi-ink .type-subsection-header::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  border-radius: var(--radius-s) 0 0 var(--radius-s);
  background: linear-gradient(to bottom, #f59e0b, #8b5cf6, #10b981, #ef4444, #3b82f6, #94a3b8);
}

/* Inkless section header styling */
.inkless-header {
  --ink-color: #a8a8a8;
}


/* Location-card canvas used by the card-details dialog (`.cd-art`). The portrait
   source is drawn rotated 90° CW onto it (see _wireCardDialog). Its DISPLAYED size
   comes from `.cd-art > canvas { width:100%; height:100% }` (responsive); the JS
   sizes the backing store to that × devicePixelRatio so it stays crisp. The width/
   height below are a legacy fallback only — overridden whenever it's inside `.cd-art`. */
.location-img-canvas {
  display: block;
  width: 356px;
  height: 254px;
  border-radius: var(--radius-m);
  box-shadow: 0 14px 52px rgba(0,0,0,0.75);
  flex-shrink: 0;
}




/* ═══════════════════════════════════════
   MY COLLECTION PAGE
   ═══════════════════════════════════════ */
.mycollection-page {
  display: flex;
  flex-direction: column;
}

#ill-content {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;   /* flex-safety so a flex:1 child (the builder tab content) can size + scroll */
}
/* Card-grid tabs (My Cards / Missing / Add / deck grids) don't need the flex-column
   sizing — only the fullBleed builder/detail workspaces do. For them #ill-content is a
   plain BLOCK, matching Card Catalog's `.sets-page`. A foldable fold/unfold correctly
   REFLOWS a block page but NOT a flex column (the latter stays at its pre-fold width →
   off-centre breadcrumb + overflowing filter bar). Toggled by renderIllTab via !fullBleed. */
#ill-content.ill-content--block { display: block; }

/* ── Illumineer picker ── */
.ill-picker {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 20px 22px;
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}

.ill-picker h1 {
  font-family: 'Bitter', serif;
  font-size: 23px;
  color: var(--text-bright);
}

.ill-select {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 7px 14px;
  color: var(--text-main);
  font-size: 15px;
  cursor: pointer;
  transition: border-color 0.2s;
  min-width: 180px;
}

.ill-select:focus { outline: none; border-color: var(--gold); }
.ill-select option { background: var(--bg-elevated); }

/* Illumineer selection cards (no selection yet) */
.ill-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px;
  max-width: 860px;
  margin-bottom: 28px;
}

.ill-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 24px 20px;
  cursor: pointer;
  transition: all 0.22s;
  text-align: center;
  font-family: inherit;
}

@media (any-hover: hover) {
.ill-card:hover {
  border-color: var(--purple);
  transform: translateY(-4px);
  box-shadow: 0 12px 32px rgba(0,0,0,0.35), 0 0 20px rgba(139,92,246,0.2);
}
}

.ill-card .ill-avatar { font-size: 39px; margin-bottom: 10px; }

.ill-card .ill-name {
  font-family: 'Bitter', serif;
  font-size: 16px;
  color: var(--text-bright);
  margin-bottom: 4px;
}

.ill-card .ill-since { font-size: 12px; color: var(--text-dim); }

/* ── Stats grid ── */
/* Illumineer stats and breakdown sections */
#ill-stats-section {
  padding: 20px var(--gutter-content) 0;
}

#ill-breakdown-section {
  padding: 0 var(--gutter-content) 6px;
  border-bottom: 1px solid var(--border);
}

#ill-tab-content {
  padding: 20px var(--gutter-content);
}
/* Full-bleed variant — applied by renderIllTab when the My Decks sub-view is
   'detail' or 'builder'. The deck workspace owns its own internal padding
   (.detail-head .dh-inner + .detail-body .stats-card / card grid) so removing
   the parent padding lets the deck-detail upper section span edge-to-edge
   without leaving a top/left/right gutter where the body background would
   otherwise show through. Other sub-tabs (My Cards / Missing / Add Card /
   the My Decks grid) keep the standard 20px/22px gutter. */
#ill-tab-content.subview-fullbleed {
  padding: 0;
}

/* The My Collection and Missing In Collection filter bars are inset
   rounded panels (see #coll-filter-bar / #missing-filter-bar), so they
   keep the parent padding and are not pulled to the edges. */

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 12px;
  margin-bottom: 22px;
}

.stat-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 14px 18px;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 16px;
}

.sc-icon {
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  opacity: 0.85;
}

.sc-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.sc-val {
  font-family: 'Bitter', serif;
  font-size: 24px;
  color: var(--gold-light);
  display: block;
  line-height: 1;
  margin-bottom: 4px;
  text-shadow: 0 0 16px var(--gold-glow);
}

.sc-lbl {
  font-size: 12px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.8px;
}

/* ── Set breakdown cards ── */
.set-breakdown {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 8px;
  margin-bottom: 4px;
}

.set-prog-card {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 10px 14px;
}

.spc-name { font-size: 13px; color: var(--text-muted); margin-bottom: 4px; }
.spc-img  { width: 100%; height: 81px; object-fit: contain; pointer-events: none; }
/* margin-bottom lives on the wrapper so the pill anchors flush to the image bottom */
.set-prog-card .set-img-wrap { margin-bottom: 6px; }
.spc-count { font-size: 16px; color: var(--text-bright); font-weight: 600; margin-bottom: 6px; }

.prog-bar {
  height: 3px;
  background: var(--bg-panel);
  border-radius: 2px;
  overflow: hidden;
}

.prog-fill {
  height: 100%;
  background: linear-gradient(to right, var(--gold), var(--purple));
  border-radius: 2px;
  transition: width 0.6s ease;
}

/* ── Breadcrumb bar (replaces the old My Collection sub-tab strip) ──
   Occupies the same row the tab strip did (≈ --tabs-h tall, so the
   builder-overlay height calc that subtracts --tabs-h stays correct).
   The "where am I" hint; the section switcher is the dropdown under
   "My Collection" in the top nav. */
.breadcrumb-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px var(--gutter-content);      /* tightened (was 6px) so the bar is shorter */
  background: var(--bg-surface);          /* same colour the tab strip used */
  border-bottom: 1px solid var(--border);
  /* Sticky: the page scrolls inside the window (no inner overflow container)
     and the top-nav is position:fixed, so pin the bar just below the nav so
     it stays visible while long tab content scrolls beneath it. */
  position: sticky;
  top: var(--nav-h);
  z-index: 30;
}
/* Anon public-deck page ONLY: a deep-blue gradient band under the "Public Deck"
   breadcrumb — a stronger visual cue that this is a shared/anonymous public page
   (distinct from the app's ordinary surface-coloured breadcrumb bars). The crumb
   text is brightened for contrast against the blue. */
.breadcrumb-bar--pubview {
  background: linear-gradient(90deg, #0073BA 0%, #18175B 50%, #0073BA 100%);
  border-bottom-color: rgba(0, 0, 0, 0.35);
}
.breadcrumb-bar--pubview .crumb { color: #eef4fb; }
/* Card Catalog page: a "Card Catalog" location breadcrumb, MOBILE ONLY (the hamburger
   nav hides the page title on mobile — this restores orientation, matching the My Cards
   bar). Hidden on desktop; the crumb is static (not a nav action). */
.sets-breadcrumb { display: none; }
.sets-breadcrumb .crumb { cursor: default; }
@media (max-width: 945px) { .sets-breadcrumb { display: flex; } }

.breadcrumb {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  min-width: 0;                            /* allow the name crumb to shrink */
  /* Farmiles display font + size match the top-nav links (the font is
     all-caps by design, so no text-transform needed). */
  font-family: 'HF Farmiles', sans-serif;
  font-size: 18px;                         /* −2px (was 20px) */
  letter-spacing: var(--ls-farmiles);
  line-height: 1.1;
}
/* Leading glyph on a crumb (e.g. the world icon on the public-deck view). Sized in
   em so it tracks the crumb's font-size, with a real gap before the label. */
.breadcrumb .app-glyph {
  width: .95em; height: .95em;
  vertical-align: -2px;
  margin-right: .5em;
  opacity: .9;
}
.crumb {
  flex: 0 0 auto;                          /* fixed crumbs never shrink */
  white-space: nowrap;
  /* Same colour as an unselected top-nav link (e.g. "Home"). */
  color: var(--text-muted);
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  cursor: pointer;
  transition: color 0.2s;
}
@media (any-hover: hover) {
.crumb:hover { text-decoration: underline; }
}
/* The deck-name crumb is the ONLY shrinkable segment: when the strip is
   too narrow it ellipsis-truncates while My Decks + Deck Builder stay
   fully visible. min-width keeps a few chars + "…" on screen; the full
   name is in the crumb's title tooltip. */
.crumb--name {
  flex: 0 1 auto;
  min-width: 3em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.crumb-sep {
  flex: 0 0 auto;
  color: var(--text-muted);
  opacity: 0.7;
  font-size: 30px;                         /* same size as the nav caret (item 2) */
  line-height: 0;                          /* big glyph must not inflate the bar height */
}

/* ── Quantity Selector Dialog ── */




.card-preview {
  width: 100%;
  height: 240px;
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 12px;
  background: var(--bg-surface);
  display: flex;
  align-items: center;
  justify-content: center;
}

.card-preview img {
  max-width: 100%;
  max-height: 240px;
  width: auto;
  height: auto;
  display: block;
  /* natural size, capped — no forced stretch or upscaling */
}





.quantity-selectors {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-bottom: 16px;
}




.quantity-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px;
}

.qty-btn {
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  background: var(--border);
  color: var(--text-bright);
  cursor: pointer;
  border-radius: 4px;
  font-size: 16px;
  font-weight: 600;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
}

@media (any-hover: hover) {
.qty-btn:hover:not(:disabled) {
  background: var(--border-hover);
  transform: scale(1.05);
}
}

.qty-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.qty-input {
  flex: 1;
  border: none;
  background: transparent;
  text-align: center;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-bright);
  cursor: default;
  min-width: 50px;
}

.quantity-total {
  padding: 12px;
  background: rgba(212, 169, 59, 0.08);
  border: 1px solid rgba(212, 169, 59, 0.2);
  border-radius: 6px;
  font-size: 13px;
  text-align: center;
  color: var(--text-bright);
}

.quantity-total span {
  font-weight: 600;
  color: var(--gold-light);
}



/* ── Add card tab layout ──
   Just the input form now (the successful-add preview moved into the card-details
   dialog — see openPanel opts.addedReceipt). The lone form is centred. */
.add-card-row {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  flex-wrap: wrap;
  /* Dialog-receipt redesign: the tab is now just the form (the successful-add
     preview moved into the card-details dialog), so the lone form is centred. */
  justify-content: center;
}

.add-card-form {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  /* A touch more breathing room than the filter panel — this is a form,
     not a chip row, so the slightly larger pad reads as less cramped. */
  padding: 18px 20px;
  /* >=700px (desktop / tablet / UNFOLDED foldable): centred + capped so it isn't
     a full-bleed strip. */
  flex: 0 1 460px;
  min-width: 260px;
  max-width: 100%;
}
/* Full-width ONLY on true phone-portrait widths. The 700px boundary mirrors the
   card-details dialog's desktop switch (CARD_DIALOG_DESKTOP_MIN_PX = 700, keyed on
   window.innerWidth >= 700) — a derived width anchor, deliberately NOT the 945 nav
   breakpoint. So an unfolded foldable / small tablet (>=700, e.g. ~760) gets the
   centred desktop form, and the form + receipt dialog flip to desktop at the same
   width. 699.98px matches the dialog's >=700 boundary exactly (no sub-pixel gap). */
@media (max-width: 699.98px) {
  .add-card-form { flex-basis: 100%; max-width: none; }
}

/* Stepper block (replaces the old std/foil text-field .form-row). One clean 16px
   gap below it — the old grid stacked a .form-group margin ON TOP of the row +
   button margins (~36px dead band). */
.add-card-qty { margin-bottom: 16px; }

.add-result {
  margin-top: 14px;
  font-size: 12px;
}
/* The shared .loading reserves min-height:200px for full-section spinners; inside
   the Add-Card result that balloons the form. Here it's a compact inline line —
   grow only enough for the spinner + "Loading…" text. */
.add-result .loading { min-height: 0; }

/* Undo button (Add-Card receipt dialog) — clear red affordance (no transparent
   ghost variant). Solid red background + white text; only the border highlights
   to a complementary tone on hover. */
.undo-add-btn {
  background: var(--danger);
  border: 1px solid var(--danger);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  padding: 5px 12px;
  border-radius: var(--radius-s);
  cursor: pointer;
  font-family: inherit;
  transition: border-color 0.18s;
}
@media (any-hover: hover) {
.undo-add-btn:hover:not(:disabled) {
  /* Brighter complementary red on the border; no background change. */
  border-color: #fca5a5;
}
}
.undo-add-btn:disabled { opacity: 0.35; cursor: not-allowed; }

/* ── Non-interactive variant of .f-chip ── informational pills only (no hover
   state, no pointer cursor). */
.f-chip-static {
  cursor: default;
}
@media (any-hover: hover) {
.f-chip-static:hover,
.f-chip-static:focus {
  border-color: var(--border);
  color: var(--text-muted);
  outline: none;
}
}

/* Panel heading. */
.form-title {
  font-family: 'Bitter', serif;
  font-size: 17px;
  font-weight: 700;
  color: var(--text-bright);
  margin-bottom: 18px;
}

.form-group { margin-bottom: 16px; }

/* Field labels — uppercase muted tone (CARD FULL IDENTIFIER, STANDARD, FOIL). */
.form-lbl {
  display: block;
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  color: var(--text-muted);
  margin-bottom: 4px;
}

.form-inp {
  width: 100%;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 7px 12px;
  color: var(--text-main);
  font-size: 12px;
  transition: border-color 0.2s;
}

.form-inp:focus { outline: none; border-color: var(--gold); }
.form-inp::placeholder { color: var(--text-dim); }

.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }

/* Field hint — plain, no extra weight. */
.form-hint {
  font-size: 12px;
  font-weight: normal;
  color: var(--text-dim);
  margin-top: 4px;
  line-height: 1.5;
}

/* ── Buttons ── */
.btn {
  padding: 9px 20px;
  border-radius: var(--radius-s);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
  border: 1px solid transparent;
  font-family: inherit;
  letter-spacing: 0.3px;
}

.btn-primary {
  background: var(--gold-dim);
  border-color: var(--gold);
  color: var(--gold-light);
}

@media (any-hover: hover) {
.btn-primary:hover {
  background: rgba(212,169,59,0.22);
  box-shadow: 0 0 16px var(--gold-glow);
}
}

.btn-secondary {
  background: var(--bg-elevated);
  border-color: var(--border);
  color: var(--text-main);
}

@media (any-hover: hover) {
.btn-secondary:hover { border-color: var(--border-hover); color: var(--text-bright); }
}

/* Secondary / dismiss button — matches the look of the Disclaimer
   dialog's close button so every modal's Cancel/Back action reads
   uniformly. Was previously `background: transparent; color: text-muted`
   which felt insubstantial next to the gold .btn-primary; the muted
   surface + readable text gives it the same weight as the
   primary action without competing visually. */
.btn-ghost {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  color: var(--text-main);
}

@media (any-hover: hover) {
.btn-ghost:hover { border-color: var(--border-hover); color: var(--text-bright); }
}

/* ── Collection filter bar (with set dropdown) ── */
.coll-filter-bar {
  background: var(--bg-deep);
  border-bottom: 1px solid var(--border);
  padding: 8px var(--gutter-content);
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  flex-shrink: 0;
}

/* Inset rounded-panel filter bars — My Collection, Missing In Collection,
   Card Sets. ID-scoped so the deck builder bar keeps its own layout.
   Mirrors the .decks-filterbar panel introduced by the deck feature. */
#coll-filter-bar,
#missing-filter-bar,
#set-filter-bar,
#detail-filter-bar {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 11px 15px;
  /* Cap the horizontal size on wide viewports and centre the bar so the
     page chrome lines up with the Home welcome banner. Below
     --max-content-width the bar fills its parent's available width.
     width:auto (NOT width:100%) — auto is fill-available stretch sizing; this aligns
     the three card-grid bars with #set-filter-bar (Card Catalog), which always used
     auto. NOTE: this is a consistency/defensive choice, NOT the fix for the narrow
     mobile filter bar — that was the host's display:flex starving its single .fbm-bar
     child (reset to display:block at ≤945, below). See SPEC.md → "Mobile / foldable layout". */
  max-width: var(--max-content-width);
  margin-left:  auto;
  margin-right: auto;
  width: auto;
}
/* The Card Sets page (.sets-page) has no padding of its own. Below the
   --max-content-width breakpoint the bar would otherwise touch the screen
   edges, so we override the auto-margins above with a 20 px gutter. Above
   the breakpoint the bar is centred by the auto margins (no override
   needed — the cap takes effect first and there's empty space on both
   sides). The other three pages get their narrow-viewport inset from
   #ill-tab-content padding, so this override is .sets-page-specific. */
/* NB: 1536px below must stay in sync with --max-content-width on :root —
   CSS @media (width:) does not yet substitute custom-property values in
   mainstream browsers. */
@media (max-width: 1536px) {
  /* width:auto (overriding the base width:100%) so the 20px gutters inset the bar
     instead of stacking on top of a full-width box — width:100% + 40px of margin
     overflowed the parent by 40px, clipping the right border on ≤1536px viewports.
     Matches the My Cards bar: capped+centred above the breakpoint, gutter+adaptive below. */
  #set-filter-bar { margin-left: 20px; margin-right: 20px; width: auto; }
}

/* ── Set multi-select dropdown ── */
.set-multiselect {
  position: relative;
  flex-shrink: 0;
}

.set-ms-trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--bg-filter-control);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 4px var(--filter-pill-pad-x);   /* L/R shared with .f-chip via --filter-pill-pad-x */
  color: var(--text-muted);
  font-size: 12px;
  font-family: inherit;
  /* No min-width floor: the trigger pill sizes to its label (like the --narrow
     variant), so the ▾ sits one flex-gap after the text instead of being shoved to a
     padded 155px edge by margin-left:auto. Longer labels still expand. The dropdown
     PANEL keeps its own min-width (.set-ms-dropdown) so expanded contents are unchanged. */
  min-width: 0;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.2s, color 0.2s;
  white-space: nowrap;
}
@media (any-hover: hover) {
.set-ms-trigger:hover,
.set-ms-trigger:focus  { outline: none; border-color: var(--gold); color: var(--text-bright); }
}
.set-ms-trigger.active { background: var(--gold); border-color: var(--gold); color: #1a1405; font-weight: 700; }

.set-ms-arrow {
  margin-left: auto;
  font-size: 10px;
  opacity: 0.6;
}

.set-ms-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: var(--z-dropdown);
  background: var(--bg-filter-control);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  box-shadow: 0 8px 28px rgba(0,0,0,0.45);
  min-width: 230px;
  max-height: 300px;
  overflow-y: auto;
  padding: 4px 0;
}
.set-ms-dropdown.open { display: block; }

/* ⛔ MOBILE-ONLY (≤945px) dropdown sizing: cap at ~5 rows + a visibly-present
   scrollbar. DESKTOP IS DELIBERATELY UNTOUCHED — it has the room, keeps the full
   ~10-row (300px) panel above. This override exists only because the mobile filter
   dialog is cramped. Row ≈ 30px (border-box; 8px padding + 2px border) → 5 rows ≈
   158px. Recorded in THEME_TOKENS.md → Dropdowns. Do NOT apply this to desktop. */
@media (max-width: 945px) {
  .set-ms-dropdown {
    max-height: 158px;
    scrollbar-width: thin;
    scrollbar-color: var(--gold-dim) transparent;
  }
  /* Keep the scrollbar visibly present (not a fade-out overlay) so the "there is
     more — scroll" affordance is always obvious on touch. */
  .set-ms-dropdown::-webkit-scrollbar { width: 9px; }
  .set-ms-dropdown::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; border: 2px solid var(--bg-filter-control); }
  .set-ms-dropdown::-webkit-scrollbar-track { background: transparent; }
}

.set-ms-option {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 14px;
  cursor: pointer;
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  transition: background 0.12s, color 0.12s;
  user-select: none;
}
@media (any-hover: hover) {
.set-ms-option:hover { background: rgba(255,255,255,0.05); color: var(--text-bright); }
}

.set-ms-all-option {
  color: var(--text-muted);
  font-style: italic;
  border-bottom: 1px solid var(--border);
  padding-bottom: 8px;
  margin-bottom: 2px;
}

.set-ms-option input[type="checkbox"] {
  accent-color: var(--gold);
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  cursor: pointer;
}

/* ── Narrow variant — for dropdowns with short options (e.g. Ink Cost 1-9+) ── */
.set-multiselect--narrow .set-ms-trigger {
  min-width: 0;
}
.set-multiselect--narrow .set-ms-dropdown {
  min-width: 0;
  width: max-content;
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE FILTER BAR (.fbm-*) — the collapsed, group-button presentation of the
   filter bar for the phone UI (≤945px). Emitted ONLY by mountFilterBar's mobile
   skin (js/components/mobile-filter-bar.js), so these elements never exist at
   desktop width — no media query needed. Token-only (see THEME_TOKENS.md).
   ═══════════════════════════════════════════════════════════════════════════ */
.fbm-bar {
  margin-top: 14px;
  /* Breathing room before the result line ("Showing N …") that follows the bar on
     every mobile filter surface (My Cards / Missing / Card Sets / deck detail). The
     margin collapses out of the flush filter host, so it lands as the gap before the
     result row — the bar and the count text were touching without it. */
  margin-bottom: 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 12px 12px 14px;
  /* BLOCK, not a flex column: a foldable mis-sizes a flex-column container to its
     content's max width (here the filter grid's ~590px) and overflows the viewport
     even when its parents are correct (diagnosed via ?ovdbg: vw=443 but .fbm-bar w=590).
     A block is always its parent's width — exactly like Card Catalog's filter bar.
     Title centred via text-align; the group grid (block) fills the width below. */
  text-align: center;
}
.fbm-title {
  display: block;
  margin-bottom: 11px;   /* the spacing the removed flex `gap` provided */
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-bright);
}
/* Responsive columns driven by the VIEWPORT (media query), NOT auto-fit on the
   container width: auto-fit computes from the container, which a foldable can leave
   stale after fold/unfold (→ 4 columns at ~590px inside a 443px viewport). A media
   query re-evaluates on the (correct) layout viewport, and `minmax(0, 1fr)` makes the
   columns FILL the container exactly, so the grid can never exceed its width.
   Folded phone → 2-up; unfolded foldable / wider mobile → 4-up (all four groups in a row). */
.fbm-groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  width: 100%;
}
@media (min-width: 600px) {
  .fbm-groups { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* Center a trailing ORPHAN group button so an odd group count (My Private Decks
   has 3: Ink · Format · Decks) never left-packs with dead space. In the 2-up grid
   the lone last button (last-child at an odd position) spans the row and centers
   at one-cell width. Even-count bars (card pages + public decks = 4) never match. */
.fbm-groups .fbm-group-btn:last-child:nth-child(odd) {
  grid-column: 1 / -1;
  justify-self: center;
  width: calc(50% - 4px);   /* one 2-col cell incl. half the 8px gap */
}
/* At 4-up a 3-group bar would leave column 4 empty; render it as a centered
   3-track row instead, and neutralize the 2-up orphan rule above. */
@media (min-width: 600px) {
  .fbm-groups--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: 480px; margin: 0 auto; }
  .fbm-groups--3 .fbm-group-btn:last-child:nth-child(odd) { grid-column: auto; justify-self: stretch; width: auto; }
}
/* ⚠ on a group button (the DECKS button when a deck needs review — mirrors the
   "With issues" toggle that only appears then). */
.fbm-warn { margin-right: 6px; }
.fbm-group-btn {
  position: relative;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 11px 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 0.14s, background 0.14s;
}
@media (any-hover: hover) {
.fbm-group-btn:hover { border-color: var(--gold-dim); background: rgba(255, 255, 255, 0.03); }
}
.fbm-group-btn.has-active { border-color: var(--gold); background: var(--gold-dim); }
/* The group-button label reuses the real .filter-label so caps/spacing/gold match
   the desktop bar exactly; strip its desktop row spacing. */
.fbm-group-btn .filter-label { padding-top: 0; margin-right: 0; }
.fbm-badge {
  position: absolute;
  top: -7px;
  right: -7px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--gold);
  color: var(--bg-void);
  font-size: 11px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}

/* Modal dialog — full-viewport scrim (above content + top-nav, below the mobile
   nav drawer at z-index 300). A DOM child of the filter-bar host so the host's
   delegated attachFilterListeners reaches the chips/dropdowns inside it. */
.fbm-dlg-scrim {
  position: fixed;
  inset: 0;
  z-index: 250;
  background: rgba(4, 4, 15, 0.7);
  backdrop-filter: blur(4px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.fbm-dlg-scrim.open { opacity: 1; pointer-events: auto; }
/* NO overflow / no fixed height: the dialog sizes to its (small, bounded) chip
   content and does NOT clip or scroll — so the .set-ms-dropdown panels inside
   (position:absolute, top:calc(100%+4px)) float ON TOP of the modal and may extend
   BEYOND it, exactly like the desktop bar. A scroll/clip container here was a bug:
   it trapped the dropdown inside a sub-panel, AND overflow-y:auto makes overflow-x
   compute to auto too, producing spurious scrollbars on any sub-pixel size. */
.fbm-dlg {
  width: 100%;
  max-width: clamp(300px, 90%, 460px);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-l);
  display: flex;
  flex-direction: column;
  overflow: visible;
  transform: translateY(14px);
  transition: transform 0.2s;
}
.fbm-dlg-scrim.open .fbm-dlg { transform: none; }
/* Head/foot round the dialog's outer corners (the dialog no longer clips them). */
.fbm-dlg-head {
  flex: none;
  padding: 13px var(--gutter-content);
  border-bottom: 1px solid var(--border);
  text-align: center;
  border-radius: var(--radius-l) var(--radius-l) 0 0;
}
.fbm-dlg-head .filter-label { padding-top: 0; margin-right: 0; font-size: 13px; }
.fbm-dlg-body {
  overflow: visible;
  padding: 14px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
  align-content: flex-start;
}
.fbm-dlg-foot {
  flex: none;
  padding: 10px var(--gutter-content);
  border-top: 1px solid var(--border);
  border-radius: 0 0 var(--radius-l) var(--radius-l);
}
.fbm-dlg-close {
  width: 100%;
  padding: 11px;
  border-radius: var(--radius-s);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  color: var(--text-bright);
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
@media (any-hover: hover) {
.fbm-dlg-close:hover { background: rgba(255, 255, 255, 0.05); border-color: var(--gold-dim); }
}
/* The name-search box spans the full dialog width (TYPE group). */
.fbm-dlg-body .filter-search { flex-basis: 100%; box-sizing: border-box; }

/* ⛔ MOBILE ONLY (≤945px): the mobile bar (.fbm-bar) brings its OWN panel, so the
   host filter-bar wrapper must NOT also draw one — otherwise it's a panel nested in
   a panel (the #…-filter-bar rule above gives bg/border/radius/padding). Strip the
   wrapper's chrome but keep its sizing/inset (max-width, margins, gutters) so .fbm-bar
   becomes the single, correctly-positioned panel. The card surfaces use the mobile
   skin via mountFilterBar; #detail-filter-bar (Deck Detail) collapses to the same
   fbm skin via its own matchMedia controller (_renderDeckDetailCards). Desktop
   (>945) is untouched. */
@media (max-width: 945px) {
  #coll-filter-bar,
  #missing-filter-bar,
  #set-filter-bar,
  #detail-filter-bar {
    background: none;
    border: none;
    border-radius: 0;
    padding: 0;
    /* THE fix for the narrow mobile filter bar: the desktop `.coll-filter-bar` /
       `.filter-bar` CLASS sets display:flex (to lay chips in a row). On mobile the
       host holds a single `.fbm-bar` child, which — as a flex item with no flex-grow
       — starved to its content width (~172px) instead of filling (diagnosed on-device:
       host[flex] 452 but bar 172). The ID selector here (specificity beats the class)
       resets it to block so .fbm-bar fills. */
    display: block;
  }
  /* The wrapper no longer pads; .fbm-bar sits flush at the top of it. */
  #coll-filter-bar .fbm-bar,
  #missing-filter-bar .fbm-bar,
  #set-filter-bar .fbm-bar,
  #detail-filter-bar .fbm-bar { margin-top: 0; }

  /* HUNNY ONLY sub-section inside the INK group dialog (exemption decks only):
     a full-width block below the ink chips so the dashed bee chips keep their own
     labelled row instead of flowing inline with the deck's own inks. */
  .fbm-dlg-body .fbm-subgroup {
    flex-basis: 100%;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: center;
    margin-top: 4px;
  }
  .fbm-dlg-body .fbm-subgroup .filter-label { flex-basis: 100%; text-align: center; }
}

/* ── Card Catalog top instructions (.cat-instructions) — MOBILE ONLY ──
   On desktop the "pick a card set" guidance is the card-area empty state
   (.empty-state--no-set), which stays exactly as-is. On mobile the set grid fills
   the screen, so the guidance moves ABOVE the grid (reworded "tap … below … scroll
   down") and the empty-state copy is suppressed to avoid showing the same prompt
   twice. Desktop (>945) is untouched: .cat-instructions is hidden and the empty
   state shows normally. */
.cat-instructions { display: none; }
@media (max-width: 945px) {
  .cat-instructions {
    display: block;
    text-align: center;
    padding: 16px 12px 4px;
  }
  .cat-instructions .ci-icon { font-size: 30px; line-height: 1; }
  .cat-instructions p {
    margin: 8px auto 0;
    max-width: 320px;
    color: var(--text-muted);
    font-size: 13.5px;
    line-height: 1.45;
  }
  /* The top instructions replace the empty-state guidance on mobile (the 🔍
     no-match state keeps its own class and still shows). */
  .sets-page .empty-state--no-set { display: none; }
}

/* ── Collection export button ── */
.btn-coll-export {
  flex-shrink: 0;
  padding: 5px 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-main);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

@media (any-hover: hover) {
.btn-coll-export:hover {
  background: var(--bg-hover);
  border-color: var(--gold);
  color: var(--gold-light);
}
}

/* Create illumineer form */
.create-ill-section {
  margin-top: 24px;
}

.create-ill-form {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  padding: 24px;
  max-width: 360px;
}


/* ═══════════════════════════════════════
   LOADING / EMPTY / UTILITIES
   ═══════════════════════════════════════ */
.loading {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-height: 200px;
  color: var(--text-muted);
  font-size: 15px;
}

.spinner {
  width: 22px;
  height: 22px;
  border: 2px solid var(--border);
  border-top-color: var(--gold);
  border-radius: 50%;
  animation: spin 0.75s linear infinite;
  flex-shrink: 0;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* ── Boot loader ─────────────────────────────────────────────────────────────
   Shown in #app-root only during the initial auth check (initRouter).
   Intentionally isolated: do NOT reuse .boot-loader outside that code path.
   Every exit from initRouter (enterApp / renderLogin) overwrites #app-root,
   so the element is never explicitly removed. */
.boot-loader {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 60vh;
}

.boot-loader__spinner {
  width: 48px;
  height: 48px;
  border: 3px solid rgba(255, 255, 255, 0.15);
  border-top-color: var(--gold);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80px 24px;
  gap: 14px;
  text-align: center;
}

.empty-state .es-icon { font-size: 57px; }

.empty-state h3 {
  font-family: 'Bitter', serif;
  font-size: 21px;
  color: var(--text-bright);
}

.empty-state p {
  font-size: 15px;
  color: var(--text-muted);
  line-height: 1.65;
  max-width: 360px;
}

.hidden { display: none !important; }


/* ═══════════════════════════════════════
   TOAST NOTIFICATIONS
   ═══════════════════════════════════════ */
#toast-container {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: none;
}

.toast {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 12px 18px;
  font-size: 14px;
  color: var(--text-main);
  box-shadow: 0 8px 28px rgba(0,0,0,0.45);
  animation: toast-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  max-width: 320px;
  border-left: 3px solid var(--border);
}

.toast.success { border-left-color: #4ade80; color: #86efac; }
.toast.error   { border-left-color: #f87171; color: #fca5a5; }
.toast.info    { border-left-color: var(--gold); color: var(--gold-light); }

@keyframes toast-in {
  from { transform: translateX(20px); opacity: 0; }
  to   { transform: translateX(0);   opacity: 1; }
}

/* ═══════════════════════════════════════
   MY DECKS TAB  (Deck Builder — Screen 1)
   ═══════════════════════════════════════ */

/* ── Toolbar: heading, count pill, search, sort, New Deck ── */

/* ── Filter bar: ink + format + result count + clear ── */
.decks-filterbar {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 11px 15px;
  margin-top: 0;
  /* No bottom margin — spacing to the result line comes solely from
     .cards-count's padding-top, matching the My Collection tab. */
  margin-bottom: 0;
  /* Cap the horizontal size and centre on wide viewports — same pattern
     as #coll-filter-bar / #missing-filter-bar / #set-filter-bar and the
     Home welcome banner. See --max-content-width on :root. */
  max-width: var(--max-content-width);
  margin-left:  auto;
  margin-right: auto;
  width: 100%;
}
/* Sit the filterbar close to the tabs when it is the first element in the
   tab pane.  Applies to all inset rounded-panel filter bars under the My
   Collection page so My Cards / Missing Cards / My Decks all sit at the same
   -8px offset. The deck filter bars now render into a host wrapper
   (#deck-filter-bar / #pdeck-filter-bar) that swaps desktop⇄mobile skins, so the
   offset targets the `.decks-filterbar` inside that wrapper (desktop skin only). */
#ill-tab-content > #deck-filter-bar > .decks-filterbar,
#ill-tab-content > #pdeck-filter-bar > .decks-filterbar,
#ill-tab-content > #coll-filter-bar,
#ill-tab-content > #missing-filter-bar {
  margin-top: -8px;
}
.decks-filterbar .spacer { flex: 1; }

.df-group {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
/* Matches .filter-label on the card pages (My Cards / Missing / Card Sets)
   so the group labels (INK / FORMAT / DECKS / CARDS / RARITY / TYPE) read
   identically across every filter bar in the app. */
.df-glabel {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--gold-light);
  margin-right: 2px;
  flex-shrink: 0;
}
.df-divider {
  width: 1px;
  height: 24px;
  background: var(--border);
}

/* Filter chips reused for ink + format */
.decks-filterbar .chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  padding: 3px 11px;
  border-radius: 13px;
  background: var(--bg-filter-control);
  border: 1px solid var(--border);
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.18s, border-color 0.18s, color 0.18s, opacity 0.18s;
  font-family: inherit;
}
@media (any-hover: hover) {
.decks-filterbar .chip:hover { border-color: var(--border-hover); color: var(--text-bright); }
}
.decks-filterbar .chip.on {
  background: var(--gold);
  border-color: var(--gold);
  color: #1a1405;
  font-weight: 700;
}
.decks-filterbar .chip.disabled {
  opacity: 0.32;
  pointer-events: none;
}

/* Active item in the deck sort dropdown */
.deck-sort-active {
  color: var(--gold);
  font-weight: 700;
}

.df-ink-icon {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  object-fit: contain;
}

.df-result {
  font-size: 12px;
  color: var(--text-muted);
}
/* ── Search input inside the deck filter panel ── */
.df-search {
  background: var(--bg-filter-control);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 3px 10px;
  color: var(--text-main);
  font-size: 12px;
  font-family: inherit;
  width: 160px;
  transition: border-color 0.2s;
}
.df-search:focus { outline: none; border-color: var(--gold); }
.df-search::placeholder { color: var(--text-dim); }

/* ── Result summary row: below filter bar, above deck grid ──
   Pairs the cards-count text (left) with the New Deck button (right).
   .cards-count handles its own bottom margin (10px); the row itself
   adds no further spacing. */
.deck-result-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 20px;
  /* Vertical breathing room lives on the ROW itself, so the count text is spaced
     from the filter bar (above) and the grid (below) whether or not the surface
     has action buttons. (My Decks / My Public Decks used to get this ONLY from the
     buttons' 5px margin below — the My Favorites page has no buttons, so its count
     text hugged both neighbours. Centralised here so every deck-list surface — and
     any future one — is consistent.) */
  margin: 6px 0;
}
.deck-result-row .cards-count { flex: 1; margin-bottom: 0; }
/* Tighter than .btn-sm's 8 x 16 padding — about 15 % smaller — so the
   button sits proportionally next to the 12 px count text instead of
   towering over it. Font-size stays inherited from .btn-sm (13 px).
   Vertical margin (not padding) gives 5 px breathing room above and
   below the button without making the button itself bigger. */
.deck-result-row .deck-actions-inline {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}
.deck-result-row .deck-actions-inline .btn {
  padding: 7px 14px;
  /* Vertical spacing now comes from .deck-result-row's own margin (above), so the
     button no longer needs its own 5px top/bottom margin — this keeps the row height
     driven by the button and avoids doubling the gap. */
  margin: 0;
}

/* ── Deck tile grid ── */
.deck-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
  gap: 18px;
  padding-bottom: 30px;
}

.deck-tile {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  overflow: hidden;
  transition: border-color 0.15s, box-shadow 0.15s;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  position: relative;   /* anchor for the favorites unpublished-warning corner icon */
}
@media (any-hover: hover) {
/* Static border highlight on hover — no lift/zoom. Gold border + glow,
   identical to the Home page collection summary panel (.home-card). */
.deck-tile:hover {
  border-color: var(--gold);
  box-shadow: 0 0 32px var(--gold-glow);
}
}
@media (any-hover: hover) {
/* Deck with review issues (needsReview) — orange-red border matching the
   per-card issue treatment in the builder + the My Decks mockup. Held on
   hover so the red doesn't flip to gold (the gold glow box-shadow still
   applies). 2px is no-shift under the global box-sizing: border-box. */
.deck-tile--issue,
.deck-tile--issue:hover { border: 2px solid #FF5F00; }
}

.dt-accent { height: 6px; }

.dt-body {
  padding: 15px 16px 13px;
  flex: 1;
  display: flex;
  flex-direction: column;
}
/* Title — 2-line clamp with ellipsis (was single-line). The name no longer
   shares a row with the ink badges (they moved to the footer), so it can
   use the full tile width across both lines. 14px Bitter serif. */
.dt-name {
  font-family: 'Bitter', serif;
  font-size: 14px;
  font-weight: 700;
  color: var(--text-bright);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
  word-break: break-word;
  margin-bottom: 8px;
  /* Explicit line-height + matching max-height pins the clamp to an
     integer pixel boundary — without these, the inherited default
     line-height computes to a fractional value (1.2 × 14 = 16.8 px) and
     the overflow:hidden ends up cropping mid-glyph, so a sliver of the
     ascenders from the next line peeks below the ellipsis.  18 px ×
     2 = 36 px gives a clean cut. */
  line-height: 18px;
  max-height: 36px;
}
/* Description — 2-line clamp, smaller and dimmer than the title.
   Same explicit line-height / max-height pinning as .dt-name above —
   without it a fractional default line-height (1.2 × 12 = 14.4 px)
   makes the clamp leak the next line's ascenders below the ellipsis
   (the gray-ish dots the user spotted in My Decks tiles). 16 px × 2 =
   32 px and matches the existing min-height so empty / 1-line /
   2-line descriptions all share the same vertical footprint. */
.dt-desc {
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 10px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 16px;
  min-height: 32px;
  max-height: 32px;
  /* Long unbreakable strings (URLs, "aaaa…") must still wrap inside the box
     instead of overflowing horizontally and breaking the tile layout. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.dt-badges {
  display: flex;
  gap: 6px;
  margin: 0 0 10px;
  flex-wrap: wrap;
}

.dt-progress { margin-top: auto; }
.dt-prog-label {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.dt-prog-legal { color: var(--emerald); }

.dt-types {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-bright);
}
.dt-types span {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.dt-types b {
  color: var(--text-bright);
  font-weight: 700;
}

/* Footer — date on the left; ink badges and Edit button grouped on the
   right (ink badges sit immediately to the left of the Edit button). */
.dt-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4.5px 16px;
  border-top: 1px solid var(--border);
  background: var(--bg-deep);
}
.dt-date {
  font-size: 12px;
  color: var(--text-muted);
}
/* Ink badges — kept at the original 25px size, just moved into the footer. */
.dt-inks {
  display: flex;
  gap: 4px;
  flex: none;
}
/* Favorites tile: right-hand cluster in the footer — heart (unfavorite) then ink icons. */
.dt-foot-right {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
}
/* Heart button (bottom-right, left of the inks) → unfavorite with a confirm dialog. */
.dt-fav-heart {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  background: none;
  border-radius: 6px;
  cursor: pointer;
  color: var(--ruby, #e0455e);
  transition: background 0.12s, transform 0.12s;
}
.dt-fav-heart .app-glyph,
.dt-fav-heart img,
.dt-fav-heart svg { width: 18px; height: 18px; display: block; }
@media (any-hover: hover) {
  .dt-fav-heart:hover { background: rgba(224, 69, 94, 0.14); transform: scale(1.08); }
}
/* Unpublished-favorite warning corner (top-right of the tile). Amber, over the body. */
.dt-fav-warn {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--gold, #f0c864);
  pointer-events: none;   /* purely decorative — the whole tile opens the "unavailable" dialog */
}
.dt-fav-warn .app-glyph,
.dt-fav-warn img,
.dt-fav-warn svg { width: 20px; height: 20px; display: block; }
/* A subtle amber edge on the unpublished favorite tile reinforces the corner icon. */
.deck-tile--unpub { border-color: rgba(240, 200, 100, 0.45); }
.dt-ink-icon {
  width: 25px;
  height: 25px;
  flex-shrink: 0;
  object-fit: contain;
}

/* Progress bar atoms (My Decks tiles) */
.deck-tile .bar {
  height: 7px;
  background: var(--bg-deep);
  border-radius: 4px;
  overflow: hidden;
}
.deck-tile .bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--gold), var(--gold-light));
  border-radius: 4px;
}
.deck-tile .bar-fill.full {
  background: linear-gradient(90deg, var(--emerald), #34d399);
}

/* Dashed "create a new deck" tile */
.deck-tile.new-tile {
  border-style: dashed;
  align-items: center;
  justify-content: center;
  min-height: 230px;
  color: var(--text-dim);
  text-align: center;
  padding: 20px;
}
@media (any-hover: hover) {
.deck-tile.new-tile:hover {
  color: var(--gold-light);
  transform: translateY(-3px);
}
}
.deck-tile.new-tile.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
@media (any-hover: hover) {
.deck-tile.new-tile.disabled:hover {
  color: var(--text-dim);
  transform: none;
  box-shadow: none;
  border-color: var(--border);
}
}
.new-tile .plus {
  font-size: 38px;
  line-height: 1;
  margin-bottom: 8px;
}
.new-tile-title {
  font-weight: 700;
  color: var(--text-main);
}
.new-tile-sub {
  font-size: 11.5px;
  margin-top: 3px;
}

/* Format + deck-type badges on tiles */
.badge {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 4px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.badge-core {
  background: rgba(96, 165, 250, 0.16);
  color: #9ec5fb;
  border: 1px solid rgba(96, 165, 250, 0.4);
}
.badge-infinity {
  background: var(--purple-dim);
  color: var(--purple-light);
  border: 1px solid rgba(139, 92, 246, 0.4);
}
.badge-casual {
  background: rgba(148, 163, 184, 0.14);
  color: #c3ccd9;
  border: 1px solid rgba(148, 163, 184, 0.35);
}
/* "Needs review" — a deck that's gone invalid (rotation / collection shortfall).
   Red-tinted to read as an attention state, distinct from the neutral badges. */
.badge-review {
  background: rgba(220, 38, 38, 0.16);
  color: #ff9a9a;
  border: 1px solid rgba(220, 38, 38, 0.45);
}
.badge-type {
  background: var(--gold-dim);
  color: var(--gold-light);
  border: 1px solid rgba(212, 169, 59, 0.35);
}

/* My Decks empty / no-match state */
.decks-empty {
  grid-column: 1 / -1;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 700;
  text-align: center;
  padding: 28px 8px;
  line-height: 1.6;
}

/* Small button variant — used by tile Edit / overflow controls.
   .btn-ghost is defined with the other button variants further up. */
.btn-sm { padding: 8px 16px; font-size: 13px; }

@media (max-width: 560px) {
  .deck-grid { grid-template-columns: 1fr; }
  .df-search { width: 120px; }
}

/* ═══════════════════════════════════════
   DECK CREATION WIZARD  (Deck Builder — Screen 2)
   Full-screen overlay; ported from mockup-deck-builder.html.
   ═══════════════════════════════════════ */
/* Overlay + open/close animation mirror the Disclaimer dialog (see
   .disclaimer-overlay) so every blocking modal in the app shares the
   same opacity-fade + slide-up entry. The .is-open class is toggled by
   openDeckWizard()/_closeDeckWizard() in app.js — the
   requestAnimationFrame in the open path is what lets the CSS
   transition actually run instead of snapping to the end state. The
   350 ms duration is also pinned in _closeDeckWizard's setTimeout —
   keep all three in sync. */
.wizard-bg {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);   /* fallback; the modal-stack manager overrides per depth */
  background: rgba(0,0,0,0.65);
  backdrop-filter: blur(5px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  overflow-y: auto;
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}
.wizard-bg.is-open { opacity: 1; pointer-events: all; }

.wizard {
  width: 600px;
  max-width: 100%;
  background: var(--bg-elevated);
  /* Fantasy window border — same asset and slice the Disclaimer and Help
     dialogs use, so every blocking modal has a consistent frame. */
  border: 30px solid transparent;
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch;
  border-image-width: 30px;
  background-clip: padding-box;
  border-radius: 0;
  box-shadow: 0 24px 70px rgba(0,0,0,0.7);
  overflow: hidden;
  transform: translateY(60px);
  transition: transform 350ms ease;
}
.wizard-bg.is-open .wizard { transform: translateY(0); }
.wiz-head {
  padding: 20px 26px 16px;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, var(--bg-elevated), var(--bg-surface));
}
.wiz-head h2 { font-size: 19px; font-family: 'Bitter', serif; color: var(--text-bright); }
.wiz-head p  { font-size: 12.5px; color: var(--text-dim); margin-top: 2px; }
.wiz-steps { display: flex; gap: 8px; margin-top: 14px; }
.wiz-step {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-dim);
}
.wiz-step .dot {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1.5px solid var(--border-hover);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 700;
  flex: none;
}
.wiz-step.done .dot { background: var(--emerald); border-color: var(--emerald); color: #04120c; }
.wiz-step.on   .dot { background: var(--gold);    border-color: var(--gold);    color: #1a1405; }
.wiz-step.on        { color: var(--text-bright); font-weight: 600; }
/* No min-height — the dialog sizes to its current step's content so Step 1
   doesn't leave an empty band below the description. */
.wiz-body { padding: 24px 26px; }
.wiz-pane { display: none; }
.wiz-pane.on { display: block; animation: wiz-fade 0.2s ease; }
@keyframes wiz-fade {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.wiz-pane h3 { font-size: 17px; margin-bottom: 3px; color: var(--text-bright); }
.wiz-pane .sub { font-size: 13px;   color: var(--text-muted); margin-bottom: 16px; }
.wiz-field { margin-bottom: 16px; }
/* Last field's bottom margin is redundant with .wiz-body's 24px bottom padding —
   together they left a ~40px dead band above the wizard footer (most visible on
   step 1, below the Description field). Drop it so the only bottom spacing is the
   body's own padding. */
.wiz-pane > .wiz-field:last-child { margin-bottom: 0; }
.wiz-field label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.wiz-field .wiz-optional { font-size: 13px; color: var(--text-dim); font-weight: 400; }
.wiz-field .inp,
.wiz-field textarea {
  width: 100%;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 10px 12px;
  font-size: 13px;
  font-family: inherit;
  color: var(--text-main);
  outline: none;
}
.wiz-field .inp:focus,
.wiz-field textarea:focus { border-color: var(--gold); }
/* resize: vertical blocks horizontal resizing entirely; max-height caps
   the user-drag to 2 × min-height so the textarea can't grow to a
   ridiculous size and shove the wizard footer off-screen. */
.wiz-field textarea { resize: vertical; min-height: 64px; max-height: 128px; }
.wiz-charcount { font-size: 12px; color: var(--text-dim); text-align: right; margin-top: 3px; }
.wiz-error {
  font-size: 13px;
  color: var(--ruby);
  margin-top: 4px;
}
.ink-pick { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.ink-opt {
  background: var(--bg-deep);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-m);
  padding: 14px 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  transition: 0.13s;
  position: relative;
  cursor: pointer;
}
@media (any-hover: hover) {
.ink-opt:hover { border-color: var(--border-hover); }
}
.ink-opt .ink-opt-icon { width: 34px; height: 34px; flex-shrink: 0; object-fit: contain; }
.ink-opt span { font-size: 13px; font-weight: 500; color: var(--text-muted); }
.ink-opt.sel {
  border-color: var(--gold);
  background: var(--gold-dim);
  box-shadow: 0 0 0 1px var(--gold) inset;
}
.ink-opt.sel span { color: var(--gold-light); }
.ink-opt.disabled { opacity: 0.32; pointer-events: none; }
.ink-note {
  font-size: 11.5px;
  color: var(--text-dim);
  margin-top: 13px;
  text-align: center;
  border: 1px dashed var(--border-hover);
  border-radius: var(--radius-s);
  padding: 8px;
}
.radio-grid { display: flex; flex-direction: column; gap: 9px; }
.radio-card {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  background: var(--bg-deep);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-m);
  padding: 12px 13px;
  transition: 0.13s;
  cursor: pointer;
}
@media (any-hover: hover) {
.radio-card:hover { border-color: var(--border-hover); }
}
.radio-card.sel { border-color: var(--gold); background: var(--gold-dim); }
.radio-card .rdot {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid var(--border-hover);
  flex: none;
  margin-top: 1px;
}
.radio-card.sel .rdot {
  border-color: var(--gold);
  background: radial-gradient(circle, var(--gold) 38%, transparent 42%);
}
.radio-card .rc-title { font-size: 13.5px; font-weight: 700; color: var(--text-bright); }
.radio-card .rc-title .rc-hint { color: var(--text-dim); font-weight: 400; }
.radio-card .rc-desc { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
.wiz-group-label {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--gold-light);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  margin: 4px 0 9px;
}
.wiz-group-label.spaced { margin-top: 16px; }
.wiz-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 26px;
  border-top: 1px solid var(--border);
  background: var(--bg-deep);
}
.wiz-foot .spacer { flex: 1; }
.wiz-foot .btn:disabled { opacity: 0.45; cursor: not-allowed; }
@media (any-hover: hover) {
.wiz-foot .btn:disabled:hover { box-shadow: none; }
}

/* ── Location card landscape display ─────────────────────────────────────────
   Location card images are stored PORTRAIT in R2 (same as all other cards).
   Mid-size images are 700 × 977 px (resizeToMid: 700 px wide, proportional
   height from 734×1024 lorcanajson source).  To display them landscape we
   rotate the <img> 90° inside a landscape cell.

   Math: portrait mid image is 700:977 (width:height).
   We want the rotated image to fill the container exactly.
   After rotate(90deg), CSS dimensions swap:
     visual width  = img CSS height
     visual height = img CSS width
   We need visual width = CW, so img CSS height = CW:
     img CSS height = img CSS width × (977/700)  [natural portrait ratio]
     → img CSS width × (977/700) = CW
     → img CSS width = CW × (700/977)  = calc(100% * 700 / 977)  ≈ 71.65%
   Container height CH = img CSS width = CW × (700/977)
     → aspect-ratio: 977/700  (the rotated image's landscape ratio)
   After rotate(90deg):
     visual width  = img CSS height = CW                  ← fills container width  ✓
     visual height = img CSS width  = CH (977/700 ratio)  ← fills container height ✓
   No overflow; nothing to clip.                                                */

/* Regular card grids (Card Sets, My Collection, Missing) */
.card-thumb--location {
  aspect-ratio: 977 / 700; /* landscape ratio of the rotated 700×977 mid image */
  /* The flat element background (portrait card back) can't be rotated to match the
     landscape cell, so suppress it here and paint the rotated back via ::before. */
  background-image: none;
}
/* DIRECT-child img only — the rotated card image. Scoped with `>` so it never
   matches NESTED imgs (e.g. the Hunny badge icon in .card-badges), which would
   otherwise inherit position:absolute + rotate(90deg) and end up displaced. */
.card-thumb--location > img {
  position: absolute;
  width: calc(100% * 700 / 977); /* ≈ 71.65% — see math above */
  height: auto;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(90deg);
}
/* Placeholder (card back) for location tiles — a portrait-proportioned ::before sized
   and rotated 90° CW exactly like the real location <img> above, so the card back
   fills the landscape cell in the same orientation as a real location card. It sits
   below the <img>/badges via DOM order (::before is the first child; equal z-index =
   tree order), so no z-index juggling is needed; the real image covers it on load. */
.card-thumb--location::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(100% * 700 / 977);   /* container height after rotate → fills width  */
  height: calc(100% * 977 / 700);  /* container width  after rotate → fills height */
  background: url('../images/cards/card_back.jpg') center / cover no-repeat;
  transform: translate(-50%, -50%) rotate(90deg);
}

/* ── Confirm delete dialog ──
   Open/close animation + frame mirror the Disclaimer dialog (see
   .disclaimer-overlay) so every blocking modal in the app shares the
   same opacity-fade + slide-up entry, the same fantasy frame, and the
   same backdrop blur.  The .is-open class is toggled by _openModal /
   _closeModal in app.js.

   ⚠ DEPRECATED (2026-06-27): every dialog was migrated to the canonical
   `.bld-modal` shell (head/body/foot). These `.confirm-dialog*` structural
   rules are now UNUSED and kept only transiently — do NOT build new dialogs on
   them. The ONE survivor is `.confirm-dialog-warn` (the ruby warning line),
   reused inside `.bld-modal .modal-body`. Safe to delete this block (minus
   `.confirm-dialog-warn`) in a later cleanup pass. */
.confirm-dialog-overlay {
  position: fixed;
  inset: 0;
  background: rgba(4, 4, 15, 0.78);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  z-index: var(--z-modal);   /* fallback; the modal-stack manager overrides per depth */
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}
.confirm-dialog-overlay.is-open { opacity: 1; pointer-events: all; }
.confirm-dialog {
  background: var(--bg-elevated);
  /* Fantasy window border — same asset / pattern used by .disclaimer-dialog.
     Replaces the previous 1 px border + radius-xl rounded box so every
     blocking modal has a consistent frame. */
  border: 30px solid transparent;
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch;
  border-image-width: 30px;
  background-clip: padding-box;
  border-radius: 0;
  padding: 24px 28px;
  max-width: 420px;
  width: 90vw;
  box-shadow: 0 24px 64px rgba(0,0,0,0.75);
  transform: translateY(60px);
  transition: transform 350ms ease;
}
.confirm-dialog-overlay.is-open .confirm-dialog { transform: translateY(0); }
.confirm-dialog-title {
  font-family: 'Bitter', serif;
  font-size: 18px;
  color: var(--text-bright);
  margin-bottom: 18px;
}
.confirm-dialog-body p { color: var(--text-muted); font-size: 14px; margin-bottom: 10px; }
.confirm-dialog-msg p {
  color: var(--text-muted);
  font-size: 14px;
  margin-bottom: 10px;
  /* Substituted user-supplied content (deck name, card name, etc.) can
     contain extremely long unbreakable tokens — paste from clipboard or
     user typing a single uninterrupted word.  Without these two rules
     the long token would extend past the dialog's right edge and bleed
     outside the rounded box.  `anywhere` allows a break at ANY character
     when nothing else can; `break-word` is the legacy property kept for
     older browsers.  Same pattern .dt-desc uses for the same reason. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.confirm-dialog-msg p:first-child { margin-bottom: 12px; font-size: 15px; }
.confirm-dialog-warn   { color: var(--ruby) !important; }
.confirm-dialog-actions { display: flex; gap: 12px; margin-top: 24px; }

/* Destructive action buttons use --danger (tinted red) rather than
   --ruby (bright Lorcana ink colour) so the two concerns can be tuned
   independently. Keep the white text — it preserves contrast against
   the darker tinted background. */
.btn-danger              { background: var(--danger); color: #fff; border: none; }
@media (any-hover: hover) {
.btn-danger:hover        { background: var(--danger-hover); }
}

.btn-danger-outline      { background: transparent; border-color: var(--danger); color: var(--danger); }
@media (any-hover: hover) {
.btn-danger-outline:hover { background: rgba(160,0,0,0.12); }
}

.btn-info-outline       { background: transparent; border-color: var(--sapphire); color: var(--sapphire); }
@media (any-hover: hover) {
.btn-info-outline:hover { background: rgba(59,130,246,0.12); }
}

/* ── Textarea for deck description ── */
.form-textarea {
  resize: vertical;
  min-height: 72px;
  font-family: 'Bitter', serif;
}

/* ═══════════════════════════════════════
   ACCOUNT SETTINGS DIALOG
   ═══════════════════════════════════════ */

/* Overlay + dialog chrome for the Account Settings ("Edit Illumineer")
   modal.  Open/close animation + fantasy frame mirror the Disclaimer
   dialog (see .disclaimer-overlay); the .is-open class is toggled by
   _openModal / _closeModal in app.js.

   These rules were previously named .trade-dialog-* and shared with the
   Trade Helper feature.  Trade Helper was deleted; the Account Settings
   dialog was the sole remaining consumer, so the classes were renamed to
   .edit-ill-overlay / .edit-ill-close on 2026-06-06 to drop the dead
   trade naming (the dialog box itself is .edit-ill-dialog, set in
   showAccountSettingsDialog). */
.edit-ill-overlay {
  position: fixed;
  inset: 0;
  background: rgba(4, 4, 15, 0.8);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: var(--z-modal);   /* fallback; the modal-stack manager overrides per depth */
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}
.edit-ill-overlay.is-open { opacity: 1; pointer-events: all; }

.edit-ill-overlay .edit-ill-dialog {
  background: var(--bg-panel);
  border: 30px solid transparent;
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch;
  border-image-width: 30px;
  background-clip: padding-box;
  border-radius: 0;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.75);
  max-width: 1060px;
  width: 95%;
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  position: relative;
  transform: translateY(60px);
  transition: transform 350ms ease;
}
.edit-ill-overlay.is-open .edit-ill-dialog {
  transform: translateY(0);
}

.edit-ill-close {
  position: absolute;
  top: 16px;
  right: 16px;
  background: none;
  border: none;
  font-size: 24px;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px 8px;
  transition: color 0.2s;
  z-index: 10;
}

@media (any-hover: hover) {
.edit-ill-close:hover {
  color: var(--text-bright);
}
}

/* ── How-to-play modal ──
   Open/close animation matches the Disclaimer dialog (.disclaimer-overlay).
   The modal lives persistently in the DOM (unlike confirm / bld
   modals which are insertion/removal), so `.hidden` is still added on
   close to take it out of the flow once the fade-out finishes — see
   openHtpModal / closeHtpModal in app.js for the JS sequencing. */

.htp-modal {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Fade-in / out — same 350 ms ease as every other blocking modal. */
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}
.htp-modal.is-open { opacity: 1; pointer-events: all; }
.htp-modal.hidden { display: none; }

.htp-modal-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
}

.htp-modal-content {
  position: relative;
  z-index: 1;
  background: var(--surface-2, #1e1e2e);
  border: 30px solid transparent;                                              /* [FRAME] */
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch; /* [FRAME] */
  border-image-width: 30px;                                                    /* [FRAME] */
  background-clip: padding-box;                                                /* [FRAME] */
  border-radius: 0;
  width: min(680px, 94vw);
  max-height: 88vh;
  overflow-y: auto;
  padding: 16px 20px 20px;
  box-shadow: 0 24px 80px rgba(0,0,0,0.8);
  /* Slide-up animation matches .disclaimer-dialog. */
  transform: translateY(60px);
  transition: transform 350ms ease;
}
.htp-modal.is-open .htp-modal-content { transform: translateY(0); }

.htp-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 18px;
  cursor: pointer;
  line-height: 1;
  padding: 4px 8px;
  border-radius: var(--radius-s);
  transition: color 0.15s, background 0.15s;
}
@media (any-hover: hover) {
.htp-modal-close:hover {
  color: var(--text-main);
  background: rgba(255,255,255,0.07);
}
}

.htp-modal-header {
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.htp-modal-title {
  font-family: 'Bitter', serif;
  font-size: 20px;
  font-weight: 700;
  color: var(--text-main);
  margin: 0 0 4px;
}
.htp-modal-subtitle {
  font-size: 12px;
  color: var(--text-muted);
  margin: 0;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.htp-section {
  margin-bottom: 28px;
}
.htp-section:last-child { margin-bottom: 0; }

.htp-section-heading {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-muted);
  margin: 0 0 12px;
}

.htp-explanation {
  font-size: 14px;
  line-height: 1.7;
  color: var(--text-main);
  margin: 0;
}

.htp-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.htp-synergy-item,
.htp-example-item {
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.07);
  border-radius: var(--radius-m, 10px);
  padding: 14px 16px;
}

.htp-item-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-main);
  margin-bottom: 6px;
}

.htp-item-body {
  font-size: 13px;
  line-height: 1.65;
  color: var(--text-secondary, #b0b0c0);
}


.htp-synergy-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

.htp-synergy-chip {
  font-size: 11px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(99, 102, 241, 0.18);
  color: #a5b4fc;
  border: 1px solid rgba(99, 102, 241, 0.3);
  white-space: nowrap;
}

.htp-loading,
.htp-unavailable,
.htp-error {
  text-align: center;
  padding: 48px 24px;
  font-size: 14px;
  color: var(--text-muted);
}
.htp-error { color: #f87171; }

/* ── Language selector (nav) ── */
.nav-lang {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: 12px;
  flex-shrink: 0;
}
.nav-lang-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}
.lang-select {
  appearance: none;
  background: rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.13);
  border-radius: var(--radius-s, 6px);
  color: var(--text-main);
  font-size: 13px;
  font-weight: 600;
  padding: 3px 24px 3px 8px;
  cursor: pointer;
  /* custom arrow */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 7px center;
  transition: border-color 0.15s;
}
.lang-select:focus { outline: none; border-color: var(--gold-light, #e8c96d); }
.lang-select option { background: #1a1c2e; color: var(--text-main); }

/* ── HTP language badge ── */
.htp-lang-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  padding: 2px 6px;
  border-radius: 4px;
  background: rgba(232, 201, 109, 0.18);
  border: 1px solid rgba(232, 201, 109, 0.35);
  color: var(--gold-light, #e8c96d);
  vertical-align: middle;
  margin-left: 6px;
}


/* ═══════════════════════════════════════
   DECK BUILDER — Screen 3
   Full-screen overlay: header + two-pane body (search & add | live deck stats).
   Ported from mockup-deck-builder.html Screen 3.
   ═══════════════════════════════════════ */

/* Full-screen overlay shell (mirrors .wizard-bg). */
/* The builder is rendered into #app-root like any other page (Home, Card
   Sets, My Collection), so a nav click naturally replaces it. The container
   simply fills the space between the fixed top nav and bottom bar. */
.builder-overlay {
  /* Builder lives INSIDE #ill-tab-content (My Decks sub-tab).  Fallback height
     for browsers without :has() — subtracts --tabs-h (the breadcrumb height).
     The flex override below is preferred: it fills the tab area EXACTLY so no
     void shows above the fixed bottom bar, instead of relying on --tabs-h
     matching the breadcrumb's real (post-tighten) line box. overflow:hidden so
     the inner #bld-search-body / #bld-deck-scroll own all the scrolling. */
  height: calc(100vh - var(--nav-h) - var(--bottom-bar-h) - var(--tabs-h));
  background: var(--bg-void);
  overflow: hidden;
}
/* Preferred fill: make the builder's tab-content a flex column that takes all
   the height left after the breadcrumb, and let the overlay fill it. Scoped to
   the builder via :has() so the deck-detail page (also .subview-fullbleed, but
   relies on natural-height window scroll) keeps its current behaviour.

   CRITICAL: percentage/flex heights only resolve if the chain has a DEFINITE
   height. .mycollection-page (.page) only carries a min-height, so when the
   builder is mounted we pin it to the exact viewport slot (nav + bottom bar
   removed). Without this the overlay's height:100% computes to auto, the
   overlay grows to its content, overflow:hidden stops clipping, the panes
   lose their independent scroll (the window scrolls instead) and the
   vertically-centred divider toggle is pushed out of view. */
.mycollection-page:has(.builder-overlay) {
  height: calc(100vh - var(--nav-h) - var(--bottom-bar-h));
}
#ill-tab-content.subview-fullbleed:has(.builder-overlay) {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
#ill-tab-content.subview-fullbleed:has(.builder-overlay) .builder-overlay {
  height: 100%;
}
.builder-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  height: 100%;
}
.builder-error p { color: var(--ruby); font-size: 14px; }

.builder {
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* (1) top header — deck name, attributes, inks, total */
.bld-top {
  display: flex;
  align-items: flex-start;        /* was center — now anchors the right-side Done button to the top of a possibly-multi-line title block */
  gap: 14px;
  padding: 11px var(--gutter-content);
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border);
  flex: none;
}
.bld-title-block {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.bld-title { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.bld-title .bld-name {
  /* Matched to .detail-head h1 so the same deck title reads identically
     on the deck-builder and deck-detail pages.  Both pages already share
     the same icons + badges + description; the title was the last visual
     difference.  Dropped 25 px → 24 px on 2026-06-06 — at 25 px the
     title felt outsized relative to the surrounding chrome on both
     surfaces.  Then 24 px → 23 px on 2026-06-06 (another 1 px down).
     Keep the two literals in lock-step. */
  font-size: 23px;
  font-weight: 700;
  color: var(--text-bright);
}
.bld-pencil { font-size: 16px; color: var(--text-dim); padding: 2px; cursor: pointer; }   @media (any-hover: hover) {
/* +25% (was 13px) */
.bld-pencil:hover { color: var(--gold-light); }
}

/* Description line below the deck title, inside the .bld-top panel — sits
   in the .bld-title-block flex column alongside the title row. Clickable
   shortcut into the same "Edit deck info" modal the pencil opens.
   Mirrors .detail-head .dh-desc on the read-only deck page (max-width
   620px, muted, line-height 1.55) so the two surfaces are visually
   indistinguishable. Multi-line allowed (no nowrap) — descriptions are
   short by design (200-char input cap). */
.bld-description {
  font-size: 13px;
  color: var(--text-muted);
  max-width: 620px;
  line-height: 1.55;
  cursor: pointer;
}
@media (any-hover: hover) {
.bld-description:hover { color: var(--text-bright); }
}
/* The element stays in the DOM even when the deck has no description, so
   _bldRefreshChrome can update its textContent in place after the Edit
   deck info modal saves a new value. Hide it visually + take it out of
   the flex column's gap calculation while empty. */
.bld-description.empty { display: none; }
.bld-save-state {
  font-size: 11.5px;
  color: var(--emerald);
  display: flex;
  align-items: center;
  gap: 5px;
}
.bld-save-state.saving { color: var(--gold-light); }
.bld-counter { margin-left: auto; display: flex; align-items: center; gap: 14px; }
/* Builder deck title — ink badges sit inline BEFORE the deck name, matching
   the Deck Detail page's <h1> markup exactly.  Same .dh-inks + .dh-ink-icon
   classes; we just open up the .detail-head-scoped CSS so the builder can
   use them too.  See _bldRenderShell + _bldInkClusterHTML. */
.bld-title .dh-inks { display: inline-flex; flex-direction: row; align-items: center; gap: 5px; }
.bld-title .dh-ink-icon {
  width: 22px;
  height: 22px;
  flex-shrink: 0;   /* never squash the ink icons next to a long deck name */
  border-radius: 50%;
  object-fit: contain;
}
#bld-ink-cluster { display: inline-flex; flex-direction: row; align-items: center; gap: 5px; }
.bld-ink-none { font-size: 13px; color: var(--text-dim); }
.bld-count-big { font-size: 15px; font-weight: 700; color: var(--text-bright); }
.bld-count-big .max { color: var(--text-dim); font-weight: 400; }

/* (2) two-pane body */
.bld-split { display: flex; flex: 1; overflow: hidden; }
/* Right pane: a 68/32 split, but the right "Cards in deck" pane is CAPPED at
   570px on widescreen (its deck-list pills don't need more — past that it's
   wasted space; the freed width goes to the left search grid).
   Pane width is animated via flex-basis (a length), NOT flex-grow. The two
   bases ALWAYS sum to 100% of the row (divider is 0-wide, grow/shrink are no-ops
   with no free space), so width == flex-basis throughout and interpolating it is
   LINEAR in width — making collapse and expand symmetric.
   The 570px cap is baked into the flex-basis itself with min()/max() so the
   sum-to-100% invariant still holds and the basis recomputes per frame at the
   current window size. A `max-width` clamp instead (the earlier attempt) broke
   the animation: it changed the RENDERED width independently of the animated
   basis, so the motion snapped to the cap. 0.6s so the motion is clearly
   visible. */
.bld-search {
  flex: 1 1 max(68%, calc(100% - 570px));   /* fills whatever the capped deck pane leaves */
  min-width: 0;
  overflow: hidden;                 /* clip content when collapsed to 0 width (expanded) */
  display: flex;
  flex-direction: column;
  transition: flex-basis 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.bld-deck {
  flex: 1 1 min(32%, 570px);        /* 32% until that exceeds 570px, then capped */
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg-surface);
  transition: flex-basis 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Expanded → deck pane fills the row, search pane collapses to zero width. */
.bld-split.expanded .bld-search { flex-basis: 0%; }
.bld-split.expanded .bld-deck   { flex-basis: 100%; }

/* (3) divider with arrow button */
/* The dark middle bar is gone — divider is now a zero-width layout anchor.
   The .divider-toggle button (position:absolute, top/left:50%) floats over
   the boundary between the two panes, so the toggle stays clickable but
   takes no horizontal space. */
.bld-divider {
  width: 0;
  flex: none;
  background: transparent;
  border: none;
  position: relative;
}
/* Inverted colour scheme (2026-06-06): the button now reads as a gold
   pill with a dark-navy arrow, which makes the affordance obvious
   without a separate label.  Previous treatment was dark bg + gold
   arrow + a small "VIEW DECK" / "SEARCH" caption above the button;
   the caption + caption rule are gone, and the aria-label flips
   inside _bldToggleExpand to keep screen readers in sync. */
.divider-toggle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 32px;
  height: 64px;
  border-radius: 9px;
  background: var(--gold);
  border: 1px solid var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Arrow colour — dark navy lifted straight from the page bg so the
     arrow reads as a deep-ink cutout against the gold pill. */
  color: var(--bg-deep);
  z-index: 6;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.5);
  transition: 0.13s;
}
@media (any-hover: hover) {
.divider-toggle:hover {
  background: var(--gold-light);
  border-color: var(--gold-light);
}
}
.divider-toggle .arrow {
  font-size: 14px;
  /* Slightly heavier so the dark glyph reads against the gold pill at
     the same optical weight the previous gold-on-dark treatment had. */
  font-weight: 700;
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);   /* flips in sync with the 0.6s pane animation */
}
.bld-split.expanded .divider-toggle .arrow { transform: rotate(180deg); }
/* When expanded the search pane is 0-wide, so the boundary the toggle floats
   over sits at the very left edge — pin the button just inside it (instead of
   centred on a zero-width divider, where it would be half off-screen) so the
   user can always collapse again. */
.bld-split.expanded .divider-toggle { left: 6px; transform: translate(0, -50%); }
/* Give the deck content a left gutter in expanded mode so the floating
   collapse button never overlaps a card / stat block. */
.bld-split.expanded .deck-scroll { padding-left: 52px; }

/* pane label */
.builder .pane-label {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 18px 5px;          /* top/bottom halved (was 12px/10px) — item 8 */
  flex: none;
}
.builder .pane-label .pl-icon {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  border-radius: 7px;
  background: var(--gold-dim);
  border: 1px solid rgba(212, 169, 59, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
}
.builder .pane-label .pl-text .t {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-bright);
}
.builder .pane-label .pl-text .s { font-size: 11px; color: var(--text-dim); }

/* search filters (in the left pane, above the grid) — no longer its own
   scroll area; it scrolls together with the grid inside .bld-search-body. */
.search-filters {
  padding: 10px 18px 12px;
  border-bottom: 1px solid var(--border);
  flex: none;
}
.search-filters .filter-row { margin-bottom: 8px; }
.search-filters .filter-row:last-child { margin-bottom: 0; }
/* in expanded mode the left pane is just a peek strip — hide the heavy filters */
.bld-split.expanded .search-filters { display: none; }

/* ── Deck-construction rule chip ──
   Persistent reminder beneath the filter bar when a rule like
   Gather the Party is active. See SPEC.md → Deck construction rule engine.
   Empty container (#bld-rule-chip) collapses to nothing when no rule is
   active — no extra space taken by an empty div. */
#bld-rule-chip { padding: 0 18px; }
#bld-rule-chip:empty { display: none; }
.bld-rule-chip {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 6px;
  padding: 6px 10px;
  border: 1px solid var(--gold, #d9b76b);
  border-radius: 6px;
  background: rgba(217, 183, 107, 0.10);
  color: var(--text, inherit);
  font-size: 12px;
  line-height: 1.3;
  cursor: default;
}
/* display:inline-block (matching .f-chip-icon) — the global `img{display:block}`
   makes a block SVG with width="100%" collapse to 0 in the flex chip; inline-block
   sizes correctly. Same convention every icon img in the app uses. */
.bld-rule-chip .rc-icon {
  display: inline-block;
  width: 16px; height: 16px;
  object-fit: contain; flex-shrink: 0;
}
.bld-rule-chip .rc-text { font-weight: 500; }

/* Honey glyph beside the "Gather the Party" rule-triggered modal title — reuses the
   feature's hunny_icon.svg. Flex title keeps the icon vertically centred with the
   heading; explicit width sizes the width="100%" SVG (see .rc-icon convention). */
.gtp-modal-title { display: flex; align-items: center; gap: 10px; }
.gtp-title-icon { width: 26px; height: 26px; flex-shrink: 0; object-fit: contain; }
.bld-split.expanded #bld-rule-chip { display: none; }

/* Left pane body — ONE scroll area holding the filter bar AND the card grid so
   they scroll together, matching the My Cards page. */
.bld-search-body { flex: 1; overflow-y: auto; }
.bld-search-grid { padding: 8px 18px 50px; }
.builder .results-line {
  /* Same scale and tone as .cards-count on the My Cards page so the two
     filter-panel result lines read the same. */
  font-size: 12px;
  font-weight: 500;
  color: var(--text-bright);
  margin-bottom: 11px;
}
.builder .card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 13px;
}

@media (any-hover: hover) {
/* The builder reuses the My Cards .card-thumb image tiles. The hover zoom
   (lift + scale) is removed — only the border / glow highlight is kept. */
.builder .card-thumb:hover,
.builder .card-thumb[data-multi-ink="true"]:hover {
  transform: none;
}
}
/* Builder grid cell — the card PICTURE + a stepper / owned footer wrapped in a
   single panel (#070719) so the stepper and the two text lines read as part of
   the card above them rather than floating loose. When the card is in the deck
   the panel takes the gold highlight border + glow (the stepper keeps its own
   .card-stepper mini-panel). */
.builder .bld-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: #070719;
  border: 1px solid #17172c;
  border-radius: 12px;
  padding: 7px;
}
/* White (not gold) highlight — gold blended too much with Amber-ink cards. */
.builder .bld-card--in-deck {
  border-color: #ffffff;
  box-shadow: 0 0 0 1px #ffffff, 0 0 18px rgba(255, 255, 255, 0.40);
}
/* collection_and_deck_tracked: a card with no copies free (owned − used ≤ 0)
   is greyed out (white/gray/black) so the user sees it can't be used. The
   footer (Owned/Used line + the disabled stepper) stays in colour. */
.builder .bld-card--unavailable .card-thumb { filter: grayscale(1); opacity: 0.72; }
.builder .bld-card-foot { display: flex; flex-direction: column; gap: 4px; }
/* Builder card grid uses smaller cells than the Card Sets page so more cards
   fit per row in the narrower left pane. */
/* LEFT search grid ONLY — its own tile size. Scoped to #bld-search-grid (not
   .builder) so the RIGHT pane's expanded "Card pictures" grid is NOT shrunk:
   it falls back to the global .cards-grid / .cards-grid-locations and therefore
   matches the Deck Detail page pixel-for-pixel.  200px (was 150px) so the
   picture inside the new .bld-card panel reads close to the Card Sets / My
   Cards tiles. */
#bld-search-grid .cards-grid           { grid-template-columns: repeat(auto-fill, minmax(min(200px, (100% - 12px) / 2), 1fr)); }
#bld-search-grid .cards-grid-locations { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }

/* Paired stat blocks (Ink Balance + Inkable Ratio, Willpower + Strength)
   render side by side when the column is wide enough; otherwise they stack. */
/* Stat row used by BOTH the builder's right pane AND the deck-detail page's
   sidebar.  The selector was previously scoped to `.builder` only, which left
   the .detail-body sidebar with no gap — adjacent panels (Ink Balance ↔
   Inkable Ratio, Willpower ↔ Strength) visually touched when the row
   wrapped to a single column.  Sharing the rule between both surfaces keeps
   the 11px rhythm consistent everywhere. */
.builder .bld-stat-row,
.detail-body .bld-stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 11px;
}
.builder .bld-empty {
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  padding: 28px 8px;
}
.builder .bld-empty-small {
  color: var(--text-dim);
  font-size: 12px;
  text-align: center;
  padding: 14px 8px;
}
/* Empty-deck state (right pane) — fills the pane so the 🃏 + message sit
   vertically + horizontally centered, mirroring the Card Sets empty state. */
.builder .bld-deck-empty { min-height: 60vh; padding: 24px; }

/* card tile (builder-scoped — distinct from the global .card-thumb) */
.builder .card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  overflow: hidden;
  transition: 0.12s;
  position: relative;
}
@media (any-hover: hover) {
.builder .card:hover { border-color: var(--border-hover); }
}
.builder .card.in-deck {
  border-color: var(--gold);
  box-shadow: 0 0 0 1px var(--gold-dim);
}
.builder .card-art {
  height: 116px;
  position: relative;
  display: flex;
  align-items: flex-end;
  padding: 8px;
  background: var(--bg-deep);
}
.builder .card-art img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.builder .card-art .card-no-img {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.builder .card-cost {
  position: absolute;
  top: 7px;
  left: 7px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--bg-void);
  border: 2px solid var(--gold);
  color: var(--gold-light);
  font-size: 13px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
.builder .card-inkdots {
  position: absolute;
  top: 9px;
  right: 8px;
  display: flex;
  gap: 3px;
  z-index: 2;
}
.bld-card-inkdot { width: 15px; height: 15px; object-fit: contain; }
.bld-card-inkdot.sm { width: 11px; height: 11px; }
.builder .card-rarity {
  position: absolute;
  bottom: 8px;
  right: 8px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.62);
  color: var(--text-bright);
  z-index: 2;
}
.builder .multi-tag {
  position: absolute;
  bottom: 8px;
  left: 8px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 3px;
  background: rgba(0, 0, 0, 0.66);
  color: var(--gold-light);
  border: 1px solid rgba(212, 169, 59, 0.4);
  z-index: 2;
}
.builder .card-body { padding: 8px 9px 9px; }
.builder .card-name {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-bright);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.builder .card-sub {
  font-size: 10.5px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 7px;
}
.builder .card-stepper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 3px;
}
.builder .step-btn {
  width: 26px;
  height: 24px;
  border-radius: 4px;
  background: var(--bg-elevated);
  font-size: 15px;
  font-weight: 700;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  border: none;
  cursor: pointer;
}
@media (any-hover: hover) {
.builder .step-btn:hover { background: var(--gold); color: #1a1405; }
}
/* Dimmed (not clickable) but still hoverable so its title= tooltip explains
   the block — the click handlers ignore `.dim`, so no pointer-events:none. */
.builder .step-btn.dim { opacity: 0.3; cursor: default; }
.builder .step-qty { font-size: 12px; font-weight: 700; color: var(--text-bright); }
/* "/ N" denominator — same size as the rest of the counter (11px); the leading
   count (the bare .step-qty text, 12px) is the only part 1px bigger. */
.builder .step-qty .of { color: var(--text-dim); font-weight: 400; font-size: 11px; }
/* "(all prints)" suffix in the stepper — same size as the rest of the counter
   (11px), dim so the leading count (12px) still reads first. */
.builder .step-qty .ap { color: var(--text-dim); font-weight: 400; font-size: 11px; }
.builder .add-btn {
  flex: 1;
  height: 24px;
  border-radius: 4px;
  background: var(--gold-dim);
  border: 1px solid rgba(212, 169, 59, 0.4);
  color: var(--gold-light);
  font-size: 11.5px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  cursor: pointer;
}
@media (any-hover: hover) {
.builder .add-btn:hover { background: var(--gold); color: #1a1405; }
}
.builder .add-btn.dim { opacity: 0.3; cursor: default; }
.builder .card-limit {
  font-size: 11px;
  color: var(--text-dim);
  text-align: center;
  margin-top: 4px;
}
/* "You own N" — cyan when copies are still available. */
.builder .card-limit--owned { color: #00F3FF; }
/* At a card-specific limit (owned reached / copy cap reached) — red + bold so
   the user understands why the ＋ is disabled. */
.builder .card-limit--maxed { color: #FF5F00; font-weight: 700; }
/* Shared per-NAME aggregate line for reprinted cards — slightly dimmer than
   .card-limit; turns red + bold when the shared copy cap is reached. */
.builder .card-name-total {
  font-size: 11px;
  color: var(--text-muted);
  text-align: center;
  margin-top: 2px;
}
.builder .card-name-total--maxed { color: #FF5F00; font-weight: 700; }
/* "Used N" / "Used X · Other decks Y" line (collection-bound deck tiles).
   GREEN while copies are still free to add; YELLOW (the "+ Add" colour) + bold
   when this printing can't add more (used == owned, or C&D: owned − used here −
   used elsewhere ≤ 0). Exhaustion is NOT an error, so it must not use the
   orange-red (#FF5F00) reserved for real problems — that's the issue border +
   .cil-line legend only. */
.builder .card-used {
  font-size: 11px;
  text-align: center;
  margin-top: 4px;
}
.builder .card-used--free  { color: #24CF31; }
.builder .card-used--maxed { color: var(--gold-light); font-weight: 700; }

/* ink-section header (builder-scoped) */
.builder .ink-section-h {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 16px 0 10px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-bright);
}
.builder .ink-section-h:first-child { margin-top: 0; }
.builder .ink-section-h .line { flex: 1; height: 1px; background: var(--border); }
.builder .ink-section-h .sc {
  font-size: 11px;
  color: var(--text-dim);
  font-weight: 600;
}
.bld-dual-dot {
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--amber) 50%, var(--ruby) 50%);
  border: 1px solid rgba(255, 255, 255, 0.25);
  flex: none;
}

/* right deck pane */
.deck-scroll { flex: 1; overflow-y: auto; padding: 14px 16px 50px; }
/* Collapsed: drop the scroll area's top padding so the "Cards in deck" panel
   sits flush against the top of the pane (no gap above it). Expanded keeps the
   14px so the Deck Progress / stats stack has breathing room at the top. */
.bld-split:not(.expanded) .deck-scroll { padding-top: 0; }
.deck-stats { display: flex; flex-direction: column; gap: 11px; }
.deck-review { display: flex; flex-direction: column; }

/* expanded → 2-column layout: stat column + the deck's cards as a grid */
.bld-split.expanded .deck-review {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 22px;
  align-items: start;
}
.deck-stats-col { min-width: 0; }
.stat-block {
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 13px 14px;
}
.stat-block.span2 { grid-column: span 2; }
/* Builder right-pane stat-block headers (Deck Progress, Ink Balance, Card
   Types, Cost Curve, Inkable Ratio, …). Unified with the .filter-label /
   .df-glabel scale (12 px / 500) so every gold-uppercase group label in
   the app reads the same. */
.sb-h {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--gold-light);
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.sb-h .spacer { flex: 1; }
.sb-h .mini { font-size: 10px; color: var(--text-dim); font-weight: 600; letter-spacing: 0; }
/* No margin-bottom — the parent (.stats-card / .deck-stats) already sets
   gap: 11px between every stat block. The extra 11px margin used to
   double the gap below ONLY this block, making the spacing under
   "Deck Progress" visibly larger than every other inter-panel gap. */

.pg-num {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 6px;
}
.pg-num b { font-size: 23px; color: var(--text-bright); }
.pg-num .max { font-size: 13px; color: var(--text-dim); }
.pg-num .left { font-size: 11.5px; color: var(--text-dim); }
.pg-num .left.bld-legal { color: var(--emerald); }
.builder .bar {
  height: 7px;
  background: var(--bg-void);
  border-radius: 4px;
  overflow: hidden;
}
.builder .bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--gold), var(--gold-light));
  border-radius: 4px;
  transition: width 0.25s;
}
.builder .bar-fill.full {
  background: linear-gradient(90deg, var(--emerald), #34d399);
}

/* type counts */
.type-rows { display: flex; flex-direction: column; gap: 7px; }
.type-row { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.type-row .ic { width: 20px; text-align: center; font-size: 13px; }
.type-row .tn { color: var(--text-muted); min-width: 74px; }
.type-row .tbar {
  flex: 1;
  height: 8px;
  background: var(--bg-surface);
  border-radius: 4px;
  overflow: hidden;
}
.type-row .tbar i {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--gold), var(--gold-light));
  border-radius: 4px;
}
.type-row .tc {
  font-weight: 700;
  color: var(--text-bright);
  min-width: 20px;
  text-align: right;
}

/* ink balance split */
.ink-split {
  display: flex;
  height: 26px;
  border-radius: var(--radius-s);
  overflow: hidden;
  border: 1px solid var(--border);
}
.ink-split .ink-seg {
  display: flex;
  align-items: center;
  justify-content: center;
  /* White + bold, +1px — matches the card-text grid item numbers. */
  font-size: 12px;
  font-weight: 700;
  color: #fff;
  min-width: 0;
}
/* Amber / Steel segments are light — a black number reads better than white. */
.ink-split .ink-seg--dark { color: #1a1405; }
.ink-split .ink-seg.bld-dim { color: var(--text-dim); background: var(--bg-elevated); }
.split-legend {
  display: flex;
  gap: 14px;
  margin-top: 7px;
  font-size: 11.5px;
  color: var(--text-muted);
  flex-wrap: wrap;
}
/* nowrap keeps each item (icon + label + count) atomic: when the row is too narrow
   (e.g. the 224px Save-Image column), the WHOLE item wraps to the next line via the
   container's flex-wrap — instead of the label breaking mid-phrase ("Not" / "Inkable"). */
.split-legend span { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
/* Bold ink name in the legend (" Emerald - 16"); the count stays muted. */
.split-legend .split-name { font-weight: 700; color: var(--text-main); }
.bld-dim { color: var(--text-dim); }

/* ink cost curve */
/* padding-top reserves headroom for the tallest bar's count label (which floats
   just above the bar top); it is ~one 10px number line + breathing room. */
.curve { display: flex; align-items: flex-end; gap: 5px; height: 100px; margin-top: 4px; padding-top: 13px; box-sizing: border-box; }
.curve-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100%;
}
/* The BAR TRACK is the only thing the bar height is a % of — the count label
   (padding-top on .curve) and the x-label (below) are OUTSIDE it. So the tallest
   bucket fills the track (100%) and every other bar is strictly proportional to
   it. This replaced a layout where the bar %, the number and the label all
   shared the column height, so tall bars overflowed and flex-shrink clamped them
   all to nearly the same height (18 and 14 looked identical). */
.curve-bar-track { position: relative; flex: 1 1 0; min-height: 0; width: 100%; display: flex; align-items: flex-end; }
.curve-bar-wrap {
  flex: none;
  width: 100%;
  display: flex;
  flex-direction: column-reverse;
  border-radius: 3px 3px 0 0;
  overflow: hidden;
}
.curve-seg { width: 100%; }
/* Absolute, at the same % from the track bottom as the bar's own height → the
   count hugs ITS bar's top instead of sitting in a fixed row. */
.curve-col .cv-num { position: absolute; left: 0; right: 0; text-align: center; margin-bottom: 2px; font-size: 10px; font-weight: 700; color: var(--text-main); line-height: 1; }
.curve-col .cv-x { flex: none; margin-top: 3px; font-size: 10px; color: var(--text-dim); }
.curve-col.empty .curve-bar-wrap {
  background: var(--bg-surface);
  height: 3px;
  border-radius: 3px;
}

/* willpower spread */
.wp-rows { display: flex; flex-direction: column; gap: 5px; }
.wp-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
.wp-row .wn { min-width: 16px; text-align: center; color: var(--text-dim); font-weight: 700; }
.wp-row .wbar {
  flex: 1;
  height: 11px;
  background: var(--bg-surface);
  border-radius: 3px;
  overflow: hidden;
}
.wp-row .wbar i {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--sapphire), #7eb3fb);
  border-radius: 3px;
}
.wp-row .wc { min-width: 16px; text-align: right; color: var(--text-main); font-weight: 700; }

/* inkable ratio */
.inkable-bar {
  display: flex;
  height: 22px;
  border-radius: var(--radius-s);
  overflow: hidden;
  border: 1px solid var(--border);
  font-size: 10.5px;
  font-weight: 700;
}
.inkable-bar .ya {
  background: linear-gradient(180deg, #6ee7b7, var(--emerald));
  color: #04120c;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
}
.inkable-bar .na {
  background: var(--bg-elevated);
  color: var(--text-dim);
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
}

/* expanded-only: the cards-in-deck section */
.deck-cards-section { min-width: 0; }
/* Builder expanded pane reuses the Deck Detail filter-bar + result-line; mirror
   that page's spacing (see .detail-cards #detail-filter-bar). */
.deck-cards-section #bld-exp-filter-bar  { margin-bottom: 14px; }
.deck-cards-section #bld-exp-result-line { margin-bottom: 6px; }
.dcs-head { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; }
.dcs-head .t { font-size: 14px; font-weight: 700; color: var(--text-bright); }
.dcs-head .spacer { flex: 1; }

/* collapsed-only: text list of cards in the deck */
.deck-list-section {
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  /* L/R padding cut to 2px so the rows get maximum horizontal width (title
     indent restored on .dls-head). */
  padding: 13px 2px;
  margin-bottom: 11px;
}
.dls-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-left: 12px; }
/* Inline deck-count summary on the "Cards in deck" header (collapsed pane) —
   reuses the Deck Progress .pg-num style; fills the row and right-aligns the
   "<n> / 60 cards" count to the far edge of the panel. */
.dls-head .pg-num { flex: 1; margin: 0; justify-content: flex-end; }
/* Builder right-pane "deck list section" header (expanded view). Same
   unified group-label scale as .sb-h / .filter-label / .df-glabel. */
.dls-head .t {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--gold-light);
}
.dls-head .spacer { flex: 1; }
.dls-head .dls-count { font-size: 10.5px; color: var(--text-dim); }
.dl-group { margin-bottom: 11px; }
.dl-group:last-child { margin-bottom: 0; }
.dl-gh {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 4px;
  display: flex;
  justify-content: space-between;
}
/* Builder "Cards in deck" row — a flex wrapper holding the type emoji
   OUTSIDE a solid ink pill (left) and the pill itself.  The pill reuses the
   compact-list .pill base + .pill--<ink> solid background + .pill-cost-icon /
   .pill-ink-hex / .pill-text / .pill-name / .pill-ident styling; only the
   layout glue below is row-specific. */
.dl-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}
.dl-row:last-child { margin-bottom: 0; }
/* Type emoji rendered outside the pill, ≈ ink-icon size. */
.dl-type-emoji {
  flex: 0 0 auto;
  width: 27px;
  text-align: center;
  font-size: 22px;
  line-height: 1;
}
/* The pill fills the rest of the row (drops the compact pill's grid sizing).
   cursor:default — the pill body isn't clickable in the builder; only the
   stepper buttons are. */
.dl-pill { flex: 1 1 auto; min-width: 0; cursor: default; }

/* −/+ stepper, inside the pill on the right.  .pill-text (flex:1) pushes it
   to the trailing edge.  Dark-navy buttons matching the screenshot. */
.dl-step { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; margin-left: 6px; }
.dl-step button {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  background: #0d1030;
  font-size: 15px;
  font-weight: 700;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #2a2a52;
  cursor: pointer;
}
@media (any-hover: hover) {
.dl-step button:hover { background: var(--gold); color: #1a1405; border-color: var(--gold); }
}
/* Disabled ＋ (copy cap / owned / used reached) — mirrors the left grid's .dim. */
/* Hoverable when dim so the title= tooltip shows; the row handler ignores dim. */
.dl-step button.dim { opacity: 0.3; cursor: default; }
.dl-step .q {
  font-size: 14px;
  font-weight: 700;
  color: #fff;
  min-width: 16px;
  text-align: center;
}

/* ── Builder modals: ink-editor, rename, 200-card limit ──
   Open/close animation + frame mirror the Disclaimer dialog (see
   .disclaimer-overlay) — every blocking modal in the app shares the
   same opacity-fade + slide-up entry, fantasy frame, and backdrop blur. */
.bld-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(2, 2, 10, 0.74);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  z-index: var(--z-modal);   /* fallback; the modal-stack manager overrides per depth */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 350ms ease;
}
.bld-modal-overlay.is-open { opacity: 1; pointer-events: all; }
.bld-modal {
  /* 572px = 440 × 1.3 (widened 2026-06-06 — 440 felt cramped for the deck
     name + description + ink picker). max-width: 100% keeps it within the
     overlay's 24px gutter on narrow screens, so it only takes the extra
     width when the viewport permits. */
  width: 572px;
  max-width: 100%;
  background: var(--bg-surface);
  /* Fantasy window border — same asset / pattern used by .disclaimer-dialog. */
  border: 30px solid transparent;
  border-image: url('../images/borders/inkcannon-window-border2-v1-small.png') 92 83 81 85 stretch;
  border-image-width: 30px;
  background-clip: padding-box;
  border-radius: 0;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.8);
  overflow: hidden;
  transform: translateY(60px);
  transition: transform 350ms ease;
}
.bld-modal-overlay.is-open .bld-modal { transform: translateY(0); }
.bld-modal .modal-head {
  padding: 17px 22px;
  border-bottom: 1px solid var(--border);
}
.bld-modal .modal-head h3 { font-size: 16px; color: var(--text-bright); }
.bld-modal .modal-head p { font-size: 11.5px; color: var(--text-dim); margin-top: 2px; }
/* Body prose for the plain confirm-style dialogs migrated onto the .bld-modal
   shell (delete-deck, delete-account, remove-from-collection, limit, gather,
   language-change, profile-required, "Delete Your <lang> Cards"). Scoped to the
   opt-in `.modal-prose` class so it never clobbers the classed paragraphs the
   richer bld-modals use (size-cap's .bld-sizecap-*, edit-info's .wiz-field).
   Mirrors the old .confirm-dialog-msg typography (incl. the 15px lead line and
   the long-unbreakable-token guard for substituted card/deck names). */
.bld-modal .modal-body.modal-prose p {
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1.55;
  margin: 0 0 10px;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.bld-modal .modal-body.modal-prose p:first-child { font-size: 15px; }
.bld-modal .modal-body.modal-prose p:last-child  { margin-bottom: 0; }
/* Language-change confirm only: the SAME question repeated in the TARGET language
   (see _confirmLanguageChange). A rule divider separates it from the lead
   paragraph so the two readings are visibly one question twice, not two
   different statements. Dimmer than the lead because the lead is the one the
   user can definitely read — unless they are stranded in a language they can't,
   which is exactly why this second line exists. Tokens only; no new component. */
.bld-modal .modal-body.modal-prose p.lang-change-alt {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
}
/* Small help line under a field label inside the Edit deck info dialog
   (currently used by the ink-picker section).  Same size + colour as the
   deleted modal-head .sub paragraph used to be, so the dialog has one
   consistent "guidance text" treatment. */
.bld-modal .bld-edit-inks-sub {
  font-size: 11.5px;
  color: var(--text-dim);
  margin: 2px 0 8px;
}
.bld-modal .modal-body { padding: 20px 22px; }
/* Last field's bottom margin is redundant with the body's own 20px bottom
   padding — together they left a ~36px dead band above the footer (most
   visible in the Edit deck info dialog when the ink-removal warning is
   collapsed). Drop it so the only bottom spacing is the body's 20px padding. */
.bld-modal .modal-body > .wiz-field:last-child { margin-bottom: 0; }
.bld-modal .modal-foot {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 14px 22px;
  border-top: 1px solid var(--border);
  background: var(--bg-deep);
}
.bld-modal .modal-foot .spacer { flex: 1; }
/* ── .modal-foot--dual — footer whose buttons carry TWO languages ────────────
   Only the language-change confirm uses this (see _confirmLanguageChange). Its
   labels read "current · target", so the worst pair (FR→IT) is 54 characters on
   cancel against 26 for a normal confirm. On a folded phone that wraps to 3-4
   lines, and because the two buttons wrap to DIFFERENT line counts they end up
   visibly unequal (measured 110px vs 79px at 384px) — a ragged pair filling the
   lower third of the dialog.

   `align-items: stretch` makes both buttons take the taller one's height, so an
   uneven wrap still reads as a matched pair. Deliberately a MODIFIER: the base
   .modal-foot is shared by every .bld-modal in the app, and stretching those
   would change dialogs this feature has nothing to do with. ── */
.bld-modal .modal-foot--dual { align-items: stretch; }
/* Buttons centre their own label again once stretched (the flex item is now full
   height, so the text would otherwise sit at the top of a tall button).
   ⚠ Because this makes the button a flex container, the label MUST be wrapped in a
   single `.btn-dual` child — see _confirmLanguageChange. Two bare spans here become
   two flex items and lay out as side-by-side columns that each wrap internally,
   which rendered as an unreadable jumble on a phone (reported 2026-07-26). */
.bld-modal .modal-foot--dual .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* The single flex item. Inside it the two language spans are ordinary inline
   text, so they flow and wrap as one sentence rather than as separate boxes. */
.bld-modal .modal-foot--dual .btn-dual {
  display: block;
  text-align: center;
}
/* The target-language half of a dual label. Dimmer so each button parses as
   "my language, then the same thing again" instead of one run-on string —
   the same lead + dim-echo treatment the dialog body already uses. */
.btn-alt-lang { color: var(--text-dim); }
/* Desktop separator. Supplied by CSS rather than baked into the string so the
   mobile rule below can drop it when the two languages stack. */
.btn-alt-lang::before { content: " · "; }

/* Phones: side-by-side no longer fits — at ≤945px these labels wrap to 3-4 lines
   and the two languages run into each other with no visible boundary (exactly the
   reported bug). Give the target language its OWN line, and drop the separator: a
   "·" stranded at the start of a wrapped line reads as a bullet, not a divider. */
@media (max-width: 945px) {
  .bld-modal .modal-foot--dual .btn-alt-lang {
    display: block;
    margin-top: 5px;
  }
  .bld-modal .modal-foot--dual .btn-alt-lang::before { content: none; }
}
/* ── Account Settings — wide .bld-modal variant (mockup-approved 2026-06-27) ──
   Account Settings rides the canonical .bld-modal shell but is far larger than a
   confirm: a fixed-height, scrolling 8-panel dialog. This variant widens it
   (≈1060px, like the retired .edit-ill-dialog) and makes it a flex column so the
   .modal-head stays pinned while the body scrolls. The body element carries BOTH
   .modal-body and .edit-ill-body — the latter supplies the nav+content flex, so
   the modal-body's own padding is zeroed here (nav/content provide their own).
   position:relative anchors the close ✕ inside the head. */
.bld-modal.bld-modal--settings {
  width: min(880px, 94vw);
  height: min(600px, 86vh);          /* FIXED → switching sections never resizes the dialog */
  max-width: 100%;
  display: flex;
  flex-direction: column;
}
.bld-modal.bld-modal--settings .modal-body { padding: 0; }
.bld-modal--settings .modal-head { position: relative; flex-shrink: 0; }
.bld-modal--settings .modal-head .edit-ill-close { top: 12px; right: 14px; }
/* Mobile-only bars are hidden on desktop. Scoped to .bld-modal--settings (0,2,0) so it
   beats the .m-drawer-header { display:flex } the back bar also carries. */
.bld-modal--settings .edit-ill-m-titlebar,
.bld-modal--settings .edit-ill-m-backbar { display: none; }

/* ── MOBILE (≤700px): full-screen cover + master-detail slide ──
   Switch is WIDTH-ONLY at 700px (same idiom as the card dialog); ONE DOM, CSS decides the
   shape — no JS re-render. */
@media (max-width: 700px) {
  /* Full-screen via inset:0 (NOT height:100% — that overrides a bottom inset; and because
     .bld-modal ships transform:translateY(60px) until .is-open, a mis-sized cover would sit
     60px low and clip its footer). The fixed cover ignores the overlay's centering. */
  .bld-modal.bld-modal--settings {
    position: fixed; inset: 0;
    width: auto; height: auto; max-width: none;
    border: none; border-radius: 0; box-shadow: none;
  }
  .bld-modal--settings .modal-head.edit-ill-head-d { display: none; }   /* hide desktop head */
  /* Sticky Farmiles title bar (matches the hamburger menu) + X far right.
     Scoped to .bld-modal--settings (0,2,0) so it beats the desktop hide rule. */
  .bld-modal--settings .edit-ill-m-titlebar {
    display: flex; position: relative; flex-shrink: 0;
    align-items: center; justify-content: center;
    padding: 14px 16px; border-bottom: 1px solid var(--border);
  }
  .edit-ill-m-apptitle {
    font-family: var(--font-display); font-size: var(--fz-lg);
    letter-spacing: var(--ls-farmiles); color: var(--gold-light);
  }
  .edit-ill-m-titlebar .edit-ill-m-x {
    position: absolute; top: 50%; right: 12px; transform: translateY(-50%); font-size: 22px;
  }
  /* Sticky ‹ Back (reuses .m-drawer-header / .m-back / .m-drawer-back-label — card-dialog look);
     shown ONLY in the detail screen. */
  .edit-ill-m-backbar { flex-shrink: 0; cursor: pointer; }
  .bld-modal--settings.detail .edit-ill-m-backbar { display: flex; }
  /* Track: two full-width screens that slide. */
  .edit-ill-body { width: 200%; transform: translateX(0); transition: transform .28s ease; }
  .bld-modal--settings.detail .edit-ill-body { transform: translateX(-50%); }
  .edit-ill-nav { width: 50%; border-right: none; padding: 6px 0; }
  .edit-ill-content { width: 50%; }
  /* Nav rows render as a tappable list (no desktop left-rail chrome). */
  .edit-ill-nav-item {
    display: flex; align-items: center; justify-content: space-between;
    padding: 16px 20px; font-size: 16px; border-left: none;
    border-bottom: 1px solid var(--border); color: var(--text-muted);   /* match the ‹ Back label */
  }
  .edit-ill-nav-item::after { content: '›'; color: var(--text-muted); font-size: 20px; }
  .edit-ill-nav-item.active { background: none; font-weight: 400; }
  .edit-ill-nav-item[data-panel="delete"] { color: var(--ruby); }
  /* Fill-panel footer clears the phone's bottom safe area. */
  .edit-ill-panel--fill .edit-ill-fill-foot { padding-bottom: calc(14px + env(safe-area-inset-bottom)); }
}
.bld-modal .ink-opt .inuse {
  position: absolute;
  top: 5px;
  right: 6px;
  font-size: 9px;
  font-weight: 700;
  color: var(--gold-light);
  background: var(--gold-dim);
  border-radius: 3px;
  padding: 1px 4px;
}
.warn-box {
  display: none;
  margin-top: 14px;
  background: rgba(239, 68, 68, 0.1);
  border: 1px solid rgba(239, 68, 68, 0.45);
  border-radius: var(--radius-s);
  padding: 11px 13px;
}
.warn-box.on { display: block; }
.warn-box .wt {
  font-size: 12.5px;
  font-weight: 700;
  color: #fca5a5;
  display: flex;
  align-items: center;
  gap: 6px;
}
.warn-box .wd { font-size: 11.5px; color: var(--text-muted); margin-top: 5px; }
.bld-sizecap-main {
  font-size: 13.5px;
  color: var(--text-main);
  margin-bottom: 9px;
}
.bld-sizecap-note {
  font-size: 12px;
  color: var(--text-dim);
  line-height: 1.6;
}

/* ════════════════════════════════════════════════════════════════════════
   DECK DETAIL — read-only view (Deck Builder — Screen 4)
   Full-screen overlay: header + 2-column body (sticky stats / card grid).
   Ported from mockup-deck-builder.html Screen 4.
   ════════════════════════════════════════════════════════════════════════ */

/* The deck detail view renders into #app-root, so it's a normal block that
   fills the space between the fixed top nav and bottom bar. */
.detail-overlay {
  /* Detail page lives INSIDE #ill-tab-content (one of the four My Collection
     sub-tabs).  No fixed height — the deck detail content (header + sidebar
     + grid) grows naturally and the page-level scroll handles overflow.

     No background of its own: the .detail-head supplies its own
     gradient (bg-surface → bg-void) for the upper section, and the body
     below shows the page's own background + radial gradients + bg image
     through .detail-body. */
}
.detail-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  height: 100%;
}
.detail-error p { color: var(--ruby); font-size: 14px; }

.deck-detail {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

/* ── Header ── */
.detail-head {
  background: linear-gradient(180deg, var(--bg-surface), var(--bg-void));
  border-bottom: 1px solid var(--border);
  flex: none;
  /* Flex ROW: the title/description column (.dh-inner) on the left, the action
     buttons (.dh-actions) as their OWN column on the right. align-items:flex-start
     so a taller button column NEVER stretches .dh-inner — the description then
     hugs the title (matches the builder) instead of being pushed down by the
     buttons sharing the title's row. */
  display: flex;
  align-items: flex-start;
}
.detail-head .dh-inner {
  flex: 1 1 auto;
  min-width: 0;
  /* width 100% (no max-width) so the header spans the tab content area on
     wide screens. Side padding mirrors the rest of #ill-tab-content.
     Top padding matches .bld-top's 11px so the deck-detail and deck-
     builder pages anchor their title block at the same offset below the
     tab strip (the builder's tighter spacing is preferred). Bottom
     padding kept small — just enough breathing room below the action
     buttons. The visible head-to-body GAP lives on .detail-body's
     padding-top so it's in the body's territory.

     Flex COLUMN: the <h1> title row takes the full panel width on its own
     line, so a long deck name + ink icons stay on a single line for as
     long as the name fits the panel (only truly oversize names wrap).
     The badges now sit INLINE in the <h1> (no longer a separate row with
     margin), so the title→description breathing room comes from this
     column `gap` instead — matching the deck-builder's .bld-title-block
     spacing. Row 1 is .dh-title-row ([title | Edit/Delete] flex row); the
     optional .dh-desc is row 2, rendered ONLY when a description exists, so
     the column collapses to just the title row for description-less decks
     (the space below the title is reclaimed). */
  padding: 11px var(--gutter-content) 8px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* Title row: deck title (left, grows + wraps) and the Edit/Delete actions
   (right, pinned). align-items: flex-start keeps the buttons at the top of
   the row when a long title wraps to a second line. */
.detail-head .dh-title-row {
  display: flex;
  align-items: flex-start;
  gap: 16px;
}
.detail-head h1 {
  /* Keep in lock-step with .bld-title .bld-name. */
  font-size: 23px;
  color: var(--text-bright);
  display: flex;
  align-items: center;
  gap: 11px;
  flex-wrap: wrap;
  margin: 0;
  /* Grow to fill the row; min-width:0 lets a long name wrap within the <h1>
     instead of pushing the action buttons off the right edge. */
  flex: 1 1 auto;
  min-width: 0;
}
.detail-head .dh-inks { display: flex; gap: 5px; }
.detail-head .dh-ink-icon {
  width: 22px;
  height: 22px;
  flex-shrink: 0;   /* never squash the ink icons next to a long deck name */
  border-radius: 50%;
}
/* Format + deck-type badge row.  Used on both the read-only Deck Detail
   page (inside .detail-head) AND the Deck Builder shell (inside
   .bld-title-block).  De-scoped from .detail-head so the two surfaces
   share a single rule — a change in one place affects both, as it
   should for a rhythm that's deliberately kept identical.
   The row is never truly empty in practice — _bldFormatBadge always
   emits the construct-format badge — so no `:empty` collapse rule is
   needed. */
.dh-badges {
  display: flex;
  gap: 7px;
  margin: 10px 0;
  flex-wrap: wrap;
}
/* Builder override.  .bld-title-block is `display: flex; flex-direction:
   column; gap: 6px`, so the 6 px gap is ALREADY adding vertical space
   between sibling rows; the badge row's own 10 px margin would stack on
   top of that and produce a too-loose 16 px gap.  Tightening the margin
   to 4 px gives 6 + 4 = 10 px above and below — same total rhythm as
   the detail-head block-flow path. */
.bld-title-block .dh-badges { margin: 4px 0; }
/* Badges rendered inline after the deck title (deck-detail <h1> + builder
   .bld-title) instead of on their own row — no block margin, vertically
   centred with the title text. */
.dh-badges--inline { margin: 0; align-items: center; }
/* Optional description — row 2 of .dh-inner, full width below the title row
   (capped at 620px for readability). Rendered only when the deck has a
   description; otherwise the header is just the title row. */
.detail-head .dh-desc {
  font-size: 13px;
  color: var(--text-muted);
  max-width: 620px;
  line-height: 1.55;
  margin: 0;
}
/* Edit / Delete actions, pinned to the right of the title row. flex:none +
   margin-left:auto absorbs the row's leftover free space so the buttons stay
   top-right while the <h1> (flex:1) takes the rest. */
.detail-head .dh-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
  flex: none;
  margin-left: auto;
  /* Own column now (sibling of .dh-inner). Top padding mirrors .dh-inner's so the
     buttons top-align with the title; right gutter keeps them off the edge; no
     left padding (the flex:1 .dh-inner already supplies the gap). */
  padding: 11px var(--gutter-content) 8px 0;
}

/* ── Body — 2-column: sticky stats sidebar + card grid ── */
/* Review banners across the deck-detail page when the deck is invalid. Split
   into a legality banner and an ownership banner (each shown only when it
   applies); they stack. Red-tinted strip; the user clicks "Edit deck" (right
   above) to open the builder and fix it. Same red palette as the My Decks
   page banner (.decks-issues-banner) and the "issues" filter button. */
.detail-review-banner {
  margin: 10px 22px 0;
  padding: 9px 14px;
  border-radius: var(--radius-s);
  background: rgba(162, 0, 0, 0.16);
  border: 1px solid rgba(255, 0, 19, 0.5);
  color: #ffd6d6;
  font-size: 13px;
  line-height: 1.4;
}
.detail-review-banner + .detail-review-banner { margin-top: 8px; }

/* ── .deck-action-banner — dismissable info/action banner in the deck builder ──
   Shared structure (icon · text · action button · dismiss ×), two color variants:
     --info  (amber)  = GUIDANCE (play-format retarget after a rotation)
     --issue (red)    = a TRUE issue (collection-ownership shortfall); reuses the
                        .detail-review-banner red so it reads as a problem.
   The action button opens the tabbed Edit deck info dialog on the relevant tab;
   the × dismisses for the SESSION only (re-shown next time the deck is opened).
   See PROPOSAL-deck-format-rotation.md §4a + THEME_TOKENS.md. */
.deck-action-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 10px 22px 0;
  padding: 9px 14px;
  border-radius: var(--radius-s);
  font-size: 13px;
  line-height: 1.45;
}
.deck-action-banner + .deck-action-banner { margin-top: 8px; }
.deck-action-banner .dab-icon    { flex: none; font-weight: 700; }
.deck-action-banner .dab-text    { flex: 1 1 240px; min-width: 200px; }
.deck-action-banner .dab-actions { flex: none; display: flex; align-items: center; gap: 8px; }
.deck-action-banner .dab-dismiss {
  flex: none; align-self: flex-start;
  background: none; border: none; color: inherit;
  opacity: .55; font-size: 18px; line-height: 1; cursor: pointer; padding: 0 2px;
}
.deck-action-banner .dab-dismiss:hover { opacity: 1; }
.deck-action-banner--info {
  background: rgba(240, 200, 100, 0.09);
  border: 1px solid rgba(240, 200, 100, 0.34);
  color: #e7d6a8;
}
.deck-action-banner--info  .dab-icon   { color: var(--gold, #f0c864); }
.deck-action-banner--info  .dab-text b { color: #f4e4b6; }
.deck-action-banner--issue {
  background: rgba(162, 0, 0, 0.16);
  border: 1px solid rgba(255, 0, 19, 0.5);
  color: #ffd6d6;
}
.deck-action-banner--issue .dab-icon   { color: #ff8a8a; }
.deck-action-banner--issue .dab-text b { color: #ffecec; }

/* Allow-and-flag hint on a play-format radio option the deck doesn't fully satisfy
   (Edit deck info → Play format tab). Amber/orange, below the option description. */
.radio-card .rc-illegal {
  display: block;
  margin-top: 4px;
  font-size: 11.5px;
  font-weight: 600;
  color: #e0754f;
  line-height: 1.35;
}

/* My Decks page banner — shown when ≥1 deck needs review. */
.decks-issues-banner {
  margin: 0 0 14px;
  padding: 10px 14px;
  border-radius: var(--radius-s);
  background: rgba(162, 0, 0, 0.16);
  border: 1px solid rgba(255, 0, 19, 0.5);
  color: #ffd6d6;
  font-size: 14px;
  line-height: 1.4;
}

/* "Needs review" status on a My Decks tile's count line (replaces Legal ✓). */
.dt-prog-review { color: #FF5F00; font-weight: 700; }

/* "⚠️ With issues" / "⚠️ Cards with issues" filter button — used on the My
   Decks filter bar AND the deck-detail / builder filter bars. Off = translucent
   red outline; on = solid dark red. White text, colored ⚠️ emoji. */
.issbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Match the sibling filter controls (.set-ms-trigger / .f-chip / .chip) so the
     button is the same height + text size — only the red colour sets it apart. */
  padding: 4px var(--filter-pill-pad-x);
  border-radius: var(--radius-s);
  font-size: 12px;
  line-height: 1.2;
  font-family: inherit;
  color: #fff;
  cursor: pointer;
  white-space: nowrap;
  background: rgba(255, 0, 19, 0.25);
  border: 1px solid #FF0013;
}
.issbtn.on { background: #A20000; border-color: #A20000; font-weight: 700; }

/* ── Per-card review-issue treatment (Deck Detail + builder) ──
   Orange-red border wrapping the card AND its legend; the legend lines are bold
   orange-red. The border color (#FF5F00) matches the builder's at-limit accents. */
.card-issue-wrap {           /* picture view (tileCardReadonly) */
  border: 2px solid #FF5F00;
  border-radius: 12px;
  padding: 5px;
  display: flex;
  flex-direction: column;
}
.pill-issue-wrap {           /* compact text view (tileCardPill) — pill internals untouched */
  border: 2px solid #FF5F00;
  border-radius: 11px;
  padding: 6px;
}
.pill-issue-wrap .pill { margin: 0; }
/* Builder stepper tile — override the in-deck highlight with the issue red.
   .bld-card--in-deck draws its white highlight with BOTH a border-color and a
   `box-shadow: 0 0 0 1px #fff …` ring, so overriding only the border left the
   white ring showing around the red. The `.builder` prefix beats
   `.builder .bld-card--in-deck` on specificity and `box-shadow: none` removes
   the ring → an issue card shows the red border alone. */
.builder .bld-card--issue { border: 2px solid #FF5F00; box-shadow: none; }

/* An issue tile is taller than its siblings (it carries the legend below the
   card). The card grids default to `align-items: stretch`, so that extra
   height was inflating the non-issue tile sharing its row. Pin the read-only
   Deck Detail + builder-expanded grids (picture .cards-grid AND compact
   .pill-grid) to `align-items: start` so each tile keeps its natural height.
   Scoped by host id so My Cards / Card Sets / the builder left grid (whose
   tiles vary by design) are untouched. */
#detail-cards-grid .cards-grid,  #detail-cards-grid .pill-grid,
#bld-exp-cards-grid .cards-grid, #bld-exp-cards-grid .pill-grid { align-items: start; }

/* Builder right-pane "Cards in deck" list — problematic rows get the orange-red
   border only (no legend; the dense list has no room and the issue is detailed
   in the left grid + Deck Detail). Overrides the .pill base 1px dark border;
   border-color animates via the .pill transition. */
.dl-pill--issue { border: 2px solid #FF5F00; }

/* Cross-pane hover link (left card pictures ↔ right "Cards in deck" list) +
   direct hover on a right-pane row. A crisp white outline that reads as "this
   is the linked card" — layered over (not replacing) any red issue border or
   white in-deck ring. outline doesn't affect layout and follows border-radius. */
.bld-card.x-hl, .dl-pill.x-hl { outline: 2px solid #fff; outline-offset: 1px; }

/* Legend (one or two ⚠️ lines) below the picture / pill, inside the border. */
.card-issue-legend { margin-top: 7px; padding: 0 2px 2px; }
.card-issue-legend .cil-line {
  color: #FF5F00;
  font-weight: 700;
  font-size: 12px;
  line-height: 1.35;
  margin-top: 3px;
}
.card-issue-legend .cil-line:first-child { margin-top: 0; }

.detail-body {
  /* width 100% (no max-width / margin: 0 auto) so the detail page expands to
     fill #ill-tab-content. Side padding mirrors the rest of the My Collection
     tab area; the grid is 300px sidebar + 1fr for the card grid.
     padding-top: 11px — the visible head-to-body gap, matched to the inter-
     panel gap in the stats sidebar (.stats-card { gap: 11px }) so the spacing
     below the header reads the same as the spacing between every other panel
     (was 22px, which looked like an oversized outlier — see screenshot). */
  padding: 11px var(--gutter-content) 90px;
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 24px;
  align-items: start;
}
.detail-body .stats-card {
  display: flex;
  flex-direction: column;
  gap: 11px;
  /* No `position: sticky` — the two columns scroll as a single unit with
     the rest of the page. Previously the stats card stuck to top:18px
     while the cards grid scrolled, which read as "the page is split in
     two halves diverging" and broke the impression of a single document. */
}
.detail-cards { min-width: 0; }
/* Adaptive filter bar + result line above the deck-detail card grid.  Margin
   below mirrors the spacing pattern of the other surfaces (My Cards / Card
   Sets / Missing). */
.detail-cards #detail-filter-bar { margin-bottom: 14px; }
.detail-cards #detail-result-line { margin-bottom: 6px; }

/* View-toggle dropdown.  Sits on the right of #detail-result-line — the
   surrounding .count-only-row gives .cards-count flex: 1 so this element
   floats right naturally.  The dropdown itself uses the canonical
   .set-multiselect chrome (see CLAUDE.md → Dropdowns rule), so no
   per-element <select> styling is needed here. */
.detail-view-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}
/* Deck-detail result-line controls — unify the hover of the VIEW dropdown
   (.set-ms-trigger) and Export deck (.btn-coll-export) with the Show-filter-bar
   button (.pdv-showfilter) beside them: hover shows ONLY a solid gold border,
   with NO text-colour or background change. Scoped to #detail-result-line so the
   shared .set-ms-trigger / .btn-coll-export keep their usual hover elsewhere.
   The extra :not(.active) keeps the VIEW trigger's OPEN state (gold pill, dark
   text) intact — the override only applies while the dropdown is closed. */
@media (any-hover: hover) {
  #detail-result-line .detail-view-toggle .set-ms-trigger:hover:not(.active) {
    border-color: var(--gold);
    color: var(--text-muted);
  }
  #detail-result-line .btn-coll-export:hover {
    border-color: var(--gold);
    color: var(--text-main);
    background: var(--bg-elevated);
  }
}
.detail-view-label {
  /* Bumped from --text-muted (#c0c0d8) to --text-main (#d4cfc4): at this
     11 px uppercase letter-spaced size --text-muted reads as washed-out
     against the deep page bg.  --text-main matches the perceived weight
     of the nav-bar text in the screenshot.
     font-weight 700 — at 11 px uppercase the default 400 weight rendered
     too thin; the trigger button right of it is heavier, so the label
     needs a matching weight to read as a peer rather than as caption. */
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-main);
}

/* Single-select variant of .set-ms-option (radio input + label).  Same
   outer chrome as the multi-select version; the only behavioural
   difference is the input type.  Used by the deck-detail View toggle
   and any future single-pick dropdown that wants the canonical look. */
.set-ms-option--single { gap: 8px; }
.set-ms-option--single input[type="radio"] {
  accent-color: var(--gold);
  flex-shrink: 0;
}

/* The detail card grid reuses the global .stat-block renderers, but the
   .bar / .results-line classes are otherwise scoped to .builder —
   re-declare them here for the detail view.  Section headers used to be a
   detail-only smaller variant (.ink-section-h) too; Phase 5 of the
   unified card-grid refactor moved the section headers to the canonical
   .ink-section-header (shared with Card Sets / My Cards / Missing) so
   that styling now comes from the global rule, not from here. */
.deck-detail .bar {
  height: 7px;
  background: var(--bg-deep);
  border-radius: 4px;
  overflow: hidden;
}
.deck-detail .bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--gold), var(--gold-light));
  border-radius: 4px;
}
.deck-detail .bar-fill.full {
  background: linear-gradient(90deg, var(--emerald), #34d399);
}

.detail-cards .results-line {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 14px;
}
.detail-cards .results-line b { color: var(--gold-light); }

/* Read-only quantity badge replaces the +/- stepper. The card grid itself is
   the shared .card-thumb component, so only the badge needs styling here —
   cardThumbHTML already renders ×N into .card-qty-badge. */

/* Empty-deck state. */
.detail-empty {
  padding: 40px 20px;
  text-align: center;
  font-size: 13px;
  color: var(--text-dim);
  background: var(--bg-deep);
  border: 1px dashed var(--border);
  border-radius: var(--radius-m);
}

/* ── Responsive — single column below 1100px ── */
@media (max-width: 1100px) {
  .detail-body { grid-template-columns: 1fr; }
  .detail-body .stats-card { position: static; }
}
@media (max-width: 640px) {
  .detail-head .dh-inner { padding: 16px 16px 18px; }
  /* Horizontal padding uses the shared content gutter so the card grid / FILTERS
     bar line up with the header + info panel (was a one-off 16px). */
  .detail-body { padding: 11px var(--gutter-content) 80px; }
  .detail-head h1 { font-size: 21px; }
}

/* ═══════════════════════════════════════════════════════════════════════
   DECK DETAIL — MOBILE (≤945px)
   Collapsible stats, stacked header, reflowed result line, compact-list tuning.
   The desktop layout is untouched (.detail-stats-rest is display:contents there, so
   the stat panels flow in the sidebar exactly as before; the toggle is hidden).
   ═══════════════════════════════════════════════════════════════════════ */
.detail-stats-toggle { display: none; }
.detail-stats-rest   { display: contents; }
@media (max-width: 945px) {
  /* Header: inks + name inline; badges → own line; then the description; then the
     actions (centred flow, natural-width buttons). The header KEEPS its panel band
     on mobile (it used to be de-chromed to the bare page background, which left the
     title/badges/description floating on the artwork) — 2026-07-12. */
  /* Stack the two columns: title/description (.dh-inner) then the actions. */
  .deck-detail .detail-head { flex-direction: column; align-items: stretch; }
  .deck-detail .detail-head .dh-inner { padding: 12px var(--gutter-content) 10px; gap: 8px; }
  .deck-detail .detail-head h1 { display: block; font-size: 18px; line-height: 1.3; }
  .deck-detail .detail-head .dh-inks { display: inline-flex; vertical-align: -3px; margin-right: 7px; }
  .deck-detail .detail-head .dh-ink-icon { width: 18px; height: 18px; }
  .deck-detail .detail-head .dh-badges--inline { display: flex; margin: 8px 0 0; }
  /* .dh-actions is a sibling of .dh-inner now — it flows below it; supply its own
     gutter padding (it no longer inherits .dh-inner's) and centre the buttons. */
  .deck-detail .detail-head .dh-actions { margin: 0; padding: 0 var(--gutter-content) 12px; justify-content: center; }
  .deck-detail .detail-review-banner { margin: 10px var(--gutter-content) 0; }

  /* Consistent ~12px gap between the stacked panels: the stats sidebar and the
     card grid (DECK STATS → FILTERS) previously sat 24px apart. */
  .detail-body { gap: 12px; }

  /* Collapsible stats: Cards-in-deck stays visible; the rest folds behind the toggle.
     No margin-top — the .stats-card 11px gap already separates it from Cards-in-deck. */
  .detail-stats-toggle {
    display: flex; align-items: center; justify-content: center; gap: 9px; width: 100%;
    padding: 11px; margin-top: 0; border-radius: var(--radius-s);
    /* --bg-deep (NOT --bg-surface): matches the Cards-in-deck panel it sits under
       (.stat-block is --bg-deep) — the two used to be different shades of blue. */
    background: var(--bg-deep); border: 1px solid var(--border); color: var(--text-bright);
    cursor: pointer; font: 700 12px var(--font-ui); letter-spacing: 1.5px; text-transform: uppercase;
  }
  .detail-stats-toggle .detail-stats-chev { width: 14px; height: 14px; flex: none; transition: transform .2s; }
  .stats-card.stats-open .detail-stats-toggle .detail-stats-chev { transform: rotate(180deg); }
  .detail-stats-rest { display: flex; flex-direction: column; gap: 11px; }
  .detail-stats-rest .bld-stat-row { display: contents; }   /* panels flow individually */
  .stats-card:not(.stats-open) .detail-stats-rest { display: none; }

  /* Result line: count on its own line; View toggle + Export on the line below. */
  #detail-result-line { flex-wrap: wrap; row-gap: 10px; }
  #detail-result-line .cards-count { flex: 1 1 100%; text-align: center; }
  #detail-result-line .btn-coll-export { margin-left: auto; }

  /* Compact list: the real .pill-grid is minmax(420px) → one pill per row on a phone. */
  .pill-grid { grid-template-columns: minmax(0, 1fr); }
}
/* Unfolded / wider mobile (601–945px): stats + compact pills go two-up. */
@media (min-width: 601px) and (max-width: 945px) {
  .detail-stats-rest { flex-direction: row; flex-wrap: wrap; align-items: stretch; }
  .detail-stats-rest .stat-block { flex: 1 1 calc(50% - 6px); max-width: calc(50% - 6px); min-width: 0; box-sizing: border-box; }
  .pill-grid { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
}
/* Folded phone (≤600px): drop the ink-colour icon from compact pills — the pill's
   ink-tinted background already conveys the colour. */
@media (max-width: 600px) {
  .pill-grid .pill-ink-hex,
  .pill-grid .pill-ink-glyph { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   FLOATING "Scroll To Top" BUTTON
   ═══════════════════════════════════════════════════════════════════════
   Single shared button (created on-demand by _installScrollToTopButton)
   that fades in when the filter bar of the active page scrolls out of
   view. Position: fixed at the bottom-centre of the viewport, just above
   the bottom nav bar. Hidden by default; the .visible class is toggled
   by an IntersectionObserver on the sentinel element. */
.scroll-to-top-btn {
  position: fixed;
  left: 50%;
  bottom: calc(var(--bottom-bar-h) + 12px);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--gold);
  border-radius: 999px;
  color: var(--gold-light);
  font-size: 12px;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  box-shadow: 0 6px 18px rgba(0,0,0,0.5), 0 0 12px var(--gold-glow);
  opacity: 0;
  pointer-events: none;
  /* translateX(-50%) centres the button against `left: 50%`. */
  transform: translateX(-50%) translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.scroll-to-top-btn .arrow {
  font-size: 13px;
  line-height: 1;
}
.scroll-to-top-btn.visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}
/* Hover-fill only on devices with a hover-capable pointer. On touch (Pixel etc.) the
   browser applies :hover on tap and it STICKS until you tap elsewhere — so the button
   stayed gold/dark after one press. `any-hover: hover` is true whenever ANY connected
   pointer can hover (desktop mouse; a hybrid laptop's mouse, even when it reports
   hover:none as its PRIMARY pointer) and false only on pure-touch devices — so
   desktop/hybrid hover is unchanged and pure-touch loses the sticky state. This is the
   app-wide pattern for every :hover rule (see the @media (any-hover: hover) blocks
   throughout this file). */
@media (any-hover: hover) {
  .scroll-to-top-btn:hover {
    background: var(--gold);
    color: #1a1405;
  }
}

/* When the host surface is a sub-pane rather than the full viewport, the
   button reanchors over the centre of that pane. The deck builder uses this for
   the left search pane. Its width is whatever the capped deck pane leaves —
   max(68vw, 100vw − 570px) — so its centre is half of that: 50vw − min(16vw,
   285px). Computed from the SAME 570px cap as the panes (transform stays
   translateX(-50%)), so it tracks the window size with no hardcoded assumption.
   Keep the 570px / 285px pair in sync with .bld-deck's flex-basis cap. */
.scroll-to-top-btn.align-left-pane,
.scroll-to-top-btn.align-left-pane.visible { left: calc(50vw - min(16vw, 285px)); }

/* ═══════════════════════════════════════════════════════════════════════════
   PUBLIC DECKS (P1) — net-new styling over the private deck surfaces.
   Copied from the committed mockups (MOCKUP-my-public-decks.html /
   MOCKUP-public-deck-builder.html / MOCKUP-public-deck-details-owner.html /
   MOCKUP-public-deck-dialogs.html), which were authored against these real
   production CSS vars. Only these .pdt-/.pdb-/.pdo-/.pdd- classes are net-new;
   every other class on the public surfaces is reused from the private decks.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── My Public Decks tile additions ── */
.pdt-status { font: 700 10px/1 'Bitter', serif; letter-spacing: .05em; text-transform: uppercase; padding: 4px 8px; border-radius: 5px; }
.pdt-status--pub   { background: rgba(36,207,49,.16);  color: #24CF31; border: 1px solid rgba(36,207,49,.4); }
.pdt-status--unpub { background: rgba(140,140,160,.14); color: #9a9ab0; border: 1px solid rgba(140,140,160,.3); }
.pdt-social { display: flex; gap: 16px; margin: 8px 0 2px; font: 12px 'Bitter', serif; color: var(--text-secondary, #c9c9dc); }
.pdt-social span { display: inline-flex; align-items: center; gap: 5px; }
.pdt-social b { color: #fff; }

/* ── Builder publish/unpublish toggle (title panel, bottom-right) ── */
.pdb-pub-wrap { margin-left: auto; display: flex; align-items: center; gap: 12px; align-self: flex-end; }
/* "Done" button, bottom-right of the deck-builder title panel. margin-left:auto pushes it
   right (private builder); on the public builder it sits just after .pdb-pub-wrap (the .bld-top
   flex gap separates them). align-self:flex-end anchors it to the bottom so a multi-line title
   never adds panel height. */
.bld-done-wrap { margin-left: auto; align-self: flex-end; }

/* Full-height variant of the empty state — used for the deck-builder "published, unpublish
   to edit" locked notice so the icon + text center vertically AND horizontally in the left
   pane (not just top-anchored). Mirrors the Card Sets "click to explore" empty state. */
.empty-state.empty-state--fill { min-height: 55vh; justify-content: center; }
.pdb-pub-reason { font: 700 13px 'Bitter', serif; color: #FF5F00; }
.pdb-live { font: 700 13px 'Bitter', serif; color: #24CF31; display: inline-flex; align-items: center; gap: 5px; }
.pdb-copylink { padding: 8px 12px; border: 1px solid var(--gold, #d4a93b); border-radius: 8px; background: rgba(212,169,59,.16); color: var(--gold-light, #f0c864); font: 700 13px 'Bitter', serif; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
@media (any-hover: hover) {
.pdb-copylink:hover { background: var(--gold, #d4a93b); color: #1a1405; }
}
.pdb-url { display: inline-flex; align-items: center; border: 1px solid var(--bg-panel-alt, #20203e); border-radius: 8px; overflow: hidden; background: var(--bg-void, #04040f); margin-top: 8px; }
.pdb-url .u { padding: 8px 12px; font: 12.5px ui-monospace, monospace; color: #9fc0e8; }
.pdb-url .cp { padding: 8px 12px; border-left: 1px solid var(--bg-panel-alt, #20203e); background: var(--bg-elevated, #141432); color: var(--text-muted, #7a7a92); cursor: pointer; font-size: 12px; }

/* ── Owner details info strip (below the title header, capped + centred) ── */
/* Owner status strip. width:fit-content (NOT 100%) + centred: the old full-width bar
   left a huge dead gap between the status/counts and the action buttons on a wide
   screen. fit-content is what keeps it compact — it can never be wider than its own
   content — so max-width is only a SAFETY CEILING and must NOT do the compacting.
   (A --max-content-width/2 = 768px cap was tried and was 40px too small for the
   German unpublished row, forcing the note to wrap; the ceiling is now the page's own
   --max-content-width token — no magic number, consistent with the filter bar.)
   Sizing to content is also what lets a longer translation simply GROW the strip and
   its buttons instead of clipping. MEASURED widths (published/unpublished):
   EN 626/645 · FR 653/715 · DE 762/808 · IT 777/768 — worst case DE unpublished 808px.
   Colour = the card-grid filter bar's (--bg-surface + --border), was a gradient. */
.pdo-info { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 14px auto 6px; width: fit-content; max-width: var(--max-content-width, 1536px); box-sizing: border-box; padding: 10px 16px; border-radius: var(--radius-m); background: var(--bg-surface); border: 1px solid var(--border); }
.pdo-status { font: 700 11px/1 'Bitter', serif; letter-spacing: .05em; text-transform: uppercase; padding: 5px 10px; border-radius: 6px; display: inline-flex; align-items: center; gap: 6px; flex: none; white-space: nowrap; }
.pdo-status--pub   { background: rgba(36,207,49,.16);  color: #24CF31; border: 1px solid rgba(36,207,49,.4); }
.pdo-status--unpub { background: rgba(140,140,160,.14); color: #9a9ab0; border: 1px solid rgba(140,140,160,.3); }
/* Counts never shrink or wrap, and the digits are tabular — a growing view/like
   count widens the strip rather than clipping the number. */
.pdo-stat { display: inline-flex; align-items: center; gap: 6px; font: 13.5px 'Bitter', serif; color: var(--text-secondary, #c9c9dc); flex: none; white-space: nowrap; }
.pdo-stat b { color: #fff; font-variant-numeric: tabular-nums; }
.pdo-stat .app-glyph { width: 17px; height: 17px; }
.pdo-link { margin-left: auto; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pdo-url { display: inline-flex; align-items: center; border: 1px solid var(--bg-panel-alt); border-radius: 8px; overflow: hidden; background: var(--bg-void, #04040f); }
.pdo-url .u { padding: 7px 11px; font: 12.5px ui-monospace, monospace; color: #9fc0e8; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdo-copy { padding: 8px 12px; border: 1px solid var(--gold, #d4a93b); border-radius: 8px; background: rgba(212,169,59,.16); color: var(--gold-light, #f0c864); font: 700 13px 'Bitter', serif; cursor: pointer; display: inline-flex; gap: 6px; align-items: center; }
.pdo-view-btn { padding: 8px 12px; border: 1px solid var(--bg-panel-alt); border-radius: 8px; background: var(--bg-elevated); color: var(--text-secondary, #c9c9dc); font: 600 13px 'Bitter', serif; cursor: pointer; display: inline-flex; gap: 6px; align-items: center; }
@media (any-hover: hover) {
.pdo-view-btn:hover { border-color: var(--gold, #d4a93b); color: var(--gold-light, #f0c864); }
}
.pdo-unpub-note { margin-left: auto; color: var(--text-muted, #7a7a92); font: 13px 'Bitter', serif; }
.pdo-unpub-note b { color: var(--text-secondary, #c9c9dc); }
/* Owner panel grouping (.pdo-grp wrappers). Desktop: the wrappers are display:contents
   so their children lay out as a single flat flex row exactly as before; `order` keeps
   status + counts on the left and the action buttons on the right (the old
   .pdo-link margin-left:auto). Mobile: the wrappers become 3 stacked rows —
   [status · copy] / [views · likes] / [save · view-public] — so nothing overflows. */
.pdo-grp { display: contents; }
@media (min-width: 946px) {
  .pdo-info .pdo-status  { order: 1; }
  .pdo-info .pdo-stat    { order: 2; }
  .pdo-info .pdo-copy    { order: 3; margin-left: auto; }
  .pdo-info .pdo-view-btn{ order: 3; }
  /* Without this the note defaults to order:0 and would jump AHEAD of the status
     pill (the unpublished strip is [status][views][likes][note]). */
  .pdo-info .pdo-unpub-note { order: 3; }
}
@media (max-width: 945px) {
  .pdo-info { flex-direction: column; align-items: stretch; gap: 10px; }
  /* Owner deck-detail panel: inset from the screen edges by the SAME content gutter
     as the header + FILTERS bar (it used to touch the edges on a folded phone), with
     a consistent 12px gap above and 0 below (the next block owns the lower gap). On an
     unfolded phone the .pub-top flex row overrides this margin to 0 (it sits in the
     50/50 row instead).
     Colour scheme mirrors the mobile FILTERS bar (.fbm-bar): the PANEL takes the
     brighter --bg-elevated fill + --border, and its buttons take the darker
     --bg-surface (see below) — so the figure/ground matches the filter panel. Scoped
     to --public + mobile: the anon public-view page and the desktop layout keep the
     original gradient panel. */
  .deck-detail--public .pdo-info {
    width: auto; margin: 12px var(--gutter-content) 0;
    background: var(--bg-elevated); border-color: var(--border);
  }
  /* Buttons darker than the panel, mirroring the FILTERS group buttons
     (.fbm-group-btn → --bg-surface). Copy link keeps its gold border + gold text as
     the primary accent, just on the same dark fill (like the filter buttons' gold
     active state). */
  .deck-detail--public .pdo-view-btn { background: var(--bg-surface); border-color: var(--border); }
  .deck-detail--public .pdo-copy     { background: var(--bg-surface); }
  /* CENTER each stacked row so the lone status pill + the views/likes read as a
     centred block (matches the approved mockup); the copy/view button row fills. */
  .pdo-grp  { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: center; }
  .pdo-grp .pdo-copy, .pdo-grp .pdo-view-btn { flex: 1; justify-content: center; }
  .pdo-link { margin-left: 0; flex-wrap: wrap; }   /* anon panel (.pdv-owner) keeps .pdo-link */
  .pdo-link .pdo-copy, .pdo-link .pdo-view-btn { flex: 1; justify-content: center; }
  .pdo-unpub-note { margin-left: 0; }
}

/* ── Public deck-detail top row (.pub-top) — public page only ──────────────────
   Wraps the info panel + a DUPLICATE Cards-in-deck progress block (the canonical
   copy stays inside .stats-card). Default — desktop (>945) AND folded phone
   (≤600) — .pub-top is display:contents, so the info panel flows full-width
   exactly as before and the duplicate progress copy is hidden. Only on an
   UNFOLDED phone (601–945px) does it become a 50/50 flex row [Cards-in-deck |
   info panel] to reclaim vertical space, at which point the in-stats-card copy is
   hidden instead. Duplicate-render + CSS show/hide per breakpoint — the sanctioned
   .deck-actions-top pattern. */
.pub-top { display: contents; }
.pub-top > .deck-progress-block { display: none; }   /* duplicate hidden by default */
@media (min-width: 601px) and (max-width: 945px) {
  .pub-top { display: flex; align-items: stretch; gap: 12px; margin: 12px var(--gutter-content) 0; }
  .pub-top > .deck-progress-block,
  .pub-top > .pdo-info { flex: 1 1 0; min-width: 0; margin: 0; }
  .pub-top > .deck-progress-block { display: flex; flex-direction: column; justify-content: center; }
  /* Cards-in-deck now lives in .pub-top up here — hide the canonical sidebar copy
     so it isn't shown twice. Scoped to the PUBLIC page (--public): the private page
     has no .pub-top duplicate, so it must KEEP its in-stats-card Cards-in-deck. */
  .deck-detail--public .detail-body .stats-card .deck-progress-block { display: none; }
}

/* ── P2 anonymous public-view creator panel (.pdv-panel) ─────────────────────
   Its OWN shell — deliberately not .pdo-info, which the owner status strip owns
   along with its tuned mobile rules. Borrows the card-grid filter bar's surface +
   border (#detail-filter-bar), is capped to --max-content-width + centred, and
   keeps the content gutter so its edges line up with the Cards-in-deck panel.
   Desktop row: [id] [views · Likes · Favorite] [Copy link · Save image →right].
   It reuses .pdo-stat / .pdo-copy and adds the creator identity block + the
   Like / Favorite social buttons. */
.pdv-panel {
  /* width:fit-content (NOT 100%) so the panel HUGS its content and never stretches
     across a widescreen — the same treatment as the owner status strip. It also
     neutralises .pdv-actions' margin-left:auto (no free space ⇒ no dead gap), and
     lets a longer translation grow the panel instead of clipping. max-width is only
     a safety ceiling, anchored to the page's own token. On ≤945 it goes back to
     filling the content gutter (see the mobile block).
     MEASURED desktop widths: EN 878 · FR 941 · DE 934 · IT 997. Worst case is
     ITALIAN (not German) — "{n} visualizzazioni" + "Salva immagine" — at 997px,
     comfortably under the 1536 ceiling. */
  width: fit-content;
  max-width: var(--max-content-width);
  margin: 14px auto 6px;
  box-sizing: border-box;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  padding: 14px 16px;
  display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
}
/* Transparent on desktop so views/Likes/Favorite flow as items of the single row;
   becomes a real centred line at ≤945 (below). */
.pdv-statsrow { display: contents; }
.pdv-panel .pdo-stat { font-size: 14px; white-space: nowrap; }
.pdv-panel .pdo-stat .app-glyph { width: 17px; height: 17px; }
/* Save image is .btn-ghost, whose fill is --bg-surface — a shade darker than the
   social buttons' --bg-elevated. Match all three on the brighter tone. */
.pdv-panel .pdv-actions .btn-ghost { background: var(--bg-elevated); }

.pdv-id { display: flex; align-items: center; gap: 12px; min-width: 0; }
.pdv-id .ava { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; flex: none; border: 2px solid rgba(212,169,59,.45); }
.pdv-meta { min-width: 0; }
.pdv-createdby { font: 600 11px/1.2 'Bitter', serif; letter-spacing: .04em; text-transform: uppercase; color: var(--text-dim); }
.pdv-handle { font-family: 'HF Farmiles', sans-serif; font-size: 19px; line-height: 1.15; letter-spacing: var(--ls-farmiles); color: var(--gold-light, #f0c864); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pdv-sub { font: 13px 'Bitter', serif; color: var(--text-muted, #7a7a92); margin-top: 1px; }
.pdv-sub b { color: var(--text-secondary, #c9c9dc); }
/* Social buttons (Like / Favorite). For an anonymous visitor these route to
   Sign In (P3 wires the real toggle); styled as toggle-capable for that phase. */
.pdv-social-btn { display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--bg-panel-alt); background: var(--bg-elevated); color: var(--text-secondary, #c9c9dc); border-radius: 8px; padding: 7px 12px; font: 600 13px 'Bitter', serif; cursor: pointer; }
@media (any-hover: hover) {
.pdv-social-btn:hover { border-color: var(--gold, #d4a93b); color: var(--gold-light, #f0c864); }
}
.pdv-social-btn b { color: #fff; }
/* Pressed (liked / favorited) state — gold fill so the toggle is obvious (P3). */
.pdv-social-btn.is-active { border-color: var(--gold, #d4a93b); background: rgba(212,169,59,.16); color: var(--gold-light, #f0c864); }
.pdv-social-btn.is-active b { color: var(--gold-light, #f0c864); }
.pdv-social-btn:disabled { opacity: .6; cursor: progress; }
/* Favorite heart turns ruby when active (filled ♥). */
.pdv-social-btn.is-active .pdv-fav-icon { color: var(--ruby, #c0392b); }
/* My Favorites tile (P3) — creator handle + the simple card-count line (the
   favorites tile omits the owner-only progress bar). */
.dt-creator { font: 600 12px 'Bitter', serif; color: var(--text-secondary, #c9c9dc); margin: 2px 0 4px; }
/* Favorite-tile creator header: icon + handle, centred horizontally above the deck
   title. The handle uses the Farmiles display face in gold — size + tracking mirror
   the My-Collection nav dropdown items (.nav-coll-item, 17px/--ls-farmiles), scaled
   down slightly for the tile. The icon is a touch larger than the name; both are
   vertically centred (align-items:center). */
.dt-creator-head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  margin: 0 0 7px;
  min-width: 0;
}
.dt-creator-ava {
  width: 22px; height: 22px; flex: none;
  border-radius: 50%; object-fit: cover;
  border: 1px solid rgba(212, 169, 59, .45);
}
.dt-creator-name {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: normal;
  letter-spacing: var(--ls-farmiles);
  color: var(--gold-light, #f0c864);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.dt-types-line { font: 600 12.5px 'Bitter', serif; color: var(--text-secondary, #c9c9dc); margin: 4px 0; }
/* The link / actions cluster pushes to the right edge like .pdo-link. */
.pdv-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* (.pdv-print-btn removed 2026-07-12 — the panel's Save image now uses the shared
   .btn.btn-ghost.btn-sm, matching the owner page's header action.) */
/* Show-filter-bar affordance in the result line (left of VIEW). The grid's own
   filter bar starts hidden; this reveals it. */
.pdv-showfilter { display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--bg-panel-alt); background: var(--bg-elevated); color: var(--text-secondary, #c9c9dc); border-radius: 8px; padding: 6px 12px; font: 600 12.5px 'Bitter', serif; cursor: pointer; margin-right: 10px; transition: border-color 0.15s; }
@media (any-hover: hover) {
/* Hover = ONLY a solid gold border (no text/background change) — matches the
   VIEW dropdown + Export deck controls beside it in the result line. */
.pdv-showfilter:hover { border-color: var(--gold, #d4a93b); }
}
/* ── Creator panel, MOBILE (≤945: folded AND unfolded) ───────────────────────
   Narrow at both sizes (full-width on the fold; HALF-width beside Cards-in-deck
   when unfolded via .pub-top), so it stacks the same way:
     line 1  id  [+ QR top-right, fold only]
     line 2  views · Likes · Favorite   (CENTRED)
     line 3  Copy link · Save image
   Driven by flex `order` (not absolute positioning) so the fold's QR reserves its
   own space and a long creator handle ellipsises instead of sliding under it. */
@media (max-width: 945px) {
  /* Back to filling the content gutter: fit-content is a DESKTOP device (stop the
     stretch on a widescreen). On a phone the panel should span the column like the
     Cards-in-deck panel beneath it, not shrink-wrap to its text. */
  .pdv-panel { width: auto; max-width: none; margin: 14px var(--gutter-content) 6px; }
  .pdv-panel { flex-direction: row; flex-wrap: wrap; align-items: flex-start; gap: 12px; }
  .pdv-id { flex: 1 1 auto; min-width: 0; order: 0; }
  .pdv-statsrow { display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap; flex: 1 1 100%; order: 2; }
  .pdv-social-btn { justify-content: center; }
  .pdv-actions { margin-left: 0; flex: 1 1 100%; order: 3; justify-content: center; }
  .pdv-actions > * { flex: 1 1 auto; justify-content: center; }
}
/* Unfolded phone: the creator panel rides the .pub-top 50/50 row beside
   Cards-in-deck (production styles .pub-top > .pdo-info for the owner page; the
   anon panel is .pdv-panel). The sidebar's Cards-in-deck copy hides — production's
   equivalent rule is scoped to --public, so --view needs its own. */
@media (min-width: 601px) and (max-width: 945px) {
  .pub-top > .pdv-panel { flex: 1 1 0; min-width: 0; margin: 0; width: auto; max-width: none; }
  .deck-detail--view .detail-body .stats-card .deck-progress-block { display: none; }
}

/* ── Anon public deck view (.deck-detail--view) — CARDS IN DECK panel tweaks ─────
   Implements MOCKUP-public-deck-cards-balance.html (approved 2026-07-18):
   (1) show only the count number, not "N / N cards", at ALL widths — this page ONLY
       (the builder + private deck detail keep the full "N / N cards");
   (2) on an UNFOLDED phone the creator id is duplicated into the top of the CARDS IN
       DECK panel to balance the .pub-top 50/50 row — the creator panel then drops its
       own id and vertically centres its rows. */
.deck-detail--view .deck-progress-block .pg-num .max { display: none; }
/* The creator-id copy duplicated into the CARDS IN DECK panel — hidden except on the
   unfolded phone (below). */
.deck-detail--view .deck-progress-block .pdv-id--incards { display: none; }
@media (min-width: 601px) and (max-width: 945px) {
  .deck-detail--view .pub-top > .deck-progress-block { justify-content: flex-start; }  /* creator id at top */
  .deck-detail--view .deck-progress-block .pdv-id--incards { display: flex; }
  /* Breathing room between the creator id and the CARDS IN DECK heading — absorbed by
     the panel's spare height, so it does NOT grow the panel. */
  .deck-detail--view .deck-progress-block .dpb-cards { margin-top: 22px; }
  .deck-detail--view .pub-top > .pdv-panel > .pdv-id { display: none; }  /* hide the panel's own id */
  /* Creator panel is the shorter one now (id moved left) — vertically centre its
     wrapped rows (views/Likes/Favorite + Copy link/Save image) rather than top-align. */
  .deck-detail--view .pub-top > .pdv-panel { align-content: center; }
}

/* Anonymous public-view error / not-found state — centred panel mirroring the
   login chrome spacing (the page has no other content). */
.pdv-state { max-width: 560px; margin: 64px auto; padding: 32px 28px; text-align: center; background: var(--bg-surface); border: 1px solid var(--bg-panel-alt); border-radius: 14px; }
.pdv-state h1 { font-family: 'HF Farmiles', sans-serif; letter-spacing: var(--ls-farmiles); color: var(--text-bright); margin: 0 0 12px; }
.pdv-state p { color: var(--text-secondary, #c9c9dc); font: 14px/1.6 'Bitter', serif; margin: 0 0 20px; }
.pdv-state .pdv-state-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }

/* ── Wizard unrestricted note (replaces the deck-type radio grid, step 3) ── */
.pdd-note { margin-top: 6px; border: 1px solid var(--border); background: var(--bg-deep); border-radius: var(--radius-s); padding: 11px 14px; color: var(--text-secondary, #c9c9dc); font: 13px/1.5 'Bitter', serif; }
.pdd-note b { color: var(--gold-light, #f0c864); }

/* ── Single-deck import dialog (P5) — file picker + parsed preview ── */
.pdd-import-area input[type="file"] { width: 100%; color: var(--text-secondary, #c9c9dc); font: 13px 'Bitter', serif; }
.pdd-import-area input[type="file"]::file-selector-button { background: var(--bg-deep); border: 1px solid var(--border); color: var(--text-bright, #fff); padding: 6px 12px; border-radius: var(--radius-s); cursor: pointer; font: 13px 'Bitter', serif; margin-right: 10px; }
@media (any-hover: hover) {
.pdd-import-area input[type="file"]::file-selector-button:hover { border-color: var(--gold, #d4a93b); }
}
.pdd-import-preview { margin-top: 12px; border: 1px solid var(--border); border-radius: var(--radius-s); background: var(--bg-deep); padding: 11px 14px; font: 13px/1.55 'Bitter', serif; color: var(--text-secondary, #c9c9dc); }
.pdd-import-preview b { color: var(--text-bright, #fff); }
.pdd-import-preview .ok { color: var(--green, #24CF31); }
.pdd-import-preview .bad { color: var(--ruby, #e0506a); }

/* ════════════════════════════════════════════════════════════════════════════
   PRINT VIEW (P4) — shared public + private. Ported from MOCKUP-public-deck-
   print.html (.pp-* page chrome + .po-* dialog controls). ONE renderer in
   app.js (_openDeckPrintView) parameterized by hasQr / isOwnerKnown.
   ════════════════════════════════════════════════════════════════════════════ */

/* When printing, hide the app chrome on screen and let #print-root take over.
   The options dialog (.print-opts-overlay) stays visible on screen so the user
   can tweak + hit Print; it is suppressed in @media print below. */
body.printing .top-nav,
body.printing .bottom-bar,
body.printing #app-root,
body.printing .scroll-top-btn { display: none !important; }

/* Flat dark backdrop behind the Save-image options dialog — same colour as the
   deck-builder shell (--bg-void). Covers the fixed page background image so the
   view reads as a clean dark panel. There is no on-screen print preview anymore;
   the page is rendered only inside the capture iframe (app.js _savePrintAsImage). */
#print-root { position: fixed; inset: 0; z-index: 1; background: var(--bg-void); }

/* Legacy on-screen preview container — no longer emitted (kept only so the
   @media print block below still parses); the print page lives in the iframe. */
.print-stage { display: flex; justify-content: center; padding: 30px 10px 40px; min-height: 100vh; }
.print-page {
  width: 1150px; max-width: 100%; border-radius: 10px; padding: 26px 15px 30px;
  color: #e8e8f2; font-family: 'Bitter', serif; box-shadow: 0 10px 50px rgba(0,0,0,.5); position: relative;
}
.print-page.bg-dark { background: #010106; }
.print-page.bg-ink  { background: linear-gradient(150deg, #3a2c12 0%, #0a0a14 45%, #0e2a1c 100%); }

/* Header panel — logo (+ QR placeholder) left, titles/owner/summary right. */
.pp-head {
  display: flex; align-items: center; gap: 18px; background: var(--bg-deep);
  border: 1px solid var(--border); border-radius: var(--radius-m); padding: 14px 18px; margin-bottom: 14px;
}
.pp-brand { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; flex: none; }
.pp-logo  { height: 42px; }
/* QR placeholder slot — QR generation is a separate pending step (P3); no
   library is bundled. Left intentionally empty + outlined so it is visible. */
/* Matches the reduced .pdv-qr (public-view owner panel) so the saved-image QR
   looks identical to the on-screen one: same 87px box, 4px quiet-zone padding,
   10px radius. */
.print-qr-slot {
  /* Match the anon public-deck page QR (.pdv-qr): 68px box, 2px quiet-zone pad,
     5px radius — smaller, to save space in the head panel. */
  width: 68px; height: 68px; border-radius: 5px; flex: none;
  background: #fff; padding: 2px; box-sizing: border-box; overflow: hidden;
}
.print-qr-slot:empty { border: 2px dashed #ffffff44; background: #ffffff0a; }
/* QR on the anonymous public-deck view — a white card with a THIN frame: the code
   itself is generated with border:1 quiet-zone module (vs _renderQrSvg's default 2)
   and only 2px of padding here, so the whole thing fits the deck-title panel without
   growing it. Scan-verified on a real phone at this size (2026-07-12).
   Rendered TWICE (duplicate-render): --head at the far right of the title panel on
   desktop/unfolded, --panel in the creator panel's top-right corner on the fold.
   Exactly one is visible at any width. */
.pdv-qr {
  width: 68px; height: 68px; padding: 2px; box-sizing: border-box;
  background: #fff; border-radius: 5px;
  box-shadow: 0 2px 8px rgba(0,0,0,.35);
}
.pdv-qr:empty { display: none; }   /* QR lib missing / encode failed → no empty box */
.qr-svg { display: block; width: 100%; height: 100%; }
/* ABSOLUTE → contributes ZERO height, so the title panel is exactly as tall as it
   is without the QR and the description keeps hugging the title. */
.detail-head { position: relative; }
.pdv-qr--head  { position: absolute; right: var(--gutter-content); top: 50%; transform: translateY(-50%); }
.pdv-qr--panel { display: none; }
/* Reserve the QR's lane so a long deck name / description never runs under it.
   Scoped to --view: the private + owner heads have no QR (their .dh-actions own
   that side) and must not be indented. */
@media (min-width: 601px) {
  .deck-detail--view .detail-head .dh-inner { padding-right: calc(var(--gutter-content) + 90px); }
}
@media (max-width: 600px) {
  .pdv-qr--head  { display: none; }
  .pdv-qr--panel { display: block; order: 1; flex: none; margin-left: auto; }
}
.pp-titles { flex: 1; min-width: 0; }
/* Creator identity block on the RIGHT of the head, vertically centred (the .pp-head
   flex row is align-items:center). Reuses the anon page's .pdv-id shell (avatar + 3
   stacked lines). flex:none so it sizes to content; .pp-titles (flex:1) pushes it
   right. max-width guards against an unusually long handle stealing the title column. */
.pp-creator { flex: none; align-self: center; max-width: 300px; }
.pp-creator .pdv-id { min-width: 0; }
.pp-title  { font-family: 'Bitter', serif; font-weight: 700; font-size: 24px; line-height: 1.3; color: #fff; }
.pp-inks   { display: inline-flex; gap: 5px; vertical-align: middle; margin-right: 8px; }
.pp-inks img { width: 24px; height: 24px; }
.pp-title .dh-badges--inline { display: inline-flex; vertical-align: middle; margin-left: 8px; }
/* Avatar + ONE text span (see _buildPrintPageMarkup): the `gap` must only ever apply
   between the avatar and the sentence, never inside the sentence. Font is pinned
   explicitly rather than inherited so the export is identical on every device. */
.pp-owner  { display: flex; align-items: center; gap: 9px; margin-top: 7px; color: #cfcfe0; font: 400 14px/1.4 'Bitter', serif; }
.pp-owner img { width: 30px; height: 30px; border-radius: 50%; border: 1px solid #d4a93b66; flex: none; }
/* ⛔ nowrap on the WHOLE sentence — this is a RASTERISER constraint, not a design one.
   modern-screenshot clones the DOM and copies every computed property onto the clone
   as an inline style (see its At() → setProperty loop), INCLUDING `width`. So the
   exported SVG carries a width FROZEN from the live layout (e.g. 358px, measured with
   Bitter). Inside the SVG the text is laid out again, and if it needs even one pixel
   more than that frozen box it WRAPS — in the image only, while the live DOM stays on
   one line. That is why a DOM probe reporting LINES:1 could still export a wrapped
   date. nowrap makes the line immune: it cannot break at any width, frozen or not,
   and with ~876px of free space beside it a few px of overflow is invisible.
   .pp-summary .seg carries the same rule for the same reason — keep them in step, and
   do NOT "simplify" either back to normal wrapping. */
.pp-owner-text { white-space: nowrap; }
/* Phrases stay individually atomic too: belt and braces if the container rule is ever
   relaxed, the date can still never split into "on Jul 12," / "2026". */
.pp-owner-text .pp-nb { white-space: nowrap; }
/* The handle is bold + gold but the SAME size/face as its label — it must not read as
   a different or larger font. Inherit everything except weight and colour. */
/* Creator name (the handle only) — HF Farmiles + the app's Farmiles letter-spacing,
   matching the on-screen .pdv-handle. Normal weight: Farmiles is single-weight, so
   the <b>'s default bold would render as faux-bold. The surrounding "Deck created by
   … on …" prose stays Bitter (inherited from .pp-owner). */
.pp-owner b { font-family: 'HF Farmiles', sans-serif; letter-spacing: var(--ls-farmiles); font-size: inherit; line-height: inherit; font-weight: normal; color: var(--gold-light, #f0c864); }
.pp-summary { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 9px; font: 13px 'Bitter', serif; color: #d8d8e8; }
.pp-summary b { color: #fff; }
.pp-summary img { width: 16px; height: 16px; vertical-align: middle; }
/* A segment sizes to its OWN text and never breaks inside itself. Flex items default
   to flex-shrink:1, so an over-full line used to squeeze the segments and wrap their
   text mid-label ("60 cards" → "60" / "cards"). Nothing here may assume an English
   label width: `flex:none` + `nowrap` mean a longer translation (or a bigger font)
   makes the segment WIDER, and the row simply wraps whole segments onto the next
   line via .pp-summary's flex-wrap. */
.pp-summary .seg { display: inline-flex; align-items: center; gap: 6px; flex: none; white-space: nowrap; }
.pp-summary .sep { color: #5a5a72; }

/* Sits INSIDE .pp-titles now — directly under the deck title and above the creator
   line — instead of as its own full-width band under the header panel, which cost a
   whole strip of vertical space (2026-07-17). Tight top margin, no bottom margin:
   .pp-owner's own margin-top owns the gap below it. */
.pp-desc { color: #c7c7da; font-size: 14px; line-height: 1.45; margin: 5px 0 0; }

.pp-body { display: grid; grid-template-columns: 1fr; gap: 18px; }
.pp-body.with-graphs { grid-template-columns: 224px 1fr; }   /* graphs LEFT */
.pp-graphs { display: flex; flex-direction: column; gap: 0; }
/* The print page reuses the real .stat-block / .curve / .wp-rows markup AND the
   real renderers (_bldCostCurveHTML / _bldLoreHTML / _bldWillpowerHTML /
   _bldStrengthHTML), so the export always matches the deck-detail sidebar.
   (.curve-seg.pp-seg-stat removed 2026-07-17 with the print-only vertical WP/STR
   renderer that drifted out of sync — see app.js _buildPrintPageMarkup.) */
.pp-graphs .stat-block { margin: 0; }

.pp-type-head {
  display: flex; align-items: center; gap: 8px; color: #bdbdd0;
  font: 700 12px 'Bitter', serif; text-transform: uppercase; letter-spacing: .05em; margin: 8px 0 6px;
}

/* Pictures grids — reuse .card-thumb (real R2/CDN art + Locations landscape via
   .card-thumb--location). Print just sets the column counts. */
.pp-pics      { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
.pp-pics--loc { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }

/* ── Print-options dialog controls (inside the framed .bld-modal) ── */
.print-opts { width: 560px; max-width: 92vw; }
.po-row { margin-bottom: 16px; }
.po-row > label.po-q { display: block; font: 700 13px 'Bitter', serif; color: var(--text-bright, #fff); margin-bottom: 7px; }
.po-seg { display: inline-flex; border: 1px solid var(--bg-panel-alt, #20203e); border-radius: 8px; overflow: hidden; }
.po-seg button { background: var(--bg-elevated, #141432); border: none; color: var(--text-secondary, #c9c9dc); padding: 7px 14px; font: 600 13px 'Bitter', serif; cursor: pointer; }
.po-seg button + button { border-left: 1px solid var(--bg-panel-alt, #20203e); }
.po-seg button.on { background: rgba(212,169,59,.18); color: var(--gold-light, #f0c864); font-weight: 700; }
.po-checks { display: flex; gap: 18px; flex-wrap: wrap; }
.po-check { display: inline-flex; align-items: center; gap: 8px; color: var(--text-secondary, #c9c9dc); font: 14px 'Bitter', serif; cursor: pointer; }
.po-check input { width: 16px; height: 16px; accent-color: var(--gold, #d4a93b); }

/* ── Save image dialog (.bld-modal--saveimg) — the renamed "Print view" dialog with
   the 🖶 Print button removed (Save image only). Adopts the card-details scheme: a
   --bg-panel-alt title panel with a centred .cd-title + a top-right .cd-close X, the
   option rows centred, ~10% wider, and full-screen on mobile. ──────────────────────── */
.bld-modal.bld-modal--saveimg { width: 630px; max-width: 94vw; }
.bld-modal--saveimg .modal-head { position: relative; text-align: center; background: var(--bg-panel-alt); }
.bld-modal--saveimg .po-row  { text-align: center; }
.bld-modal--saveimg .po-q    { text-align: center; }
.bld-modal--saveimg .po-checks { justify-content: center; }
@media (max-width: 945px) {
  .print-opts-overlay { padding: 0; }
  /* Full-screen sheet on a phone (drop the stone frame). */
  .bld-modal.bld-modal--saveimg {
    width: 100%; max-width: 100%; height: 100%; max-height: 100%;
    border: none; border-image: none; border-radius: 0;
    display: flex; flex-direction: column;
  }
  .bld-modal--saveimg .modal-body { flex: 1 1 auto; overflow-y: auto; }
  /* Long option rows (e.g. German "Cards as") share the width instead of overflowing. */
  .bld-modal--saveimg .po-seg { display: flex; flex-wrap: wrap; justify-content: center; }
  .bld-modal--saveimg .po-seg button { flex: 1 1 auto; min-width: 44%; text-align: center; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE NAV (≤945px) — hamburger left-drawer + right account panel.
   Additive layer: the desktop nav is untouched ≥945px. The drawer/panels exist
   always but sit off-screen + inert; the `.open` overrides + the desktop-hiding
   live INSIDE the @media block, so desktop can never reveal them. Token-driven.
   Breakpoint 945px = the decided mobile line (see THEME_TOKENS / mobile plan).
   ═══════════════════════════════════════════════════════════════════════════ */
.nav-hamburger { display:none; background:none; border:none; color:var(--text-bright); cursor:pointer; padding:6px; margin-right:2px; }
.nav-hamburger svg { width:26px; height:26px; display:block; }

.m-nav-scrim { position:fixed; inset:0; background:rgba(0,0,0,.55); opacity:0; pointer-events:none; transition:opacity .22s; z-index:300; }
.m-drawer { position:fixed; top:0; bottom:0; left:0; width:300px; max-width:86%; background:var(--bg-surface); border-right:1px solid var(--border); transform:translateX(-100%); transition:transform .25s ease; z-index:301; display:flex; flex-direction:column; overflow:hidden; }
.m-acct-panel { position:fixed; top:0; bottom:0; right:0; width:280px; max-width:84%; background:var(--bg-surface); border-left:1px solid var(--border); transform:translateX(100%); transition:transform .25s ease; z-index:301; display:flex; flex-direction:column; overflow:hidden; padding:8px; }

.m-drawer-header, .m-acct-head { display:flex; align-items:center; gap:12px; padding:16px var(--gutter-content); border-bottom:1px solid var(--border); flex:none; }
.m-acct-head { padding:12px var(--gutter-content); margin-bottom:6px; }
.m-ava { width:44px; height:44px; border-radius:8px; object-fit:contain; flex:none; }
.m-acct-head .m-ava { width:36px; height:36px; }
.m-nm { font-family:var(--font-display); font-size:var(--fz-lg); letter-spacing:var(--ls-farmiles); color:var(--text-bright); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Account-panel display name: gold to match the home welcome greeting; +1px
   over the base --fz-md so the 16-char worst-case name fills the panel width. */
.m-acct-head .m-nm { font-size:17px; color:var(--gold-light); }
/* Identity column in the account panel: display name over Last Login (the
   welcome panel surrenders Last Login in mobile view — see the ≤945 block). */
.m-acct-id { display:flex; flex-direction:column; gap:2px; min-width:0; }
.m-acct-lastlogin-label { font-size:11px; font-weight:800; letter-spacing:1px; text-transform:uppercase; color:var(--text-dim); }
.m-acct-lastlogin-value { font-family:var(--font-ui); font-size:13px; font-weight:var(--fw-semibold); color:var(--text-muted); }

/* Drawer "< Back" header row — closes the drawer; styled as a tappable option
   above Home (the .m-drawer-header border-bottom is the divider). */
.m-drawer-header { cursor:pointer; }
@media (any-hover: hover) {
.m-drawer-header:hover .m-drawer-back-label { color:var(--text-bright); }
}
.m-drawer-back-label { font-family:var(--font-display); font-size:var(--fz-lg); letter-spacing:var(--ls-farmiles); color:var(--text-muted); }
/* Nested sub-panel title ("My Collection"): same size as the menu options
   below it, gold to read as the section heading. */
.m-subpanel-title { font-family:var(--font-display); font-size:var(--fz-lg); letter-spacing:var(--ls-farmiles); color:var(--gold-light); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

.m-drawer-nav { display:flex; flex-direction:column; padding:8px; gap:2px; overflow-y:auto; }
.m-drawer-item { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:13px var(--gutter-content); border-radius:6px; cursor:pointer; background:none; border:none; text-align:left; width:100%; font-family:var(--font-display); font-size:var(--fz-lg); letter-spacing:var(--ls-farmiles); color:var(--text-muted); transition:background .12s, color .12s; }
@media (any-hover: hover) {
.m-drawer-item:hover  { color:var(--text-bright); background:rgba(255,255,255,.05); }
}
.m-drawer-item.active { color:var(--gold-light);  background:var(--gold-dim); }
.m-chev { width:15px; height:15px; opacity:.6; flex:none; }

.m-subpanel { position:absolute; inset:0; background:var(--bg-surface); z-index:2; transform:translateX(100%); transition:transform .25s ease; display:flex; flex-direction:column; overflow:hidden; }
/* Sub-panel "< Title" header row — same tappable-row idiom as .m-drawer-header:
   the whole row (chevron + title) closes the sub-panel, not just the chevron. */
.m-subpanel-head { display:flex; align-items:center; gap:10px; padding:12px var(--gutter-content); border-bottom:1px solid var(--border); flex:none; cursor:pointer; }
@media (any-hover: hover) {
.m-subpanel-head:hover .m-subpanel-title { color:var(--text-bright); }
}
.m-back { background:none; border:none; color:var(--text-bright); cursor:pointer; padding:4px; display:flex; flex:none; }
.m-back svg { width:20px; height:20px; }

/* Farmiles display face to match the hamburger drawer items (.m-drawer-item):
   same font / --ls-farmiles tracking / --fz-lg size. Log Out keeps ruby via
   .m-acct-item.logout below. */
.m-acct-item { font-family:var(--font-display); letter-spacing:var(--ls-farmiles); font-size:var(--fz-lg); font-weight:var(--fw-medium); color:var(--text-muted); padding:13px var(--gutter-content); border-radius:6px; background:none; border:none; text-align:left; cursor:pointer; transition:background .12s, color .12s; }
@media (any-hover: hover) {
.m-acct-item:hover { color:var(--text-bright); background:rgba(255,255,255,.05); }
}
.m-acct-item.logout { color:var(--ruby); }

@media (max-width: 945px) {
  .nav-hamburger { display:flex; }
  .top-nav { gap:14px; }                          /* halve the 28px gap → tighter hamburger↔logo */
  .top-nav .nav-links { display:none; }          /* desktop links → drawer */
  .top-nav .nav-end { margin-left:auto; }        /* lang pill + avatar pinned right (nav-links no longer flexes them over) */
  .top-nav .nav-user-badge { display:none; }     /* hide name+caret; avatar opens the panel */
  .top-nav .nav-ill-dropdown { display:none !important; }  /* mobile uses the right panel */
  .m-nav-scrim.open  { opacity:1; pointer-events:auto; }
  .m-drawer.open     { transform:none; }
  .m-acct-panel.open { transform:none; }
  .m-subpanel.open   { transform:none; }
  /* Mobile view: the welcome panel drops Last Login unconditionally — it would
     otherwise be squeezed out as the panel narrows. It now lives in the
     right-side account panel instead (tap the top-right avatar). */
  .welcome-last-login { display: none; }
}

/* ── Actual browser print: strip everything but the print page ── */
@media print {
  body.printing .print-opts-overlay { display: none !important; }
  #print-root { position: static; }
  .print-stage { padding: 0; min-height: 0; display: block; }
  .print-page { width: 100%; max-width: none; box-shadow: none; border-radius: 0; }
  /* Force backgrounds/ink colours to actually print. */
  .print-page, .print-page * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARD DETAILS DIALOG  —  .bld-modal--card (desktop) + .cd-mobile (mobile)
   ───────────────────────────────────────────────────────────────────────────
   The redesigned card-details surface, built by openPanel() in app.js. There are
   TWO layouts chosen purely by WINDOW WIDTH (openPanel re-renders on resize):
     • width ≥ CARD_DIALOG_DESKTOP_MIN_PX (700)  → DESKTOP: a .bld-modal--card
       inside the canonical .bld-modal stone frame, opened via _openModal (so it
       joins the modal z-order stack). Two columns: full-bleed ink-gradient card
       panel (left) + details (right).
     • width <  700                              → MOBILE: a full-screen .cd-mobile
       surface pinned BETWEEN the app's top nav (--nav-h) and bottom bar
       (--bottom-bar-h); NO stone frame; sticky "‹ Back" header (reuses the real
       .m-drawer-header / .m-back control); everything centred and scrollable.
   The switch threshold is independent of the 945px nav breakpoint on purpose:
   an unfolded foldable / landscape phone / tablet all get the desktop dialog.
   See SPEC.md → "Card Details Dialog" and THEME_TOKENS.md for the full contract.
   Colours: title panel = --bg-panel-alt (#20203E); details = --bg-surface
   (#0D0D24). Info pills deliberately match the filter-bar buttons (.set-ms-trigger
   / .f-chip). Card art + set image are CDN-hosted; the ink gradient is the card's
   EN-canonical ink colour(s), darkening toward the bottom-right.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Shared bits (used by both layouts) ─────────────────────────────────────── */
/* Title (clamped: 2 lines desktop / 3 lines mobile); matches .modal-head h3 colour. */
.cd-title { font-family: 'Bitter', serif; font-weight: 800; font-size: 21px; line-height: 1.25;
            color: var(--text-bright); margin: 0; text-align: center;
            display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* Card image holder — rounded + shadow; the <img> (portrait) or <canvas> (rotated
   location) fills it. aspect-ratio flips for locations (landscape). */
.cd-art { position: relative;   /* hosts the owned-count .card-badges in the Add-Card receipt */
          width: 100%; aspect-ratio: 1468 / 2048; border-radius: 14px; overflow: hidden;
          box-shadow: 0 12px 30px rgba(0,0,0,.6); background: rgba(0,0,0,.28);
          border: 1px solid rgba(255,255,255,.10);
          display: flex; align-items: center; justify-content: center; text-align: center; }
.cd-art > img, .cd-art > canvas { width: 100%; height: 100%; object-fit: contain; display: block; }
.cd-art .cd-art-noimg { font-size: 12px; color: rgba(255,255,255,.7); padding: 8px; }
.is-location .cd-art { aspect-ratio: 2048 / 1468; }
.cd-ident { font-family: 'Bitter', serif; font-weight: 700; letter-spacing: .6px;
            color: var(--gold-light); font-size: 14px; }

/* "CARD SET - N" label + set image + format legality. */
.cd-set-label { font-family: 'Bitter', serif; font-weight: 800; letter-spacing: 1.2px;
                color: var(--text-secondary, #9a9ac0); font-size: 12.5px; text-transform: uppercase; }
.cd-set-row { display: flex; gap: 14px; align-items: flex-start; }
/* width + aspect-ratio match the 300×169 set thumbnails so the whole banner shows
   (object-fit:contain); the old 133×90 (≈3:2) box cropped the 16:9 image. */
.cd-set-img-wrap { flex: 0 0 auto; width: 160px; aspect-ratio: 300 / 169; border-radius: 8px; overflow: hidden;
                   border: 1px solid rgba(255,255,255,.10); background: linear-gradient(135deg, #2a2a52, #15152e);
                   display: flex; align-items: center; justify-content: center; }
.cd-set-img-wrap > img { width: 100%; height: 100%; object-fit: contain; display: block; }
.cd-set-img-wrap .cd-set-fallback { color: rgba(255,255,255,.72); font-weight: 700; font-size: 12px;
                                    text-align: center; padding: 6px; }
.cd-set-legal { display: flex; flex-direction: column; gap: 9px; padding-top: 2px; }
.cd-legal-line { display: flex; align-items: center; gap: 8px; }
.cd-legal-line img { width: 18px; height: 18px; flex: 0 0 auto; }    /* green_checkmark / red_cross */
.cd-legal-line .badge { font-size: 11.5px; padding: 4px 10px; }      /* reuse .badge-core / .badge-infinity */
.badge.badge--off { background: rgba(148,163,184,.14); color: #c3ccd9; border: 1px solid rgba(148,163,184,.35); }

/* Info pills — IDENTICAL look to the filter-bar buttons (.set-ms-trigger / .f-chip).
   max-width bounds the wrap (~3 pills/row) so the desktop dialog hugs content. */
.cd-pills { display: flex; flex-wrap: wrap; gap: 8px; max-width: 360px; }
.cd-pill { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
           background: var(--bg-filter-control); border: 1px solid var(--border);
           border-radius: var(--radius-s); padding: 4px var(--filter-pill-pad-x);
           color: var(--text-muted); font-size: 12px; font-family: inherit; }
.cd-pill img { width: 15px; height: 15px; }
/* Ink-cost pill: the cost NUMBER is baked into the SVG icon, so the icon grows to
   the pill's vertical edges for legibility. width:auto preserves its aspect ratio;
   the negative block-margin cancels the extra height so the pill's size is unchanged. */
.cd-pill--cost img { width: auto; height: 23px; margin-block: -4px; }
.cd-pill .f-chip-emoji { font-size: 13px; line-height: 1; }   /* inkless ✖ reuses the filter chip's emoji span */
.cd-pill--subtype { background: color-mix(in srgb, var(--bg-filter-control), #fff 9%); }

/* Actions area (below the pills): My Cards steppers, the +Add button, How to Play. */
.cd-actions { margin-top: 6px; display: flex; flex-direction: column; gap: 10px; }

/* ── Add-Card "Card Added" receipt (opts.addedReceipt) ──────────────────────────
   Reuses the whole card-details dialog; adds a banner at the TOP of the details
   column (.cd-right / .cd-m-details, above the set row), the Undo button at the
   BOTTOM in .cd-actions, and the My-Cards ×N owned-count badges on the art. */
.cd-added { display: flex; flex-direction: column; gap: 5px; align-items: flex-start; width: 100%; }
.cd-added-label { font-family: 'Bitter', serif; font-weight: 700; font-size: 12.5px;
                  letter-spacing: 1.2px; text-transform: uppercase; color: var(--gold-light); line-height: 1.4; }
/* "✓ Added…" and the optional "Now you own…" line share one style. */
.cd-added-msg, .cd-added-owned { font-family: 'Bitter', serif; font-weight: 400; font-size: 14px;
                                 color: #4ade80; line-height: 1.4; }
.cd-m-details .cd-added { align-items: center; text-align: center; }   /* mobile centres */
/* Undo keeps its natural width (don't let the stretch flex column widen it). */
.cd-actions .undo-add-btn { align-self: flex-start; }
.cd-m-details .cd-actions .undo-add-btn { align-self: center; }
/* Inline std/foil steppers (My Cards). The IDs inside (#qty-std/#qty-foil,
   #std-/#foil-inc/dec) are kept identical to the legacy panel so attachQtyControls()
   drives them unchanged (cap-422, remove-last confirm, debounced delta preserved). */
.cd-qty { display: flex; flex-direction: column; gap: 10px; width: 100%; max-width: 484px; }
.cd-qty-row { display: flex; align-items: center; gap: 10px; min-width: 0; box-sizing: border-box;
              background: var(--bg-elevated); border: 1px solid var(--bg-panel-alt);
              border-radius: var(--radius-m); padding: 8px 12px; }
.cd-qty-row > img { width: 26px; height: 26px; flex: 0 0 auto; }
/* Label flexes + wraps to 2 lines for long (e.g. German) strings instead of truncating. */
.cd-qty-label { flex: 1 1 auto; min-width: 0; text-align: left; font-weight: 700; font-size: 13px;
                white-space: normal; overflow-wrap: anywhere; line-height: 1.2; }
.cd-qty-stepper { flex: 0 0 auto; margin-left: auto; display: flex; align-items: center; gap: 12px; }
.cd-step { width: 30px; height: 30px; border-radius: 8px; line-height: 1; font-size: 18px;
           background: var(--bg-panel-alt); border: 1px solid rgba(255,255,255,.12); color: var(--gold-light);
           display: flex; align-items: center; justify-content: center; }
.cd-qty-val { min-width: 22px; text-align: center; font-weight: 800; font-size: 16px; }
.cd-foil-note { align-self: flex-start; }   /* always-foil note: left on desktop, centred on mobile (below) */
@media (any-hover: hover) {
  .cd-close:hover { color: var(--gold-light); background: rgba(255,255,255,.09); }
  .cd-step:hover  { background: rgba(212,169,59,.18); }
}

/* ── Desktop layout — .bld-modal--card (inside the .bld-modal stone frame) ───── */
/* width:fit-content + max-width cap ⇒ hugs content (no reserved right-side gap), capped. */
.bld-modal--card { width: fit-content; max-width: min(760px, 94vw); background: var(--bg-surface); }
.bld-modal--card.is-location { max-width: min(880px, 94vw); }
.bld-modal--card .modal-head { position: relative; text-align: center; background: var(--bg-panel-alt); }
/* Vertically centred in the head bar with right padding (was top:-2px; right:0 —
   jammed into the very corner). */
.cd-close { position: absolute; top: 50%; right: 12px; transform: translateY(-50%);
            width: 34px; height: 34px; border-radius: 50%;
            font-size: 20px; color: var(--text-secondary, #9a9ac0); background: rgba(255,255,255,.04);
            display: flex; align-items: center; justify-content: center; }
.cd-grid { display: flex; gap: 0; padding: 0; }   /* left panel full-bleed; right panel owns its padding */
.cd-left { flex: 0 0 auto; width: clamp(228px, 38vw, 328px); border-radius: 0; padding: 22px 18px;
           display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; }
.bld-modal--card.is-location .cd-left { width: clamp(285px, 44vw, 418px); }
.cd-right { flex: 0 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 14px;
            padding: 18px 22px 22px 22px; }

/* ── Mobile layout — .cd-mobile (full-screen, COVERS the nav bars) ──────────────
   inset:0 covers the whole viewport — at z-index --z-overlay (450) it sits ABOVE the
   top nav (--z-nav 100) and the hamburger drawer (--z-drawer 300), so those bars are
   obscured AND non-interactive (you can't open the drawer behind the dialog), but still
   BELOW .bld-modal confirms (--z-modal 1000) + toasts (--z-toast 9000) so the
   remove-last-copy confirm and success toasts render correctly on top. The sticky
   ‹ Back is the only nav while the dialog is open. */
.cd-mobile { position: fixed; inset: 0;
             overflow-y: auto; -webkit-overflow-scrolling: touch; background: var(--bg-surface);
             z-index: var(--z-overlay, 450); }
.cd-m-back { position: sticky; top: 0; z-index: 3; background: var(--bg-surface); cursor: pointer; }
.cd-m-title-panel { text-align: center; padding: 14px 18px; background: var(--bg-panel-alt); }
.cd-m-title-panel .cd-title { -webkit-line-clamp: 3; font-size: 19px; }
.cd-m-art-panel { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 18px; }
.cd-m-art-panel .cd-art { width: 78%; max-width: 320px; }
.cd-m-art-panel.is-location .cd-art { width: 100%; max-width: 480px; aspect-ratio: 2048 / 1468; }
.cd-m-details { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 18px 16px 28px; }
.cd-m-details .cd-set-label { text-align: center; }
.cd-m-details .cd-set-row { flex-direction: column; align-items: center; gap: 12px; }
.cd-m-details .cd-set-legal { flex-direction: row; gap: 18px; }
.cd-m-details .cd-pills { justify-content: center; }
.cd-m-details .cd-qty { width: 100%; max-width: 504px; }
.cd-m-details .cd-actions { width: 100%; align-items: center; text-align: center; }
.cd-m-details .cd-foil-note { align-self: center; }

/* ── Mobile only (≤945px): pills +15% ─────────────────────────────────────────
   Scoped to the mobile card-detail dialog (.cd-m-details) and the three mobile
   filter dialogs (.fbm-dlg-body — Card Catalog / My Cards / Missing) so DESKTOP
   is untouched. The bigger chips just wrap to more rows inside the width-capped
   .fbm-dlg (clamp(300px,90%,460px)) — the dialog never grows horizontally and
   adds no scrollbar. Fully-qualified selectors → specificity beats the shared
   base rules regardless of source order. */
@media (max-width: 945px) {
  /* card-detail info pills */
  .cd-m-details .cd-pill { font-size: 14px; padding: 5px 11px; gap: 7px; }
  .cd-m-details .cd-pill img { width: 17px; height: 17px; }
  /* width:auto MUST be repeated here, not inherited from the base .cd-pill--cost
     img rule: this selector and ".cd-m-details .cd-pill img" above tie at
     specificity (0,2,1)/(0,3,1 for this one, but it wasn't setting width at all),
     so without an explicit width the cascade fell through to the generic 17px
     regular-icon rule (same 0,2,1 as the base cost rule, but later in the file —
     ties break by source order) and squashed the cost hex to 17×27 instead of its
     real ~27×27 aspect ratio. Confirmed via computed-style devtools 2026-07-05. */
  .cd-m-details .cd-pill--cost img { width: auto; height: 27px; margin-block: -5px; }  /* keeps mobile pill height */
  .cd-m-details .cd-pill .f-chip-emoji { font-size: 15px; }

  /* filter dialog: toggle chips + dropdown triggers / options / fields */
  .fbm-dlg-body .f-chip { font-size: 14px; padding: 4px 11px; border-radius: 15px; }
  .fbm-dlg-body .f-chip-icon { width: 17px; height: 17px; margin-right: 6px; }
  .fbm-dlg-body .f-chip-emoji { font-size: 14px; }
  .fbm-dlg-body .set-ms-trigger { font-size: 14px; padding: 5px 11px; gap: 7px; }
  .fbm-dlg-body .set-ms-arrow { font-size: 11.5px; }
  .fbm-dlg-body .set-ms-option { font-size: 14px; padding: 8px 16px; gap: 10px; }
  .fbm-dlg-body .set-ms-option input[type="checkbox"] { width: 16px; height: 16px; }
  .fbm-dlg-body .set-ms-dropdown { min-width: 264px; max-height: 182px; }  /* ≈5 taller rows (was 158) */
  .fbm-dlg-body .filter-search { font-size: 14px; }

  /* ── Deck filter dialogs (My Private / My Public Decks) ──────────────────────
     The deck bar uses `.chip` / `.issbtn` / `.df-search` (scoped to the desktop
     `.decks-filterbar`), not the card `.f-chip`. Inside the mobile group dialog
     those controls live in `.fbm-dlg-body`, so restate their styling here — all at
     the SAME 14px as the card dialog controls (chip / issues / sort / search). */
  .fbm-dlg-body .df-deckgroups { display: flex; flex-direction: column; gap: 14px; width: 100%; }
  .fbm-dlg-body .df-group { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; justify-content: center; }
  .fbm-dlg-body .chip {
    display: inline-flex; align-items: center; gap: 6px; font-size: 14px; padding: 5px 13px;
    border-radius: 15px; background: var(--bg-filter-control); border: 1px solid var(--border);
    color: var(--text-muted); cursor: pointer; font-family: inherit;
  }
  .fbm-dlg-body .chip.on { background: var(--gold); border-color: var(--gold); color: #1a1405; font-weight: 700; }
  .fbm-dlg-body .chip.disabled { opacity: 0.32; pointer-events: none; }
  .fbm-dlg-body .df-ink-icon { width: 16px; height: 16px; flex-shrink: 0; object-fit: contain; }
  .fbm-dlg-body .issbtn { font-size: 14px; }
  .fbm-dlg-body .df-search { width: 100%; box-sizing: border-box; font-size: 14px; }
  /* "With issues" + the sort dropdown share one centered row; search is full-width below. */
  .fbm-dlg-body .df-toolrow { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
}

/* ── Deck-list action buttons (+ New / Import) — position depends on breakpoint ──
   Desktop: they live in `.deck-result-row` (right of the count). Mobile (≤945px):
   they move to `.deck-actions-top` ABOVE the collapsed filter bar. Each layout
   renders BOTH copies and CSS shows exactly one per breakpoint. */
.deck-actions-top { display: none; }
@media (max-width: 945px) {
  .deck-actions-top { display: flex; gap: 9px; margin: 8px 0 12px; }
  .deck-actions-top .btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; }
  .deck-result-row .deck-actions-inline { display: none; }
  /* The action row above owns the spacing to the filter; drop the .fbm-bar's own
     top margin so the two don't stack into a doubled gap. Bottom margin tuned to
     12px so the gap ABOVE the count (filter bar → count) matches the gap BELOW it
     (count → deck grid, .deck-result-row margin-bottom) — they were lopsided
     (~14px above vs ~0 below). .deck-grid has no top margin, so 12/12 is equal. */
  #deck-filter-bar .fbm-bar,
  #pdeck-filter-bar .fbm-bar { margin-top: 0; margin-bottom: 12px; }
  /* Center "Showing all N decks" (private + public share .deck-result-row). */
  .deck-result-row { justify-content: center; margin-bottom: 12px; }
  .deck-result-row .cards-count { flex: 0 1 auto; text-align: center; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE DECK BUILDER — implements MOCKUP-mobile-deck-builder.html.

   Every rule here lives INSIDE a media query. The desktop builder must not change
   at all, so there is deliberately no bare (unscoped) rule in this section.

   Two layers:
     ≤945px  — shared mobile chrome (the Gather-the-Party filter GROUP button).
     ≤600px  — the FOLDED phone: the stacked .mbld-* bands + the deck sheet.
   Unfolded (601–945px) keeps the desktop .bld-split — see the mockup's frame C.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 945px) {
  /* Gather-the-Party gets its own filter-group button on the compact bar (desktop
     surfaces the same chips as a labelled HUNNY ONLY .filter-row instead). The bee
     glyph is applied here rather than inside the button's label because
     attachMobileFilterBar writes the dialog title with textContent — HTML in a group
     label would render as literal markup. */
  .fbm-group-btn[data-fbm-group="hunny"] .filter-label::before {
    content: '';
    display: inline-block;
    width: 13px;
    height: 13px;
    margin-right: 5px;
    vertical-align: -2px;
    background: url('../images/navigation/hunny_icon.svg') center / contain no-repeat;
  }
  /* 4 or 5 groups read better 2-up than 3+1 / 4+1, so the Hunny variant pins two
     columns. Declared after the base .fbm-groups rules so it wins on source order
     (no !important needed). Remove the enabler and the class + the button both go,
     reverting the bar to its normal grid. */
  .fbm-groups--hunny { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* The Hunny group dialog contains ONLY the off-ink chip row — no dropdown or search
     box beside it, unlike every other group — so a left-aligned row sits visibly off
     against the centred dialog title and the full-width Close button. Centre it.
     Scoped to this group so the other filter-group dialogs keep their current
     left-aligned layout (they mix chips with dropdowns, where left-alignment is right). */
  .fbm-dlg-body .fbm-hunny-chips { justify-content: center; }
}

@media (max-width: 600px) {
  /* .builder is already `display:flex; flex-direction:column; height:100%` and
     .builder-overlay carries a definite height, so the bands are genuinely sticky.
     min-height:0 lets the S4 scroller actually shrink; position:relative anchors
     the absolutely-positioned sheet + scrim. */
  .builder--folded { min-height: 0; position: relative; background: var(--bg-void); }

  /* ── S1 · header ─────────────────────────────────────────────────────────
     Styled like the mobile deck-detail head: pencil + inks + title inline on one
     line, badges on the next, then a CENTERED buttons row. No description line
     (edit it via the pencil) so the narrow frame reclaims the height. */
  .mbld-head {
    flex: none;
    background: linear-gradient(180deg, var(--bg-surface), var(--bg-void));
    border-bottom: 1px solid var(--border);
  }
  .mbld-head .detail-head { margin: 0; background: none; border: none; display: block; position: static; }
  /* var(--gutter-content) rather than the mockup's literal 12px, so the title's left edge
     lines up with the label, filter bar and card grid below it (all on the shared gutter
     now — see the deck-details match further down). */
  .mbld-head .detail-head .dh-inner { display: block; padding: 10px var(--gutter-content) 4px; min-width: 0; }
  .mbld-head .detail-head .dh-title-row { display: block; }
  /* Title + ink icons sit a touch below the 18px mobile deck-detail size so they do
     not dominate the narrow folded frame; the ink icon is about title cap-height so
     the hexes read inline with the text rather than looming over it. */
  .mbld-head .detail-head h1 {
    display: block;
    margin: 0;
    font-size: 16px;
    line-height: 1.3;
    color: var(--text-bright);
    font-weight: 700;
  }
  .mbld-head .bld-pencil { vertical-align: middle; margin-right: 6px; }
  .mbld-head .detail-head .dh-inks { display: inline-flex; gap: 4px; vertical-align: -2px; margin-right: 6px; }
  .mbld-head .detail-head .dh-ink-icon { width: 15px; height: 15px; }
  .mbld-head .detail-head .dh-badges--inline {
    display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 8px 0 0;
  }
  .mbld-headbtns {
    display: flex; justify-content: center; align-items: center;
    gap: 8px; flex-wrap: wrap; padding: 2px var(--gutter-content) 9px;
  }
  .mbld-headbtns .pdb-copylink { white-space: nowrap; }

  /* ── Public builder header (frames E / F) ─────────────────────────────────
     ⚠ `display: contents` is what makes Publish / Copy link / Unpublish sit in the SAME
     centred row as Close. .pdb-pub-wrap is a desktop flex box with `margin-left:auto`
     and `align-self:flex-end` — nested inside .mbld-headbtns it would become ONE flex
     item and push Close onto its own line. That "buttons stack instead of forming one
     centred row" is precisely the defect called out in the 9985b11 post-mortem, so it is
     dissolved here rather than restyled. */
  .mbld-headbtns .pdb-pub-wrap {
    display: contents;
  }
  /* "Complete your deck before publishing" — its own centred line above the buttons. */
  .mbld-pubreason {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 5px;
    font-size: 11.5px;
    color: var(--gold-light);
    padding: 0 var(--gutter-content) 5px;
    text-align: center;
  }
  .mbld-pubreason:empty { display: none; }   /* no reason → no gap above the buttons */
  .mbld-pubreason .app-glyph { width: 14px; height: 14px; }

  /* Published → read-only. The locked notice is the S4 band's entire content, so give it
     enough height to read as a deliberate state rather than a stray line of text. */
  .builder--folded .pbld-locked { min-height: 200px; }
  .builder--folded .pbld-locked .es-icon img { width: 44px; height: 44px; }

  /* ONE button size across the folded builder = the "Show deck issues" (.issbtn)
     size, so no two buttons differ in height. Scoped to .builder--folded on purpose:
     applying it to every .btn on a phone would restyle unrelated mobile pages. */
  .builder--folded .btn,
  .builder--folded .pdb-copylink,
  .builder--folded .issbtn {
    height: 26px;
    padding: 0 12px;
    font-size: 12px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
  }
  .builder--folded .btn .app-glyph { margin-right: 0.35em; }

  /* ── S2 · "Add Cards to Your Deck" — icon pinned to line 1, text WRAPS ── */
  .mbld-addlabel { flex: none; padding: 8px var(--gutter-content) 7px; border-bottom: 1px solid var(--border); }
  .builder--folded .mbld-addlabel .pane-label {
    display: flex; align-items: flex-start; gap: 8px; padding: 0;
  }
  /* ⚠ Specificity: the desktop rules are `.builder .pane-label .pl-icon` (0,3,0) and
     `.builder .pane-label .pl-text .t` (0,4,0). A shorter selector here LOSES to them
     regardless of source order — which is exactly what happened first time round: the
     line-height applied but the font-size stayed at the desktop 14px, and the icon kept
     its 28px gold-dim box. Match/beat their specificity and explicitly unset the box. */
  .builder--folded .mbld-addlabel .pane-label .pl-icon {
    width: auto;
    height: auto;
    flex: none;
    margin-top: 1px;
    padding: 0;
    background: none;
    border: none;
    border-radius: 0;
    color: var(--gold-light);
    display: flex;
  }
  .builder--folded .mbld-addlabel .pane-label .pl-icon .app-glyph { width: 16px; height: 16px; }
  .mbld-addlabel .pl-text { flex: 1; min-width: 0; }
  .builder--folded .mbld-addlabel .pane-label .pl-text .t {
    font-size: 12.5px;
    line-height: 1.4;
    font-weight: 400;
    color: var(--text-main);
  }

  /* ── S3 · deck-issues opener (rendered only while the deck HAS issues) ── */
  .mbld-issues {
    flex: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 7px var(--gutter-content);
    background: rgba(162, 0, 0, .07);
    border-bottom: 1px solid rgba(255, 0, 19, .22);
  }
  .mbld-issues .issbtn { margin: 0; }
  .mbld-issues .mbld-x {
    margin-left: auto; flex: none;
    background: none; border: none;
    color: var(--text-muted); font-size: 20px; line-height: 1;
    cursor: pointer; padding: 0 2px;
  }

  /* ── The scrolling band — now holds S1-S4 (header + label + issues + grid) ──
     No horizontal padding here: each band supplies its own, so the header/label/issues
     backgrounds stay full-bleed while the grid keeps a narrow gutter. */
  .mbld-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 0; }
  .mbld-scroll .fbm-bar { margin: 0 0 8px; }
  .mbld-scroll .bld-rule-chip { margin: 0 0 8px; }
  .builder--folded .results-line { text-align: center; }

  /* ── Gutter: MATCH the mobile private deck-details page (Gus, 2026-07-20) ──
     That page uses `.detail-body { padding: 11px var(--gutter-content) 90px }`, i.e. the
     app-wide 15px content gutter and a 90px bottom allowance. The builder was inset 28px
     per side — `.mbld-scroll`'s 10px PLUS `.bld-search-grid`'s own 18px — which squeezed
     the two card columns; adopting the shared token makes the two deck surfaces line up
     instead of picking an arbitrary number.
     The 90px bottom is the same allowance that keeps the deck-details page's last row
     clear of the floating Scroll-To-Top. Every band uses the one gutter so the left edge
     of the title, the label, the filter bar and the cards all align. */
  .builder--folded .bld-search-grid { padding: 8px var(--gutter-content) 90px; }
  .builder--folded .search-filters { padding: 8px var(--gutter-content) 10px; }
  .builder--folded #bld-rule-chip { padding: 0 var(--gutter-content); }

  /* ── S5 · thin deck bar — count only; everything else lives in the sheet ── */
  .mbld-deckbar {
    flex: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 14px;
    background: var(--bg-surface);
    border-top: 1px solid var(--gold-dim);
    cursor: pointer;
  }
  .mbld-deckbar .db-count { font-family: var(--font-ui); font-size: 13.5px; color: var(--text-bright); }
  .mbld-deckbar .db-spacer,
  .mbld-sheet .sheet-grip .db-spacer { flex: 1; }
  /* var(--font-display) is 'HF Farmiles', sans-serif — NOT var(--font-ui), which is
     'Bitter', serif. That exact substitution was a shipped bug (see commit 9985b11).
     --ls-farmiles is 0.05em, which is the value the mockup states. */
  .mbld-deckbar .db-showdeck,
  .mbld-sheet .sheet-grip .db-showdeck {
    font-family: var(--font-display);
    letter-spacing: var(--ls-farmiles);
    font-size: 14px;
    color: var(--gold-light);
  }
  .mbld-deckbar .db-chev,
  .mbld-sheet .sheet-grip .db-chev {
    width: 30px; height: 30px;
    border-radius: 8px;
    border: 1px solid var(--gold);
    color: var(--gold-light);
    display: flex; align-items: center; justify-content: center;
    font-size: 13px; flex: none; cursor: pointer;
  }

  /* ── The deck sheet ───────────────────────────────────────────────────────
     Slides up from the deck bar rather than appearing instantly. Both the sheet and its
     scrim mount WITHOUT `.is-open`; JS adds it on the next frame (and removes it, waiting
     BLD_SHEET_ANIM_MS, on close). 260ms matches the app's other overlay transitions.
     `prefers-reduced-motion` collapses it to an instant show — the JS timing is unchanged,
     so the node still unmounts correctly. */
  .mbld-scrim {
    position: absolute; inset: 0; background: rgba(4, 4, 10, .55); z-index: 5;
    opacity: 0;
    transition: opacity .26s ease;
  }
  .mbld-scrim.is-open { opacity: 1; }
  /* Covers the whole builder, so the expanded deck view gets the same full height
     the deck-details page has. */
  .mbld-sheet {
    position: absolute;
    inset: 0;
    z-index: 6;
    background: var(--bg-void);
    border-top: 1px solid var(--gold-dim);
    box-shadow: 0 -14px 40px rgba(0, 0, 0, .6);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transform: translateY(100%);
    transition: transform .26s cubic-bezier(0.4, 0, 0.2, 1);
    will-change: transform;
  }
  .mbld-sheet.is-open { transform: translateY(0); }

  @media (prefers-reduced-motion: reduce) {
    .mbld-sheet, .mbld-scrim { transition: none; }
  }
  .mbld-sheet .sheet-grip {
    flex: none;
    display: flex; align-items: center; gap: 10px;
    padding: 9px 14px;
    border-bottom: 1px solid var(--border);
    cursor: pointer;
  }
  .mbld-sheet .sheet-body {
    flex: 1; min-height: 0; overflow-y: auto;
    /* Bottom padding clears the floating Scroll-To-Top so the last card row is never
       covered by it at the end of the list. Same gutter + 90px bottom allowance as the
       mobile deck-details page (`.detail-body { padding: 11px var(--gutter-content) 90px }`),
       which is the surface Gus asked this to match. */
    padding: 10px var(--gutter-content) 90px;
    position: relative;
  }
  .mbld-sheet .stat-block { margin: 0 0 10px; }
  /* Two analytics panels side by side. Declared after the 945px rule that makes
     .detail-stats-rest .bld-stat-row `display:contents`, so this wins on order. */
  .mbld-sheet .bld-stat-row { display: flex; gap: 10px; }
  .mbld-sheet .bld-stat-row > .stat-block { flex: 1; min-width: 0; }
  .mbld-sheet .count-only-row {
    display: flex; flex-wrap: wrap; row-gap: 8px; align-items: center; margin: 12px 0 6px;
  }
  /* Centered result line. The real mobile rule keys on the #detail-result-line id,
     so it can never reach the sheet's own result line. */
  .mbld-sheet .count-only-row .cards-count {
    flex: 1 1 100%; text-align: center; font-size: 12.5px; color: var(--text-main);
  }
  .mbld-sheet .pill-grid { grid-template-columns: minmax(0, 1fr); }
  /* 4-5 deck-view filter groups in a balanced 2-up grid. */
  .mbld-sheet .fbm-groups { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .mbld-sheet .type-section-header { margin-top: 12px; }

  /* Scroll-to-top. ⚠ The mockup styles this as `.mbld-sheet .scroll-to-top-btn`, but in the
     REAL app the button is `position:fixed` and lives on <body>, not inside the surface it
     scrolls — so a descendant selector NEVER matches and that ported rule was dead. It also
     sits at `bottom: var(--bottom-bar-h) + 12px`, which on a folded phone lands on top of
     the .mbld-deckbar (measured overlap on staging). Lift it clear using the deck bar's
     measured height, published as --mbld-deckbar-h by _bldSetFoldedBodyState. The body
     marker is what lets a body-level element respond to a builder-only layout. */
  body.bld-folded-active .scroll-to-top-btn {
    bottom: calc(var(--bottom-bar-h) + var(--mbld-deckbar-h, 49px) + 12px);
  }
  /* Sheet raised → the deck bar is COVERED by the sheet, so the button must not keep
     reserving its height; it only needs to clear the app's bottom bar. Without this it
     floated a deck-bar's height too high and sat over the card list. */
  body.bld-folded-active.bld-sheet-open .scroll-to-top-btn {
    bottom: calc(var(--bottom-bar-h) + 12px);
  }

  /* Compact pill, shrunk proportionally for the phone list: the base .pill is 14px /
     30px min-height with a 27px cost icon and 26px stepper buttons — sized for a
     full-width desktop row. 30 to 26px row (-13%), cost icon held at about 85% of
     the new height, text 14 to 12.5px, ident 12 to 11px, stepper 26 to 20px so it
     still clears the 26px pill. */
  .mbld-sheet .pill { font-size: 12.5px; min-height: 26px; }
  .mbld-sheet .pill-cost-icon { height: 22px; }
  .mbld-sheet .pill-ident { font-size: 11px; }
  .mbld-sheet .dl-step button { width: 20px; height: 20px; border-radius: 6px; font-size: 13px; }
  .mbld-sheet .dl-step .q { font-size: 12px; min-width: 14px; }
  /* Rows are tappable (opens the card panel); the stepper is a dead zone for that. */
  .mbld-sheet .pill.dl-pill { cursor: pointer; }

  /* ── Deck-issues dialog ─────────────────────────────────────────────────────
     The real .deck-action-banner is a WIDE desktop flex row (.dab-text is
     `flex:1 1 240px; min-width:200px`, .dab-actions is `flex:none`) — icon + 200px
     text + a ~130px button cannot fit a 320px phone dialog, so the text ran outside
     the panel. There is no mobile stacking rule in the base CSS, so add one: the
     text keeps row 1 next to the icon, the action button wraps to its own row. */
  .mbld-issdlg { width: 100%; max-width: 320px; max-height: 82%; display: flex; flex-direction: column; }
  .mbld-issdlg .modal-body { flex: 1; min-height: 0; overflow-y: auto; padding: 16px 12px; }
  .mbld-issdlg .modal-head,
  .mbld-issdlg .modal-foot { padding-left: 14px; padding-right: 14px; }
  .mbld-issdlg .deck-action-banner { margin: 0 0 10px; flex-wrap: wrap; }
  .mbld-issdlg .detail-review-banner { margin: 0 0 10px; }
  .mbld-issdlg .deck-action-banner .app-glyph { width: 15px; height: 15px; }
  .mbld-issdlg .deck-action-banner .dab-text { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
  .mbld-issdlg .deck-action-banner .dab-actions { flex: 1 1 100%; justify-content: flex-end; margin-top: 6px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE DECK BUILDER — UNFOLDED (601-945px), mockup frames C / C2 / C3 / C3b.

   At ~844px there IS width for both panes, so this range deliberately KEEPS the
   desktop .bld-split side-by-side structure (unlike the folded ≤600px layout, which
   is a different DOM). Only the chrome is compacted. The compact filter bar is
   supplied by JS (_bldMountSearchFilterBar uses the mobile skin across the whole
   ≤945px range).
   ═══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 601px) and (max-width: 945px) {
  /* ── Header: same .bld-top row, tightened ─────────────────────────────── */
  .builder .bld-top { padding: 8px 12px; gap: 8px; }
  .builder .bld-title-block { gap: 4px; }
  .builder .bld-title { gap: 7px; }
  .builder .bld-title .bld-name { font-size: 15.5px; }
  .builder .bld-title .dh-ink-icon { width: 18px; height: 18px; }
  .builder .bld-done-wrap { flex: none; margin: 0; }
  /* Description hidden on a phone-sized screen — edit it via the ✎ pencil, exactly as
     the folded layout does. Reclaims the height the two panes need. */
  .builder .bld-description { display: none; }

  /* ── Split: deck pane pinned at 40% ───────────────────────────────────────
     A `1 1 62%` / `1 1 38%` pair would sum to 100% BEFORE the divider's width, so both
     panes shrink proportionally and the deck pane lands near ~25%. Pin the deck pane and
     let the search pane absorb whatever the divider leaves. */
  .builder .bld-split { flex-direction: row; }
  /* ⚠ Override ONLY flex-basis, never the `flex` shorthand. The desktop panes are
     `flex: 1 1 <basis>` with `transition: flex-basis .6s` — width == flex-basis, so the
     expand/collapse interpolates linearly. Replacing the shorthand (an earlier attempt used
     `flex: 1 1 auto` / `flex: 0 0 40%`) animated `auto` → `0` AND changed grow/shrink
     mid-transition, which is what made the unfolded animation look broken.
     Keeping just the basis also means the shipped `.bld-split.expanded` rules
     (`flex-basis: 0%` / `100%`, specificity 0,3,0) still win here — so expand/collapse
     animates exactly as it does on desktop, for free. */
  .builder .bld-search { flex-basis: 60%; min-width: 0; }
  .builder .bld-divider { display: flex; flex: none; }
  .builder .bld-deck { flex-basis: 40%; min-width: 0; display: flex; flex-direction: column; }
  .builder .bld-deck .deck-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 8px var(--gutter-content) 90px; }

  /* ── Search pane: 2 cards per row ─────────────────────────────────────────
     Was 3-up. On a real unfolded phone that left the +/− stepper and "＋ Add" captions
     too narrow, so the button text WRAPPED (Gus, 2026-07-20). Two per row gives the
     footer controls room to stay on one line.
     ⚠ Must be written as `#bld-search-grid .cards-grid`: the shipped base rule uses that
     ID (specificity 1,0,0), so a class-only selector like `.builder .bld-search .cards-grid`
     (0,3,0) LOSES no matter where it sits in the file.
     Scoped to the SEARCH grid only: the expanded deck pane keeps the real .cards-grid rule
     (a 2-column floor that grows with width) so C3b's Card-pictures view still reaches
     3-4 per row — an over-broad pin is what the mockup warns against. */
  #bld-search-grid .cards-grid { grid-template-columns: repeat(2, 1fr); }
  /* Location cards are LANDSCAPE (rotated 90°), so at the base
     `minmax(280px, 1fr)` they rendered one enormous card per row. Two per row matches the
     portrait grid's rhythm and keeps them a sane size. */
  #bld-search-grid .cards-grid-locations { grid-template-columns: repeat(2, 1fr); }
  .builder .bld-search-body .fbm-bar { margin: 0 0 8px; }
  .builder .bld-search-body .bld-rule-chip { margin: 0 0 8px; }
  .builder .pane-label .pl-text .t { font-size: 13px; }
  .builder .results-line { text-align: center; }

  /* ── Collapsed deck pane (C / C2): cost + name + stepper only ─────────────
     Drop the card-type icon AND the ink icon so each row is just cost + name + stepper —
     the same display:none technique the real CSS uses to drop .pill-ink-hex on a folded
     phone. Scoped to the COLLAPSED pane: the expanded pane keeps its type-section headers,
     so it loses nothing by this. */
  .builder .bld-split:not(.expanded) .bld-deck .dl-type-emoji,
  .builder .bld-split:not(.expanded) .bld-deck .pill-ink-hex { display: none; }

  /* Compact pill for the narrow deck pane — same proportional shrink as the folded
     sheet: 30→26px row, cost icon ≈85% of the new height, text 14→12.5px, ident 12→11px,
     stepper 26→20px so it still clears the 26px pill. */
  .builder .bld-deck .pill { font-size: 12.5px; min-height: 26px; }
  .builder .bld-deck .pill-cost-icon { height: 22px; }
  .builder .bld-deck .pill-ident { font-size: 11px; }
  .builder .bld-deck .dl-step button { width: 20px; height: 20px; border-radius: 6px; font-size: 13px; }
  .builder .bld-deck .dl-step .q { font-size: 12px; min-width: 14px; }
  .builder .bld-deck .pill-count { font-size: 11.5px; padding: 1px 7px; }
  .builder .bld-deck .pill.dl-pill { cursor: pointer; }

  /* ── Expanded pane result line (C3): centred, like the deck-view result line.
     The shipped mobile rule keys on the #detail-result-line id, so it cannot reach the
     builder's own #bld-exp-result-line. */
  .builder #bld-exp-result-line .cards-count { flex: 1 1 100%; text-align: center; }
  .builder #bld-exp-result-line { display: flex; flex-wrap: wrap; row-gap: 8px; align-items: center; }

  /* Collapsible stats card in the expanded pane (C3). The toggle itself is already
     styled by the shipped ≤945px block; these only space it inside the builder pane. */
  .builder .bld-deck .stats-card { display: flex; flex-direction: column; gap: 11px; }
  .builder .bld-deck .stats-card .stat-block { margin: 0; }

  /* ⚠ STACK the expanded pane. The desktop rule is
     `.bld-split.expanded .deck-review { grid-template-columns: 300px 1fr }` — a stats
     column beside the cards. At 844px that leaves the card grid only ~500px, which the
     real .cards-grid rule turns into TWO columns; the mockup's C3b note is explicit that
     this pane must reach 3-4 per row. Mockup frames C3/C3b stack instead: the collapsible
     stats card on top, full-width cards beneath. One column here gives the grid the whole
     pane (~740px after the 52px expanded gutter) → 3 per row, with NO override of
     .cards-grid itself (an over-broad pin is what the mockup warns against). */
  .bld-split.expanded .deck-review { grid-template-columns: minmax(0, 1fr); gap: 12px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Edit-deck-info dialog — master→detail across the FULL mobile range.

   The shipped master→detail rules live in `@media (max-width: 700px)` because that
   block is shared with Account Settings. The deck builder wants the same pattern for
   its whole mobile range (≤945px — Gus, 2026-07-19), so the 701-945px slice is
   re-declared here scoped to `.edit-ill--builder` ONLY. Account Settings keeps its
   700px breakpoint untouched.

   Deliberate duplication: the alternative (moving the shared breakpoint to 945px) would
   change the Account Settings dialog between 700-945px, which is outside this task.
   ⚠ If the ≤700px block above is edited, mirror the change here.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 701px) and (max-width: 945px) {
  .bld-modal.bld-modal--settings.edit-ill--builder {
    position: fixed; inset: 0;
    width: auto; height: auto; max-width: none;
    border: none; border-radius: 0; box-shadow: none;
  }
  .bld-modal--settings.edit-ill--builder .modal-head.edit-ill-head-d { display: none; }
  .bld-modal--settings.edit-ill--builder .edit-ill-m-titlebar {
    display: flex; position: relative; flex-shrink: 0;
    align-items: center; justify-content: center;
    padding: 14px 16px; border-bottom: 1px solid var(--border);
  }
  .edit-ill--builder .edit-ill-m-apptitle {
    font-family: var(--font-display); font-size: var(--fz-lg);
    letter-spacing: var(--ls-farmiles); color: var(--gold-light);
  }
  .edit-ill--builder .edit-ill-m-titlebar .edit-ill-m-x {
    position: absolute; top: 50%; right: 12px; transform: translateY(-50%); font-size: 22px;
  }
  .edit-ill--builder .edit-ill-m-backbar { flex-shrink: 0; cursor: pointer; }
  .bld-modal--settings.edit-ill--builder.detail .edit-ill-m-backbar { display: flex; }
  /* Track: two full-width screens that slide.
     ⚠ Expressed in vw, NOT the % the ≤700px block uses. With `width: 200%` on this flex
     item, `translateX(-50%)` computed to a MATRIX OF ZERO here — verified on staging:
     `translateX(-100px)` and `scale(.5)` both applied to the same element, and
     `margin-left:-844px` moved it, so only the PERCENTAGE failed to resolve. Since this
     dialog is a full-viewport cover (position:fixed; inset:0) at mobile widths, the track
     is exactly two viewports wide and the slide is exactly one — so vw is both correct
     and immune to whatever the percentage was resolving against.
     The ≤700px block is left alone: its % form demonstrably works there, and it is shared
     with Account Settings. */
  .edit-ill--builder .edit-ill-body { width: 200vw; transform: translateX(0); transition: transform .28s ease; }
  .bld-modal--settings.edit-ill--builder.detail .edit-ill-body { transform: translateX(-100vw); }
  .edit-ill--builder .edit-ill-nav { width: 50%; border-right: none; padding: 6px 0; }
  .edit-ill--builder .edit-ill-content { width: 50%; }
  .edit-ill--builder .edit-ill-nav-item {
    display: flex; align-items: center; justify-content: space-between;
    padding: 16px 20px; font-size: 16px; border-left: none;
    border-bottom: 1px solid var(--border); color: var(--text-muted);
  }
  .edit-ill--builder .edit-ill-nav-item::after { content: '›'; color: var(--text-muted); font-size: 20px; }
  .edit-ill--builder .edit-ill-nav-item.active { background: none; font-weight: 400; }
  .edit-ill--builder .edit-ill-panel--fill .edit-ill-fill-foot {
    padding-bottom: calc(14px + env(safe-area-inset-bottom));
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE BUILDER — fixes from real-device testing on a Pixel (Gus, 2026-07-19).
   Emulated widths in DevTools cannot reproduce #1 below, which is why it survived
   every earlier verification pass.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 945px) {
  /* ── 1. ⭐ THE BOTTOM BAND COULD SIT BELOW THE FOLD ───────────────────────
     `.builder-overlay` is sized with `100vh`. On a mobile browser `vh` is the LARGE
     viewport — it deliberately ignores the retractable URL bar — so the builder was
     taller than the visible area and its last band (.mbld-deckbar, the ONLY way to
     open the deck) was pushed under the browser/system chrome. It could be scrolled
     into view by dragging the page itself, which is why it looked "missing" rather
     than merely clipped.

     `dvh` tracks the *dynamic* viewport, so the builder always fits what the user can
     actually see. Mobile-scoped: desktop keeps `vh` (no retractable chrome there, and
     the desktop builder must not change).

     This also explains why the ▲ chevron "did nothing": the sheet is `inset:0` of the
     builder, so it opened into the same off-screen overflow. */
  .builder-overlay {
    height: calc(100dvh - var(--nav-h) - var(--bottom-bar-h) - var(--tabs-h));
  }
  /* ⚠ min-height MUST be converted too, not just height. `.mycollection-page` also carries
     `.page`, whose `min-height: calc(100vh - …)` is LARGER than the dvh height whenever the
     browser's address bar is showing — and min-height beats height, so the page stayed at
     the LARGE-viewport size, overflowed, and the deck bar sat under the browser chrome
     until the user scrolled the page. That is exactly the residual symptom Gus saw: bar
     hidden with the address bar visible, bar fine once Chrome hid it.
     Invisible in DevTools emulation, where vh and dvh are identical. */
  .mycollection-page:has(.builder-overlay) {
    height: calc(100dvh - var(--nav-h) - var(--bottom-bar-h));
    min-height: calc(100dvh - var(--nav-h) - var(--bottom-bar-h));
  }
  /* ⛔ Do NOT use `overflow: hidden` here. It was tried and made things worse: it does not
     shorten the document, it just FREEZES whatever scroll offset already exists — so the
     builder stayed scrolled with its title stuck under the fixed top nav and no way to
     scroll back up.

     The document is taller than the dynamic viewport because THREE separate constraints
     are expressed in `vh`, and the tallest one wins. All three must move to `dvh` together:
       body      min-height: 100vh                                   (inkcannon.css:348)
       #app-root min-height: 100vh + nav/bottom-bar padding          (inkcannon.css:1197)
       .page     min-height: calc(100vh - nav - bottom-bar)          (inkcannon.css:1217)
     Leaving #app-root on `100vh` while the page shrank to `dvh` is what produced the empty
     band below the deck bar — app-root kept reserving large-viewport height the page no
     longer filled.

     With all of them on `dvh` the document is EXACTLY the visible viewport, so nothing
     needs to scroll and nothing has to be frozen. */
  body.bld-mobile-builder { min-height: 100dvh; }
  body.bld-mobile-builder #app-root { min-height: 100dvh; }

  /* ── 2. Scroll-To-Top centred on the pane that scrolls ────────────────────
     --stt-left is the measured centre of the active scroll pane, published by
     _bldSetScrollTopCentre. It replaces the desktop `.align-left-pane` offset, which is
     computed from the desktop 68/32 split and therefore lands off-centre on a phone.
     The `.visible` variant is included because the desktop rule sets `left` on both
     states, and translateX(-50%) is what actually centres the button on that point. */
  body.bld-mobile-builder .scroll-to-top-btn,
  body.bld-mobile-builder .scroll-to-top-btn.visible {
    left: var(--stt-left, 50%);
  }
}

@media (max-width: 600px) {
  /* ── 3. "Show deck" / "Hide deck" sized to the breadcrumb ─────────────────
     Matched to the mobile breadcrumb crumb (18px, HF Farmiles) so the deck bar reads as
     primary navigation rather than a caption. Was 14px, which measured small on a real
     phone. Declared after the ≤600 block that sets 14px, so it wins on source order. */
  .mbld-deckbar .db-showdeck,
  .mbld-sheet .sheet-grip .db-showdeck {
    font-size: 18px;
  }
  /* Keep the chevron proportional to the now-larger label. */
  .mbld-deckbar .db-chev,
  .mbld-sheet .sheet-grip .db-chev {
    width: 34px;
    height: 34px;
    font-size: 15px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CREATE-DECK WIZARD — FOLDED phone (≤600px). Implements MOCKUP-mobile-deck-wizard.html.

   The wizard keeps its flow (4 sequential steps, Back / Next, Create deck) and its real
   `.wiz-pane` bodies; only the CHROME changes: a full-screen cover with a Farmiles title
   bar, a dot-only step track, and a 2-button footer.

   Every rule is inside the media query — the unfolded (601-945px) and desktop wizard are
   untouched. The `.wizard--folded` class is emitted only by the folded branch of
   _wizShellHTML, so these can never reach the wide chrome even by accident.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 600px) {
  /* The backdrop keeps its .is-open fade + z-index; it just stops centring a 600px box. */
  .wizard-bg:has(.wizard--folded) {
    padding: 0;
    overflow: hidden;
  }

  /* Full-screen cover. `inset: 0` (NOT height:100%) so the fixed cover ignores the
     overlay's centring — the same construction the Account-Settings mobile dialog uses.
     The fantasy window border/radius is dropped: at 332px a framed box inside a backdrop
     reads as cramped, and every other full-screen mobile surface is edge-to-edge. */
  .wizard.wizard--folded {
    position: fixed;
    inset: 0;
    width: auto;
    max-width: none;
    border: none;
    border-image: none;
    border-radius: 0;
    box-shadow: none;
    background: var(--bg-void);
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  /* Title bar — mirrors .edit-ill-m-titlebar so the two mobile dialogs read identically. */
  .wizard--folded .mwiz-titlebar {
    flex: none;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 14px 16px;
    border-bottom: 1px solid var(--border);
    background: var(--bg-surface);
  }
  .wizard--folded .mwiz-apptitle {
    font-family: var(--font-display);
    font-size: var(--fz-lg);
    letter-spacing: var(--ls-farmiles);
    color: var(--gold-light);
  }
  .wizard--folded .mwiz-x {
    position: absolute;
    top: 50%;
    right: 12px;
    transform: translateY(-50%);
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
  }

  /* Step track — dots only, centred. */
  .wizard--folded .mwiz-steps {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 12px 16px 0;
  }
  /* ⚠ flex:none is what CENTRES the track. The base .wiz-step is `flex: 1`, so each step
     takes an equal share of the row and its dot sits at the START of that cell — the
     trailing cell's empty space then pushes the whole track visibly left of centre
     (spotted in review). Sizing each step to its dot lets justify-content do its job.
     font-size:0 hides the label text node that the desktop chrome renders; the folded
     markup emits no label, so this is belt-and-braces. */
  .wizard--folded .mwiz-steps .wiz-step {
    flex: none;
    padding: 0;
    border: none;
    background: none;
    font-size: 0;
  }
  /* Connector bars turn four separate chips into one progress track. */
  .wizard--folded .mwiz-steps .mwiz-bar {
    flex: 0 1 26px;
    height: 2px;
    background: var(--border);
    border-radius: 2px;
  }
  .wizard--folded .mwiz-steps .mwiz-bar.done { background: var(--emerald); }
  /* Carries the count the dropped step labels used to convey. */
  .wizard--folded .mwiz-count {
    flex: none;
    text-align: center;
    font-size: 11.5px;
    color: var(--text-dim);
    padding: 6px 16px 0;
    letter-spacing: .04em;
  }

  /* The real panes, in the cover. This band is the only scroller — the title bar, step
     track and footer stay put, so Next/Back are always reachable with the keyboard up. */
  .wizard--folded .wiz-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 18px var(--gutter-content) 22px;
  }
  .wizard--folded .wiz-pane h3 { font-size: 16px; }
  .wizard--folded .wiz-pane .sub { font-size: 12.5px; margin-bottom: 14px; }
  .wizard--folded .ink-pick { grid-template-columns: repeat(3, 1fr); gap: 8px; }
  .wizard--folded .ink-opt { padding: 10px 4px; font-size: 12px; }
  .wizard--folded .ink-opt-icon { width: 26px; height: 26px; }
  .wizard--folded .radio-card { margin-bottom: 8px; }
  .wizard--folded .wiz-field .inp,
  .wizard--folded .wiz-field textarea { font-size: 14px; }

  /* Footer — Back / Next only (Cancel is the title-bar ✕). */
  .wizard--folded .mwiz-foot {
    flex: none;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px var(--gutter-content);
    border-top: 1px solid var(--border);
    background: var(--bg-surface);
  }
  .wizard--folded .mwiz-foot .spacer { flex: 1; }
}
