/* Host-side styling for the showroom page. Not part of the module. */

/*
 * Electric blue on dark glass. The palette used to be warm — amber on near-black
 * — and every surface below still refers to it through these names, so the
 * direction changes here and only here.
 *
 * Three neon stops rather than one: `--neon` is the colour itself, `--neon-hot`
 * is what a control does when you touch it, and `--neon-deep` is the ambient
 * bloom that never sits behind text. A single blue used for all three either
 * glares or goes flat.
 */
/* Where the moving border light stands on each edge of a pane. REGISTERED so
   they can animate: an unregistered custom property is a string, and a string
   cannot tween. Parked outside the edge by default, so a browser that ignores
   @property shows a still rim and nothing else. */
@property --sw-top    { syntax: '<percentage>'; inherits: false; initial-value: 115%; }
@property --sw-right  { syntax: '<percentage>'; inherits: false; initial-value: 115%; }
@property --sw-bottom { syntax: '<percentage>'; inherits: false; initial-value: -15%; }
@property --sw-left   { syntax: '<percentage>'; inherits: false; initial-value: -15%; }

:root {
  --ink: #f2f5f7;
  --muted: rgba(242, 245, 247, 0.60);
  --line: rgba(255, 255, 255, 0.24);

  /*
   * Was electric blue (#35c8ff). The client asked for pure glass, so the
   * material is achromatic now: these are kept as *tokens* rather than deleted
   * because they still mean "the interactive accent" — every hover, focus ring
   * and active state reads them. Putting a colour back is an edit to these
   * three lines, not a sweep of the file.
   */
  /*
   * WARM GOLD since 2026-09-18 -- the client's call, and the third colour these
   * three lines have held (electric blue, then white for "pure glass"). The
   * brief: popups that look like glass with a NEON effect in the borders, and
   * the neon is the logo's gold, because this is a jeweller's shop. Everything
   * interactive still reads these tokens, which is why it was three lines.
   * `--neon-rgb` is the same gold as bare channels, for the glows that need
   * their own alpha.
   */
  --neon: #e9c47c;
  --neon-hot: #ffe2a6;
  --neon-deep: #8a6a2f;
  --neon-rgb: 233, 196, 124;
  /* The logo's warmth (reff/logo.jpeg): identity accent. Neon stays the
     interaction accent — gold names things, blue answers touch. */
  --gold: #c9a45c;
  /* The logo's lettering face; serif fallback so a blocked font loses only
     the branding, not the words. */
  --display-font: 'Cinzel', 'Times New Roman', serif;
  /* Text on an accent fill. Near-black rather than white: the fill is far
     brighter than any ink could be, so the contrast has to run the other way.
     Neutral now the fill is white — a blue-black on white glass reads as a
     tint at large sizes. */
  --accent-ink: #0b0d0f;
  /* Errors must not be neutral. Everything else on the page now is, so a white
     or grey warning would read as chrome rather than as something being wrong.
     This is the only saturated colour left besides the logo's gold. */
  --warn: #ff8f6b;

  --panel: rgba(12, 14, 16, 0.86);

  /*
   * Glass. One recipe for every floating surface — the banner, the popups, the
   * detail card and the cart — so they read as the same material rather than
   * several separately-tuned panes.
   *
   * `--glass-bg` is deliberately transparent: the point of glass is that the
   * room shows through it. `--glass-bg-solid` is what a browser without
   * backdrop-filter gets instead, and has to be near-opaque because there is no
   * blur to keep text legible over a moving 3D scene.
   */
  /* Clearer than it was (0.55): over the shop's warm marble a dense dark pane
     read as brown smoke. The blur and the saturation do the legibility work. */
  /* 0.58, warm: the pane must read the SAME over the house's blue sky as over
     the shop's warm marble. At 0.44 it took the colour of whatever was behind
     it -- gold-brown in the shop, blue in the garden (seen 2026-09-28). */
  --glass-bg: rgba(22, 17, 12, 0.66);
  --glass-bg-solid: rgba(14, 13, 12, 0.94);
  --glass-blur: 26px;
  --glass-line: rgba(var(--neon-rgb), 0.22);
  --glass-hi: rgba(255, 255, 255, 0.10);
  --glass-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);

  /*
   * The border glow, defined once. It is a box-shadow rather than a border so
   * it can be swapped for the hot variant on hover without moving anything by a
   * pixel — a 1px border that changes colour is fine, a border that changes
   * width reflows the control.
   */
  /* A neon tube is three things: a bright CORE line, a tight glow hugging it,
     and a wide soft bloom. One shadow each. Subtle-to-medium, as agreed. */
  --neon-rim:
    0 0 0 1px rgba(var(--neon-rgb), 0.52),
    0 0 8px rgba(var(--neon-rgb), 0.26),
    0 0 26px rgba(var(--neon-rgb), 0.11),
    inset 0 0 18px rgba(var(--neon-rgb), 0.05);
  --neon-rim-hot:
    0 0 0 1px rgba(255, 226, 166, 0.95),
    0 0 12px rgba(var(--neon-rgb), 0.50),
    0 0 36px rgba(var(--neon-rgb), 0.22),
    inset 0 0 22px rgba(var(--neon-rgb), 0.09);
  /* The edge light of the pane, as a gradient for the border band: brightest
     through two opposite corners, the way light catches the edge of real glass. */
  --rim-gradient: linear-gradient(
      65deg,
      rgba(255, 226, 166, 0.95) 0%,
      rgba(var(--neon-rgb), 0.42) 12%,
      rgba(var(--neon-rgb), 0.16) 40%,
      rgba(var(--neon-rgb), 0.16) 60%,
      rgba(var(--neon-rgb), 0.44) 86%,
      rgba(255, 226, 166, 0.95) 100%);

  --banner-h: 72px;   /* what the popups hang below; kept in step at each breakpoint */
  --audio-h: 46px;    /* the mute button's corner, which the detail strip clears */
}

* { box-sizing: border-box; }

/* Fullscreen, no scrollbar — the interactive view owns the viewport. */
html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  overscroll-behavior: none;
  background: #08090a;
}

body {
  color: var(--ink);
  font: 16px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* The module's mount. Fixed to the viewport; the canvas fills it. */
.hall {
  position: fixed;
  inset: 0;
  outline: none;
}
.hall:focus-visible { box-shadow: inset 0 0 0 2px rgba(255, 176, 32, 0.65); }

/* --- overlay -------------------------------------------------------------
   pointer-events:none so drags fall through to the canvas; individual
   controls opt back in. */
/*
 * No safe-area padding here, deliberately.
 *
 * Insetting the whole overlay also insets every background it contains, so on
 * a notched phone in landscape the ribbon's gradient stopped 44px short of the
 * screen edge and left a bare strip with the 3D showing through. The insets
 * belong on each bar's own padding instead: the bar still spans edge to edge,
 * and only its *contents* keep clear of the notch.
 */
.overlay {
  position: fixed;
  inset: 0;
  pointer-events: none;
  display: flex;
  flex-direction: column;
}

.overlay a,
.overlay button,
.overlay .swatches { pointer-events: auto; }

/*
 * While the environment loads, the chrome stands down — the module's own
 * loading overlay is the only thing on stage. Faded rather than display:none
 * so arrival is a half-second bloom instead of a pop, and so nothing reflows.
 */
body.is-loading .ribbon,
body.is-loading .audio-toggle,
body.is-loading .hint {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}
/* The buttons too, by name: `.overlay button { pointer-events: auto }` above
   re-arms every button inside a none'd parent, so an invisible banner would
   still be clickable — an invisible click target is worse than a visible one. */
body.is-loading .ribbon button,
body.is-loading .audio-toggle { pointer-events: none; }
/*
 * The fullscreen toggle is the exception, and stays live and on top while the
 * environment loads. `.hall` is position:fixed with no z-index, so it opens no
 * stacking context and the module's loading overlay (z-index 50) competes
 * directly with this button in the root context — at 20 it was painted over,
 * and the is-loading rule above had also taken its clicks. A visitor watching a
 * progress bar is exactly who wants the extra screen, and the offer used to be
 * unreachable until the room had finished arriving.
 */
body.is-loading .corner-toggle { z-index: 60; pointer-events: auto; }
.ribbon, .audio-toggle, .corner-toggle { transition: opacity 0.45s ease; }

/* --- landing choice ------------------------------------------------------ */

.landing {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 24px;
  text-align: center;
  /*
   * A neon field, in three layers over the ground: a dot grid that gives the
   * dark somewhere to be, a wash from the top, and the two blurred blobs below.
   * All CSS — the landing runs before any WebGL exists, so it is the one screen
   * with frames to spare, and a canvas here would only be another thing to tear
   * down before the hall boots.
   */
  background:
    radial-gradient(circle at center, rgba(255, 255, 255, 0.14) 1px, transparent 1.4px) 0 0 / 34px 34px,
    radial-gradient(130% 90% at 50% 0%, rgba(120, 126, 132, 0.55), transparent 62%),
    #08090a;
  overflow: hidden;
}

/* The drifting blobs. Pseudo-elements rather than markup: they are decoration,
   and the landing's own children are the choice the visitor came to make. */
.landing::before,
.landing::after {
  content: '';
  position: absolute;
  width: 46vmax;
  height: 46vmax;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.42;
  pointer-events: none;
}
.landing::before {
  background: radial-gradient(circle, var(--neon) 0%, transparent 68%);
  top: -18vmax; left: -12vmax;
  animation: drift-a 26s ease-in-out infinite alternate;
}
.landing::after {
  background: radial-gradient(circle, var(--neon-deep) 0%, transparent 68%);
  bottom: -20vmax; right: -14vmax;
  animation: drift-b 32s ease-in-out infinite alternate;
}
@keyframes drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(14vmax, 9vmax, 0) scale(1.16); }
}
@keyframes drift-b {
  from { transform: translate3d(0, 0, 0) scale(1.1); }
  to   { transform: translate3d(-12vmax, -8vmax, 0) scale(1); }
}
/* Ambience is never worth motion sickness. The gradients stay, the drift goes. */
@media (prefers-reduced-motion: reduce) {
  .landing::before, .landing::after { animation: none; }
}

.landing > * { position: relative; }   /* above the blobs */
.landing[hidden] { display: none; }

/*
 * The mark above the question. `<use>` clones the banner's traced SVG, so the
 * logo exists in the document exactly once — but that also means it renders
 * only while the source is; the banner's is always in the DOM, so it is.
 * currentColor comes from here, not the banner.
 */
