:root {
  --bg-deep: #0a0a12;
  --bg-surface: #12121e;
  --bg-card: #1a1a2e;
  --bg-elevated: #22223a;
  --accent-sun: #f7a825;
  --accent-flare: #ff5e3a;
  --accent-corona: #7b61ff;
  --accent-cool: #38bdf8;
  /* BETA-mode advisory orange (Lex P2 round-2: BETA badge + beta-
     banner were three near-duplicate orange literals drifting
     independently; locked behind one token). Softer + lighter than
     --accent-sun on purpose — BETA reads as a friendly advisory,
     not a CTA. */
  --beta-orange: #ffb74d;
  --beta-orange-bg: rgba(255, 152, 0, 0.14);
  --beta-orange-border: rgba(255, 152, 0, 0.48);
  --text-primary: #eeeef6;
  --text-secondary: #9898b8;
  /* WCAG 1.4.3 AA requires 4.5:1 contrast for normal text. The old
     value (#5e5e80) computed to ~3.0:1 against the .section / panel
     backgrounds (#1a1a2e), which a beta accessibility audit flagged.
     #8e8eab measures ~4.6:1 against #1a1a2e — comfortable AA pass. */
  --text-dim: #8e8eab;
  /* `--accent-corona-text` is a brightened variant of --accent-corona
     (#7b61ff, ~3.7:1 on #1a1a2e — sub-AA for normal text) intended
     ONLY for places the purple is used as text on a dark background.
     Decorative usages (borders, focus rings, gradients) keep the
     original --accent-corona. #a48dff measures ~6.0:1 against the
     same dark panel — comfortable AA pass for body and small text. */
  --accent-corona-text: #a48dff;
  --border: #2a2a44;
  --border-active: #7b61ff;
  --glow-sun: rgba(247,168,37,0.25);
  --glow-corona: rgba(123,97,255,0.2);
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 20px;
  --transition: 0.3s cubic-bezier(0.4,0,0.2,1);
}

/* Default color-scheme tells the browser to render its OWN chrome
   (scrollbar, autofill, native form controls, mobile address-bar
   hint) in the correct theme. Without this the frame "around" the
   page rendered in OS-light while the page itself was dark, which
   read as incongruous (friction audit). `light dark` lets the browser
   pick based on what's actually rendered; html.dark forces dark even
   if the OS is light. */
html { color-scheme: light dark; background: var(--bg-deep); }
html.dark { color-scheme: dark; }

html:not(.dark) {
  --bg-deep: #f0eff5;
  --bg-surface: #faf9fe;
  --bg-card: #ffffff;
  --bg-elevated: #eeedf5;
  /* On light backgrounds, the original --accent-corona (#7b61ff)
     reads fine (~5.5:1 against white). Hold the same value so
     light-mode borders/focus/text all stay in sync; dark-mode
     drops to the brightened variant defined in :root above. */
  --accent-corona-text: #6a4eff;
  --text-primary: #1a1a2e;
  --text-secondary: #5e5e80;
  /* Old value #9898b8 on white = ~3.9:1, fails AA. #6a6a8a measures
     ~6.0:1 against white — comfortable AA pass. */
  --text-dim: #6a6a8a;
  --border: #d8d8e8;
  --border-active: #7b61ff;
  --glow-sun: rgba(247,168,37,0.15);
  --glow-corona: rgba(123,97,255,0.1);
}

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

/* Global focus-visible ring. WCAG 2.4.7 (Sam Rosenberg round-2 a11y
   audit, P1) requires a visible focus indicator on every interactive
   element. Many of our buttons and tabs currently rely on a default
   browser outline that's been stripped by `outline:none` on a few
   nearby rules, breaking keyboard navigation. `:focus-visible` only
   fires for keyboard / programmatic focus (NOT mouse clicks), so the
   ring doesn't show up for sighted-mouse users — best of both worlds.
   Two-token outline so the ring is visible against both the deep
   purple panels and the orange CTAs. */
:focus-visible {
  outline: 2px solid var(--border-active);
  outline-offset: 2px;
  border-radius: 4px;
}
/* Some compound widgets manage their own focus styling already
   (date input, modal close X). Re-applying the global ring on top
   doubles the indicator. Opt out by adding `data-no-focus-ring` to
   the element. */
[data-no-focus-ring]:focus-visible {
  outline: none;
}

/* Honor prefers-reduced-motion (Sam P1, Asha P1 overlap). Wipe
   transitions, animations, and smooth scroll behaviour for users
   who've asked the OS for less motion. The `* {...}` rule is
   intentionally heavy-handed — anything we want to keep animated
   for that audience should opt in with `data-animate-always`. */
