/* Collection page — light theme inspired by the Ako Mātātupu site
   (Manrope + Gabarito, green primary #009572, rounded pills, white nav).
*/

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Embedded-modal mode — the picture editor loads this page in a
   transparent iframe with ?modal=…&embed=1 so a single modal can
   show in-place over the editor. Hide every direct child of <body>
   except modals, and clear the page bg so the editor behind shows
   through. The modal's own backdrop continues to dim things. */
html[data-embedded-modal="1"],
html[data-embedded-modal="1"] body {
  background: transparent;
}
html[data-embedded-modal="1"] body > *:not(.modal):not(#context-menu) {
  display: none !important;
}

/* ---- Embedded-modal iframe overlay --------------------------------
   Used by /3d (BoxCraft) and /edit (BoxPerfect) to host BoxArts
   modals (Settings, Account, Spine Designer) in-place rather than
   navigating away. The iframe loads /?modal=…&embed=1, which sets
   data-embedded-modal on the inner document to hide page chrome,
   so only the modal's backdrop + panel paint through.
   Same rules as styles.css's copy on the BP side. */
.modal-iframe-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
}
.modal-iframe-overlay.hidden { display: none; }
.modal-iframe-overlay iframe {
  width: 100%;
  height: 100%;
  border: 0;
  background: transparent;
}

:root {
  --green: #009572;
  --green-hover: #007d61;
  --green-tint: #d6ede7;

  --white: #ffffff;
  --off:   #f7f8f6;
  --light: #eef2ee;
  --border:#e2e8e2;
  --text:  #111111;
  --mid:   #4a5048;
  --muted: #7a847a;

  /* Foreground colour for content sitting on a hardcoded dark overlay
   * (e.g. the `rgba(0,0,0,0.6)` pills used for gallery captions, image
   * names, and inline rename fields). Distinct from `--white`, which
   * themes redefine as the *panel surface* — using `--white` for text
   * on these pills made the rename input read as dark-on-dark in
   * Steamy / Goodish / Monkey. Each theme tunes this to a light tone
   * that complements its palette while staying readable. */
  --on-dark: #ffffff;

  --danger: #b03a4a;
  --danger-tint: #fce4e8;

  --radius: 12px;
  --radius-lg: 18px;
  --radius-pill: 24px;

  --shadow-card: 0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06);
  --shadow-hover: 0 6px 24px rgba(0,0,0,0.10), 0 2px 4px rgba(0,0,0,0.06);
  --shadow-modal: 0 30px 60px rgba(0,0,0,0.20);

  --tile-size: 180px;

  /* Fonts as CSS vars so themes can swap them. */
  --font-body:    'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Gabarito', sans-serif;
}

/* ---- Theme variants -------------------------------------------------------
   Themes are applied by setting data-theme on <html>. Each block re-declares
   the palette + font vars; the rest of the stylesheet uses the vars.
*/

/* "BoxLight" (user-facing label; internal slug "boxperfect") — the
   default BoxArts theme. Inherits the full :root palette unchanged;
   only the header band is overridden so Game Title art reads in the
   nav, same pattern as the BoxArts light themes. The dark-cyan-green
   header keeps the original #009572 brand family. */
[data-theme="boxperfect"] {
  --header-bg:    #0d2e26;
  --header-fg:    #f4f1ea;
  --header-muted: #8aa39d;
  --header-border: rgba(255, 255, 255, 0.08);
}

/* "Steamy" — Steam-store-inspired: dark navy panels, light-blue accents. */
[data-theme="steamy"] {
  --green:       #66c0f4;
  --green-hover: #a4d8ff;
  --green-tint:  #2a4860;
  --white:       #2a475e;
  --off:         #1b2838;
  --light:       #16202d;
  --border:      #3c5874;
  --text:        #c7d5e0;
  --mid:         #8b9aa6;
  --muted:       #67707b;
  --danger:      #d94e5d;
  --danger-tint: #44232a;
  --on-dark:     #c7d5e0;          /* pale Steam blue-grey — the body fg */
  --font-body:    'Inter', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Inter', sans-serif;
  color-scheme: dark;
}

/* "Goodish Games" — GOG Galaxy-inspired: near-black backgrounds with cool
   purple undertones, vivid magenta-purple accent, Roboto typeface. */
[data-theme="goodish"] {
  --green:       #b85eff;
  --green-hover: #cf80ff;
  --green-tint:  #2d2040;
  --white:       #23202c;          /* card / panel surface */
  --off:         #14121a;          /* page background — near-black, cool */
  --light:       #1c1924;          /* subtle filled fields */
  --border:      #2e2a3a;          /* low-contrast divider */
  --text:        #e2e0e8;
  --mid:         #9b97ad;
  --muted:       #6a6679;
  --danger:      #ff5e84;
  --danger-tint: #3d1f2a;
  --on-dark:     #e2e0e8;          /* GOG body text, also reads on overlays */
  --font-body:    'Roboto', system-ui, sans-serif;
  --font-display: 'Roboto', sans-serif;
  color-scheme: dark;
}

/* ---- BoxArts evaluation themes -----------------------------------------
   Three additive palette themes from the BoxArts brand brief — sit
   alongside the existing themes (steamy / boxperfect / goodish) and
   exist so the user can A/B their preferred accent direction. None
   of them touches the wordmark or glyph; those stay #000/#fff via
   the .bxa-wordmark and .bxa-lockup rules further down.

   Each new theme shares the same warm-cream paper + ink palette so
   the comparison is honest — only the accent shifts. Mapped to the
   existing custom-property names (--green, --green-hover, --green-
   tint, --white, --off, --light, --border, --text, --mid, --muted,
   --on-dark) so the rest of the stylesheet picks them up without
   change. */

/* Theme A — refined evolution of the original #009572 green. Forest
   green over warm cream, replaces the lighter / SaaS-y default.
   Header band is intentionally a dark forest tint regardless of
   surface lightness so Game Title art (which the user typically
   prepares as a near-white knockout on transparent) reads cleanly
   in the page nav — a white-on-white header eats it. */
[data-theme="boxarts-green"] {
  --green:       #2b6f5a;
  --green-hover: #3a8a72;
  --green-tint:  #cfe0d8;
  --white:       #ffffff;
  --off:         #f4f1ea;
  --light:       #ebe6dc;
  --border:      #d4cdbf;
  --text:        #1a1814;
  --mid:         #4a443c;
  --muted:       #7a7468;
  --danger:      #b03a4a;
  --danger-tint: #f4d6dc;
  --on-dark:     #ffffff;
  --accent-2:    #c89a3c;          /* gilt — secondary accent for header hovers */
  --header-bg:    #1a3a30;         /* deep forest — always dark */
  --header-fg:    #f4f1ea;
  --header-muted: #a89e90;
  --header-border: rgba(255, 255, 255, 0.08);
  --font-body:    'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Gabarito', sans-serif;
}

/* Theme B — library / archive. Oxblood primary with gilt secondary
   accent (rare — used for dividers, hovers, accent details). */
[data-theme="boxarts-oxblood"] {
  --green:       #7a2828;
  --green-hover: #9a3838;
  --green-tint:  #e8d4d4;
  --white:       #ffffff;
  --off:         #f4f1ea;
  --light:       #e8dfd0;
  --border:      #d4cdbf;
  --text:        #231a14;
  --mid:         #4a443c;
  --muted:       #7a7468;
  --danger:      #a02828;
  --danger-tint: #f3d8d4;
  --on-dark:     #ffffff;
  --accent-2:    #c89a3c;          /* gilt — secondary accent */
  --header-bg:    #2a1410;         /* deep oxblood — always dark */
  --header-fg:    #f4f1ea;
  --header-muted: #b0a08c;
  --header-border: rgba(255, 255, 255, 0.08);
  --font-body:    'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Gabarito', sans-serif;
}

/* Theme C — cabinet / dark library. Cool teal primary with brass
   secondary accent. */
[data-theme="boxarts-teal"] {
  --green:       #1f4a4d;
  --green-hover: #2c6a6d;
  --green-tint:  #d4e0e1;
  --white:       #ffffff;
  --off:         #f4f1ea;
  --light:       #e8e1d4;
  --border:      #c8c0b0;
  --text:        #0f1a1c;
  --mid:         #4a5658;
  --muted:       #7a8284;
  --danger:      #a02828;
  --danger-tint: #f3d8d4;
  --on-dark:     #ffffff;
  --accent-2:    #d6a85a;          /* brass — secondary accent */
  --header-bg:    #112a2d;         /* deep cabinet teal — always dark */
  --header-fg:    #f4f1ea;
  --header-muted: #a0a8a8;
  --header-border: rgba(255, 255, 255, 0.08);
  --font-body:    'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Gabarito', sans-serif;
}

/* ---- BoxArts dark pairings -------------------------------------------
   Standalone dark companions for each BoxArts theme, lifted from the
   brand brief's "Dark-mode pairing" tables. Independent themes (no
   prefers-color-scheme magic) so the user can A/B them directly in
   the picker. Same accent colours as the light variants; only the
   paper / panel / ink shift to dark. */

/* Theme A · Dark — forest green on warm near-black */
[data-theme="boxarts-green-dark"] {
  --green:       #2b6f5a;
  --green-hover: #3a8a72;
  --green-tint:  #1f3a30;
  --white:       #3a342b;
  --off:         #1a1814;
  --light:       #2a2620;
  --border:      #3a342b;
  --text:        #f4f1ea;
  --mid:         #a89e90;
  --muted:       #7c736a;
  --danger:      #d94e5d;
  --danger-tint: #3a2024;
  --on-dark:     #f4f1ea;
  --accent-2:    #c89a3c;          /* gilt — secondary accent */
  --header-bg:    #1a1814;         /* match dark theme body so the nav reads as
                                       a continuous dark surface; slightly darker
                                       than --white (the panel/card colour) so it
                                       still separates from the cards below. */
  --header-fg:    #f4f1ea;
  --header-muted: #a89e90;
  --header-border: rgba(255, 255, 255, 0.06);
  --font-body:    'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Gabarito', sans-serif;
  color-scheme: dark;
}

/* Theme B · Dark — oxblood + gilt on warm near-black */
[data-theme="boxarts-oxblood-dark"] {
  --green:       #7a2828;
  --green-hover: #9a3838;
  --green-tint:  #3d2020;
  --white:       #4a3826;
  --off:         #231a14;
  --light:       #3a2c1f;
  --border:      #3a2c1f;
  --text:        #f4f1ea;
  --mid:         #b0a08c;
  --muted:       #847766;
  --danger:      #d94e5d;
  --danger-tint: #3a2024;
  --on-dark:     #f4f1ea;
  --accent-2:    #c89a3c;          /* gilt — secondary accent */
  --header-bg:    #231a14;         /* match dark body — see green-dark comment */
  --header-fg:    #f4f1ea;
  --header-muted: #b0a08c;
  --header-border: rgba(255, 255, 255, 0.06);
  --font-body:    'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Gabarito', sans-serif;
  color-scheme: dark;
}

/* Theme C · Dark — cabinet teal + brass on cool near-black */
[data-theme="boxarts-teal-dark"] {
  --green:       #1f4a4d;
  --green-hover: #2c6a6d;
  --green-tint:  #152e30;
  --white:       #243033;
  --off:         #0f1a1c;
  --light:       #1a2628;
  --border:      #1a2628;
  --text:        #f4f1ea;
  --mid:         #a0a8a8;
  --muted:       #767e7e;
  --danger:      #d94e5d;
  --danger-tint: #3a2024;
  --on-dark:     #f4f1ea;
  --accent-2:    #d6a85a;          /* brass — secondary accent */
  --header-bg:    #0f1a1c;         /* match dark body — see green-dark comment */
  --header-fg:    #f4f1ea;
  --header-muted: #a0a8a8;
  --header-border: rgba(255, 255, 255, 0.06);
  --font-body:    'Manrope', system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: 'Gabarito', sans-serif;
  color-scheme: dark;
}

/* ---- Dark-mode accent text contrast --------------------------------
   The three BoxArts dark themes intentionally keep the same dark
   accent as their light siblings (per the brief). That works fine
   for accent fills (white text on #2b6f5a, #7a2828, #1f4a4d all
   reads), but where the accent is used as TEXT (badges, active
   tabs, hover pills) it ends up dark-on-dark.

   `--green-fg` adds a brighter accent specifically for text on
   non-accent surfaces. Light themes leave it equal to --green
   (no behaviour change); dark themes brighten it from the
   theme's own palette — the brief's secondary accent for oxblood
   + teal (gilt / brass) which were already designed to be the
   high-contrast partner, and a lightened tint of the primary
   green for green-dark since the brief didn't give it a
   secondary. */
:root,
[data-theme="steamy"],
[data-theme="goodish"],
[data-theme="boxarts-green"],
[data-theme="boxarts-oxblood"],
[data-theme="boxarts-teal"] {
  --green-fg: var(--green);
}
[data-theme="boxarts-green-dark"]   { --green-fg: #7fbfa5; }  /* sage */
[data-theme="boxarts-oxblood-dark"] { --green-fg: #c89a3c; }  /* gilt */
[data-theme="boxarts-teal-dark"]    { --green-fg: #d6a85a; }  /* brass */

/* Apply the brighter accent text only inside the dark themes —
   light themes are left alone. Scoped to elements that use the
   accent as TEXT colour on a non-accent surface (badges, active
   tabs, hover-state pills). Buttons that fill with the accent
   keep white-on-accent contrast and are unaffected. */
[data-theme="boxarts-green-dark"] .game-count strong,
[data-theme="boxarts-oxblood-dark"] .game-count strong,
[data-theme="boxarts-teal-dark"] .game-count strong,
[data-theme="boxarts-green-dark"] .play-chip,
[data-theme="boxarts-oxblood-dark"] .play-chip,
[data-theme="boxarts-teal-dark"] .play-chip,
[data-theme="boxarts-green-dark"] .play-chip[aria-pressed="true"],
[data-theme="boxarts-oxblood-dark"] .play-chip[aria-pressed="true"],
[data-theme="boxarts-teal-dark"] .play-chip[aria-pressed="true"],
[data-theme="boxarts-green-dark"] .game-row [data-action]:hover,
[data-theme="boxarts-oxblood-dark"] .game-row [data-action]:hover,
[data-theme="boxarts-teal-dark"] .game-row [data-action]:hover,
[data-theme="boxarts-green-dark"] .game-row .row-link:hover,
[data-theme="boxarts-oxblood-dark"] .game-row .row-link:hover,
[data-theme="boxarts-teal-dark"] .game-row .row-link:hover,
[data-theme="boxarts-green-dark"] .row-play,
[data-theme="boxarts-oxblood-dark"] .row-play,
[data-theme="boxarts-teal-dark"] .row-play,
[data-theme="boxarts-green-dark"] .row-card:hover,
[data-theme="boxarts-oxblood-dark"] .row-card:hover,
[data-theme="boxarts-teal-dark"] .row-card:hover,
[data-theme="boxarts-green-dark"] .modal-tabs button[aria-selected="true"],
[data-theme="boxarts-oxblood-dark"] .modal-tabs button[aria-selected="true"],
[data-theme="boxarts-teal-dark"] .modal-tabs button[aria-selected="true"],
[data-theme="boxarts-green-dark"] .slip-align-selected.has-selection,
[data-theme="boxarts-oxblood-dark"] .slip-align-selected.has-selection,
[data-theme="boxarts-teal-dark"] .slip-align-selected.has-selection {
  color: var(--green-fg);
}

/* `.modal-tabs button[aria-selected="true"]` also sets the
   underline via border-bottom-color; sync that to the brighter
   accent so the underline matches the new text colour. */
[data-theme="boxarts-green-dark"] .modal-tabs button[aria-selected="true"],
[data-theme="boxarts-oxblood-dark"] .modal-tabs button[aria-selected="true"],
[data-theme="boxarts-teal-dark"] .modal-tabs button[aria-selected="true"] {
  border-bottom-color: var(--green-fg);
}

/* ---- BoxArts wordmark + glyph (theme-independent) -----------------------
   The wordmark and glyph stay #000 (or #fff on dark surfaces)
   across EVERY theme. No accent colour bleeds in. Themes change
   everything around them; the wordmark is the constant. */
.bxa-wordmark {
  font-family: 'Gabarito', system-ui, sans-serif;
  font-weight: 800;
  font-size: var(--bxa-wordmark-size, 1.25rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: #000;
  white-space: nowrap;
}
.bxa-wordmark__box {
  display: inline-block;
}
.bxa-wordmark__arts {
  display: inline-block;
  font-weight: 800;
  background: #000;
  color: #fff;
  padding: 0.05em 0.32em 0.12em;
  margin-left: 0.06em;
  border-radius: 0.1em;
}
.bxa-lockup {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  color: #000;
}
.bxa-lockup__glyph {
  display: inline-flex;
  width: 1.4em;
  height: 1.4em;
}
.bxa-lockup__glyph svg {
  width: 100%;
  height: 100%;
}
/* Anchor wrapping the glyph (back-to-BoxArts cross-nav on /3d).
   Inherits the lockup's currentColor so the SVG doesn't pick up
   the browser-default link blue; small hover dim hints
   interactivity without disturbing the lockup's static look. */
.bxa-lockup__glyph-link {
  display: inline-flex;
  color: inherit;
  text-decoration: none;
  border-radius: 4px;
}
.bxa-lockup__glyph-link:hover .bxa-lockup__glyph { opacity: 0.78; }
.bxa-lockup__glyph-link:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}
/* Dark-surface inversion — invoked by themes whose body bg is dark
   (Steamy / Goodish today). The lockup + wordmark flip to white;
   the inverted-Arts panel flips to white-on-black-text. */
[data-theme="steamy"] .bxa-lockup,
[data-theme="goodish"] .bxa-lockup,
[data-theme="boxarts-green-dark"] .bxa-lockup,
[data-theme="boxarts-oxblood-dark"] .bxa-lockup,
[data-theme="boxarts-teal-dark"] .bxa-lockup,
/* The 3 light BoxArts themes have dark headers but light bodies,
   so the wordmark inverts ONLY when it sits inside .main-nav.
   Outside the nav (e.g. on grid cards) the wordmark keeps its
   normal dark-on-light treatment. */
[data-theme="boxperfect"] .main-nav .bxa-lockup,
[data-theme="boxarts-green"] .main-nav .bxa-lockup,
[data-theme="boxarts-oxblood"] .main-nav .bxa-lockup,
[data-theme="boxarts-teal"] .main-nav .bxa-lockup,
.bxa--on-dark .bxa-lockup {
  color: #fff;
}
[data-theme="steamy"] .bxa-wordmark,
[data-theme="goodish"] .bxa-wordmark,
[data-theme="boxarts-green-dark"] .bxa-wordmark,
[data-theme="boxarts-oxblood-dark"] .bxa-wordmark,
[data-theme="boxarts-teal-dark"] .bxa-wordmark,
[data-theme="boxperfect"] .main-nav .bxa-wordmark,
[data-theme="boxarts-green"] .main-nav .bxa-wordmark,
[data-theme="boxarts-oxblood"] .main-nav .bxa-wordmark,
[data-theme="boxarts-teal"] .main-nav .bxa-wordmark,
.bxa--on-dark .bxa-wordmark {
  color: #fff;
}
[data-theme="steamy"] .bxa-wordmark__arts,
[data-theme="goodish"] .bxa-wordmark__arts,
[data-theme="boxarts-green-dark"] .bxa-wordmark__arts,
[data-theme="boxarts-oxblood-dark"] .bxa-wordmark__arts,
[data-theme="boxarts-teal-dark"] .bxa-wordmark__arts,
[data-theme="boxperfect"] .main-nav .bxa-wordmark__arts,
[data-theme="boxarts-green"] .main-nav .bxa-wordmark__arts,
[data-theme="boxarts-oxblood"] .main-nav .bxa-wordmark__arts,
[data-theme="boxarts-teal"] .main-nav .bxa-wordmark__arts,
.bxa--on-dark .bxa-wordmark__arts {
  background: #fff;
  color: #000;
}

/* ---- Sub-brand wordmark variants -----------------------------------
   Both variants share BoxArts' construction; only the inner block's
   colour + typography change. The block colours are identity
   constants (WordPerfect blue / warm bronze) and DO NOT shift with
   the active theme. Only the outer "Box" half adapts to surface
   brightness via the existing .bxa-wordmark__box rule. */

/* BoxPerfect — picture editor sub-brand. */
.bxa-wordmark--perfect .bxa-wordmark__perfect {
  display: inline-block;
  font-family: 'Gabarito', system-ui, sans-serif;
  font-weight: 800;
  background: #1a2a8a;
  color: #fff;
  padding: 0.05em 0.32em 0.12em;
  margin-left: 0.06em;
  border-radius: 0.1em;
}

/* BoxCraft — 3D Builder sub-brand. Cinzel's metrics sit slightly
   lower than Gabarito's, so a -0.02em top nudge re-aligns the
   baselines optically. Adjust by ±0.01em if it reads off at the
   sizes the masthead + splash use. */
.bxa-wordmark--craft .bxa-wordmark__craft {
  display: inline-block;
  font-family: 'Cinzel', serif;
  font-weight: 800;
  background: #8b6028;
  color: #f4f1ea;
  padding: 0.08em 0.32em 0.14em;
  margin-left: 0.06em;
  border-radius: 0.1em;
  letter-spacing: 0.03em;
  position: relative;
  top: -0.02em;
}

/* On dark surfaces the inner blocks stay the brand colour; only
   the outer "Box" flips. The base .bxa-wordmark__box already
   handles that — these rules just make sure the inversion is
   wired for the new variants too. */
[data-theme="steamy"] .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="goodish"] .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="boxarts-green-dark"] .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="boxarts-oxblood-dark"] .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="boxarts-teal-dark"] .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="boxperfect"] .main-nav .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="boxarts-green"] .main-nav .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="boxarts-oxblood"] .main-nav .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="boxarts-teal"] .main-nav .bxa-wordmark--perfect .bxa-wordmark__box,
[data-theme="steamy"] .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="goodish"] .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="boxarts-green-dark"] .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="boxarts-oxblood-dark"] .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="boxarts-teal-dark"] .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="boxperfect"] .main-nav .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="boxarts-green"] .main-nav .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="boxarts-oxblood"] .main-nav .bxa-wordmark--craft .bxa-wordmark__box,
[data-theme="boxarts-teal"] .main-nav .bxa-wordmark--craft .bxa-wordmark__box,
.bxa--on-dark .bxa-wordmark--perfect .bxa-wordmark__box,
.bxa--on-dark .bxa-wordmark--craft .bxa-wordmark__box {
  color: #fff;
}

