*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* Surfaces */
  --bg:            #f5f5f7;
  --surface:       #ffffff;
  --surface-2:     #f0f0f2;

  /* Text */
  --text-primary:   #1d1d1f;
  --text-secondary: #6e6e73;
  --text-tertiary:  #86868b;

  /* Lines */
  --border: #d2d2d7;
  --border-strong: #b8b8bd;

  /* Accent / semantic */
  --accent:        #0071e3;
  --accent-hover:  #0077ed;
  --accent-active: #0068d1;
  --success:       #34c759;
  --error:         #ff3b30;
  --error-bg:      #fff1f0;
  --warning:       #ff9500;

  /* Elevation */
  --shadow-sm: 0 1px 2px rgba(0,0,0,.05);
  --shadow-md: 0 4px 16px rgba(0,0,0,.10);
  --shadow-lg: 0 16px 48px rgba(0,0,0,.16);

  /* Spacing scale */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px;

  /* Radius scale */
  --r-sm: 8px; --r-md: 12px; --r-lg: 20px; --r-full: 999px;

  /* Solution filmstrip's distance from the right edge — shared with the
     step counter so it can dock immediately to the filmstrip's right. */
  --sol-dock-right: clamp(90px, 14vw, 220px);
}

html, body { height: 100vh; overflow: hidden; background: var(--bg); color: var(--text-primary); font-family: system-ui, -apple-system, sans-serif; }
/* flex-shrink:0 is load-bearing: inside a flex row, an <svg> with no HTML
   width/height attributes (only viewBox) has an intrinsic min-width the
   browser treats as 0 for flex-shrink purposes — without this, every icon
   collapses to a 0-width sliver instead of the size set below. */
svg { display: block; flex-shrink: 0; }

/* pico.min.css sets body>main{padding-block:...} by default, which ate into
   main's height and left the page's own light background showing as white
   bars above/below .camera-wrap (position:relative, so — unlike #cubeEditor,
   which is position:absolute and escapes main's box entirely — it sits
   inside main's padded content box instead of spanning the full viewport).
   Same specificity (0,0,2) as pico's selector, so it needs to win on cascade
   order instead: style.css is linked after pico.min.css in index.html. */
body > main { height: 100vh; padding: 0; }

