/* ─────────────────────────────────────────────────────────────────────────────
   components.css — the shared widgets, used by both pages

   Each block below is one self-contained control. They share the tokens but
   nothing else, so a component can be moved or removed without touching the
   others. Page-specific layout lives in configurator.css and layout.css.

   Contents: header · brand · status dot · option buttons · buttons ·
             section titles · sliders · toggles · stat rows · hints
───────────────────────────────────────────────────────────────────────────── */

/* ── App header ─────────────────────────────────────────────────────────── */
.app-header {
  flex-shrink: 0;   /* never let the header compress when the main area fills */
  height: var(--header-height);
  padding: 0 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  z-index: var(--z-header);
}

/* ── Brand ──────────────────────────────────────────────────────────────── */
.logo {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink);
}

/* The second half of the wordmark, in gold. */
.logo span {
  color: var(--accent);
}

.header-link {
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--muted);
  transition: color var(--transition-fast) var(--ease);
}

.header-link:hover {
  color: var(--ink);
}

/* ── Status indicator ───────────────────────────────────────────────────── */
#status {
  font-size: var(--text-sm);
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 8px;

  /* Capped and clipped to one line. A camera failure puts a browser's own
     exception message in here, and those run long enough to wrap the header
     onto three lines and shove the gear off the edge. The full text is in the
     overlay over the video, which has room for it. */
  min-width: 0;
  max-width: 42ch;
}

#status span {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Idle by default; .active is toggled from setStatus() in js/ui/controls.js. */
.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--line);
  transition: background var(--transition-base);
}

.dot.active {
  background: var(--accent);
}

/* ── Section titles ─────────────────────────────────────────────────────── */
.section-title,
.option-title {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-wider);
  color: var(--muted);
  text-transform: uppercase;
  margin-bottom: 10px;
}

/* ── Option tiles ───────────────────────────────────────────────────────────
   The configurator's four groups. Generated in JS from the catalogues in
   js/config.js: a <button class="option"> holding a circular .option-disc and
   a .option-text label beneath it.

   WHY CIRCLES WITH THE NAME UNDERNEATH
   ────────────────────────────────────
   The choice is between physical things, and a photograph of a pavé shank
   answers "what is this?" faster than the word "Diamond" does. The name stays
   below because a picture of a band and a picture of a mounting look alike at
   this size, and because the summary line and the URL both speak in names.

   A circle rather than a square: these are crops of a round object photographed
   from different distances, and a circular mask hides the fact that the two
   sets are framed quite differently. It also stops the tiles reading as a grid
   of cards competing with the ring itself. */
.option-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.option {
  /* Tiles share the row evenly, so two options are two half-width columns and
     three are thirds — the disc is a fixed size and centres within whatever
     that comes to, which keeps the rows visually aligned down the rail. */
  flex: 1 1 0;
  min-width: 0;

  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding: 2px 0;

  background: none;
  border: none;
  font-size: var(--text-xs);
  color: var(--muted);
  text-align: center;
  transition: color var(--transition-fast) var(--ease);
}

/* ── The disc ─────────────────────────────────────────────────────────────── */
.option-disc {
  display: block;
  width: var(--disc-image);
  height: var(--disc-image);
  border-radius: 50%;
  overflow: hidden;          /* the circular mask over a square photograph */
  background: var(--surface-alt);

  /* The ring is drawn with box-shadow rather than a border so that turning it
     gold on selection cannot change the disc's size and nudge the row. */
  box-shadow: 0 0 0 1px var(--line);
  transition: box-shadow var(--transition-fast) var(--ease),
              transform var(--transition-fast) var(--ease);
}

.option-disc img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Metal and stone are LARGER. A colour has to be judged rather than
   identified, and a bigger area of it is easier to judge — where a photograph
   only has to be recognised. */
.option--swatch .option-disc {
  width: var(--disc-swatch);
  height: var(--disc-swatch);

  /* An inner highlight, so a near-white stone still reads as a disc of colour
     against a white rail rather than as a hole in it. */
  box-shadow: 0 0 0 1px var(--line),
              inset 0 2px 10px rgba(255, 255, 255, 0.55),
              inset 0 -6px 14px rgba(23, 22, 20, 0.10);
}

/* The gem stills are centred on white with a wide margin; left alone they
   render as a small object adrift in a large circle. The bands are already
   tight crops and are shown as they are. */
.option-row[data-group="head"] .option-disc img {
  transform: scale(1.42);
}

/* ── States ───────────────────────────────────────────────────────────────── */
.option:hover {
  color: var(--ink);
}