/* ---- Nav-position wordmark modifier -------------------------------
   For nav/menu placements (NOT the main page brand on the left),
   the wordmark renders smaller and monochrome — no coloured pill
   behind the inner block, font follows the surrounding text colour.
   This reserves the full-colour treatment for the page's primary
   brand on the top-left (so the active view's identity is clear)
   and demotes secondary brand references to a quieter B&W form.
   Stack on top of `.bxa-wordmark--perfect` or `--craft` to keep
   the typography distinct (BoxPerfect = Gabarito, BoxCraft = Cinzel)
   while dropping the colour cue. */
.bxa-wordmark--nav {
  font-size: 0.9em;
}
.bxa-wordmark--nav .bxa-wordmark__arts,
.bxa-wordmark--nav .bxa-wordmark__perfect,
.bxa-wordmark--nav .bxa-wordmark__craft {
  background: transparent;
  color: inherit;
  padding-left: 0.1em;
  padding-right: 0.1em;
}
/* Nav buttons hosting wordmarks: strip the green/ghost pill so the
   wordmark itself reads as the affordance. Outline-only hover keeps
   the click target obvious. `color: var(--text)` is the body text
   colour — theme-aware, so dark themes get a light readable
   wordmark and light themes get a dark one. (Was `inherit` which
   bottomed out at .btn-ghost's hardcoded `--mid` grey — invisible
   on dark themes.) */
.btn.btn-wordmark,
.btn-green.btn-wordmark,
.btn-ghost.btn-wordmark {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text);
  padding: 4px 10px;
  box-shadow: none;
  transform: none;
}
.btn.btn-wordmark:hover,
.btn-green.btn-wordmark:hover,
.btn-ghost.btn-wordmark:hover {
  background: transparent;
  border-color: currentColor;
  color: var(--text);
  transform: none;
}

/* ---- Splash screens ------------------------------------------------
   Brief animated overlays shown on first entry into the Picture
   Editor (BoxPerfect) or 3D Builder (BoxCraft) views in a session.
   The session-flag check lives in JS (sessionStorage); this CSS
   just defines the animation choreography. Mapped --color-paper /
   --color-ink-soft to this stylesheet's existing var names so the
   splash background follows the active theme. */
.bxa-splash {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--off, #f4f1ea);
  animation: bxa-splash-out 0.4s ease-in forwards;
  animation-delay: 0.8s;
  pointer-events: none;
}
.bxa-splash.hidden { display: none; }
.bxa-splash__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.2em;
  animation: bxa-splash-in 0.5s ease-out;
}
.bxa-splash__lockup {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  color: #000;
  font-size: 3.2rem;
}
[data-theme="steamy"] .bxa-splash__lockup,
[data-theme="goodish"] .bxa-splash__lockup,
[data-theme="boxarts-green-dark"] .bxa-splash__lockup,
[data-theme="boxarts-oxblood-dark"] .bxa-splash__lockup,
[data-theme="boxarts-teal-dark"] .bxa-splash__lockup,
.bxa--on-dark .bxa-splash__lockup {
  color: #fff;
}
.bxa-splash__lockup .bxa-lockup__glyph {
  width: 1.2em;
  height: 1.2em;
}
.bxa-splash__lockup .bxa-lockup__glyph svg {
  width: 100%;
  height: 100%;
}
.bxa-splash__cursor {
  display: inline-block;
  width: 0.35em;
  height: 0.75em;
  background: #1a2a8a;
  margin-left: 0.1em;
  vertical-align: middle;
  animation: bxa-cursor-blink 0.5s steps(2) 2 forwards;
  animation-delay: 0.3s;
  opacity: 0;
}
.bxa-splash__caption {
  font-family: 'Manrope', system-ui, sans-serif;
  font-weight: 300;
  font-style: italic;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  color: var(--mid, #4a443c);
  opacity: 0;
  animation: bxa-caption-in 0.4s ease-out 0.25s forwards;
}
.bxa-splash__rule {
  width: 0;
  height: 2px;
  background: #8b6028;
  margin-top: -0.4em;
  animation: bxa-rule-draw 0.5s ease-out 0.2s forwards;
}

@keyframes bxa-splash-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes bxa-splash-out {
  from { opacity: 1; }
  to   { opacity: 0; visibility: hidden; }
}
@keyframes bxa-caption-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 0.85; transform: translateY(0); }
}
@keyframes bxa-cursor-blink {
  0%   { opacity: 0; }
  50%  { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes bxa-rule-draw {
  from { width: 0; }
  to   { width: 4em; }
}

/* Respect users who prefer reduced motion. */
@media (prefers-reduced-motion: reduce) {
  .bxa-splash,
  .bxa-splash__inner,
  .bxa-splash__cursor,
  .bxa-splash__caption {
    animation: none;
  }
  .bxa-splash {
    animation: bxa-splash-out 0.2s linear 0.6s forwards;
  }
  .bxa-splash__caption { opacity: 0.85; }
  .bxa-splash__rule {
    animation: none;
    width: 4em;
  }
}

html { scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  background: var(--off);
  color: var(--text);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

.hidden { display: none !important; }
.muted { color: var(--muted); }
.small { font-size: 12px; }

/* Lift secondary-text tokens within BoxCraft (#box3d-modal) and
   the Side Panel Designer (#spine-designer-modal) so headings,
   hints, ghost-button labels — everything that uses --mid /
   --muted — reads as the lighter greyish-white the user sees in
   BA and BP. On dark themes the default --mid (#a89e90) and
   --muted (#7c736a) resolved to muddy taupe against the modal's
   white panel; piping --text through the same custom-property
   name brightens everything in scope without rewriting
   individual rules. Side-effect: hover borders also use --text
   — reads as a crisper hover edge rather than the previous
   muted shift, which is a fair trade for the colour-consistency
   win. */
#spine-designer-modal,
#box3d-modal {
  --mid: var(--text);
  --muted: var(--text);
}

/* ── Nav ────────────────────────────────────────────────────────────── */

.main-nav {
  /* Header is always a dark band — see the BoxArts theme blocks
     above. `--header-bg` defaults back to `--white` for the legacy
     themes (boxperfect / steamy / goodish) so their pale headers
     are unaffected. */
  background: var(--header-bg, var(--white));
  color: var(--header-fg, var(--text));
  border-bottom: 1px solid var(--header-border, var(--border));
  height: 64px;
  display: flex;
  align-items: center;
  padding: 0 28px;
  gap: 24px;
  position: sticky;
  top: 0;
  z-index: 100;
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--header-fg, var(--text));
}
.brand-mark {
  width: 28px; height: 28px; border-radius: 8px;
  background: linear-gradient(135deg, var(--green) 0%, #4dc3c7 100%);
  display: inline-block;
}
.brand-text {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 18px;
  color: var(--header-fg, var(--text));
  letter-spacing: 0.01em;
}

.nav-spacer { flex: 1; }
.nav-right { display: flex; align-items: center; gap: 12px; }
/* View-toggle sitting in the nav (next to the brand) gets a small
   left margin so it reads as a separate cluster from the wordmark
   rather than being glued to it. */
.nav-view-toggle { margin-left: 14px; }

/* Game context — the current-game label that lives between brand
   and nav-right on /3d. Switches `.main-nav` from flex to a 3-col
   grid only when a game-context is present (collection.html has the
   same .main-nav but no game-context, so it keeps the flex spacer
   layout). The grid keeps the title in normal flow so it pushes
   siblings apart instead of overlapping them when the viewport
   narrows. `position: relative` gives the dropdown a positioning
   anchor on the title. */
.main-nav:has(> .game-context) {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 0;
  column-gap: 12px;
}
.main-nav:has(> .game-context) > :first-child { justify-self: start; }
.main-nav:has(> .game-context) > .nav-right   { justify-self: end; }
.main-nav:has(> .game-context) > .nav-spacer  { display: none; }
.main-nav .game-context {
  position: relative;
  padding: 4px 12px;
  color: var(--header-muted, var(--fg-mid, var(--mid)));
  font-size: 13px;
}
/* Strong (highlighted current-game name) uses the secondary accent
   so it pops against the dark header without being mistaken for an
   accent fill button. Falls back to primary accent for legacy
   themes that didn't define --accent-2. */
.main-nav .game-context strong {
  color: var(--accent-2, var(--green-fg, var(--green)));
  font-weight: 700;
}

/* Game switcher — mirrors the rules in styles.css (BoxPerfect) so
   the same markup renders identically on /3d. Keep the two blocks
   in sync if either is edited. */
.game-switcher {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.game-switcher-arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--muted, #9aa);
  cursor: pointer;
  transition: color 0.15s, background-color 0.15s;
}
.game-switcher-arrow:hover {
  color: var(--text);
  background: rgba(127, 127, 127, 0.12);
}
.game-switcher-title {
  display: inline-flex;
  align-items: center;
  /* Centre the title image inside a fixed-width slot so the chevrons
     sit at consistent positions across all games. Without min-width
     the slot collapsed to the image's natural width, which meant
     adjacent titles in the dropdown / quick-nav appeared "left-
     aligned" relative to each other as the slot shifted left/right
     by the image's variable width. The min-width matches the title
     image's max-width so post-aspect-padding titles fill the slot
     without overflow. */
  justify-content: center;
  min-width: 240px;
  padding: 4px 10px;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: inherit;
  cursor: pointer;
  font: inherit;
  transition: background-color 0.15s;
}
.game-switcher-title:hover { background: rgba(127, 127, 127, 0.12); }
.game-switcher-title strong { color: var(--accent); font-weight: 700; }

/* Switcher inside the dark page header — chevrons, title, and hover
   tints need to read against the dark band rather than the original
   light-row defaults. Falls through to the base rules for legacy
   themes whose header is light (no --header-bg set). */
.main-nav .game-switcher-arrow {
  color: var(--header-muted, var(--muted));
}
.main-nav .game-switcher-arrow:hover {
  color: var(--header-fg, var(--text));
  background: rgba(255, 255, 255, 0.10);
}
.main-nav .game-switcher-title {
  color: var(--header-fg, var(--text));
}
.main-nav .game-switcher-title:hover {
  background: rgba(255, 255, 255, 0.08);
}
.main-nav .game-switcher-title strong,
.main-nav .game-switcher-placeholder {
  color: var(--accent-2, var(--green-fg, var(--green)));
}

/* Nav-right affordances on the dark header — strip the pale chip
   styling those buttons get from their default rules. The buttons
   keep their click targets but pick up the header foreground colour
   for icon / label, with a subtle white-tint hover rather than the
   default light-grey fill that would look misplaced on a dark band.
   Legacy themes still see `--header-bg` resolving to the original
   `--white`, so these overrides fire harmlessly there too — the
   white tint barely shows on a white surface.
   `.btn-wordmark` (no prefix) is used here so it catches every
   variant: `.btn.btn-wordmark`, `.btn-ghost.btn-wordmark`, and
   `.btn-green.btn-wordmark`. Earlier this rule only listed
   `.btn-ghost`, which missed the cross-nav buttons (`btn-go-3d`
   etc.) on collection.html since they're `.btn.btn-wordmark` —
   they fell through to the base rule's `color: var(--text)` and
   showed up dark / unreadable on the new dark header. */
.main-nav .btn-wordmark,
.main-nav .btn-icon-only,
.main-nav .account-pill,
.main-nav .btn-ghost {
  background: transparent;
  color: var(--header-fg, var(--text));
  border-color: var(--header-border, var(--border));
}
.main-nav .btn-wordmark:hover,
.main-nav .btn-icon-only:hover,
.main-nav .account-pill:hover,
.main-nav .btn-ghost:hover {
  background: rgba(255, 255, 255, 0.08);
  color: var(--header-fg, var(--text));
}
.main-nav .account-pill .account-avatar {
  /* Avatar swatch stays its own colour so the pill still reads as
     an account chip rather than a generic ghost button. */
  background: var(--accent-2, var(--green-fg, var(--green)));
}
.game-switcher-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  min-width: 240px;
  max-width: 360px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--panel, var(--white, #fff));
  border: 1px solid var(--border, rgba(0, 0, 0, 0.15));
  border-radius: 6px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  z-index: 50;
  padding: 4px;
}
.game-switcher-item {
  display: block;
  width: 100%;
  padding: 6px 10px;
  background: transparent;
  border: none;
  border-radius: 4px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  font: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.game-switcher-item:hover { background: rgba(127, 127, 127, 0.14); }
.game-switcher-item.is-current {
  background: rgba(127, 127, 127, 0.18);
  font-weight: 600;
}
/* "+ New game" entry — pinned at top of the dropdown when the page
   has no current game loaded. Slight emphasis + a divider beneath
   to set it apart from the alphabetical list. */
.game-switcher-item.game-switcher-new {
  color: var(--accent, #3aa3b0);
  font-weight: 600;
  border-bottom: 1px solid var(--border, rgba(0,0,0,0.12));
}
/* Title-cell placeholder when no game is loaded — slightly muted
   so it reads as "pick something" rather than "this is the title". */
.game-switcher-placeholder {
  color: var(--muted, #888);
  font-style: italic;
}

/* View-mode toggle */
.view-toggle {
  display: flex;
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 2px;
}
.view-toggle button {
  appearance: none;
  background: transparent;
  border: none;
  border-radius: var(--radius-pill);
  padding: 6px 14px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--mid);
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.view-toggle button.active {
  background: var(--white);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.view-toggle button:hover:not(.active) { color: var(--text); }
@media (max-width: 700px) {
  .view-toggle button { padding: 6px 10px; font-size: 11px; }
}

/* Each view-toggle button has BOTH a text label and an icon SVG. The
   data-button-style attribute on <html> selects which one shows. */
.view-toggle button .view-toggle-icon { display: none; }
[data-button-style="icons"] .view-toggle button .view-toggle-label { display: none; }
[data-button-style="icons"] .view-toggle button .view-toggle-icon {
  display: inline-block;
  vertical-align: middle;
}
[data-button-style="icons"] .view-toggle button {
  padding: 7px 12px;          /* tighter once labels are gone */
}

/* Settings cog button (sits next to the +Add button in the nav). */
.btn-icon-only {
  appearance: none;
  background: var(--white);
  color: var(--mid);
  border: 1px solid var(--border);
  border-radius: 50%;
  width: 38px; height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s, transform .1s;
}
.btn-icon-only:hover {
  background: var(--off);
  color: var(--text);
  border-color: var(--mid);
}
.btn-icon-only:active { transform: scale(0.95); }

/* Settings modal — theme picker + button-style toggle */
.settings-section { margin: 14px 0 18px; }
.settings-section + .settings-section {
  border-top: 1px solid var(--border);
  padding-top: 14px;
}
.settings-h4 {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  margin: 0 0 4px;
  color: var(--text);
}
.theme-picker, .button-style-toggle {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 10px;
}
.theme-card {
  appearance: none;
  background: var(--white);
  border: 2px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  cursor: pointer;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 4px;
  transition: border-color .12s, transform .1s, box-shadow .12s;
  font-family: inherit;
  color: var(--text);
}
.theme-card:hover { border-color: var(--mid); transform: translateY(-1px); }
.theme-card[aria-checked="true"] {
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--green-tint);
}
.theme-swatch {
  display: block;
  width: 100%;
  height: 38px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border);
}
.theme-name {
  font-weight: 700;
  font-size: 13px;
  margin-top: 4px;
}
.theme-tag { font-size: 11px; }

/* Fullscreen mode — toggled via the toolbar's expand button. Hides the
   nav + toolbar so only the grid is visible. ESC or the floating exit
   button returns to normal view. */
/* Toolbar fullscreen button: smaller than the nav-bar variant so the
   toolbar's overall height (used by sticky headers below) doesn't grow. */
.toolbar #btn-fullscreen {
  width: 30px;
  height: 30px;
}
.fs-exit {
  display: none;          /* shown only in fullscreen */
  position: fixed;
  top: 14px;
  right: 14px;
  z-index: 200;
  background: var(--white);
  box-shadow: var(--shadow-card);
}
[data-fullscreen="1"] .main-nav,
[data-fullscreen="1"] .toolbar {
  display: none;
}
[data-fullscreen="1"] .fs-exit {
  display: inline-flex;
}
[data-fullscreen="1"] .container {
  padding-top: 18px;      /* normal padding without the toolbar above */
}

.search-wrap { position: relative; }
.search-input {
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 8px 16px 8px 36px;
  font-family: inherit;
  font-size: 13px;
  color: var(--text);
  width: 220px;
  outline: none;
  transition: all .2s;
}
.search-input::placeholder { color: var(--muted); }
.search-input:focus {
  border-color: var(--green);
  background: var(--white);
  width: 280px;
  box-shadow: 0 0 0 3px rgba(0,149,114,0.10);
}
.search-ico {
  position: absolute; left: 14px; top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  font-size: 14px;
  pointer-events: none;
}

/* ── Toolbar (filters / sort / zoom / count) ────────────────────────── */

.toolbar {
  position: sticky;
  top: 64px;                        /* sits below the main nav */
  z-index: 99;
  background: var(--white);
  border-bottom: 1px solid var(--border);
  padding: 10px 28px;
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.game-count {
  font-size: 11px;
  color: var(--muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.game-count strong {
  color: var(--green);
  background: var(--green-tint);
  border-radius: 6px;
  padding: 3px 9px;
  font-weight: 800;
  font-size: 12px;
  letter-spacing: 0;
}
.toolbar-group { display: flex; gap: 8px; flex-wrap: wrap; }
.filter-select {
  background: var(--white);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 6px 28px 6px 12px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
  /* Custom chevron */
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%237a847a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 10px center;
}
.filter-select:hover { border-color: var(--mid); }
.filter-select:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(0,149,114,0.10);
}
.toolbar-spacer { flex: 1; }
.zoom-control {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--mid);
}
.zoom-control input[type="range"] {
  width: 130px;
  accent-color: var(--green);
  cursor: pointer;
}
.zoom-mark {
  font-weight: 700;
  color: var(--mid);
  font-size: 14px;
  width: 12px;
  text-align: center;
  user-select: none;
}

/* "Highlighted" filter pill — hidden until 2+ cards are highlighted. */
.filter-pill {
  background: var(--white);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 6px 14px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.filter-pill .pill-star { color: #f4c542; font-size: 13px; }
.filter-pill:hover { border-color: var(--mid); }
.filter-pill[aria-pressed="true"] {
  background: #fff7d6;
  border-color: #f4c542;
  color: #8a5a00;
}
.filter-pill[aria-pressed="true"] .pill-star { color: #c69400; }

/* Filter "chips" — small square buttons sized to match the
 * game-count badge on the far left of the toolbar (~24×24, 6px
 * radius). Each chip's COLOUR is the affordance: gold square =
 * highlighted-card filter, theme-accent square with a ▶ glyph =
 * ready-to-play filter. Counts intentionally absent — the visible
 * grid count in the game-count badge already reflects the
 * pressed filter state. */
.filter-chip {
  appearance: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  width: 28px;
  height: 28px;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  transition: background 0.15s, border-color 0.15s, transform 0.08s;
}
.filter-chip:hover { transform: translateY(-1px); }
.filter-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Play chip — neutral light-grey outline matching the other
 * filter pills (Clear filters, the dropdown selects). The ▶
 * glyph itself carries the theme-accent colour, signalling
 * "play" without dominating the toolbar. Pressed state fills
 * with the accent tint to match the highlight chip's
 * outline-when-idle / fill-when-active pattern. */
.play-chip {
  background: var(--white);
  border-color: var(--border);
  color: var(--green);
}
.play-chip:hover {
  background: var(--off);
  border-color: var(--mid);
}
.play-chip .chip-glyph {
  /* Slightly nudged right — ▶ has trailing optical weight. */
  transform: translateX(1px);
}
.play-chip[aria-pressed="true"] {
  background: var(--green-tint);
  border-color: var(--green);
  color: var(--green-hover);
}

/* Highlight chip — gold OUTLINE by default; the chip "fills in"
 * (solid gold) when pressed so the active state reads clearly.
 * #f4c542 matches the highlighted-card halo so the affordance is
 * visually consistent with the cards the filter selects. */
.highlight-chip {
  background: transparent;
  border-color: #f4c542;
  border-width: 2px;
}
.highlight-chip:hover {
  background: #fff7d6;
}
.highlight-chip[aria-pressed="true"] {
  background: #f4c542;
  border-color: #c69400;
}

@media (max-width: 700px) {
  .toolbar { padding: 10px 16px; gap: 10px; }
  .zoom-control input[type="range"] { width: 90px; }
  .filter-select { font-size: 11px; padding: 5px 24px 5px 10px; }
}

/* ── Buttons ────────────────────────────────────────────────────────── */

/* Placeholder-cover modifier — applied to any <img> on a game whose
   cover is the downloaded Wikipedia thumbnail rather than a user
   upload. The greyscale signals "this is a stand-in until you add
   your own"; the brightness dip + opacity tweak keep it from
   competing visually with real covers on the same grid. Cleared
   automatically the moment the user uploads any real image (server
   drops is_placeholder, frontend re-renders without the class). */
.placeholder-cover {
  filter: grayscale(1) brightness(0.92);
  opacity: 0.78;
}

.btn-green {
  background: var(--green);
  color: var(--white);
  border: none;
  border-radius: var(--radius-pill);
  padding: 9px 20px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.01em;
  cursor: pointer;
}
/* Round "+" variant used by the header Add-game button. Replaces
   the pill-shaped "+ Add game" so the BoxPerfect/BoxCraft/Search
   cluster gains the freed horizontal space. Sized to match the
   account-pill / settings-icon line-height so all three controls
   on the right read as the same row of round affordances. The
   selector is one-class-deep on top of .btn-green; the cascade
   ordering after .btn-green's `padding: 9px 20px` matters, which
   is why the `}` on the parent rule is just above. */
.btn-green.btn-add-round {
  width: 36px;
  height: 36px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  font-weight: 600;
  line-height: 1;
  border-radius: 50%;
  transition: background .15s, transform .1s;
}
.btn-green:hover { background: var(--green-hover); transform: translateY(-1px); }
.btn-green:disabled { opacity: .5; cursor: not-allowed; transform: none; }

.btn-ghost {
  background: var(--white);
  color: var(--mid);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 9px 18px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all .15s;
}
.btn-ghost:hover { background: var(--off); color: var(--text); border-color: var(--mid); }

.btn-danger {
  background: var(--white);
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius-pill);
  padding: 9px 18px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all .15s;
}
.btn-danger:hover { background: var(--danger); color: var(--white); }

/* ── Container + grid ───────────────────────────────────────────────── */

.container {
  max-width: 1500px;
  margin: 0 auto;
  padding: 28px 28px 80px;
}

.empty-state {
  text-align: center;
  padding: 80px 24px;
  background: var(--white);
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
}
.empty-state h2 {
  font-family: var(--font-display);
  font-size: 24px;
  margin: 16px 0 6px 0;
}
.empty-state p { color: var(--mid); }
.empty-mark {
  width: 56px; height: 56px;
  background: linear-gradient(135deg, var(--green) 0%, #4dc3c7 100%);
  border-radius: 14px;
  margin: 0 auto;
  position: relative;
}
.empty-mark::before {
  content: ""; position: absolute; inset: 14px;
  border: 2px dashed rgba(255,255,255,0.85);
  border-radius: 6px;
}

.game-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--tile-size), 1fr));
  gap: 22px;
  margin-top: 20px;
}

/* Real-world proportions across gallery / bookshelf / 3D views. The zoom
   slider's --tile-size is the height in px of a reference 24cm box; every
   other game scales proportionally from that. */
.game-grid.view-gallery,
.game-grid.view-3d,
.game-grid.view-bookshelf {
  --base-cm-px: calc(var(--tile-size) / 24);
}
.game-grid.view-gallery,
.game-grid.view-3d {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 22px;
}

/* Gallery view: uniform-sized tiles in a clean grid. Per-game proportions
   are still preserved INSIDE each tile (the image keeps its real aspect
   ratio via object-fit: contain and is centered), but the outer tile is
   always the reference 19×24 cm so columns line up perfectly across
   rows regardless of which boxes happen to be a few cm wider or taller. */
.game-grid.view-gallery .game-card {
  flex: 0 0 auto;
  width: calc(19 * var(--base-cm-px));
}
.game-grid.view-gallery .card-image {
  padding-top: 0;
  aspect-ratio: 19 / 24;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 3D view: keep real-world proportions per game — the whole point of the
   3D view is to show how each box's footprint actually looks. */
.game-grid.view-3d .game-card {
  flex: 0 0 auto;
  width: calc(var(--box-w-cm, 19) * var(--base-cm-px));
}
.game-grid.view-3d .card-image {
  padding-top: 0;
  aspect-ratio: var(--box-w-cm, 19) / var(--box-h-cm, 24);
}

/* Bookshelf view — vertical "spines" (right-face image) lined up like
   books on a shelf. Hover lifts + reveals the game name pop-up. */
.game-grid.view-bookshelf {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: flex-end;
  padding: 24px 0 60px;
  background: linear-gradient(180deg,
    transparent 0%,
    transparent 80%,
    rgba(0, 0, 0, 0.06) 92%,
    rgba(0, 0, 0, 0.10) 100%);
  margin-top: 20px;
}
.game-spine {
  position: relative;
  flex: 0 0 auto;
  /* Width = depth, height = box height — real proportions on the shelf.
     min-width keeps very thin items (e.g. floppy sleeves) clickable. */
  width: calc(var(--box-d-cm, 5) * var(--base-cm-px));
  height: calc(var(--box-h-cm, 24) * var(--base-cm-px));
  min-width: 12px;
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: 2px;
  /* No overflow:hidden — would clip the hover tooltip (::after). The img/
     placeholder fill the bordered box exactly so nothing visually overflows. */
  cursor: pointer;
  transition: transform 0.15s, box-shadow 0.15s, z-index 0s 0.15s;
  box-shadow: 1px 0 0 rgba(0, 0, 0, 0.04);
}
.game-spine:hover {
  transform: translateY(-8px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.20), 0 4px 6px rgba(0, 0, 0, 0.08);
  z-index: 50;
  transition: transform 0.15s, box-shadow 0.15s;
}
.game-spine img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: 2px;
}
/* Folios: most spine artwork is wider than tall (the user
   photographed the book lying on its spine, or extracted a thin
   horizontal strip). object-fit: cover would crop that to a
   near-empty central slice in the 12 px wide tile, which
   produces the "block grey" rendering. `contain` shows the
   whole spine art letterboxed inside the tile so the title /
   stripe / colour stays visible. */
.game-spine[data-kind="portfolio"] img {
  object-fit: contain;
  background: var(--off);
}
.spine-placeholder {
  width: 100%; height: 100%;
  border-radius: 2px;
  background: repeating-linear-gradient(45deg,
    var(--light) 0, var(--light) 4px,
    var(--off) 4px, var(--off) 8px);
}
/* Tooltip pop-up shown on hover */
.game-spine::after {
  content: attr(data-name);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  background: var(--text);
  color: var(--white);
  padding: 6px 10px;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s 0.05s;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
}
.game-spine:hover::after { opacity: 1; }

.game-card {
  position: relative;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* No overflow:hidden — would clip the hover-name tooltip (::after).
     The image area has its own overflow:hidden + border-radius below. */
  cursor: pointer;
  transition: transform .2s, box-shadow .2s, border-color .2s;
  box-shadow: var(--shadow-card);
  display: flex;
  flex-direction: column;
}
.game-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-hover);
  border-color: var(--green);
  z-index: 20;     /* lift above neighbours so the tooltip isn't covered */
}

/* Highlighted card — glowing yellow ring. Same effect across all three card
   views (gallery, 3d, and the .game-spine in bookshelf). */
.game-card.highlighted {
  border-color: #f4c542;
  box-shadow: 0 0 0 3px rgba(244, 197, 66, 0.50), var(--shadow-card);
}
.game-card.highlighted:hover {
  border-color: #f4c542;
  box-shadow: 0 0 0 3px rgba(244, 197, 66, 0.65), var(--shadow-hover);
}
.game-spine.highlighted {
  border-color: #f4c542;
  box-shadow: 0 0 0 2px rgba(244, 197, 66, 0.55);
}
.game-spine.highlighted:hover {
  box-shadow: 0 0 0 2px rgba(244, 197, 66, 0.7),
              0 12px 24px rgba(0, 0, 0, 0.20),
              0 4px 6px rgba(0, 0, 0, 0.08);
}

/* List view — text-only rows, no covers. Header + scannable rows.
   No overflow:hidden — that creates a scroll container that traps the
   sticky header inside the grid. We round the corners on the first/last
   child instead so the visual remains clean. */
.game-grid.view-list {
  display: block;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-top: 20px;
}
.game-grid.view-list > :first-child {
  border-top-left-radius: var(--radius);
  border-top-right-radius: var(--radius);
}
.game-grid.view-list > :last-child {
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
}
.list-header,
.game-row {
  display: grid;
  /* thumb · star · name · year · publisher · genre · card · play · 3D-badge */
  grid-template-columns: 40px 28px minmax(0, 3fr) 70px minmax(0, 2fr) minmax(0, 2fr) 28px 32px 36px;
  gap: 14px;
  align-items: center;
  padding: 8px 16px;
}
.list-header {
  background: var(--off);
  border-bottom: 1px solid var(--border);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mid);
  /* Sticks BELOW the nav (64px) AND the toolbar (~56px) so it has its own
     visible band rather than hiding under the toolbar. z-index lower than
     the toolbar (99) so we don't ever paint over it. */
  position: sticky;
  top: 120px;
  z-index: 90;
}
.game-row {
  border-bottom: 1px solid var(--border);
  /* Cursor is set per-cell now (cells with data-action are
     clickable; the genre column stays inert). The row no longer
     pretends every column opens the gallery. */
  cursor: default;
  transition: background 0.1s;
  font-size: 14px;
  color: var(--text);
}
.game-row:last-child { border-bottom: none; }
.game-row:hover { background: var(--off); }
/* Per-column click affordances. The thumbnail + name open the
   gallery (the existing primary action). Year and publisher
   become filter shortcuts — clicking them sets the toolbar
   dropdown to that value. The 3D badge opens the rotating box
   modal. The play button launches the linked game. Genre is
   intentionally inert — no hover, no cursor change. */