.landing-mark {
  /* Sized by height, width from the viewBox: the brand monogram is wider than
     it is tall (346 × 274), so a square box would letterbox it and draw it
     small. */
  height: 84px;
  width: auto;
  color: var(--ink);
  filter: drop-shadow(0 0 14px rgba(255, 255, 255, 0.22));
}

.landing .eyebrow { margin: 0; letter-spacing: 0.42em; }
.landing h1 {
  margin: 0 0 26px;
  font-family: var(--display-font);
  font-size: clamp(26px, 4vw, 40px);
  font-weight: 600;
  letter-spacing: 0.04em;
}

/*
 * The landing's fullscreen offer — an aside, not a third choice.
 *
 * Sized and weighted well below `.landing-card` on purpose: the page asks one
 * question ("house or shop?") and this must not read as a third answer to it.
 * A quiet text button under the pair, the way a "skip" link sits under a form.
 */
.landing-fullscreen {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: 22px;
  padding: 9px 16px;
  border: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
  transition: color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.landing-fullscreen svg { width: 17px; height: 17px; display: block; }
.landing-fullscreen[hidden] { display: none; }
.landing-fullscreen:hover,
.landing-fullscreen:focus-visible {
  outline: none;
  color: var(--neon-hot);
  background: rgba(255, 255, 255, 0.09);
  box-shadow: var(--neon-rim-hot);
}

.landing-choices {
  display: grid;
  /* The width is stated rather than left to shrink-to-fit. `.landing` is a
     centred flex column, so this grid sized to its content and `auto-fit` only
     ever saw room for one track — the two cards stacked on a 1440px screen. */
  width: min(718px, 100%);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
}

.landing-card {
  display: grid;
  gap: 10px;
  padding: 26px 24px;
  border: 0;
  border-radius: 18px;
  background:
    radial-gradient(130% 90% at 50% 0%, rgba(255, 255, 255, 0.11), transparent 58%),
    var(--glass-bg);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  box-shadow: 0 0 18px rgba(255, 255, 255, 0.09);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: box-shadow 0.2s ease, transform 0.2s ease, background 0.2s ease;
}
.landing-card:hover,
.landing-card:focus-visible {
  outline: none;
  box-shadow: var(--neon-rim-hot);
  background: rgba(255, 255, 255, 0.05);
  transform: translateY(-2px);
}
.landing-card strong { font-size: 19px; font-weight: 600; }
.landing-card span { color: var(--muted); font-size: 13.5px; line-height: 1.5; }

/* --- top ribbon ---------------------------------------------------------- */

/*
 * The banner is a layout row, not a surface. It had a gradient band with a
 * neon underline; the client cut both, and rightly — every control in it
 * already carries its own glass pill, so the band was paint on top of paint.
 * What remains is flex, safe-area padding, and a z-index.
 */
.ribbon {
  position: relative;
  display: flex;
  align-items: center;
  gap: 18px;
  /* Left inset is tighter than the right: the wordmark is an anchor, not a
     control, and the client wanted it hugging the corner a little closer. */
  padding:
    calc(18px + env(safe-area-inset-top))
    calc(28px + env(safe-area-inset-right))
    18px
    calc(16px + env(safe-area-inset-left));
  /* z-index by intent: above the ambience (10) and above the popups' own
     stacking, so a popup slides out from behind it rather than over it. */
  z-index: 20;
}


.ribbon-left,
.ribbon-right { display: flex; align-items: center; gap: 12px; min-width: 0; }
/* Explicit, rather than the source order and a stray margin that used to do it. */
.ribbon-right { margin-left: auto; }

/*
 * The mark, and the one thing standing between it and a white room.
 *
 * `--ink` is #f2f5f7 — near-white — and the ribbon has no background of its
 * own. Every control beside it survives that because `.banner-button` carries
 * dark glass; the wordmark was deliberately bare, "a logo, not a control". In
 * the bright shop that meant a near-white mark on a near-white wall, which is
 * to say no mark at all. Reported on mobile, but the shop is bright at every
 * width and it was only ever least bad on desktop.
 *
 * A RADIAL GRADIENT, not a pill. A second glass button beside the real ones
 * would read as a control and invite a press, which is the exact thing the
 * original note was protecting against. This is a pool of shade the mark sits
 * in: opaque under the glyph, gone by its edges, no border and no shape to
 * point at. The mark keeps its own white glow on top, so it now reads as lit
 * rather than as printed.
 */
/*
 * The mark, in a disc of its own.
 *
 * `--ink` is #f2f5f7 and the ribbon has no background, so in the bright shop a
 * near-white logo sat on a near-white wall and vanished. Two earlier attempts
 * are worth not repeating: a radial gradient on this element read as a stain,
 * and a corner scrim across the ribbon read as a smudge over the room. Both
 * failed the same way — soft-edged darkness over a photograph looks like dirt
 * on the lens, because nothing in the frame explains where it came from.
 *
 * A CIRCLE explains itself. It is the same translucent glass the mute and
 * fullscreen buttons wear, so it reads as a deliberate piece of chrome rather
 * than as shading, and the room still shows through it.
 *
 * It does now look like a control, which the note below was originally against
 * — but it IS one: pressing the mark returns to the landing, the way a site
 * logo does. That was always true and previously undiscoverable.
 */
.wordmark {
  display: inline-grid;
  place-items: center;
  /* Square, so the radius draws a circle rather than a lozenge. Sized off the
     34px glyph, with room around it. */
  width: 48px;
  height: 48px;
  border-radius: 999px;
  /* It is a button, but it should read as the logo, not as a control. */
  font-family: inherit;
  color: inherit;
  border: 0;
  padding: 0;
  /* `--glass-bg` and `--neon-rim`, the same tokens every other pill in the
     chrome uses — NOT a hand-picked rgba beside them. A one-off 0.42 black was
     visibly lighter than the Racks pill next to it, which is how four
     near-identical button recipes drifted apart in this file before. One
     material, defined once. */
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  backdrop-filter: blur(10px) saturate(140%);
  box-shadow: var(--neon-rim);
  cursor: pointer;
  pointer-events: auto;
  transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.wordmark:hover,
.wordmark:focus-visible {
  outline: none;
  color: var(--neon-hot);
  box-shadow: var(--neon-rim-hot);
}
.wordmark-glyph {
  /*
   * 25px inside a 48px disc — a little over half, which leaves the mark ringed
   * by its own space rather than pressed against the glass.
   *
   * It was 34px, sized back when it sat in an open row beside the word MAAYA
   * and had the whole ribbon to breathe into. Dropped into a circle at that
   * size it touched the edges and the disc read as a crop rather than a frame.
   * The mark is not square (346x274), so height is what is set and the width
   * follows — see .landing-mark, which has the same note for the same reason.
   */
  height: 25px; width: auto;
  flex: none;
  /* The mark is drawn in the page ink with gold accents — the logo's own navy
     reads as black on this ground. The glow is soft and blue: identity in the
     letter, techno in the halo. */
  color: var(--ink);
  filter: drop-shadow(0 0 7px rgba(255, 255, 255, 0.34));
}
/* `.wordmark-text` lived here — the word MAAYA set in the body face beside the
   mark. The mark carries the name on its own and the pair cost width the header
   needs on a phone, so the span is gone from index.html and its rule with it.
   The brand's own lettering still exists as vector in models/maaya-logo.svg if
   the full lockup is ever wanted back. */

/*
 * Every control in the banner is the same pill. The old page had four
 * near-identical recipes — .cart-button, .back-button, .rooms-toggle,
 * .icon-button — that had already drifted apart by a pixel here and a shade
 * there; this is the one they collapse into.
 */
.banner-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 0;
  border-radius: 999px;
  padding: 8px 18px;
  min-height: 44px;              /* a finger; the old pill was 30 */
  /* Firmer than the panes: a pill is small and sits straight on the sky. */
  background: rgba(24, 18, 12, 0.86);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  box-shadow: var(--neon-rim);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  transition: box-shadow 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.banner-button:hover,
.banner-button:focus-visible {
  outline: none;
  color: var(--neon-hot);
  box-shadow: var(--neon-rim-hot);
}
.banner-button[hidden] { display: none; }
.banner-button svg { width: 19px; height: 19px; display: block; }

/*
 * Skip intro: bottom right, immediately LEFT of the fullscreen toggle.
 *
 * Anchored off that button's own geometry rather than a hand-picked number —
 * `.corner-toggle` is 38px wide at a 16px inset, so this clears it by 10px and
 * stays cleared if either changes. A literal `right: 64px` would silently
 * overlap the day the toggle is resized.
 *
 * Same corner as fullscreen on purpose: both are "get on with it" controls,
 * and the intro is the moment a visitor most wants them under one thumb.
 */
.skip-intro-button {
  position: fixed;
  bottom: calc(20px + env(safe-area-inset-bottom));
  right: calc(16px + 38px + 10px + env(safe-area-inset-right));
  transform: none;
  /* The same gold glass pill as every other control (2026-09-28). It was
     half-transparent black at 50% opacity -- the one grey button left, and on
     the house's bright lawn it read as disabled. */
  font-size: 14px;
  padding: 10px 24px;
  z-index: 100;
}

/* Square-ish, for the ones carrying a glyph rather than a word. */
.banner-button.icon { padding: 8px; min-width: 44px; position: relative; }
.banner-button[aria-pressed="true"] {
  background: var(--neon);
  color: var(--accent-ink);
  box-shadow: var(--neon-rim-hot);
}

/* Open is a state worth showing: the popup is off to one side and easy to lose
   track of, so its button stays lit while it is up. */
.banner-button[aria-expanded="true"] {
  background: rgba(var(--neon-rgb), 0.14);
  color: var(--neon-hot);
  box-shadow: var(--neon-rim-hot);
}

/* Points at the popup, and turns over when it is open. */
.banner-caret {
  width: 0; height: 0;
  border-left: 4.5px solid transparent;
  border-right: 4.5px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform 0.2s ease;
}
.banner-button[aria-expanded="true"] .banner-caret { transform: rotate(180deg); }

.cart-button { position: relative; }

/*
 * The count rides on the corner of the cart glyph rather than sitting beside
 * it, so the button stays the same width whether the cart holds 1 or 11 — a
 * banner control that resizes as you shop is a banner control that moves.
 * cart.js hides it at zero; an empty cart is said by the absence of a badge.
 */
.cart-count {
  position: absolute;
  top: -3px; right: -3px;
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--neon);
  color: var(--accent-ink);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
}
.cart-count[hidden] { display: none; }

/* --- product details ----------------------------------------------------- */

/*
 * The bottom-left strip. Its lower padding clears the mute button, which is
 * pinned to the corner underneath it — the swatches are the bottom-most thing
 * in here and they sit hard left, so without this the two share a pixel.
 */
.details {
  margin-top: auto;
  padding:
    22px
    calc(28px + env(safe-area-inset-right))
    calc(22px + var(--audio-h))
    calc(28px + env(safe-area-inset-left));
  max-width: 420px;
  transition: opacity 0.25s ease;
}
/* Stands down while the detail panel is showing the same information. */
.details.is-hidden { opacity: 0; pointer-events: none; }

.eyebrow {
  margin: 0 0 8px;
  font-size: 11px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--gold);
}