@media (prefers-reduced-motion: reduce) {
  *:not([data-animate-always]),
  *::before:not([data-animate-always]),
  *::after:not([data-animate-always]) {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

body {
  font-family: 'Outfit', sans-serif;
  background: var(--bg-deep);
  color: var(--text-primary);
  min-height: 100vh;
  overflow-x: hidden;
}

/* Animated background */
body::before {
  content: '';
  position: fixed;
  top: -50%; left: -50%;
  width: 200%; height: 200%;
  background: radial-gradient(ellipse at 30% 20%, var(--glow-sun) 0%, transparent 50%),
              radial-gradient(ellipse at 70% 80%, var(--glow-corona) 0%, transparent 50%);
  animation: bgDrift 20s ease-in-out infinite alternate;
  z-index: 0;
  pointer-events: none;
}

@keyframes bgDrift {
  0% { transform: translate(0,0) rotate(0deg); }
  100% { transform: translate(-5%,3%) rotate(3deg); }
}

.app-container {
  position: relative;
  z-index: 1;
  max-width: 960px;
  margin: 0 auto;
  padding: 20px 16px 80px;
}

/* Skip-to-content link — Sam Rosenberg + Cole Ramirez round-2 a11y
   (WCAG 2.4.1 Bypass Blocks). Off-screen until focused, then snaps
   into the top-left corner so a keyboard user can jump past the hero
   and banners straight into <main>. Corona-orange so it stays visible
   over both the dark page background and the corona-glow body::before
   layer; sun-yellow outline on focus mirrors the rest of the focus
   styles in this stylesheet. */
.skip-link {
  position: absolute;
  top: -40px;
  left: 8px;
  background: var(--accent-corona);
  color: white;
  padding: 8px 12px;
  border-radius: 4px;
  z-index: 10000;
  text-decoration: none;
  font-weight: 600;
}
.skip-link:focus {
  top: 8px;
  outline: 3px solid var(--accent-sun);
  outline-offset: 2px;
}

/* Header */
.hero {
  text-align: center;
  padding: 32px 0 24px;
  animation: fadeInUp 0.8s ease-out;
}

.hero-icon {
  width: 64px; height: 64px;
  border-radius: 50%;
  background: radial-gradient(circle at 40% 40%, #ffe066, var(--accent-sun), var(--accent-flare));
  margin: 0 auto 16px;
  box-shadow: 0 0 40px var(--glow-sun), 0 0 80px rgba(255,94,58,0.15);
  animation: pulse 3s ease-in-out infinite;
}

@keyframes pulse {
  0%,100% { box-shadow: 0 0 40px var(--glow-sun), 0 0 80px rgba(255,94,58,0.15); transform: scale(1); }
  50% { box-shadow: 0 0 60px var(--glow-sun), 0 0 120px rgba(255,94,58,0.25); transform: scale(1.05); }
}

.hero h1 {
  font-size: 2rem;
  font-weight: 800;
  letter-spacing: -0.03em;
  background: linear-gradient(135deg, var(--accent-sun), var(--accent-flare), var(--accent-corona));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.hero p {
  color: var(--text-secondary);
  font-size: 1rem;
  margin-top: 6px;
  font-weight: 300;
}
/* NASA/SDO acknowledgement under the hero subtitle — present but quiet,
   so it's prominent (top of page, seen first) without competing with
   the tagline. */
.hero .hero-credit {
  font-size: 0.72rem;
  color: var(--text-dim);
  margin-top: 8px;
  font-weight: 400;
  letter-spacing: 0.01em;
}

/* Beta-mode banner under the hero. Hidden by default; the
   .beta-mode-active class on <body> (set by JS once /store-config
   returns beta_mode:true) reveals it. Warm-orange so it reads as a
   friendly advisory rather than a red error. */
.beta-banner {
  display: none;
  margin: 12px auto 0;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--beta-orange-bg);
  border: 1px solid var(--beta-orange-border);
  color: var(--beta-orange);
  font-size: 0.85rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  max-width: max-content;
  align-items: center;
  gap: 8px;
}
.beta-banner i {
  color: var(--beta-orange);
  font-size: 0.95rem;
}
body.beta-mode-active .beta-banner {
  display: inline-flex;
}
/* Beta-mode title badge — appended as " BETA" to the existing
   "Solar Archive" h1 via JS. Same warm orange so the header and
   advisory banner read as one visual cue. */
.app-title-beta-badge {
  display: inline-block;
  margin-left: 10px;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--beta-orange-bg);
  border: 1px solid var(--beta-orange-border);
  color: var(--beta-orange);
  font-size: 0.42em;
  font-weight: 700;
  letter-spacing: 0.18em;
  vertical-align: middle;
  -webkit-text-fill-color: var(--beta-orange); /* override the gradient text-fill on h1 */
}

/* ── Birthday Sun CTA ──────────────────────────────────────
   Above-the-fold gifting hook. Two beta personas (Riley + Jordan)
   independently flagged birthday/anniversary as the dominant
   conversion path; this surfaces it as the primary entry point
   without removing the "explore by date + wavelength" flow below. */
.birthday-cta-section {
  /* Tighter than a regular section — feels like a hero CTA, not
     another numbered step. */
  padding: 0;
  background: transparent;
  border: none;
  box-shadow: none;
  margin-bottom: 18px;
}
.birthday-cta {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    "icon text  form";
  align-items: center;
  gap: 18px 20px;
  padding: 22px 26px;
  background: linear-gradient(135deg,
    rgba(247, 168, 37, 0.16) 0%,
    rgba(255, 94, 58, 0.12) 50%,
    rgba(123, 97, 255, 0.16) 100%);
  border: 1px solid rgba(247, 168, 37, 0.32);
  border-radius: var(--radius-md);
}
.birthday-cta-icon {
  grid-area: icon;
  font-size: 1.8rem;
  color: var(--accent-sun, #f7a825);
  flex: none;
}
.birthday-cta-text { grid-area: text; min-width: 0; }
.birthday-cta-title {
  margin: 0 0 4px;
  font-size: 1.18rem;
  font-weight: 700;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
.birthday-cta-sub {
  margin: 0;
  font-size: 0.86rem;
  color: var(--text-secondary);
  line-height: 1.4;
}
.birthday-cta-form {
  grid-area: form;
  display: flex;
  align-items: center;
  gap: 8px;
}
.birthday-cta-label {
  /* Visually hidden but available to screen readers. */
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.birthday-cta-input {
  font-family: inherit;
  font-size: 0.95rem;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-elevated);
  color: var(--text-primary);
  outline: none;
  min-width: 0;
}
.birthday-cta-input:focus {
  border-color: var(--accent-corona);
  box-shadow: 0 0 0 3px rgba(123, 97, 255, 0.2);
}
.birthday-cta-btn {
  padding: 10px 18px;
  font-family: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  background: linear-gradient(135deg, var(--accent-sun), var(--accent-flare));
  color: #fff;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
  white-space: nowrap;
}
.birthday-cta-btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 14px rgba(247, 168, 37, 0.35);
}
.birthday-cta-btn:active { transform: translateY(0); }

/* Stack on narrow screens — three rows: icon+title row collapses
   to text-only, then the form below. */
@media (max-width: 740px) {
  .birthday-cta {
    grid-template-columns: 1fr;
    grid-template-areas:
      "icon"
      "text"
      "form";
    text-align: center;
    padding: 18px 18px 20px;
  }
  .birthday-cta-icon {
    margin: 0 auto;
  }
  .birthday-cta-form {
    flex-direction: column;
    align-items: stretch;
  }
  .birthday-cta-input,
  .birthday-cta-btn {
    width: 100%;
  }
}

/* ── Post-download thank-you modal (beta flow only) ─────── */
.beta-thanks-modal {
  position: fixed;
  inset: 0;
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.beta-thanks-modal.hidden { display: none; }
.beta-thanks-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 9, 26, 0.72);
  backdrop-filter: blur(2px);
}
.beta-thanks-panel {
  position: relative;
  /* Pinned background instead of var(--bg-elevated) — that variable
     flips white in light system mode, and the box-shadow + light-on-
     light text would be illegible. Scope a dark colour and the
     light text variables so the panel reads correctly regardless of
     OS theme (same fix as the feedback / confirm modal panels). */
  background: #1a1a2e;
  --text-primary:   #e9eaf5;
  --text-secondary: #a8a8c8;
  --text-dim:       #8e8eab;
  --accent-corona-text: #a48dff;
  border: 1px solid rgba(255, 152, 0, 0.35);
  border-radius: 12px;
  padding: 28px 28px 24px;
  max-width: 440px;
  width: 100%;
  text-align: center;
  box-shadow: 0 24px 56px rgba(0, 0, 0, 0.45);
}
.beta-thanks-close {
  position: absolute;
  top: 10px;
  right: 12px;
  background: transparent;
  border: 0;
  color: var(--text-dim, #6a6a8a);
  font-size: 1.1rem;
  cursor: pointer;
  padding: 4px 8px;
}
.beta-thanks-close:hover { color: var(--text-primary, #e9eaf5); }
.beta-thanks-icon {
  font-size: 2.4rem;
  color: #ffb74d;
  margin-bottom: 8px;
}
.beta-thanks-title {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0 0 12px;
  color: var(--text-primary, #e9eaf5);
}
.beta-thanks-body {
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--text-secondary, #9898b8);
  margin: 0 0 20px;
}
.beta-thanks-actions {
  display: flex;
  justify-content: center;
  gap: 10px;
}
.beta-thanks-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border-radius: 8px;
  border: 0;
  font: 600 0.92rem -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  cursor: pointer;
  transition: transform 0.06s ease, box-shadow 0.15s ease;
}
.beta-thanks-btn-primary {
  background: linear-gradient(135deg, #7b61ff, #c06bff);
  color: #fff;
}
.beta-thanks-btn-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 6px 16px rgba(123, 97, 255, 0.4);
}

/* Sections / Steps */
.section {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 24px;
  margin-bottom: 20px;
  /* Opacity-only fade. The previous `animation: fadeInUp` keyframes
     used transform: translateY(...) which, even at the "to" state of 0,
     promoted .section to a containing block for position:fixed
     descendants AND broke position:sticky behaviour for the editor
     preview pane in some browsers. The result Gilly hit on prod was
     the master tier toggle scrolling with the section instead of
     pinning, and the editor preview pane not floating. Opacity-only
     has no side effects on positioning. */
  animation: sectionFadeIn 0.45s ease-out both;
}

.section:nth-child(2) { animation-delay: 0.1s; }
.section:nth-child(3) { animation-delay: 0.2s; }
.section:nth-child(4) { animation-delay: 0.3s; }
.section:nth-child(5) { animation-delay: 0.4s; }

@keyframes sectionFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
/* Legacy keyframes preserved for any external rule still referencing
   fadeInUp (search confirms only `.section` used it, but leave for
   safety). */
@keyframes fadeInUp {
  from { opacity: 0; }
  to { opacity: 1; }
}

.section-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

/* ── Collapsible sections ─────────────────────────────────────────
   Step 1 (config — time + wavelength) is collapsed by default
   because most users get into the editor via a vibe-card preset
   that fills both fields automatically. The full controls only
   need to come out for the "your day" birthday card or when the
   user explicitly clicks the "Customize time & wavelength" CTA
   in step 2.

   .section-collapse-toggle is a real <button> wrapper around the
   header so click + Space + Enter all work + a screen reader
   announces it as a disclosure. .section-collapsible-body holds
   everything beneath the header that should collapse away. */
.section-collapse-toggle {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  width: 100%;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font: inherit;
}
.section-collapse-toggle .section-header {
  margin-bottom: 0;
}
.section-collapse-chevron {
  margin-left: auto;
  font-size: 14px;
  color: var(--text-secondary, #a0a0bd);
  transition: transform 0.15s ease;
  transform: rotate(180deg);  /* up when expanded */
}
.section-collapsed .section-collapse-chevron {
  transform: rotate(0deg);    /* down when collapsed (default) */
}
.section-collapse-hint {
  color: var(--text-secondary, #a0a0bd);
  font-weight: 400;
  font-size: 0.85em;
  margin-left: 6px;
}
.section-collapsible-body {
  /* When expanded, add breathing room above the body so it doesn't
     crash into the now-clickable header. Collapse via display:none
     so hidden grids inside don't run their reveal animations on
     every section toggle. */
  margin-top: 16px;
}
.section-collapsed .section-collapsible-body {
  display: none;
}

/* Small "secondary" CTA buttons in section headers — used for
   things like "Customize time & wavelength →" inside step 2's
   header. Sits at the right of the header (margin-left:auto). */
.section-secondary-cta {
  margin-left: auto;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: var(--text-secondary, #a0a0bd);
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.section-secondary-cta:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-primary, #e9eaf5);
  border-color: rgba(255, 255, 255, 0.3);
}

.step-badge {
  width: 32px; height: 32px;
  border-radius: 50%;
  /* corona → flare per Lex round-2 ("strongest opinion"): the prior
     corona→cool teal had no parent in the sun/flare/corona triad and
     "read like leftover Tailwind sky-400." Corona→flare keeps the
     badges inside the brand palette. */
  background: linear-gradient(135deg, var(--accent-corona), var(--accent-flare));
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.85rem;
  color: #fff;
  flex-shrink: 0;
}

.section-title {
  font-size: 1.15rem;
  font-weight: 700;
}

/* Controls */
.controls-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

@media (max-width: 560px) {
  .controls-grid { grid-template-columns: 1fr; }
}

.field-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field-label {
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

input[type="date"],
input[type="time"],
select {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  font-size: 16px;
  font-family: 'Outfit', sans-serif;
  color: var(--text-primary);
  transition: border-color var(--transition), box-shadow var(--transition);
  width: 100%;
  -webkit-appearance: none;
}

/* Mouse-focus styling: subtle border + glow, no default outline.
   :focus-visible (keyboard) still applies the global outline via the
   top-of-file rule — the agent friction audit found those rings were
   getting stripped because the old `:focus` block beat the global
   `:focus-visible` on specificity. Splitting :focus from :focus-visible
   keeps the mouse-clean look while preserving keyboard ring. */
input[type="date"]:focus:not(:focus-visible),
input[type="time"]:focus:not(:focus-visible),
select:focus:not(:focus-visible) {
  outline: none;
  border-color: var(--border-active);
  box-shadow: 0 0 0 3px var(--glow-corona);
}
input[type="date"]:focus-visible,
input[type="time"]:focus-visible,
select:focus-visible {
  border-color: var(--border-active);
  box-shadow: 0 0 0 3px var(--glow-corona);
}

/* Wavelength cards */
.wavelength-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  margin-top: 10px;
}

@media (max-width: 500px) {
  .wavelength-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.filter-toggle {
  display: inline-flex;
  gap: 2px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 2px;
}

.filter-opt {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  font-size: 0.75rem;
  color: var(--text-secondary);
  cursor: pointer;
  border-radius: 4px;
  transition: all 0.15s ease;
  white-space: nowrap;
}

.filter-opt:has(input:checked),
.filter-opt.active {
  background: var(--accent-corona);
  color: #fff;
  font-weight: bold;
  outline: 2px solid rgba(255, 255, 255, 0.5);
  outline-offset: 1px;
}

/* Match option shows mode color (set in JS); when selected, .active provides outline and bold */

/* HQ RHEF option — matches sibling filter options. A subtle star cue in the
   label signals "premium/slow" without the old gold treatment that tested as
   intimidating to click. When selected, it uses the same accent-corona as
   the other filters. */
#hqRhefFilterOpt {
  color: var(--text-secondary);
}

.filter-opt input[type="radio"] {
  display: none;
}

/* ── Filter quality timeline ──
   Replaces the JPG/Raw/RHEF/HQ-RHEF radio row with a stepper that reads as
   "image quality progresses through these tiers": locked tiers show a small
   lock icon, loading tiers spin, ready tiers get a check, the active tier
   gets the accent treatment. Connectors between steps fill in as each tier
   becomes ready. Underlying radio inputs are kept hidden for a11y. */
.filter-timeline {
  position: relative;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  padding: 6px 10px;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.filter-timeline-label {
  /* Own row so the four steps align vertically — beta tester noted the
     prior layout staggered "QUALITY" inline next to JPG which made the
     stages drift across rows. */
  flex: 0 0 100%;
  font-size: 0.7rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0 0 2px;
}
.filter-step {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 6px;
  border-radius: 999px;
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease, opacity 0.15s ease;
  color: var(--text-dim);
  user-select: none;
}
.filter-step input[type="radio"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.filter-step-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--border);
  color: var(--text-dim);
  font-size: 0.7rem;
  flex-shrink: 0;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.filter-step-info {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1;
}
.filter-step-name {
  font-size: 0.78rem;
  font-weight: 600;
}
.filter-step-status {
  font-size: 0.62rem;
  letter-spacing: 0.02em;
  margin-top: 1px;
  min-height: 1em;
}
.filter-step-status::before {
  content: "";
  display: inline-block;
  vertical-align: middle;
}
.filter-step-status[data-status="locked"]::before {
  content: "\f023 Locked";
  font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
  font-weight: 900;
  /* Removed letter-spacing: 0.4em — the wide-tracked uppercase-feeling
     "L O C K E D" alongside lowercase "Loading…" looked inconsistent
     and the agent friction audit read it as an error state. */
  letter-spacing: 0.02em;
  color: var(--text-dim);
}
.filter-step-status[data-status="loading"]::before {
  content: "";
  width: 8px;
  height: 8px;
  border: 1.5px solid rgba(123, 97, 255, 0.35);
  border-top-color: var(--accent-corona, #7b61ff);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  margin-right: 4px;
}
.filter-step-status[data-status="loading"]::after {
  content: "Loading…";
  color: var(--accent-corona, #7b61ff);
}
.filter-step-status[data-status="ready"]::before {
  content: "\f00c Ready";
  font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
  font-weight: 900;
  letter-spacing: 0.4em;
  color: #3ddc84;
}
.filter-step-status[data-status="active"]::before {
  content: "\f00c Active";
  font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
  font-weight: 900;
  letter-spacing: 0.4em;
  color: #fff;
}

/* Locked steps are dimmed and not clickable (cursor stays default) — but
   we still allow the click so the existing handleFilterChange() can show
   the "starting fetch…" toast. The cursor cue communicates "not yet". */
.filter-step.locked {
  opacity: 0.55;
  cursor: progress;
}
.filter-step.ready:hover {
  background: rgba(123, 97, 255, 0.08);
  color: var(--text-primary);
}
.filter-step.ready .filter-step-num {
  background: rgba(61, 220, 132, 0.18);
  color: #3ddc84;
}
.filter-step.loading .filter-step-num {
  background: rgba(123, 97, 255, 0.18);
  color: var(--accent-corona, #7b61ff);
}
.filter-step.active {
  background: var(--accent-corona, #7b61ff);
  color: #fff;
  font-weight: 700;
}
.filter-step.active .filter-step-num {
  background: rgba(255, 255, 255, 0.22);
  color: #fff;
}

/* Connector line between steps. Thicker + accent when both flanking steps
   are ready; muted otherwise. */
.filter-step-connector {
  flex: 0 0 18px;
  height: 2px;
  background: var(--border);
  border-radius: 2px;
  transition: background-color 0.18s ease;
}
.filter-step-connector.completed {
  background: linear-gradient(90deg, #3ddc84, var(--accent-corona, #7b61ff));
}

/* Defensive: hide any leftover old-markup labels on caches that haven't
   reloaded. */
.filter-timeline .filter-opt { display: none; }

.filter-status-line {
  font-size: 0.7rem;
  font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
  padding: 3px 12px;
  margin-top: 4px;
  border-radius: 4px;
  line-height: 1.4;
  transition: opacity 0.3s ease;
}
.filter-status-line.loading {
  background: rgba(255, 165, 0, 0.12);
  color: var(--accent-sun, #ffa500);
}
.filter-status-line.success {
  background: rgba(61, 220, 132, 0.12);
  color: #3ddc84;
}
.filter-status-line.error {
  background: rgba(255, 87, 51, 0.12);
  color: var(--accent-flare, #ff5733);
}

.wl-card {
  background: var(--bg-elevated);
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  padding: 12px 10px;
  text-align: center;
  cursor: pointer;
  transition: all var(--transition);
  position: relative;
  overflow: hidden;
  /* Button-tag reset (round-2 a11y, Cole Ramirez P0): tiles are
     now real <button> elements so Voice Control + screen readers
     can name them. The defaults wreck the look, so we re-baseline
     here. The shared styling above still applies regardless of
     tag. */
  display: block;
  width: 100%;
  font: inherit;
  color: inherit;
  -webkit-appearance: none;
  appearance: none;
}

.wl-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 4px;
}

.wl-card[data-wl="94"]::before { background: #3ddc84; }
.wl-card[data-wl="131"]::before { background: #00bcd4; }
.wl-card[data-wl="171"]::before { background: #f7a825; }
.wl-card[data-wl="193"]::before { background: #e67e22; }
.wl-card[data-wl="211"]::before { background: #9b59b6; }
.wl-card[data-wl="304"]::before { background: #e74c3c; }
.wl-card[data-wl="335"]::before { background: #3498db; }
.wl-card[data-wl="1600"]::before { background: #f0c040; }
.wl-card[data-wl="1700"]::before { background: #f5deb3; }

.wl-card:hover {
  border-color: var(--text-dim);
  transform: translateY(-2px);
}

.wl-card.selected {
  border-color: var(--accent-corona);
  background: rgba(123,97,255,0.08);
  box-shadow: 0 0 16px var(--glow-corona);
}

.wl-thumb {
  width: 120px;
  height: 120px;
  margin: 0 auto 8px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--bg-deep);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.3s ease;
}
/* Desktop: roughly double the thumb radius (120 → 220) so the
   wavelength previews land as posters in their own right, not
   afterthoughts. Mobile keeps the compact 120 so the 9 tiles still
   fit on a phone without two-row tile labels. */
@media (min-width: 720px) {
  .wl-thumb { width: 220px; height: 220px; }
}

.wl-thumb:not(.loaded) {
  opacity: 0.5;
}

.wl-thumb.loaded {
  opacity: 1;
  box-shadow: 0 0 12px rgba(0,0,0,0.4);
}

.wl-thumb img,
.wl-thumb canvas {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}

.wl-thumb-spinner {
  width: 16px;
  height: 16px;
  border: 2px solid var(--border);
  border-top-color: var(--accent-corona);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

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

.wl-value {
  font-family: 'JetBrains Mono', monospace;
  font-size: 1.1rem;
  font-weight: 500;
}

.wl-label {
  font-size: 0.72rem;
  color: var(--text-dim);
  margin-top: 4px;
  line-height: 1.3;
}

/* Preview Products Button */
.btn-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 11px;
  margin-top: 14px;
  background: transparent;
  color: var(--accent-cool);
  font-size: 0.9rem;
  font-weight: 600;
  border: 1.5px solid var(--accent-cool);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  font-family: 'Outfit', sans-serif;
  letter-spacing: 0.02em;
}
.btn-preview:hover:not(:disabled) {
  background: rgba(52,152,219,0.1);
  transform: translateY(-1px);
}
.btn-preview:disabled { opacity: 0.4; cursor: not-allowed; }

/* Generate Button */
.btn-generate {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 14px;
  margin-top: 18px;
  background: linear-gradient(135deg, var(--accent-sun), var(--accent-flare));
  color: #fff;
  font-size: 1rem;
  font-weight: 700;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  font-family: 'Outfit', sans-serif;
  letter-spacing: 0.02em;
}

.btn-generate:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 8px 30px rgba(255,94,58,0.35);
}

.btn-generate:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Progress bar */
.progress-track {
  width: 100%;
  height: 6px;
  background: var(--bg-elevated);
  border-radius: 3px;
  margin-top: 14px;
  overflow: hidden;
  display: none;
}

.progress-track.active { display: block; }

.progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--accent-corona), var(--accent-cool));
  border-radius: 3px;
  transition: width 0.4s ease;
}

.status-msg {
  font-size: 0.85rem;
  color: var(--text-secondary);
  margin-top: 8px;
  min-height: 20px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.status-msg .spinner {
  width: 14px; height: 14px;
  border: 2px solid var(--border);
  border-top-color: var(--accent-corona);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

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

/* Image Display.
   Sharp corners on the editor canvas — beta tester pointed out that the
   rounded image-stage corners weren't a faithful preview of the final
   physical product (acrylic / canvas / poster prints have square edges).
   Outer panel still has its own rounding via .section. */
.image-stage {
  /* Default backdrop is solid black so transparency in the canvas
     reads as "what you'll get printed against a dark fabric/medium"
     rather than a generic editor checker. The .image-stage--checker
     modifier opts back into the transparency-checker pattern for
     users who want to visualise alpha explicitly. */
  position: relative;
  background-color: #000;
  border-radius: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  /* User-resizable editor (proportional + centered). The corner handle
     sets --editor-scale (0.4–1, persisted per device).
     `width: fit-content` makes the stage HUG the canvas — no side
     letterbox — so the handle scales the image directly across its
     full range instead of first collapsing empty black bars. The
     scale is applied as a max-width cap (× available width) plus the
     canvas height cap below; both shrink together so the image scales
     linearly regardless of which axis is binding. margin-inline:auto
     centers it in BOTH the static grid cell and the floating
     (absolute, left:0/right:0) embed state. */
  width: fit-content;
  max-width: calc(100% * var(--editor-scale, 1));
  margin-inline: auto;
}
/* Empty / loading states have no canvas to hug, so give them a
   definite box (and the dashed placeholder room). */
.image-stage.empty,
.image-stage.loading {
  width: 100%;
  min-height: 200px;
}

/* Resize handle — bottom-right corner grip. Pointer + touch (one
   pointer model). Sits above the canvas; its pointerdown stops
   propagation so it never triggers canvas pan/crop. */
.editor-resize-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 22px;
  height: 22px;
  z-index: 7;
  cursor: nwse-resize;
  touch-action: none;
  opacity: 0.55;
  transition: opacity 0.15s ease;
  background:
    linear-gradient(-45deg,
      transparent 0 6px,
      rgba(255, 255, 255, 0.7) 6px 7px,
      transparent 7px 10px,
      rgba(255, 255, 255, 0.7) 10px 11px,
      transparent 11px 14px,
      rgba(255, 255, 255, 0.7) 14px 15px,
      transparent 15px);
}
.editor-resize-handle:hover,
.editor-resize-handle:focus-visible {
  opacity: 0.95;
}
@media (pointer: coarse) {
  .editor-resize-handle { width: 32px; height: 32px; }
}

html.dark .image-stage {
  background-color: #000;
}

/* Opt-in: classic editor checker pattern. Not used today; kept so
   a future "show transparency" toggle can flip the class. */
.image-stage.image-stage--checker {
  background-color: #fff;
  background-image:
    linear-gradient(45deg, #c8c8c8 25%, transparent 25%),
    linear-gradient(-45deg, #c8c8c8 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #c8c8c8 75%),
    linear-gradient(-45deg, transparent 75%, #c8c8c8 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 8px;
}
html.dark .image-stage.image-stage--checker {
  background-color: #1a1a2e;
  background-image:
    linear-gradient(45deg, #2a2a44 25%, transparent 25%),
    linear-gradient(-45deg, #2a2a44 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, #2a2a44 75%),
    linear-gradient(-45deg, transparent 75%, #2a2a44 75%);
}

.image-stage.empty {
  background: var(--bg-elevated) !important;
  background-image: none !important;
  border: 2px dashed var(--border);
}

.image-stage.empty::after {
  content: 'Your solar image will appear here';
  color: var(--text-dim);
  font-size: 0.9rem;
}

/* Loading spinner inside the image stage while a wavelength preview
   is being fetched. Beta tester reported the canvas looked frozen
   between tile click and image arrival ("had to be clicked a
   couple of times"); now a centred spinner makes the load visible
   so users don't re-click. JS toggles .loading on/off in
   loadHelioviewerPreview / _installPreviewImage. */
.image-stage.loading::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 56px;
  height: 56px;
  margin: -28px 0 0 -28px;
  border: 4px solid rgba(255, 255, 255, 0.16);
  border-top-color: var(--accent-corona, #7b61ff);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  z-index: 5;
  pointer-events: none;
}
.image-stage.loading::after {
  /* Subtle "Loading…" label below the spinner so the affordance is
     obvious even on very small canvases. */
  content: "Loading…";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, calc(-50% + 48px));
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.7);
  letter-spacing: 0.04em;
  z-index: 5;
  pointer-events: none;
}

#solarCanvas {
  max-width: 100%;
  /* Height cap scales with the user's --editor-scale so tall/narrow
     products shrink proportionally too (width already scales via the
     stage). */
  max-height: calc(70vh * var(--editor-scale, 1));
  width: auto;
  height: auto;
  display: block;
  cursor: crosshair;
  object-fit: contain;
  transform-origin: center center;
  transition: transform 0.15s ease;
}

#solarImg {
  display: none;
}

/* Edit Tools */
.edit-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

.edit-toolbar-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.edit-toolbar-break {
  width: 100%;
  height: 0;
  flex-basis: 100%;
}

/* Tab bar */
.edit-tab-bar {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-basis: 100%;
  width: 100%;
  border-bottom: 1px solid var(--border);
  padding-bottom: 6px;
  margin-top: 4px;
}
/* Reset button lives in the tab bar, pushed to the far right */
.edit-btn-reset {
  margin-left: auto;
  padding: 4px 12px;
  font-size: 0.78rem;
  opacity: 0.75;
}
.edit-btn-reset:hover { opacity: 1; }

.edit-tab {
  padding: 5px 16px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  color: var(--text-secondary);
  font-size: 0.82rem;
  font-family: 'Outfit', sans-serif;
  cursor: pointer;
  transition: all var(--transition);
}

.edit-tab:hover { color: var(--text-primary); border-color: var(--border); }
.edit-tab.active {
  /* `--accent-corona-text` is the brightened variant of the same
     purple, tuned for legibility on dark backgrounds (~6:1 against
     #1a1a2e). The original --accent-corona stays on the border so
     the visual identity is unchanged. */
  color: var(--accent-corona-text);
  border-color: var(--accent-corona);
  background: rgba(123,97,255,0.08);
  font-weight: 600;
}

/* Clock tab specific styling — when visible (wall_clock selected) the tab
   pulses gently so first-time users notice the clock-face customization
   controls. The pulse stops once the user activates the tab. */
.edit-tab-clock:not(.hidden):not(.active) {
  border-color: rgba(34, 197, 94, 0.45);
  color: #22c55e;
  font-weight: 600;
  animation: clock-tab-pulse 1.6s ease-in-out infinite;
}
@keyframes clock-tab-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.0); }
  50%      { box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.18); }
}
.edit-tab-clock.active {
  color: #22c55e;
  border-color: #22c55e;
  background: rgba(34,197,94,0.08);
  animation: none;
}

/* Tab content panels */
.edit-tab-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  flex-basis: 100%;
  width: 100%;
  padding-top: 8px;
}

.edit-tab-panel.hidden { display: none; }

/* Friendly hint shown when no tab panel is expanded — leans on the
   "defaults are already great" message so the editor doesn't intimidate
   on first open. Hidden via .hidden when a panel is active. */
.edit-tab-hint {
  flex-basis: 100%;
  width: 100%;
  margin-top: 6px;
  padding: 12px 14px;
  border-radius: var(--radius-sm, 8px);
  background: linear-gradient(135deg, rgba(123, 97, 255, 0.06), rgba(192, 107, 255, 0.06));
  border: 1px dashed rgba(123, 97, 255, 0.18);
  color: var(--text-secondary, #9898b8);
  font-size: 0.82rem;
  display: flex;
  align-items: center;
  gap: 10px;
}
.edit-tab-hint.hidden { display: none; }
.edit-tab-hint i {
  color: var(--accent-corona, #b692ff);
  font-size: 0.95rem;
}

.edit-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: 0.82rem;
  font-family: 'Outfit', sans-serif;
  cursor: pointer;
  transition: all var(--transition);
}

.edit-btn:hover { border-color: var(--accent-corona); background: rgba(123,97,255,0.06); }
.edit-btn.active { border-color: var(--accent-corona); background: rgba(123,97,255,0.12); color: var(--accent-corona); }

.edit-btn i { font-size: 0.85rem; }

/* Clock button: more apparent when visible (wall clock product selected) */
.edit-btn-clock:not(.hidden) {
  background: rgba(34, 197, 94, 0.12);
  border-color: #22c55e;
  color: #16a34a;
  font-weight: 600;
  box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.2);
}
.edit-btn-clock:not(.hidden):hover {
  background: rgba(34, 197, 94, 0.2);
  box-shadow: 0 0 12px rgba(34, 197, 94, 0.25);
}
.edit-btn-clock.active { border-color: #22c55e; background: rgba(34, 197, 94, 0.2); color: #16a34a; }

/* Timestamp position picker — 2×3 of small radio cells. Sits inline
   alongside the Tools-tab buttons; hidden until the user enables the
   timestamp so it doesn't crowd the panel for users who don't need it. */
.timestamp-pos {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 6px;
  padding: 6px 10px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
}
.timestamp-pos.hidden { display: none; }
.timestamp-pos-label {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.timestamp-pos-grid {
  display: grid;
  grid-template-columns: repeat(3, 22px);
  grid-template-rows: repeat(2, 22px);
  gap: 4px;
}
.timestamp-pos-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--text-secondary, #9898b8);
  font-size: 0.7rem;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.timestamp-pos-cell input[type="radio"] { display: none; }
.timestamp-pos-cell:hover {
  color: var(--text-primary, #e9eaf5);
  background: rgba(123, 97, 255, 0.1);
}
.timestamp-pos-cell:has(input:checked) {
  background: linear-gradient(135deg, rgba(123, 97, 255, 0.35), rgba(192, 107, 255, 0.35));
  border-color: rgba(123, 97, 255, 0.6);
  color: #fff;
}

/* Vertical-offset slider — sits to the right of the position grid so
   the user can nudge the caption inward from the chosen edge. */
.timestamp-pos-offset {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 130px;
}
.timestamp-pos-offset input[type="range"] {
  width: 70px;
  height: 4px;
}
.timestamp-pos-offset .slider-val {
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-secondary, #9898b8);
  min-width: 18px;
  text-align: right;
}

/* Clock color presets — sits inline with the colour picker. Smaller
   than the standard preset row so we can fit four chips + a picker
   on one line, and tagged with the same `.preset-btn` styling so the
   active-state highlight matches the crop/vignette presets above. */
.clock-color-presets {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.clock-color-presets .clock-color-preset {
  padding: 4px 10px;
  font-size: 0.72rem;
}

/* Preset buttons row */
.preset-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 8px;
  width: 100%;
}

.preset-row-label {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-right: 2px;
  white-space: nowrap;
}

/* Inline axis-letter chip ("X", "Y") that sits between the two
   Hard/Soft button pairs on the combined Crop Edge preset row.
   Tighter than .preset-row-label so the four buttons + label still
   fit one line on a desktop editor column. */
.preset-axis-label {
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--text-secondary);
  opacity: 0.9;
  padding: 0 2px;
  margin-left: 6px;
}
.preset-axis-label:first-of-type {
  margin-left: 0;
}

.preset-btn {
  font-size: 0.72rem !important;
  padding: 3px 8px !important;
  border-style: dashed !important;
  opacity: 0.85;
}

.preset-btn:hover {
  opacity: 1;
  border-style: solid !important;
}

/* Slider Row */
.slider-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 10px;
  /* Let the −/＋ step buttons (Cole P0) wrap to a second line on
     narrow viewports instead of squeezing the track to nothing. */
  flex-wrap: wrap;
}

/* ── Slider step buttons (Cole Ramirez round-2 motor/AT, P0) ──────
   Visible −/＋ controls flanking every range input so Voice Control
   can address them by name and Switch Control can scan them. 32px
   (38px on coarse pointers) clears WCAG 2.5.8 target size. The
   global :focus-visible ring from the a11y pass applies as-is. */
.slider-step-btn {
  flex: none;
  width: 32px;
  height: 32px;
  min-width: 32px;
  padding: 0;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text-primary);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation; /* no 300ms tap delay / double-tap zoom */
  transition: background var(--transition), border-color var(--transition);
}
.slider-step-btn:hover {
  border-color: var(--border-active);
  background: var(--bg-card);
}
.slider-step-btn:active {
  transform: translateY(1px);
}
@media (pointer: coarse) {
  .slider-step-btn {
    width: 38px;
    height: 38px;
    min-width: 38px;
  }
}
/* Dense text-effect rows pack a range input next to color pickers and
   toggles; allow them to wrap so the added step buttons never force
   horizontal overflow on mobile. */
.field-row { flex-wrap: wrap; }

.slider-row label {
  font-size: 0.82rem;
  color: var(--text-secondary);
  min-width: 80px;
}

.slider-row input[type="range"] {
  flex: 1;
  accent-color: var(--accent-corona);
  height: 10px;
}

/* Hank P1 (round-2 ergonomics): bump slider hit-area above Fitts' Law
   threshold. Native ~14px thumbs on 6px tracks were below the ~9.6mm
   trackpad acquisition sweet spot, and with 6-9 sliders stacked per
   tab a 30-min session was a first-dorsal-interosseous fatigue / RSI
   risk. Track 6→10px, thumb ~14→22px (26px on coarse pointers). The
   thumb's negative margin-top keeps it centered: -(22-10)/2 = -6px.
   Applies to every range input in the editor (slider-row, text-tool,
   clock numbers, vignette, etc.); the compact .timestamp-pos-offset
   inline offset keeps its own smaller height. */
input[type="range"]::-webkit-slider-runnable-track {
  height: 10px;
  border-radius: 5px;
}
input[type="range"]::-moz-range-track {
  height: 10px;
  border-radius: 5px;
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  margin-top: -6px;
}
input[type="range"]::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
}
@media (pointer: coarse) {
  input[type="range"]::-webkit-slider-thumb {
    width: 26px;
    height: 26px;
    margin-top: -8px;
  }
  input[type="range"]::-moz-range-thumb {
    width: 26px;
    height: 26px;
  }
}

.slider-row .slider-val {
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.8rem;
  color: var(--text-dim);
  min-width: 40px;
  text-align: right;
}

.vignette-fade-row {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 12px;
  margin-top: 8px;
}

.vignette-fade-toggle {
  flex-shrink: 0;
}

.vignette-fade-row .vignette-fade-color-label {
  margin-left: auto;
  margin-right: 0;
}

.vignette-fade-row .vignette-color-input {
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  background: var(--bg-card);
  flex-shrink: 0;
}

/* Background toggle: Color option shows current color as face; picker opens on click */
.filter-opt-color {
  position: relative;
  min-width: 36px;
  padding-left: 10px;
  padding-right: 10px;
}
.filter-opt-color .vignette-fade-color-picker {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  padding: 0;
  border: none;
}
.filter-opt-color .filter-opt-color-label {
  position: relative;
  z-index: 0;
  pointer-events: none;
  color: inherit;
}

/* Non-blocking loading indicator pinned to the upper-right of the quality
   timeline, next to the "Quality" label, so it doesn't shift the step row. */
.filter-loading-indicator {
  position: absolute;
  top: 4px;
  right: 10px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.7rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.filter-loading-indicator.hidden {
  display: none !important;
}
.filter-loading-spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--border);
  border-top-color: var(--accent-corona);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

/* Product Cards */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 16px;
}

.product-card {
  background: var(--bg-elevated);
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: pointer;
  transition: all var(--transition);
}

/* Hover = subtle lift + soft neutral border tint. NOT the same color
   as the .selected sun-orange ring (friction audit: agent reported
   the hover glow read as a permanent "you selected this" state and
   followed the cursor card to card, making the actual selection
   ambiguous). Selected stays a thicker orange ring + glow. */
.product-card:hover {
  border-color: var(--text-dim);
  transform: translateY(-3px);
  box-shadow: 0 8px 25px rgba(0,0,0,0.2);
}

.product-card.selected {
  border-color: var(--accent-sun);
  box-shadow: 0 0 0 2px var(--accent-sun), 0 0 24px var(--glow-sun);
}
.product-card.selected:hover {
  border-color: var(--accent-sun);
}

.product-preview {
  width: 100%;
  aspect-ratio: 1;
  background: var(--bg-deep);
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

.product-preview canvas {
  max-width: 70%;
  max-height: 70%;
}

/* Cold-load mockup skeleton (product-first refactor, commit 4).
   When the defaultMockupManifest hasn't resolved yet, the product
   cards render with empty preview boxes. The skeleton animates a
   subtle shimmer so the empty state reads as "loading" rather than
   "broken". Removed by JS on the second renderProducts() call after
   the manifest lands. */
.product-grid.is-loading-mockups .product-preview {
  background: linear-gradient(110deg,
    var(--bg-deep) 30%,
    var(--bg-elevated) 50%,
    var(--bg-deep) 70%);
  background-size: 200% 100%;
  animation: productMockupShimmer 1.4s ease-in-out infinite;
}
.product-grid.is-loading-mockups .product-preview::after {
  content: "Loading photoreal previews…";
  position: absolute;
  inset: auto 0 14px;
  font-size: 0.68rem;
  color: var(--text-dim);
  text-align: center;
  letter-spacing: 0.04em;
}
@keyframes productMockupShimmer {
  from { background-position: 100% 0; }
  to   { background-position: -100% 0; }
}

.product-preview .product-icon {
  font-size: 3rem;
  color: var(--text-dim);
}

/* Tiny upper-left glyph that persists once the mockup loads. Beta testers
   asked for an at-a-glance product-type indicator so they can scan the grid
   without reading each name — this is the same fa-icon that renders pre-load,
   shrunk to a corner badge. The backdrop is semi-opaque for legibility on
   both light-product and dark-product mockups. */
/* Faint popularity counter, caddy-corner (bottom-right) from the type
   icon. Shows "buys | clicks-that-didn't-convert"; tooltip "buys |
   clicks". The grid sorts by these same numbers. */
.product-preview .product-stats-badge {
  position: absolute;
  bottom: 8px;
  right: 8px;
  padding: 2px 7px;
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
  color: rgba(255, 255, 255, 0.62);
  background: rgba(10, 12, 22, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 8px;
  cursor: help;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  z-index: 2;
}
.product-preview .product-icon-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 12, 22, 0.72);
  color: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 10px;
  font-size: 24px;
  pointer-events: none;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  z-index: 2;
}

.product-info {
  padding: 14px;
}

.product-name {
  font-weight: 600;
  font-size: 1rem;
}

.product-desc {
  font-size: 0.8rem;
  color: var(--text-secondary);
  margin-top: 4px;
}

.product-price {
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  color: var(--accent-sun);
  margin-top: 6px;
}

/* Order Button */
.btn-order {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 16px;
  margin-top: 18px;
  background: linear-gradient(135deg, var(--accent-corona), #5c4bbf);
  color: #fff;
  font-size: 1.05rem;
  font-weight: 700;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  font-family: 'Outfit', sans-serif;
}

.btn-order:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 8px 30px var(--glow-corona);
}

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

.btn-secondary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 20px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: 0.9rem;
  font-family: 'Outfit', sans-serif;
  cursor: pointer;
  transition: all var(--transition);
  text-decoration: none;
}

.btn-secondary:hover { border-color: var(--accent-corona); }

/* Modal */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  /* z-index bumped above the feedback FAB cluster (9990) so the
     orange/purple Request + Comment pills don't bleed on top of every
     showInfo dialog. Scroll/popup reviewer flagged this as BLOCKER. */
  z-index: 9996;
  padding: 20px;
  animation: fadeIn 0.2s ease;
}
/* Belt-and-suspenders: also hide the FAB cluster while ANY modal is
   open (body.modal-open is toggled by showInfo / showConfirmSelectModal
   / showColorChooser). Covers edge cases where stacking contexts
   conspire to keep FAB visible despite the z bump. */
body.modal-open .feedback-fab-group { display: none !important; }

@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }

.modal-box {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 28px;
  max-width: 420px;
  width: 100%;
  animation: slideUp 0.3s ease;
}

@keyframes slideUp { from { transform:translateY(20px); opacity:0; } to { transform:translateY(0); opacity:1; } }

.modal-box h3 { margin-bottom: 12px; font-size: 1.1rem; }

.modal-box p { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 16px; line-height: 1.5; }

.modal-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}

.modal-actions button {
  padding: 8px 18px;
  border-radius: var(--radius-sm);
  font-family: 'Outfit', sans-serif;
  font-size: 0.9rem;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text-primary);
  transition: all var(--transition);
}

.modal-actions .btn-confirm {
  background: var(--accent-corona);
  border-color: var(--accent-corona);
  color: #fff;
}

/* Loading state for the modal's Create/Confirm button. While the Promise
   returned by onConfirm is pending the button shows a spinner + label so
   users see the click registered and compute is underway. Both buttons are
   disabled until the handler settles. */
.modal-actions .btn-confirm.is-loading {
  opacity: 0.88;
  cursor: progress;
  /* Keep background full-strength so "loading" reads as active, not inert. */
}
.modal-actions button:disabled {
  cursor: progress;
  opacity: 0.7;
}
.modal-actions .btn-confirm .btn-spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: btn-spin 0.7s linear infinite;
  vertical-align: -2px;
  margin-right: 6px;
}
@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

.hidden { display: none !important; }

/* Info tip */
.info-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  padding: 10px 14px;
  background: rgba(123,97,255,0.06);
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--accent-corona);
}

.info-row i { color: var(--accent-corona); }
.info-row span { font-size: 0.82rem; color: var(--text-secondary); }

/* ── Vibe grid (above-the-fold curated moments) ─────────────────────
   Six cards in a 3-col grid on desktop, 2-col on tablet, 1-col on mobile.
   Each card is a real <button>: clicking pre-fills date+wl+time in the
   configurator below and scrolls to it. The thumbnail lives in a circular
   well so the Sun reads as a Sun even before the image loads. */
.vibe-grid-section { margin-bottom: 24px; }
.vibe-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
}
@media (max-width: 880px) { .vibe-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .vibe-grid { grid-template-columns: 1fr; } }

.vibe-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-md, 14px);
  transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
  overflow: visible;  /* allow info-popover to escape */
}
.vibe-card:hover {
  background: rgba(255,179,71,0.06);
  border-color: rgba(255,179,71,0.35);
}
@media (prefers-reduced-motion: no-preference) {
  .vibe-card:hover { transform: translateY(-1px); }
}

/* The main click target — wraps thumb + meta. */
.vibe-open {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md, 14px);
  cursor: pointer;
  text-align: left;
  color: inherit;
  font: inherit;
  width: 100%;
}
.vibe-open:focus-visible {
  outline: 2px solid var(--accent-corona, #ffb347);
  outline-offset: -2px;
}

/* Controls cluster (Raw/RHEF + info) sits top-right, layered over thumb.
   Hidden by default; .has-tiers reveals the toggle once the manifest
   confirms a Raw/RHEF pair exists for this slug. */
.vibe-controls {
  position: absolute;
  top: 8px; right: 8px;
  display: flex;
  align-items: center;
  gap: 6px;
  z-index: 2;
  pointer-events: none;  /* let clicks pass to .vibe-open by default */
}
.vibe-controls > * { pointer-events: auto; }
.vibe-tier-toggle {
  display: none;  /* shown via .has-tiers below */
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(6px);
  border-radius: 999px;
  padding: 2px;
  border: 1px solid rgba(255,255,255,0.10);
}
.vibe-card.has-tiers .vibe-tier-toggle { display: inline-flex; }

/* Click feedback — testers reported a multi-second pause with no visual
   confirmation that tapping a vibe card had been received. Dim the card
   and overlay a soft spinner ring until _installPreviewImage clears the
   class (or the 12s backstop in _activateVibe fires). */
.vibe-card.is-loading {
  pointer-events: none;
  opacity: 0.85;
}
.vibe-card.is-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 38px;
  height: 38px;
  margin: -19px 0 0 -19px;
  border-radius: 50%;
  border: 3px solid rgba(255, 179, 71, 0.25);
  border-top-color: var(--accent-corona, #ffb347);
  animation: spin 0.9s linear infinite;
  z-index: 5;
  pointer-events: none;
}
.vibe-card.is-loading::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(8, 10, 20, 0.32);
  border-radius: inherit;
  pointer-events: none;
}
.vibe-tier-btn {
  background: transparent;
  color: var(--text-secondary, #d0c8d8);
  border: 0;
  padding: 4px 10px;
  border-radius: 999px;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
  min-height: 24px;
  transition: background 120ms ease, color 120ms ease;
}
.vibe-tier-btn.is-active {
  background: var(--accent-corona, #ffb347);
  color: #1a1326;
}
.vibe-tier-btn:focus-visible {
  outline: 2px solid var(--accent-corona, #ffb347);
  outline-offset: 1px;
}
.vibe-info-btn {
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(6px);
  color: var(--text-secondary, #d0c8d8);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 999px;
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.vibe-info-btn:hover { color: var(--accent-corona, #ffb347); }
.vibe-info-btn:focus-visible { outline: 2px solid var(--accent-corona, #ffb347); outline-offset: 1px; }

/* Attribution popover (created dynamically — kept stylable here). */
/* Narrative reveal CTA — a single pulsing orange button above the
   vibe grid. Cards start in Raw mode; clicking this button kicks off
   a radial-wipe transition to RHEF across every card, then fades the
   button out and reveals the per-card Raw/RHEF toggles. The "Ohhh and
   ahhh" hook at the start of the user experience. */
.vibe-reveal-cta {
  display: flex;
  justify-content: center;
  margin: 4px 0 18px;
}
.vibe-reveal-cta.hidden { display: none; }
.vibe-reveal-cta.is-dismissed { opacity: 0; pointer-events: none; transition: opacity 600ms ease-out; }
.vibe-reveal-btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 24px;
  font: inherit;
  font-size: 1.0rem;
  font-weight: 700;
  color: #1a1326;
  background: linear-gradient(135deg, #ffb347 0%, #ff8a3d 100%);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 4px 18px rgba(255,179,71,0.35);
  min-height: 48px;
  letter-spacing: 0.01em;
}
.vibe-reveal-btn i { font-size: 1.05rem; }
/* Subtitle next to the main CTA label — explains WHAT "filtered view"
   means without making the user click to find out. Friction audit
   flagged the unexplained CTA as competing with the vibe cards. */
.vibe-reveal-sub {
  display: inline-block;
  margin-left: 8px;
  font-size: 0.78rem;
  font-weight: 500;
  opacity: 0.85;
  letter-spacing: 0;
}
@media (max-width: 520px) {
  .vibe-reveal-sub { display: block; margin: 4px 0 0; }
}
.vibe-reveal-btn:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .vibe-reveal-btn {
    animation: vibeRevealPulse 2.2s ease-in-out infinite;
  }
  .vibe-reveal-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 6px 24px rgba(255,179,71,0.55);
  }
}
@keyframes vibeRevealPulse {
  0%, 100% { box-shadow: 0 4px 18px rgba(255,179,71,0.35), 0 0 0 0 rgba(255,179,71,0.55); }
  50%      { box-shadow: 0 4px 18px rgba(255,179,71,0.35), 0 0 0 14px rgba(255,179,71,0.00); }
}

/* ── Workflow step-machine section visibility (commit 3) ──────
   Body classes drive which sections are visible per step. The
   .hidden class was removed from #productSection's initial markup
   so it's visible by default; CSS body.step-product hides everything
   else.

   Step "product" (cold landing): products only. Vibe-grid + configure
   + editor hidden.
   Step "image" (variant picked): vibe-grid + configure visible.
   Product section + editor hidden — the breadcrumb pill is how the
   user navigates back to product.
   Step "editor" (image picked): editor visible. Product + vibe-grid
   + configure hidden — the editor's own UI takes over.

   Each rule uses !important to defeat any leftover JS-managed .hidden
   class. JS no longer manages .hidden on these sections under the
   new flow, but be defensive in case a legacy code path adds it back. */
body.step-product .vibe-grid-section,
body.step-product #configSection,
body.step-product #editSection,
body.step-product #vibeRevealCta {
  display: none !important;
}
/* On step "image" the master tier toggle is the user's primary control
   (cards visible, mockups updating). Friction agent observed it stayed
   hidden because of the legacy two-state "Reveal button → toggle"
   narrative — on the new product-first flow the narrative beat is gone,
   the user has already committed to a product and just needs to choose
   an image at their preferred tier. Force the toggle visible on step
   "image", and hide the per-product "Reveal" pulsing button (it was a
   nice intro but it now blocks the toggle from showing). */
body.step-image #vibeRevealCta {
  display: flex !important;
}
body.step-image .vibe-reveal-btn { display: none !important; }
body.step-image .vibe-master-toggle { display: inline-flex !important; }
body.step-image #productSection,
body.step-image #editSection {
  display: none !important;
}
body.step-editor #productSection,
body.step-editor .vibe-grid-section,
body.step-editor #configSection {
  display: none !important;
}
/* When a product is selected (step image/editor), .product-card
   stays in DOM but we override .hidden so the user can see their
   pick rendered with the editor canvas in scroll-back view. NOT
   implemented in commit 3 — defer to a future polish pass that
   adds a "selected product mini preview" affordance. */

/* ── Workflow breadcrumb (product-first refactor, commit 2) ──────
   Sticky-top compact bar. Hosts back-pills for completed steps.
   Stacks ABOVE the master tier toggle (`#vibeRevealCta`) on step
   "image" — friction-audit agent 2 reported both were fixed-top
   centered and visually collided. The breadcrumb takes top:8px,
   the master toggle below it takes top:52px (offset = pill height
   + gap). */
.workflow-breadcrumb {
  position: fixed;
  top: max(8px, env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 52;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  pointer-events: none;
}
/* On step "image" the master toggle sits below the breadcrumb. */
body.step-image #vibeRevealCta {
  top: max(52px, calc(env(safe-area-inset-top, 0px) + 44px));
}
/* Mobile: the breadcrumb + master-toggle stack ate ~100px of the
   viewport's top chrome on a 390x844 iPhone (persona-sweep finding).
   Hide the breadcrumb on narrow screens — the product name is still
   accessible via the editor's "Change product" button and the master
   toggle is the only thing that needs constant access on step image. */
@media (max-width: 749px) {
  .workflow-breadcrumb { display: none !important; }
  body.step-image #vibeRevealCta {
    top: max(8px, env(safe-area-inset-top, 0px)) !important;
  }
}
/* Force-clear the rail-mode pane when leaving the editor. Friction
   agent 4 reported the editor's left-rail preview canvas, title, and
   aspect-ratio label leaked into the product picker after browser back —
   the rail-mode JS toggle re-evaluates on scroll but not on step
   transitions, so the leftover preview overlay sat there until the
   user scrolled. Override visibility from CSS so the leak can't happen
   regardless of JS class timing. The body class itself is cleared in
   _applyStep (solar-archive.js). */
body.step-product #selectedProductPreview,
body.step-image #selectedProductPreview {
  display: none !important;
}
.workflow-breadcrumb > * { pointer-events: auto; }
.workflow-breadcrumb.hidden { display: none; }
.workflow-breadcrumb-pills {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.workflow-breadcrumb-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(255,179,71,0.30);
  color: var(--text-primary, #eeeef6);
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease;
}
.workflow-breadcrumb-pill:hover,
.workflow-breadcrumb-pill:focus-visible {
  background: rgba(0,0,0,0.72);
  border-color: var(--accent-corona, #ffb347);
}
.workflow-breadcrumb-pill .arrow {
  font-size: 0.78rem;
  opacity: 0.7;
}
.workflow-breadcrumb-sep {
  color: var(--text-dim, #8e8eab);
  font-size: 0.85rem;
  opacity: 0.6;
}

/* Master Preview / Original / Filtered segmented toggle. Sticky-pinned
   to the top of the viewport so the user can flip tiers while scrolled
   into the product picker — the original placement at the vibe-grid
   header was off-screen by the time the products were visible. Hidden
   once the editor engages (body.left-rail-preview) since the editor
   has its own Quality timeline. */
.vibe-master-toggle {
  display: inline-flex;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(6px);
  border-radius: 999px;
  padding: 4px;
  border: 1px solid rgba(255,179,71,0.30);
  gap: 2px;
}
.vibe-master-toggle.hidden { display: none; }
/* Floating placement. position:fixed escapes the .section parent and
   pins to the viewport so the toggle stays visible while the user
   scrolls from the vibe-card section down into the product picker.
   Centered horizontally; top-padded clear of the address bar on mobile
   (env(safe-area-inset-top) for notched devices). The first-reveal
   pulsing button (.vibe-reveal-btn) also floats here during its brief
   pre-reveal lifetime, then is hidden by JS once clicked. */
/* !important on position guards against any later rule (or browser
   quirk involving ancestor transforms) downgrading the float to
   static. z-index sits above .workflow-breadcrumb (52) so when the
   user scrolls and both stack at the top, the toggle is the upper
   layer rather than the lower. */
/* Defence in depth — every property that controls "is this floating
   at the top of the viewport?" is !important so no other rule (or
   future regression) can quietly drop the toggle into the document
   flow. Reviewer & Gilly both kept hitting "the toggle isn't sticking";
   strengthening the rules here closes the door on it. */
#vibeRevealCta {
  position: fixed !important;
  top: max(8px, env(safe-area-inset-top, 0px)) !important;
  left: 50% !important;
  transform: translateX(-50%) !important;
  z-index: 60 !important;
  display: flex !important;
  flex-direction: column !important;
  align-items: center !important;
  gap: 8px !important;
  margin: 0 !important;  /* override .vibe-reveal-cta margin: 4px 0 18px */
  pointer-events: none;
}
#vibeRevealCta > * { pointer-events: auto; }
/* The vibe-grid section reserves space for the toggle's original (in-flow)
   slot so the floating widget doesn't slide over the section header.
   Adds 64px of breathing room at the top of the vibe grid. */
.vibe-grid-section { padding-top: 64px; }
/* Drop the toggle's float-pin on step "editor" — the editor's Quality
   timeline owns the control surface there. Gating on body.step-editor
   (NOT the prior :has(#editSection:not(.hidden)) selector) so the
   selector doesn't keep matching after a popstate that leaves the
   editSection element with .hidden class removed but the section
   visually hidden by body-class CSS — friction-audit agent 3 found
   this left the toggle "position: static" on step "image" after a
   back-nav from editor, so it scrolled out of view. */
body.step-editor #vibeRevealCta {
  /* Editor owns the Quality timeline; hide the master toggle entirely
     instead of dropping it into static flow (which used to leak a
     visible row at the top of the editor on prod). */
  display: none !important;
}
body.step-editor .vibe-master-toggle,
body.step-editor .vibe-reveal-btn {
  display: none;
}

.vibe-master-btn {
  background: transparent;
  color: var(--text-secondary, #d0c8d8);
  border: 0;
  padding: 8px 18px;
  border-radius: 999px;
  font: inherit;
  font-size: 0.88rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  min-height: 36px;
  transition: background 160ms ease, color 160ms ease;
}
.vibe-master-btn.is-active {
  background: var(--accent-corona, #ffb347);
  color: #1a1326;
}
/* Indicator-first: while a tier request is in flight, the toggle shows
   a subtle pending state on the TARGET pill (where the indicator will
   land if the wipe succeeds). The button doesn't take .is-active until
   _wipeAllCards resolves, so the visible "you are here" indicator can't
   de-sync from the actually-rendered tier. */
.vibe-master-toggle.is-pending .vibe-master-btn { cursor: progress; }
.vibe-master-btn.is-pending-target {
  background: rgba(255,179,71,0.18);
  color: var(--accent-corona, #ffb347);
  position: relative;
}
.vibe-master-btn.is-pending-target::after {
  content: "";
  display: inline-block;
  width: 9px; height: 9px;
  margin-left: 8px;
  border: 1.5px solid rgba(255,179,71,0.30);
  border-top-color: var(--accent-corona, #ffb347);
  border-radius: 50%;
  vertical-align: middle;
  animation: spin 0.7s linear infinite;
}
.vibe-master-btn:focus-visible {
  outline: 2px solid var(--accent-corona, #ffb347);
  outline-offset: 2px;
}
@media (pointer: coarse) {
  .vibe-master-btn { min-height: 44px; padding: 10px 22px; }
}

/* Radial-wipe transition overlay: the new RHEF (or Raw) image is
   appended over the current thumb image with clip-path: circle(0%
   at center), then animated out to circle(150%). The underlying img
   is removed once the animation ends; see _runTierWipe() in
   solar-archive.js. */
.vibe-thumb-overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  z-index: 1;
  clip-path: circle(0% at 50% 50%);
}
@media (prefers-reduced-motion: no-preference) {
  .vibe-thumb-overlay.is-wiping {
    animation: vibeThumbWipe 720ms ease-out forwards;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* Snap rather than animate when reduced-motion is requested. */
  .vibe-thumb-overlay.is-wiping {
    clip-path: circle(150% at 50% 50%);
  }
}
@keyframes vibeThumbWipe {
  0%   { clip-path: circle(0% at 50% 50%); opacity: 0.6; }
  20%  { opacity: 1; }
  100% { clip-path: circle(150% at 50% 50%); opacity: 1; }
}

/* HEK event-tile selected styling — make it loud enough that the
   X9.3-match pre-check (or any selection-follows-focus pick) is
   obviously "the one." Adds a checkmark + brighter background +
   ring on top of the existing aria-checked = orange-tint style. */
.hek-tile[role="radio"][aria-checked="true"] {
  background: rgba(255,179,71,0.22);
  border-color: #ffb347;
  box-shadow: 0 0 0 1px #ffb347 inset, 0 6px 18px rgba(255,179,71,0.18);
}
.hek-tile[role="radio"][aria-checked="true"]::after {
  content: "\f00c";  /* Font Awesome check (solid) */
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  position: absolute;
  top: 8px;
  right: 10px;
  color: #ffb347;
  font-size: 0.85rem;
  opacity: 0.85;
}

.vibe-info-popover {
  position: absolute;
  top: 44px; right: 8px;
  z-index: 10;
  max-width: min(280px, calc(100% - 16px));
  padding: 12px 14px 12px 14px;
  background: #1a1326;
  border: 1px solid rgba(255,179,71,0.35);
  border-radius: var(--radius-sm, 10px);
  box-shadow: 0 8px 24px rgba(0,0,0,0.55);
  font-size: 0.80rem;
  color: var(--text-secondary, #d0c8d8);
  line-height: 1.4;
}
.vibe-info-popover.hidden { display: none; }
.vibe-info-popover h4 {
  margin: 0 0 6px;
  font-size: 0.85rem;
  color: var(--text-primary, #fff);
}
.vibe-info-popover p { margin: 0 0 6px; }
.vibe-info-popover p:last-child { margin-bottom: 0; }
.vibe-info-popover a { color: var(--accent-corona, #ffb347); }
.vibe-info-close {
  position: absolute;
  top: 4px; right: 6px;
  background: transparent;
  color: var(--text-dim, #9a93a8);
  border: 0;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 4px 6px;
}
.vibe-info-close:hover { color: var(--text-primary, #fff); }

/* "Your day" birthday card — no thumb; date input + hint only. */
.vibe-card-birthday { background: linear-gradient(135deg, rgba(255,179,71,0.06), rgba(123,97,255,0.06)); border-color: rgba(255,179,71,0.20); }
.vibe-birthday-inner {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 18px 16px;
  height: 100%;
  justify-content: center;
  text-align: left;
}
.vibe-birthday-icon {
  color: var(--accent-corona, #ffb347);
  font-size: 1.8rem;
}
.vibe-birthday-meta { display: flex; flex-direction: column; gap: 2px; margin-bottom: 6px; }
.vibe-birthday-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim, #9a93a8);
  font-weight: 600;
}
.vibe-birthday-input {
  padding: 10px 12px;
  background: rgba(0,0,0,0.30);
  color: var(--text-primary, #fff);
  border: 1px solid rgba(255,255,255,0.15);
  border-radius: 8px;
  font: inherit;
  font-size: 1.0rem;
  min-height: 44px;
  width: 100%;
}
.vibe-birthday-input:focus-visible { outline: 2px solid var(--accent-corona, #ffb347); outline-offset: 1px; }
.vibe-birthday-hint {
  font-size: 0.78rem;
  color: var(--text-dim, #9a93a8);
}
.vibe-thumb {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(255,179,71,0.10), rgba(0,0,0,0.4) 70%);
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.vibe-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; }
/* JPG plate-scale match. Helioviewer takeScreenshot at 1024² covers
   FOV = 3000 arcsec (image_scale = 3000/1024 ≈ 2.93 arcsec/px). SDO/AIA
   L1 FITS rendered at 3000² with sunpy's native extent covers
   FOV ≈ 2458 arcsec (4096 × 0.6). So the JPG shows the disk at 2458/3000
   = 0.82× the relative size the science image shows. We scale the JPG up
   by 3000/2458 ≈ 1.22× so its disk co-registers with the Raw/RHEF disk
   when toggling. This crops ~11% off each edge of the JPG (off-limb
   empty space — no info loss at the card thumb size). The alternative
   — shrinking the science to match — would leave a transparent margin
   since FITS data doesn't extend past 2458 arcsec. */
.vibe-thumb img.is-jpg-tier { transform: scale(1.22); transform-origin: center; }
.vibe-thumb.is-loading::after {
  content: "";
  position: absolute; inset: 0;
  background: radial-gradient(circle at 50% 50%, rgba(255,179,71,0.18), transparent 70%);
}
.vibe-meta { display: flex; flex-direction: column; gap: 2px; }
.vibe-title { font-weight: 700; font-size: 1.0rem; color: var(--text-primary, #fff); }
.vibe-sub   { font-size: 0.85rem; color: var(--text-secondary, #d0c8d8); }
.vibe-date  { font-size: 0.78rem; color: var(--text-dim, #9a93a8); margin-top: 2px; }

/* ── HEK 4-tile picker (replaces the single badge) ───────────────────
   3 event tiles + 1 custom-time tile. Section is visible-but-skeleton
   during HEK loading; never hidden on quiet/error days (landmark
   consistency per a11y agent). */
.hek-picker-section {
  margin-top: 12px;
  padding: 14px;
  background: rgba(255,179,71,0.05);
  border: 1px solid rgba(255,179,71,0.18);
  border-radius: var(--radius-md, 14px);
}
.hek-picker-section.hidden { display: none; }
.hek-picker-header { margin-bottom: 10px; }
.hek-picker-title  { margin: 0; font-size: 1.0rem; color: var(--text-primary, #fff); font-weight: 600; }
.hek-picker-sub    { margin: 4px 0 0; font-size: 0.82rem; color: var(--text-secondary, #d0c8d8); }
.hek-tile-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}
@media (max-width: 760px) { .hek-tile-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.hek-tile {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 12px;
  background: rgba(0,0,0,0.18);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-sm, 10px);
  cursor: pointer;
  text-align: left;
  color: inherit;
  font: inherit;
  position: relative;
  transition: background 160ms ease, border-color 160ms ease;
  /* WCAG 2.5.5 / 2.5.8 target-size: guarantee 64px+ hit area regardless
     of inner content height. Inner flex column will still grow past
     this for multi-line tiles. */
  min-height: 64px;
}
.hek-tile:hover { background: rgba(255,179,71,0.10); border-color: rgba(255,179,71,0.45); }
.hek-tile:focus-visible { outline: 2px solid var(--accent-corona, #ffb347); outline-offset: 2px; }
.hek-tile[aria-checked="true"] {
  background: rgba(255,179,71,0.15);
  border-color: #ffb347;
}
.hek-tile.is-top { border-left: 3px solid #ffb347; }
.hek-tile.is-fallback { border-left-color: rgba(160,160,160,0.45); background: rgba(255,255,255,0.02); }
.hek-tile-rank {
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-dim, #9a93a8); font-weight: 600;
}
.hek-tile.is-top .hek-tile-rank { color: #ffb347; }
.hek-tile-time { font-size: 1.05rem; font-weight: 700; color: var(--text-primary, #fff); }
.hek-tile-type { font-size: 0.82rem; color: var(--text-secondary, #d0c8d8); }
.hek-tile-meta { font-size: 0.72rem; color: var(--text-dim, #9a93a8); }

/* GOES class badges — three redundant channels per WCAG 1.4.1 (color
   alone isn't enough; icon-shape + letter + color). */
.hek-tile-goes {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 0.72rem; font-weight: 700;
  background: rgba(255,255,255,0.06);
}
.hek-tile-goes[data-cls="X"] { background: rgba(255,80,80,0.20); color: #ffb0b0; }
.hek-tile-goes[data-cls="M"] { background: rgba(255,160,60,0.18); color: #ffd089; }
.hek-tile-goes[data-cls="C"] { background: rgba(255,220,80,0.12); color: #ffe18a; }

/* Fine-tune custom-time tile — [-] / display / [+] row.
   Replaces the earlier <select>+Apply pattern. ±1 min per click, with
   a 1.5 s debounce in JS before the JPG previews refresh. The display
   doubles as the live aria-live readout. */
.hek-tile-custom {
  background: rgba(255,255,255,0.03);
  cursor: default;
}
.hek-tile-custom:hover { background: rgba(255,179,71,0.06); }
.hek-tile-custom .hek-tile-rank { color: var(--accent-corona, #ffb347); }
.hek-time-fine {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
  margin-top: 4px;
}
.hek-time-step {
  flex: 0 0 auto;
  width: 44px; height: 44px;     /* WCAG 2.5.5 hit-area */
  background: rgba(255,179,71,0.12);
  color: var(--accent-corona, #ffb347);
  border: 1px solid rgba(255,179,71,0.40);
  border-radius: 999px;
  font: inherit; font-size: 1.25rem; font-weight: 700;
  line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background 120ms ease, transform 120ms ease;
}
.hek-time-step:hover { background: rgba(255,179,71,0.22); }
@media (prefers-reduced-motion: no-preference) {
  .hek-time-step:active { transform: scale(0.95); }
}
.hek-time-step:focus-visible {
  outline: 2px solid var(--accent-corona, #ffb347);
  outline-offset: 2px;
}
@media (pointer: coarse) {
  .hek-time-step { width: 48px; height: 48px; }
}
.hek-time-display {
  flex: 1 1 auto;
  text-align: center;
  font-size: 1.25rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary, #fff);
  letter-spacing: 0.02em;
}
/* Exact-time picker row — sits below the [-]/[+] cluster. Uses the
   native <input type="time"> so the user gets the OS's clock picker
   (analog clock on iOS, time spinner on desktop). Same debounced
   refresh as the step buttons. */
.hek-time-exact {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px dashed rgba(255,255,255,0.08);
}
.hek-time-exact-label {
  font-size: 0.75rem;
  color: var(--text-dim, #9a93a8);
}
.hek-time-exact-input {
  padding: 6px 10px;
  background: rgba(0,0,0,0.30);
  color: var(--text-primary, #fff);
  border: 1px solid rgba(255,255,255,0.15);
  border-radius: 6px;
  font: inherit;
  font-size: 0.95rem;
  min-height: 36px;
  font-variant-numeric: tabular-nums;
}
.hek-time-exact-input:focus-visible {
  outline: 2px solid var(--accent-corona, #ffb347);
  outline-offset: 1px;
}
@media (pointer: coarse) {
  .hek-time-exact-input { min-height: 44px; }
}
.hek-custom-utc { font-size: 0.72rem; color: var(--text-dim, #9a93a8); }

/* Skeleton tiles during loading. */
.hek-tile.is-skeleton {
  background: rgba(255,255,255,0.04);
  border-color: rgba(255,255,255,0.06);
  cursor: default;
  pointer-events: none;
  min-height: 80px;
}
@media (prefers-reduced-motion: no-preference) {
  .hek-tile.is-skeleton::before {
    content: "";
    position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.05) 50%, transparent 100%);
    animation: hekShimmer 1.4s ease-in-out infinite;
  }
}
@keyframes hekShimmer {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

/* Wavelength auto-suggest caption (above the wavelength grid).
   Surfaces "we picked 304 Å because the event was a prominence" so the
   user understands the recommendation isn't random. */
.wl-suggest-caption {
  margin: 10px 0 6px;
  padding: 6px 10px;
  font-size: 0.78rem;
  color: var(--text-secondary, #d0c8d8);
  background: rgba(123,97,255,0.06);
  border-left: 3px solid var(--accent-corona, #ffb347);
  border-radius: var(--radius-sm, 8px);
}
.wl-suggest-caption.hidden { display: none; }
.wl-suggest-caption strong { color: var(--text-primary, #fff); font-weight: 600; }
.wl-card.is-suggested { box-shadow: 0 0 0 2px rgba(255,179,71,0.55) inset; }

/* HEK best-time badge — sits between the info-row and the wavelength grid.
   Visually parallel to .info-row (same padding / radius) but uses a
   warmer accent so it reads as a per-date suggestion rather than a
   static notice. The dot before the label gets a subtle pulse to draw
   the eye when the time auto-fills. */
.hek-event-info {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  padding: 8px 14px;
  background: rgba(255,179,71,0.08);
  border-radius: var(--radius-sm);
  border-left: 3px solid #ffb347;
  font-size: 0.82rem;
  color: var(--text-secondary);
  line-height: 1.35;
}
.hek-event-info.hidden { display: none; }
.hek-event-info.is-loading { opacity: 0.7; }
.hek-event-info.is-fallback { border-left-color: rgba(160,160,160,0.5); background: rgba(255,255,255,0.03); }
.hek-event-info strong { color: var(--text-primary); font-weight: 600; }
.hek-event-info .hek-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: #ffb347; flex: 0 0 7px;
}
.hek-event-info.is-fallback .hek-dot { background: rgba(160,160,160,0.6); }
@media (prefers-reduced-motion: no-preference) {
  .hek-event-info:not(.is-fallback) .hek-dot {
    animation: hekPulse 2.4s ease-in-out 1;
  }
}
@keyframes hekPulse {
  0%   { box-shadow: 0 0 0 0 rgba(255,179,71,0.45); }
  60%  { box-shadow: 0 0 0 8px rgba(255,179,71,0); }
  100% { box-shadow: 0 0 0 0 rgba(255,179,71,0); }
}

/* Crop overlay */
.crop-overlay {
  position: absolute;
  border: 2px dashed rgba(255,255,255,0.7);
  background: rgba(123,97,255,0.1);
  pointer-events: none;
  z-index: 5;
}
.crop-overlay.circular {
  border-radius: 50%;
}

/* Backend status banner */
.backend-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  border-radius: var(--radius-md);
  margin-bottom: 20px;
  animation: fadeInUp 0.5s ease-out;
  font-size: 0.9rem;
  flex-wrap: wrap;
}

.backend-banner.checking {
  background: rgba(56,189,248,0.08);
  border: 1px solid rgba(56,189,248,0.25);
  color: var(--accent-cool);
}

.backend-banner.online {
  background: rgba(61,220,132,0.08);
  border: 1px solid rgba(61,220,132,0.25);
  color: #3ddc84;
}

.backend-banner.offline {
  background: rgba(255,94,58,0.08);
  border: 1px solid rgba(255,94,58,0.3);
  color: var(--accent-flare);
}

.backend-banner.waking {
  background: rgba(247,168,37,0.08);
  border: 1px solid rgba(247,168,37,0.3);
  color: var(--accent-sun);
}

.backend-banner .banner-icon {
  font-size: 1.1rem;
  flex-shrink: 0;
}

.backend-banner .banner-text {
  flex: 1;
  min-width: 0;
}

.backend-banner .banner-text strong {
  display: block;
  margin-bottom: 2px;
}

.backend-banner .banner-text small {
  color: var(--text-secondary);
  font-size: 0.78rem;
}

.backend-banner .banner-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.backend-banner .banner-btn {
  padding: 6px 14px;
  border-radius: 6px;
  font-family: 'Outfit', sans-serif;
  font-size: 0.82rem;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--bg-elevated);
  color: var(--text-primary);
  transition: all var(--transition);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.backend-banner .banner-btn:hover {
  border-color: var(--accent-corona);
}

/* Footer */
.app-footer {
  text-align: center;
  padding: 30px 0 10px;
  color: var(--text-dim);
  font-size: 0.78rem;
}

.app-footer a { color: var(--accent-cool); text-decoration: none; }

/* Footer-legal nav (Privacy / Terms / Refunds / Shipping / Data credits)
   sits above the build-info row so legal links are the prominent footer
   element required for Shopify Payments / Stripe operation. */
.footer-legal-nav {
  margin-bottom: 8px;
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 4px 10px;
}
.footer-legal-nav a {
  color: var(--text-secondary, #9898b8);
  font-weight: 500;
  font-size: 0.82rem;
}
.footer-legal-nav a:hover {
  color: var(--text-primary, #eeeef6);
  text-decoration: underline;
}
.footer-legal-nav .footer-link-btn {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--text-secondary, #9898b8);
  font-weight: 500;
  font-size: 0.82rem;
  font-family: inherit;
}
.footer-legal-nav .footer-link-btn:hover {
  color: var(--text-primary, #eeeef6);
  text-decoration: underline;
}
.footer-meta {
  font-size: 0.74rem;
  color: var(--text-dim, #8e8eab);
}
/* Bumped to 0.72rem + opacity .82 — fix for footer-build-low-contrast
   WCAG finding (was 0.5 opacity). */
.footer-build-tag {
  font-family: 'JetBrains Mono', monospace;
  opacity: 0.82;
  font-size: 0.72rem;
}

/* ── Cookie consent banner (GDPR/ePrivacy minimum) ────────────────
   Shown on first visit OR after the user clears their localStorage.
   Sticky-bottom, two buttons (Accept / Decline), persists choice. */
.cookie-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  /* Lift above the feedback FAB cluster (which sits at bottom: 18px,
     height ~52px). 90px clears it on mobile so the banner doesn't
     cover the Request / Comment pills (persona-sweep finding). */
  bottom: calc(90px + env(safe-area-inset-bottom, 0px));
  /* Below FAB (9990) so a modal-open scroll-lock can still grey it. */
  z-index: 9989;
  background: rgba(15, 17, 30, 0.96);
  backdrop-filter: blur(8px);
  border: 1px solid var(--border, #2a2a44);
  border-radius: 14px;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
  color: var(--text-secondary, #9898b8);
  font-size: 0.85rem;
  line-height: 1.45;
}
@media (min-width: 720px) {
  .cookie-banner { bottom: 16px; }
}
.cookie-banner.hidden { display: none; }
.cookie-banner-inner {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px 18px;
  max-width: 920px;
  margin: 0 auto;
}
@media (min-width: 720px) {
  .cookie-banner-inner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
  }
}
.cookie-banner-body strong { color: var(--text-primary, #eeeef6); display: block; margin-bottom: 4px; font-size: 0.92rem; }
.cookie-banner-body p { margin: 0; }
.cookie-banner-body a { color: var(--accent-corona-text, #a48dff); }
.cookie-banner-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
  justify-content: flex-end;
}
.cookie-banner-btn {
  padding: 8px 14px;
  border-radius: 8px;
  border: 1px solid var(--border, #2a2a44);
  background: transparent;
  color: var(--text-secondary, #9898b8);
  font-weight: 600;
  font-size: 0.82rem;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}
.cookie-banner-btn:hover { background: rgba(123, 97, 255, 0.10); color: var(--text-primary, #eeeef6); border-color: var(--accent-corona, #7b61ff); }
.cookie-banner-btn-accept {
  background: var(--accent-corona, #7b61ff);
  color: #fff;
  border-color: var(--accent-corona, #7b61ff);
}
.cookie-banner-btn-accept:hover {
  background: #6a4eff;
  color: #fff;
}

/* Notification toast */
.toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%) translateY(80px);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 12px 22px;
  font-size: 0.9rem;
  box-shadow: 0 8px 30px rgba(0,0,0,0.3);
  z-index: 200;
  transition: transform 0.4s cubic-bezier(0.34,1.56,0.64,1);
  display: flex;
  align-items: center;
  gap: 10px;
}

.toast.show { transform: translateX(-50%) translateY(0); }
.toast.success { border-color: #3ddc84; }
.toast.error { border-color: var(--accent-flare); }

/* HQ button */
.btn-hq {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 12px;
  margin-top: 12px;
  background: linear-gradient(135deg, #3498db, var(--accent-corona));
  color: #fff;
  font-size: 0.95rem;
  font-weight: 600;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  font-family: 'Outfit', sans-serif;
}

.btn-hq:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 25px rgba(52,152,219,0.3); }
.btn-hq:disabled { opacity: 0.4; cursor: not-allowed; }

/* Crop ratio buttons */
.crop-ratios {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

.crop-ratio-btn {
  padding: 6px 12px;
  font-size: 0.78rem;
  font-family: 'JetBrains Mono', monospace;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-secondary);
  cursor: pointer;
  transition: all var(--transition);
}

.crop-ratio-btn:hover, .crop-ratio-btn.active {
  border-color: var(--accent-corona);
  color: var(--accent-corona);
}

.product-crop-btn {
  background: rgba(255,150,50,0.1);
  border-color: var(--accent-sun);
  color: var(--accent-sun);
  font-size: 0.72rem;
}

.product-crop-btn.active {
  background: rgba(255,150,50,0.2);
  border-color: var(--accent-corona);
  color: var(--accent-corona);
}

/* Text tool panel */
.text-tool-panel {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 14px;
  margin-top: 10px;
}

.text-tool-panel .field-row {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 10px;
  flex-wrap: wrap;
}

.text-tool-panel input[type="text"],
.text-tool-panel input[type="number"],
.text-tool-panel select {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 16px;
  font-family: 'Outfit', sans-serif;
  color: var(--text-primary);
  flex: 1;
  min-width: 0;
}

.text-tool-panel input[type="color"] {
  width: 36px;
  height: 36px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-card);
  cursor: pointer;
  padding: 2px;
  flex-shrink: 0;
}

.text-tool-panel .field-label-sm {
  font-size: 0.78rem;
  color: var(--text-dim);
  min-width: 50px;
  flex-shrink: 0;
}

.text-effect-row {
  border-top: 1px solid var(--border);
  padding-top: 8px;
  margin-top: 4px;
}

.toggle-label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.78rem;
  color: var(--text-secondary);
  cursor: pointer;
  min-width: 50px;
  white-space: nowrap;
}

.toggle-label input[type="checkbox"] {
  accent-color: var(--accent-corona);
  width: 16px;
  height: 16px;
}

/* Overlay / guide-line checkboxes row inside Geometry tab */
.guide-checks {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  flex-basis: 100%;
  width: 100%;
  padding: 4px 0 2px;
}

#textFontSelect optgroup {
  font-family: 'Outfit', sans-serif;
  font-weight: 600;
  color: var(--text-secondary);
  font-size: 0.82rem;
  padding: 4px 0;
}

.text-tool-hint {
  font-size: 0.78rem;
  color: var(--text-dim);
  margin-top: 6px;
  display: flex;
  align-items: center;
  gap: 6px;
}

.text-tool-hint i { color: var(--accent-cool); }

/* Dragging text cursor */
.text-dragging {
  cursor: move !important;
}

/* Catalog modal */
/* Catalog CSS removed — product creation handled on Printify's site */

/* Variant info panel on product cards */
.variant-panel {
  border-top: 1px solid var(--border);
  padding-top: 8px;
  margin-top: 6px;
  max-height: 200px;
  overflow-y: auto;
}

.variant-loading {
  font-size: 0.75rem;
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 0;
}

.variant-summary {
  font-size: 0.75rem;
}

.variant-count {
  display: inline-block;
  background: var(--accent-corona);
  color: #fff;
  font-size: 0.68rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 10px;
  margin-bottom: 6px;
}

.variant-group {
  margin-top: 4px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 3px;
}

.variant-group-label {
  font-weight: 600;
  color: var(--text-secondary);
  font-size: 0.7rem;
  margin-right: 2px;
}

.variant-tag {
  display: inline-block;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 6px;
  font-size: 0.68rem;
  color: var(--text-secondary);
  white-space: nowrap;
}

.variant-color {
  background: rgba(123,97,255,0.08);
  border-color: rgba(123,97,255,0.2);
}

.variant-more {
  font-size: 0.68rem;
  color: var(--text-dim);
  font-style: italic;
}

.variant-selected-msg {
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--accent-cool);
  margin-top: 4px;
  margin-bottom: 6px;
}

.variant-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 6px;
}

/* Variant row in the inline pane is read-only: label left, price middle,
   optional "✓ Selected" badge on the active row. Selection happens only
   through the "Pick a variant" modal — these rows just summarise what's
   available so users can see the catalog without committing. */
.variant-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 8px;
  font-size: 0.78rem;
  padding: 6px 8px;
  border-radius: 6px;
  border: 1px solid transparent;
  transition: background-color 0.15s ease, border-color 0.15s ease;
  color: var(--text-primary);
  background: var(--bg-deep);
}

.variant-list-readonly .variant-row { cursor: default; }
.variant-list-readonly .variant-row:hover { background: var(--bg-deep); }

.variant-row.confirmed {
  border-color: var(--accent-cool);
  background: rgba(123, 97, 255, 0.15);
  font-weight: 600;
}

.variant-row-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.variant-price {
  font-size: 0.74rem;
  color: var(--text-dim);
  font-weight: 500;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.variant-price-empty { min-width: 28px; }

/* Read-only "Selected" pill on the active variant row in the inline pane.
   Replaces the old per-row Select button now that selection lives in the
   modal — keeps the list informative without offering a hidden second
   commit path. */
.variant-row-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  font-size: 0.7rem;
  font-weight: 600;
  border-radius: 999px;
  background: var(--accent-cool);
  color: #fff;
  white-space: nowrap;
}
.variant-row-badge i { font-size: 0.62rem; }
/* Defensive: hide any leftover Select buttons from older renders that
   haven't been re-rendered yet. */
.variant-select-btn { display: none !important; }
/* Variant-list expander: collapse long t-shirt/garment lists so the inline
   pane doesn't become a wall of rows. Friction audit found the unfolded
   list dwarfed every other section. */
.variant-list-expander {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 8px 0 0;
  padding: 6px 14px;
  border: 1px dashed var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.variant-list-expander:hover {
  background: var(--bg-elevated);
  color: var(--text-primary);
  border-color: var(--text-dim);
}
.variant-list-rest-count {
  color: var(--text-dim);
  font-weight: 500;
  font-size: 0.7rem;
}
.variant-list-rest.hidden { display: none; }

.variant-clock-note {
  font-size: 0.7rem;
  color: var(--text-dim);
  margin: 6px 0 0;
  font-style: italic;
}

.variant-pick-hint {
  font-size: 0.72rem;
  color: var(--text-dim);
  margin: 8px 0 0;
  text-align: center;
}
.variant-pick-hint strong { color: var(--accent-cool); }

.mockup-status {
  font-size: 0.82rem;
  color: var(--text-secondary);
  margin-bottom: 10px;
  min-height: 0;
  transition: min-height 0.3s ease;
}

.mockup-status:not(:empty) {
  min-height: 24px;
}

.product-preview img.mockup-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

/* Buy button on product cards */
.product-buy-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  padding: 10px;
  margin-top: 8px;
  background: linear-gradient(135deg, var(--accent-sun), var(--accent-flare));
  color: #fff;
  font-size: 0.85rem;
  font-weight: 600;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: all var(--transition);
  font-family: 'Outfit', sans-serif;
}

.product-buy-btn:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 15px rgba(255,94,58,0.3);
}

.product-buy-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  background: var(--bg-elevated);
  color: var(--text-dim);
}

/* Buy / Download button — lives in the sticky editor action bar at
   the bottom of the editor frame. Same style whether it's in the
   preview pane (legacy) or the action bar; the action bar uses
   flex: 1 on its children so width: 100% reads as "fill the flex
   slot." Margin-top removed because the action bar handles its
   own padding and would otherwise misalign the two buttons. */
.btn-buy-in-editor {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 12px 20px;
  background: linear-gradient(135deg, var(--accent-sun), var(--accent-flare));
  color: #fff;
  font-size: 0.95rem;
  font-weight: 600;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  font-family: 'Outfit', sans-serif;
}

.btn-buy-in-editor:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 15px rgba(255, 94, 58, 0.3);
}

.btn-buy-in-editor.hidden {
  display: none;
}

/* Locked state: shown until a real Printify mockup has been generated, so
   the user can verify their preview matches what they'll get on Shopify. */
.btn-buy-in-editor:disabled,
.btn-buy-in-editor.buy-locked {
  background: #333344;
  color: rgba(255, 255, 255, 0.55);
  cursor: not-allowed;
  box-shadow: none;
  transform: none;
}
.btn-buy-in-editor:disabled:hover,
.btn-buy-in-editor.buy-locked:hover {
  transform: none;
  box-shadow: none;
}

/* Checkout progress overlay */
.checkout-progress {
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 20px;
  margin-top: 16px;
  text-align: center;
}

.checkout-progress .checkout-step {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  font-size: 0.88rem;
  color: var(--text-secondary);
}

.checkout-progress .checkout-step.active {
  color: var(--accent-cool);
  font-weight: 500;
}

.checkout-progress .checkout-step.done {
  color: #3ddc84;
}

.checkout-progress .checkout-step i {
  width: 18px;
  text-align: center;
}

.btn-shopify-checkout {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 28px;
  margin-top: 14px;
  background: linear-gradient(135deg, #96bf48, #5a8a1c);
  color: #fff;
  font-size: 1rem;
  font-weight: 700;
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  font-family: 'Outfit', sans-serif;
  text-decoration: none;
}

.btn-shopify-checkout:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 25px rgba(150,191,72,0.35);
}

/* ── Editor layout — CSS grid frame ────────────────────────────
   Four regions: canvas (top-left), preview (top-right, spans the
   canvas + toolbar rows on desktop), toolbar (middle-left), action
   bar (bottom, full-width). On mobile the grid collapses to one
   column: canvas → preview → toolbar → actions stacked vertically.
   Canvas + preview are sticky at the top so they stay visible
   while the toolbar scrolls; the action bar is sticky at the
   bottom so the Generate + Buy/Download buttons never scroll off
   the screen. */
/* Selected-product hero in the editor section header. Gilly: "Let the
   type of object to be printed be centered and at least h3 or h2 as
   appropriate, so it is much more clear which object was selected."
   Lives BELOW the small "3 · View & edit your image" badge row, sized
   like a real page title (1.7rem desktop, scales down on mobile). */
.editor-selected-product-name {
  margin: 6px auto 18px;
  text-align: center;
  font-size: 1.7rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text-primary, #eeeef6);
  /* Subtle sun-glow underline so the title reads as a marquee. */
  padding-bottom: 8px;
  border-bottom: 2px solid var(--accent-sun, #f7a825);
  max-width: max-content;
}
.editor-selected-product-name:empty { display: none; }
@media (max-width: 600px) {
  .editor-selected-product-name {
    font-size: 1.25rem;
    margin: 4px auto 12px;
    padding-bottom: 6px;
  }
}

.editor-with-preview {
  display: grid;
  grid-template-columns: 1.4fr minmax(260px, 1fr);
  grid-template-areas:
    "canvas  preview"
    "toolbar preview"
    "actions actions";
  gap: 20px 20px;
  align-items: start;
}
/* STRESS-002: at tablet widths below the rail breakpoint (1100 px) the
   two-column grid was squeezing the toolbar column down to ~150 px,
   below the toolbar's intrinsic content width — friction agent 4
   reported the entire toolbar disappeared with a phantom black gutter
   on the right at 768 px. Force the single-column stack at narrow
   widths so the toolbar always gets a full row of breathing room. */
@media (max-width: 1099px) {
  .editor-with-preview {
    grid-template-columns: 1fr;
    grid-template-areas:
      "canvas"
      "preview"
      "toolbar"
      "actions";
  }
}

.editor-with-preview .image-stage   { grid-area: canvas; }
.editor-with-preview .editor-main   { grid-area: toolbar; min-width: 0; }
.editor-with-preview .selected-product-preview { grid-area: preview; }
.editor-with-preview .editor-action-bar       { grid-area: actions; }

/* ── Single-preview mode ─────────────────────────────────────────
   The editor's working canvas (#imageStage / #solarCanvas) used to
   sit beside a smaller product-mockup preview, showing the source
   sun with overlays on the left and the actual product mockup on
   the right. Two canvases for the same content was confusing — the
   user just wants to see "what does my design look like ON the
   product I picked?". This mode hides the left/editor canvas and
   promotes the right/product-mockup preview into the canvas grid
   area, doubling its visible size. The hidden #solarCanvas stays
   in the DOM as the backing render target — drawProductMockup
   reads from it via getCleanCanvasSnapshot — so every slider /
   colour / crop control still updates the visible mockup via
   refreshLivePreview. Activated by .single-preview-mode on body
   (set by JS once on init; cleared by ?legacy-canvas=1 URL escape
   hatch for power users who still want the editor canvas visible). */
body.single-preview-mode .editor-with-preview .image-stage {
  display: none;
}
body.single-preview-mode .editor-with-preview {
  grid-template-columns: 1fr;
  grid-template-areas:
    "preview"
    "toolbar"
    "actions";
}
body.single-preview-mode .editor-with-preview .selected-product-preview {
  max-width: none;
  flex: 1 0 auto;
  /* Big square slot keyed off viewport so the preview reads as the
     dominant content surface, not a sidekick. Capped at 720 px so on
     very wide monitors it doesn't dwarf the toolbar. */
  width: 100%;
  max-height: min(72vh, 720px);
}
body.single-preview-mode .editor-with-preview .selected-product-preview .preview-mockup {
  /* Square aspect (matches the underlying canvas backing store),
     centered, fills available width. */
  aspect-ratio: 1 / 1;
  max-width: min(72vh, 720px);
  margin: 0 auto;
}
body.single-preview-mode .editor-with-preview .selected-product-preview canvas.live-preview-canvas {
  /* Display at full container width; backing-store resolution is
     bumped in updateSelectedProductPreview so this upscale doesn't
     pixellate. Grab cursor hints that the user can pan the source
     image by dragging on the mockup — the _mobilePan* handlers
     pick this gesture up and update state.panX/panY in REF space.
     Switches to grabbing on :active for the standard direct-
     manipulation feedback. */
  width: 100%;
  height: auto;
  max-width: 100%;
  cursor: grab;
}
body.single-preview-mode .editor-with-preview .selected-product-preview canvas.live-preview-canvas:active {
  cursor: grabbing;
}

/* Resize handle (bottom-right corner). Drag to shrink/grow the
   preview pane. Stores --user-preview-size on the pane element so
   the choice persists for the session. */
.preview-pane-resize-handle {
  position: absolute;
  right: 4px;
  bottom: 4px;
  width: 18px;
  height: 18px;
  cursor: nwse-resize;
  background:
    linear-gradient(135deg, transparent 0 35%, rgba(255,255,255,0.4) 35% 45%, transparent 45% 60%, rgba(255,255,255,0.4) 60% 70%, transparent 70%);
  z-index: 4;
  border-radius: 3px;
  opacity: 0.5;
  transition: opacity 0.15s ease;
}
.preview-pane-resize-handle:hover { opacity: 0.95; }

/* Pop-out toggle button (top-left, mirrors the close button). When
   active, the preview pane detaches into a draggable floating panel
   (position: fixed, body.preview-popped-out). */
.preview-popout-btn {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 3;
  background: rgba(0,0,0,0.35);
  color: var(--text-primary, #e9eaf5);
  border: 1px solid rgba(255,255,255,0.15);
  border-radius: 6px;
  width: 28px;
  height: 28px;
  cursor: pointer;
  font-size: 13px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.preview-popout-btn:hover { background: rgba(0,0,0,0.55); }

/* Popped-out floating mode — preview detaches from grid, hovers in
   the bottom-right corner, draggable by its chrome (not the canvas;
   the canvas is reserved for the source-image pan gesture). */
body.preview-popped-out .selected-product-preview {
  position: fixed !important;
  bottom: 24px;
  right: 24px;
  top: auto !important;
  left: auto !important;
  width: var(--popout-w, 360px);
  max-width: 90vw;
  max-height: 80vh;
  z-index: 9000;
  box-shadow: 0 12px 40px rgba(0,0,0,0.55);
  border-color: rgba(255,255,255,0.25);
  cursor: grab;
}
body.preview-popped-out .selected-product-preview:active { cursor: grabbing; }
body.preview-popped-out .selected-product-preview .preview-mockup {
  /* Constrain the floating-mode preview so the canvas doesn't push
     the whole panel beyond viewport. */
  max-width: 100%;
  max-height: 60vh;
}

/* X / close button repurposed (2026-05-25): used to deselect the
   product, which conflicted with the dedicated "Change product"
   CTA. Now it docks a popped-out preview back into the editor grid.
   In docked single-preview-mode there's nothing to "close" — hide
   it so the chrome isn't misleading. The popout button (top-left)
   is the way to re-detach when docked. */
body.single-preview-mode:not(.preview-popped-out) .selected-product-preview .preview-close-btn {
  display: none;
}

/* ── Two-column desktop layout: preview rail + dashboard ────────
   On wide-enough viewports (≥1100 px), when single-preview-mode
   is active and a product is selected, the preview pane leaves
   the editor's internal grid and becomes a fixed-position left
   rail that follows the user as they scroll. The rest of the page
   (vibe cards, wavelength picker, product grid, editor controls)
   shifts right and right-justifies inside its column. Gilly's
   ask 2026-05-25: "two columns, preview floats and follows as
   the user scrolls, dashboard right-justified."

   The toggle gate is `body.left-rail-preview`, set by JS on
   viewport-resize when:
     - body.single-preview-mode is on, AND
     - a product is selected (preview pane not .hidden), AND
     - body is NOT .preview-popped-out (floating mode owns its
       own positioning; rail layout would fight it), AND
     - window.innerWidth >= 1100.
   The popout button and X button update this state so toggling
   modes plays nice with the rail. */
body.left-rail-preview {
  /* Centered two-column composition. Both rail + dashboard get
     sized as a single block, then centered in the viewport via
     `--left-edge`. Matches the pattern Gilly likes from the
     Pixelated Enso layout: balanced columns with comfortable
     margins on both sides, not edge-pinned. Below the breakpoint
     (1100 px) this whole rule set is skipped and we fall back to
     the single-column docked layout. */
  --rail-w: clamp(340px, 40vw, 560px);
  --dashboard-w: clamp(420px, 44vw, 680px);
  --rail-gap: 32px;
  --total-w: calc(var(--rail-w) + var(--rail-gap) + var(--dashboard-w));
  /* Half the empty space outside the total block — applied as the
     left margin of the rail and the right margin of the dashboard
     so the whole composition reads as one centered group. */
  --left-edge: max(24px, calc((100vw - var(--total-w)) / 2));
}
body.left-rail-preview .app-container {
  /* Dashboard sits in the right half of the centered block. */
  margin-left: calc(var(--left-edge) + var(--rail-w) + var(--rail-gap));
  margin-right: var(--left-edge);
  max-width: var(--dashboard-w);
  padding-left: 0;
  padding-right: 0;
}
body.left-rail-preview .selected-product-preview {
  /* Fixed-position left rail, anchored to the same `--left-edge` so
     it shares a centered axis with the dashboard. Follows scroll
     automatically because position:fixed pins to viewport coords. */
  position: fixed !important;
  top: 24px;
  left: var(--left-edge);
  right: auto !important;
  bottom: auto !important;
  width: var(--rail-w);
  max-width: var(--rail-w);
  max-height: calc(100vh - 48px);
  overflow: auto;
  z-index: 20;
}
body.left-rail-preview .editor-with-preview {
  /* Preview hoisted to the rail; editor grid collapses to a
     single column (toolbar then actions). */
  grid-template-columns: 1fr;
  grid-template-areas:
    "toolbar"
    "actions";
}
body.left-rail-preview .editor-with-preview .selected-product-preview {
  /* Override the grid-area from the docked layout — the pane is
     no longer inside this grid visually, but it's still a DOM
     descendant so we have to neutralize the grid-area assignment. */
  grid-area: unset;
}
/* Override the rail mode's positioning when the user pops the
   preview out into floating mode. The popped-out CSS already
   pins to bottom-right; the .left-rail-preview rule above would
   otherwise pin it top-left and override the float. */
body.left-rail-preview.preview-popped-out .selected-product-preview {
  position: fixed !important;
  top: auto !important;
  left: auto !important;
  bottom: 24px;
  right: 24px;
  width: var(--popout-w, 360px);
  max-width: 90vw;
  max-height: 80vh;
}

/* Sticky editor canvas — see header comment for rationale. The cap
   on max-height keeps room visible for the toolbar to scroll behind
   it on shorter viewports. */
.editor-with-preview .image-stage {
  position: sticky;
  top: 20px;
  z-index: 5;             /* above the scrolling toolbar behind it */
  max-height: 45vh;       /* keep room for sliders below */
}

/* Sticky action bar — Generate real mockup + Buy/Download buttons.
   `position: sticky; bottom: 0` pins to the bottom of whatever
   scroll container holds it (the page in standalone, the embedded-
   iframe in Shopify after the parent listener resizes). The bar
   has its own dark backdrop so toolbar content scrolling underneath
   doesn't bleed through, plus a soft top fade so the transition
   reads as a docked footer rather than an abrupt cut. Hidden until
   a product is selected (mirrors .selected-product-preview). */
.editor-action-bar {
  position: sticky;
  bottom: 0;
  z-index: 6;
  display: flex;
  gap: 12px;
  padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(
    to top,
    var(--bg-card, #1a1a2e) 0%,
    var(--bg-card, #1a1a2e) 70%,
    rgba(26, 26, 46, 0.92) 100%
  );
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 -10px 28px rgba(0, 0, 0, 0.35);
}
.editor-action-bar.hidden { display: none; }
.editor-action-bar > button {
  flex: 1 1 0;
  min-width: 0;
}
/* Inline locked-state hint pointing at the mockup button. Lives in
   its own row directly beneath the action bar (NOT inside it — the
   previous flex-child layout collapsed the Generate/Download buttons
   to ~30% width). */
.editor-buy-lock-hint {
  margin: 8px 0 0;
  padding: 8px 12px;
  border-radius: 8px;
  background: rgba(255, 179, 71, 0.08);
  border: 1px solid rgba(255, 179, 71, 0.28);
  color: var(--accent-corona, #ffb347);
  font-size: 0.82rem;
  text-align: center;
  display: block;
}
.editor-buy-lock-hint i { margin-right: 4px; }
@media (max-width: 740px) {
  .editor-action-bar {
    flex-direction: column;
    gap: 8px;
  }
}

.editor-with-preview .selected-product-preview {
  flex: 1 0 260px;
  max-width: 400px;
  background: var(--bg-elevated);
  border: 2px solid var(--border);
  border-radius: var(--radius-md);
  padding: 14px;
  text-align: center;
  /* sticky on scroll; also acts as containing block for the absolute close button */
  position: sticky;
  top: 20px;
}

/* Once the user has scrolled past the editor row entirely, sticky
   releases the preview and it scrolls away with the page. We want
   the preview to keep tracking the viewport — JS adds .preview-
   pinned and inline left/width/top so the pane stays anchored in
   its column all the way to the footer. */
.editor-with-preview .selected-product-preview.preview-pinned {
  position: fixed;
  z-index: 10;
  /* left, top, and width are set inline by the scroll handler */
}

/* ✕ close / deselect button in the product preview panel */
.preview-close-btn {
  position: absolute;
  top: 8px;
  right: 10px;
  background: transparent;
  border: none;
  color: var(--text-dim);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 3px 5px;
  border-radius: var(--radius-sm);
  transition: color var(--transition), background var(--transition);
  z-index: 1;
}
.preview-close-btn:hover {
  color: var(--accent-flare);
  background: rgba(255,94,58,0.12);
}

/* Navigation row — three equal buttons (Change wavelength · product ·
   variant). Each takes ~1/3 of the row at the natural toolbar width so
   they read as a balanced triad rather than three different chip sizes. */
.edit-toolbar-nav-group {
  flex: 1 1 auto;
  margin-left: auto;
  display: flex;
  gap: 6px;
  min-width: 0; /* allow buttons to shrink below their content width */
}
.edit-toolbar-nav-group .edit-btn-nav {
  flex: 1 1 0;
  min-width: 0;
  justify-content: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Navigation-style edit-btn (Change Wavelength etc.) */
.edit-btn-nav {
  opacity: 0.7;
  font-size: 0.78rem;
}
.edit-btn-nav:hover { opacity: 1; }

.selected-product-preview .preview-product-name {
  font-weight: 600;
  font-size: 0.95rem;
  margin-bottom: 8px;
}

.selected-product-preview .preview-mockup {
  width: 100%;
  background: var(--bg-deep);
  border-radius: var(--radius-sm);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 10px;
}

/* Canvas is always square 260×260; product shape is drawn inside. img fills when real mockup is shown */
.selected-product-preview .preview-mockup canvas.live-preview-canvas {
  max-width: 100%;
  height: auto;
  display: block;
}
.selected-product-preview .preview-mockup canvas.live-preview-canvas.circular {
  border-radius: 50%;
}

.selected-product-preview .preview-mockup > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.selected-product-preview .preview-product-ratio {
  font-size: 0.78rem;
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
}

.selected-product-preview .preview-variant-wrap {
  margin-top: 6px;
}
.selected-product-preview .preview-variant-wrap label {
  display: block;
  font-size: 0.7rem;
  color: var(--text-dim);
  margin-bottom: 2px;
}
/* Make the variant select look like a real dropdown — beta tester didn't
   realize it was clickable. Adds a chevron, hover/focus affordances, and
   purple accent border to match the editor's interactive controls. */
.selected-product-preview .preview-variant-wrap select {
  width: 100%;
  padding: 8px 32px 8px 12px;
  font-size: 0.85rem;
  font-weight: 500;
  background-color: var(--bg-deep);
  /* Inline SVG chevron — no font-awesome dependency, scales cleanly. */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path fill='none' stroke='%237b61ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M1 1l5 5 5-5'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px 8px;
  border: 1.5px solid var(--accent-corona, #7b61ff);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  transition: background-color 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
}
.selected-product-preview .preview-variant-wrap select:hover {
  background-color: rgba(123, 97, 255, 0.08);
}
.selected-product-preview .preview-variant-wrap select:focus {
  outline: none;
  box-shadow: 0 0 0 2px rgba(123, 97, 255, 0.3);
}
.selected-product-preview .preview-variant-note {
  font-size: 0.7rem;
  color: var(--text-dim);
  margin: 4px 0 0;
  font-style: italic;
}

/* Dual-panel layout toggle (throw pillow + hardcover journal).
   Two radio chips that look like the variant select: small label
   on top, two side-by-side pill buttons under it. */
.selected-product-preview .preview-layout-wrap {
  margin-top: 6px;
}
.selected-product-preview .preview-layout-label {
  display: block;
  font-size: 0.7rem;
  color: var(--text-dim);
  margin-bottom: 2px;
}
.selected-product-preview .preview-layout-options {
  display: flex;
  gap: 6px;
}
.selected-product-preview .preview-layout-opt {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 6px 8px;
  font-size: 0.75rem;
  font-weight: 500;
  background-color: var(--bg-deep);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.selected-product-preview .preview-layout-opt:hover {
  background-color: rgba(123, 97, 255, 0.08);
}
.selected-product-preview .preview-layout-opt input[type="radio"] {
  /* Hide the native input; the label itself becomes the chip. */
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.selected-product-preview .preview-layout-opt:has(input:checked) {
  background-color: rgba(123, 97, 255, 0.18);
  border-color: var(--accent-corona, #7b61ff);
}

/* Generate-real-mockup button — styled the same way whether it
   lives in the preview pane (legacy) or the new sticky action
   bar at the bottom of the editor frame. The action bar handles
   sizing via flex: 1 on its children, so `width: 100%` here
   degrades gracefully to "full width" inside the preview pane
   and "fill flex slot" inside the action bar. */
.btn-preview-mockup {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  padding: 10px 14px;
  font-size: 0.86rem;
  font-weight: 600;
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  cursor: pointer;
  transition: all var(--transition);
}

.btn-preview-mockup:hover {
  background: var(--accent-sun);
  color: #fff;
  border-color: var(--accent-sun);
}

/* ── In-flight spinner state for "Generate real mockup" ─────────
   The button shows one of three faces at any time:
     default → rocket icon + "Generate real mockup"
     loading → spinner + "Generating mockup…"   (disabled, no hover)
     done    → rocket icon + "Reset to mock mockup"
   .btn-preview-mockup-spinner is hidden by default and only shown
   when the button carries the .is-loading class. The icon is
   hidden in the same state so the spinner takes its slot.
   The status-msg spinner ring shares this style — keeping it
   inline here avoids a forward dependency on .status-msg. */
.btn-preview-mockup-spinner {
  display: none;
  width: 14px;
  height: 14px;
  border: 2px solid var(--border);
  border-top-color: var(--accent-corona);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

.btn-preview-mockup.is-loading {
  pointer-events: none;
  opacity: 0.85;
  cursor: progress;
}

.btn-preview-mockup.is-loading .btn-preview-mockup-icon {
  display: none;
}

.btn-preview-mockup.is-loading .btn-preview-mockup-spinner {
  display: inline-block;
}

.btn-preview-mockup.is-loading:hover {
  /* don't flash the sun-accent hover while disabled */
  background: var(--bg-deep);
  color: var(--text-secondary);
  border-color: var(--border);
}

.selected-product-preview .preview-mockup img.preview-real-mockup {
  object-fit: contain;
  width: 100%;
  height: auto;
  display: block;
}

/* ── Mockup slideshow (preview pane) ── */
.mockup-slideshow {
  position: relative;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.slide-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.55);
  border: none;
  color: #fff;
  font-size: 1.5rem;
  line-height: 1;
  width: 28px;
  height: 48px;
  cursor: pointer;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.65;
  transition: opacity 0.15s, background 0.15s;
  z-index: 2;
  padding: 0;
  user-select: none;
}

.slide-nav:hover {
  opacity: 1;
  background: rgba(0, 0, 0, 0.82);
}

.slide-prev { left: 4px; }
.slide-next { right: 4px; }

.slide-counter {
  position: absolute;
  bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 0.68rem;
  padding: 2px 8px;
  border-radius: 10px;
  pointer-events: none;
  font-family: 'JetBrains Mono', monospace;
  white-space: nowrap;
}

/* Loading overlay for the big selected-product preview pane's mockup
   slideshow. Shown while the next/prev Printify mockup image is fetching
   from the CDN, hidden when img load/error fires. Sized larger than the
   product-card variant since this pane is the hero preview. Nav buttons
   remain clickable (z-index:3 above this layer's z-index:2). */
.mockup-slideshow .slide-loader {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 12, 22, 0.55);
  backdrop-filter: blur(1.5px);
  -webkit-backdrop-filter: blur(1.5px);
  z-index: 2;
  pointer-events: none;
  transition: opacity 140ms ease-out;
  border-radius: inherit;
}
.mockup-slideshow .slide-loader.hidden {
  opacity: 0;
}
.mockup-slideshow .slide-spinner {
  display: inline-block;
  width: 44px;
  height: 44px;
  border: 4px solid rgba(255, 255, 255, 0.22);
  border-top-color: rgba(255, 255, 255, 0.92);
  border-radius: 50%;
  animation: mockup-slide-spin 0.75s linear infinite;
}
@keyframes mockup-slide-spin {
  to { transform: rotate(360deg); }
}
.mockup-slideshow .slide-nav { z-index: 3; }

/* ── Mockup slideshow (product card grid) ── */
.card-slide-counter {
  position: absolute;
  top: 5px;
  right: 5px;
  background: rgba(0, 0, 0, 0.62);
  color: #fff;
  font-size: 0.6rem;
  padding: 1px 5px;
  border-radius: 8px;
  pointer-events: none;
  font-family: 'JetBrains Mono', monospace;
  z-index: 2;
}

.card-slide-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.52);
  border: none;
  color: #fff;
  font-size: 1.1rem;
  line-height: 1;
  width: 22px;
  height: 36px;
  cursor: pointer;
  border-radius: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 2;
  padding: 0;
  user-select: none;
}

.product-preview:hover .card-slide-nav {
  opacity: 0.72;
}

.card-slide-nav:hover {
  opacity: 1 !important;
  background: rgba(0, 0, 0, 0.82);
}

.card-slide-prev { left: 2px; }
.card-slide-next { right: 2px; }

/* Loading overlay shown while the next/prev mockup image is fetching from
   Printify's CDN. Covers only the preview square (the nav buttons sit at
   z-index 3 so they stay clickable above the overlay). Kept deliberately
   subtle — thin translucent gray, small centered spinner — so it reads as
   "fetching" not "the app is broken." */
.card-slide-loader {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 12, 22, 0.52);
  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);
  z-index: 2;
  pointer-events: none;
  transition: opacity 120ms ease-out;
}
.card-slide-loader.hidden {
  opacity: 0;
}
.card-slide-spinner {
  display: inline-block;
  width: 28px;
  height: 28px;
  border: 3px solid rgba(255, 255, 255, 0.22);
  border-top-color: rgba(255, 255, 255, 0.88);
  border-radius: 50%;
  animation: card-slide-spin 0.7s linear infinite;
}
@keyframes card-slide-spin {
  to { transform: rotate(360deg); }
}
/* Nav buttons must sit above the loader so users can re-click during load. */
.card-slide-nav { z-index: 3; }

@media (max-width: 740px) {
  /* Single-column grid: canvas → preview → toolbar → actions. The
     canvas + preview pair sticks to the top as a unit (canvas pinned
     at top:0, preview pinned just below it via top:<canvas-max-h>)
     so the user can adjust sliders below without losing sight of
     either visual reference. */
  .editor-with-preview {
    grid-template-columns: 1fr;
    grid-template-areas:
      "canvas"
      "preview"
      "toolbar"
      "actions";
  }
  .editor-with-preview .image-stage {
    position: sticky;
    top: 0;
    max-height: 30vh;
    z-index: 6;
  }
  .editor-with-preview .selected-product-preview {
    position: sticky;
    /* Sits directly below the sticky canvas so both stay onscreen
       together. Matches the canvas max-height above. */
    top: 30vh;
    z-index: 5;
    max-height: 35vh;
    overflow-y: auto;
  }
}

/* ── Touchscreen tap-target bumps ──────────────────────────────
   `(pointer: coarse)` matches touch-primary devices (phones,
   tablets, touch laptops) regardless of viewport width. WCAG and
   Apple HIG both call for ~44×44pt minimum tap targets; the default
   styling on chips and the modal close button is below that, which
   is fine for mouse-driven UI but cramped for thumbs. Desktop hover
   devices are left untouched so the layout doesn't get oversized
   on the editor's tight slider rows. */
@media (pointer: coarse) {
  /* Modal close (✕) goes from 32×32 to 44×44. */
  .feedback-close-btn,
  .confirm-modal-close,
  .preview-close-btn {
    width: 44px;
    height: 44px;
    font-size: 1.15rem;
  }
  /* Preset chips (Crop / Vig.Edge / Vig.Radius / Crop Edge / X·Y)
     get more vertical room. We don't push to a strict 44 because the
     Geometry tab packs four preset rows in a small space; 36px is
     the comfort floor that keeps the rows compact and still reaches
     a thumb. The `!important` overrides the `font-size:0.72rem !important`
     on the base class. */
  .preset-btn {
    padding: 8px 12px !important;
    min-height: 36px;
    font-size: 0.8rem !important;
  }
  /* FAB pills sit on a corner — taller padding helps thumb-reach. */
  .feedback-fab {
    padding: 14px 18px;
  }
  /* Feedback layout-toggle pills (Same both sides / Spans across). */
  .selected-product-preview .preview-layout-opt {
    padding: 10px 10px;
    font-size: 0.85rem;
  }
}

/* ───────────────────────────────────────────────────────────────
   Feedback widget — floating action button + modal. Sits above all
   other app chrome (z-index above the catalog modal) so users can
   reach it from any state of the flow.
   ─────────────────────────────────────────────────────────────── */
/* Two-pill FAB group — Request on the left, Comment on the right.
   Stays bottom-right via the group container; each pill is a normal
   inline-flex button so they read as siblings rather than a stack. */
.feedback-fab-group {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 9990;
  display: flex;
  align-items: center;
  gap: 8px;
}
.feedback-fab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  border-radius: 999px;
  border: 0;
  color: #fff;
  font-weight: 600;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.3);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.feedback-fab-request {
  /* Warmer gradient so the "request" CTA reads as the action-y one */
  background: linear-gradient(135deg, #ff8a3d, #c06bff);
}
.feedback-fab-comment {
  background: linear-gradient(135deg, var(--accent-corona, #7b61ff), #c06bff);
}
.feedback-fab:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 24px rgba(123, 97, 255, 0.45), 0 2px 6px rgba(0, 0, 0, 0.35);
}
.feedback-fab:active { transform: translateY(0); }
.feedback-fab i { font-size: 1rem; }

/* When the editor is engaged (rail mode active), the editor's sticky
   "Download your design" / "Create on Shopify" CTA bar sits in the
   bottom-right too. Friction audit found the feedback pills crash on
   top of it. Shrink the group to icon-only and shift it left of the
   CTA bar so both remain reachable. */
body.left-rail-preview .feedback-fab-group {
  right: 18px;
  bottom: 88px;  /* clear the ~64px CTA bar */
}
body.left-rail-preview .feedback-fab-label { display: none; }
body.left-rail-preview .feedback-fab { padding: 11px; }
/* STRESS-004: same treatment on step "product" — agent 1 reported the
   two pills covered the third-column product cards' title / price /
   "Pick a variant" button on the top product row at desktop, and
   overlapped card text on mobile. Shrink to icon-only + nudge up so
   they clear the grid's bottom-right cell. */
body.step-product .feedback-fab-group {
  right: 18px;
  bottom: 88px;
}
body.step-product .feedback-fab-label { display: none; }
body.step-product .feedback-fab { padding: 11px; }

@media (max-width: 600px) {
  /* On narrow screens, hide labels so the two pills stay icon-only and
     don't crowd each other — full label still appears in the title attr. */
  .feedback-fab-label { display: none; }
  .feedback-fab { padding: 12px; }
}

.feedback-modal {
  position: fixed;
  inset: 0;
  z-index: 9995;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  /* When iOS pushes the keyboard up, the visual viewport shrinks but
     `vh` keeps the old document height — the modal would stay full-
     viewport-tall and the panel could end up centered behind the
     keyboard. `dvh` (dynamic viewport height) tracks the visual
     viewport, so when the keyboard opens the modal collapses to the
     visible region and the centered panel reflows above it. Falls
     back to `vh` on older browsers (Safari < 15.4, Chrome < 108) —
     they keep the old behaviour, which is the status quo. */
  height: 100vh;
  height: 100dvh;
}
.feedback-modal.hidden { display: none; }
.feedback-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 10, 20, 0.72);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.feedback-modal-panel {
  position: relative;
  width: 100%;
  max-width: 520px;
  /* Same dvh fallback chain so the panel itself never exceeds the
     visible area when the keyboard is up. */
  max-height: calc(100vh - 40px);
  max-height: calc(100dvh - 40px);
  overflow-y: auto;
  background: #17192a;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  padding: 28px 24px 24px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6);
  /* The panel has a hardcoded dark background, so it can't follow
     the document-level `--text-*` variables — those flip to dark
     values in light system mode and the panel goes black-on-black.
     Redefine the variables scoped to the panel so every descendant
     using `var(--text-primary)` / `--text-secondary` / `--text-dim`
     resolves to a light value regardless of the user's OS theme. */
  --text-primary:   #e9eaf5;
  --text-secondary: #a8a8c8;
  --text-dim:       #8e8eab;
  --accent-corona-text: #a48dff;
  color: var(--text-primary);
}
.feedback-close-btn {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: transparent;
  color: var(--text-secondary, #9898b8);
  border: 0;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  transition: background 0.15s ease, color 0.15s ease;
}
.feedback-close-btn:hover {
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
}

.feedback-modal-title {
  margin: 0 0 4px;
  font-size: 1.15rem;
  font-weight: 700;
}
.feedback-modal-sub {
  margin: 0 0 18px;
  font-size: 0.82rem;
  color: var(--text-secondary, #9898b8);
}

.feedback-tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  margin-bottom: 18px;
}
.feedback-tab {
  flex: 1;
  padding: 10px 12px;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--text-secondary, #9898b8);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.feedback-tab:hover { color: var(--text-primary, #e9eaf5); }
.feedback-tab.active {
  color: #fff;
  border-bottom-color: var(--accent-corona, #7b61ff);
}
.feedback-tab i { margin-right: 6px; }

.feedback-tab-panel { display: block; }
.feedback-tab-panel.hidden { display: none; }

.feedback-field-label {
  display: block;
  margin: 10px 0 6px;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-secondary, #9898b8);
  letter-spacing: 0.02em;
}
.feedback-field-opt {
  font-weight: 400;
  color: var(--text-dim, #6a6a8a);
}

.feedback-modal textarea,
.feedback-modal input[type="text"],
.feedback-modal input[type="email"],
.feedback-modal select {
  width: 100%;
  box-sizing: border-box;
  background: #0f1120;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 8px;
  padding: 10px 12px;
  /* Same "black-on-black if system is in light mode" bug — the
     input has a hardcoded dark background, so hardcode the typed
     text colour to a light tone too. `!important` defends against
     Shopify storefront themes that might cascade `input { color:
     black }` over our embedded iframe. */
  color: #e9eaf5 !important;
  /* iOS Safari auto-zooms when any focused input is < 16px. The page
     was zooming in every time the feedback modal opened on mobile —
     visually jarring and made the input field jump out of view.
     16px is the minimum that keeps Safari at the natural zoom. */
  font-size: 16px;
  font-family: inherit;
  outline: none;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.feedback-modal textarea::placeholder,
.feedback-modal input::placeholder {
  color: #6f6f8c;
}
/* No drag-handle resize — JS auto-grows the textarea on input, which
   works on touch (where the resize grip is too small to grab) and on
   desktop (no manual sizing needed). overflow-y stays hidden so we
   don't flash a scrollbar mid-grow; the JS caps height at 60vh so
   the modal still scrolls past the textarea on overflow. */
.feedback-modal textarea {
  resize: none;
  min-height: 88px;
  overflow-y: hidden;
  line-height: 1.4;
}
.feedback-modal textarea:focus,
.feedback-modal input:focus,
.feedback-modal select:focus {
  border-color: var(--accent-corona, #7b61ff);
  background: #12152a;
}
.feedback-field-error {
  border-color: #ff5a5f !important;
  animation: feedback-shake 0.24s ease;
}
@keyframes feedback-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

.feedback-context-note {
  display: flex;
  gap: 8px;
  margin-top: 8px;
  padding: 8px 10px;
  background: rgba(123, 97, 255, 0.08);
  border-radius: 6px;
  font-size: 0.72rem;
  color: var(--text-secondary, #9898b8);
  line-height: 1.4;
}
.feedback-context-note i { color: var(--accent-corona, #7b61ff); margin-top: 2px; }

/* Include-canvas opt-out — checked by default. Looks like a regular
   checkbox row, sits above the submit button. Sized to match the
   field-label scale so it doesn't shout for attention. */
.feedback-include-canvas {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0 0;
  font-size: 0.78rem;
  color: var(--text-secondary, #9898b8);
  cursor: pointer;
  user-select: none;
}
.feedback-include-canvas input[type="checkbox"] {
  width: 14px;
  height: 14px;
  cursor: pointer;
  accent-color: var(--accent-corona, #7b61ff);
}
.feedback-include-canvas:hover { color: var(--text-primary, #e9eaf5); }

.feedback-submit-btn {
  width: 100%;
  margin-top: 14px;
  padding: 11px 16px;
  border: 0;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--accent-corona, #7b61ff), #c06bff);
  color: #fff;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.15s ease, transform 0.12s ease;
}
.feedback-submit-btn:hover:not(:disabled) { transform: translateY(-1px); }
.feedback-submit-btn:disabled {
  opacity: 0.6;
  cursor: wait;
}
.feedback-submit-btn i { margin-right: 6px; }
.feedback-submit-secondary {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.15);
}

.feedback-product-hint {
  margin-top: 10px;
  font-size: 0.78rem;
  color: var(--text-secondary, #9898b8);
}
.feedback-product-hint.hidden { display: none; }

/* Category chips — horizontal scroll on narrow modals so all categories
   stay reachable without wrapping into 4+ rows that crowd the search. */
.feedback-category-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0 4px;
}
.feedback-category-row.hidden { display: none; }
.feedback-category-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--text-secondary, #9898b8);
  font-size: 0.74rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.feedback-category-chip:hover {
  background: rgba(123, 97, 255, 0.08);
  color: var(--text-primary, #e9eaf5);
}
.feedback-category-chip.active {
  background: linear-gradient(135deg, rgba(123, 97, 255, 0.18), rgba(192, 107, 255, 0.18));
  border-color: rgba(123, 97, 255, 0.5);
  color: #fff;
}
.feedback-category-chip i { font-size: 0.78rem; }
.feedback-category-chip-count {
  font-weight: 500;
  color: var(--text-dim, #6a6a8a);
  font-size: 0.7rem;
}
.feedback-category-chip.active .feedback-category-chip-count { color: rgba(255, 255, 255, 0.7); }

.feedback-product-results {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 220px;
  overflow-y: auto;
}
.feedback-product-placeholder {
  padding: 10px 12px;
  font-size: 0.8rem;
  color: var(--text-dim, #6a6a8a);
  text-align: center;
}
.feedback-product-hit {
  position: relative; /* anchor for the hover-mockup popover */
  display: grid;
  grid-template-columns: 36px 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  background: #0f1120;
  border: 1px solid rgba(255, 255, 255, 0.05);
  border-radius: 6px;
  color: var(--text-primary, #e9eaf5);
  font-size: 0.85rem;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.feedback-product-hit:hover {
  background: #151832;
  border-color: rgba(123, 97, 255, 0.3);
}
.feedback-product-hit-thumb {
  width: 36px;
  height: 36px;
  border-radius: 6px;
  background: #08091a;
  object-fit: cover;
  flex: 0 0 auto;
}
.feedback-product-hit-thumb-empty {
  width: 36px;
  height: 36px;
  border-radius: 6px;
  background: #08091a;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim, #6a6a8a);
  font-size: 0.9rem;
}
.feedback-product-hit-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.feedback-product-hit-title {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.feedback-product-hit-brand {
  font-size: 0.72rem;
  color: var(--text-dim, #6a6a8a);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.feedback-product-hit-price {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--accent-corona, #7b61ff);
  white-space: nowrap;
}
.feedback-product-hit-price-empty {
  font-size: 0.7rem;
  color: var(--text-dim, #6a6a8a);
  white-space: nowrap;
  font-style: italic;
}
/* Hover popover — JS-driven (appended to <body>) so it floats above
   the search-results container without getting clipped by its
   overflow:auto. Anchored next to the hovered row by JS. */
.feedback-hit-popover {
  position: fixed;
  width: 200px;
  height: 200px;
  background: #08091a;
  border: 1px solid rgba(123, 97, 255, 0.3);
  border-radius: 8px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55);
  padding: 4px;
  pointer-events: none;
  z-index: 10000;
  opacity: 0;
  transform: scale(0.9);
  transition: opacity 0.12s ease, transform 0.12s ease;
}
.feedback-hit-popover.visible {
  opacity: 1;
  transform: scale(1);
}
.feedback-hit-popover img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 5px;
}

/* When a blueprint has a single print provider, we hide the dropdown
   and surface the picked provider as a small read-only line so the user
   still sees who'll fulfill the order without an extra interaction. */
.feedback-provider-row.hidden { display: none; }
.feedback-provider-auto {
  margin-top: 10px;
  padding: 8px 10px;
  background: rgba(123, 97, 255, 0.06);
  border: 1px dashed rgba(123, 97, 255, 0.18);
  border-radius: 6px;
  font-size: 0.78rem;
  color: var(--text-secondary, #9898b8);
}
.feedback-provider-auto.hidden { display: none; }
.feedback-provider-auto strong { color: var(--text-primary, #e9eaf5); font-weight: 600; }

/* Print-area shape picker on the chosen-blueprint pane. Two radios laid
   out side-by-side; the active option highlights so the choice reads
   clearly. */
.feedback-shape-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 12px 0 4px;
}
.feedback-shape-row .feedback-field-label { margin: 0 8px 0 0; }
.feedback-shape-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--text-secondary, #9898b8);
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.feedback-shape-opt input[type="radio"] { display: none; }
.feedback-shape-opt:hover { color: var(--text-primary, #e9eaf5); }
.feedback-shape-opt:has(input:checked) {
  background: linear-gradient(135deg, rgba(123, 97, 255, 0.18), rgba(192, 107, 255, 0.18));
  border-color: rgba(123, 97, 255, 0.5);
  color: #fff;
}
.feedback-shape-opt i { font-size: 0.72rem; opacity: 0.85; }

/* Big mockup on the chosen-blueprint pane — confirms which product was
   picked once the search list collapses. */
.feedback-chosen-mockup {
  width: 100%;
  max-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #08091a;
  border-radius: 8px;
  margin: 10px 0;
  padding: 8px;
  box-sizing: border-box;
}
.feedback-chosen-mockup img {
  max-width: 100%;
  max-height: 184px;
  object-fit: contain;
  border-radius: 6px;
}
.feedback-chosen-mockup-empty {
  color: var(--text-dim, #6a6a8a);
  font-size: 0.78rem;
  padding: 24px 0;
}

.feedback-product-chosen {
  margin-top: 14px;
  padding: 12px;
  background: rgba(123, 97, 255, 0.06);
  border: 1px solid rgba(123, 97, 255, 0.2);
  border-radius: 8px;
}
.feedback-product-chosen.hidden { display: none; }
.feedback-chosen-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.feedback-chosen-name {
  font-weight: 700;
  font-size: 0.95rem;
}
.feedback-chosen-brand {
  font-size: 0.72rem;
  color: var(--text-dim, #6a6a8a);
  margin-top: 2px;
}
.feedback-chosen-clear {
  background: transparent;
  border: 0;
  color: var(--text-secondary, #9898b8);
  cursor: pointer;
  font-size: 0.9rem;
  padding: 4px 6px;
  border-radius: 4px;
}
.feedback-chosen-clear:hover {
  background: rgba(255, 255, 255, 0.06);
  color: #fff;
}

.feedback-thanks {
  text-align: center;
  padding: 20px 10px;
}
.feedback-thanks i {
  font-size: 2.6rem;
  color: #3ddc84;
  margin-bottom: 10px;
}
.feedback-thanks h3 {
  margin: 0 0 6px;
  font-size: 1.1rem;
}
.feedback-thanks p {
  margin: 0 0 16px;
  font-size: 0.85rem;
  color: var(--text-secondary, #9898b8);
}

/* ───────────────────────────────────────────────────────────────
   Pre-editor confirmation modal — previews the product & variant
   after the user clicks "Select this product" and before the editor
   loads. Smaller than the feedback modal since it's a single action
   with a compact metadata readout.
   ─────────────────────────────────────────────────────────────── */
.confirm-modal {
  position: fixed;
  inset: 0;
  z-index: 9994;
  display: flex;
  /* flex-start instead of center: when the modal panel is taller than
     the viewport the centered layout pushed the panel header above the
     fold, making it look like the page "scrolled down to the variant
     picker" rather than overlaying. Anchoring to the top keeps the
     title + close button always visible. */
  align-items: flex-start;
  justify-content: center;
  padding: 24px 20px;
  /* Internal scroll: the panel + its own overflow handles tall content
     without pushing the page itself. */
  overflow-y: auto;
}
.confirm-modal.hidden { display: none; }

/* Colour chooser (merged-product, e.g. mug White/Black) ─────────── */
.color-chooser-modal {
  position: fixed;
  inset: 0;
  z-index: 9995;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.color-chooser-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(6, 7, 14, 0.66);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.color-chooser-panel {
  position: relative;
  width: 100%;
  max-width: 460px;
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  background: #17192a;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  padding: 24px 22px 22px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6);
  color: #e9eaf5;
}
.color-chooser-close {
  position: absolute;
  top: 10px; right: 12px;
  width: 30px; height: 30px;
  border-radius: 50%;
  background: transparent;
  color: #a8a8c8;
  border: 0; cursor: pointer;
}
.color-chooser-close:hover { background: rgba(255,255,255,0.08); color: #fff; }
.color-chooser-title { margin: 0 0 4px; font-size: 1.1rem; font-weight: 700; }
.color-chooser-sub { margin: 0 0 16px; font-size: 0.85rem; color: #a8a8c8; }
.color-chooser-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.color-chooser-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 12px;
  background: var(--bg-deep, #0a0a12);
  border: 2px solid var(--border, #2a2a44);
  border-radius: 12px;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.1s ease;
}
.color-chooser-tile:hover,
.color-chooser-tile:focus-visible { border-color: var(--border-active, #7b61ff); }
.color-chooser-tile:active { transform: translateY(1px); }
/* "You picked this" confirmation flash — ~220ms window between click
   and modal-close so the user sees their selection register. */
.color-chooser-tile.is-picking {
  border-color: var(--accent-corona, #7b61ff);
  box-shadow: 0 0 0 4px rgba(123, 97, 255, 0.35);
  transform: scale(0.98);
}
.color-chooser-tile.is-picking::after {
  content: "\f00c";  /* fa-check */
  font-family: "Font Awesome 6 Free", "Font Awesome 5 Free", sans-serif;
  font-weight: 900;
  position: absolute;
  top: 8px; right: 10px;
  width: 26px; height: 26px;
  border-radius: 50%;
  background: var(--accent-corona, #7b61ff);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.85rem;
  animation: tilePickPop 0.22s ease-out;
}
.color-chooser-tile { position: relative; }
@keyframes tilePickPop {
  from { transform: scale(0.6); opacity: 0; }
  to   { transform: scale(1);   opacity: 1; }
}
.color-chooser-preview {
  width: 120px; height: 120px;
  display: flex; align-items: center; justify-content: center;
}
.color-chooser-preview canvas { max-width: 100%; max-height: 100%; }
.color-chooser-label {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 0.95rem; font-weight: 600; color: #e9eaf5;
}
.color-chooser-swatch {
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.25);
  display: inline-block;
}
.confirm-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 10, 20, 0.72);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.confirm-modal-panel {
  position: relative;
  width: 100%;
  max-width: 460px;
  /* Cap to the VISIBLE viewport so a tall variant list (canvas
     stretched has 78+ variants) can't push the Continue button
     off-screen. `100vh` on iOS includes the area behind the URL
     bar, which sent the bottom of the panel below the fold with
     no way to scroll there — `100dvh` tracks the visual viewport.
     Fallback `100vh` keeps the previous behaviour on browsers
     without dvh (Safari < 15.4). */
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  /* Plain block layout instead of flex — flex containers with
     overflow-y: auto have iOS-Safari quirks where the scrollHeight
     gets miscalculated and the scrollbar never appears. Plain
     block + overflow-y: auto always works. */
  display: block;
  background: #17192a;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  padding: 26px 22px 20px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.6);
  /* Re-scope the text colour variables so descendants (variant
     tiles, summary, sub, note, size chips, swatches) all resolve
     to light values regardless of the user's OS theme. The panel
     has a hardcoded dark background; the document-level variables
     flip to dark in light system mode and renders the variant
     labels black-on-black — the report behind this fix. */
  --text-primary:   #e9eaf5;
  --text-secondary: #a8a8c8;
  --text-dim:       #8e8eab;
  --accent-corona-text: #a48dff;
  color: var(--text-primary);
  overflow-y: auto;
  /* iOS momentum scrolling — without this the user has to drag-
     scroll without flick inertia, which feels broken on touch. */
  -webkit-overflow-scrolling: touch;
}
.confirm-modal-close {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: transparent;
  color: var(--text-secondary, #9898b8);
  border: 0;
  cursor: pointer;
  font-size: 0.95rem;
  transition: background 0.15s ease, color 0.15s ease;
}
.confirm-modal-close:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
.confirm-modal-title {
  margin: 0 0 4px;
  font-size: 1.1rem;
  font-weight: 700;
}
.confirm-modal-sub {
  margin: 0 0 16px;
  font-size: 0.78rem;
  color: var(--text-secondary, #9898b8);
}
/* Mock-mockup preview inside the "Ready to customize?" confirm modal.
   Renders the product silhouette + user's current solar image so the
   variant pick commits with visual context, not just a text summary. */
.confirm-mockup {
  margin: 0 auto 14px;
  /* Sized by the canvas child (_renderMockup writes c.style.width/
     height per the variant's aspect ratio). Width/height auto means
     the container shrinks/expands with the canvas. Caps stop the box
     from ever exceeding the modal panel. */
  max-width: 240px;
  max-height: 240px;
  width: auto;
  height: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0f1120;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 10px;
  overflow: hidden;
}
.confirm-mockup.empty {
  display: none;
}
/* Photoreal Printify default mockup at the variant-modal preview slot
   when no source image is loaded yet — replaces the cute sun-icon
   placeholder per Gilly's "I liked it better when I could see the
   aspect ratio and how it was going to crop" feedback. */
.confirm-mockup-default {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 6px;
}
/* Fallback placeholder (no default mockup on disk for this product) —
   kept so the slot doesn't collapse to zero when the manifest is
   sparse. */
.confirm-mockup-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 18px;
  text-align: center;
}
.confirm-mockup-placeholder-icon {
  width: 56px; height: 56px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(255,179,71,0.20), rgba(255,179,71,0.04) 70%);
  border: 1px dashed rgba(255,179,71,0.35);
  display: flex; align-items: center; justify-content: center;
  font-size: 1.4rem;
  color: var(--accent-sun, #f7a825);
}
.confirm-mockup-placeholder-text {
  font-size: 0.82rem;
  color: var(--text-secondary, #9898b8);
  line-height: 1.35;
}
.confirm-mockup-placeholder-text span {
  display: block;
  margin-top: 4px;
  font-size: 0.72rem;
  color: var(--text-dim, #6a6a8a);
}
.confirm-mockup .confirm-mockup-canvas {
  width: 220px;
  height: 220px;
  image-rendering: -webkit-optimize-contrast;
}
.confirm-metadata {
  margin: 0 0 14px;
  padding: 12px 14px;
  background: #0f1120;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 8px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 12px;
}
.confirm-metadata dt {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-dim, #6a6a8a);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.confirm-metadata dd {
  margin: 0;
  font-size: 0.86rem;
  color: var(--text-primary, #e9eaf5);
  font-weight: 500;
}
.confirm-modal-note {
  margin: 0 0 14px;
  font-size: 0.75rem;
  color: var(--text-secondary, #9898b8);
  line-height: 1.4;
}
.confirm-modal-actions {
  display: flex;
  gap: 8px;
  /* Continue button sticks to the bottom of the scrolling panel so
     it's always reachable, no matter how tall the variant list is
     or how short the viewport is. The mid-modal variant picker has
     its own 240-px scroll on desktop — without this sticky, wheel
     events over the picker scroll only the picker, and users hit
     the picker's bottom thinking they've reached the end of the
     modal. With sticky, Continue is pinned at the panel's bottom
     edge regardless of scroll position. Mobile gets the same
     guarantee for the same reason. */
  position: sticky;
  bottom: 0;
  z-index: 1;
  /* Solid background + a real upward fade so list content scrolling
     under the sticky CTA visibly dissolves into the button row
     instead of half-tiles being chopped off. Beta-tester crew
     filed: on mobile the previous 12-px box-shadow wasn't enough,
     so "16.0\" tall" text leaked out from underneath the button. */
  background: #17192a;
  padding-top: 14px;
  margin-top: 4px;
}
.confirm-modal-actions::before {
  /* Gradient fade above the sticky actions row. Transparent at top
     → solid panel background at bottom, sitting just above the
     button so content scrolling under it dissolves smoothly. */
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 28px;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(23, 25, 42, 0) 0%, #17192a 100%);
}
.confirm-modal-btn {
  flex: 1;
  padding: 10px 14px;
  border: 0;
  border-radius: 8px;
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.12s ease, opacity 0.15s ease;
}
.confirm-modal-btn-primary {
  background: linear-gradient(135deg, var(--accent-corona, #7b61ff), #c06bff);
  color: #fff;
}
.confirm-modal-btn-primary:hover { transform: translateY(-1px); }
.confirm-modal-btn-secondary {
  background: transparent;
  color: var(--text-primary, #e9eaf5);
  border: 1px solid rgba(255, 255, 255, 0.15);
}
.confirm-modal-btn-secondary:hover { background: rgba(255, 255, 255, 0.06); }

/* ───────────────────────────────────────────────────────────────
   Variant picker inside the confirm modal (single-step selection).
   Replaces the older inline collapsible pane on product cards as the
   primary flow — testers found two-step "expand → row-Select → confirm"
   too cluttered.
   ─────────────────────────────────────────────────────────────── */
.confirm-variant-summary {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 0 0 12px;
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.04);
  border-radius: 8px;
  font-size: 0.82rem;
  color: var(--text-secondary, #9898b8);
}
.confirm-variant-summary strong { color: var(--text-primary, #e9eaf5); font-weight: 600; }
.confirm-variant-summary .confirm-summary-dot { opacity: 0.5; }
.confirm-variant-summary .confirm-summary-price {
  /* Brightened purple variant — see `--accent-corona-text` in :root.
     The original #7b61ff measured ~3.7:1 against #1a1a2e (sub-AA);
     `--accent-corona-text` lands at ~6:1. */
  color: var(--accent-corona-text, #a48dff);
  font-weight: 600;
}

/* Colour-swatch row in the variant picker. Each swatch is a tiny
   coloured square with a hover state and a thicker outline when the
   currently-active variant matches that colour. Hidden via .hidden
   when the product's variants don't carry resolvable colours. */
/* Label naming what the swatch row affects (e.g. "Frame color: White").
   `cursor:help` + the JS-set title surface the "physical product, not
   the image" clarification on hover. */
.confirm-axis-label {
  margin: 6px 2px 2px;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-secondary, #a8a8c8);
  cursor: help;
}
.confirm-axis-label.hidden { display: none; }
.confirm-color-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 4px 0 12px;
  padding: 0 2px;
}
.confirm-color-swatches.hidden { display: none; }
.confirm-color-swatch {
  width: 26px;
  height: 26px;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  cursor: pointer;
  padding: 0;
  background-clip: padding-box;
  transition: transform 0.08s ease, box-shadow 0.12s ease, border-color 0.12s ease;
  position: relative;
}
.confirm-color-swatch:hover {
  transform: scale(1.08);
  border-color: rgba(255, 255, 255, 0.45);
}
.confirm-color-swatch.active {
  border-color: var(--accent-corona, #b692ff);
  box-shadow: 0 0 0 2px var(--accent-corona, #b692ff), 0 0 0 4px rgba(123, 97, 255, 0.25);
}
/* When the resolved hex is very dark, ring the swatch with a subtle
   light line so it doesn't disappear into the modal background. */
.confirm-color-swatch[data-tone="dark"] {
  border-color: rgba(255, 255, 255, 0.35);
}

/* Size chips — same row pattern as the colour swatches, but textual.
   Each chip carries one size value (S/M/L/XL/2XL/...); the active
   chip matches the currently-selected variant's size. */
.confirm-size-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 12px;
  padding: 0 2px;
}
.confirm-size-chips.hidden { display: none; }
.confirm-size-chip {
  min-width: 34px;
  height: auto;
  min-height: 36px;
  padding: 4px 10px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: var(--text-secondary, #9898b8);
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
  /* STRESS-016: chip now hosts label + price stacked. */
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  line-height: 1.05;
}
.confirm-size-chip-price {
  font-size: 0.66rem;
  font-weight: 500;
  opacity: 0.78;
  letter-spacing: 0.01em;
}
.confirm-size-chip.active .confirm-size-chip-price { opacity: 0.9; }
.confirm-size-chip:hover {
  color: var(--text-primary, #e9eaf5);
  background: rgba(123, 97, 255, 0.12);
}
.confirm-size-chip.active {
  background: linear-gradient(135deg, rgba(123, 97, 255, 0.35), rgba(192, 107, 255, 0.35));
  border-color: rgba(123, 97, 255, 0.6);
  color: #fff;
}
/* Size chips that are unavailable in the currently-selected color
   appear dimmed — keeps the layout consistent but flags that picking
   it will lose color match. */
.confirm-size-chip[data-unavailable="true"] {
  opacity: 0.35;
}

.confirm-variant-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 8px;
  max-height: 240px;
  overflow-y: auto;
  margin: 0 0 14px;
  padding: 4px 2px;
  /* Subtle inset so the scroll area reads as its own region. */
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.18);
  border: 1px solid rgba(255, 255, 255, 0.06);
  padding: 8px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
/* On narrow screens (mobile portrait) the nested scroll inside the
   modal competes with the modal's own scroll — the variant picker
   shows ~3 rows in its 240-px window and the user reasonably
   assumes that's the whole list, never realising the modal itself
   scrolls past it to the Continue button below. Drop the inner cap
   so the variant list grows naturally and the WHOLE modal scrolls
   as one continuous flow. */
@media (max-width: 600px) {
  .confirm-variant-picker {
    max-height: none;
    overflow-y: visible;
  }
}
.confirm-variant-picker::-webkit-scrollbar { width: 8px; }
.confirm-variant-picker::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.18);
  border-radius: 4px;
}

.confirm-variant-tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 4px;
  padding: 10px 12px;
  border: 1.5px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  background: rgba(20, 22, 36, 0.6);
  color: var(--text-primary, #e9eaf5);
  cursor: pointer;
  text-align: left;
  font-family: inherit;
  font-size: 0.84rem;
  line-height: 1.25;
  transition: border-color 0.12s ease, background 0.12s ease, transform 0.08s ease;
}
.confirm-variant-tile:hover {
  border-color: rgba(123, 97, 255, 0.55);
  background: rgba(40, 30, 70, 0.55);
}
.confirm-variant-tile.active {
  border-color: var(--accent-corona, #7b61ff);
  background: rgba(123, 97, 255, 0.16);
  box-shadow: 0 0 0 1px var(--accent-corona, #7b61ff) inset;
}
.confirm-variant-tile:active { transform: scale(0.98); }
.confirm-variant-tile-label {
  font-weight: 600;
  /* Allow long labels to wrap to a second line rather than truncate — beta
     reported truncated labels (Athletic Heather / XL Long T → "Athletic He…")
     hid critical information. The tile grows in height; the grid flexes. */
  white-space: normal;
  overflow-wrap: anywhere;
}
.confirm-variant-tile-price {
  font-size: 0.74rem;
  color: var(--text-secondary, #9898b8);
  font-family: 'JetBrains Mono', monospace;
}
.confirm-variant-tile.active .confirm-variant-tile-price { color: var(--accent-corona, #7b61ff); }
.confirm-variant-tile-dims {
  font-size: 0.7rem;
  color: var(--text-dim, #6b6b85);
  font-family: 'JetBrains Mono', monospace;
  letter-spacing: 0.02em;
}

.confirm-variant-loading,
.confirm-variant-empty {
  grid-column: 1 / -1;
  padding: 16px;
  text-align: center;
  color: var(--text-secondary, #9898b8);
  font-size: 0.85rem;
}

/* ───────────────────────────────────────────────────────────────
   Session-only "Your Requests" section — cards the user submitted
   via the feedback modal, visible immediately in their session
   before admin approval.
   ─────────────────────────────────────────────────────────────── */
.user-requests-section {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px dashed rgba(255, 255, 255, 0.08);
}
.user-requests-section.hidden { display: none; }
.user-requests-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.user-requests-title {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text-primary, #e9eaf5);
}
.user-requests-sub {
  margin: 0 0 10px;
  font-size: 0.75rem;
  color: var(--text-secondary, #9898b8);
}
.product-card.product-card-requested {
  border-color: rgba(123, 97, 255, 0.3);
}
.product-card-requested .product-name::before {
  content: "Requested";
  display: inline-block;
  margin-right: 6px;
  padding: 1px 6px;
  background: rgba(123, 97, 255, 0.15);
  border: 1px solid rgba(123, 97, 255, 0.35);
  border-radius: 4px;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #c0afff;
  vertical-align: middle;
}

/* ===============================================================
   MOBILE-ONLY OVERRIDES  (≤ 749 px)
   ---------------------------------------------------------------
   Single place to tune the phone experience. Every rule here is
   scoped inside `@media (max-width: 749px)` so desktop is never
   touched by changes in this block. Audit context: real-device
   inspection at iPhone 14 viewport (390 × 844) on 2026-05-22.
   Keep this block as the LAST thing in the file so it always wins.
   =============================================================== */
@media (max-width: 749px) {

  /* ── F1 · Wavelength thumbnail clip fix ────────────────────────
     `.wl-thumb` is a fixed 120 × 120 px disc. At 390 px viewport the
     `.wavelength-grid` drops to 3 columns and each `.wl-card` is
     only ~96 px wide with `overflow:hidden` and 10 px side padding —
     so the right ~36 px of every sun preview was being clipped.
     Make the thumb fluid: fill the card's content box but cap at
     the original 120 so wider phones don't blow it up. */
  .wl-thumb {
    width: 100%;
    max-width: 120px;
    height: auto;
    aspect-ratio: 1;
  }

  /* ── F2 · Product grid: 2 columns on phones ────────────────────
     Default grid is `repeat(auto-fill, minmax(240px, 1fr))` which
     collapses to ONE column at 390 px → 24 cards × ~450 px = ~11k
     px of scroll. Force a 2-up so the chooser is half as tall.
     Tighter gap because each card is now ~167 px wide. */
  .product-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }
  /* Product-card font tweaks for the narrower 2-col cell. */
  .product-name { font-size: 0.92rem; }
  .product-desc { font-size: 0.72rem; }
  .product-price { font-size: 0.85rem; }
  .product-info { padding: 10px; }

  /* ── F3 · Product preview wastes pixels (square aspect) ────────
     `.product-preview { aspect-ratio: 1 }` reserves a full square
     even though the rendered canvas is `max-width/height: 70%` so
     ~30% of the box is empty. Use 4:3 on mobile — saves ~25% of
     each card's preview height (×24 cards = a lot of scroll). */
  .product-preview { aspect-ratio: 4 / 3; }
  .product-preview canvas { max-width: 80%; max-height: 80%; }
  /* Shrink the corner icon-badge and stats-badge proportionally so
     they don't dominate the smaller preview area. */
  .product-preview .product-icon-badge {
    width: 32px; height: 32px;
    font-size: 16px;
    border-radius: 8px;
    top: 6px; left: 6px;
  }
  .product-preview .product-stats-badge {
    font-size: 9px;
    padding: 1px 5px;
    bottom: 6px; right: 6px;
  }

  /* ── F4 · Touch-target heights (Apple HIG 44 px) ───────────────
     Audited under-44 buttons: `.product-buy-btn` (37 px) and the
     birthday CTA `.birthday-cta-btn` (39 px). Bump both to 44 by
     widening vertical padding, keeping the text size where it is. */
  .product-buy-btn { min-height: 44px; padding: 12px 10px; }
  /* Hide the arrow icon on narrow viewports so the "Pick a variant"
     label has the full button width to lay out on one line. Beta-
     tester crew filed: 2-column grid + arrow + 12 chars wrapped to
     two lines ("Pick a / variant"), reducing predictability of the
     tap target. Label-only on phone is readable + tappable. */
  .product-buy-btn .fa-arrow-right,
  .product-buy-btn .fas.fa-arrow-right { display: none; }
  .product-buy-btn { white-space: nowrap; }
  .birthday-cta-btn { min-height: 44px; padding: 12px 16px; }

  /* ── F5 · Section headers larger on mobile ─────────────────────
     `.section-title` and `.birthday-cta-title` were 18.4 / 18.88 px
     — fine on desktop, undersized for a primary CTA on a phone. */
  .section-title { font-size: 1.25rem; }
  .birthday-cta-title { font-size: 1.3rem; }

  /* ── F6 · Wavelength labels readable on phone ──────────────────
     Default `.wl-label` is 0.72 rem (~11 px). Bump for legibility;
     the descriptor still wraps to 2 lines but is now scannable. */
  .wl-label {
    font-size: 0.78rem;
    line-height: 1.25;
    /* Friction audit: at ~390 px the second word ("Chromosphere",
       "Photosphere") was being hard-truncated mid-word ("Chromosphe")
       because .wl-card has overflow:hidden and a fixed height. Allow
       hyphenated soft-wraps + give the card room to breathe. */
    overflow-wrap: anywhere;
    hyphens: auto;
  }
  .wl-value { font-size: 1rem; }
  .wl-card {
    /* Let the wrapped label fit without clipping. The ::before tinted
       bar at the top still clips since it's anchored to the card edge
       (overflow:hidden on its own pseudo). */
    overflow: visible;
    min-height: 120px;
  }

  /* ── F7 · Beta banner: collapse to a slim chip on phone ────────
     The full sentence eats 52 px above the picker. The H1 already
     carries a "BETA" badge, so on mobile we keep the banner only
     as an iconified pill — tap-target preserved, vertical cost
     halved. Pair with JS that lets the user dismiss; CSS handles
     the size shrink unconditionally. */
  body.beta-mode-active .beta-banner {
    font-size: 0.72rem;
    padding: 4px 10px;
    margin-top: 8px;
  }

  /* ── F8 · Hero credit + hero padding ───────────────────────────
     The "Courtesy of NASA/SDO…" credit + tagline collectively push
     the picker below the fold. Trim hero vertical padding so the
     date input lands sooner. */
  .hero { padding-top: 14px; padding-bottom: 8px; }
  .hero-credit { font-size: 0.7rem; line-height: 1.3; }
  /* STRESS-005: friction agent 1 measured firstCard.top = 380 px on
     a 667 px-tall mobile viewport — hero alone consumed >55 % of
     the vertical space, so only a sliver of the first product card
     showed above the fold. Defeat the point of product-first.
     Mobile-only treatment:
       * Shrink H1 + tagline, drop the "Solar Archive" line height
       * Move the SDO acknowledgement to a smaller second-line
       * Hide the beta banner here (#betaBanner) — its content is
         duplicated in the global backend banner and the H1's BETA
         badge already, so the third repetition is pure occupy. */
  .hero h1 { font-size: 1.4rem; line-height: 1.1; margin-bottom: 4px; }
  .hero p { font-size: 0.78rem; line-height: 1.25; margin-bottom: 6px; }
  .hero-credit { font-size: 0.62rem; opacity: 0.7; margin-top: 2px; }
  .hero-icon { width: 36px; height: 36px; margin-bottom: 6px; }
  body.step-product .beta-banner { display: none; }
  /* Product section: trim header padding so the first card lands
     above the fold on a 667 px-tall viewport. */
  body.step-product #productSection .section-header { padding-bottom: 4px; }
  body.step-product #productSectionIntro { font-size: 0.72rem; margin-bottom: 6px; }

  /* ── F9 · Confirm-modal & feedback area form fields ────────────
     iOS Safari auto-zooms any <input> with font-size < 16 px on
     focus, then never zooms back — disorienting. Force ≥16 px on
     all text inputs so the OS leaves the viewport alone. */
  input[type="text"],
  input[type="email"],
  input[type="number"],
  input[type="search"],
  input[type="date"],
  input[type="time"],
  select,
  textarea {
    font-size: 16px;
  }

  /* ── F10 · Editor: drop sticky on the image-stage only ─────────
     The existing rule at line ~3106 keeps `.image-stage` sticky at
     top:0 AND `.selected-product-preview` sticky at top:30vh — on a
     390 × 844 viewport that's 65 vh of pinned chrome (≈ 549 px),
     leaving ~295 px for the toolbar/sliders. Since F15 hides the
     image-stage entirely on mobile, only the preview pane needs to
     stick — and F16 (below) handles that with a single sticky-at-
     top pinning the canvas while the toolbar scrolls underneath.
     The !important is needed to beat the more-specific
     `.editor-with-preview .image-stage` selectors above. */
  .editor-with-preview .image-stage {
    position: static !important;
    top: auto !important;
    max-height: none !important;
    overflow: visible !important;
  }

  /* ── F12 · Toolbar overflow (the showstopper) ──────────────────
     `.edit-toolbar-row` flex-wraps, but its children
     (`.filter-timeline.editor-filter`, `.edit-toolbar-nav-group`)
     refuse to shrink below their content's natural width — each
     reports 446 px inside a 308 px column, bleeding ~138 px off the
     right edge of the viewport (what looks like a phantom second
     column with "RHE…C" peeking in the screenshot). Force each row
     child to take the full row and shrink to fit, and let their own
     inner items wrap. */
  .editor-with-preview .editor-main,
  .editor-with-preview .edit-toolbar,
  .editor-with-preview .edit-toolbar-row {
    min-width: 0;
    max-width: 100%;
  }
  .edit-toolbar-row > .filter-timeline,
  .edit-toolbar-row > .edit-toolbar-nav-group {
    flex: 1 1 100%;
    min-width: 0;
    max-width: 100%;
    width: 100%;
  }
  /* The "Change wavelength / product / variant" trio: stack vertical
     if they still won't fit in a single row at narrow widths. They
     already have `flex: 1 1 0; min-width: 0`, so wrapping is enough. */
  .edit-toolbar-nav-group { flex-wrap: wrap; }
  .edit-toolbar-nav-group .edit-btn-nav { flex: 1 1 auto; }

  /* Final safety net: clip anything inside the editor that somehow
     still escapes the column — keeps the visible viewport tidy even
     if a new toolbar widget lands with the same min-width pattern. */
  .editor-with-preview { overflow-x: clip; min-width: 0; }

  /* ── F13 · Canvas/preview reasonable mobile heights ────────────
     Without sticky pinning we don't need vh caps; instead cap the
     intrinsic heights so the canvas + preview don't dominate the
     scroll. */
  .editor-with-preview .image-stage {
    max-height: 50vh;
    margin-inline: auto;
  }
  .editor-with-preview .selected-product-preview {
    /* drop the absolute-positioned close button anchor weirdness */
    width: 100%;
    max-width: 100%;
  }

  /* ── F14 · Edit tab bar: wrap so "Reset" doesn't overflow ──────
     Tabs (Geometry / Adjust / Tools) + the right-aligned Reset
     button total ~310 px in a 308 px column → 14 px clip on the
     right. Letting the bar wrap pushes Reset to its own row,
     where `margin-left: auto` still right-aligns it cleanly. */
  .edit-tab-bar { flex-wrap: wrap; row-gap: 4px; }

  /* ── F15 · Single-canvas mobile editor ─────────────────────────
     Gilly + beta feedback (2026-05-22): seeing TWO canvases on a
     phone (raw image-stage on top + mockup preview below) is
     redundant — the mockup IS the WYSIWYG view of what you're
     buying. Hide image-stage on mobile and promote the live mockup
     preview as the primary visual. Edits cascade automatically:
     renderCanvas() (the editor's source of truth) calls
     refreshLivePreview() at the end of every redraw, so cropping /
     wavelength / colour / sliders all update the preview live.
     The "Generate real mockup" button already toggles back to the
     live canvas when re-pressed, so the back-out flow is built in. */
  .editor-with-preview .image-stage,
  .editor-with-preview .image-stage-placeholder {
    display: none !important;
  }
  /* Hide the editor canvas's resize handle (it lives inside
     image-stage but renders absolutely outside on some browsers). */
  .editor-resize-handle { display: none !important; }

  /* Promote the preview to hero size. Canvas is drawn at 260×260
     pixels but stretches up cleanly via CSS — the underlying art is
     vector-style shapes + a bitmap sun overlay, so a moderate scale
     reads sharp enough on a Retina phone. (For maximum sharpness
     also bump the JS canvas pixel size — see solar-archive.js
     `pw = ph = 260` → 360 on mobile.) */
  .selected-product-preview .preview-mockup {
    max-width: 340px;
    margin: 4px auto 12px;
  }
  .selected-product-preview .preview-mockup canvas.live-preview-canvas,
  .selected-product-preview .preview-mockup img.preview-real-mockup {
    width: 100%;
    height: auto;
    max-width: 340px;
  }
  /* The product preview pane is now the visual centerpiece; give it
     a bit more breathing room and bolder typography for the name. */
  .selected-product-preview { padding: 16px; }
  .selected-product-preview .preview-product-name {
    font-size: 1.1rem;
    margin-bottom: 6px;
  }
  /* The "Generate real mockup" / "Reset to mock mockup" toggle —
     rename via CSS pseudo-content. The default label text is
     wrapped in `.btn-preview-mockup-label`; we hide it and inject a
     mobile-clearer string. (JS still updates the inner label on
     state change, so we keep both in sync by re-asserting both.) */

  /* ── F16 · Sticky mobile preview pane + drag-to-resize ─────────
     Gilly + beta (2026-05-22, round 3): with image-stage hidden
     (F15) the mockup preview IS the canvas — let it float pinned
     at the top of the viewport so edits made in the toolbar below
     are visible in real time without scrolling back up. Drag the
     bottom-right corner grip to scale the canvas down (free up
     viewport for sliders) or back up (see detail). Persisted in
     localStorage so the choice survives page loads.
     Implementation: pane goes position:sticky with a solid
     background so toolbar contents scrolling underneath don't
     bleed through. The canvas itself is scaled via the
     --preview-scale CSS var, written by JS setupMobilePreviewResize. */
  .editor-with-preview .selected-product-preview {
    position: sticky;
    top: 0;
    z-index: 6;
    background: var(--bg-elevated, #1a1a2e);
    /* Hard cap on the pane's height so the toolbar isn't pushed
       offscreen even when the user maxes the canvas. */
    max-height: 70vh;
    /* Reviewer M6: dropped overflow-y:auto here — a sticky child with
       its own scroll container intercepted touch deltas when users
       thumb-dragged the toolbar below, leaving the page unable to
       scroll. Let the page scroll; the pane caps height via max-height
       and the canvas scales via --preview-scale instead. */
    overflow: visible;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
  }
  /* Canvas + real-mockup img both ride on --preview-scale. Default
     1 = the 340px cap from F15. */
  .editor-with-preview .selected-product-preview .preview-mockup {
    max-width: calc(340px * var(--preview-scale, 1));
    transition: max-width 0.05s linear; /* a hair, so dragging feels live */
  }
  .editor-with-preview .selected-product-preview .preview-mockup
    canvas.live-preview-canvas,
  .editor-with-preview .selected-product-preview .preview-mockup
    img.preview-real-mockup {
    max-width: 100%;
    width: 100%;
    height: auto;
  }
  /* Corner grip — bottom-right, a "≡"-ish chevron stack so it
     reads as draggable rather than decorative. Touch-action:none
     keeps page scroll from stealing the drag gesture. */
  .preview-resize-handle {
    position: absolute;
    bottom: 4px; right: 4px;
    width: 30px; height: 30px;
    cursor: nwse-resize;
    touch-action: none;
    z-index: 7;
    border-radius: 6px;
    background:
      linear-gradient(135deg,
        transparent 0 38%,
        var(--accent-corona, #7b61ff) 38% 46%,
        transparent 46% 56%,
        var(--accent-corona, #7b61ff) 56% 64%,
        transparent 64% 74%,
        var(--accent-corona, #7b61ff) 74% 82%,
        transparent 82% 100%);
    opacity: 0.65;
    transition: opacity 0.15s ease;
  }
  .preview-resize-handle:hover,
  .preview-resize-handle:focus-visible,
  .preview-resize-handle.dragging { opacity: 1; outline: none; }
  /* Move the close × inboard a touch so it doesn't crowd the handle
     when the user drags the pane to its smallest size. */
  .selected-product-preview .preview-close-btn {
    /* keep top-right — handle is bottom-right, so they're far apart */
  }
}

/* ── F11 · Very-narrow phones (≤ 389 px, e.g. iPhone SE 1) ──────
   Drop wavelength grid to 2 columns so each tile is wide enough
   that the descriptor fits without breaking into 3 lines. */
@media (max-width: 389px) {
  .wavelength-grid { grid-template-columns: repeat(2, 1fr); }
  /* Product grid stays 2-up but tighten gap further. */
  .product-grid { gap: 8px; }
}