.game-row [data-action] { cursor: pointer; }
.game-row [data-action]:hover { color: var(--green); }
.game-row .row-link:hover { color: var(--green); }
/* Year / publisher / genre cells get the same green text-tint
   on hover as everything else with [data-action]; we don't add
   an underline so the row reads cleanly without a typewriter
   look. The genre cell joins the same is-clickable pattern when
   the game has a genre — non-clickable cells stay default. */
.row-play,
.row-card {
  appearance: none;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  color: var(--green);
  cursor: pointer;
  padding: 0;
  line-height: 1;
}
.row-play:hover,
.row-card:hover { background: var(--green-tint); border-color: var(--green); }
/* The card icon uses a stroke-only SVG; lighten the resting
   stroke colour slightly so it doesn't dominate the row, then
   ramp to full green on hover. */
.row-card { color: var(--mid); }
.row-card:hover { color: var(--green); }
.game-row .row-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Centre the title image (or text fallback) within the name
     column so titles read as labels rather than left-anchored
     entries. Without this, narrow title images sat hard against
     the left edge of the column and looked offset from neighbours
     with wider art. `display: block` is needed because the row-
     name is a <span> — without it text-align wouldn't apply to
     the inline-block <img>. */
  display: block;
  text-align: center;
}
.game-row .row-year,
.game-row .row-publisher,
.game-row .row-genre {
  color: var(--mid);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.game-row .row-star {
  font-size: 14px;
  color: var(--border);
  text-align: center;
  user-select: none;
}
/* List-row cover thumbnail (left of the row). object-fit:cover lets the
   image fill the 40×40 square without distortion; missing cover renders
   a faint placeholder so the column stays aligned across all rows. */
.row-thumb {
  width: 40px;
  height: 40px;
  border-radius: 4px;
  object-fit: cover;
  background: var(--off);
  border: 1px solid var(--border);
  display: block;
}
.row-thumb-empty {
  background: var(--off);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.row-thumb-empty-text {
  /* Game name as a stand-in thumbnail when the card has no image
     at all (user deleted everything including the placeholder).
     Uppercase + display-font + theme accent so it reads as a
     branded panel rather than an error state. */
  font-family: var(--font-display, var(--font-body, system-ui));
  font-weight: 800;
  font-size: 9px;
  letter-spacing: 0.04em;
  color: var(--accent, var(--green, var(--text)));
  text-align: center;
  padding: 2px;
  line-height: 1.05;
  word-break: break-word;
}
/* 3D badge on the far right — only visible when isComplete3d is true. */
.row-3d-badge {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--green);
  background: var(--green-tint);
  border: 1px solid var(--green);
  border-radius: 4px;
  padding: 2px 6px;
  text-align: center;
  user-select: none;
}
.row-3d-badge.empty {
  visibility: hidden;
}
.game-row.highlighted {
  background: #fffbe9;
  box-shadow: inset 3px 0 0 #f4c542;
}
.game-row.highlighted .row-star { color: #f4c542; }
.game-row.highlighted:hover { background: #fff3c4; }
@media (max-width: 700px) {
  .list-header,
  .game-row {
    /* thumb · star · name · year · publisher · 3D-badge (genre dropped) */
    grid-template-columns: 36px 22px minmax(0, 3fr) 60px minmax(0, 2fr) 32px;
    padding: 8px 12px;
    gap: 10px;
  }
  .game-row .row-genre,
  .list-header .col-genre { display: none; }
  .row-thumb { width: 36px; height: 36px; }
}

/* Hover tooltip — banner across the bottom of the cover, on top of the
   image (not above the card). */
.game-card::after {
  content: attr(data-name);
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 9px 12px;
  background: linear-gradient(to top,
    rgba(0, 0, 0, 0.85) 0%,
    rgba(0, 0, 0, 0.78) 60%,
    rgba(0, 0, 0, 0.0) 100%);
  color: var(--on-dark);
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  white-space: normal;
  line-height: 1.3;
  border-radius: 0 0 var(--radius) var(--radius);
  max-height: 60%;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s 0.05s;
}
.game-card:hover::after { opacity: 1; }

/* Hide the under-card name+meta block in gallery + 3D — the hover tooltip
   above the cover replaces it. (Trial: easy to revert by removing this rule.) */
.game-grid.view-gallery .card-body,
.game-grid.view-3d .card-body { display: none; }

.card-image {
  position: relative;
  width: 100%;
  padding-top: 140%;          /* book-cover aspect */
  background: var(--light);
  overflow: hidden;
  border-radius: var(--radius);   /* round all corners — card-body is hidden */
}
.card-image img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;          /* show the full cover; letterbox over the tile bg */
  display: block;
}
.card-image-empty {
  /* "Thumbnail" panel for games with no cover image — renders the
     game's name in CAPS using the active theme's display font +
     accent colour, so the card still reads as a distinct entry
     in the grid rather than an empty hole. Theme variables
     (`--font-display`, `--accent`) cascade so every theme picks
     the look up automatically. `container-type: inline-size` on
     this element makes `cqi` units below scale the text size to
     the tile width — so the name reads big on a wide card and
     reasonably-small on a narrow zoomed-out tile. */
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--off);
  padding: 12px;
  container-type: inline-size;
}
.card-image-empty-text {
  font-family: var(--font-display, var(--font-body, system-ui));
  font-weight: 800;
  font-size: clamp(14px, 8cqi, 28px);
  letter-spacing: 0.04em;
  color: var(--accent, var(--green, var(--text)));
  text-align: center;
  line-height: 1.1;
  word-break: break-word;
}

/* 3D thumbnail — rendered when all 6 box_faces are set on a game.
   Reuses .box3d-face geometry from the builder so face images line up. */
.card-3d-stage {
  position: absolute;
  inset: 0;
  perspective: 900px;
  perspective-origin: 50% 45%;
  display: grid;
  place-items: center;
  pointer-events: none;          /* let card click pass through to the article */
}
.card-3d-box {
  position: relative;
  transform-style: preserve-3d;
  /* Static 3/4 view — same default as the gallery's interactive 3D box,
     so the front, right-edge and top all read at a glance. Hover spins
     the box around its Y axis (only THIS card; other cards keep their
     static pose because the :hover selector scopes to one .game-card).
     The transition smooths the snap-back when the cursor leaves. */
  transform: rotateX(-15deg) rotateY(25deg);
  transition: transform 0.4s ease;
  will-change: transform;

  /* Projection-aware sizing. The box face dimensions need to fit
     INSIDE the card-image area AFTER the (-15°, +25°) rotation —
     not merely the unrotated front face. Without this, depth-heavy
     boxes (Sierra big-box, jewel cases standing on edge) overflowed
     the top / bottom of the card because the projected bounding
     box is taller than the front face's height.

     Bounding-box of the rotated cube on screen (axis-aligned):
       bbox_x = |cos(25°)|·w + |sin(25°)|·d ≈ 0.906·w + 0.423·d
       bbox_y = |cos(15°)|·h + |sin(15°)| · (|sin(25°)|·w + |cos(25°)|·d)
              ≈ 0.966·h + 0.110·w + 0.235·d
     The fit-scale is whichever axis is the binding constraint —
     usually Y for tall big-boxes, X for very wide-and-thick ones.
     Values are unitless because --box-*-cm are stored as plain
     numbers. The trailing 0.95 is the visible gutter so the box
     doesn't kiss the card edge. */
  --bbox-x: calc(var(--box-w-cm, 19) * 0.906 + var(--box-d-cm, 5) * 0.423);
  --bbox-y: calc(var(--box-h-cm, 24) * 0.966 + var(--box-w-cm, 19) * 0.110 + var(--box-d-cm, 5) * 0.235);
  --fit-scale: min(
    calc(var(--box-w-cm, 19) / var(--bbox-x)),
    calc(var(--box-h-cm, 24) / var(--bbox-y))
  );
  --w: calc(var(--box-w-cm, 19) * var(--base-cm-px, 7.5) * var(--fit-scale) * 0.95);
  --h: calc(var(--box-h-cm, 24) * var(--base-cm-px, 7.5) * var(--fit-scale) * 0.95);
  --d: calc(var(--box-d-cm, 5)  * var(--base-cm-px, 7.5) * var(--fit-scale) * 0.95);
}
.card-3d-box .box3d-face {
  background-color: #cdd1cd;
  /* 100% × 100% to match the builder + print kit: every place a user
     sees a 3D box conforms the image to the face's measured size. */
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-position: center;
  border: 1px solid rgba(0,0,0,0.15);
  box-shadow: inset 0 0 16px rgba(0,0,0,0.18);
  image-rendering: -webkit-optimize-contrast;   /* Chrome / Edge */
  image-rendering: high-quality;                /* modern */
}

/* Hover-to-spin: only the 3D-view card under the cursor animates;
   every other card keeps its static 3/4 pose. The animation
   carries the same -15° X tilt as the static rule so the box
   doesn't leap upright when the cursor enters — only the Y
   rotation changes. Stopping the animation drops back to the
   static `transform`, smoothed by the 0.4s transition above. */
@keyframes card-3d-spin {
  from { transform: rotateX(-15deg) rotateY(25deg);  }
  to   { transform: rotateX(-15deg) rotateY(385deg); }
}
.game-grid.view-3d .game-card:hover .card-3d-box {
  animation: card-3d-spin 6s linear infinite;
}
/* Respect reduced-motion users — the static pose still reads
   well, no need to spin if the OS asks us not to. */
@media (prefers-reduced-motion: reduce) {
  .game-grid.view-3d .game-card:hover .card-3d-box { animation: none; }
}

.card-body {
  padding: 12px 14px 14px;
}
.card-title {
  font-weight: 700;
  font-size: 14px;
  color: var(--text);
  line-height: 1.25;
  margin: 0 0 4px 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-align: center;
}
.card-meta {
  font-size: 12px;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.card-meta-sep { color: var(--border); }

/* Title image — replaces the game's text name in headers. Height is
   locked per-surface so a long, narrow title strip and a chunky
   square one both feel consistent. Width auto-scales from the
   image's aspect ratio so the user keeps control over the visual
   weight via cropping in BoxPerfect. `object-fit: contain` keeps
   the image inside its box if max-width clamps it.
   The base class sets the default; surface modifiers (card / row /
   modal / nav) override the height. */
/* Fixed-width box per surface (roughly 5x the locked height — chosen
   to match a typical title-art aspect ratio). EVERY title image
   renders into the same on-screen footprint and the logo art
   letterboxes inside via object-fit: contain — square logos sit
   centred with whitespace either side, ultra-wide banners get
   vertically compressed inside the fixed box. The earlier soft
   min/max band did nothing visible because typical 3:1-7:1 logos
   landed inside it and never hit either bound. */
/* Height-locked sizing — width adapts to each title's natural aspect.
   Title art varies from ~1:1 to >10:1 in the corpus; a fixed-aspect
   cell with object-fit:contain produced a 5× spread in rendered widths
   PLUS height-squashed the banner-thin titles. Locking just the height
   gives every title the same visual baseline weight; width follows
   the natural aspect with a soft cap that prevents the wides from
   shoving the layout. Each surface keeps its own height + cap to
   match the surrounding density. */
.game-title-image {
  display: inline-block;
  height: 36px;
  width: auto;
  max-width: 280px;
  object-fit: contain;
  vertical-align: middle;
}
.card-title-image  { height: 36px; width: auto; max-width: 280px; }
.row-title-image   { height: 24px; width: auto; max-width: 190px; }
.modal-title-image { height: 32px; width: auto; max-width: 250px; }
.nav-title-image   { height: 28px; width: auto; max-width: 220px; }
/* Inside a .card-title (which clamps text to 2 lines via -webkit-box),
   the title image is the ONLY content — strip the text clamp so the
   img isn't cropped to a single line of fake text. */
.card-title:has(.game-title-image) {
  display: block;
  -webkit-line-clamp: unset;
}

/* Game Card modal — title-image dropdown sits next to the heading
   (replaces the older separate "Title image" row in the Details
   tab). Tiny chevron button. Hidden in Add mode; surfaced in Edit
   mode by refreshTitleMenuButton. */
.modal-title-menu-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-left: 6px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--border, rgba(0,0,0,0.15));
  border-radius: 4px;
  color: var(--mid, #555);
  cursor: pointer;
  vertical-align: middle;
}
.modal-title-menu-btn:hover {
  background: rgba(0,0,0,0.05);
  color: var(--text, #222);
}
.modal-title-menu-btn[data-has-title="true"] {
  border-color: var(--green, #3aa3b0);
  color: var(--green, #3aa3b0);
}

/* ── Modal ──────────────────────────────────────────────────────────── */

.modal {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding: 24px;
}
.modal-backdrop {
  position: absolute; inset: 0;
  background: rgba(15, 23, 28, 0.45);
  backdrop-filter: blur(2px);
}
.modal-panel {
  position: relative;
  background: var(--white);
  width: min(680px, 100%);
  max-height: calc(100vh - 48px);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-narrow {
  width: min(440px, 100%);
}

/* ── Progress bar ───────────────────────────────────────────────────── */

.progress-bar {
  position: relative;
  width: 100%;
  height: 4px;
  background: var(--light);
  border-radius: 2px;
  overflow: hidden;
  margin-top: 16px;
}
.progress-bar .progress-fill {
  height: 100%;
  width: 0%;
  background: var(--green);
  border-radius: 2px;
  transition: width 0.25s ease;
}
.progress-bar.indeterminate .progress-fill {
  width: 30%;
  animation: progress-slide 1.4s infinite linear;
}
@keyframes progress-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(333%); }
}

/* Page-top loading bar (for collection-wide operations) */
.page-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: transparent;
  z-index: 1500;
  pointer-events: none;
}
.page-progress .progress-fill {
  height: 100%;
  background: var(--green);
  width: 30%;
  animation: progress-slide 1.4s infinite linear;
}
.page-progress.hidden { display: none; }

/* Centre-screen loading overlay. Hidden by default; activated by
   adding `.is-visible` (window.bxaLoader.show() toggles it). The
   full BoxArts iso-stamp glyph (same paths as the header lockup)
   snap-rotates 90° at a time, looping while work is in flight.
   No surrounding panel / shadow / label — just the glyph itself,
   at 2× the header glyph size, in the page's accent colour. Sits
   above modals but below toasts. */
.bxa-loader {
  position: fixed;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 1400;
}
.bxa-loader.is-visible {
  display: flex;
  animation: bxa-loader-fade 0.18s ease-out both;
}
@keyframes bxa-loader-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.bxa-loader__glyph {
  /* Header lockup glyph is 1.4em; this is 2×. */
  width: 2.8em;
  height: 2.8em;
  /* White on every theme — currentColor cascades to stroke + fill
     on the SVG's paths. The loader only appears during long ops
     on top of a dimmed page; pure white reads cleanly regardless
     of the underlying theme. */
  color: #fff;
  animation: bxa-loader-spin 0.8s linear infinite;
}
@keyframes bxa-loader-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .bxa-loader__glyph { animation: none; }
}

.modal-header {
  padding: 20px 24px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.modal-header h3 {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}
.modal-x {
  background: none; border: none;
  font-size: 28px; line-height: 1;
  color: var(--muted);
  cursor: pointer; padding: 0;
}
.modal-x:hover { color: var(--text); }

.modal-body {
  padding: 20px 24px;
  overflow-y: auto;
  flex: 1;
}
.modal-section + .modal-section { margin-top: 24px; }

/* Tab strip used by Settings + Game Details modals to split a long
 * scroll into navigable sections. Shape: a horizontal row of buttons
 * with an underline on the active one (theme-accent colour) + a thin
 * border under the whole strip so the active underline reads as
 * "ahead of" the boundary. Themed via existing CSS vars so swapping
 * skin colours just works. */
.modal-tabs {
  display: flex;
  gap: 2px;
  border-bottom: 1px solid var(--border);
  margin: -8px -24px 16px -24px;     /* extend to modal edges */
  padding: 0 16px;
  flex-wrap: wrap;
}
.modal-tabs button {
  appearance: none;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--mid);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 10px 14px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}
.modal-tabs button:hover { color: var(--text); }
.modal-tabs button[aria-selected="true"] {
  color: var(--green);
  border-bottom-color: var(--green);
}
.modal-tab-panel { display: none; }
.modal-tab-panel.active { display: block; }

/* Collapsible drawer sections in the Edit Game modal — Details and Box
   Dimensions are closed by default; click the header to expand. Images
   stays as a regular .modal-section (always open). */
details.drawer { padding: 0; }
details.drawer > .drawer-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--mid);
  padding: 8px 0;
  border-bottom: 1px solid transparent;
  transition: color 0.15s, border-color 0.15s;
  user-select: none;
}
details.drawer > .drawer-summary::-webkit-details-marker { display: none; }
details.drawer > .drawer-summary::before {
  content: "▸";
  display: inline-block;
  font-size: 10px;
  color: var(--muted);
  transition: transform 0.15s;
  width: 12px;
}
details.drawer[open] > .drawer-summary::before { transform: rotate(90deg); }
details.drawer > .drawer-summary:hover {
  color: var(--text);
  border-color: var(--border);
}
details.drawer[open] > .drawer-summary {
  color: var(--text);
  border-color: var(--border);
  margin-bottom: 12px;
}
.section-title {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--mid);
  margin-bottom: 10px;
}