.details h1 {
  margin: 0 0 6px;
  font-family: var(--display-font);
  font-size: clamp(24px, 3.2vw, 34px);
  font-weight: 600;
  letter-spacing: 0.03em;
  line-height: 1.12;
}

.tagline { margin: 0 0 10px; color: var(--muted); font-size: 14px; }
.price { margin: 0 0 16px; font-variant-numeric: tabular-nums; font-size: 15px; }

.controls { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }

.swatches { display: flex; gap: 8px; }
.swatch {
  width: 26px; height: 26px;
  border-radius: 999px;
  border: 1px solid var(--line);
  cursor: pointer;
  padding: 0;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
/*
 * The touch target, not the dot. A 26px swatch is the right size to look at and
 * far too small to hit — but growing the dot would turn the colourway row into
 * a row of buttons. The pseudo-element extends the hit area to 44px in every
 * direction without changing a single pixel of what is drawn.
 *
 * Note for anyone measuring: getBoundingClientRect() on the swatch still
 * reports 26px. The tappable area is the ::after, and that is deliberate.
 */
.swatch { position: relative; }
.swatch::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}

.swatch[aria-pressed="true"] {
  box-shadow: 0 0 0 2px #08090a, 0 0 0 4px var(--neon), 0 0 14px rgba(255, 255, 255, 0.31);
  transform: scale(1.06);
}

.primary {
  border: 0;
  background: linear-gradient(180deg, var(--neon-hot), var(--neon));
  color: var(--accent-ink);
  border-radius: 10px;
  padding: 11px 22px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  letter-spacing: 0.02em;
  box-shadow: 0 0 0 1px rgba(255, 236, 196, 0.55), 0 0 18px rgba(var(--neon-rgb), 0.34);
  transition: box-shadow 0.18s ease, filter 0.18s ease;
}
.primary:hover { box-shadow: 0 0 0 1px rgba(255, 236, 196, 0.9), 0 0 30px rgba(var(--neon-rgb), 0.52); filter: brightness(1.05); }
.primary:disabled, .primary[aria-disabled="true"] {
  background: rgba(255, 255, 255, 0.07); color: var(--muted); box-shadow: 0 0 0 1px var(--glass-line);
  cursor: not-allowed; filter: none;
}
.primary:active { transform: translateY(1px); }

/* --- sound ---------------------------------------------------------------- */

/*
 * Bottom-left, pinned to the viewport rather than left in the overlay's flex
 * flow — `.details` above it is display:none whenever nothing is selected, and
 * anything riding below that sibling moves when it disappears.
 */
/*
 * The two corner settings: mute bottom-left, fullscreen bottom-right.
 *
 * They share everything but which corner they sit in, so they share a recipe —
 * the fourth near-identical copy of this pill was how `.back-button` drifted a
 * line-height away from `.banner-button` and made the header uneven.
 */
.audio-toggle,
.corner-toggle {
  position: fixed;
  bottom: calc(20px + env(safe-area-inset-bottom));
  z-index: 20;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  backdrop-filter: blur(10px) saturate(140%);
  box-shadow: var(--neon-rim);
  color: var(--ink);
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
  transition: box-shadow 0.2s ease, color 0.2s ease;
}
/* The corners themselves — the only thing that differs. */
.audio-toggle { left: calc(16px + env(safe-area-inset-left)); }
.corner-toggle { right: calc(16px + env(safe-area-inset-right)); }

.audio-toggle[hidden],
.corner-toggle[hidden] { display: none; }
.audio-toggle:hover,
.audio-toggle:focus-visible,
.corner-toggle:hover,
.corner-toggle:focus-visible {
  outline: none;
  color: var(--neon-hot);
  box-shadow: var(--neon-rim-hot);
}
.audio-toggle svg,
.corner-toggle svg { width: 19px; height: 19px; display: block; }
/* In fullscreen the button is the way back out, so it stays lit. */
.corner-toggle[aria-pressed="true"] {
  color: var(--neon-hot);
  box-shadow: var(--neon-rim-hot);
}

/*
 * One icon, two states. The waves and the cross are both in the markup and CSS
 * chooses; swapping the whole glyph would mean the speaker cone re-rendering on
 * every toggle, and it is the part that should look like it never moved.
 */
.audio-toggle .audio-cross { opacity: 0; transition: opacity 0.18s ease; }
.audio-toggle .audio-waves { opacity: 1; transition: opacity 0.18s ease; }
.audio-toggle[data-muted="true"] { color: var(--muted); }
.audio-toggle[data-muted="true"] .audio-cross { opacity: 1; }
.audio-toggle[data-muted="true"] .audio-waves { opacity: 0; }

/* Muted is the resting state, not a pressed one — no neon fill. */
.audio-toggle[aria-pressed="true"] { background: var(--glass-bg); }

/* --- hint ---------------------------------------------------------------- */

/* Bottom centre, 20px off the screen edge.
   Pinned to the viewport rather than left in the overlay's flex flow: it used
   to ride below `.details`, which is display:none whenever nothing is
   selected, and with that sibling gone the `margin-top: auto` that pushed the
   pair down stopped applying and the hint rose up under the ribbon. Fixed
   positioning makes it independent of whether anything is on display. */
.hint {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(20px + env(safe-area-inset-bottom));
  padding: 0 calc(28px + env(safe-area-inset-right)) 0 calc(28px + env(safe-area-inset-left));
  text-align: center;
  font-size: 12px;
  color: var(--muted);
  transition: opacity 0.6s ease;
}
.hint.faded { opacity: 0; }
.hint-key {
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 1px 6px;
  color: var(--ink);
}

/*
 * Placement mode. The visitor asked to see the piece standing in the room, so
 * the room is the whole point — the panel stands down entirely rather than
 * describing a thing it is covering, and the way back becomes the one control
 * on screen, bottom-left where a thumb reaches and an eye returns.
 */
/* `.panel-heart` is inside the panel now, so it stands down with it and needs
   no rule of its own — it was listed here when it floated over the 3D view. */
body.is-placing #product-panel.popup,
body.is-placing .popup-scrim {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}

/*
 * The banner wishlist and cart stand down while the product panel is open.
 *
 * They sit top-right, which is exactly where the panel arrives, so they end
 * up floating ON the panel -- and the panel already carries its own heart
 * beside the title, so the visitor is shown two hearts a few centimetres
 * apart meaning different things: one saves the piece being described, the
 * other opens the whole wishlist.
 *
 * Same fade as everything else that stands down for the panel: opacity and
 * visibility over 0.18s, matching .popup so the two move together rather
 * than one lagging the other. visibility is delayed by the full duration so
 * the buttons stay hittable until they have actually gone.
 */
.panel-backdrop {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  z-index: 24;
  background: transparent;
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 20%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 20%);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
}

@media (min-width: 601px) {
  .panel-backdrop { width: 59vw; }
}

body:has(#product-panel[data-open="true"]) .panel-backdrop {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.18s ease, visibility 0s;
}

/*
 * Spelled with the same :has() as the rule above ON PURPOSE. Written as plain
 * `body.is-placing .panel-backdrop` this lost to it on specificity -- the
 * "panel is open" selector carries an id -- so the panel stood down for a
 * placed piece and its blur did not: the visitor looked at their idol in the
 * room through frosted glass across 59% of the screen (reported 2026-10-05).
 * The panel stays "open" all through a placement; see product-panel.js.
 */
body.is-placing .panel-backdrop,
body.is-placing:has(#product-panel[data-open="true"]) .panel-backdrop {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}

body:has(#product-panel[data-open="true"]) .ribbon-right #wishlist-button,
body:has(#product-panel[data-open="true"]) .ribbon-right #cart-button {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
}

body.is-placing #back-button {
  position: fixed;
  left: calc(28px + env(safe-area-inset-left));
  /* Above the mute button, which keeps the corner itself. */
  bottom: calc(78px + env(safe-area-inset-bottom));
  z-index: 30;
  padding: 14px 26px;
  font-size: 15px;
  background: var(--glass-bg);
  box-shadow: var(--neon-rim-hot);
}
@media (max-width: 900px) {
  body.is-placing #back-button {
    left: calc(14px + env(safe-area-inset-left));
    bottom: calc(64px + env(safe-area-inset-bottom));
    padding: 12px 20px;
  }
}

/* --- back button --------------------------------------------------------- */

/*
 * `.back-button` is a HOOK, not a recipe.
 *
 * Every element carrying it also carries `.banner-button` (the three back-outs
 * in index.html), so it used to be a near-exact copy of that pill — and being
 * later in the file, it quietly won. That is precisely the drift the note above
 * `.banner-button` describes, caught in the act: its `font: inherit` is a
 * SHORTHAND, so it reset `line-height` back to the inherited ~1.5 and undid
 * `.banner-button`'s `line-height: 1`. The result was that "Exit shopping" and
 * "Back to collection" stood about 6px taller than "Navigation" beside them,
 * for no reason anyone had chosen.
 *
 * The name survives only as a selector for the placement rules below and for
 * the audio cue mapping. It deliberately declares no appearance of its own —
 * one pill, defined once.
 */
.back-button[hidden] { display: none; }

/* --- popups ---------------------------------------------------------------
 * Everything that used to be a full-height drawer bolted to the screen edge is
 * a floating card now: navigation under the banner's left, the product detail,
 * the cart and the saved pieces under its right.
 *
 * They are detached from the edge on purpose. A drawer flush to the right cuts
 * the viewport in two and reads as a second page; a card inset from it leaves
 * the room continuous behind and around it, which is the whole reason to build
 * a showroom in 3D. product-panel.js measures the gap as well as the card when
 * it tells the hall where to hold a piece, so the inset costs nothing.
 *
 * Visibility is `data-open`, not `hidden`: `hidden` means display:none, and
 * something that never renders cannot animate in. popup.js sets `inert` while
 * closed so nothing inside is tabbable in the meantime.
 */
