/* ==========================================================================
   Cube viewer — the 3D cube, the folded-out net and the transport.

   Loaded by /viewer and by every page that embeds `mountCaseViewer`.
   Built entirely on the tokens in base.css.
   ========================================================================== */

/* --------------------------------------------------------------------- */
/* Page layout (/viewer)                                                  */
/* --------------------------------------------------------------------- */

.viewer-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(300px, 0.85fr);
  gap: var(--space-5);
  align-items: start;
}

@media (max-width: 1040px) {
  .viewer-layout { grid-template-columns: minmax(0, 1fr); }
}

.viewer-stage {
  overflow: visible;
  padding: 0;
}

.viewer-stage__toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--ink-100);
}

.viewer-mount { padding: var(--space-4) var(--space-4) var(--space-5); }

.viewer-editor .field__error,
.field__error {
  margin: 0;
  font-size: 0.82rem;
  color: var(--danger);
  font-weight: 550;
}

.viewer-editor textarea.is-invalid,
.viewer-editor input.is-invalid {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-soft);
}

.viewer-json {
  border-top: 1px solid var(--ink-100);
  padding-top: var(--space-4);
}
.viewer-json summary {
  cursor: pointer;
  font-size: 0.88rem;
  font-weight: 580;
  color: var(--ink-700);
  list-style: none;
}
.viewer-json summary::-webkit-details-marker { display: none; }
.viewer-json summary::before {
  content: '▸';
  display: inline-block;
  width: 1em;
  color: var(--ink-400);
  transition: transform var(--dur-fast) var(--ease);
}
.viewer-json[open] summary::before { transform: rotate(90deg); }
.viewer-json > *:not(summary) { margin-top: var(--space-3); }

.viewer-hint {
  margin-top: var(--space-5);
  color: var(--ink-400);
  font-size: 0.82rem;
  text-align: center;
}
.viewer-hint kbd {
  display: inline-block;
  padding: 1px 6px;
  margin: 0 1px;
  border: 1px solid var(--ink-200);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--paper);
  color: var(--ink-700);
  font-size: 0.76rem;
  line-height: 1.5;
}

/* --------------------------------------------------------------------- */
/* Embeddable viewer (mount.js)                                           */
/* --------------------------------------------------------------------- */

.case-viewer {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.case-viewer__views {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: var(--space-4);
  align-items: center;
}

@media (max-width: 780px) {
  .case-viewer__views { grid-template-columns: minmax(0, 1fr); }
}

/* `minmax(0, …)` everywhere: a grid track's default `min-width: auto` refuses
   to shrink below the stage's min-content width, which the aspect ratio and
   min-height together push to 250px. Without it a 180px thumbnail embed
   overflows its container. */
.case-viewer--compact .case-viewer__views { grid-template-columns: minmax(0, 1fr); }
.case-viewer--compact { gap: var(--space-3); }

/* A compact embed can be far narrower than the viewport (a 180px list
   thumbnail on a desktop), so the viewport media queries below never fire for
   it. Let its transport shrink on its own terms instead. The floor is then the
   five transport buttons, about 160px; below that pass `timeline: false` and
   show a still cube. */
.case-viewer--compact .timeline__controls { gap: var(--space-2); }
.case-viewer--compact .timeline__counter { display: none; }
.case-viewer--compact .timeline__btn { width: 28px; height: 28px; }
.case-viewer--compact .timeline__btn svg { width: 13px; height: 13px; }
.case-viewer--compact .timeline__btn--play { width: 32px; height: 32px; }
.case-viewer--compact .timeline__btn--play svg { width: 15px; height: 15px; }
.case-viewer--compact .timeline__speed {
  flex: 1 1 100%;
  min-width: 0;
}
.case-viewer--compact .timeline__speed-label { display: none; }
.case-viewer--compact .timeline__speed-value { min-width: 0; }

/* --------------------------------------------------------------------- */
/* 3D cube                                                                */
/* --------------------------------------------------------------------- */

.cube3d { position: relative; }

.cube3d__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 5 / 4;
  /* Only a floor against a zero-height canvas — keep it below the natural
     height of the smallest embed (180px wide → 144px) so it never fights the
     aspect ratio. */
  min-height: 120px;
  border-radius: var(--radius);
  background:
    radial-gradient(120% 90% at 50% 8%, rgba(61, 90, 254, 0.05), transparent 62%),
    var(--ink-050);
  overflow: hidden;
  cursor: grab;
  touch-action: none;
}
.cube3d__stage:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cube3d__stage.is-grabbing,
.cube3d__canvas.is-grabbing { cursor: grabbing; }