.field { display: block; margin-bottom: 14px; }
.dim-summary { display: block; margin-top: 6px; }
/* Divider between the pinned "Standard '90s Big Box" and the
   alphabetical rest. Implemented as a disabled <option> filled with
   box-drawing dashes rather than <hr> because <option> text colour
   is reliably stylable across Chromium / Firefox / Safari, whereas
   <hr> inside <select> only respects custom colours when the parent
   opts into `appearance: base-select`. */
#f-dim-preset .dim-separator,
#f-dim-preset .dim-separator:disabled {
  color: var(--text);
  background-color: transparent;
  text-align: left;
  /* Slightly tighter letter-spacing so the U+2500 box-drawing
     dashes join into a continuous line in the popup. */
  letter-spacing: -1px;
}
.field-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--mid);
  margin-bottom: 6px;
}
.field-input {
  width: 100%;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 9px 12px;
  font-family: inherit;
  font-size: 14px;
  color: var(--text);
  outline: none;
  transition: border-color .15s, box-shadow .15s;
}
.field-input:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(0,149,114,0.10);
}
.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.field-with-button {
  display: flex;
  gap: 8px;
}
.field-with-button .field-input { flex: 1; min-width: 0; }
.field-with-button .btn-ghost {
  flex-shrink: 0;
  padding: 9px 16px;
}

.lookup-results {
  margin: -6px 0 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 320px;
  overflow-y: auto;
}
.lookup-card {
  display: flex;
  gap: 12px;
  padding: 10px 12px;
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  transition: all .15s;
}
.lookup-card:hover {
  background: var(--green-tint);
  border-color: var(--green);
}
.lookup-card-thumb {
  flex-shrink: 0;
  width: 56px;
  height: 76px;
  background: var(--light);
  border-radius: 4px;
  overflow: hidden;
}
.lookup-card-thumb img {
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.lookup-card-body {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column;
}
.lookup-card-title {
  font-weight: 700; font-size: 14px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.lookup-card-meta {
  font-size: 12px;
  color: var(--mid);
  margin-top: 2px;
}
.lookup-card-extract {
  font-size: 11px;
  color: var(--muted);
  margin-top: 4px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.35;
}
.lookup-status {
  padding: 12px;
  background: var(--off);
  border: 1px dashed var(--border);
  border-radius: 8px;
  font-size: 12px;
  color: var(--muted);
  text-align: center;
}

.image-actions {
  display: flex;
  gap: 8px;
  margin: 8px 0 12px;
}

.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 8px;
}
.image-tile {
  position: relative;
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  border: 2px solid transparent;
  background: var(--light);
  cursor: pointer;
  transition: border-color .15s, opacity .15s, box-shadow .15s;
}
.image-tile.dragging { opacity: 0.4; }
.image-tile.drag-over { border-color: var(--green); }
.image-tile img {
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* The currently-selected tile (drives the action-row dimensions
   panel) lights up with the same accent ring the Cover state uses
   but in the editor accent so it can stand alongside an is-cover
   ring without conflict. */
.image-tile.is-dim-selected {
  border-color: var(--accent, var(--mid));
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
}

/* Per-image dimensions panel — sits at the right of the Upload
   Files / Upload Folder row, at the SAME level as those buttons.
   Populates on tile click; PATCHes on input / change. */
.image-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}
.image-actions-spacer { flex: 1; }
.modal-image-dims {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
}
.modal-image-dims.hidden { display: none; }
.modal-image-dims-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--fg-mid, var(--mid));
  max-width: 220px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.modal-image-dims-field {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.modal-image-dims-field input {
  width: 64px;
  padding: 4px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--fg);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  -moz-appearance: textfield;
}
.modal-image-dims-field input::-webkit-outer-spin-button,
.modal-image-dims-field input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.modal-image-dims-unit {
  padding: 4px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--fg);
  font-size: 12px;
}
.image-tile.is-cover {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(0,149,114,0.15);
}
.image-tile .cover-badge {
  position: absolute;
  bottom: 4px; left: 4px;
  background: var(--green);
  color: var(--white);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  padding: 2px 6px;
  border-radius: 4px;
  text-transform: uppercase;
}

/* Hidden images: greyscale + reduced opacity in the modal grid AND in the
   floating preview overlay. The image is still visible (so the user can
   see what they're un-hiding), just visibly de-emphasised. */
.image-tile.is-hidden img { filter: grayscale(1); opacity: 0.55; }
.image-tile .hidden-badge {
  position: absolute;
  top: 4px; left: 4px;
  background: rgba(20, 20, 20, 0.78);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  padding: 2px 6px;
  border-radius: 4px;
  text-transform: uppercase;
}
#image-preview-img.is-hidden { filter: grayscale(1); opacity: 0.55; }
.image-tile .remove {
  position: absolute;
  top: 4px; right: 4px;
  background: rgba(0,0,0,0.55);
  color: white;
  border: none;
  border-radius: 50%;
  width: 20px; height: 20px;
  font-size: 14px; line-height: 1;
  cursor: pointer; display: none;
}
.image-tile:hover .remove { display: block; }

/* Reusable hover share affordance — bottom-left circular pill that
 * fades in when its parent surface is hovered. Visible at most one at
 * a time per tile, so its z-index sits just above the image but
 * below the cover-badge / remove-X. The class is injected by JS
 * (image-actions.js → makeShareOverlayButton); hidden entirely on
 * browsers without Web Share support. */
.share-overlay-btn {
  position: absolute;
  bottom: 6px;
  left: 6px;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(0, 0, 0, 0.15);
  color: #111;
  border-radius: 50%;
  cursor: pointer;
  opacity: 0;
  transform: scale(0.85);
  transition: opacity 0.12s, transform 0.12s, background-color 0.12s;
  z-index: 3;
  padding: 0;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
}
.share-overlay-btn:hover {
  background: #fff;
  transform: scale(1);
  opacity: 1;
}
.share-overlay-btn:focus-visible {
  outline: 2px solid var(--accent, #2563eb);
  outline-offset: 2px;
  opacity: 1;
  transform: scale(1);
}
/* Reveal on parent hover. Multiple host classes share the same
 * affordance — only the surface needs to add the wrapper hover rule
 * (or rely on the existing :hover state). */
.image-tile:hover .share-overlay-btn,
.gallery-3d-stage:hover .share-overlay-btn,
.box3d-lightbox-panel:hover .share-overlay-btn {
  opacity: 1;
  transform: scale(1);
}

.modal-footer {
  padding: 16px 24px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 10px;
}
.footer-spacer { flex: 1; }

/* 3D builder's "X of 6 faces filled" status pill. Sits between the spacer
   and the Save button so the user always sees it. Red-tinted while
   incomplete, green when ready. */
.box3d-faces-status {
  font-size: 12px;
  font-weight: 600;
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}
.box3d-faces-status.incomplete {
  background: var(--danger-tint);
  color: var(--danger);
  border: 1px solid var(--danger);
}
.box3d-faces-status.ready {
  background: var(--green-tint);
  color: var(--green);
  border: 1px solid var(--green);
}
/* Save button disabled state — visually clear it can't be clicked. */
#btn-box3d-save:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ── Quick image preview (over the Edit Game modal) ──────────────────── */

/* Sits above the standard Edit Game modal (z 200) — quick preview only,
   no zoom slider / no edit button. Right/left arrows + close + delete +
   image name. Same right-click menu as the small thumbnail grid. */
.image-preview-modal { z-index: 300; padding: 24px; }
.image-preview-modal .modal-backdrop {
  background: rgba(15, 23, 28, 0.55);
}
.image-preview-panel {
  position: relative;
  z-index: 1;
  background: var(--white);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  width: min(720px, 92vw);
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 14px 14px 14px;
  gap: 8px;
}
.image-preview-stage {
  position: relative;
  width: 100%;
  flex: 1;
  min-height: 0;
  display: grid;
  place-items: center;
}
.image-preview-stage img {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
  border-radius: 6px;
  background: var(--light);
}
.image-preview-name {
  margin: 4px 0 0;
  background: rgba(0, 0, 0, 0.85);
  color: var(--on-dark);
  padding: 6px 18px;
  border-radius: var(--radius-pill);
  font-weight: 600;
  font-size: 13px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 80%;
  cursor: text;
  user-select: none;
  transition: background 0.15s;
}
.image-preview-name:hover { background: rgba(0, 0, 0, 0.95); }
.image-preview-name.editing { padding: 4px 12px; user-select: text; }
.image-preview-name input {
  background: transparent;
  border: none;
  color: var(--on-dark);
  font: inherit;
  font-weight: 600;
  text-align: center;
  width: 100%;
  outline: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.45);
  padding: 2px 0;
}

/* The image-preview pill + its chevron sit on one row, centred. */
.image-preview-name-row {
  display: flex;
  align-items: center;
  gap: 4px;
  justify-content: center;
  margin: 4px 0 0;
}

/* Chevron button next to the name pill — opens the preset-name menu.
 * Matches the dark pill's tone so it reads as part of the same control. */
.name-preset-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  background: rgba(0, 0, 0, 0.6);
  color: var(--on-dark);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
  opacity: 0.85;
  transition: opacity 0.15s, background 0.15s;
}
.name-preset-trigger:hover { opacity: 1; background: rgba(0, 0, 0, 0.85); }
.name-preset-trigger[aria-expanded="true"] { background: var(--green); color: var(--on-dark); }

/* Floating popover anchored to the chevron — dark themed pill list.
 * z-index has to clear the gallery + image-preview modals (z-index 200 /
 * 300) and the context menu (1500). */
/* Slip-cover preview (3D Box modal → Slip Cover tab). The SVG
 * shows the unfolded strip at the correct ASPECT (panel widths
 * proportional to box dims) so the user sees seam positions
 * before they download. The container caps the height; the SVG
 * itself uses preserveAspectRatio so wide boxes never overflow. */
.slip-intro { margin-bottom: 10px; }
.slip-preview-wrap {
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px;
  margin-bottom: 14px;
  overflow: hidden;
  display: flex;
  justify-content: center;
}
.slip-preview {
  width: 100%;
  max-height: 280px;
  display: block;
}
.slip-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 8px;
}
.slip-actions .footer-spacer { flex: 1 0 auto; }

/* Print-tab inline actions: size dropdown on the left, Print +
   Download PDF on the right, all on the same row directly under
   the preview. */
.print-actions-row {
  display: flex;
  gap: 12px;
  align-items: end;
  flex-wrap: wrap;
  margin: 6px 0 8px;
}
.print-actions-row .footer-spacer { flex: 1 0 auto; }
.print-size-label { min-width: 280px; }