.popup {
  position: fixed;
  /* Squarer than the first round — the client asked the roundness down; the
     glass and the glow carry the look now, not the corner radius. */
  border-radius: 10px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow-y: auto;
  overscroll-behavior: contain;
  pointer-events: auto;
  z-index: 25;

  /* Closed: out of the way, out of the tab order, and not painting. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px) scale(0.985);
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s linear 0.18s;
}

/*
 * Two anchor families. `.popup-center` is the modal: dead centre, 60% of a
 * desktop, 90% of a phone, over the scrim. `.popup-right` survives for exactly
 * one popup — the product detail card, which sits beside the 3D piece it
 * describes; centring that would put the glass on top of its own subject.
 *
 * The closed/open transforms are written per-family because transform is one
 * property: the centred popup's translate(-50%,-50%) has to be restated in
 * both states or the closed nudge would replace the centring outright.
 */
.popup-center {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(0.97);
  /* The checkout card's exact size — the client called that one perfect, so
     every centred popup wears it and the modals read as one family. */
  width: min(520px, 92vw);
  max-height: min(78dvh, 720px);
}
.popup-center[data-open="true"] { transform: translate(-50%, -50%); }

.popup-right {
  top: calc(var(--banner-h) + 12px + env(safe-area-inset-top));
  max-height: calc(100dvh - var(--banner-h) - 28px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  transform: translateY(-8px) scale(0.985);
}
.popup-right[data-open="true"] { transform: none; }

/*
 * The product panel fill, at EVERY width.
 *
 * This used to live only inside @media (min-width: 601px), so a phone got
 * the panel with no fill behind it at all -- the copy sat straight on the
 * shop. Declared here it covers narrow windows and phones as well, and the
 * wide-screen block no longer restates it.
 *
 * Every stop is a PERCENTAGE of the panel, never a pixel: the panel is 50vw
 * on a desktop and near full-width on a phone, and a ramp measured in pixels
 * would be a third of the panel on one and a sliver on the other. In
 * percent the fade is the same shape at any size.
 *
 * Transparent at the left edge, 0.7 black by 10%, holding 0.7 across the
 * remaining 90%. Translucent throughout, so the shop reads faintly behind
 * the copy rather than being walled off.
 */
/* 2026-09-18: the fill that lived here was a left-to-right FADE with the blur
   switched off, because a blur clips hard at the box edge and a fade must not
   have a hard edge. The client asked for the panel to be GLASS with a neon
   border like every other popup -- and a card HAS an edge, so the reason for
   the exception is gone. It inherits `.popup` whole: fill, blur, rim, light. */
#product-panel.popup { z-index: 26; }

.popup[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
  .popup { transition: opacity 0.18s ease, visibility 0s linear 0.18s; }
  .popup-center, .popup-center[data-open="true"] { transform: translate(-50%, -50%); }
  .popup-right, .popup-right[data-open="true"] { transform: none; }
  .popup[data-open="true"] { transition: opacity 0.18s ease, visibility 0s; }
}

/*
 * The scrim under the centered popups. Shown by popup.js while any of them is
 * open; a click on it closes the top one. Light-handed on purpose — the room
 * should dim, not disappear: it is still the product being sold.
 */
.popup-scrim {
  position: fixed;
  inset: 0;
  z-index: 24;                /* under the popups at 25/30, over everything else */
  background: rgba(6, 7, 8, 0.38);
  /* Hidden until a popup opens it. Without these the flat wash sat over the
   * stage permanently — every render was being judged through 38% black,
   * which read as "the lighting is flat" rather than as a stray overlay. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
}
.popup-scrim[data-open="true"] { pointer-events: auto; }

/*
 * The product panel's scrim is a gradient, not a flat wash. A flat one dims
 * and blurs the idol as hard as the room — and the idol is the thing the panel
 * exists to present, held deliberately in the clear 40%. This keeps the modal
 * contract (a dimmed backdrop, click-outside to close) while arriving with the
 * panel rather than covering the subject.
 */
body:has(#product-panel[data-open="true"]) .popup-scrim {
  background: linear-gradient(90deg,
    rgba(6, 7, 8, 0) 0%,
    rgba(6, 7, 8, 0) 34%,
    rgba(6, 7, 8, 0.22) 58%,
    rgba(6, 7, 8, 0.42) 100%);
  /* No blur: see the note on #product-panel.popup. The scrim is a dim, not a
     frosting -- the shop behind the panel stays sharp. */
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  pointer-events: auto;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.18s ease, visibility 0s linear 0.18s;
}
.popup-scrim[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.18s ease, visibility 0s;
}

/*
 * A popup that scrolls gets the browser's default scrollbar, which on Windows
 * is a light grey slab — the one opaque, un-neon thing on the page, sitting on
 * the glass's rounded corner. Both syntaxes: Firefox takes the standard one,
 * WebKit and Chromium still need the pseudo-elements.
 */
.popup {
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--neon-rgb), 0.45) transparent;
}
.popup::-webkit-scrollbar { width: 6px; }
.popup::-webkit-scrollbar-track { background: transparent; }
.popup::-webkit-scrollbar-thumb {
  background: rgba(var(--neon-rgb), 0.42);
  border-radius: 999px;
}
.popup::-webkit-scrollbar-thumb:hover { background: rgba(var(--neon-rgb), 0.75); }

.popup-right { right: calc(28px + env(safe-area-inset-right)); width: min(380px, calc(100vw - 56px)); }


/* The cart sits above the detail card: adding to it is what you do *from* the
   card, so it must be able to cover it rather than open behind. */
#cart { z-index: 30; }

.popup-head {
  display: flex;
  /* Centred, not flex-start: with a one-line Cinzel title the close button
     used to ride high against the text's cap height. */
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.popup-head h2 {
  margin: 0;
  font-family: var(--display-font);
  font-size: 19px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
/* Compound selector on purpose: every close button is also a .ghost, whose
   own `padding: 8px 12px` sits later in this file and would win on order —
   squeezing the content box to 8px and shoving the 13px glyph 2.5px right of
   centre. The extra class outranks it regardless of position. */
.ghost.popup-close {
  flex: none;
  width: 34px; height: 34px;
  padding: 0;
  display: inline-grid;
  place-items: center;
  line-height: 1;
}

/* --- navigation tiles ---------------------------------------------------- */

/* Square icon tiles in a fluid grid — eight rooms or eight racks, same board. */
/*
 * Search. Not a popup of its own and not a pill — a quiet field that reads as
 * part of the popup's own glass, with the icon inside it rather than beside it
 * so the row costs one line of height instead of two.
 */
.nav-search {
  position: relative;
  margin-bottom: 14px;
}
.nav-search[hidden] { display: none; }

.nav-search-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  color: var(--muted);
  pointer-events: none;   /* the field owns the whole row's clicks */
  background:
    /* icons/search.svg, inline: a background can't be currentColor, and this
       one glyph never changes state, so a mask would be ceremony. */
    no-repeat center/18px
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%238fb4d8" stroke-width="1.6" stroke-linecap="round"><circle cx="10.8" cy="10.8" r="6.3"/><path d="M15.4 15.4 20 20"/></svg>');
}

#nav-search-input {
  width: 100%;
  /* 44px tall everywhere, not just under the coarse-pointer media query —
     it is a text target and a short one is fiddly with a mouse too. */
  min-height: 44px;
  padding: 10px 14px 10px 38px;
  border: 0;
  border-radius: 8px;
  background: rgba(12, 14, 16, 0.45);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.17);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
}
#nav-search-input::placeholder { color: var(--muted); }
#nav-search-input:focus-visible {
  outline: none;
  box-shadow: var(--neon-rim-hot);
}
/* WebKit draws its own clear button; ours is Escape, and the native one lands
   on the glass looking like a defect. */
#nav-search-input::-webkit-search-cancel-button { display: none; }

.nav-empty {
  margin: 14px 0 0;
  color: var(--muted);
  font-size: 14px;
  text-align: center;
}
.nav-empty[hidden] { display: none; }

/* A filtered-out tile must leave the grid, not just go invisible — otherwise
   auto-fill keeps its column and the results sit in a sieve. */
.nav-tile[hidden] { display: none; }

.nav-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
  gap: 10px;
}
/* Rashi / Intentions. Spans the grid so it reads as a rule between two sets. */
.nav-group-title {
  grid-column: 1 / -1;
  margin: 6px 0 0;
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--display-font);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--neon);
}
.nav-group-title::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, rgba(var(--neon-rgb), 0.45), transparent);
}
.nav-group-title:first-child { margin-top: 0; }
.nav-group-title[hidden] { display: none; }

.nav-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 104px;             /* was a forced square; a two-line name needs the room */
  border: 0;
  border-radius: 8px;
  padding: 12px 8px;
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(255, 255, 255, 0.08), transparent 60%),
    rgba(255, 255, 255, 0.025);
  color: rgba(242, 245, 247, 0.82);
  font: inherit;
  cursor: pointer;
  transition: box-shadow 0.18s ease, color 0.18s ease, background 0.18s ease;
}
.nav-tile:hover,
.nav-tile:focus-visible {
  outline: none;
  color: var(--neon-hot);
  background: rgba(var(--neon-rgb), 0.08);
  box-shadow: var(--neon-rim-hot);
  transform: translateY(-1px);
}
.nav-tile[aria-pressed="true"] {
  background: rgba(var(--neon-rgb), 0.13);
  color: var(--neon-hot);
  box-shadow: var(--neon-rim-hot), inset 0 0 24px rgba(var(--neon-rgb), 0.14);
}
/* The active tile marks itself with a glow rather than a solid fill: a filled
   square of neon next to seven glass ones read as a different component. */

.nav-tile-icon { display: block; }
.nav-tile-icon svg { width: 30px; height: 30px; display: block; }
/* Two lines, never an ellipsis: "Marriage Blessings" wraps, it is not cut. */
.nav-tile-label {
  font-size: 13px;
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: 0.02em;
  text-align: center;
  max-width: 100%;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-wrap: balance;
}
.nav-tile-sub {
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--muted);
  line-height: 1.2;
}
.nav-tile:hover .nav-tile-sub,
.nav-tile[aria-pressed="true"] .nav-tile-sub { color: rgba(var(--neon-rgb), 0.85); }
/* The list runs out of the bottom of the pane: fade it, do not slice a row in
   half. Sticky, so it holds the pane's foot while the tiles scroll under it. */
#nav-popup:not([data-scrollable="true"])::after { display: none; }
#nav-popup::after {
  content: '';
  position: sticky;
  bottom: -20px;
  flex: 0 0 34px;
  margin-top: -48px;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(14, 13, 12, 0), rgba(14, 13, 12, 0.78));
}

/* --- saved pieces -------------------------------------------------------- */