/* ── Camera ──────────────────────────────────────────── */
.camera-wrap { width: 100%; height: 100%; position: relative; overflow: hidden; background: #000; }
#cam     { width: 100%; height: 100%; object-fit: cover; display: block; transform: scaleX(-1); }
/* No mirror here: sendFrame() in app.js already captures a horizontally-
   flipped frame (matching what #cam shows), so overlay pixel coords line up
   with the video's visual (mirrored) rendering without a second flip. */
#overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }

/* Floating action pills, top-right */
.cam-actions { position: absolute; top: var(--sp-4); right: var(--sp-4); display: flex; gap: var(--sp-2); z-index: 5; }
.btn-pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: .5rem 1rem; border-radius: var(--r-full); border: 1px solid var(--border);
  background: rgba(255,255,255,.72);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  color: var(--text-primary); font-size: .85rem; font-weight: 600; cursor: pointer;
  box-shadow: var(--shadow-sm); transition: background .15s, transform .1s;
}
.btn-pill:hover  { background: rgba(255,255,255,.92); }
.btn-pill:active { transform: scale(.96); }
.btn-pill-accent { background: var(--accent); color: #fff; border-color: transparent; }
.btn-pill-accent:hover { background: var(--accent-hover); }
.btn-pill svg { width: 15px; height: 15px; }

/* ── Rotation guide shown after each capture ──────────── */
/* Pas de fond : reste au-dessus du flux caméra en direct (pas d'écran
   d'instructions séparé qui le cache). Chaque élément assure sa propre
   lisibilité (text-shadow / drop-shadow) puisque ce qu'il y a derrière
   n'est plus une surface neutre contrôlée mais l'image de la caméra. */
.rotate-anim {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px;
  pointer-events: none; z-index: 10;
}
/* Texte retiré à la demande de l'utilisateur — seul le cube reste visible
   pendant la transition. Le JS continue d'écrire dans ces éléments (inutile
   de toucher à cette logique), ils sont juste masqués. */
.ra-check      { display: none; }
.ra-next-wrap  { display: none; }
.ra-next-label { font-size: .72rem; color: #fff; text-transform: uppercase; letter-spacing: .08em; text-shadow: 0 1px 4px rgba(0,0,0,.6); }
.ra-next-name  { font-size: 1.6rem; font-weight: 800; color: #fff; letter-spacing: .02em; text-shadow: 0 1px 4px rgba(0,0,0,.7), 0 0 16px rgba(0,0,0,.5); }

.ra-instr-block { display: flex; flex-direction: column; align-items: center; gap: 10px; }
/* Cube filaire du guide de scan — même mécanique perspective/preserve-3d/
   translateZ que .cube3d-scene/.cube3d-box (voir FACE_BASE_TRANSFORM dans
   app.js), mais une seule boîte à 6 faces (pas de subdivision en cubies),
   faces transparentes, et sa propre variable --ra-c3-size (indépendante de
   --c3-size) pour ne jamais être affectée par la taille du cube plein
   étape 2/3. Toutes les arêtes sont affichées, y compris celles des 3
   faces cachées derrière (vue filaire complète façon rayons X, pas juste
   les faces tournées vers la caméra). drop-shadow (blanc + noir) sur la
   boîte entière pour que les arêtes restent lisibles quelle que soit la
   couleur du fond caméra en direct. */
.ra-cube3d-scene {
  --ra-c3-size: clamp(170px, 32vh, 240px);
  width: var(--ra-c3-size); height: var(--ra-c3-size);
  perspective: calc(var(--ra-c3-size) * 3);
  filter: drop-shadow(0 0 3px rgba(255,255,255,.85)) drop-shadow(0 2px 6px rgba(0,0,0,.45));
}
.ra-cube3d-box {
  width: 100%; height: 100%; position: relative;
  transform-style: preserve-3d;
  transform: rotateX(-10deg);
}
.ra-cube3d-face {
  position: absolute; inset: 0;
  border: 2.5px solid var(--text-tertiary);
  background: transparent;
}
.ra-cube3d-face--f { transform: translateZ(calc(var(--ra-c3-size) / 2)); }
.ra-cube3d-face--b { transform: rotateY(180deg) translateZ(calc(var(--ra-c3-size) / 2)); }
.ra-cube3d-face--u { transform: rotateX(90deg)  translateZ(calc(var(--ra-c3-size) / 2)); }
.ra-cube3d-face--d { transform: rotateX(-90deg) translateZ(calc(var(--ra-c3-size) / 2)); }
.ra-cube3d-face--l { transform: rotateY(-90deg) translateZ(calc(var(--ra-c3-size) / 2)); }
.ra-cube3d-face--r { transform: rotateY(90deg)  translateZ(calc(var(--ra-c3-size) / 2)); }
.ra-instr-lines { display: none; }
.ra-instr-main  { font-size: 1.2rem; font-weight: 700; color: #fff; text-align: center; text-shadow: 0 1px 4px rgba(0,0,0,.7), 0 0 16px rgba(0,0,0,.5); }
.ra-instr-sub   { font-size: .86rem; color: #fff; text-align: center; text-shadow: 0 1px 4px rgba(0,0,0,.7), 0 0 12px rgba(0,0,0,.5); }

.ra-remaining { display: none; }

/* ── Viseur pulsé (1ère face) ─────────────────────────────
   Une seule face vue de face, dessinée par ses 4 coins seulement (équerres
   en L, centre vide — pas un carré plein qui masquerait l'écran), qui
   « respire » pour inviter à présenter le cube. Masqué par défaut ; activé
   via #rotateAnim.vf ; s'efface via #rotateAnim.vf-locked dès qu'une face
   est détectée (setFirstFaceGuide dans app.js). Même --ra-vf-size et
   drop-shadow que .ra-cube3d-scene pour rester lisible sur le flux caméra. */
.ra-viewfinder {
  --ra-vf-size: clamp(170px, 32vh, 240px);   /* = --ra-c3-size du cube filaire */
  display: none; position: relative;
  width: var(--ra-vf-size); height: var(--ra-vf-size);
  filter: drop-shadow(0 0 3px rgba(255,255,255,.85)) drop-shadow(0 2px 6px rgba(0,0,0,.45));
}
/* Équerres courtes → « juste les angles », centre bien vide */
.ra-vf-corner {
  position: absolute; width: 30px; height: 30px; border: 3px solid #fff;
}
.ra-vf-corner--tl { top: 0; left: 0;  border-right: 0; border-bottom: 0; border-top-left-radius: 6px; }
.ra-vf-corner--tr { top: 0; right: 0; border-left: 0;  border-bottom: 0; border-top-right-radius: 6px; }
.ra-vf-corner--bl { bottom: 0; left: 0;  border-right: 0; border-top: 0; border-bottom-left-radius: 6px; }
.ra-vf-corner--br { bottom: 0; right: 0; border-left: 0;  border-top: 0; border-bottom-right-radius: 6px; }

@keyframes ra-vf-pulse {
  0%, 100% { transform: scale(1);    opacity: .9; }
  50%      { transform: scale(1.12); opacity: 1;  }
}

/* Mode 1ère face : viseur visible + pulsé, cube filaire masqué */
#rotateAnim.vf .ra-viewfinder  { display: block; animation: ra-vf-pulse 1.8s ease-in-out infinite; }
#rotateAnim.vf .ra-instr-block { display: none; }

/* Face détectée : la pulsation s'arrête, fondu de sortie */
#rotateAnim.vf.vf-locked .ra-viewfinder {
  animation: none; opacity: 0; transform: scale(1.06);
  transition: opacity .35s ease, transform .35s ease;
}

@media (prefers-reduced-motion: reduce) {
  #rotateAnim.vf .ra-viewfinder { animation-duration: 3.6s; }
}

/* Status bar (bottom) */
/* Pas de pastille/fond blanc — juste la barre, flottant directement sur le
   flux caméra (voir stability-track ci-dessous pour sa propre lisibilité). */
.cam-status {
  position: absolute; bottom: var(--sp-4); left: 50%; transform: translateX(-50%);
  z-index: 5;
  max-width: calc(100% - 32px);
}
/* Texte retiré à la demande de l'utilisateur — seule la barre (qui passe au
   vert une fois stable, voir setStability()) reste visible. */
#statusText { display: none; }
.stability-track {
  width: 260px; height: 8px; background: rgba(240,240,242,.92); border-radius: 4px; overflow: hidden;
  box-shadow: 0 1px 5px rgba(0,0,0,.55), 0 0 0 1px rgba(0,0,0,.18);
}
.stability-fill  { height: 100%; width: 0%; background: var(--accent); border-radius: 4px; transition: width .10s, background .15s; }

.hidden { display: none !important; }

/* ── 3D Cube ──────────────────────────────────────────── */
.cube3d-wrap  { display:flex; flex-direction:column; align-items:center; gap:10px; }
.cube3d-scene {
  --c3-size: 180px;
  --c3-cell: calc(var(--c3-size) / 3);
  --c3-gap:  calc(var(--c3-size) / 60);
  width:var(--c3-size); height:var(--c3-size); perspective:calc(var(--c3-size) * 3);
  cursor:grab; user-select:none; -webkit-user-select:none;
}
.cube3d-scene:active { cursor:grabbing; }
.cube3d-box {
  width:var(--c3-size); height:var(--c3-size); position:relative;
  transform-style:preserve-3d;
  transform:rotateX(-28deg) rotateY(40deg);
}
.cube3d-turn-wrapper {
  position:absolute; top:0; left:0; width:var(--c3-size); height:var(--c3-size);
  transform-style:preserve-3d; pointer-events:none;
}
.cube3d-cubie {
  position:absolute; top:50%; left:50%;
  width:var(--c3-cell); height:var(--c3-cell);
  margin-top:calc(var(--c3-cell) / -2); margin-left:calc(var(--c3-cell) / -2);
  transform-style:preserve-3d;
}
.cube3d-cubie-face {
  position:absolute; top:50%; left:50%;
  width:var(--c3-cell); height:var(--c3-cell);
  margin-top:calc(var(--c3-cell) / -2); margin-left:calc(var(--c3-cell) / -2);
  border: var(--c3-gap) solid #000;
}
.cube3d-cubie-face--inner { background:#1c1c1e; cursor:default; }
.cube3d-sticker { border-radius:5px; cursor:pointer; transition:filter .1s; }
.cube3d-sticker:hover { filter:brightness(1.15) !important; }
/* Fake directional light: brighter on top, dimmer toward the bottom/back. */
.cube3d-sticker.face-shade-U { filter: brightness(1.08); }
.cube3d-sticker.face-shade-F,
.cube3d-sticker.face-shade-R { filter: brightness(1.00); }
.cube3d-sticker.face-shade-L,
.cube3d-sticker.face-shade-B { filter: brightness(0.94); }
.cube3d-sticker.face-shade-D { filter: brightness(0.86); }
.cube3d-hint { font-size:.72rem; color:var(--text-tertiary); }

/* ── Liquid glass ambient background for the cube editor — a soft colored
   gradient instead of a flat white, so translucent glass panels floating
   on top have something to show through them. Opaque, so it fully
   occludes the camera feed running underneath. ─────────────────────── */
.cube-editor {
  background:
    radial-gradient(ellipse 60% 50% at 15% 20%, rgba(120,150,255,.35), transparent 60%),
    radial-gradient(ellipse 55% 45% at 85% 15%, rgba(255,150,220,.28), transparent 60%),
    radial-gradient(ellipse 60% 55% at 75% 85%, rgba(140,220,255,.30), transparent 60%),
    radial-gradient(ellipse 50% 50% at 20% 85%, rgba(180,160,255,.25), transparent 60%),
    #eef0f6;
}

/* ── Persistent 3-step progress indicator (Scanner / Édition / Résolution),
   floats above every sub-view — see setPhase() in app.js. ───────────── */
.step-header {
  position: absolute; top: var(--sp-4); left: 50%; transform: translateX(-50%); z-index: 40;
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; border-radius: var(--r-full);
  background: rgba(255,255,255,.55);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid rgba(255,255,255,.6);
  box-shadow: var(--shadow-md);
}
.step-icon {
  width: 34px; height: 34px; border-radius: var(--r-full); cursor: default;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--text-tertiary);
  transition: background .15s, color .15s, transform .1s;
}
.step-icon svg { width: 16px; height: 16px; }
.step-icon.active { background: var(--accent); color: #fff; }
/* "done" steps are clickable — they navigate back to that step (see the
   stepIcon click handlers in app.js) — so they get a pointer + hover cue. */
.step-icon.done   { background: #eaf7ec; color: #1f8a3d; cursor: pointer; }
.step-icon.done:hover  { background: #d9f2dd; }
.step-icon.done:active { transform: scale(.92); }
.step-line { width: 26px; height: 2px; background: var(--border); border-radius: 1px; transition: background .15s; }
.step-line.done { background: #1f8a3d; }

/* ── Step-1 "Passer" skip button — right edge, vertically centered ──────── */
/* Fond gris (pas l'accent bleu) — et masqué (voir app.js) dès qu'au moins
   une face a été scannée : "passer" ne fait plus vraiment sens une fois le
   scan commencé. */
.skip-btn {
  position: absolute; top: 50%; right: var(--sp-4); transform: translateY(-50%);
  z-index: 5;
  padding: .9rem 1.5rem; font-size: 1.1rem;
  background: var(--text-tertiary); color: #fff; border-color: transparent;
}
.skip-btn:hover { background: var(--text-secondary); }
/* .btn-pill:active seul (transform: scale(.96)) a une spécificité plus
   forte (classe + pseudo-classe) que .skip-btn seul, donc écrasait le
   translateY(-50%) du centrage vertical au clic — le bouton "sautait" vers
   le bas le temps du clic. On combine les deux transforms ici. */
.skip-btn:active { transform: translateY(-50%) scale(.96); }
.skip-btn svg { width: 20px; height: 20px; }

/* ── Patron 2D du cube (croix), gauche, centré verticalement — voir
   updateScanNet() dans app.js pour la logique de coloriage. ──────────── */
.scan-net {
  position: absolute; top: 50%; left: var(--sp-4); transform: translateY(-50%);
  z-index: 5;
  display: grid;
  grid-template-columns: repeat(4, 46px);
  grid-template-rows: repeat(3, 46px);
  grid-template-areas:
    ".  .  u  ."
    "l  f  r  b"
    ".  .  d  .";
  gap: 6px;
}
.scan-net-cell {
  border: 2px solid rgba(255,255,255,.55);
  border-radius: 6px;
  background: rgba(0,0,0,.25);
  transition: background .25s ease, border-color .25s ease;
}
.scan-net-cell.scanned { background: #00e64d; border-color: #00e64d; }
.scan-net-u { grid-area: u; }
.scan-net-l { grid-area: l; }
.scan-net-f { grid-area: f; }
.scan-net-r { grid-area: r; }
.scan-net-b { grid-area: b; }
.scan-net-d { grid-area: d; }

/* ── Unified cube editor (manual entry / post-scan check / solution) ──── */
.cube-editor {
  position: absolute; inset: 0; z-index: 20; display: flex; flex-direction: column; align-items: center;
}
#cubeEditor .cube3d-scene { --c3-size: clamp(204px, 28.9vh, 272px); }
/* Solution sub-view has no card dock at the bottom anymore (just a bare
   filmstrip), so it needs much less reserved space — let the cube grow
   bigger and sit closer to true center. */
#cubeEditorStage.solution-mode .cube3d-scene { --c3-size: clamp(238px, 35.7vh, 323px); }
/* Both sub-views' controls (.edit-layer, .solution-dock/.speed-corner/etc.)
   are absolutely positioned overlays, not part of the flex flow, so the
   cube is the stage's only flex-participating child and sits at true
   center — no reserved padding needed on either side. */
.cube-editor-stage {
  flex: 1 1 auto; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; position: relative;
}

/* Floating speed control, top-left of the editor. */
.speed-corner {
  position: absolute; top: var(--sp-4); left: var(--sp-4); z-index: 6;
  display: flex; flex-direction: column; gap: 6px;
  padding: 8px 10px; border-radius: var(--r-lg);
  background: rgba(255,255,255,.55);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid rgba(255,255,255,.6);
  box-shadow: var(--shadow-md);
}
.speed-corner-label { font-size: .68rem; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .06em; font-weight: 600; }

/* Vertical move list — absolutely positioned to the right, OUTSIDE the flex
   flow, so it never shifts the cube off true center (the cube is the only
   flex-participating child of .cube-editor-stage). No card background, just
   the letters, sitting well clear of the cube's rotated 3D footprint. */
.solution-dock {
  position: absolute; top: 50%; right: var(--sol-dock-right); transform: translateY(-50%);
  z-index: 3;
  width: 150px; height: min(64vh, 480px);
  display: flex; align-items: center; justify-content: center;
}

/* Play/pause — mirrors .solution-dock on the opposite side (same distance
   from its edge, via the shared --sol-dock-right var), same bare styling:
   just a big symbol, no card. Blue = paused (click to start); gray =
   playing (click to stop). */
.play-dock {
  position: absolute; top: 50%; left: var(--sol-dock-right); transform: translateY(-50%);
  z-index: 3;
  width: 116px; height: min(64vh, 480px);
  display: flex; align-items: center; justify-content: center;
}
.play-dock-btn {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--accent);
  transition: color .15s, transform .1s;
  outline: none;
}
.play-dock-btn:hover  { color: var(--accent-hover); }
.play-dock-btn:active { transform: scale(.92); }
.play-dock-btn.is-playing { color: var(--text-tertiary); }
.play-dock-btn.is-playing:hover { color: var(--text-secondary); }
.play-dock-btn svg { width: 88px; height: 88px; }
/* Step counter — sits just right of the move letters (closer to the screen
   edge than .solution-dock, since CSS `right` counts distance FROM that
   edge). Plain gray, no card: a diagonal fraction, current step top-left,
   a slash bar through the middle, total bottom-right — no "coups" label,
   just the two numbers. Fixed box size + absolutely-positioned corners
   (not flex align-self) is what actually produces the diagonal offset —
   a flex column shrinks to its content width, leaving no room to shift
   start/end apart when both numbers are similarly narrow. */
.step-counter-corner {
  position: absolute; top: 50%; right: calc(var(--sol-dock-right) - 78px); transform: translateY(-50%);
  z-index: 6;
  width: 64px; height: 74px;
  color: var(--text-tertiary);
}
/* Mesuré (DevTools) avec "0/25" après un premier essai à 68x78 : les deux
   nombres ne se touchaient déjà plus (~15px d'écart) mais restaient serrés
   visuellement, la barre les frôlant des deux côtés. Coins poussés hors de
   la boîte nominale (décalages négatifs) pour un vrai espace de respiration,
   quel que soit le nombre de chiffres, puis toute la composition réduite
   proportionnellement. white-space:nowrap évite tout retour à la ligne. */
#stepCounterNum, #stepCounterDen {
  position: absolute; font-size: 1.3rem; font-weight: 800; line-height: 1;
  white-space: nowrap;
}
#stepCounterNum { top: -4px; left: -6px; }
#stepCounterDen { bottom: -4px; right: -6px; }
.step-counter-slash {
  position: absolute; top: 50%; left: 50%;
  width: 5px; height: 62px; border-radius: 999px;
  background: var(--text-tertiary);
  transform: translate(-50%, -50%) rotate(28deg);
}

/* ── Edit sub-view: independent floating pieces, no surrounding card —
   palette dock left of the cube, scramble row + heading centered, solve
   button bottom-right. Transparent wrapper so it never blocks clicks on
   the cube behind/between its pieces; each floating piece re-enables its
   own pointer events. */
.edit-layer { position: absolute; inset: 0; pointer-events: none; }

/* Scramble buttons, bottom-center — bare, no card */
.moves-dock {
  position: absolute; left: 50%; bottom: var(--sp-5); transform: translateX(-50%); z-index: 2;
  pointer-events: auto;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  max-width: calc(100% - 32px);
}
.moves-row { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }
.btn-move {
  padding: .35rem .7rem; border: 1px solid var(--border); border-radius: var(--r-sm);
  font-size: .8rem; font-weight: 700; cursor: pointer;
  background: rgba(255,255,255,.7);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  color: var(--text-primary); transition: background .12s, transform .1s;
}
.btn-move:hover  { background: #fff; }
.btn-move:active { transform: scale(.93); }

/* Color palette, left of the cube — a 2-column x 3-row grid of bigger,
   rounded-square swatches. Each is a plain <div> (its background is set
   directly from RGB in app.js — reliable and identical across browsers,
   unlike trying to reskin a native <input type="color"> as the visible
   swatch itself, which renders inconsistently — some browsers refuse to
   size/color it once its native appearance is stripped, showing solid
   black instead). Two-click flow (see selectColor()/paintSticker() and
   openColorEditPopover() in app.js): clicking an unselected swatch just
   selects it for sticker-painting (white palette icon appears on top);
   clicking the *already selected* swatch opens the custom color-edit
   popover (.color-edit-popover) instead of a native <input type="color">. */
.palette-dock {
  position: absolute; top: 50%; left: clamp(20px, 5vw, 56px); transform: translateY(-50%); z-index: 2;
  pointer-events: auto;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.palette-label   { font-size: .68rem; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .06em; font-weight: 600; }
.palette-row     { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.palette-swatch {
  position: relative; overflow: hidden;
  width: 76px; height: 76px; border-radius: 18px;
  border: 3px solid transparent; cursor: pointer; box-shadow: 0 0 0 1px var(--border);
  transition: transform .12s, box-shadow .12s;
  display: flex; align-items: center; justify-content: center;
}
.palette-swatch:hover    { transform: scale(1.05); }
.palette-swatch.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); transform: scale(1.08); }
.palette-swatch-icon {
  width: 30px; height: 30px; color: #fff;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.5));
  opacity: 0; transform: scale(.6); transition: opacity .15s ease, transform .15s ease;
  pointer-events: none;
}
.palette-swatch.selected .palette-swatch-icon { opacity: 1; transform: scale(1); }
@keyframes nudge { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }
.palette-row.nudge { animation: nudge .3s ease-in-out; }

/* Popover d'édition de couleur (2e clic sur une couleur déjà sélectionnée).
   position:fixed avec top/left posés en JS (voir openColorEditPopover() dans
   app.js) pour démarrer exactement à la position de la pastille cliquée
   (coin haut-gauche aligné) et l'envelopper, avec repli à gauche si ça
   déborderait de l'écran à droite. IMPORTANT : doit rester un enfant direct
   d'un ancêtre SANS transform (voir commentaire HTML) pour que ce
   position:fixed se calcule bien par rapport à toute la fenêtre. Rangée
   horizontale : la pastille de couleur (même taille que dans la palette) à
   gauche, les curseurs à droite. */
.color-edit-popover {
  position: fixed;
  z-index: 4;
  pointer-events: auto; /* .edit-layer met pointer-events:none — sans ce
    contrepoids (même chose que .palette-dock plus haut), les clics sur les
    curseurs traversaient jusqu'à ce qu'il y a derrière, ce que le clic
    "en dehors -> fermeture" prenait pour un clic hors du popover. */
  display: flex; flex-direction: row; align-items: center; gap: 18px;
  padding: 14px 20px 14px 14px;
  border-radius: var(--r-lg);
  background: rgba(255,255,255,.92);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  box-shadow: var(--shadow-md);
}
.color-edit-chip {
  flex: none; width: 76px; height: 76px; border-radius: 18px;
  box-shadow: inset 0 0 0 1px var(--border);
}
/* Pas de texte "Teinte"/"Luminosité" — les dégradés des pistes se suffisent
   à eux-mêmes (arc-en-ciel = teinte, noir->couleur->blanc = luminosité). */
.color-edit-sliders { width: 220px; display: flex; flex-direction: column; gap: 16px; }
.color-edit-slider {
  width: 100%; -webkit-appearance: none; appearance: none;
  height: 20px; border-radius: 999px; outline: none; cursor: pointer;
  background-color: transparent; /* le dégradé réel vient de .color-edit-slider--hue/--light */
}
/* Chrome/Safari dessinent quand même leur propre rainure par défaut sur
   ::-webkit-slider-runnable-track même quand l'input a appearance:none —
   c'est cette rainure (pas mon dégradé) qui apparaissait comme une barre
   noire au milieu de la piste. Il faut la neutraliser explicitement pour
   que seul le dégradé posé sur l'input lui-même reste visible. */
.color-edit-slider::-webkit-slider-runnable-track {
  height: 20px; border-radius: 999px; background: transparent;
}
.color-edit-slider::-moz-range-track {
  height: 20px; border-radius: 999px; background: transparent;
}
.color-edit-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 30px; height: 30px; border-radius: 50%;
  margin-top: -5px; /* (piste 20px - curseur 30px) / 2, pour le centrer verticalement */
  background: #fff; border: 1.5px solid rgba(0,0,0,.12); box-shadow: 0 1px 4px rgba(0,0,0,.3);
  cursor: pointer;
}
.color-edit-slider::-moz-range-thumb {
  width: 30px; height: 30px; border-radius: 50%;
  background: #fff; border: 1.5px solid rgba(0,0,0,.12); box-shadow: 0 1px 4px rgba(0,0,0,.3);
  cursor: pointer;
}
.color-edit-slider--hue {
  background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
}

/* Solve button + validation error, bottom-right — the "Résoudre" pill
   mirrors step 1's "Passer" pill (same corner, same treatment). */
.solve-corner-stack {
  position: absolute; bottom: var(--sp-4); right: var(--sp-4); z-index: 5;
  pointer-events: auto;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
}
.btn-solve {
  width: 100%; padding: .65rem; border: none; border-radius: var(--r-sm); font-size: .92rem;
  cursor: pointer; font-weight: 600;
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  transition: background .15s, transform .1s;
}
.btn-solve:hover    { background: var(--accent-hover); }
.btn-solve:active   { transform: scale(.98); }
.btn-solve:disabled { background: var(--surface-2); color: var(--text-tertiary); cursor: not-allowed; }
.btn-solve svg { width: 16px; height: 16px; }
.btn-solve-sm { padding: .45rem .8rem; font-size: .82rem; }
.btn-solve-sm svg { width: 14px; height: 14px; }
.solve-error {
  max-width: min(320px, calc(100vw - 32px)); text-align: right;
  font-size: .78rem; color: var(--error); background: var(--error-bg);
  border-radius: var(--r-sm); padding: 8px 10px;
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
}
.solve-error.solve-success { color: #1f8a3d; background: #eaf7ec; }

/* ── Solution display: a vertical list of moves, right of the cube. Plain
   native scrolling (touch swipe / trackpad / wheel) — each letter is also
   directly clickable. Whichever move is centered gets magnified like a
   loupe, smoothly, via a live per-frame transform computed in app.js
   (renderFilmstrip) — scale/opacity here are inline styles, not CSS.
   Top/bottom padding is set from JS (half the visible height), NOT here:
   percentage padding-top/bottom is always resolved against the container's
   WIDTH per spec, never its height, so a "45% 0" here would be ~45% of this
   ~110px-wide column — nowhere near enough vertical room to actually let
   the first/last move scroll all the way to center. */
.solution-filmstrip {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  overflow-y: auto; overflow-x: hidden; scroll-snap-type: y proximity;
  -ms-overflow-style: none; scrollbar-width: none;
}
.solution-filmstrip::-webkit-scrollbar { display: none; }
/* Each row is a fixed-size hit target (never transformed) so clicks always
   land on the exact move under the pointer — only the inner label is
   magnified/faded, which can visually overflow into a neighboring row
   without ever stealing that row's clicks (pointer-events: none on it). */
.move-chip {
  flex: 0 0 auto; scroll-snap-align: center;
  width: 100%; height: 64px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.move-chip-label {
  font-size: 1.8rem; font-weight: 700; color: var(--text-tertiary);
  pointer-events: none;
  transition: color .15s, transform .12s ease-out, opacity .12s ease-out;
}
.move-chip.active .move-chip-label { color: var(--accent); }
.move-chip.done   .move-chip-label { color: var(--text-secondary); }
.move-chip.move-check.active .move-chip-label { color: var(--success); }

.btn-nav         { display:flex; align-items:center; justify-content:center; padding: 6px 14px; border: 1px solid var(--border); border-radius: var(--r-sm); font-size: .85rem; cursor: pointer; background: var(--surface); color: var(--text-primary); transition: background .1s; }
.btn-nav:hover   { background: var(--surface-2); }
.btn-nav:disabled { opacity: .35; cursor: not-allowed; }
.btn-nav svg { width: 16px; height: 16px; }
.solution-filmstrip.locked .move-chip { pointer-events: none; }

/* ── Speed / autoplay controls ──────────────────────────── */
.speed-btns { display: flex; gap: 4px; }
.speed-btn.active { background: var(--accent); color: #fff; border-color: var(--accent-active); }
#playPauseBtn { width: 100%; padding: .5rem 1.1rem; }

@media (max-width: 700px) {
  #cubeEditor .cube3d-scene { --c3-size: clamp(170px, 25.5vh, 221px); }
  .ra-cube3d-scene { --ra-c3-size: clamp(140px, 28vh, 190px); }
  .ra-viewfinder   { --ra-vf-size: clamp(140px, 28vh, 190px); }
  .scan-net { grid-template-columns: repeat(4, 32px); grid-template-rows: repeat(3, 32px); gap: 4px; }
  .palette-swatch { width: 44px; height: 44px; }
  .palette-row { gap: 10px; }
  .palette-dock { left: var(--sp-2); }
  .solution-dock { width: 110px; height: min(56vh, 380px); }
  .move-chip-label { font-size: 1.35rem; }
  .step-header { padding: 6px 10px; gap: 5px; }
  .step-icon { width: 28px; height: 28px; }
  .step-line { width: 18px; }
}