.cube3d__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
}

.cube3d--fallback { display: flex; flex-direction: column; gap: var(--space-3); }
.cube3d__notice {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--warn-soft);
  color: var(--warn);
  font-size: 0.86rem;
}

@media (max-width: 480px) {
  .cube3d__stage { aspect-ratio: 1 / 1; }
}

/* --------------------------------------------------------------------- */
/* Flat net                                                               */
/* --------------------------------------------------------------------- */

.cube-net { width: 100%; }

.cube-net__svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}

.cube-net__block {
  fill: none;
  stroke: var(--ink-200);
  stroke-width: 0.8;
}

.cube-net__label {
  font-family: var(--font-sans);
  font-size: 7px;
  font-weight: 700;
  fill: var(--ink-300);
  letter-spacing: 0.08em;
}

.cube-net__sticker {
  stroke: rgba(20, 22, 26, 0.16);
  stroke-width: 0.7;
  transition: fill var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

.cube-net__sticker.is-turning {
  stroke: rgba(20, 22, 26, 0.5);
  stroke-width: 1.3;
}

.cube-net__sticker.is-masked { opacity: 0.22; }

@media (prefers-reduced-motion: reduce) {
  .cube-net__sticker { transition: none; }
}

/* --------------------------------------------------------------------- */
/* Timeline                                                               */
/* --------------------------------------------------------------------- */

.timeline {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.timeline__controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.timeline__transport {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  background: var(--ink-100);
  border-radius: var(--radius-pill);
  padding: 3px;
}

.timeline__btn {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-500);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
    transform var(--dur-fast) var(--ease);
}
.timeline__btn svg { width: 16px; height: 16px; fill: currentColor; }
.timeline__btn:hover:not(:disabled) { background: var(--paper); color: var(--ink-900); }
.timeline__btn:active:not(:disabled) { transform: scale(0.92); }
.timeline__btn:disabled { opacity: 0.35; cursor: default; }

.timeline__btn--play {
  background: var(--ink-900);
  color: #fff;
  width: 40px;
  height: 40px;
}
.timeline__btn--play:hover:not(:disabled) { background: var(--accent); color: #fff; }
.timeline__btn--play svg { width: 18px; height: 18px; }

.timeline__btn--wide {
  width: auto;
  height: 34px;
  gap: var(--space-2);
  padding: 0 14px;
  border-radius: var(--radius-pill);
  background: var(--ink-100);
  grid-auto-flow: column;
  font: inherit;
  font-size: 0.84rem;
  font-weight: 560;
  margin-left: auto;
}
.timeline__btn--wide:hover:not(:disabled) { background: var(--ink-200); color: var(--ink-900); }

.timeline__counter {
  font-family: var(--font-mono);
  font-size: 0.84rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-500);
  min-width: 4.5ch;
}

.timeline__speed {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 190px;
  flex: 0 1 240px;
}
.timeline__speed-label { font-size: 0.8rem; color: var(--ink-400); white-space: nowrap; }
.timeline__speed-range { flex: 1 1 auto; min-width: 70px; width: auto; }
.timeline__speed-value {
  font-size: 0.78rem;
  color: var(--ink-500);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  min-width: 9ch;
}

/* --- scrub bar --- */

.timeline__scrub { padding: var(--space-2) 0; }

.timeline__slider {
  position: relative;
  height: 22px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  touch-action: none;
  display: flex;
  align-items: center;
}
.timeline__slider::before {
  content: '';
  position: absolute;
  inset: 8px 0;
  border-radius: var(--radius-pill);
  background: var(--ink-100);
}
.timeline__slider[aria-disabled='true'] { cursor: default; opacity: 0.6; }

.timeline__segments { position: absolute; inset: 8px 0; }
.timeline__segment {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--ink-200);
  border-right: 2px solid var(--paper);
  border-radius: 2px;
}
.timeline__segment:last-child { border-right: 0; }
.timeline__segment--cross { background: color-mix(in srgb, var(--phase-cross) 26%, var(--ink-100)); }
.timeline__segment--f2l { background: color-mix(in srgb, var(--phase-f2l) 26%, var(--ink-100)); }
.timeline__segment--oll { background: color-mix(in srgb, var(--phase-oll) 30%, var(--ink-100)); }
.timeline__segment--pll { background: color-mix(in srgb, var(--phase-pll) 26%, var(--ink-100)); }