.saved-list { display: grid; gap: 8px; }
.saved-list:empty { display: none; }

.saved-empty {
  display: grid;
  justify-items: center;
  gap: 6px;
  padding: 26px 12px 30px;
  text-align: center;
  color: var(--muted);
}
.saved-empty[hidden] { display: none; }
.saved-empty svg {
  width: 36px;
  height: 36px;
  color: rgba(var(--neon-rgb), 0.78);
  filter: drop-shadow(0 0 10px rgba(var(--neon-rgb), 0.35));
  margin-bottom: 4px;
}
.saved-empty p { margin: 0; font-size: 15px; color: var(--ink); }
.saved-empty span { font-size: 12.5px; }
.saved-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 8px;
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(255, 255, 255, 0.07), transparent 60%),
    rgba(255, 255, 255, 0.02);
}
.saved-thumb {
  width: 38px; height: 38px;
  flex: none;
  object-fit: cover;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
}
.saved-text { display: grid; flex: 1; min-width: 0; font-size: 13px; }
.saved-text strong { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved-price { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.ghost.saved-remove {
  flex: none;
  width: 30px; height: 30px;
  padding: 0;
  display: inline-grid;
  place-items: center;
  line-height: 1;
}

/* --- product detail panel -------------------------------------------------
 * On a wide screen this is not a card but the right 40% of the viewport, the
 * selected piece held in the left 60%. Three things make it read as one
 * surface with the scene rather than a pane laid over it:
 *
 *   1. No rim and no radius. The `.popup` edge light is explicitly unset here;
 *      every other popup announces its edges, this one must not have any.
 *   2. The left edge is *masked out*, not drawn — the glass thins to nothing
 *      over ~14% of its width, so it dissolves toward the idol. A mask, not a
 *      gradient overlay, because it has to take the backdrop-filter with it:
 *      an overlay would fade the tint and leave a hard blur edge behind.
 *   3. The scrim behind it is a gradient too (see `.popup-scrim` below), so
 *      the dimming arrives with the panel instead of covering the idol.
 * ======================================================================== */

/*
 * 601px, not 901px.
 *
 * A phone held landscape is ~844x390, which fell under the old 900px cut and
 * got the narrow side card instead of this — so the split existed on desktop
 * and nowhere else, and mobile kept the panel this replaced. Portrait is not a
 * case to design for: demo/mobile.js covers it with "turn your device", so
 * "mobile" here means a landscape phone, which is a 60/40 split's natural
 * shape. The remaining fallback below 601px is for a narrow DESKTOP window,
 * which gets no rotate cover and still needs somewhere sensible to put a card.
 */
/*
 * THE PRODUCT PANEL, AS A CARD (2026-09-18).
 *
 * It was a 59vw full-height fade with no edge at all: copy floating on the
 * shop, name centred over a left-aligned paragraph, two dividers where one was
 * meant. Now a glass card docked to the right, one left-aligned column, and
 * the commit -- total and the two buttons -- pinned to its foot so it is
 * reachable however long the copy runs.
 *
 * Narrower on purpose. product-panel.js reports the panel's left edge to the
 * hall (syncInspectOffset), and the hall holds the inspected piece in the strip
 * that is left: 59vw of panel gave the statue 41% of the screen, this gives it
 * about two thirds. The piece is what is for sale.
 */
@media (min-width: 601px) {
  #product-panel.popup {
    top: calc(12px + env(safe-area-inset-top));
    right: calc(12px + env(safe-area-inset-right));
    bottom: calc(12px + env(safe-area-inset-bottom));
    left: auto;
    width: clamp(360px, 31vw, 470px);
    max-width: none;
    max-height: none;
    border-radius: 14px;
    padding: 22px 22px 0;
    gap: 16px;
    transform: translateX(18px);
  }
  #product-panel.popup[data-open="true"] { transform: none; }
}
/*
 * ON A PHONE IT IS A BOTTOM SHEET. It used to keep `.popup-right`'s phone width
 * (62vw, hung off the right edge), which left a third of a 390 px screen empty
 * beside a column too narrow for its own buttons. Full width, rising from the
 * foot, the piece showing above it -- where a thumb already is.
 */
@media (max-width: 600px) {
  #product-panel.popup {
    --panel-pad: 18px;
    top: auto;
    left: calc(8px + env(safe-area-inset-left));
    right: calc(8px + env(safe-area-inset-right));
    bottom: calc(8px + env(safe-area-inset-bottom));
    width: auto;
    max-height: 74dvh;
    border-radius: 16px;
    padding: 18px 18px 0;
    transform: translateY(26px);
  }
  #product-panel.popup[data-open="true"] { transform: none; }
  #product-panel .panel-thumb { width: 104px; height: 104px; }
  #product-panel .panel-buy h2 { font-size: 21px; }
}

#product-panel.popup { --panel-pad: 22px; overflow-x: hidden; }
#product-panel .panel-close {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 2;
}
#product-panel .panel-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
  padding-right: 0;
}
#product-panel .panel-head { align-items: flex-start; justify-content: flex-start; gap: 12px; padding-right: 44px; }
#product-panel .panel-thumb {
  width: 132px;
  height: 132px;
  border-radius: 12px;
  box-shadow: 0 0 0 1px rgba(var(--neon-rgb), 0.42), 0 0 18px rgba(var(--neon-rgb), 0.14);
}
/* ONE rule between sections. .panel-buy and .panel-main each drew one, which
   stacked into the doubled line under the price. */
#product-panel .panel-buy { gap: 8px; text-align: left; justify-items: start; border-bottom: 0; padding-bottom: 0; }
/* The base `.panel-main` centres its children and its text, and every
   `.panel-section` draws its own TOP border -- which stacked against the rule
   under the price as the doubled line. Both are undone here, for the card. */
#product-panel .panel-main { padding-bottom: 16px; border-bottom: 1px solid rgba(var(--neon-rgb), 0.18); justify-items: stretch; text-align: left; }
#product-panel .panel-section { border-top: 0; padding-top: 0; }
#product-panel .panel-description { margin: 0; max-width: none; }
#product-panel .panel-colourway,
#product-panel #panel-attachments-section { border-bottom-color: rgba(var(--neon-rgb), 0.18); }
#product-panel .panel-buy h2 {
  font-family: var(--display-font);
  font-size: 24px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.18;
  text-align: left;
}
#product-panel .panel-tagline {
  text-align: left;
  color: var(--neon);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-size: 11px;
}
#product-panel .panel-tagline:empty { display: none; }
#product-panel .panel-description { text-align: left; font-size: 14px; line-height: 1.55; color: rgba(242, 245, 247, 0.78); }
#product-panel .panel-buy .panel-price {
  font-size: 26px;
  font-weight: 600;
  color: var(--neon-hot);
  text-align: left;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}
#product-panel .panel-section h3 {
  text-align: left;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 10px;
}
#product-panel .panel-colourway { text-align: left; }
#product-panel .panel-colourway .swatches { justify-content: flex-start; }
#product-panel .panel-note { text-align: left; font-size: 12px; color: var(--muted); }
/* One colourway is not a choice; a lone swatch under a heading is noise. */
#product-panel .panel-colourway:has(.swatch:only-child) { display: none; }

/* Read top to bottom: what it is, colour, what comes with it -- then the commit. */
#product-panel .panel-body { order: 3; }
#product-panel .panel-total { border-top: 0; padding-top: 0; }
#product-panel .panel-total span { color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; font-size: 12px; }
#product-panel .panel-total strong { font-size: 22px; color: var(--neon-hot); }
#product-panel .panel-actions {
  order: 4;
  position: sticky;
  bottom: 0;
  z-index: 1;
  margin: auto calc(-1 * var(--panel-pad)) 0;
  padding: 14px var(--panel-pad) calc(16px + env(safe-area-inset-bottom));
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: 10px;
  /* Opaque enough that copy scrolling UNDER it does not read through the
     buttons (it did, on a phone). The pane's own blur is behind this already. */
  background: rgba(17, 15, 13, 0.94);
  border-top: 1px solid rgba(var(--neon-rgb), 0.30);
  border-bottom: 0;
  box-shadow: 0 -14px 22px -10px rgba(0, 0, 0, 0.55);
}
#product-panel .panel-actions > .primary { margin-top: 0; padding: 14px 12px; font-size: 15px; }

/* The commit block's own furniture, shared by both widths. */
.panel-buy { display: grid; gap: 10px; align-content: start; }
/*
 * Three things carry this panel: the picture, the name and the price. The
 * category and the copy support them and are sized to say so -- the old set
 * ran 22 / 19 / 14 / 13.5, four sizes close enough that nothing led.
 */
.panel-buy h2 {
  margin: 0;
  font-size: 27px;
  line-height: 1.12;
  letter-spacing: -0.01em;
}
.panel-buy .panel-price {
  font-size: 23px;
  font-weight: 600;
  color: var(--ink);
}
/* The copy sits directly under the price, not in a section of its own — it is
   part of what the piece is, and the space beside the thumbnail is where a
   visitor is already looking. */
.panel-buy .panel-tagline { margin: 0; }
.panel-buy .panel-description { margin: 0; }

/* Both ways to buy, side by side beneath the piece. They stack on the narrow
   fallback, where the panel is a column. */
.panel-actions { display: flex; gap: 10px; }
.panel-actions > .primary { flex: 1; }

.panel-body { display: grid; gap: 16px; }

/*
 * The panel is four decisions stacked, each closed by a rule:
 *
 *   what it is   thumbnail + save, title, category, description, price
 *   ---
 *   colour       the last choice before committing
 *   ---
 *   commit       add to cart / buy now
 *   ---
 *   included     what comes with it
 *   ---
 *   margin       so the last line never sits on the edge of a phone
 *
 * Bottom borders only, never top-and-bottom: two adjacent blocks would then
 * draw a pair of lines a padding apart, which reads as a mistake.
 */
/*
 * Centred, and clear of the close button.
 *
 * The close owns the top-right corner (34px, 44px on a touch screen), and the
 * content used to start level with it -- the thumbnail sat in its shadow at
 * the very top of the panel. Padding the top rather than shrinking the copy
 * column means the picture leads the panel with the corner left to the one
 * control that lives there.
 */
.panel-main {
  display: grid;
  gap: 14px;
  align-content: start;
  justify-items: center;
  text-align: center;
  padding-top: 56px;
}

/*
 * `justify-items: center` shrinks every grid child to its content width,
 * which is right for the thumbnail row and wrong for the copy: a paragraph
 * sized to its longest line wraps at a width nobody chose. The copy block
 * spans; only its text is centred.
 */
