/* ─────────────────────────────────────────────────────────────────────────────
   configurator.css — layout for the configurator (index.html) only

   Two columns: the 3D stage takes what is left, the options rail is fixed. The
   widgets inside both come from components.css.

   Structure:
     body ─ flex column
       ├── .app-header    fixed height
       └── .configurator  fills the rest, 2-column grid
            ├── .stage    the WebGL canvas
            └── .rail     the four option groups + try-on
───────────────────────────────────────────────────────────────────────────── */

.configurator {
  flex: 1;          /* claim all the vertical space the header leaves */
  display: grid;
  grid-template-columns: 1fr var(--rail-width);

  /* The positioning context for the tuning drawer and its scrim, which cover
     this whole area — stage and rail both. They are position:absolute, so they
     are out of flow and add no track to the grid above. */
  position: relative;

  /* Essential: grid items default to a min-content minimum, so without this a
     large canvas could force the grid wider than the viewport and break the
     fixed-height layout. */
  min-height: 0;
  overflow: hidden;
}

/* ── The stage ──────────────────────────────────────────────────────────── */
.stage {
  position: relative;   /* positioning context for the hint */
  background: var(--bg);

  /* BOTH are needed, and the missing one caused a real bug. A grid item's
     automatic minimum size is its MIN-CONTENT, not zero — so a stage holding a
     canvas refused to shrink past that canvas's intrinsic height, and when the
     options sheet grew taller than the space left, the grid overflowed
     downwards instead: the sheet slid off the bottom of the window and the
     layout looked broken. min-height:0 lets the row give way. */
  min-width: 0;
  min-height: 0;
}

#viewer-canvas {
  display: block;   /* kills the inline-element baseline gap under the canvas */
  width: 100%;
  height: 100%;

  /* The pointer belongs to the orbit controls, not to text selection: without
     this a drag across the ring selects the hint underneath it. */
  touch-action: none;
  user-select: none;
}

.stage-hint {
  position: absolute;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--muted);
  pointer-events: none;   /* never intercept a drag meant for the ring */
  z-index: var(--z-panel-ui);
}

/* ── The frame readout (?tune=1) ────────────────────────────────────
   Top-right of the stage: .stage-toggle owns the top-left and .stage-hint the
   bottom edge, so this is the corner that is free at every width.

   It does NOT take pointer events. It sits over the canvas, and a diagnostic
   that swallowed a drag meant for the ring would be worse than no diagnostic —
   the same rule .stage-hint follows, and the same bug the try-on's closed scrim
   once had.

   Tabular figures matter more than they look: without them the box changes
   width as the digits change, five times a second, which reads as the meter
   flickering rather than as the number moving. */
.fps-meter {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: var(--z-panel-ui);

  padding: 6px 10px 5px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  box-shadow: var(--shadow-sm);

  pointer-events: none;
  user-select: none;
}

.fps-line {
  display: flex;
  align-items: baseline;
  gap: 4px;
}

.fps-value {
  font-size: 1rem;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  min-width: 2.1ch;          /* holds its width from 9 to 144 */
  text-align: right;
}

.fps-unit {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
}

.fps-graph {
  display: block;
  width: 64px;
  height: 20px;
  margin-left: 7px;
  align-self: center;
}

.fps-detail {
  margin: 3px 0 0;
  font-size: 0.6rem;
  letter-spacing: var(--tracking-wide);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
}

/* The state colours are deliberately OUTSIDE the jewellery palette. Gold means
   "the option you chose" everywhere else on this page, and a meter that went
   gold when the frame rate was healthy would be borrowing that word for
   something else entirely. These are a warning language, and they are only ever
   seen by whoever added ?tune=1 to the URL. */
.fps-meter[data-state='warn'] .fps-value { color: #be8a30; }
.fps-meter[data-state='bad']  .fps-value { color: #b24a3a; }

@media (max-width: 820px) {
  /* Smaller, and clear of the header's own edge on a narrow screen. */
  .fps-meter { top: 10px; right: 10px; padding: 5px 8px 4px; }
  .fps-graph { width: 48px; height: 18px; }
}

/* ── The turntable's switch ─────────────────────────────────────────
   Top-left of the stage, matching where the try-on floats its own on-picture
   controls. NOT bottom-left, which is the same edge as .stage-hint: the hint is
   centred, so at 360px the two would overlap, and the fix would be a media
   query moving one of them for no reason other than that it was in the way.

   Gold is reserved for ON, which is the same is-active language the option
   tiles use, so "this is the state you chose" looks the same everywhere on the
   page. Hover deliberately does NOT go gold, though every other control here
   does: a hovered OFF button that turns gold is indistinguishable from an ON
   one, and the pointer is on it exactly when the customer is deciding. It
   darkens the ink instead. */
/* The stage's controls share one corner, in a row, so a second pill lands
   beside the first rather than on top of it. The wrapper owns the position;
   the pills own their look. */
.stage-controls {
  position: absolute;
  top: 16px;
  left: 16px;
  z-index: var(--z-panel-ui);

  display: flex;
  align-items: center;
  gap: 8px;
}

.stage-toggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px 7px 11px;

  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--muted);

  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;

  box-shadow: var(--shadow-sm);
  transition: color var(--transition-fast) var(--ease),
              border-color var(--transition-fast) var(--ease),
              background var(--transition-fast) var(--ease);
}