.option:hover .option-disc {
  box-shadow: 0 0 0 1px var(--muted);
  transform: translateY(-1px);
}

.option--swatch:hover .option-disc {
  box-shadow: 0 0 0 1px var(--muted),
              inset 0 2px 10px rgba(255, 255, 255, 0.55),
              inset 0 -6px 14px rgba(23, 22, 20, 0.10);
}

/* Selected: a gold ring set slightly off the disc, so the image is framed
   rather than overlaid. The label goes to full ink at the same time — colour
   alone should not be the only thing carrying the state. */
.option.is-active {
  color: var(--ink);
}

.option.is-active .option-disc {
  box-shadow: 0 0 0 2px var(--accent), 0 0 0 5px var(--accent-soft);
}

.option--swatch.is-active .option-disc {
  box-shadow: 0 0 0 2px var(--accent), 0 0 0 5px var(--accent-soft),
              inset 0 2px 10px rgba(255, 255, 255, 0.55),
              inset 0 -6px 14px rgba(23, 22, 20, 0.10);
}

.option-text {
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1.2;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 13px 18px;
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  transition: background var(--transition-fast) var(--ease),
              color var(--transition-fast) var(--ease),
              border-color var(--transition-fast) var(--ease);
}

.btn-primary {
  background: var(--ink);
  color: #fff;
}

.btn-primary:hover:not(:disabled) {
  background: var(--accent);
}

.btn-primary:disabled {
  background: var(--muted);
  cursor: wait;
}

.btn-secondary {
  background: var(--surface);
  border: 1px solid var(--line);
  color: var(--ink);
}

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

/* ── Sliders ────────────────────────────────────────────────────────────── */
.slider-row {
  margin-bottom: 14px;
}

.slider-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: var(--text-xs);
  color: var(--ink-soft);
  margin-bottom: 6px;
}

.slider-value {
  font-variant-numeric: tabular-nums;   /* stops the readout jittering wider */
  color: var(--muted);
}

/* Which hand the roll slider is editing. */
.hand-tag {
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

input[type='range'] {
  width: 100%;
  height: 18px;
}

/* The track is drawn on the input's own background; both vendor thumb rules
   are needed because neither browser recognises the other's selector — and a
   rule list containing an unknown selector is dropped whole, so they cannot be
   merged into one block. */
input[type='range']::-webkit-slider-runnable-track {
  height: 2px;
  background: var(--line);
  border-radius: var(--radius-pill);
}

input[type='range']::-moz-range-track {
  height: 2px;
  background: var(--line);
  border-radius: var(--radius-pill);
}

input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -6px;   /* centres the thumb on a 2px track */
  border-radius: 50%;
  background: var(--ink);
  border: 2px solid var(--surface);
  box-shadow: var(--shadow-sm);
}

input[type='range']::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--ink);
  border: 2px solid var(--surface);
}

/* ── Toggles ────────────────────────────────────────────────────────────── */
.toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 7px 0;
  font-size: var(--text-sm);
  color: var(--ink-soft);
}

.toggle {
  position: relative;
  width: 36px;
  height: 20px;
  flex: 0 0 36px;
  border-radius: var(--radius-pill);
  background: var(--line);
  cursor: pointer;
  transition: background var(--transition-base) var(--ease);
}

/* The knob. Positioned from the left and translated when on, so only one
   property animates. */
.toggle::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: transform var(--transition-base) var(--ease);
}

.toggle.on {
  background: var(--accent);
}

.toggle.on::after {
  transform: translateX(16px);
}

/* ── Stat rows ──────────────────────────────────────────────────────────── */
.stat-row {
  display: flex;
  justify-content: space-between;
  padding: 5px 0;
  font-size: var(--text-xs);
  border-bottom: 1px solid var(--line-soft);
}

.stat-row:last-child {
  border-bottom: none;
}

.stat-label {
  color: var(--muted);
}