.panel-buy {
  justify-self: stretch;
  width: 100%;
}

.panel-colourway { text-align: center; }
.panel-colourway .swatches { justify-content: center; }

/*
 * The description is the one thing left ranged left.
 *
 * Centred body copy gives every line a different starting x, so the eye has
 * to hunt for the start of each one -- fine for a title, tiring for four
 * lines of product copy. Titles, price and the swatches centre; the paragraph
 * does not. Say the word if it should.
 */
.panel-description { text-align: left; }

/* The picture of the piece and the one control that saves it, before any copy. */
.panel-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

/*
 * The placement tile does not apply in the shop and product-panel.js sets
 * [hidden] to say so -- but `.place-tile { display: grid }` outranks the bare
 * attribute, so it kept showing anyway. The attribute has to win, and the
 * wrapper has to collapse with it or its padding leaves a gap where a control
 * used to be.
 */
.place-tile[hidden] { display: none; }

/*
 * Same trap, third time: `.panel-thumb { display: block }` outranks the bare
 * `hidden` attribute, so product-panel.js could set hidden on a failed image
 * and the browser still drew its broken-image icon with the alt text spilling
 * into the layout. The attribute has to win.
 */
.panel-thumb[hidden] { display: none; }
.panel-place:has(.place-tile[hidden]) { display: none; }

.panel-buy,
.panel-colourway,
#panel-attachments-section {
  padding-bottom: 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

/* .panel-actions keeps its flex row above; this only closes the block. */
.panel-actions {
  padding-bottom: 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

/* Breathing room at the very bottom. */
.panel-body { padding-bottom: 40px; }

/* Placement: a tile, not a button in the buying stack — it acts on the room,
   not on the order, and standing apart is what keeps that legible. */
.place-tile {
  display: grid;
  justify-items: center;
  gap: 8px;
  width: 104px;
  padding: 16px 10px;
  border: 0;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.03);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.19);
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: box-shadow 0.18s ease, color 0.18s ease, background 0.18s ease;
}
.place-tile svg { width: 26px; height: 26px; }
.place-tile:hover,
.place-tile:focus-visible {
  outline: none;
  color: var(--neon-hot);
  background: rgba(255, 255, 255, 0.07);
  box-shadow: var(--neon-rim-hot);
}

/*
 * Name and wishlist on one line, inside the panel.
 *
 * This heart used to be `.idol-heart`: fixed at `left: 30vw`, floating over
 * the 3D view, and `display: none` below 601px. Two problems fixed by moving
 * it here. It was positioned against a stage width it did not own, so it
 * drifted whenever the panel's width changed; and because `#panel-wishlist`
 * IS this element and there is only one in the markup, hiding it at a
 * breakpoint left those visitors with no wishlist control at all — a bug the
 * old comment described as the heart moving "back into the panel's own action
 * row", which nothing ever did.
 *
 * It now exists at every width, next to the name it saves.
 */
/* The name. The heart moved up beside the thumbnail -- see .panel-head. */
.panel-buy h2 { margin: 0; min-width: 0; }

.panel-heart {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 40px;
  height: 40px;
  margin-top: -2px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--glass-bg);
  box-shadow: var(--neon-rim);
  color: var(--ink);
  cursor: pointer;
  transition: box-shadow 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.panel-heart svg { width: 20px; height: 20px; display: block; }
.panel-heart[hidden] { display: none; }
.panel-heart:hover,
.panel-heart:focus-visible {
  outline: none;
  color: var(--neon-hot);
  box-shadow: var(--neon-rim-hot);
}
.panel-heart[aria-pressed="true"] {
  color: var(--accent-ink);
  background: var(--neon);
}

/* --- product detail card ------------------------------------------------- */

/* The card is named by its product — a name, not a label, so it keeps the
   body face and its case rather than the popups' Cinzel capitals. */
.panel-buy h2 {
  font-family: inherit;
  font-size: 22px;
  letter-spacing: -0.01em;
  line-height: 1.15;
  text-transform: none;
}

/*
 * A thumbnail, not a hero.
 *
 * It shares the top row with the wishlist heart. 162px is set by eye against
 * the live panel: big enough to show the piece, short enough that the title,
 * copy and price still sit above the fold on a landscape phone.
 */
.panel-thumb {
  display: block;
  flex: none;
  width: 162px;
  height: 162px;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 10px;
  margin: 0;
  background: rgba(255, 255, 255, 0.04);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
}

/* An eyebrow, not a second title: small, spaced and quiet, so the name above
   it is unmistakably the heading. */
.panel-tagline {
  margin: 0;
  color: var(--muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}
/* Capped to a comfortable measure. A 59vw panel on a wide monitor otherwise
   runs the copy to 100+ characters a line, which is where reading breaks down
   regardless of how good the type is. */
.panel-description {
  margin: 0 auto;
  color: var(--muted);
  font-size: 13.5px;
  line-height: 1.6;
  max-width: 52ch;
}
.panel-price { margin: 0; font-size: 18px; font-variant-numeric: tabular-nums; }

.panel-section { display: grid; gap: 10px; padding-top: 10px; border-top: 1px solid var(--glass-line); }
.panel-section h3 {
  margin: 0;
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--neon);
  font-weight: 600;
}
.panel-section[hidden] { display: none; }

/*
 * The selected idol, inside an open rasi box.
 *
 * Deliberately reads as SUBORDINATE to the box's copy above it — tighter gap,
 * a rule down the side rather than a full section break, and a name at h3
 * weight rather than the panel's h2. The box is the thing being bought and the
 * idol is one of its contents; styling the two alike would invite the visitor
 * to think Add to cart buys the idol they are reading.
 */
.panel-idol {
  gap: 6px;
  padding: 10px 0 0 12px;
  border-top: 1px solid var(--glass-line);
  border-left: 2px solid var(--neon-hot);
  margin-left: 2px;
}
.panel-idol-eyebrow {
  margin: 0;
  font-size: 11.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--neon-hot);
}
.panel-idol h3 { margin: 0; font-size: 15px; }
.panel-note { margin: 0; font-size: 12px; color: var(--muted); }

.attachments { display: grid; gap: 8px; }
.attachment {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 12px;
  cursor: pointer;
  font-size: 13px;
}
.attachment:hover { border-color: rgba(255, 255, 255, 0.42); }
.attachment input { accent-color: var(--neon); width: 16px; height: 16px; flex: none; }
.attachment-text { display: grid; flex: 1; }
.attachment-text span { color: var(--muted); font-size: 12px; }
.attachment-price { font-variant-numeric: tabular-nums; color: var(--muted); }

.panel-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-top: 12px;
  border-top: 1px solid var(--glass-line);
  font-variant-numeric: tabular-nums;
}
.panel-total strong { font-size: 19px; }

/* `.panel-price-block`, `.panel-actions-row` and `.panel-head` went with the
   2026-08-29 panel rework — price now sits with the name beside the thumbnail,
   the two buy buttons are their own row, and the close button is absolutely
   positioned rather than sharing a header. `.panel-actions` is defined once,
   above, with the rest of the new layout. */
.primary-alt {
  background: rgba(var(--neon-rgb), 0.06);
  color: var(--neon-hot);
  box-shadow: var(--neon-rim);
}
.primary-alt:hover { background: rgba(var(--neon-rgb), 0.12); box-shadow: var(--neon-rim-hot); filter: none; }
.ghost.wide { width: 100%; text-align: center; }
.ghost.icon { font-size: 13px; }
.ghost.is-on { border-color: var(--neon); color: var(--neon-hot); box-shadow: 0 0 16px rgba(255, 255, 255, 0.25); }

/* --- cart ---------------------------------------------------------------- */

/* --- glass ---------------------------------------------------------------
 * The three floating surfaces share one material. Kept as a single rule so
 * they cannot drift apart: the ribbon used to be a plain gradient scrim with
 * no blur at all, which is why it read as a different thing from the panels.
 *
 * `saturate()` alongside the blur is what stops a blurred 3D scene going grey
 * behind the glass — without it the amber and the greenery wash out.
 */
/*
 * The popup's glass, edge light and corner glints — all as *backgrounds*, not
 * pseudo-elements.
 *
 * Every other surface here wears its rim as an `inset: 0` ::before (below).
 * The popup cannot: it is the one surface that scrolls its own content, and an
 * absolutely-positioned child of a scroll container scrolls with that content
 * while sizing to the box at scroll origin. The ring therefore slid up as the
 * visitor scrolled and drew its bottom edge straight through the footer text —
 * which is exactly what the client photographed on a short phone screen.
 *
 * Backgrounds are painted on the border box and stay put no matter how far the
 * content scrolls. The transparent 1.5px border is what gives the edge-light
 * layer a band to show through: `border-box` for the rim and the glints that
 * sit on it, `padding-box` for the fill and the navy heart inside it.
 */
.popup {
  border: 1.5px solid transparent;
  /*
   * EVERY RIM LAYER IS A STRIP, NOT A SHEET. The fill is translucent -- it has
   * to be, the blur behind it is the glass -- so anything painted across the
   * whole border-box shows THROUGH it: the first gold rim tinted the entire
   * pane, and the moving light fanned across it as a bright wedge (looked at,
   * 2026-09-18; the old white rim did the same at 9% and nobody could tell).
   * A masked ::before ring is the usual answer and is ruled out here: these
   * panes scroll their own content and the ring would scroll away with it.
   * So: four strips the width of the border, one per edge, sized and placed so
   * they cover the band and nothing else. The unbroken 1px line round the
   * corners is the box-shadow's; the strips add where the light catches.
   */
  background:
    /* THE MOVING LIGHT -- one glint per edge, run in sequence by rim-sweep so it
       reads as a single light travelling round the pane */
    linear-gradient(90deg, transparent 46%, rgba(255, 240, 205, 1) 50%, transparent 54%)
      var(--sw-top) 0 / 300% 1.5px no-repeat border-box,
    linear-gradient(180deg, transparent 46%, rgba(255, 240, 205, 1) 50%, transparent 54%)
      100% var(--sw-right) / 1.5px 300% no-repeat border-box,
    linear-gradient(90deg, transparent 46%, rgba(255, 240, 205, 1) 50%, transparent 54%)
      var(--sw-bottom) 100% / 300% 1.5px no-repeat border-box,
    linear-gradient(180deg, transparent 46%, rgba(255, 240, 205, 1) 50%, transparent 54%)
      0 var(--sw-left) / 1.5px 300% no-repeat border-box,
    /* THE EDGE LIGHT -- brightest into the top-right and bottom-left corners */
    linear-gradient(90deg, rgba(var(--neon-rgb), 0.34), rgba(var(--neon-rgb), 0.14) 50%, rgba(255, 226, 166, 0.95))
      0 0 / 100% 1.5px no-repeat border-box,
    linear-gradient(180deg, rgba(255, 226, 166, 0.95), rgba(var(--neon-rgb), 0.14) 50%, rgba(var(--neon-rgb), 0.34))
      100% 0 / 1.5px 100% no-repeat border-box,
    linear-gradient(90deg, rgba(255, 226, 166, 0.95), rgba(var(--neon-rgb), 0.14) 50%, rgba(var(--neon-rgb), 0.34))
      0 100% / 100% 1.5px no-repeat border-box,
    linear-gradient(180deg, rgba(var(--neon-rgb), 0.34), rgba(var(--neon-rgb), 0.14) 50%, rgba(255, 226, 166, 0.95))
      0 0 / 1.5px 100% no-repeat border-box,
    /* the pane itself: lighter where the light enters at the top, then the fill */
    radial-gradient(130% 90% at 50% 0%, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0) 55%) padding-box,
    linear-gradient(var(--glass-bg), var(--glass-bg)) padding-box;
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(112%) brightness(0.92);
  backdrop-filter: blur(var(--glass-blur)) saturate(112%) brightness(0.92);
  box-shadow: var(--glass-shadow),
    0 0 0 1px rgba(var(--neon-rgb), 0.46),
    0 0 10px rgba(var(--neon-rgb), 0.22),
    0 0 34px rgba(var(--neon-rgb), 0.10),
    inset 0 1px 0 rgba(255, 255, 255, 0.10);
}
/* Only while OPEN, and never for a visitor who asked for less motion. Parked
   values (115% / -15%) hold a glint just outside its edge until its turn. */