.timeline__progress {
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 0;
  border-radius: var(--radius-pill);
  background: var(--accent);
  opacity: 0.85;
  pointer-events: none;
}

.timeline__handle {
  position: absolute;
  top: 50%;
  left: 0;
  width: 16px;
  height: 16px;
  margin-left: -8px;
  border-radius: 50%;
  background: var(--paper);
  border: 3px solid var(--accent);
  box-shadow: var(--shadow-sm);
  transform: translateY(-50%);
  pointer-events: none;
  transition: transform var(--dur-fast) var(--ease);
}
.timeline__slider:hover .timeline__handle,
.timeline__slider.is-dragging .timeline__handle,
.timeline__slider:focus-visible .timeline__handle { transform: translateY(-50%) scale(1.18); }

.timeline__markers { position: absolute; inset: 0; pointer-events: none; }
.timeline__marker {
  position: absolute;
  top: 1px;
  bottom: 1px;
  width: 3px;
  margin-left: -1.5px;
  border-radius: 2px;
  background: var(--warn);
}

/* --- savepoints --- */

/* `timeline.js` toggles the `hidden` attribute on this list when there are no
   savepoints. A class rule setting `display` outranks the UA's bare
   `[hidden]` selector, so the attribute alone would not hide it. base.css now
   carries a global `[hidden] { display: none !important; }`, but this rule
   keeps the component correct on its own rather than depending on it. */
.timeline__savepoints[hidden] { display: none; }

.timeline__savepoints {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}
.timeline__savepoint {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--ink-200);
  border-radius: var(--radius-pill);
  background: var(--paper);
  overflow: hidden;
}
.timeline__savepoint-jump {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 0;
  background: transparent;
  padding: 4px 6px 4px 10px;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 560;
  color: var(--ink-700);
  cursor: pointer;
}
.timeline__savepoint-jump svg { width: 11px; height: 11px; fill: var(--warn); }
.timeline__savepoint-jump:hover { background: var(--warn-soft); }
.timeline__savepoint-remove {
  border: 0;
  border-left: 1px solid var(--ink-200);
  background: transparent;
  padding: 0 9px;
  font-size: 1rem;
  line-height: 1;
  color: var(--ink-400);
  cursor: pointer;
}
.timeline__savepoint-remove:hover { background: var(--danger-soft); color: var(--danger); }

/* --- move strip --- */

.timeline__moves {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.timeline__moves-empty {
  color: var(--ink-400);
  font-size: 0.86rem;
  padding: var(--space-2) 0;
}

.timeline__step {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--ink-050);
  border-left: 3px solid var(--ink-200);
}
.timeline__step--cross { border-left-color: var(--phase-cross); }
.timeline__step--f2l { border-left-color: var(--phase-f2l); }
.timeline__step--oll { border-left-color: var(--phase-oll); }
.timeline__step--pll { border-left-color: var(--phase-pll); }

.timeline__step-label {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 660;
  color: var(--ink-400);
  min-width: 7ch;
}

.timeline__step-moves {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
}