.stage-toggle:hover {
  background: var(--surface-alt);
  color: var(--ink);
}

.stage-toggle.is-on {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}

.stage-toggle.is-on:hover {
  border-color: var(--accent);
  background: var(--surface);
  color: var(--accent);
}

/* The icon turns while the turntable does, at the ring's own 6°/s — a minute
   for one revolution, the same as the piece behind it. Slow enough that it
   reads as "still going" rather than as a spinner claiming the page is busy.

   Purely decorative, and the class is what states it: a reduced-motion machine
   turns this off in base.css along with every other transition, while the ring
   itself answers to VIEWER.respectReducedMotion, which is a separate decision
   and deliberately defaults to false. */
.stage-toggle.is-on .stage-toggle-icon {
  animation: stage-toggle-spin 60s linear infinite;
}

/* The copy button's one moment of feedback: it goes to the chosen-option gold
   and says "Copied" for as long as main.js keeps the class on it. The same
   look as a toggle that is on, because for that second and a half it IS a
   state — the link is on the clipboard — and not a hover. */
.stage-toggle.is-done {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}

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

@media (prefers-reduced-motion: reduce) {
  .stage-toggle.is-on .stage-toggle-icon {
    animation: none;
  }
}

/* ── Loading the stage ──────────────────────────────────────────────────────
   An empty canvas that later pops a fully-formed ring into existence reads as a
   bug: for a second or two the page looks broken rather than busy. So a veil
   covers the canvas from the first paint and fades once there is a ring under
   it.

   IT USED TO BLUR WHAT WAS BEHIND IT, with backdrop-filter: blur(18px), so that
   each piece was on screen as it arrived — merely out of focus — and lifting
   the veil sharpened what was already there rather than revealing something
   new. It was reported as too much, and it was: an 18px blur over a mostly
   empty cream stage does not read as a ring coming into focus, it reads as a
   smear, and the pieces it was meant to be softening are barely there yet.

   The tint is raised from 72% to 88% to take its place. That is the same idea
   with a simpler mechanism — the stage is plainly BUSY rather than plainly
   broken — and it costs the compositor nothing, where a full-stage
   backdrop-filter is a real per-frame cost on exactly the frames where the page
   is already loading three GLBs and a 7MB HDRI. */
.stage-loading {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;

  background: color-mix(in srgb, var(--bg) 88%, transparent);

  transition: opacity var(--transition-slow) var(--ease);
  z-index: var(--z-loading);
}

/* Faded out once the ring is mounted and drawn. pointer-events matters as much
   as the opacity — the element still covers the canvas after fading, and would
   otherwise swallow every drag meant for the orbit controls. */
.stage-loading.is-ready {
  opacity: 0;
  pointer-events: none;
}

/* ── The options rail ───────────────────────────────────────────────────────
   Two parts: a scrolling list of options, and a footer pinned beneath it. The
   footer used to be `position: sticky` INSIDE the scrolling column, which put
   it on top of the last row of labels whenever the content came within a few
   pixels of the height available — and on a laptop it always did. Taking it out
   of the scroll area entirely means it can never overlap anything, and "Try it
   on" is on screen whatever the window height. */
.rail {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-left: 1px solid var(--line);
  min-height: 0;      /* lets the scrolling child actually shrink */
}

.rail-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;

  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px 20px 18px;
}

.rail-intro h1 {
  font-size: var(--text-xl);
  margin-bottom: 6px;
}

.rail-intro p {
  font-size: var(--text-sm);
  color: var(--muted);
  margin-top: 2px;
}

.option-group {
  border-top: 1px solid var(--line-soft);
  padding-top: 12px;
}

/* ── Rail footer ────────────────────────────────────────────────────────────
   Outside the scroll area, so the try-on button is always reachable and never
   sits over the options. */
.rail-footer {
  flex: 0 0 auto;
  padding: 14px 20px 18px;
  border-top: 1px solid var(--line);
  background: var(--surface);
}

/* The current ring in words. Small and quiet: it confirms the choices rather
   than competing with them, and it is the same line the try-on shows over the
   video so the two pages agree on what the ring is called. */
.summary {
  font-size: var(--text-xs);
  color: var(--muted);
  margin-bottom: 10px;
  line-height: 1.4;
}

.note {
  margin-top: 10px;
}

/* ── The sheet toggle ───────────────────────────────────────────────────────
   Desktop has room for the rail and the ring at once, so there is nothing to
   collapse and no button. It appears only where the two compete. */
.rail-toggle {
  display: none;
}