.popup[data-open="true"] { animation: rim-sweep 7s linear infinite; }
@keyframes rim-sweep {
  0%   { --sw-top: 115%; --sw-right: 115%; --sw-bottom: -15%; --sw-left: -15%; }
  25%  { --sw-top: -15%; --sw-right: 115%; --sw-bottom: -15%; --sw-left: -15%; }
  50%  { --sw-top: -15%; --sw-right: -15%; --sw-bottom: -15%; --sw-left: -15%; }
  75%  { --sw-top: -15%; --sw-right: -15%; --sw-bottom: 115%; --sw-left: -15%; }
  100% { --sw-top: -15%; --sw-right: -15%; --sw-bottom: 115%; --sw-left: 115%; }
}
@media (prefers-reduced-motion: reduce) {
  .popup[data-open="true"] { animation: none; }
}

/*
 * The edge light. A ring of gradient one pixel wide, drawn on ::before and
 * masked so only the border band survives: brightest sweeping through the
 * top-left and bottom-right corners, near-dark on the runs between — the way
 * light actually catches a pane's edge, and exactly the reference's look.
 * A border property cannot do this; a border is one colour all the way round.
 */
/*
 * The edge light, for surfaces that do NOT scroll their own content.
 *
 * ⚠ Before adding a surface to this list, check whether it can ever scroll
 * internally. An absolutely-positioned child of a scroll container scrolls
 * *with* the content while sizing to the box at scroll origin, so the ring
 * slides out of place the moment the surface overflows — it drew its bottom
 * edge through the cart's footer text on a short phone screen (fixed
 * 2026-08-29). The four below are safe because each is sized by its content
 * and never gains its own scrollbar.
 *
 * A scrolling surface wants the background-layer version instead — transparent
 * border, gradient painted into the border band — as `.popup` does above.
 * Backgrounds are painted on the border box and stay put.
 */
.nav-tile::before,
.landing-card::before,
.cart-line::before,
.saved-row::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  /* 65deg runs bottom-left → top-right, so the bright ends of the sweep land
     on those two corners — the client asked the light moved there. */
  background: var(--rim-gradient);
  opacity: 0.62;                 /* quieter than the pane they sit in */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* The rim ring needs a positioning context on everything that wears it.
   NOT .popup: it is position:fixed already, and this rule sits later in the
   file — relative here would silently win and drop every popup out of its
   fixed placement into the overlay's flow. */
.nav-tile, .landing-card, .cart-line, .saved-row { position: relative; }





/*
 * No backdrop-filter — Firefox with the pref off, and older WebKit. There is
 * nothing blurring the scene behind the glass, so translucency would leave
 * text sitting directly on a moving render. Go nearly opaque instead: less
 * pretty, still readable, which is the right trade.
 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  /* Only the fill layer goes opaque; the rim keeps its own border-box layer,
     or a browser without backdrop-filter would lose the edge light too. */
  .popup {
    background:
      linear-gradient(var(--glass-bg-solid), var(--glass-bg-solid)) padding-box,
      var(--rim-gradient) border-box;
  }
  .banner-button, .back-button, .landing-card { background: var(--glass-bg-solid); }
}
/*
 * `overflow-x: hidden` is load-bearing: the line used to be a flex row that
 * could out-grow the card, and the styled horizontal scrollbar it produced is
 * what the client screenshotted as a mystery blue slider under the first line.
 */
.cart-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; overflow-x: hidden; flex: 1; }
/* Each line is its own glass strip — the reference image is a board of lit
   tiles, so the rows read as tiles too, not as ruled entries in a ledger. */
.cart-line {
  display: grid;
  gap: 4px;
  font-size: 15px;
  padding: 12px 14px;
  border-radius: 8px;
  background:
    radial-gradient(120% 100% at 50% 0%, rgba(255, 255, 255, 0.07), transparent 60%),
    rgba(255, 255, 255, 0.025);
}
/*
 * Columns, not a flex row: the name is the only elastic part, and `min-width: 0`
 * on it is what lets a long product name ellipsize instead of shoving the price
 * and the ✕ out through the card wall. The thumbnail column exists only on rows
 * that actually have an image (cart.js tags them) — a hidden <img> used to hold
 * an empty 48px slot that indented every imageless line.
 */
.cart-line-main {
  display: grid;
  /* The picture slot always exists — cart.js renders a placeholder tile when
     a line has no image — so one template serves every row and the names
     stay in a column. */
  grid-template-columns: 64px minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 14px;
}
.cart-line-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16px; font-weight: 500; }
.cart-line-thumb {
  width: 64px;
  height: 64px;
  object-fit: cover;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.04);
  box-shadow: inset 0 0 0 1px rgba(var(--neon-rgb), 0.30);
  flex-shrink: 0;
}
/* The imageless slot: same tile, a faint diamond resting in it. */
.cart-line-thumb-empty {
  display: grid;
  place-items: center;
  color: rgba(255, 255, 255, 0.34);
}
.cart-line-thumb-empty svg { width: 26px; height: 26px; }
.cart-sub {
  display: flex;
  justify-content: space-between;
  padding-left: 32px;
  font-size: 12.5px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.cart-dot { width: 22px; height: 22px; border-radius: 6px; border: 1px solid var(--line); flex: none; }
.cart-qty, .cart-line-price { color: var(--muted); font-variant-numeric: tabular-nums; }
.cart-line-price { color: var(--ink); font-weight: 600; min-width: 72px; text-align: right; }
.cart-qty { display: inline-flex; align-items: center; gap: 8px; }
.cart-qty > span { min-width: 26px; text-align: center; font-variant-numeric: tabular-nums; }
/* Same trick as the swatches: a 22px control in a tight cart line, with a
   44px target over it. Making these physically bigger would break the line. */
.cart-step, .cart-remove { position: relative; }
.cart-step::after, .cart-remove::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}

.cart-step svg, .cart-remove svg { width: 12px; height: 12px; display: block; }
.popup-close svg { width: 13px; height: 13px; display: block; }
.saved-remove svg { width: 12px; height: 12px; display: block; }

.cart-step, .cart-remove {
  border: 1px solid var(--line);
  background: transparent;
  color: inherit;
  border-radius: 8px;
  width: 30px; height: 30px;     /* was 24: visibly a button now, 44 to the finger either way */
  /* place-items, not line-height: −, + and ✕ have different metrics, and
     text baselines put each of them somewhere slightly different. A grid
     centres the glyph as a box and all three land identically. */
  display: inline-grid;
  place-items: center;
  padding: 0;
  line-height: 1;
  cursor: pointer;
}
.cart-step:hover, .cart-remove:hover { border-color: var(--neon-hot); color: var(--neon-hot); box-shadow: 0 0 10px rgba(var(--neon-rgb), 0.38); }
.cart-remove { color: var(--muted); flex: none; }
.primary.wide { width: 100%; text-align: center; margin-top: 12px; }
.primary.wide { padding: 14px 22px; font-size: 16px; }
.cart-empty[hidden] { display: none; }
.cart-empty-title, .cart-empty-hint { margin: 0; }
.cart-empty-title { color: var(--ink); font-size: 16px; }
.cart-empty-hint { color: var(--muted); font-size: 13px; }
.cart-empty.cart-error .cart-empty-title { color: var(--warn); }
.cart-empty.cart-error svg, .cart-empty.cart-error .cart-empty-hint { display: none; }
/* Nothing to total, nothing to show a total of. */
.cart[data-empty="true"] .cart-foot { display: none; }
.cart-foot strong { font-size: 20px; color: var(--neon-hot); letter-spacing: 0.01em; }
.cart-foot span { color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; font-size: 12px; align-self: center; }
.cart-foot { display: flex; justify-content: space-between; padding-top: 12px; border-top: 1px solid var(--glass-line); font-variant-numeric: tabular-nums; }
.cart-note { margin: 0; color: var(--muted); font-size: 12px; text-align: center; letter-spacing: 0.03em; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }

.ghost {
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.03);
  color: inherit;
  border-radius: 10px;
  padding: 8px 12px;
  font: inherit;
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.ghost:hover,
.ghost:focus-visible {
  outline: none;
  border-color: var(--neon-hot);
  color: var(--neon-hot);
  box-shadow: 0 0 14px rgba(var(--neon-rgb), 0.30);
}

/* --- no-WebGL fallback --------------------------------------------------- */

[data-maya-fallback] {
  list-style: none;
  margin: 0;
  padding: 24px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 14px;
  align-content: center;
  height: 100%;
}
[data-maya-fallback] li {
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 18px;
  background: rgba(255, 255, 255, 0.03);
  display: grid;
  gap: 8px;
}
[data-maya-swatch] { width: 44px; height: 44px; border-radius: 12px; display: block; }
[data-maya-fallback] span:last-child { color: var(--muted); font-size: 13px; }