/* Two-pane Print preview: dieline on the left, assembled-box
   iso render + rulers on the right. The dieline takes the
   remaining space; the right pane is sized to fit the largest
   preset (miniature, ~116x153x44 mm) at the configured pxPerMm
   plus rulers. */
.print-preview-row {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  margin-bottom: 8px;
}
.print-preview-row .slip-preview-wrap {
  flex: 1 1 auto;
  min-width: 0;
}
.print-3d-wrap {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Width matches the iso stage so the horizontal ruler aligns
     with the box column. */
  width: 230px;
}
.print-3d-stage {
  position: relative;
  display: grid;
  /* 2×2 layout — vertical ruler in column 1 (left), box in
     column 2; horizontal ruler under column 2 only so its 0
     tick aligns with the box's left edge. */
  grid-template-columns: 28px 1fr;
  grid-template-rows: 1fr 24px;
  gap: 0;
  height: 280px;
  padding: 6px;
  border: 1px solid var(--border, rgba(0,0,0,0.15));
  border-radius: 6px;
  background: var(--white, #fff);
  /* Orthographic (parallel) projection. Explicit `none` — relying
     on the absence of `perspective` is fine, but stating the
     intent here keeps it from being silently re-introduced by a
     theme stylesheet or a future tweak. */
  perspective: none;
}
.print-3d-ruler-v {
  grid-row: 1; grid-column: 1; align-self: stretch;
  /* Must match the box's `bottom` offset so v-ruler 0 stays at
     the same y as the front-bottom-left vertex. Tight 14px gap
     here (vs 36px on the left) is fine because the iso
     projection has no downward overhang — the pivot vertex is
     the lowest visible point on the box. */
  margin-bottom: 14px;
}
.print-3d-box-cell {
  grid-row: 1;
  grid-column: 2;
  /* Absolute-position the box inside this cell rather than relying
     on flex — flex sized the visual box by the CSS bounding box,
     and any subtle layout shift (preset switch, font load, etc.)
     could nudge the bottom-left vertex away from where the rulers
     describe (0,0). With absolute `left`/`bottom` the corner is
     pinned to the cell coordinate, period. */
  position: relative;
}
.print-3d-corner { grid-row: 2; grid-column: 1; }
.print-3d-ruler-h {
  grid-row: 2; grid-column: 2; align-self: start;
  /* Mirror box's `left` offset so the 0 tick sits under the
     box's bottom-front-left vertex. 36px is wide enough for the
     iso projection's leftward overhang (~0.53×depth ≈ 28px on
     miniature) to fit between the v-ruler scale line and the
     box's leftmost visible vertex (BL), while keeping both
     rulers symmetrically spaced from the box's pivot corner. */
  margin-left: 36px;
}
.print-3d-box {
  /* Anchored to the cell's bottom-left via absolute positioning
     so the bottom-left vertex sits at a fixed screen position
     regardless of preset (mini vs super-mini have different
     CSS w/h, but `bottom` + `left` pin one corner). */
  position: absolute;
  left: 36px;        /* gap from vertical ruler — wide enough to
                        absorb the iso projection's leftward
                        overhang (~0.53×depth) */
  bottom: 14px;      /* gap from horizontal ruler — tighter than
                        left because the iso projection has no
                        downward overhang past the FL pivot */
  width: var(--w, 100px);
  height: var(--h, 130px);
  /* Pivot at the FRONT-bottom-left vertex. The z-component is
     half the depth, set inline by JS to avoid CSS calc()/var()
     edge cases that varied between presets. */
  transform-style: preserve-3d;
  /* +32deg shows front + LEFT face (the user-preferred 3/4
     direction). The iso projection's leftward overhang of
     ~0.53×depth past the pivot is absorbed by the 36px
     box-cell padding — the BL vertex falls in the gap, not on
     the ruler. */
  transform: rotateX(-22deg) rotateY(32deg);
  transition: width 0.25s ease, height 0.25s ease;
}
.print-3d-stage .box3d-face {
  background-color: #ddd;
  /* Stretch (not cover) so the face matches the PDF output —
     Pillow's resize() in the PDF renderer scales the source
     image to the panel's exact w×h, so previewing with `cover`
     would crop differently from the printed result. The mini
     preset's faces have a different aspect ratio than the
     original game box, so cropping shows up as a top-of-spine
     cutoff on the side panels. */
  background-size: 100% 100%;
  background-position: center;
  border: 1px solid rgba(0,0,0,0.18);
  box-shadow: inset 0 0 18px rgba(0,0,0,0.15);
}
/* Rulers — vertical sits in column 2 of the iso stage,
   horizontal sits below as a sibling spanning the full width.
   `currentColor` lets themes recolor the ticks via the ambient
   text colour. */
.print-3d-ruler {
  color: var(--text, #333);
  user-select: none;
  pointer-events: none;
  flex: 0 0 auto;
}
/* Vertical ruler: bottom-right-anchor the SVG inside the div so
   the SVG's bottom edge (= 0 tick) sits at the div's bottom edge.
   `height: 100%` is intentionally omitted here — combined with
   the `margin-bottom: 14px` on the class rule it would push the
   div's bottom 14px BELOW the row (margin extending outside),
   making the SVG anchor to row.bottom and the box appear to
   float above the 0 mark. With only `align-self: stretch` from
   the class rule, the div correctly sizes to row.height minus
   the margin, putting div.bottom at the same y as box.bottom. */
#print-3d-ruler-v {
  width: 28px;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
}
#print-3d-ruler-h { width: 100%; height: 24px; }
.print-flap-label { min-width: 200px; }

/* Spine Designer modal — focused stage with the front face image
   and a single drag-to-create selection rectangle. The rectangle
   tracks the IMAGE's bounding rect, not the stage's, so the
   image's natural aspect can letterbox inside the modal without
   throwing off coords. */
.spine-designer-body {
  /* Other modals wrap their body in .modal-panel for the white
     backdrop + shadow + radius — this modal puts those styles on
     .modal-body directly because there's no inner panel element.
     Widened (was 880 / 1080 max) so the elements rail on the left,
     the canvas in the middle, and the tool toolbar above no longer
     crowd each other — the BoxCraft /3d page hosts this modal
     full-viewport via iframe, so the bigger footprint is fine. */
  position: relative;
  background: var(--white);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  max-width: min(1440px, 96vw);
  width: 1280px;
  max-height: 94vh;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: hidden;
}
/* Action row at the bottom of the modal-body — pinned via
   flex-shrink:0 so it stays visible even when the active panel's
   stage + toolbar otherwise crowd it out. */
.spine-designer-actions { flex-shrink: 0; }
.spine-designer-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  background: var(--light, #2a2a2a);
  border: 1px solid var(--border, rgba(0,0,0,0.2));
  border-radius: 6px 6px 0 0;
  display: flex;
  align-items: safe center;
  justify-content: safe center;
  user-select: none;
  cursor: crosshair;
  overscroll-behavior: contain;
}
.spine-designer-stage[data-panning="true"] { cursor: grabbing; }
.spine-designer-canvas {
  position: relative;
  flex: 0 0 auto;
}
#spine-designer-front {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;   /* let stage handle pointer events */
  -webkit-user-drag: none;
}
.spine-designer-overlays {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.spine-designer-hint {
  display: inline-block;
  margin-left: 6px;
  opacity: 0.75;
  font-style: italic;
}
/* Designed-spine workflow is folio-only — auto-stack on the spine
   of a folio is the souvenir-rebuild's sweet spot. Hide the
   trigger button for boxes/cards so the UI only surfaces what's
   relevant for the active box kind. */
.box3d-body:not([data-kind="portfolio"]) #btn-design-spine {
  display: none;
}
#spine-designer-modal .modal-tabs {
  /* Pill-style tabs in the accent colour — the underline-style
     used elsewhere is too quiet against the live front-face image
     which fills most of the modal. Pills give the active state a
     solid fill so it reads regardless of what's behind. */
  margin: 0 0 8px 0;
  border-bottom: none;
  gap: 6px;
}
#spine-designer-modal .modal-tabs button {
  background: var(--off, #f0f0f0);
  border: 1px solid var(--border, rgba(0,0,0,0.15));
  border-bottom: 1px solid var(--border, rgba(0,0,0,0.15));
  border-radius: var(--radius-pill);
  color: var(--text, #333);
  padding: 6px 16px;
  font-size: 13px;
  font-weight: 600;
}
#spine-designer-modal .modal-tabs button:hover {
  background: var(--green-tint, #d6ede7);
  color: var(--text, #222);
}
#spine-designer-modal .modal-tabs button[aria-selected="true"] {
  background: var(--green);
  border-color: var(--green);
  color: #fff;
}
#spine-designer-modal .modal-tabs button[aria-selected="true"]:hover {
  background: var(--green-hover);
  border-color: var(--green-hover);
}
.spine-designer-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1 1 auto;
  min-height: 0;
  /* Allow the active panel's contents to scroll vertically when
     they'd otherwise push the action row out of view — happens at
     high zoom in Select and when Erase reveals brush controls in
     Arrange. The stage inside still has its own overflow:auto for
     pan/scroll within the image itself.

     Was overflow-y: auto — that caused the .canvas-toolbar bar to
     drop below the viewport on shorter screens (the panel scrolled
     internally instead of forcing the stage to shrink). Switched
     to hidden so the stage's flex:1 always yields to whatever room
     is left after the bar is laid out, keeping zoom + rotate
     visible without scrolling. */
  overflow-y: hidden;
}
.spine-designer-panel.hidden { display: none; }
/* Hint paragraph above the preview — collapse the default <p>
   margin so the dark preview box sits right under the tab strip
   instead of being pushed down by ~40px of browser whitespace.
   Smaller font-size + tighter line-height keeps it readable but
   compact, freeing up vertical room so the bottom canvas-toolbar
   sits within the viewport without scrolling. */
.spine-designer-panel > p.muted.small {
  margin: 0;
  font-size: 11px;
  line-height: 1.3;
}
.spine-composer-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  /* Stage + bottom .canvas-toolbar form a single visual panel
     (matches BoxPerfect's .canvas-wrap pattern). Stage rounds the
     top corners only; the toolbar rounds the bottom. No min/max
     height — flex:1 + min-height:0 lets the stage take exactly the
     remaining column height after the bar, so the bar always sits
     within the viewport regardless of modal height. */
  overflow: auto;
  background: var(--light, #2a2a2a);
  border: 1px solid var(--border, rgba(0,0,0,0.15));
  border-radius: 6px 6px 0 0;
  display: flex;
  align-items: safe center;
  justify-content: safe center;
  padding: 24px;
}
.spine-composer-canvas {
  position: relative;
  flex: 0 0 auto;
  background: #888;     /* overridden inline with the flap or bg colour */
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
  border-radius: 2px;
}
.spine-composer-toolbar,
.spine-designer-toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 2px 10px;
  /* Single-row toolbar with horizontal scroll. flex-wrap would
     push later buttons (Wallpaper, Rotate, Download) off the
     bottom of narrow modals where vertical space is already
     tight; horizontal scroll keeps every button reachable. */
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  /* Stop labels (Sharpen / Wallpaper / Brush / Background /
     Zoom / Rotate) collapsing onto two lines inside their flex
     children — we want each whole group to stay together. */
  white-space: nowrap;
}
.spine-composer-toolbar > *,
.spine-designer-toolbar > * {
  flex-shrink: 0;
}
.zoom-slider-label,
.rotation-slider-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text, #333);
}
.zoom-slider-label input[type="range"],
.rotation-slider-label input[type="range"] {
  width: 120px;
  vertical-align: middle;
}
/* Side Panel Designer's preview footer mirrors BoxPerfect's
   .canvas-toolbar exactly so the two editors share a single bar
   convention — same buttons, same relative positions, same
   coloured bar across the bottom. The ⟳ canvas-orientation toggle
   stays in the stage's bottom-right corner (it acts on the
   canvas, not a selected element). */
.spine-designer-center .canvas-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px;
  flex-wrap: wrap;
  background: var(--white, #fff);
  border: 1px solid var(--border, rgba(0,0,0,0.15));
  border-top: 0;
  border-radius: 0 0 6px 6px;
  flex-shrink: 0;
}
.spine-designer-center .canvas-zoom-controls {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 12px;
}
.spine-designer-center .canvas-zoom-controls input[type="range"] {
  width: 120px;
}
.spine-designer-center .canvas-toolbar .btn-icon {
  font-size: 18px;
  line-height: 1;
  padding: 8px 12px;
  min-width: 40px;
}
.spine-designer-center .canvas-toolbar .btn-icon:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.spine-designer-center .canvas-toolbar-spacer { flex: 1; }
/* Save / Undo on the canvas-toolbar — Save uses BoxPerfect's
   magenta colour, Undo is ghost. Both hidden until the user
   edits anything; .hidden toggles by JS. Mirrors BP's
   #btn-undo-edit / #btn-save-collection pair on its own
   canvas-toolbar. */
.btn-save-magenta {
  background: #c2185b;
  color: #fff;
  border: 1px solid #c2185b;
  border-radius: var(--radius-pill);
  padding: 9px 20px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background .15s, transform .1s;
}
.btn-save-magenta:hover {
  background: #a01546;
  border-color: #a01546;
  transform: translateY(-1px);
}
.btn-save-magenta:disabled { opacity: .5; cursor: not-allowed; transform: none; }
.btn-save-magenta.hidden,
.btn-spine-undo.hidden { display: none; }
/* Export section — mirrors BoxPerfect's right-aside Export panel
   exactly: 11px uppercase heading with 0.08em tracking sitting
   above a flex row of select (auto width) + Download button
   (flex-grow). Sized to ~240 px so it reads as a compact widget
   on the right side of the action footer rather than spreading
   across it. */
.spine-export-section {
  display: block;
  width: 240px;
  flex-shrink: 0;
  text-align: left;
}
.spine-export-section h3 {
  margin: 0 0 10px 0;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--mid, #777);
}
.spine-export-section .export-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
/* PNG / JPEG dropdown — visually identical to the right-rail
   EDIT tool buttons (.spine-composer-toolbar .tool-grid > .btn-ghost):
   same panel-tint background, 1.5 px border, 6 px radius, 9 / 10
   padding, 13 px / 600 font, so the action-footer chip reads as
   the same chip pattern as the Grid / Background / Eraser / Invert
   row in the rail. appearance:none + cursor:pointer carry the
   select-specific tweaks. */