.timeline__move {
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  padding: 2px 6px;
  font-family: var(--font-mono);
  font-size: 0.88rem;
  font-weight: 500;
  color: var(--ink-500);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.timeline__move:hover { background: var(--ink-100); color: var(--ink-900); }
.timeline__move.is-done { color: var(--ink-300); }
.timeline__move.is-current {
  background: var(--accent);
  color: #fff;
  font-weight: 640;
  box-shadow: var(--shadow-xs);
}

@media (max-width: 600px) {
  .timeline__controls { gap: var(--space-2); }
  .timeline__speed { flex: 1 1 100%; min-width: 0; }
  .timeline__btn--wide { margin-left: 0; }
  .timeline__step-label { min-width: 0; }
}

/* --------------------------------------------------------------------- */
/* Touch targets                                                          */
/* --------------------------------------------------------------------- */

/* The sizes above are tuned for a mouse and for fitting a narrow embed on a
   desktop. On a touch device the 40px floor wins over compactness — a finger
   cannot hit a 28px step button. Keyed on the pointer rather than the
   viewport, so a compact embed on a phone is covered wherever it appears. */
@media (pointer: coarse) {
  /* Five 40px targets need 200px, which is more than the ~160px floor a list
     thumbnail embed has to survive. Rather than shrink the targets back or
     overlap them — overlapping targets hit the wrong button, which is worse
     than a small one — let the transport wrap. A narrow container gets a
     taller transport instead of an overflowing or unhittable one. */
  .timeline__transport {
    flex-wrap: wrap;
    justify-content: center;
  }

  .timeline__btn,
  .case-viewer--compact .timeline__btn {
    width: 40px;
    height: 40px;
  }
  .timeline__btn svg,
  .case-viewer--compact .timeline__btn svg {
    width: 17px;
    height: 17px;
  }

  .timeline__btn--play,
  .case-viewer--compact .timeline__btn--play {
    width: 44px;
    height: 44px;
  }
  .timeline__btn--play svg,
  .case-viewer--compact .timeline__btn--play svg {
    width: 19px;
    height: 19px;
  }

  .timeline__btn--wide { height: 40px; padding: 0 16px; }

  /* A range input's hit area is its box, not the visible track. */
  .timeline__speed-range { height: 40px; }

  /* The scrub bar and its handle are dragged, so they need the most room.
     The hit area grows to 40px while the drawn bar keeps its 6px thickness,
     so only the target changes, not the design. */
  .timeline__slider { height: 40px; }
  .timeline__slider::before,
  .timeline__segments { inset: 17px 0; }
  .timeline__progress { top: 17px; bottom: 17px; }
  .timeline__marker { top: 10px; bottom: 10px; }
  .timeline__handle { width: 22px; height: 22px; margin-left: -11px; }

  .timeline__savepoint-jump { padding: 9px 8px 9px 12px; }
  .timeline__savepoint-remove { padding: 0 14px; }
  .timeline__move { padding: 7px 10px; }
}

/* --------------------------------------------------------------------- */
/* Reserved slot beside the flat net                                      */
/*                                                                        */
/* A host page can opt into a third cell (`mountCaseViewer(..., {slot:    */
/* true})`) and mount a second visualisation into `[data-viewer-slot]`.    */
/* It is sized exactly like the net so the two read as a pair.            */
/* --------------------------------------------------------------------- */

.case-viewer--has-slot .case-viewer__views {
  /* 3D a little wider, then two equal cells: the net and its neighbour. */
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr) minmax(0, 1fr);
}

/* Once something is mounted in it, the slot is a view like any other and
   drops the "reserved" dressing. */
.case-viewer__slot--filled {
  border: 0;
  background: none;
  aspect-ratio: auto;
}

.cube-circles { width: 100%; }

.cube-circles__svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}

/* One hue per axis so the three lobes read as three families rather than
   nine unrelated rings. Kept faint: they are the scaffolding, the stickers
   are the data. */
.cube-circles__ring {
  fill: none;
  stroke-width: 0.5;
  vector-effect: non-scaling-stroke;
}
.cube-circles__ring--x { stroke: rgba(61, 90, 254, 0.38); }
.cube-circles__ring--y { stroke: rgba(0, 155, 72, 0.38); }
.cube-circles__ring--z { stroke: rgba(183, 18, 52, 0.34); }

.cube-circles__dot {
  stroke: rgba(20, 22, 26, 0.55);
  stroke-width: 0.35;
  vector-effect: non-scaling-stroke;
  transition: fill var(--dur) var(--ease);
}