@media (max-width: 720px) {
  /* The bottom padding still has to clear the mute button in the corner — this
     rule used to restate all four sides and silently dropped that clearance,
     which put the button straight on top of the colourway swatches. */
  .details {
    max-width: none;
    padding:
      18px
      calc(20px + env(safe-area-inset-right))
      calc(18px + var(--audio-h))
      calc(20px + env(safe-area-inset-left));
  }
  .ribbon { gap: 12px; padding: calc(14px + env(safe-area-inset-top)) calc(20px + env(safe-area-inset-right)) 14px calc(20px + env(safe-area-inset-left)); }
  .hint { padding: 0 calc(20px + env(safe-area-inset-right)) 0 calc(76px + env(safe-area-inset-left)); }
  .audio-toggle { left: calc(20px + env(safe-area-inset-left)); }
  .corner-toggle { right: calc(20px + env(safe-area-inset-right)); }

  /* The banner is shorter here, so the popups hang higher. One variable, so
     the four of them cannot disagree about where "under the banner" is. */
  :root { --banner-h: 60px; }

  .panel-buy h2 { font-size: 19px; }
  .panel-description { font-size: 13px; }
  .panel-actions { flex-direction: column; gap: 8px; }
  .panel-actions-row { flex-direction: column; }
}

/* ==========================================================================
   Mobile — orientation and fullscreen

   Rooms used to be a third problem here: eight buttons that would not fit
   across a phone, collapsed behind a toggle into a fixed sheet. Navigation is
   a popup at every width now, so that whole fork — and the toggle, the sheet
   and their breakpoint — is gone.
   ========================================================================== */

@media (max-width: 900px) {
  .ribbon { gap: 10px; padding: calc(10px + env(safe-area-inset-top)) calc(14px + env(safe-area-inset-right)) 10px calc(14px + env(safe-area-inset-left)); }
  :root { --banner-h: 54px; }
  /* Type properties dropped with the text span; only the glyph is left, and it
     is sized by .wordmark-glyph. The shade is the ribbon's corner scrim, which
     tightens with the ribbon's own padding — nothing to set on the mark. */

  /*
   * Leave the piece somewhere to be.
   *
   * 900px, not 720px: a phone held landscape is ~844px wide, where a 380px card
   * is nearly half the screen and a selected piece has nowhere to move to.
   * product-panel.js measures the real geometry and tells the hall where the
   * middle of the remaining strip is, so these numbers and the 3D framing
   * cannot disagree.
   */
  .popup-right { right: calc(14px + env(safe-area-inset-right)); width: min(320px, 46vw); }
  .popup { padding: 14px; border-radius: 8px; }
  .popup-center { max-height: 84dvh; }
  .nav-list { grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }

  /* The hint sits under a banner that is now shorter, and clear of the mute
     button in the corner it would otherwise run into. */
  .hint { padding: 0 calc(14px + env(safe-area-inset-right)) 0 calc(64px + env(safe-area-inset-left)); font-size: 11px; }
  .audio-toggle { left: calc(14px + env(safe-area-inset-left)); }
  .corner-toggle { right: calc(14px + env(safe-area-inset-right)); }
  /*
   * What the detail strip must leave clear above the viewport's bottom edge:
   * the button's 20px offset, its 44px height, and 10px of air. Undersized,
   * the swatches end up underneath the button and unreachable — the strip is
   * the one thing on screen the button shares a corner with.
   */
  :root { --audio-h: 54px; }
}

/* ==========================================================================
   Touch targets

   Everything in the overlay that a finger has to find is at least 44x44 here.
   Applied on coarse pointers *and* at narrow widths: the width test is what a
   desktop browser resized to a phone viewport matches, which is how this gets
   tested in practice, and a 44px control on a narrow desktop window costs
   nothing anyway.

   Deliberately not applied on a wide fine-pointer screen — a mouse does not
   need it, and it would inflate the chrome the design was tuned at.
   ========================================================================== */

@media (pointer: coarse), (max-width: 900px) {
  .nav-tile,
  .primary,
  .ghost {
    min-height: 44px;
  }
  .ghost.popup-close {
    min-width: 44px;
  }

  /*
   * The client's second pass inverted the first: full-size glyphs in SMALLER
   * buttons — 31px, which is 0.7 of the 44px they were. The 44px touch target
   * survives as an invisible ::after over each button, the same trick the
   * swatches and cart steppers already use, so the banner gets lighter without
   * anything actually getting harder to hit.
   */
  .banner-button.icon,
  .audio-toggle,
  .corner-toggle {
    width: 31px;
    height: 31px;
    min-width: 31px;
    min-height: 31px;
    padding: 0;
  }
  /*
   * `:not(.skip-intro-button)` is load-bearing, for the reason the note below
   * gives about the mute button. This rule has the same specificity as
   * `.skip-intro-button` (one class each) and sits ~1600 lines later, so it
   * wins on order: the skip button lost `position: fixed`, dropped into the
   * ribbon flex flow, and appeared at the top LEFT instead of bottom right.
   * Same trap, second victim.
   */
  .banner-button:not(.skip-intro-button) {
    min-height: 31px;
    position: relative;
  }
  .skip-intro-button { min-height: 31px; }
  .banner-button:not(.icon) { padding: 6px 13px; }
  /* NOT position:relative here — the button is position:fixed (bottom-left),
     which is already a containing block for the ::after hit area. Relative
     would win on order, drop it into the overlay's flex flow, and park the
     mute button directly under the logo — which is exactly what it did. */
  .banner-button::after,
  .audio-toggle::after {
    content: '';
    position: absolute;
    top: 50%; left: 50%;
    width: 44px; height: 44px;
    transform: translate(-50%, -50%);
  }
  /* Glyphs back at their desktop size — the drawing is the point, not the pill. */
  .banner-button svg { width: 19px; height: 19px; }
  .audio-toggle svg { width: 19px; height: 19px; }
  .wordmark-glyph { height: 30px; width: auto; }
  /* The badge rides a smaller button now. */
  .cart-count { top: -6px; right: -6px; }

  /* The banner's buttons are 31px now, so the bar itself is lean again —
     `--banner-h` is what the product side panel hangs below. */
  :root { --banner-h: 56px; }

  /* The wordmark is a button too, and at 20px tall it was the smallest target
     in the banner. Padded rather than resized: it must still read as the logo.
     Padding is NOT reset here any more — it is what gives the mark's shade pool
     its size, and flattening it to `0 2px` put the gradient hard against the
     glyph. The ::after below is the touch target and is independent of it. */
  /* The disc is already comfortably over the 44px touch target, so there is
     nothing to pad out here any more — and shrinking it to 31px like the other
     icon buttons would crop the mark inside its own circle. It stays at the
     target size, the one control in the banner that does, and the glyph keeps
     the same proportion of it as on desktop. */
  .wordmark { width: 44px; height: 44px; }
  .wordmark-glyph { height: 23px; }

  /* Cart and panel steppers keep their 44px ::after targets from above; these
     are the ones with room to simply be bigger. */
  .swatch { width: 30px; height: 30px; }
}

/* --- portrait cover ------------------------------------------------------ */

.rotate-cover {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: 32px;
  text-align: center;
  background: #08090a;
  color: var(--ink);
}
.rotate-cover[hidden] { display: none; }
.rotate-inner { max-width: 22rem; }
.rotate-cover h2 { margin: 18px 0 8px; font-size: 20px; font-weight: 600; }
.rotate-cover p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }

/* A phone rotating itself, which says the thing faster than the words do. */
.rotate-icon {
  display: inline-block;
  font-size: 46px;
  line-height: 1;
  color: var(--neon);
  animation: rotate-hint 2.4s ease-in-out infinite;
}
@keyframes rotate-hint {
  0%, 40% { transform: rotate(90deg); }
  60%, 100% { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .rotate-icon { animation: none; transform: rotate(90deg); }
}

/* --- the fullscreen invitation ------------------------------------------ */

/* Sits above the hint, which is also bottom-centre — the two are briefly on
   screen together before the first interaction fades the hint out. */
.fullscreen-prompt {
  position: fixed;
  left: 50%;
  bottom: calc(58px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 14px;
  max-width: calc(100vw - 24px);
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  pointer-events: auto;
}
.fullscreen-prompt[hidden] { display: none; }
.fullscreen-prompt p { margin: 0; font-size: 13px; white-space: nowrap; }
.fullscreen-prompt-actions { display: flex; gap: 8px; }
.fullscreen-prompt button { padding: 7px 12px; font-size: 12.5px; white-space: nowrap; }

@media (max-width: 480px) {
  .popup-right { width: min(300px, 62vw); }
  .fullscreen-prompt { flex-direction: column; gap: 8px; border-radius: 14px; }
  .fullscreen-prompt p { white-space: normal; text-align: center; }
}

/* --- the shop's checkout screen ------------------------------------------- */

/* The checkout used to be paper — a white card on a cream ground — which read
   as a different site bolted onto the end of the showroom. It is the same dark
   glass as everything else now, over the same neon field the landing uses. */
.checkout-screen {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 24px;
  color: var(--ink);
  background:
    radial-gradient(circle at center, rgba(255, 255, 255, 0.11) 1px, transparent 1.4px) 0 0 / 34px 34px,
    radial-gradient(130% 90% at 50% 0%, rgba(120, 126, 132, 0.55), transparent 62%),
    #08090a;
}

.checkout-card {
  width: min(520px, 92vw);
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  border-radius: 18px;
  padding: 28px 30px;
  box-shadow: var(--glass-shadow), var(--neon-rim);
}

.checkout-card h1 { margin: 4px 0 18px; font-family: var(--display-font); font-size: 26px; letter-spacing: 0.06em; }

.checkout-lines {
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
  display: grid;
  gap: 8px;
}

.checkout-lines li {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 14px;
  padding-bottom: 8px;
  border-bottom: 1px dashed var(--glass-line);
}

/* Same picture-slot contract as the cart panel (.cart-line-thumb), scaled
   down for the checkout's more compact line list. */
.checkout-line-thumb {
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.04);
  box-shadow: inset 0 0 0 1px rgba(var(--neon-rgb), 0.30);
  flex-shrink: 0;
}
.checkout-line-thumb-empty {
  display: grid;
  place-items: center;
  color: rgba(255, 255, 255, 0.34);
}
.checkout-line-thumb-empty svg { width: 18px; height: 18px; }

.checkout-line-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.checkout-line-price { flex-shrink: 0; font-variant-numeric: tabular-nums; }

.checkout-empty { color: var(--muted); font-size: 14px; }

.checkout-total {
  display: flex;
  justify-content: space-between;
  font-size: 16px;
  margin: 12px 0 18px;
}

.checkout-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