.stat-value {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* ── Loading spinner ────────────────────────────────────────────────────────
   Shared by the configurator's stage and the try-on's camera panel. Toned for
   the light page here; panels.css re-tones it for the dark one. */
.loader-ring {
  width: 36px;
  height: 36px;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

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

.load-text {
  font-size: var(--text-sm);
  color: var(--muted);
  letter-spacing: var(--tracking-wide);
}

/* ── Hints and notes ────────────────────────────────────────────────────── */
.hint,
.note {
  font-size: var(--text-xs);
  color: var(--muted);
  line-height: 1.45;
}

.hint {
  margin-top: 8px;
}

.note:empty {
  display: none;   /* no reserved gap when there is nothing to say */
}

/* ── Settings drawer, its scrim, and the gear ───────────────────────────────
   SHARED, and it did not used to be. This lived in layout.css while the try-on
   was the only page with a drawer; the configurator grew one when the gem
   tuning panel needed somewhere to live, and a copy in configurator.css would
   have been a second definition of one component. Both pages position it
   against their own main area, so the only requirement on a page adopting it is
   that the drawer's parent is `position: relative`.

   Off-canvas by default and slid in by .is-open, rather than being added to and
   removed from the DOM — so the sliders keep their state and their listeners
   across an open/close, and the transition has something to animate.

   [hidden] is removed by JS on first open. It matters for the very first paint:
   without it a drawer that is only translated off-screen is still focusable and
   still read by a screen reader while invisible. */
.drawer {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: var(--drawer-width);
  max-width: 88vw;

  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 16px 18px 24px;

  background: var(--surface);
  border-left: 1px solid var(--line);
  box-shadow: var(--shadow-lg);

  /* The only scrollable region on the page — body itself is overflow:hidden. */
  overflow-y: auto;

  transform: translateX(100%);
  z-index: var(--z-drawer);

  /* visibility, not just the transform: an off-canvas panel is still focusable
     and still read aloud. It is delayed by the slide duration on the way out so
     the panel does not blink away mid-animation, and switched instantly on the
     way in (0s, no delay) so the slide has something to animate. */
  visibility: hidden;
  transition: transform var(--transition-base) var(--ease),
              visibility 0s linear var(--transition-base);
}

.drawer.is-open {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--transition-base) var(--ease),
              visibility 0s;
}

.drawer[hidden] {
  display: none;   /* beats the flex display above, which would otherwise win */
}

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

.drawer-title {
  font-size: var(--text-lg);
}

/* Each block in the drawer, separated by a rule rather than by space alone —
   the list is long enough that space stops reading as grouping. */
.drawer > div:not(.drawer-head) + div {
  border-top: 1px solid var(--line-soft);
  padding-top: 16px;
}

/* ── Scrim ──────────────────────────────────────────────────────────────────
   Dims the camera panel behind the open drawer and gives a click target for
   closing it. Fades rather than appearing, so the drawer's slide has a
   companion instead of a flash. */
.scrim {
  position: absolute;
  inset: 0;
  background: rgba(23, 22, 20, 0.35);
  opacity: 0;
  transition: opacity var(--transition-base) var(--ease);
  z-index: var(--z-scrim);

  /* THE FIX FOR A REAL BUG: a closed scrim is invisible but still a full-panel
     element on top of everything, and it swallowed every click on the bar
     underneath — after opening the settings once, "Change design" stopped
     responding for the rest of the session.

     Driven by the class rather than by clearing `hidden` on transitionend: a
     transition that never fires (reduced motion, a background tab) would leave
     the panel permanently dead, and this cannot. */
  pointer-events: none;
}

.scrim.is-open {
  opacity: 1;
  pointer-events: auto;   /* while open it must catch the click that closes it */
}

.scrim[hidden] {
  display: none;
}

/* ── Header right group ─────────────────────────────────────────────────── */
.header-right {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* ── Icon button ────────────────────────────────────────────────────────────
   The gear, and the drawer's close control. Square, quiet, and only coloured on
   hover — it sits next to the status readout and must not compete with it. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-md);
  color: var(--ink-soft);
  transition: background var(--transition-fast) var(--ease),
              color var(--transition-fast) var(--ease);
}

.icon-btn:hover {
  background: var(--surface-alt);
  color: var(--ink);
}


/* ── Narrow screens ─────────────────────────────────────────────────────────
   The drawer becomes a bottom sheet: on a phone a full-height right-hand panel
   covers what you are trying to look at, and the thumb is nowhere near its
   controls. 80dvh leaves the top of the view visible while it is open. */
@media (max-width: 640px) {
  .drawer {
    top: auto;
    left: 0;
    width: auto;
    max-width: none;
    height: 80dvh;
    border-left: none;
    border-top: 1px solid var(--line);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    transform: translateY(100%);
  }

  .drawer.is-open {
    transform: translateY(0);
  }
}

/* ── Tuning panel widgets ───────────────────────────────────────────────────
   The three controls the gem panel needs that no other panel does: a select, a
   colour well and a row of buttons. Everything else in it — section titles,
   slider rows, readouts, hints — is already above, which is most of why the
   panel could be built out of markup this file had never seen.
──────────────────────────────────────────────────────────────────────────── */

.select-input {
  width: 100%;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-sm);
}