/* ── The tuning drawer stands IN the rail's column ──────────────────────────
   It does not float over the ring and there is nothing dimmed behind it. The
   panel is for judging what is on the stage, so covering or darkening the stage
   is the one thing it must not do — and a scrim would additionally stop the
   customer turning the ring while tuning it, which is most of the job.

   So it takes the rail's exact width and the rail steps aside for the length of
   its visit. The grid column is declared by `grid-template-columns`, not by the
   rail, so hiding the rail leaves the track standing at --rail-width and the
   stage never changes size — no resize, no re-frame, no jump in the view you
   were looking at.

   Only above the phone breakpoint. Below it the rail is a bottom sheet and
   there is no column to stand in, so the drawer keeps the shared behaviour —
   still without a scrim. */
@media (min-width: 821px) {
  .configurator > .drawer {
    width: var(--rail-width);
    max-width: none;

    /* Nothing behind it any more: it sits in a column of its own, against the
       page edge, with a border. A drop shadow is for something that floats. */
    box-shadow: none;
  }

  /* `.ui-tune` as well as `.drawer-open`, so the rail can only step aside when
     there is something to step aside FOR. Without the flag the drawer is
     display:none (css/presentation.css), and a drawer opened anyway — from the
     console, or by some later code path that does not check — would then hide
     the rail and leave an empty column beside the ring. */
  body.ui-tune.drawer-open .rail {
    display: none;
  }
}

/* ── Narrow screens ─────────────────────────────────────────────────────────
   The rail becomes a sheet beneath the stage rather than a column beside it,
   and the customer can put it away.

   The rows are `1fr auto`, NOT a fixed split: the stage takes whatever the
   sheet is not using, so collapsing the options grows the ring with no second
   rule to keep in step. Capping .rail-scroll is then the only number that
   decides how much of the screen the options may take. */
@media (max-width: 820px) {
  :root {
    /* Every pixel the sheet takes is a pixel of ring. */
    --disc-image:  62px;
    --disc-swatch: 74px;

    /* The sheet's share of the window, and the fixed height of the toggle plus
       the footer that lives inside it. */
    --sheet-max:    48dvh;
    --rail-chrome:  124px;
  }

  .configurator {
    grid-template-columns: 1fr;

    /* minmax(0, 1fr), not 1fr — same reason as .stage's min-height above. The
       stage yields to the sheet rather than the pair of them overflowing the
       window, which is what made a narrow window on a desktop look glitchy. */
    grid-template-rows: minmax(0, 1fr) auto;
  }

  .rail {
    border-left: none;
    border-top: 1px solid var(--line);
  }

  .rail-intro {
    display: none;   /* the heading costs a phone screen more than it explains */
  }

  /* HOW BIG THE SHEET MAY GET — the one number that decides the split.
     
     Capped on the WHOLE rail, not just its scrolling part. Capping the scroll
     area alone left the toggle and the footer on top of it as fixed pixels, so
     the shorter the window the larger the sheet's real share: at 360×560 the
     options took 65% of the screen and the ring got a quarter.
     
     The scroll area is then given that cap minus the chrome above and below it,
     so the two agree and the collapse animation has a value to run from. --rail
     -chrome is measured, not guessed: toggle ~41px + footer ~83px. If either
     changes shape, change it here. */
  .rail {
    max-height: var(--sheet-max);
  }

  .rail-scroll {
    max-height: calc(var(--sheet-max) - var(--rail-chrome));
    transition: max-height var(--transition-base) var(--ease),
                padding var(--transition-base) var(--ease);
  }

  .rail-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 7px;

    width: 100%;
    padding: 13px 16px;
    flex: 0 0 auto;

    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--muted);
    background: var(--surface);
  }

  .rail-toggle-chevron {
    transition: transform var(--transition-base) var(--ease);
  }

  /* ── Collapsed ────────────────────────────────────────────────────────────
     max-height rather than display:none, so the sheet slides instead of
     vanishing — and so the grid's `auto` row shrinks smoothly, which is what
     grows the stage. The padding has to animate with it or a 34px band of
     white is left behind. */
  .options-hidden .rail-scroll {
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    overflow: hidden;
  }

  .options-hidden .rail-toggle-chevron {
    transform: rotate(180deg);
  }

  /* With the ring full-screen, the written summary is redundant — it is
     describing something the customer is looking at. The button stays. */
  .options-hidden .summary {
    display: none;
  }

  .rail-footer {
    padding: 12px 16px 16px;
  }
}

/* Narrow: the pill becomes its icon. The word is worth the space on a laptop
   and not on a phone, where it sits over a stage barely wider than the ring —
   and the icon is the half that says what the control does while it is doing
   it, since it turns. aria-pressed and the title carry the name for anyone who
   cannot see the shape. */
@media (max-width: 520px) {
  .stage-toggle {
    padding: 8px;
    border-radius: 50%;
  }

  .stage-toggle-text {
    display: none;
  }
}

/* On a touch screen there is no scroll wheel to zoom with. */
@media (hover: none) {
  .stage-hint {
    font-size: 0;   /* replaced wholesale by the ::after below */
  }

  .stage-hint::after {
    content: 'Drag to rotate · pinch to zoom';
    font-size: var(--text-xs);
  }
}