.spine-export-section .export-row select {
  flex: 0 0 auto;
  appearance: none;
  -webkit-appearance: none;
  background: var(--white, #fff);
  color: var(--text, #222);
  border: 1.5px solid var(--border, rgba(0,0,0,0.15));
  border-radius: 6px;
  padding: 9px 10px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.spine-export-section .export-row select:hover {
  background: var(--white, #fff);
  border-color: var(--green, #009572);
}
.spine-export-section .export-row select:focus {
  outline: none;
  border-color: var(--green, #009572);
  box-shadow: 0 0 0 3px var(--green-tint, rgba(0,149,114,0.18));
}
/* Download button — overrides .btn-green's own metrics so the
   final box exactly mirrors BP's .btn + .btn-primary: 9 / 18
   padding, flex-grow to fill the row, hardcoded #fff so the
   label stays white under every theme (BC's var(--white) is
   theme-driven and goes dark on dark themes, which is what made
   the BC Download text read differently from BP's). */
.spine-export-section .export-row button {
  flex: 1;
  padding: 9px 18px;
  color: #fff;
}
/* Stack: stage column + tool rail column inside each panel. The
   outer .spine-designer-main already provides the Elements rail on
   the LEFT; this adds the symmetric tool rail on the RIGHT so the
   panel reads Elements → Canvas → Tools. */
.spine-designer-stack {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  gap: 12px;
  align-items: stretch;
}
.spine-designer-center {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* Positioning context for the floating zoom-row pill — see
     .spine-(designer|composer)-zoom-row above. */
  position: relative;
}
/* Override the horizontal-toolbar defaults when the toolbar sits
   inside .spine-designer-stack — stack its children vertically and
   give it a fixed-width right rail. */
.spine-designer-stack .spine-designer-toolbar,
.spine-designer-stack .spine-composer-toolbar {
  flex-direction: column;
  align-items: stretch;
  flex-wrap: nowrap;
  gap: 12px;
  width: 230px;
  flex: 0 0 230px;
  max-height: 80vh;
  overflow-x: hidden;
  overflow-y: auto;
  white-space: normal;
  /* Mirror BoxPerfect's right-aside .panel — primary panel
     surface bg, 1px border, modest radius, soft shadow. BC's
     --white is the same value as BP's --panel across every
     theme, so swapping --off → --white lines the rail (and the
     button tiles below) up with BP's tonal level rather than
     sitting one step toward the page bg. */
  background: var(--white, #fff);
  border: 1px solid var(--border, rgba(0,0,0,0.15));
  border-radius: 12px;
  padding: 14px;
  box-shadow: var(--shadow-card,
    0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06));
}
.spine-designer-stack .spine-designer-toolbar > *,
.spine-designer-stack .spine-composer-toolbar > * {
  width: 100%;
  flex-shrink: 0;
}
/* Right-rail tool buttons — mirror BoxPerfect's .tool-btn
   pattern from the editor's edit-tools panel: tinted panel bg,
   1.5px border, small radius (NOT pill), 13px / 600 font, accent
   border on hover, accent halo when pressed. Same visual
   language as the BP edit/select tools so the two editors share
   one tool-rail aesthetic. Buttons inside `<label>` micro-groups
   (Brush S/M/L row) opt out — they're intentionally compact. */
.spine-designer-stack .spine-designer-toolbar > .btn-ghost,
.spine-designer-stack .spine-composer-toolbar > .btn-ghost,
.spine-designer-stack .spine-composer-toolbar .tool-grid > .btn-ghost {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 9px 10px;
  /* Sit on the same primary-panel tone as the rail (BC's --white
     = BP's --panel). Hover shifts to --off, mirroring BP's
     panel → panel-2 hover. */
  background: var(--white, #fff);
  color: var(--text, #222);
  border: 1.5px solid var(--border, rgba(0,0,0,0.15));
  border-radius: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  user-select: none;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.spine-designer-stack .spine-designer-toolbar > .btn-ghost:hover,
.spine-designer-stack .spine-composer-toolbar > .btn-ghost:hover,
.spine-designer-stack .spine-composer-toolbar .tool-grid > .btn-ghost:hover {
  background: var(--off, #f7f8f6);
  border-color: var(--green, #009572);
}
.spine-designer-stack .spine-designer-toolbar > .btn-ghost:disabled,
.spine-designer-stack .spine-composer-toolbar > .btn-ghost:disabled,
.spine-designer-stack .spine-composer-toolbar .tool-grid > .btn-ghost:disabled {
  /* Lighter dim (was 0.45) so the disabled text stays a clearly
     legible greyish-white rather than washing into a muddy mid-
     tone. Matches BP's lighter perceived weight on disabled
     tool buttons. */
  opacity: 0.7;
  cursor: not-allowed;
}
.spine-designer-stack .spine-designer-toolbar > .btn-ghost:disabled:hover,
.spine-designer-stack .spine-composer-toolbar > .btn-ghost:disabled:hover,
.spine-designer-stack .spine-composer-toolbar .tool-grid > .btn-ghost:disabled:hover {
  background: var(--white, #fff);
  border-color: var(--border, rgba(0,0,0,0.15));
}
/* Wrapping label-groups (Face / Sharpen / Wallpaper / Brush /
   Background / Rotate) — let their children flow onto multiple
   rows inside the narrow rail. */
.spine-designer-stack .spine-designer-toolbar > label,
.spine-designer-stack .spine-composer-toolbar > label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}
.spine-designer-stack .spine-designer-toolbar > label > span:first-child,
.spine-designer-stack .spine-composer-toolbar > label > span:first-child {
  /* In-label section heading — mirrors .panel-section h3
     metrics so Brush / Background read the same as Edit. */
  flex-basis: 100%;
  margin: 0 0 10px 0;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--mid, #777);
}
.spine-designer-stack .spine-composer-toolbar .rotation-slider-label input[type="range"],
.spine-designer-stack .spine-composer-toolbar .spine-paint-label select {
  width: 100%;
  min-width: 0;
}
.rotation-slider-label input[type="range"]:disabled,
.rotation-slider-label button:disabled {
  opacity: 0.4;
}
#select-zoom-value,
#arrange-zoom-value,
#rotation-value {
  min-width: 38px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* Inner content layer for composer elements — rotated independently
   of the bbox so the handles stay axis-aligned for drag/resize. */
.spine-composer-element .content {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: 100% 100%;
  transform-origin: 50% 50%;
}
/* Pressed / active state on right-rail tool buttons — mirrors
   BoxPerfect's .tool-btn[data-active] accent halo: green-tint
   fill, green border, plus a double-stop box-shadow that reads
   as a soft outer glow. Far stronger visual feedback than the
   plain tint we had before, so a user glances at the rail and
   immediately sees which tools are armed. */
.spine-designer-stack .spine-designer-toolbar > .btn-ghost[aria-pressed="true"],
.spine-designer-stack .spine-composer-toolbar > .btn-ghost[aria-pressed="true"],
.spine-designer-stack .spine-composer-toolbar .tool-grid > .btn-ghost[aria-pressed="true"] {
  background: var(--green-tint, #d6ede7);
  border-color: var(--green, #009572);
  color: var(--text, #222);
  box-shadow: 0 0 0 2px var(--green, #009572),
              0 0 14px 1px var(--green, #009572);
}
.spine-paint-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text, #333);
}
.spine-paint-label select {
  font: inherit;
  padding: 3px 6px;
  border: 1px solid var(--border, rgba(0,0,0,0.2));
  border-radius: 4px;
}
.spine-bg-swatch {
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 3px;
  border: 1px solid rgba(0,0,0,0.25);
  background: #ccc;
}
/* Grid overlay — drawn purely on the composer (editor-only); never
   sent to the bitmap server or rendered into PDF/iso. SVG-style
   stripe via repeating-linear-gradient at 1px line thickness so the
   grid stays crisp at any zoom. Cell size set via --grid-step-px. */
.spine-composer-canvas .spine-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(to right,
      rgba(255,255,255,0.18) 0,
      rgba(255,255,255,0.18) 1px,
      transparent 1px,
      transparent var(--grid-step-px, 40px)),
    repeating-linear-gradient(to bottom,
      rgba(255,255,255,0.18) 0,
      rgba(255,255,255,0.18) 1px,
      transparent 1px,
      transparent var(--grid-step-px, 40px));
}
/* Snap guides — transient, rendered only during a drag where an
   alignment hit is active. Bright accent stroke that stays visible
   against any background. */
.spine-composer-canvas .spine-snap-guide {
  position: absolute;
  pointer-events: none;
  background: var(--green, #009572);
  box-shadow: 0 0 0 0.5px rgba(0,0,0,0.4);
}
.spine-composer-canvas .spine-snap-guide.vertical {
  width: 1px;
  top: 0; bottom: 0;
}
.spine-composer-canvas .spine-snap-guide.horizontal {
  height: 1px;
  left: 0; right: 0;
}
.spine-composer-stage[data-eyedrop="true"] .spine-composer-element {
  cursor: crosshair;
}
/* In erase mode hide the OS cursor on the CANVAS (not the whole
   stage) so the bottom + side scrollbars stay reachable when zoom
   makes the canvas overflow. The brush-preview overlay shows
   where + how much will be erased over the canvas area. */
.spine-composer-stage[data-erase="true"] .spine-composer-canvas,
.spine-composer-stage[data-erase="true"] .spine-composer-element {
  cursor: none;
}
.spine-eraser-cursor {
  position: absolute;
  border: 1.5px solid #fff;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.55);
  background: rgba(255,255,255,0.08);
  border-radius: 50%;
  pointer-events: none;
  transform: translate(-50%, -50%);
  display: none;
  z-index: 30;
}
.spine-eraser-cursor.visible { display: block; }
.composer-corner-btn {
  position: absolute;
  right: 10px;
  bottom: 10px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--border, rgba(0,0,0,0.18));
  background: var(--white, #fff);
  color: var(--text, #333);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0,0,0,0.25);
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
}
.composer-corner-btn:hover {
  background: var(--green, #009572);
  color: #fff;
  border-color: var(--green, #009572);
}
.composer-corner-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.composer-corner-btn:disabled:hover {
  background: var(--white, #fff);
  color: var(--text, #333);
  border-color: var(--border, rgba(0,0,0,0.18));
}
/* Canvas-orientation toggle (⟳) stays in the bottom-right corner
   per the user's "consistency" pass — it acts on the canvas, not
   the selected element, so it lives with the canvas rather than
   in the editing footer. ±90° element rotate moved to the
   .canvas-toolbar bottom bar. */
.corner-canvas-rotate { right: 10px; bottom: 10px; }
/* Free-rotation hook — a draggable circle on a short stem that
   dangles below the primary-selected element. Sits OUTSIDE the
   element's bbox so it doesn't overlap the artwork; rotates with
   the element since it's a child of the rotated div. Stem is
   absolutely positioned, hook is at the stem's far end. */
.spine-composer-element .rotate-hook {
  position: absolute;
  left: 50%;
  top: 100%;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  margin-top: 22px;
  background: #fff;
  border: 2px solid var(--green, #009572);
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0,0,0,0.35);
  cursor: grab;
  z-index: 25;
}
.spine-composer-element .rotate-hook:active { cursor: grabbing; }
.spine-composer-element .rotate-hook::before {
  /* Stem connecting the element's bottom edge to the hook. */
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 2px;
  height: 20px;
  margin-left: -1px;
  background: var(--green, #009572);
  border-radius: 1px;
}
/* Tool section header — mirrors BoxPerfect's .panel-section h3
   exactly: 11px / 700 / uppercase / 0.08em letter-spacing, mid-
   grey, with a 10px gap to the controls below it. */
.tool-section { display: block; }
.tool-section h4 {
  margin: 0 0 10px 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mid, #777);
}
.tool-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}
/* Background button removes the BG — borrow the BP editor's
   diagonal-cross treatment so the "negated" intent reads at a
   glance. Slash is hidden once the button is pressed (the
   accent fill already conveys "active"). */
.btn-bg-cross { position: relative; }
.btn-bg-cross:not([aria-pressed="true"])::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to bottom right,
    transparent calc(50% - 0.9px),
    currentColor calc(50% - 0.9px),
    currentColor calc(50% + 0.9px),
    transparent calc(50% + 0.9px));
  opacity: 0.6;
  border-radius: inherit;
}
/* Background-colour row: swatch + half-width select + eyedrop all
   on one line. The label heading ("Background") sits above. */
.spine-paint-row {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
}
.spine-paint-row .spine-bg-swatch { flex: 0 0 auto; }
.spine-paint-row .bg-color-half {
  flex: 1 1 0;
  min-width: 0;
  font: inherit;
  padding: 3px 6px;
  border: 1px solid var(--border, rgba(0,0,0,0.2));
  border-radius: 4px;
}
.spine-paint-row #btn-spine-bg-eyedrop { flex: 0 0 auto; }
.face-toggle-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text, #333);
}
.face-toggle-label .btn-ghost.active {
  background: var(--green, #009572);
  border-color: var(--green, #009572);
  color: #fff;
}
.face-toggle-label .btn-ghost[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}
/* Face badge on chip — small tag indicating which face the
   element was picked from. Helps tell elements apart in the
   list when sources are mixed. */
.spine-designer-list-item .face-badge {
  display: inline-block;
  font-size: 9px;
  font-weight: 700;
  padding: 1px 4px;
  border-radius: 6px;
  background: rgba(0,0,0,0.08);
  color: var(--text-muted, #666);
  letter-spacing: 0.4px;
  margin-left: 4px;
}
.eraser-size-label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--text, #333);
}
.eraser-size-label[hidden],
.eraser-size-label.hidden { display: none; }
.eraser-size-label .btn-ghost {
  min-width: 22px;
  padding: 4px 6px;
}
.eraser-size-label .btn-ghost.active {
  background: var(--green-tint, #d6ede7);
  border-color: var(--green, #009572);
  color: var(--green, #009572);
}
.sharpen-controls {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--text, #333);
}
.sharpen-count {
  min-width: 14px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--green, #009572);
}
.wallpaper-controls {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--text, #333);
}
.spine-composer-element {
  position: absolute;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  cursor: move;
  outline: 1px dashed rgba(255,255,255,0.45);
  outline-offset: -1px;
}
.spine-composer-element.selected {
  outline: 2px solid var(--accent, #3aa3b0);
  outline-offset: -2px;
}
/* Primary selection (last-clicked) gets a slightly thicker outline
   so the user can tell which element the toolbar is mirroring. */
.spine-composer-element.selected.primary {
  outline-width: 3px;
}
.spine-composer-element[data-index="0"] { outline-color: #3aa3b0; }
.spine-composer-element[data-index="1"] { outline-color: #e8a23a; }
.spine-composer-element[data-index="2"] { outline-color: #d04a7a; }
.spine-composer-element[data-index="3"] { outline-color: #7a3ad0; }
.spine-composer-element[data-index="4"] { outline-color: #3ad07a; }
.spine-composer-element .handle {
  position: absolute;
  width: 10px; height: 10px;
  background: #fff;
  border: 1px solid #222;
  border-radius: 2px;
}
.spine-composer-element .handle.nw { top: -5px; left: -5px; cursor: nwse-resize; }
.spine-composer-element .handle.ne { top: -5px; right: -5px; cursor: nesw-resize; }
.spine-composer-element .handle.sw { bottom: -5px; left: -5px; cursor: nesw-resize; }
.spine-composer-element .handle.se { bottom: -5px; right: -5px; cursor: nwse-resize; }
.spine-composer-element .label {
  position: absolute;
  top: -1px; left: -1px;
  background: currentColor;
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 2px 2px 2px 0;
  line-height: 1.2;
}
.spine-designer-rect {
  position: absolute;
  border: 2px solid currentColor;
  background: color-mix(in srgb, currentColor 18%, transparent);
  box-sizing: border-box;
  color: #3aa3b0;
  /* Overlay parent has pointer-events: none so the user can drag
     to draw new rects across existing ones; rects re-enable
     pointer-events so right-click can target them directly. The
     stage's pointerdown handler still gets left-click drags via
     bubbling, so drag-to-select keeps working. */
  pointer-events: auto;
}
.spine-designer-rect[data-pending="true"] {
  /* Pending (in-flight drag) rect mustn't intercept its own
     pointer events or the drag would stutter. */
  pointer-events: none;
}
.spine-designer-rect[data-pending="true"] { color: #3aa3b0; }   /* in-progress drag */
.spine-designer-rect[data-index="0"] { color: #3aa3b0; }       /* teal */
.spine-designer-rect[data-index="1"] { color: #e8a23a; }       /* amber */
.spine-designer-rect[data-index="2"] { color: #d04a7a; }       /* magenta */
.spine-designer-rect[data-index="3"] { color: #7a3ad0; }       /* purple */
.spine-designer-rect[data-index="4"] { color: #3ad07a; }       /* green */
.spine-designer-rect-label {
  position: absolute;
  top: -1px; left: -1px;
  background: currentColor;
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  padding: 1px 5px;
  border-radius: 2px 2px 2px 0;
  line-height: 1.2;
}
/* Side-rail thumbnail panel — replaces the old text-chip list.
   Sits to the right of the active panel and persists across the
   Select / Arrange tab switch (the element library is the same
   regardless of tab). One thumbnail per equivalence group
   (identical src crop + face + rotation + bg + eraser strokes).
   Editing diverges into a new group → new thumbnail. */
.spine-designer-main {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  gap: 12px;
}
.spine-designer-content {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.spine-designer-thumbs {
  /* Elements rail sits on the LEFT of .spine-designer-main now;
     border + padding are on its right edge (was left when it was
     the right rail). */
  width: 200px;
  flex-shrink: 0;
  order: -1;
  border-right: 1px solid var(--border, rgba(0,0,0,0.15));
  padding: 0 12px 0 0;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.spine-designer-thumbs h4 {
  margin: 0 0 8px 0;
  font-size: 11px;
  font-weight: 700;
  color: var(--mid, #777);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  flex-shrink: 0;
}
.spine-designer-list {
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
  overflow-y: auto;
  padding-right: 4px;
  align-content: start;
}
.spine-designer-list .thumbs-empty {
  font-size: 11px;
  color: var(--mid, #888);
  font-style: italic;
}
.spine-thumb {
  position: relative;
  cursor: pointer;
  background: var(--off, #f5f5f5);
  border: 2px solid var(--border, rgba(0,0,0,0.15));
  border-radius: 6px;
  min-height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  transition: border-color 0.12s;
}
.spine-thumb img {
  max-width: 100%;
  max-height: 80px;
  object-fit: contain;
  display: block;
}
.spine-thumb:hover { border-color: var(--green, #009572); }
.spine-thumb.selected {
  border-color: var(--green, #009572);
  box-shadow: 0 0 0 1px var(--green, #009572);
}
.spine-thumb .thumb-count {
  position: absolute;
  top: 3px;
  right: 3px;
  background: rgba(0,0,0,0.72);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 10px;
  pointer-events: none;
}
.spine-thumb .thumb-del {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px; height: 18px;
  border: 0;
  background: rgba(255,255,255,0.88);
  color: #b03a4a;
  border-radius: 50%;
  font-size: 14px;
  cursor: pointer;
  padding: 0;
  display: none;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.spine-thumb:hover .thumb-del { display: inline-flex; }
.spine-thumb .thumb-del:hover { background: #b03a4a; color: #fff; }
.spine-designer-list-item {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 4px 3px 8px;
  border-radius: 12px;
  background: var(--white, #fff);
  border: 1px solid var(--border, rgba(0,0,0,0.15));
  font-size: 11px;
}
.spine-designer-list-item .swatch {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: 2px;
  background: currentColor;
}
.spine-designer-list-item .del {
  display: inline-flex;
  align-items: center; justify-content: center;
  width: 18px; height: 18px;
  border: 0;
  background: transparent;
  color: var(--text, #333);
  border-radius: 50%;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0;
}
.spine-designer-list-item .del:hover {
  background: rgba(176, 58, 74, 0.15);
  color: #b03a4a;
}
.spine-designer-list-item .reorder {
  display: inline-flex;
  align-items: center; justify-content: center;
  width: 16px; height: 16px;
  border: 0;
  background: transparent;
  color: var(--text, #333);
  border-radius: 3px;
  cursor: pointer;
  font-size: 9px;
  line-height: 1;
  padding: 0;
}
.spine-designer-list-item .reorder:hover:not([disabled]) {
  background: rgba(0, 0, 0, 0.08);
}
.spine-designer-list-item .reorder[disabled] {
  opacity: 0.25;
  cursor: default;
}
.spine-designer-list-item .bgtog {
  display: inline-flex;
  align-items: center; justify-content: center;
  height: 16px;
  padding: 0 5px;
  border: 1px solid var(--border, rgba(0,0,0,0.2));
  background: transparent;
  color: var(--text, #333);
  border-radius: 8px;
  cursor: pointer;
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.5px;
}
.spine-designer-list-item .bgtog:hover {
  background: rgba(0,0,0,0.06);
}
.spine-designer-list-item .bgtog[aria-pressed="true"] {
  background: var(--green, #009572);
  border-color: var(--green, #009572);
  color: #fff;
}
.spine-designer-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}
.spine-designer-actions .footer-spacer { flex: 1; }
/* Restyle the existing .icon-x close button in the spine modal so
   it matches the .gallery-close pattern from the gallery modal —
   prominent 40px circle anchored at top-right of the panel. Scoped
   to .spine-designer-body so other modals' .icon-x buttons (which
   have no CSS at all today) are unaffected. */
.spine-designer-body .icon-x {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--off, #f7f8f6);
  color: var(--text, #222);
  border: 1px solid var(--border, rgba(0,0,0,0.18));
  font-size: 24px;
  line-height: 1;
  padding: 0;
  cursor: pointer;
  z-index: 5;
  display: grid;
  place-items: center;
}
.spine-designer-body .icon-x:hover {
  background: var(--green-tint, rgba(0,149,114,0.12));
  border-color: var(--green, #009572);
  color: var(--green, #009572);
}
.print-spine-label { min-width: 220px; }
/* Block-level flex container for swatch + dropdown. Selector
   chains TWO classes so its specificity (0,2,0) beats the
   inherited `.box3d-dim span` heading rule (0,1,1) AND comes
   late enough in the cascade — without it, the 4 px margin-
   bottom + 12 px font-size that .box3d-dim span pushes onto
   every span descendant was also being applied here, leaving
   an extra 4 px tail below the controls and tightening the
   select's inner type. With align-items:end on .print-actions-
   row that 4 px lifted the select above Size's select + the
   Design Side Panel button. */
.print-flap-label .print-flap-controls {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: inherit;
  color: inherit;
}
.print-flap-label .print-flap-controls select {
  flex: 1;
  min-width: 0;
}
.print-flap-swatch {
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 3px;
  border: 1px solid rgba(0,0,0,0.25);
  background: #ccc;
  flex: 0 0 auto;
}

/* Delete 3D belongs only to the Build tab — destructive box-
   level action, would feel out of place on Edit / Print which
   are about output / fine-tuning. The :has() selector watches
   the active panel; supported in every modern browser the rest
   of the app already targets. */
#box3d-modal:not(:has([data-panel="build"].active)) #btn-box3d-delete {
  display: none !important;
}

/* Adjust-panel row. Click a panel in the preview to select it;
   ⬆ / ⬇ buttons nudge that panel's alignment by 1 mm at a time. */
.slip-align-row {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin: 4px 0 8px;
}
.slip-align-label { white-space: nowrap; }
.slip-align-selected {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--off);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  min-width: 80px;
  justify-content: center;
}
.slip-align-selected.has-selection {
  border-color: var(--green);
  background: var(--green-tint);
  color: var(--green);
}
.slip-align-arrow {
  font-size: 14px;
  padding: 2px 10px;
}
.slip-align-arrow:disabled { opacity: 0.4; cursor: not-allowed; }

/* ── Edit tab — alignment workspace ───────────────────────────
   Bigger preview surface, fat arrow buttons, zoom slider + pan
   gesture on the SVG. The Print tab keeps the compact controls. */
#edit-preview-wrap {
  position: relative;
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px;
  margin: 4px 0 10px;
  overflow: hidden;
  display: flex;
  justify-content: center;
  height: 320px;
}
#edit-preview-wrap .edit-preview {
  width: 100%;
  height: 100%;
  cursor: grab;
  display: block;
  touch-action: none;
}
#edit-preview-wrap.dragging .edit-preview { cursor: grabbing; }

.edit-adjust-row {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin: 4px 0 8px;
}
.edit-adjust-label { white-space: nowrap; }
.edit-arrow {
  appearance: none;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s, transform 0.05s, border-color 0.12s;
}
.edit-arrow:hover {
  background: var(--green-tint);
  border-color: var(--green);
  color: var(--green);
}
.edit-arrow:active { transform: translateY(1px); }
.edit-arrow:disabled {
  opacity: 0.35;
  cursor: not-allowed;
  background: var(--off);
}

.edit-zoom-row {
  display: flex;
  gap: 12px;
  align-items: center;
  margin: 4px 0 8px;
}
.edit-zoom-row input[type="range"] {
  flex: 1 1 auto;
  max-width: 320px;
  accent-color: var(--green);
}
#edit-zoom-value { min-width: 44px; font-variant-numeric: tabular-nums; }
.slip-align-value { font-variant-numeric: tabular-nums; min-width: 60px; }

/* Click capture happens at the SVG level via coordinate
   delegation (see _bindSlipPreviewClick in collection.js) — no
   per-panel hit-rect overlays. The SVG itself shows a pointer
   cursor so users know panels are clickable. */
.slip-preview { cursor: pointer; }
#slip-faces-warning:empty { display: none; }
#slip-faces-warning {
  color: var(--danger);
  background: var(--danger-tint);
  padding: 6px 10px;
  border-radius: 6px;
  margin-bottom: 10px;
}

/* Account pill in the header. Sits between Add Game and Settings.
 * Pill silhouette matches the existing filter-pill family — same
 * height, same rounded-rectangle shape — so it doesn't introduce
 * a visually-novel chrome element. The avatar is a small circle
 * filled with the theme accent. */
.account-pill {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px 6px 8px;
  background: var(--white);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.account-pill:hover {
  background: var(--off);
  border-color: var(--mid);
}
.account-avatar {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--green);
  display: inline-block;
  flex: 0 0 auto;
  box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.10);
}
.account-name {
  white-space: nowrap;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Storage tab — a single horizontal bar showing consumed / cap,
 * plus a per-category breakdown table. Each category gets a
 * coloured swatch so users can read which slice is which without
 * a legend lookup. */
.storage-bar {
  position: relative;
  width: 100%;
  height: 14px;
  border-radius: 7px;
  background: var(--off);
  border: 1px solid var(--border);
  overflow: hidden;
  margin: 8px 0 14px;
}
.storage-bar-fill {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 0%;
  background: linear-gradient(90deg, var(--green) 0%, var(--green-hover) 100%);
  transition: width 0.4s ease;
}
.storage-bar.over-cap .storage-bar-fill {
  background: linear-gradient(90deg, var(--danger) 0%, #ff8585 100%);
}
.storage-breakdown {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}
.storage-breakdown li {
  display: grid;
  grid-template-columns: 14px 1fr auto auto auto;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--white);
  font-size: 13px;
}
.storage-swatch {
  width: 12px; height: 12px;
  border-radius: 3px;
}
.storage-breakdown .label-col { font-weight: 600; }
.storage-breakdown .count-col,
.storage-breakdown .size-col,
.storage-breakdown .pct-col {
  color: var(--mid);
  font-variant-numeric: tabular-nums;
}
.storage-breakdown .description {
  grid-column: 2 / -1;
  color: var(--mid);
  font-size: 12px;
  margin-top: 2px;
}

/* Plan ladder (Account → Billing). One row per plan; the active
 * plan gets a green left border + slight tint so the user can see
 * which they're on without reading labels. Cards stack vertically
 * on narrow viewports — the modal-narrow constraint already keeps
 * them sub-500px wide. */
/* Monthly / Annual segmented toggle above the plan ladder. The
   "save ~17%" hint reinforces annual as the value pick without
   making it the default — users still default to monthly so they
   can downshift painlessly. */
.billing-period-toggle {
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: 8px;
  margin: 8px 0 12px;
}
.billing-period-btn {
  background: transparent;
  color: var(--mid);
  border: 0;
  padding: 6px 14px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.billing-period-btn:hover { color: var(--text); }
.billing-period-btn.is-active {
  background: var(--white);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.billing-period-save {
  margin-left: 6px;
  font-size: 11px;
  color: var(--green);
  font-weight: 600;
}

/* "Need more than 250 GB? Get in touch" CTA — sits between the plan
   ladder and the Manage Subscription button. Quiet styling because
   it's targeted at the ~1% of users who hit the top tier's cap, not
   a primary upgrade path. */
.billing-custom-cta {
  margin: 8px 0 12px;
  padding: 8px 12px;
  border-left: 3px solid var(--accent, var(--green));
  background: var(--off);
  border-radius: 0 4px 4px 0;
}
.billing-custom-cta a {
  color: var(--accent, var(--green));
  text-decoration: none;
  font-weight: 600;
}
.billing-custom-cta a:hover { text-decoration: underline; }

.plan-ladder {
  list-style: none;
  margin: 8px 0;
  padding: 0;
  display: grid;
  gap: 6px;
}
.plan-ladder li {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-left: 3px solid transparent;
  border-radius: 6px;
  background: var(--white);
}
.plan-ladder li.is-active {
  border-color: var(--green);
  border-left-width: 3px;
  background: var(--green-tint);
}
.plan-ladder .plan-label { font-weight: 600; }
.plan-ladder .plan-cap {
  font-variant-numeric: tabular-nums;
  color: var(--mid);
}
.plan-ladder .plan-price {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.plan-ladder .plan-tagline {
  grid-column: 1 / -1;
  color: var(--mid);
  font-size: 12px;
}
.plan-ladder .plan-active-tag {
  margin-left: 6px;
  padding: 1px 6px;
  background: var(--green);
  color: var(--white);
  font-size: 10px;
  border-radius: 8px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-weight: 700;
  vertical-align: middle;
}

/* Self-contained Upgrade-button styling. styles.css's .btn / .btn-primary
   would normally handle this but collection.html only loads collection.css —
   the styles.css base button rules never reach this page. Defined here
   so the Upgrade pill has proper green-pill button affordance regardless
   of which stylesheets the page loads. */
.plan-ladder .plan-upgrade-btn {
  appearance: none;
  background: var(--green);
  color: var(--white, #fff);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 8px 18px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background 0.15s, transform 0.05s;
  user-select: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 92px;
}
.plan-ladder .plan-upgrade-btn:hover {
  background: var(--green-hover, #1b8a47);
  color: var(--white, #fff);
}
.plan-ladder .plan-upgrade-btn:active { transform: translateY(1px); }
.plan-ladder .plan-upgrade-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

/* Profile/billing tabs — small definition list for mock data. */
.account-pairs {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 6px 16px;
  margin: 12px 0;
  font-size: 13px;
}
.account-pairs dt { font-weight: 600; color: var(--mid); }
.account-pairs dd { margin: 0; color: var(--text); }

/* Toolbar-visibility tab (Settings → Toolbar). One row per
 * toolbar-targetable control. The pill is a fixed-width track with
 * a circular thumb that slides left (off) / right (on); active
 * state fills the track with the theme accent. Click the row
 * anywhere to flip — better hit-target than just the pill. */
.toolbar-toggle-list {
  list-style: none;
  margin: 8px 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px;
}
.toolbar-toggle {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--white);
  cursor: pointer;
  font: inherit;
  font-size: 13px;
  color: var(--text);
  text-align: left;
  width: 100%;
}
.toolbar-toggle:hover { background: var(--off); }
.toolbar-toggle .toggle-label { flex: 1; }
.toolbar-toggle .toggle-pill {
  position: relative;
  width: 36px;
  height: 18px;
  border-radius: 18px;
  background: var(--border);
  flex: 0 0 auto;
  transition: background 0.15s;
}
.toolbar-toggle .toggle-thumb {
  position: absolute;
  top: 2px; left: 2px;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
  transition: transform 0.18s;
}
.toolbar-toggle[aria-pressed="true"] .toggle-pill {
  background: var(--green);
}
.toolbar-toggle[aria-pressed="true"] .toggle-thumb {
  transform: translateX(18px);
}

/* Hidden-by-preference override. The user's per-control prefs
 * apply via this class; specificity + !important beats any
 * existing dynamic show/hide that the chip-show logic uses (e.g.
 * the play / highlighted chips have their own .hidden toggling). */
[data-toolbar-key].hidden-by-pref {
  display: none !important;
}

/* Theme editor (Settings → Theme Editor). Compact two-column row
 * for base + font selectors; a wrap-friendly grid for the colour
 * pickers; a separate row for the save controls + name input. */
.theme-editor-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-bottom: 12px;
}
.theme-editor-row .field { display: block; }
.theme-editor-colors {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 8px;
  margin: 8px 0 12px;
}
.theme-editor-colors label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text);
}
.theme-editor-colors input[type="color"] {
  appearance: none;
  -webkit-appearance: none;
  width: 36px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
}
.theme-editor-colors input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.theme-editor-colors input[type="color"]::-webkit-color-swatch {
  border: none;
  border-radius: 4px;
}
.theme-editor-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 4px;
}
.theme-editor-actions .field-input { flex: 1; min-width: 200px; }

/* Name-preset editor (Settings → Image name presets). A
 * reorderable list with inline edit + remove + add. The drag handle
 * doubles as the row's primary affordance — the user grabs it,
 * dragstart fires, and a CSS class shows the dragged row at half
 * opacity. The drop targets are siblings; before/after determined
 * by midpoint comparison. */
.name-preset-editor {
  list-style: none;
  margin: 8px 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  max-height: 320px;
  overflow: auto;
}
.name-preset-editor li {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--border);
  background: var(--white);
}
.name-preset-editor li:last-child { border-bottom: none; }
.name-preset-editor li.dragging { opacity: 0.4; }
.name-preset-editor li.drop-before { border-top: 2px solid var(--green); }
.name-preset-editor li.drop-after { border-bottom: 2px solid var(--green); }
.name-preset-editor .preset-handle {
  cursor: grab;
  user-select: none;
  color: var(--mid);
  padding: 0 4px;
  font-size: 16px;
  line-height: 1;
}
.name-preset-editor .preset-handle:active { cursor: grabbing; }
.name-preset-editor input[type="text"] {
  flex: 1;
  border: 1px solid transparent;
  background: transparent;
  font: inherit;
  font-size: 13px;
  padding: 4px 6px;
  border-radius: 4px;
  color: var(--text);
}
.name-preset-editor input[type="text"]:focus {
  border-color: var(--border);
  background: var(--off);
  outline: none;
}
.name-preset-editor .preset-remove {
  background: transparent;
  border: none;
  color: var(--mid);
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  padding: 2px 6px;
  border-radius: 4px;
}
.name-preset-editor .preset-remove:hover {
  color: var(--danger);
  background: rgba(218, 65, 65, 0.08);
}
.name-preset-add {
  display: flex;
  gap: 6px;
  margin-bottom: 8px;
}
.name-preset-add .field-input { flex: 1; }

/* Tag editor (Game Details → Details tab → Tags). Chips for
 * applied tags + dropdown to add new ones. Chips look like the
 * filter pills used in the toolbar — the user is adding/removing
 * the same kinds of labels they later filter by. */
/* Toolbar Tags pill + popover. The pill sits in the same row as
 * Clear filters / Highlighted / Playable; clicking opens a small
 * absolutely-positioned popover containing a checkable list. The
 * pill shows a count badge ("Tags · 3") when filters are active
 * so you can see at a glance that the grid is being narrowed. */
.tags-filter-wrap {
  position: relative;
  display: inline-block;
}
#btn-tags-filter[aria-expanded="true"],
#btn-tags-filter.has-active {
  background: var(--green-tint);
  color: var(--green-hover);
  border-color: var(--green);
}
.tags-filter-count {
  font-weight: 700;
  margin-left: 4px;
}
.tags-filter-count:empty { margin: 0; }
.tags-filter-popover {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 200;
  min-width: 240px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.10), 0 2px 4px rgba(0, 0, 0, 0.06);
  padding: 6px 4px;
}
.tags-filter-popover label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  user-select: none;
}
.tags-filter-popover label:hover {
  background: var(--off);
}
.tags-filter-popover input[type="checkbox"] {
  margin: 0;
  cursor: pointer;
}
.tags-filter-popover .muted-row {
  padding: 4px 12px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}

.tag-editor {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-height: 28px;
  padding: 4px 0;
}
.tag-chips:empty::before {
  content: "No tags yet — add one below.";
  color: var(--mid);
  font-size: 13px;
  font-style: italic;
}
.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--green-tint);
  color: var(--green-hover);
  border: 1px solid var(--green);
  border-radius: 12px;
  padding: 2px 4px 2px 10px;
  font-size: 12px;
  font-weight: 600;
  user-select: none;
}
.tag-chip button {
  appearance: none;
  background: transparent;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  padding: 0 4px;
  border-radius: 50%;
}
.tag-chip button:hover {
  background: rgba(0, 0, 0, 0.1);
}

.name-preset-menu {
  position: fixed;
  z-index: 1700;
  background: var(--white);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-modal);
  padding: 6px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px;
  max-width: 460px;
  max-height: 70vh;
  overflow: auto;
}
.name-preset-menu button {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  color: var(--text);
  text-align: left;
  font: inherit;
  font-size: 13px;
  padding: 6px 10px;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
  transition:
    background 0.12s ease,
    border-color 0.12s ease,
    box-shadow 0.12s ease,
    transform 0.08s ease;
}
.name-preset-menu button:hover,
.name-preset-menu button:focus {
  background: var(--green-tint);
  color: var(--green-hover);
  border-color: var(--green);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.06),
    0 4px 10px rgba(0, 149, 114, 0.18);
  transform: translateY(-1px);
  outline: none;
  position: relative;
  z-index: 1;
}

.image-preview-close {
  position: absolute;
  /* Bigger hit area than before — was 32×32 and fiddly to click. */
  top: 4px; right: 4px;
  width: 44px; height: 44px;
  background: transparent;
  border: none;
  color: var(--mid);
  font-size: 30px; line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  display: grid; place-items: center;
  z-index: 3;
}
.image-preview-close:hover { background: var(--off); color: var(--text); }

.image-preview-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 36px; height: 36px;
  background: rgba(0, 0, 0, 0.55);
  color: var(--on-dark);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 50%;
  font-size: 14px;
  cursor: pointer;
  display: grid; place-items: center;
  z-index: 2;
  transition: background 0.15s;
}
.image-preview-nav:hover { background: rgba(0, 0, 0, 0.75); }
.image-preview-nav.prev { left: 14px; }
.image-preview-nav.next { right: 14px; }

.image-preview-delete {
  position: absolute;
  bottom: 12px; left: 12px;
  width: 36px; height: 36px;
  background: var(--white);
  color: var(--danger);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  display: grid; place-items: center;
  z-index: 2;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.image-preview-delete:hover {
  background: var(--danger);
  color: var(--white);
  border-color: var(--danger);
}

/* ── Gallery lightbox ───────────────────────────────────────────────── */

.gallery-modal {
  padding: 0;
}
.gallery-modal .modal-backdrop {
  background: rgba(15, 23, 28, 0.92);
  backdrop-filter: blur(4px);
}

/* Gallery is always presented on a dark backdrop regardless of
   theme, so the wordmark navigation buttons (BoxArts brand on the
   left, BoxPerfect / BoxCraft cross-nav on the right) need to be
   white across every theme. Previously they inherited `color:
   var(--text)` from the .btn-green and .bxa-lockup base rules,
   which in the three LIGHT BoxArts themes is near-black — leaving
   the wordmarks dark-on-dark and almost invisible.
   .btn-wordmark (no prefix) is used so it catches every base
   variant the gallery uses today (.btn-green.btn-wordmark) plus
   any future ones (.btn.btn-wordmark / .btn-ghost.btn-wordmark). */
.gallery-actions .bxa-lockup,
.gallery-actions .bxa-wordmark,
.gallery-actions .btn-wordmark {
  color: #fff;
}
.gallery-actions .btn-wordmark:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.08);
}
.gallery-actions .bxa-wordmark__arts {
  background: #fff;
  color: #000;
}
.gallery-actions .bxa-wordmark--perfect .bxa-wordmark__box,
.gallery-actions .bxa-wordmark--craft .bxa-wordmark__box {
  color: #fff;
}
.gallery-stage {
  position: relative;
  z-index: 1;
  max-width: calc(100vw - 160px);
  max-height: calc(100vh - 120px);
  display: grid;
  place-items: center;
}
.gallery-stage {
  --zoom: 1;
  --pan-x: 0px;
  --pan-y: 0px;
  overflow: hidden;
}
.gallery-stage img {
  max-width: 100%;
  max-height: 80vh;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
  /* Pan first so it's in unscaled space (mouse-pixel-accurate), then zoom. */
  transform: translate(var(--pan-x), var(--pan-y)) scale(var(--zoom));
  transform-origin: center;
  transition: transform 0.05s linear;
}
.gallery-stage img.zoomed { cursor: grab; }
.gallery-stage img.dragging { cursor: grabbing; transition: none; }

/* 3D box in the gallery — uses viewport units rather than 100% of parent
   so it doesn't trigger a circular sizing collapse with the gallery-stage
   grid container (which auto-sizes to its content). */
.gallery-3d-stage {
  position: relative;       /* anchor for absolutely-positioned children (share overlay btn) */
  width: min(80vw, calc(100vw - 160px));
  height: 80vh;
  perspective: 1400px;
  perspective-origin: 50% 45%;
  display: grid;
  place-items: center;
}
.gallery-3d-box {
  position: relative;
  transform-style: preserve-3d;
  transform-origin: center;
  /* Drag-driven rotation — JS sets --rx and --ry on the element. */
  transform: rotateX(var(--rx, -15deg)) rotateY(var(--ry, 25deg));
  transition: transform 0.04s linear;
  will-change: transform;
}
.gallery-3d-stage {
  cursor: grab;
  touch-action: none;            /* don't scroll the page on touch drags */
  user-select: none;
}
.gallery-3d-stage.dragging { cursor: grabbing; }
.gallery-3d-stage.dragging .gallery-3d-box { transition: none; }
.gallery-3d-stage .box3d-face {
  background-color: #cdd1cd;
  /* 100% × 100% so the gallery preview conforms the image to the
     measured face size — matches the BoxCraft editor + the BoxPerfect
     skew preview. */
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-position: center;
  border: 1px solid rgba(0,0,0,0.18);
  box-shadow: inset 0 0 30px rgba(0,0,0,0.18);
  image-rendering: -webkit-optimize-contrast;
  image-rendering: high-quality;
}

/* Tools — zoom slider, parked in the bottom-right so it doesn't obscure the
   image. */
.gallery-tools {
  position: absolute;
  bottom: 24px;
  right: 24px;
  background: rgba(0, 0, 0, 0.55);
  color: var(--on-dark);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius-pill);
  padding: 6px 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: 2;
  font-size: 12px;
}
.gallery-zoom-label {
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 10px;
  color: rgba(255, 255, 255, 0.75);
}
.gallery-tools input[type="range"] {
  width: 160px;
  accent-color: var(--green);
}
.gallery-tools.hidden-tool { display: none; }
.gallery-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(255, 255, 255, 0.06);
  color: var(--on-dark);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 50%;
  width: 48px;
  height: 48px;
  font-size: 18px;
  cursor: pointer;
  z-index: 2;
  display: grid;
  place-items: center;
  transition: background 0.15s, transform 0.1s;
}
.gallery-nav:hover { background: rgba(255, 255, 255, 0.16); }
.gallery-nav:active { transform: translateY(-50%) scale(0.96); }
.gallery-prev { left: 24px; }
.gallery-next { right: 24px; }

.gallery-close {
  position: absolute;
  top: 20px; right: 20px;
  background: rgba(255, 255, 255, 0.06);
  color: var(--on-dark);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 50%;
  width: 40px;
  height: 40px;
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  z-index: 2;
  display: grid;
  place-items: center;
}
.gallery-close:hover { background: rgba(255, 255, 255, 0.16); }
.gallery-actions {
  position: absolute;
  top: 20px;
  z-index: 2;
  display: flex;
  gap: 8px;
  align-items: center;
}
.gallery-actions.gallery-actions-left  { left: 20px; }
/* Leave room on the right for the gallery-close X (top: 20px,
   right: 20px, ~40px wide + 12px gap). */
.gallery-actions.gallery-actions-right { right: 72px; }
.btn-ghost-light {
  background: rgba(255, 255, 255, 0.06);
  color: var(--on-dark);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: var(--radius-pill);
  padding: 9px 18px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s;
}
.btn-ghost-light:hover { background: rgba(255, 255, 255, 0.16); }

/* Destructive delete button parked in the gallery's bottom-left corner.
   Same red treatment as the .btn-danger "Delete game" button so all delete
   surfaces in the app are consistent. */
.gallery-delete-btn {
  position: absolute;
  bottom: 24px;
  left: 24px;
  z-index: 2;
  background: var(--white);
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius-pill);
  padding: 9px 18px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.gallery-delete-btn:hover { background: var(--danger); color: var(--white); }
@media (max-width: 700px) {
  .gallery-delete-btn { bottom: 12px; left: 12px; padding: 7px 14px; font-size: 12px; }
}
/* Caption strip is just a layout row now — the dark pill is the
 * inner #gallery-name (matching the game-details image-preview look).
 * Pill + chevron sit on one row, centred at the bottom of the gallery. */
.gallery-caption {
  position: absolute;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  z-index: 2;
}
.gallery-caption .muted { color: rgba(255, 255, 255, 0.55); }
/* #gallery-name picks up .image-preview-name from the markup so the
 * pill geometry, hover tone, and inline-edit state are shared. Override
 * the preview pill's `max-width: 80%` (which would resolve against the
 * caption's content-width here, clamping the pill to a sliver) with a
 * viewport-relative cap so long names breathe. */
#gallery-name.image-preview-name {
  cursor: text;
  max-width: min(60vw, 520px);
}

@media (max-width: 700px) {
  .gallery-stage { max-width: calc(100vw - 24px); }
  .gallery-prev { left: 8px; }
  .gallery-next { right: 8px; }
  .gallery-nav { width: 40px; height: 40px; }
  .gallery-edit { top: 12px; left: 12px; }
  .gallery-close { top: 12px; right: 12px; }
}

/* ── 3D box builder ─────────────────────────────────────────────────── */

.modal-wide { width: min(960px, 100%); }
.box3d-body { padding-bottom: 8px; }
/* Compact 3D modal header — laptop screens are vertically tight,
   so we trim the title row's padding so the panels + gallery
   strip fit above the fold without scrolling. */
#box3d-modal .modal-header { padding: 10px 18px; gap: 12px; }
#box3d-modal .modal-header h3 { font-size: 16px; }
.box3d-nav {
  display: inline-flex;
  gap: 6px;
  flex: 1 1 auto;
  margin-left: 12px;
}
.box3d-nav .btn-ghost { padding: 4px 10px; font-size: 12px; }
#box3d-modal .modal-tabs { margin-bottom: 8px; }
#box3d-modal .box3d-intro { margin-bottom: 8px; }
#box3d-modal .box3d-kind-toggle { margin-bottom: 8px; }
#box3d-modal .box3d-grid { margin-top: 6px; }
/* Strip lives directly under the panels now — small gap, no
   redundant heading. */
#box3d-modal .box3d-strip { margin-top: 10px; max-height: 110px; }

/* Kind toggle (segmented control) at the top of the 3D builder. */
.box3d-kind-toggle {
  display: inline-flex;
  background: var(--off);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 2px;
  margin-bottom: 12px;
}
.box3d-kind-toggle button {
  appearance: none;
  background: transparent;
  border: none;
  border-radius: var(--radius-pill);
  padding: 6px 14px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--mid);
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.box3d-kind-toggle button.active {
  background: var(--white);
  color: var(--text);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.box3d-kind-toggle button:hover:not(.active) { color: var(--text); }
.box3d-intro { margin-top: 0; }

/* Per-kind: hide the slots that aren't user-fillable for that kind. The
   underlying data still has all 6 face values (auto-fill colours fill
   the hidden ones), so the rendered 3D box is visually complete. */
.box3d-body[data-kind="portfolio"] .box3d-slot[data-face="top"],
.box3d-body[data-kind="portfolio"] .box3d-slot[data-face="bottom"],
.box3d-body[data-kind="portfolio"] .box3d-slot[data-face="right"],
.box3d-body[data-kind="card"] .box3d-slot[data-face="top"],
.box3d-body[data-kind="card"] .box3d-slot[data-face="bottom"],
.box3d-body[data-kind="card"] .box3d-slot[data-face="left"],
.box3d-body[data-kind="card"] .box3d-slot[data-face="right"] {
  visibility: hidden;
}

.box3d-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  margin-top: 14px;
}

.box3d-slots {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: 100px 100px 100px;
  gap: 6px;
}
.box3d-slot {
  position: relative;
  background: var(--off);
  border: 2px dashed var(--border);
  border-radius: 8px;
  display: grid;
  place-items: center;
  text-align: center;
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.15s, background 0.15s;
}
.box3d-slot:hover,
.box3d-slot.drag-over { border-color: var(--green); background: var(--green-tint); }
.box3d-slot.has-image { border-style: solid; }
.box3d-slot img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  /* `contain` (not `cover`) so the slot preview matches the live 3D
     face: the entire image is visible with letterboxing rather than
     cropped. Switching them in lockstep means what the user sees in
     the source-image picker is what they get on the 3D box. */
  object-fit: contain;
}
.slot-label {
  position: relative; z-index: 1;
  font-size: 11px;
  font-weight: 700;
  color: var(--mid);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* var(--white) (panel surface) + var(--mid) — themes redefine
   * --white to a dark panel in dark mode, so the pill stays legible
   * everywhere. Previously hardcoded rgba(255,255,255,...) which
   * left light-grey text invisible on white in dark themes. */
  background: var(--white);
  padding: 2px 6px;
  border-radius: 4px;
}
.box3d-slot.has-image .slot-label { background: rgba(0,0,0,0.55); color: #fff; }
.box3d-slot .slot-clear {
  position: absolute;
  top: 4px; right: 4px;
  z-index: 2;
  background: rgba(0,0,0,0.55); color: #fff;
  border: none; border-radius: 50%;
  width: 18px; height: 18px;
  cursor: pointer; font-size: 12px; line-height: 1;
  display: none;
}
.box3d-slot.has-image:hover .slot-clear { display: block; }

/* Block-fill: solid colour fills the slot in place of an image. The same
   X-clear control removes it. */
.box3d-slot .slot-block-fill {
  position: absolute; inset: 0;
}
.box3d-slot.has-block-fill .slot-label {
  background: rgba(0,0,0,0.55);
  color: #fff;
}

/* Live preview */
.box3d-preview {
  background: var(--light);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.box3d-stage {
  width: 100%;
  height: 280px;
  perspective: 1200px;
  perspective-origin: center;
  display: grid;
  place-items: center;
  user-select: none;
  cursor: grab;
}
.box3d-stage.dragging { cursor: grabbing; }

/* 3D lightbox — smaller than the full builder modal. Centered on
   a darkened backdrop, just the rotating box plus a hint and a
   close button. Right-click anywhere on the box surfaces the
   standard game context menu. */
.box3d-lightbox-panel {
  position: fixed;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1010;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 14px 14px 8px;
  box-shadow: var(--shadow-modal);
  width: min(92vw, 380px);
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.box3d-lightbox-panel .modal-x {
  position: absolute;
  top: 4px; right: 4px;
  /* The 3D box's transform creates its own stacking context and
     its rotated faces can paint into this corner, intercepting
     hover (cursor: grab from .box3d-stage) and pointerdown before
     they reach the close button. An explicit z-index above the
     stage forces the X back on top. */
  z-index: 2;
  /* Default .modal-x is padding: 0 + font-size: 28px, so the
     click target is just the size of the × glyph. Enlarge to a
     comfortable 36×36 square and center the glyph inside it. */
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.box3d-lightbox-stage {
  position: relative;       /* anchor for share-overlay-btn */
  width: 100%;
  height: 320px;
}
.box3d-lightbox-stage + .box3d-hint {
  margin: 0;
  text-align: center;
}
.box3d-box {
  --w: 180px;
  --h: 252px;
  --d: 54px;
  --rx: -15deg;
  --ry: 25deg;
  position: relative;
  width: var(--w); height: var(--h);
  transform-style: preserve-3d;
  transform: rotateX(var(--rx)) rotateY(var(--ry));
  transition: transform 0.05s linear;
}
.box3d-face {
  position: absolute;
  left: 50%; top: 50%;
  background-color: #ddd;
  /* 100% × 100% (stretch-to-fill) — never crop, never letterbox. The
     box face is sized from the assigned image's measured dimensions
     (via autoDimensions in box3d.js) so when dims match the image's
     pixel aspect, stretch reads as no distortion. When the user has
     manually measured the box face, the image conforms exactly to
     that measurement — matching the BoxPerfect preview skew. */
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-position: center;
  border: 1px solid rgba(0,0,0,0.15);
  box-shadow: inset 0 0 30px rgba(0,0,0,0.15);
}
.box3d-face.empty { background: repeating-linear-gradient(45deg, #cdd1cd 0, #cdd1cd 6px, #d6dad6 6px, #d6dad6 12px); }
.face-front {
  width: var(--w); height: var(--h);
  margin: calc(var(--h) / -2) 0 0 calc(var(--w) / -2);
  transform: translateZ(calc(var(--d) / 2));
}
.face-back {
  width: var(--w); height: var(--h);
  margin: calc(var(--h) / -2) 0 0 calc(var(--w) / -2);
  transform: rotateY(180deg) translateZ(calc(var(--d) / 2));
}
.face-right {
  width: var(--d); height: var(--h);
  margin: calc(var(--h) / -2) 0 0 calc(var(--d) / -2);
  transform: rotateY(90deg) translateZ(calc(var(--w) / 2));
}
.face-left {
  width: var(--d); height: var(--h);
  margin: calc(var(--h) / -2) 0 0 calc(var(--d) / -2);
  transform: rotateY(-90deg) translateZ(calc(var(--w) / 2));
}
.face-top {
  width: var(--w); height: var(--d);
  margin: calc(var(--d) / -2) 0 0 calc(var(--w) / -2);
  transform: rotateX(90deg) translateZ(calc(var(--h) / 2));
}
.face-bottom {
  width: var(--w); height: var(--d);
  margin: calc(var(--d) / -2) 0 0 calc(var(--w) / -2);
  transform: rotateX(-90deg) translateZ(calc(var(--h) / 2));
}

.box3d-hint { margin: 0; }

.box3d-dim-row {
  margin-top: 14px;
  display: grid;
  grid-template-columns: 80px repeat(3, 1fr) auto;
  gap: 12px;
  align-items: end;
}
.box3d-dim { display: block; }
.box3d-dim span {
  display: flex; justify-content: space-between;
  font-size: 12px; color: var(--mid);
  margin-bottom: 4px;
}
.box3d-dim em { font-style: normal; color: var(--text); font-family: ui-monospace, monospace; font-weight: 600; }
.box3d-dim input[type="range"] { width: 100%; accent-color: var(--green); }
.box3d-dim input[type="number"],
.box3d-dim select {
  width: 100%;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-family: inherit;
  font-size: 13px;
  color: var(--text);
  outline: none;
}
.box3d-dim input[type="number"]:focus,
.box3d-dim select:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(0,149,114,0.10);
}

.box3d-preset-row {
  margin-top: 12px;
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 10px;
  align-items: end;
}

.box3d-strip {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 8px;
  margin-top: 8px;
}
.box3d-strip-item {
  flex: 0 0 auto;
  width: 84px; height: 84px;
  border: 2px solid var(--border);
  border-radius: 6px;
  background: var(--off);
  cursor: grab;
  position: relative;
  overflow: hidden;
}
.box3d-strip-item.dragging { opacity: 0.4; }
.box3d-strip-item img {
  width: 100%; height: 100%;
  object-fit: cover; display: block; pointer-events: none;
}
.box3d-strip-item .strip-name {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  background: rgba(0,0,0,0.55); color: #fff;
  font-size: 10px; font-weight: 600;
  padding: 2px 4px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}

@media (max-width: 700px) {
  .box3d-grid { grid-template-columns: 1fr; }
  .box3d-slots { grid-template-rows: 80px 80px 80px; }
  .box3d-stage { height: 220px; }
  .box3d-box { --w: 140px; --h: 196px; --d: 42px; }
  .box3d-dim-row { grid-template-columns: 1fr 1fr; }
}

/* ── Right-click context menu ───────────────────────────────────────── */

.context-menu {
  position: fixed;
  z-index: 1200;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-modal);
  padding: 4px;
  min-width: 160px;
  font-size: 13px;
}
.context-menu button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 8px 12px;
  border-radius: 6px;
  cursor: pointer;
  color: var(--text);
  font: inherit;
}
.context-menu button:hover { background: var(--off); }
.context-menu button.danger { color: var(--danger); }
.context-menu button.danger:hover { background: var(--danger-tint); }

/* ── DOS Player ────────────────────────────────────────────────────── */

/* Sits above the Game Details modal so the user can launch from there
   without losing context. Backdrop click + ✕ close it. js-dos paints
   into #dos-player-stage via its own stylesheet (loaded with the
   library), so the styles here just frame the canvas. */
/* Bump button-class specificity to win against the Tailwind reset
 * inside the vendored js-dos.css. That reset includes
 * `[type=button]{background-color:transparent}` (specificity
 * 0,0,1,0) which ties with our `.btn-green` / `.btn-ghost` /
 * `.btn-danger` and wins by source order once js-dos.css loads —
 * the symptom is that any green / themed button "goes white" until
 * the user re-hovers (the `:hover` rule wins on its own). Prefixing
 * with the element selector lifts our specificity to 0,0,1,1 and
 * settles the cascade cleanly without `!important`. */
button.btn-green { background: var(--green); }
button.btn-ghost { background: var(--white); }
button.btn-danger { background: var(--white); }
button.btn-icon-only { background: var(--white); }

/* Override DaisyUI's `.modal` rules that ship inside the vendored
 * js-dos.css. DaisyUI hides `.modal` by default (opacity:0,
 * pointer-events:none) and shows it via `.modal-open` / `[open]` —
 * the opposite convention to BoxPerfect, which has modals visible
 * by default and hides them via `.hidden`. Without this override,
 * lazy-loading js-dos.css makes EVERY one of our modals (game,
 * gallery, box3d, image-preview, settings, …) silently disappear
 * the first time the user clicks ▶ Play. The list of ids stays
 * locked to our actual modals so we don't accidentally re-style
 * any modal js-dos itself paints. */
#game-modal.modal,
#image-preview-modal.modal,
#gallery-modal.modal,
#box3d-modal.modal,
#title-modal.modal,
#settings-modal.modal,
#unsaved-modal.modal,
#game-unsaved-modal.modal,
#box3d-unsaved-modal.modal,
#dos-player-modal.modal {
  opacity: 1 !important;
  pointer-events: auto !important;
  width: 100% !important;
  height: 100% !important;
  padding: 24px !important;
  background-color: transparent !important;
  transition: none !important;
}
.modal.hidden { display: none !important; }

/* Unsaved-changes prompts sit ABOVE every other modal so the user
   can read them when triggered from the game card or BoxCraft
   modals — both share `.modal { z-index: 200 }` by default and
   would stack as siblings, which the js-dos vendor reset can
   nudge the wrong way. Pin them to 600 so they always win. */
#game-unsaved-modal.modal,
#box3d-unsaved-modal.modal {
  z-index: 600 !important;
}

/* Both unsaved prompts borrow BoxPerfect's `.modal-body` markup
   (Save / Discard / Cancel triad), but `.modal-body` in this
   stylesheet is a scrollable layout primitive (used as a flex
   child inside `.modal-panel`) with no surface styling. Without
   the panel chrome the prompt rendered transparent on top of the
   blurred backdrop, which read as "foggy and unreadable". This
   rule gives the inner div a proper white panel, rounded border,
   and shadow — matching the surface treatment `.modal-panel`
   provides for full-size modals. Scoped to the two unsaved
   prompts only so the editor / 3D / settings modals (which
   intentionally use `.modal-body` as scroll-only) stay untouched. */
#game-unsaved-modal .modal-body,
#box3d-unsaved-modal .modal-body {
  position: relative;
  background: var(--white);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  /* Override .modal-body's default scrollable-flex shape — the
     prompt is a small fixed-size pill, not a long-form scroller. */
  overflow: visible;
  flex: 0 0 auto;
}

.dos-player-modal { z-index: 400; }
/* The vendored js-dos.css ships a Tailwind reset that competes with
 * BoxPerfect's own modal sizing — without specificity bumps the panel
 * collapses to ~290px and the game canvas renders at a postage-stamp
 * 195×146. Locking the size to the modal id wins the cascade
 * regardless of source order. */
#dos-player-modal .dos-player-panel {
  position: relative;
  background: #000;
  width: min(95vw, 1100px);
  height: min(92vh, 800px);
  max-height: 92vh;
  border-radius: 10px;
  /* Border + glow track each theme's header colour (--white in
   * BoxPerfect's CSS-var system — each theme overrides it to the
   * actual main-nav background). The player frame then visually
   * "belongs" to whichever skin the user has on without picking up
   * the louder accent. */
  border: 2px solid var(--white);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--white), var(--shadow-modal);
}
.dos-player-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  /* Mirror the theme's main-nav: same surface (--white) and same
   * text colour (--text). The player header then visually reads as
   * "BoxPerfect chrome" rather than a generic dark bar — contrast
   * is correct on both light themes (boxperfect / readit, dark
   * text on light bg) and dark ones (steamy / monkey / goodish,
   * light text on dark bg). */
  background: var(--white);
  color: var(--text);
  font-size: 13px;
  border-bottom: 1px solid var(--border);
}
.dos-player-title { flex: 1; font-weight: 600; }
.dos-player-actions { display: flex; gap: 6px; }
.dos-player-actions .btn-ghost {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 6px 12px;
  font-weight: 600;
  font-size: 12px;
}
.dos-player-actions .btn-ghost:hover {
  background: var(--off);
  border-color: var(--accent);
}
.dos-player-actions .modal-x {
  color: var(--text);
  background: transparent;
  border: none;
  font-size: 22px;
  line-height: 1;
  padding: 0 6px;
  cursor: pointer;
}
.dos-player-actions .modal-x:hover { color: var(--accent); }
.dos-player-stage {
  flex: 1;
  min-height: 480px;
  background: #000;
  display: grid;
  place-items: center;
  position: relative;
}
/* When js-dos mounts it injects its own canvas + UI inside the stage —
   make sure they fill the available space without a scrollbar. */
.dos-player-stage > * { width: 100%; height: 100%; }
.dos-player-status {
  padding: 6px 12px;
  background: #111;
  color: rgba(255,255,255,0.6);
  font-size: 12px;
  text-align: center;
}
.dos-player-status:empty { display: none; }

/* DOS Play panel inside the Game Details modal. */
#dos-play-section .dos-play-state { margin-bottom: 8px; }
#dos-play-section .dos-play-actions,
#local-play-section .dos-play-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
#local-play-section .dos-play-state { margin-bottom: 8px; }
#local-play-section .local-play-form {
  margin-top: 12px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--off, #f7f7f5);
}
#local-play-section .local-play-form .field { display: block; margin-bottom: 8px; }
#local-play-section .local-play-form .field-label { font-size: 12px; color: var(--mid); }
#local-play-section .local-play-input-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
}
#local-play-section .local-play-input-row .field-input { flex: 1; min-width: 0; }
#local-play-section .local-play-input-row .btn-ghost { white-space: nowrap; padding: 6px 12px; }
/* Launch-executable dropdown sits below the action buttons. Hidden
 * unless the catalog has >1 candidates; the row is laid out as
 * label | select with the select taking the rest of the width
 * so it aligns with the other DOS Play controls. */
#dos-play-section .dos-launch-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}
#dos-play-section .dos-launch-row label { white-space: nowrap; }
#dos-play-section .dos-launch-row select {
  flex: 1;
  max-width: 280px;
}

/* "Mouse Driven Game" toggle uses .btn-ghost as its base style so
 * it visually matches Unlink / Save / etc. on the same row. The
 * pressed state (aria-pressed="true") tints with the theme accent
 * to read as "active" — same idiom as the toolbar play/highlight
 * chips. Scoped to #btn-dos-mouse so it doesn't repaint other
 * .btn-ghost buttons that may already use aria-pressed. */
#btn-dos-mouse[aria-pressed="true"],
#btn-dos-debug[aria-pressed="true"] {
  background: var(--green-tint);
  border-color: var(--green);
  color: var(--green-hover);
}


/* ── Drop overlay ───────────────────────────────────────────────────── */

.drop-overlay {
  position: fixed; inset: 0;
  background: rgba(0, 149, 114, 0.06);
  border: 4px dashed var(--green);
  z-index: 500;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.drop-banner {
  background: var(--white);
  border: 1px solid var(--green);
  border-radius: var(--radius-pill);
  padding: 16px 28px;
  display: flex; align-items: center;
  gap: 12px;
  font-weight: 700;
  color: var(--green);
  box-shadow: var(--shadow-modal);
}
.drop-icon { font-size: 22px; }

/* ── Toast ──────────────────────────────────────────────────────────── */

.toast {
  position: fixed;
  bottom: 24px; left: 50%; transform: translateX(-50%);
  background: var(--text);
  color: var(--white);
  padding: 10px 18px;
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 500;
  z-index: 1000;
  box-shadow: var(--shadow-modal);
  max-width: 90vw;
}
.toast.error { background: var(--danger); }

/* ── Mobile ─────────────────────────────────────────────────────────── */

@media (max-width: 700px) {
  .main-nav { padding: 0 16px; gap: 12px; }
  .search-input { width: 140px; }
  .search-input:focus { width: 180px; }
  .container { padding: 16px 16px 60px; }
  .field-row { grid-template-columns: 1fr; }
  :root { --tile-size: 140px; }
}