/* A colour well, not a swatch: it opens the OS picker, so it has to look like
   something you press. Full width because the value it carries is the whole
   point of the row — a 24px chip of a body colour is too small to judge, which
   is the same argument the configurator's colour discs make at a larger size. */
.colour-input {
  width: 100%;
  height: 30px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  cursor: pointer;
}

/* Chrome and Safari wrap the colour in two nested boxes with their own padding;
   without these the well shows a thin sliver of colour in a large grey frame. */
.colour-input::-webkit-color-swatch-wrapper { padding: 0; }
.colour-input::-webkit-color-swatch { border: none; border-radius: 2px; }
.colour-input::-moz-color-swatch { border: none; border-radius: 2px; }

.panel-buttons {
  display: flex;
  gap: 8px;
}

.panel-buttons .btn {
  flex: 1;
  padding: 8px 10px;
  font-size: var(--text-xs);
}

/* The generated panel's own sections need the separator the drawer gives its
   direct children — and cannot inherit it, because `.drawer > div + div` is a
   DOM-tree selector and these sit one level down inside the mount. Restated
   here rather than reached for with `display: contents`, which would dissolve
   the mount's box and take the rule above it with it. */
.panel-mount {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.panel-mount > div + div {
  border-top: 1px solid var(--line-soft);
  padding-top: 16px;
}

/* ── Per-row tooltips ───────────────────────────────────────────────────────
   Every control in this panel needs a sentence — "body colour" is an absorption
   vector and not a tint, and nobody would guess that — but eleven sentences
   shown at once turn the panel into a wall of prose you have to scroll past to
   reach the control you wanted. So the explanation is one hover away.

   THEY ARE OVERLAYS, AND THAT IS THE WHOLE POINT. Revealing a hint with
   `display: block` grew the row, which pushed every row below it down — out
   from under the cursor, which ended the hover, which collapsed the hint, which
   pulled the row back under the cursor. A loop, at frame rate: the panel
   flickered as soon as the pointer crossed it. An absolutely positioned box
   takes no space in the flow, so nothing moves and the loop cannot start.

   `pointer-events: none` closes the other half of it — a tooltip that sat under
   the cursor would take the hover from the row that owns it.

   Laid out to the row's own width and opening downward over the rows beneath,
   rather than escaping the drawer: `.drawer` is `overflow-y: auto` with
   overflow-x computing to `auto` beside it, so anything wider than the panel
   would be clipped or would grow a horizontal scrollbar. The last row in a
   section opens UPWARD instead, since below it there is nothing to overlay and
   an absolute box still contributes to scrollable height.

   The section-level hints above each group are NOT tooltips: those say what the
   group writes to, and are worth reading before touching anything. */
.panel-mount .slider-row {
  position: relative;
}

.panel-mount .slider-row .hint {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 2;
  margin-top: 6px;
  padding: 9px 11px;

  background: var(--surface);
  color: var(--ink-soft);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);

  opacity: 0;
  visibility: hidden;
  pointer-events: none;

  /* Out is immediate; in waits. See the delay on the hover rule below. */
  transition: opacity var(--transition-fast) var(--ease),
              visibility 0s linear var(--transition-fast);
}

/* A DWELL, not a reveal. Two seconds is long enough that sweeping the panel
   looking for a control never raises one, and short enough that stopping on a
   control you do not understand answers you without a second thought. The delay
   is on the way IN only — leaving has to be instant, or the box lingers over
   whatever you moved to next. */
.panel-mount .slider-row:hover .hint,
.panel-mount .slider-row:focus-within .hint {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--transition-base) var(--ease) 2s,
              visibility 0s linear 2s;
}

/* The caret, so the box reads as pointing AT the control rather than as a
   panel that happened to appear. A rotated square rather than the usual
   border-triangle: a triangle made of borders cannot carry a border of its own,
   and this box has one — two adjacent edges of the square supply it, and the
   other two are hidden under the box's own background. */
.panel-mount .slider-row .hint::before {
  content: '';
  position: absolute;
  top: -5px;
  left: 16px;
  width: 8px;
  height: 8px;
  background: var(--surface);
  border-left: 1px solid var(--line);
  border-top: 1px solid var(--line);
  transform: rotate(45deg);
}

/* Nothing below it to overlay, and an absolute box below the last row would add
   to the drawer's scroll height while it was showing. */
.panel-mount .slider-row:last-child .hint {
  top: auto;
  bottom: 100%;
  margin-top: 0;
  margin-bottom: 6px;
}

.panel-mount .slider-row:last-child .hint::before {
  top: auto;
  bottom: -5px;
  border-left: none;
  border-top: none;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