/* Mid-turn the dot's position is set every frame, so a CSS transition on the
   coordinates would fight the animation. Drop it while travelling and lift
   the dot slightly so the moving ring reads above the resting ones. */
.cube-circles__dot.is-travelling {
  transition: none;
  stroke: rgba(20, 22, 26, 0.8);
  stroke-width: 0.5;
}

@media (prefers-reduced-motion: reduce) {
  .cube-circles__dot { transition: none; }
}

.case-viewer__slot {
  width: 100%;
  /* The net is a 4x3 cube cross; matching the ratio keeps the pair aligned
     whatever the container width. */
  aspect-ratio: 4 / 3;
  border: 1px dashed var(--ink-200);
  border-radius: var(--radius);
  background:
    repeating-linear-gradient(
      45deg,
      transparent,
      transparent 9px,
      rgba(134, 142, 156, 0.06) 9px,
      rgba(134, 142, 156, 0.06) 18px
    );
}

/* An empty slot must not push the pair off a narrow screen: below the point
   where the views stack, it is simply not worth the height. */
@media (max-width: 1100px) {
  .case-viewer--has-slot .case-viewer__views {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  }
  .case-viewer__slot:empty { display: none; }
}

@media (max-width: 780px) {
  .case-viewer--has-slot .case-viewer__views { grid-template-columns: minmax(0, 1fr); }
}

/* Reference numbering: group centres 1-3, circles 1.1 .. 3.3. Faint on
   purpose — a shared vocabulary for talking about the figure, not something
   to read past the stickers. */
.cube-circles__grouplabel,
.cube-circles__ringlabel {
  font-family: var(--font-mono);
  fill: var(--ink-900);
  pointer-events: none;
  user-select: none;
}

.cube-circles__grouplabel {
  font-size: 6px;
  font-weight: 700;
  opacity: 0.32;
}

.cube-circles__ringlabel {
  font-size: 4.2px;
  opacity: 0.42;
}

.cube-circles__ringlabel--x { fill: #3d5afe; }
.cube-circles__ringlabel--y { fill: #009b48; }
.cube-circles__ringlabel--z { fill: #b71234; }

/* Crossing names, off until toggled: 54 labels is a lot to read past, but
   they are what lets a position be named out loud. */
.cube-circles__pointlabel {
  font-family: var(--font-mono);
  font-size: 2.6px;
  fill: var(--ink-900);
  opacity: 0;
  pointer-events: none;
  user-select: none;
  transition: opacity var(--dur) var(--ease);
}

.cube-circles.is-showing-points .cube-circles__pointlabel { opacity: 0.55; }

/* The dots shrink a little so the names have room to sit above them. */
.cube-circles.is-showing-points .cube-circles__dot { r: 2.1; }

/* Finding a piece. Its stickers are scattered across the figure by design, so
   hovering one lights the others and the three circles the piece lives on. */
.cube-circles__hit {
  fill: transparent;
  cursor: pointer;
}
.cube-circles__hit:focus { outline: none; }

/* The siblings can be most of the figure away, so a heavier stroke alone is
   not enough to spot them -- they grow as well, and everything else fades
   back so the piece is the only thing at full strength. */
/* Written with the svg in the selector to outrank `is-showing-points` above,
   which shrinks every dot to make room for the crossing labels -- the piece
   must still stand out with the names turned on. */
.cube-circles__svg .cube-circles__dot.is-piece {
  r: 3.8;
  stroke: var(--ink-900);
  stroke-width: 1.1;
}

.cube-circles__svg .cube-circles__dot.is-focus {
  r: 4.4;
  stroke: var(--accent);
  stroke-width: 1.6;
}

.cube-circles__svg.is-finding .cube-circles__dot:not(.is-piece) {
  opacity: 0.3;
}

.cube-circles__ring.is-piece {
  stroke-width: 1.2;
  stroke-dasharray: none;
  opacity: 1;
}

.cube-circles__readout {
  margin: var(--space-2) 0 0;
  min-height: 1.4em;
  font-size: 0.78rem;
  font-family: var(--font-mono);
  color: var(--ink-500);
  text-align: center;
}
