/* ===========================================================
   Tagda Timer Ã¢â‚¬â€ panels, controls, overlays, history, charts
   =========================================================== */

/* ---------------- glass panel ---------------- */
.panel {
  position: relative;
  display: flex; flex-direction: column; gap: 12px;
  padding: var(--pad);
  border-radius: var(--radius);
  border: var(--border-w) solid color-mix(in srgb, var(--border) 90%, transparent);
  background: color-mix(in srgb, var(--surface) calc(var(--panel-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--panel-blur)) saturate(1.4);
  -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(1.4);
  box-shadow: var(--shadow);
  min-width: 0; min-height: 0;
}
.panel::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  background: linear-gradient(160deg, color-mix(in srgb, #fff 7%, transparent), transparent 42%);
  opacity: .5;
}
.panel-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: .68rem; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-faint);
}
.panel-sub { font-weight: 500; letter-spacing: .06em; text-transform: none; font-size: .72rem; }

/* ---------------- buttons ---------------- */
.icon-btn {
  width: 34px; height: 34px; display: grid; place-items: center;
  border-radius: 10px; color: var(--text-dim);
  border: 1px solid transparent;
  transition: all var(--dur-fast) var(--ease);
}
.icon-btn svg { width: 17px; height: 17px; }
.icon-btn:hover { color: var(--text); background: color-mix(in srgb, var(--text) 8%, transparent); border-color: var(--border); transform: translateY(-1px); }
.icon-btn:active { transform: translateY(0) scale(.94); }
.icon-btn.on { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); border-color: color-mix(in srgb, var(--accent) 34%, transparent); }

/* The box never changes shape between "square icon" and "pill with a name"
   at the CSS level Ã¢â‚¬â€ .account-box just adds the extra room and lets
   .account-username decide whether there's anything to show; sync-ui.js
   toggles that content, not this rule. Modeled on .seg-btn (same height,
   same border/background treatment) rather than the plain icon squares
   either side of it, on purpose Ã¢â‚¬â€ a box reads as "this is your thing", a
   bare icon among a row of bare icons doesn't. */
.account-box {
  width: auto; display: flex; align-items: center; padding: 0 10px 0 8px; gap: 7px;
  border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
  background: color-mix(in srgb, var(--surface) 45%, transparent);
}
.account-box.on { border-color: color-mix(in srgb, var(--border) 80%, transparent); background: color-mix(in srgb, var(--surface) 45%, transparent); }
.account-glyph { display: grid; place-items: center; width: 17px; height: 17px; flex: none; }

/* The SOTD chip. Same box as .account-box above so the two sit as a pair
   rather than as two unrelated experiments, but in gold rather than the bar's
   ordinary grey: this is the one control in the row that is about today and
   stops being available tonight, and nothing else in the bar has that
   property. Gold is hard-coded rather than themed for the same reason a
   medal is gold Ã¢â‚¬â€ it is the meaning, not the palette. */
.sotd-box {
  width: auto; display: flex; align-items: center; padding: 0 10px 0 8px; gap: 6px;
  border: 1px solid color-mix(in srgb, #f2c14e 42%, transparent);
  background: color-mix(in srgb, #f2c14e 13%, transparent);
  color: #f2c14e;
  /* Clips the sweep below to the pill. */
  position: relative; overflow: hidden;
}

/* The shine plays only while there is something to do Ã¢â‚¬â€ `.done` cancels it
   below, which is the whole of "shine until it is done".

   A sweep across the pill rather than a pulsing glow, because the bar sits
   above a timer people stare at Ã¢â‚¬â€ something that brightens and dims in place
   is caught by peripheral vision every cycle, whereas a highlight that passes
   and is gone for six seconds is noticed once and then ignored. */
.sotd-box::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(105deg,
    transparent 38%,
    color-mix(in srgb, #f2c14e 38%, transparent) 50%,
    transparent 62%);
  transform: translateX(-120%);
  animation: sotdShine 6s var(--ease) infinite;
  pointer-events: none;
}
@keyframes sotdShine {
  0%   { transform: translateX(-120%); }
  /* Held off-screen for most of the cycle: the sweep is the punctuation, not
     the sentence. */
  22%, 100% { transform: translateX(120%); }
}
/* Nothing here is load-bearing, so it is the first thing to go for anyone who
   has asked the OS for less movement. */
@media (prefers-reduced-motion: reduce) {
  .sotd-box::after { animation: none; opacity: 0; }
}
.sotd-box:hover {
  border-color: color-mix(in srgb, #f2c14e 62%, transparent);
  background: color-mix(in srgb, #f2c14e 20%, transparent);
  color: #f7d98a;
}
.sotd-glyph { display: grid; place-items: center; width: 17px; height: 17px; flex: none; }
.sotd-glyph svg { width: 17px; height: 17px; }
.sotd-label {
  font-size: .7rem; font-weight: 700; letter-spacing: .07em;
  color: inherit;
}
/* Once today's attempt is in, main.js adds `done` and the chip stops selling
   itself: the gold, the label and the sweep all go and it settles into the
   same quiet outline as the icons either side of it.

   It stays a button. An earlier version hid it entirely, which removed the
   only route back into the window and so made the board you had just earned a
   place on unreachable until midnight Ã¢â‚¬â€ the way in has to outlive the
   invitation. */
.sotd-box.done {
  border-color: color-mix(in srgb, var(--border) 80%, transparent);
  background: color-mix(in srgb, var(--surface) 45%, transparent);
  color: var(--text-faint);
  padding: 0;
  width: 34px; justify-content: center;
}
.sotd-box.done:hover {
  border-color: color-mix(in srgb, var(--border) 80%, transparent);
  background: color-mix(in srgb, var(--surface) 45%, transparent);
  color: var(--text);
}
.sotd-box.done .sotd-label { display: none; }
.sotd-box.done::after { content: none; }
.account-username {
  font-size: .78rem; font-weight: 550; color: var(--text-dim);
  max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.account-box:hover .account-username { color: var(--text); }

.account-avatar { width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex: none; }
.account-initial {
  width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center;
  font-size: .68rem; font-weight: 650; background: var(--accent); color: var(--on-accent); flex: none;
}

.seg { display: flex; }
/* The centre group is `overflow: hidden`, so a picker that does not fit is not
   truncated Ã¢â‚¬â€ it is cut off mid-button, and the last one disappears entirely.
   A flex item will not shrink below its content unless told to, and `.seg` is
   the item; `.seg-btn span` already knows how to ellipsis once it is allowed
   to. Without this the row silently loses a picker between 861px and roughly
   1000px, which is an ordinary laptop window. */
.topbar-center .seg { min-width: 0; }
.seg-btn {
  display: flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 11px; border-radius: 10px;
  font-size: .8rem; font-weight: 550; color: var(--text-dim);
  border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
  background: color-mix(in srgb, var(--surface) 45%, transparent);
  backdrop-filter: blur(10px);
  transition: all var(--dur-fast) var(--ease);
  max-width: 220px; min-width: 0;
}
.seg-btn:hover { color: var(--text); border-color: var(--text-faint); transform: translateY(-1px); }
.seg-btn .ico { width: 14px; height: 14px; opacity: .7; flex: none; }
.seg-btn .chev { width: 13px; height: 13px; opacity: .5; flex: none; }
.seg-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.seg-btn.active-mode { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 40%, transparent); }

.ghost-btn {
  display: inline-flex; align-items: center; gap: 6px;
  height: 30px; padding: 0 9px; border-radius: 9px;
  color: var(--text-faint); font-size: .78rem; font-weight: 500;
  border: 1px solid transparent;
  transition: all var(--dur-fast) var(--ease);
}
.ghost-btn svg { width: 15px; height: 15px; }
.ghost-btn:hover { color: var(--text); background: color-mix(in srgb, var(--text) 8%, transparent); }
.ghost-btn.danger:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }
.ghost-btn.sm { height: 26px; font-size: .72rem; padding: 0 8px; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 38px; padding: 0 16px; border-radius: 11px;
  font-size: .84rem; font-weight: 600;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  border: 1px solid var(--border); color: var(--text);
  transition: all var(--dur-fast) var(--ease);
}
.btn:hover { transform: translateY(-1px); border-color: var(--text-faint); }
.btn:active { transform: translateY(0) scale(.98); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn.primary:hover { box-shadow: 0 8px 24px -8px var(--accent); }
.btn.danger { background: color-mix(in srgb, var(--danger) 15%, transparent); border-color: color-mix(in srgb, var(--danger) 40%, transparent); color: var(--danger); }
.btn.full { width: 100%; }

/* ---------------- stats panel ---------------- */
/* Three across rather than two: seven figures in two columns was four ragged
   rows that ran off the bottom of the panel. */
.stat-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 5px; flex: 0 0 auto; }
@media (max-width: 1080px) {
  .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat.wide { grid-column: 1 / -1; }
  /* Seven figures into two columns leaves ao100 sitting in the left half of a
     row with a hole beside it, directly above a consistency bar that already
     spans the width -- a gap in the middle of the block with nothing to be.
     It takes the whole row too, so the two full-width rows read as the pair
     they are and the tiles above stay a clean three-by-two. */
  .stat[data-k="ao100"] { grid-column: 1 / -1; }
}
.stat {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 9px; border-radius: var(--radius-sm);
  min-width: 0;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid transparent;
  transition: all var(--dur) var(--ease);
  cursor: pointer;
}
.stat:hover {
  background: color-mix(in srgb, var(--text) 9%, transparent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.stat:hover .stat-k { color: var(--accent); }
.stat-k { font-size: .55rem; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: var(--text-faint); }
.stat-v { font-family: var(--font-mono); font-size: calc(.88rem * var(--sidebar-text)); overflow: hidden; text-overflow: ellipsis; font-weight: 600; font-variant-numeric: tabular-nums; }
.stat.flash-up   { animation: statUp 700ms var(--ease); }
.stat.flash-down { animation: statDown 700ms var(--ease); }
@keyframes statUp {
  0% { background: color-mix(in srgb, var(--ok) 30%, transparent); transform: translateY(3px); }
  100% { background: color-mix(in srgb, var(--text) 4%, transparent); transform: none; }
}
@keyframes statDown {
  0% { background: color-mix(in srgb, var(--danger) 26%, transparent); transform: translateY(-3px); }
  100% { background: color-mix(in srgb, var(--text) 4%, transparent); transform: none; }
}

.mini-chart-wrap { flex: 1 1 70px; min-height: 0; display: flex; }
#mini-trend { width: 100%; height: 100%; overflow: visible; }
.spark-area { fill: url(#sparkGrad); }
.spark-line { fill: none; stroke: var(--accent); stroke-width: 1.6; vector-effect: non-scaling-stroke; }
.spark-ao   { fill: none; stroke: var(--accent-2); stroke-width: 1.3; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; opacity: .85; }
.spark-pb   { stroke: var(--gold); stroke-width: 1; stroke-dasharray: 2 4; vector-effect: non-scaling-stroke; opacity: .7; }
.spark-dot  { fill: var(--accent); }

/* Consistency spans two columns so it pairs with the odd cell out and the last
   row is full, instead of leaving a lone box and a stray strip. */
.stat.wide { grid-column: span 2; flex-direction: row; align-items: center; gap: 8px; flex-wrap: wrap; }
.stat.wide .stat-k { flex: 0 0 auto; }
.stat.wide .stat-v { margin-left: auto; }
.stat.wide .cons-bar { flex: 1 1 100%; }
.cons-bar { height: 4px; border-radius: 99px; background: color-mix(in srgb, var(--text) 8%, transparent); overflow: hidden; }
.cons-fill { height: 100%; width: 0%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width var(--dur-slow) var(--ease); }
.cons-meta { display: flex; justify-content: space-between; font-size: .62rem; letter-spacing: .12em; text-transform: uppercase; color: var(--text-faint); font-weight: 600; }

/* ---------------- scramble preview (flat, no panel) ---------------- */
#panel-cube {
  /* --cube-edge-* is placePreview() growing the inset so the close button and
     the 2D/3D switch, which hang off the sides, stay clear of the edge too. */
  position: fixed; right: max(18px, var(--cube-edge-right, 0px)); bottom: 18px; z-index: 12;
  width: calc(clamp(96px, 12vw, 150px) * var(--cube-scale));
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  /* Interactive now: this is the drag handle as well as the 2D/3D toggle's
     host, so it has to be hit-testable. It sits in a corner well clear of the
     timer, and none of the timer's own pointer handlers reach into it. */
  pointer-events: auto;
  cursor: default;                         /* the four-way "move me" arrow */
  touch-action: none;                   /* let the drag own the gesture */
}
#panel-cube.dragging { cursor: move; user-select: none; }

/* The cube keeps its own pointer events so it can be spun to look at the back,
   so dragging the widget lives on this grip instead of the whole panel. */
#cube-grip {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 13px; margin-bottom: 1px;
  border-radius: 6px; cursor: move;
  color: var(--text-faint);
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  opacity: 0; transition: opacity var(--dur) var(--ease), color var(--dur-fast) var(--ease);
}
#cube-grip svg { width: 15px; height: 15px; }
#panel-cube:hover #cube-grip { opacity: .75; }

/* Dismiss, in the corner of the thing being dismissed. Pinned rather than in
   the flow so it cannot change the widget's measured height Ã¢â‚¬â€ placePreview
   works off that rect, and a row that appeared on hover would have moved the
   rails every time the pointer crossed it. */
#cube-close {
  position: absolute; top: -2px; right: -4px; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  color: var(--text-faint);
  background: color-mix(in srgb, var(--surface) 70%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  opacity: 0; transition: opacity var(--dur) var(--ease), color var(--dur-fast) var(--ease);
}
#cube-close svg { width: 11px; height: 11px; }
#panel-cube:hover #cube-close { opacity: .8; }
#cube-close:hover { opacity: 1; color: var(--danger); }
#panel-cube.dragging #cube-close { opacity: 0; pointer-events: none; }
#cube-grip:hover { opacity: 1; color: var(--text); }
#cube-holder { cursor: grab; touch-action: none; }
#cube-holder:active { cursor: grabbing; }
#btn-reset-orbit { font-size: .74rem; line-height: 1; padding: 1px 6px; }
#panel-cube.dragging .view-toggle { opacity: 0; }
/* Once dragged, JS pins it with left/top and these have to get out of the way. */
#panel-cube[data-placed="true"] { right: auto; bottom: auto; }
#cube-holder {
  width: 100%; height: calc(clamp(96px, 12vw, 150px) * var(--cube-scale));
  position: relative; display: grid; place-items: center; overflow: hidden;
}
#cube-holder twisty-player {
  width: 100%; height: 100%; max-height: 100%; min-height: 0;
  /* No `display` here. twisty-player sets `:host { display: grid }` and its
     internal wrapper takes its height from that grid; overriding the host to
     `block` collapsed the canvas to 150x0 Ã¢â‚¬â€ a correctly sized element with
     nothing painted in it, which is why the preview looked dead. */
  /* Graded to match the rendered logo, which is the look worth having: the
     library gives no hook for sticker colours or materials Ã¢â‚¬â€ those are baked
     into its own shaders Ã¢â‚¬â€ so the grade happens on the composited output
     instead. Saturation and contrast turn its flat sticker colours into the
     logo's glossy plastic, and the heavier shadow sits the cube on something
     rather than floating it. */
  filter: saturate(1.34) contrast(1.14) brightness(1.05)
          drop-shadow(0 14px 22px rgba(0,0,0,.6))
          drop-shadow(0 2px 3px rgba(0,0,0,.35));
}
#cube-fallback {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  width: 100%; height: 100%; padding: 10px;
  color: var(--text-dim); font-size: .66rem; font-weight: 500; text-align: center;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
#cube-fallback .ico-big { width: 26px; height: 26px; opacity: .55; color: var(--text-faint); }

.view-toggle {
  display: flex; gap: 1px; padding: 2px; border-radius: 7px;
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  /* Was opacity:0 until hover, on a panel that could not be hovered Ã¢â‚¬â€ which is
     how the 2D/3D switch went missing. It is always readable now. */
  opacity: .62; pointer-events: auto;
  transition: opacity var(--dur) var(--ease);
}
#panel-cube:hover .view-toggle { opacity: 1; }

/* Lives here, not in base.css: components.css loads later, and an equally
   specific rule in the earlier file loses regardless of the media query. */
/* Floating and draggable, the same as on a desktop Ã¢â‚¬â€ it is a preview you park
   where it suits the way you hold the phone, and pinning it into the document
   flow took a whole row away from the timer for something 80px tall.
   What it must not do is sit on the scramble, which is what `top: 54px` did:
   at phone width the notation wraps to three lines and fills exactly that
   space. Anchored to the bottom instead, above the two tiles, in the empty
   half of the timer area. */
@media (max-width: 860px) {
  #panel-cube {
    width: calc(clamp(64px, 17vw, 88px) * var(--cube-scale));
    top: auto;
    /* 16px before the script has measured: the close button's 8px overhang
       plus 8px clear, which is what placePreview() lands on anyway. */
    right: max(10px, var(--cube-edge-right, 16px));
    bottom: calc(34vh + 22px);
  }
  #cube-holder { height: calc(clamp(64px, 17vw, 88px) * var(--cube-scale)); }
  /* Both of these were revealed on hover, on a device that has none Ã¢â‚¬â€ which
     left the widget with no visible way to move it and no visible 2D/3D
     switch. The grip is also a touch target now, not a 34x13 sliver. */
  #cube-grip { opacity: .55; width: 52px; height: 20px; }
  #cube-close { opacity: .75; width: 24px; height: 24px; top: -4px; right: -8px; }
  #cube-close svg { width: 13px; height: 13px; }
  .view-toggle { opacity: 1; }

  /* Expanded, the stats tile is 46vh rather than 34, and the preview was left
     floating on top of it. Only while it is still parked in its corner Ã¢â‚¬â€ once
     dragged it is wherever you put it. */
  #app:has(#panel-stats[data-collapsed="false"]) #panel-cube:not([data-placed]) {
    bottom: calc(46vh + 22px);
  }

}

/* A phone on its side has ~375px of height for everything. The preview is the
   first thing to go: it is the one piece here you can rebuild in your head from
   the scramble two rows above it. (Here rather than base.css for the same
   reason as the block above Ã¢â‚¬â€ `display: flex` is declared in this file.) */
@media (max-width: 940px) and (max-height: 500px) and (orientation: landscape) {
  #panel-cube { display: none; }
}

.view-toggle button {
  font-size: .58rem; font-weight: 700; letter-spacing: .08em;
  padding: 2px 7px; border-radius: 5px; color: var(--text-faint);
  transition: all var(--dur-fast) var(--ease);
  /* The panel around this is a drag handle showing the four-way move cursor.
     These are ordinary buttons and must not inherit it, or they read as
     something you shove rather than something you press. */
  cursor: pointer;
}
.view-toggle { cursor: default; }
.view-toggle button.on { background: color-mix(in srgb, var(--text) 13%, transparent); color: var(--text); }

/* ---------------- times list (left column) ---------------- */
#panel-times {
  gap: 8px;
  /* One width for every average column, shared by the heading and every row, so
     each label sits over its numbers rather than near them. Content-sized
     `auto` could not do that: the heading's letters and a row's "13.66" are
     different widths, and on a narrow sidebar they drifted a good 20px apart.
     Wide enough for a minutes-long average at any times-list scale Ã¢â‚¬â€ and a
     touch narrower than when it was the only one, because two of them have to
     fit beside the times in a sidebar nobody widened. */
  --ao-col: calc(2.9rem * var(--times-scale));
  /* Set by applyHistGrid(): one track per average column, so the heading row
     and the chips cannot disagree about how many there are. */
  /* Tighter than it was with one average column: the columns have to share a
     sidebar nobody widened, and 5px between them is what buys the time column
     enough room for a solve that ran past a minute. */
  --hist-gap: 5px;
  --hist-grid: 24px minmax(0, 1fr) var(--ao-col) var(--ao-col) 20px;
  --hist-grid-touch: 24px minmax(0, 1fr) var(--ao-col) var(--ao-col);
}
#panel-times .panel-head { flex: none; }

/* Column labels for the solve list. Same grid, same gap and same horizontal
   padding as .solve-chip below, so "ao12" lands over the ao12 column instead of
   near it. */
.hist-cols {
  flex: none;
  display: grid; grid-template-columns: var(--hist-grid);
  align-items: center; gap: var(--hist-gap);
  /* Empty until renderHistCols() fills it, which is after the first paint —
     and the solve list below then started 11px lower than where it had been
     drawn. One row of labels plus this element's own padding and rule. */
  min-height: calc(1lh + 8px);
  /* #hist-list below bleeds out by half the pad and pads back in so its chips
     can have a hover background that reaches the panel edge. Matching that here
     is what makes the labels line up with the columns rather than sit 9px off. */
  margin: 0 calc(var(--pad) * -0.5);
  /* The extra right pad is the list's scrollbar, measured at render time Ã¢â‚¬â€
     without it every heading sits a scrollbar-width right of its column. */
  /* +1px for the 1px transparent border a .solve-chip carries so that its
     hover outline can appear without the row jumping. That border is inside
     the chip's box, so the chip's grid is 2px narrower than a header laid out
     on the same padding -- enough to walk every heading a pixel off the
     numbers it names, in opposite directions at the two ends of the row. */
  padding: 4px calc(var(--pad) * 0.5 + 9px + var(--hist-sbw, 0px)) 3px calc(var(--pad) * 0.5 + 9px);
  font-size: .58rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-faint);
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
/* Every sortable column. csTimer sorts its time list by clicking the column
   heading, and this is the same gesture: click to order the session by that
   column, click the active one again to put it back in solve order. */
.hist-cols [data-sort] {
  cursor: pointer; user-select: none;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  background: none; border: 0; padding: 0;
  /* A <button> centres its own label. The heading cells were built as buttons
     so the columns could be sorted, and that one inherited default was enough
     to park "TIME" in the middle of a 79px track whose numbers are all flush
     left -- the label naming a column it did not sit over. The average
     headings are wrapped in .col-avg and right-aligned there, so this only has
     to answer for the ones standing in a track on their own. */
  text-align: inherit;
  transition: color var(--dur-fast) var(--ease);
}
.hist-cols [data-sort]:hover { color: var(--text); }
.hist-cols [data-sort].sorted { color: var(--accent); }
.hist-cols [data-sort].sorted::after { content: ' \25B2'; font-size: .8em; }

/* An average column heading: which average, and a pencil to change it. Right
   aligned as one unit so "ao12" still ends over its numbers with the pencil
   hanging off the end of the label rather than off the column. */
.col-avg {
  display: flex; align-items: baseline; justify-content: flex-end; gap: 2px;
  min-width: 0;
}
.col-avg .col-sort { white-space: nowrap; }
/* The pencil holds its space whether or not it is painted, so revealing it
   cannot nudge the label out from over its column. */
.col-edit {
  flex: none; width: 9px; font-size: .62rem; line-height: 1;
  background: none; border: 0; padding: 0; color: var(--text-faint);
  cursor: pointer; opacity: 0;
  transition: opacity var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.hist-cols:hover .col-edit, .col-edit:focus-visible { opacity: .75; }
.col-edit:hover { color: var(--accent); opacity: 1; }
/* No hover to reveal it on a touch screen, so it is simply always there. */
@media (hover: none) { .col-edit { opacity: .75; } }
/* The heading row carries one cell per column plus an empty one for the
   reconstruct button, and on a touch screen that column is dropped — so the
   spare cell wraps onto a second (empty) row and the row is a gap taller. */
@media (hover: none) { .hist-cols { min-height: calc(1lh + var(--hist-gap) + 8px); } }

/* The heading, mid-edit. Sized to the column it replaces so nothing shifts
   while you type. */
.col-num {
  width: 100%; min-width: 0;
  font: inherit; font-size: .62rem; letter-spacing: .06em;
  text-align: right; color: var(--text);
  background: color-mix(in srgb, var(--text) 12%, transparent);
  border: 1px solid var(--accent); border-radius: 5px;
  padding: 1px 3px; outline: none;
  -moz-appearance: textfield;
}
.col-num::-webkit-outer-spin-button, .col-num::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0;
}

#hist-list {
  flex: 1; min-height: 0; overflow-y: auto;
  /* Newest solve at the top. renderHistory() appends newest-first, so plain
     `column` puts the solve you just did where you are already looking. */
  display: flex; flex-direction: column; justify-content: flex-start;
  gap: 1px; margin: 0 calc(var(--pad) * -0.5);
  padding: 0 calc(var(--pad) * 0.5);
}

.solve-chip {
  display: grid; grid-template-columns: var(--hist-grid); align-items: center; gap: var(--hist-gap);
  padding: 5px 8px; border-radius: 7px;
  font-family: var(--font-mono); font-size: calc(.9rem * var(--times-scale)); font-weight: 550;
  font-variant-numeric: tabular-nums;
  color: var(--text); cursor: pointer; user-select: none;
  border: 1px solid transparent;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
  animation: chipIn 380ms var(--ease-back);
}
@keyframes chipIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }
.solve-chip:hover { background: color-mix(in srgb, var(--text) 9%, transparent); border-color: var(--border); }
.solve-chip .idx { font-size: .68rem; color: var(--text-faint); font-weight: 600; text-align: right; }
/* Clipped rather than allowed to run under the averages: on the narrowest
   sidebar a minutes-long time is wider than its column, and two numbers printed
   over each other are worse than one that ends early. */
.solve-chip .t   { text-align: left; min-width: 0; overflow: hidden; }
.solve-chip .tag { font-size: .6rem; color: var(--text-faint); letter-spacing: .06em; }
/* A running average up to this solve. Quieter than the time itself Ã¢â‚¬â€ it is
   context, not the number you just set. */
.solve-chip .avg {
  font-size: calc(.72rem * var(--times-scale));
  color: var(--text-faint); font-variant-numeric: tabular-nums;
  text-align: right; white-space: nowrap;
  /* Padded even when it is not highlighted, so the hover and best states can
     paint a background without nudging the column sideways. */
  padding: 1px 5px; border-radius: 5px;
}
/* An average is a way in, not just a readout: pressing one opens the solves
   behind that particular average Ã¢â‚¬â€ the ones in the middle of a session that
   were previously unreachable, since only "current" and "best" had a door. */
.solve-chip .avg.open { cursor: pointer; }
.solve-chip .avg.open:hover,
.solve-chip .avg.open:focus-visible {
  color: var(--text);
  background: color-mix(in srgb, var(--text) 14%, transparent);
  outline: none;
}
.solve-chip .avg.best.open:hover,
.solve-chip .avg.best.open:focus-visible {
  color: var(--gold);
  background: color-mix(in srgb, var(--gold) 28%, transparent);
}
.solve-chip .avg.best {
  color: var(--gold); font-weight: 700;
  background: color-mix(in srgb, var(--gold) 16%, transparent);
}

.solve-chip.pb   { color: var(--gold); background: color-mix(in srgb, var(--gold) 11%, transparent); }
.solve-chip.dnf  { color: var(--danger); }
.solve-chip.dnf .t { text-decoration: line-through; text-decoration-thickness: 1.5px; }
.solve-chip.plus2 { color: var(--warn); }
.solve-chip.best-in-avg  { box-shadow: inset 2px 0 0 color-mix(in srgb, var(--ok) 75%, transparent); }
.solve-chip.removing { animation: chipOut 220ms var(--ease) forwards; }
/* A small dot after the time on any solve with a phase breakdown Ã¢â‚¬â€ the only
   hint, otherwise, that clicking the row opens more than the penalty menu. */
.solve-chip.has-phases .t::after {
  content: ''; display: inline-block; width: 4px; height: 4px; border-radius: 50%;
  margin-left: 6px; margin-bottom: 1px;
  background: var(--accent-2); vertical-align: middle;
}
@keyframes chipOut { to { opacity: 0; transform: translateX(-14px); } }

.hist-empty {
  font-family: var(--font-ui); font-size: .78rem; font-weight: 500;
  color: var(--text-dim); opacity: .75;
  padding: 10px 4px; text-align: center; line-height: 1.5;
}

.times-foot {
  display: flex; gap: 2px; flex: none;
  padding-top: 6px; border-top: 1px solid var(--border);
}
.times-foot .ghost-btn { flex: 1; justify-content: center; }

/* ---------------- collapsible stats ---------------- */
#panel-stats { gap: 0; overflow: hidden; }
.panel-head.as-btn {
  display: flex; width: 100%; align-items: center; gap: 8px;
  flex-wrap: wrap; row-gap: 7px;
  cursor: pointer; text-align: left;
  font-size: .68rem; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-faint);
  transition: color var(--dur-fast) var(--ease);
}
.panel-head.as-btn:hover { color: var(--text); }
.panel-head.as-btn .chev {
  width: 15px; height: 15px; margin-left: auto; flex: none;
  transition: transform var(--dur) var(--ease);
}
.stats-peek {
  margin-left: auto; font-family: var(--font-mono); font-size: .74rem;
  font-weight: 600; letter-spacing: 0; text-transform: none;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
}
#panel-stats[data-collapsed="false"] .panel-head.as-btn .chev { transform: rotate(180deg); }

/* The 0fr collapse only works with a single grid child, hence #stats-inner. */
#stats-body {
  display: grid; grid-template-rows: 1fr;
  /* Shrinkable, so the panel's max-height actually reaches the content. Without
     this the grid row sized itself to its content, nothing constrained the
     chart, and the panel just clipped whatever did not fit. */
  flex: 0 1 auto; min-height: 0;
  margin-top: 12px;
  transition: grid-template-rows var(--dur) var(--ease),
              margin-top var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
}
#stats-inner {
  min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: 9px;
}
#panel-stats[data-collapsed="true"] #stats-body {
  grid-template-rows: 0fr; margin-top: 0; opacity: 0; pointer-events: none;
}
/* Expanded, the figures scroll rather than being cut off.
   #stats-inner is `overflow: hidden` because that is what makes the 0fr
   collapse animate cleanly Ã¢â‚¬â€ but it also meant that anywhere the panel was
   shorter than its contents (which on a phone is always) the bottom of the
   grid was simply chopped, with no way to reach it. Scrolling is only turned
   on for the open state, so the collapse still animates against a clipped box.
   `contain` stops a flick that runs out of list from scrolling the page
   underneath and dragging the whole timer around. */
#panel-stats[data-collapsed="false"] #stats-inner {
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ---------------- scrim / drawer ---------------- */
#scrim {
  position: fixed; inset: 0; z-index: 100;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  animation: fadeIn var(--dur) var(--ease);
}
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }

#drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 101;
  width: min(440px, 100vw);
  display: flex; flex-direction: column;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5);
  border-left: 1px solid var(--border);
  box-shadow: -30px 0 80px -30px rgba(0,0,0,.8);
  animation: slideIn 380ms var(--ease);
}
#drawer.wide { width: min(760px, 100vw); }
@keyframes slideIn { from { transform: translateX(30px); opacity: 0 } to { transform: none; opacity: 1 } }
.drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 18px 20px 14px; border-bottom: 1px solid var(--border); flex: none;
}
.drawer-head h2 { font-size: 1.02rem; font-weight: 650; letter-spacing: -.02em; }
#drawer-body { flex: 1; overflow-y: auto; padding: 18px 20px 40px; display: flex; flex-direction: column; gap: 22px; }

/* ---------------- account merge dialog ---------------- */
.sync-merge-scrim {
  position: fixed; inset: 0; z-index: 200;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  animation: fadeIn var(--dur) var(--ease);
}
.sync-merge-card {
  position: fixed; top: 50%; left: 50%; z-index: 201;
  transform: translate(-50%, -50%);
  width: min(420px, calc(100vw - 40px));
  display: flex; flex-direction: column; gap: 12px;
  padding: 22px 24px;
  background: color-mix(in srgb, var(--surface) 96%, transparent);
  backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5);
  border: 1px solid var(--border); border-radius: 16px;
  box-shadow: 0 30px 80px -20px rgba(0,0,0,.6);
  animation: slideIn 380ms var(--ease);
}
.sync-merge-card h3 { font-size: 1rem; font-weight: 650; letter-spacing: -.02em; }
.sync-merge-card p { font-size: .86rem; line-height: 1.5; color: var(--text-faint); margin: 0; }
.sync-merge-card .btn { align-self: flex-end; margin-top: 4px; }

/* settings primitives */
.group { display: flex; flex-direction: column; gap: 10px; }
.group > h3 {
  font-size: .64rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-faint); display: flex; align-items: center; gap: 8px;
}
.group > h3::after { content: ''; flex: 1; height: 1px; background: var(--border); }

.row {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 14px;
  min-height: 34px;
}
/* :not(.switch) matters Ã¢â‚¬â€ a bare `.row > label` beats `.switch { flex: none }`
   on specificity and stretches the toggle into a slider-looking bar. */
.row > label:not(.switch), .row > .lbl { font-size: .84rem; color: var(--text-dim); flex: 1; min-width: 0; }
/* A control wider than what is left beside its label (four input chips, say)
   drops onto its own line instead of crushing the label to one word a line.
   Comes after the flex:1 shorthand above on purpose: that shorthand resets
   flex-basis to 0%, so this floor has to win the cascade, not just state it. */
.row > .lbl { flex-basis: 9rem; }
.row .sub { display: block; font-size: .72rem; color: var(--text-faint); margin-top: 1px; }
.row.stack { flex-direction: column; align-items: stretch; gap: 7px; }

.switch { position: relative; width: 40px; height: 23px; flex: 0 0 40px; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch .track {
  position: absolute; inset: 0; border-radius: 99px;
  background: color-mix(in srgb, var(--text) 14%, transparent);
  transition: background var(--dur) var(--ease);
}
.switch .thumb {
  position: absolute; top: 3px; left: 3px; width: 17px; height: 17px; border-radius: 50%;
  background: var(--text-dim);
  transition: transform var(--dur) var(--ease-back), background var(--dur) var(--ease);
}
.switch input:checked ~ .track { background: var(--accent); }
.switch input:checked ~ .thumb { transform: translateX(17px); background: #fff; }

select.inp, input.inp, textarea.inp {
  height: 34px; padding: 0 10px; border-radius: 9px;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  border: 1px solid var(--border); color: var(--text);
  font-size: .82rem; outline: none; min-width: 0;
  transition: border-color var(--dur-fast) var(--ease);
}
textarea.inp { height: auto; padding: 8px 10px; resize: vertical; font-family: var(--font-mono); font-size: .78rem; }
select.inp:focus, input.inp:focus, textarea.inp:focus { border-color: var(--accent); }
select.inp { cursor: pointer; max-width: 190px; }
input.inp[type="color"] { padding: 2px; width: 44px; cursor: pointer; }
input.inp[type="file"] { height: auto; padding: 7px; font-size: .74rem; }

input[type="range"] {
  -webkit-appearance: none; appearance: none; height: 4px; border-radius: 99px;
  background: color-mix(in srgb, var(--text) 12%, transparent);
  outline: none; flex: 1; max-width: 190px; cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 15px; height: 15px; border-radius: 50%;
  background: var(--accent); cursor: pointer; border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--accent);
  transition: transform var(--dur-fast) var(--ease);
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.22); }
input[type="range"]::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 50%; background: var(--accent);
  border: 2px solid var(--surface); cursor: pointer;
}
.range-row { display: flex; align-items: center; gap: 10px; }
.range-val { font-family: var(--font-mono); font-size: .72rem; color: var(--text-faint); min-width: 44px; text-align: right; }

/* theme swatches */
.theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; }
.theme-card {
  position: relative; height: 62px; border-radius: 11px; overflow: hidden;
  border: 1.5px solid var(--border); cursor: pointer;
  transition: all var(--dur) var(--ease);
}
.theme-card:hover { transform: translateY(-3px); box-shadow: 0 10px 24px -12px rgba(0,0,0,.7); }
.theme-card.on { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent); }
.theme-card .tc-name {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 4px 7px;
  font-size: .64rem; font-weight: 650; letter-spacing: .04em;
  background: linear-gradient(transparent, rgba(0,0,0,.72)); color: #fff;
}
.theme-card .tc-dots { position: absolute; top: 7px; left: 7px; display: flex; gap: 3px; }
.theme-card .tc-dots i { width: 8px; height: 8px; border-radius: 50%; display: block; }

.color-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.color-item { display: flex; align-items: center; gap: 7px; font-size: .74rem; color: var(--text-dim); }
.color-item input { flex: none; }

/* chips row */
.chips { display: flex; flex-wrap: wrap; gap: 5px; }
.chip {
  padding: 5px 11px; border-radius: 99px; font-size: .74rem; font-weight: 550;
  border: 1px solid var(--border); color: var(--text-dim);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  cursor: pointer; transition: all var(--dur-fast) var(--ease);
}
.chip:hover { color: var(--text); transform: translateY(-1px); }
.chip.on { background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: var(--accent); color: var(--text); }

/* ---------------- popover ---------------- */
#popover {
  position: fixed; z-index: 105;
  min-width: 210px; max-width: min(430px, 94vw); max-height: 70vh; overflow-y: auto;
  padding: 6px; border-radius: 14px;
  background: color-mix(in srgb, var(--surface-2) 92%, transparent);
  backdrop-filter: blur(26px) saturate(1.5); -webkit-backdrop-filter: blur(26px) saturate(1.5);
  border: 1px solid var(--border);
  box-shadow: 0 26px 60px -20px rgba(0,0,0,.8);
  transform-origin: top center;
  animation: popover 220ms var(--ease-back);
}
@keyframes popover { from { opacity: 0; transform: scale(.94) translateY(-8px); } to { opacity: 1; transform: none; } }
.pop-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border-radius: 9px; font-size: .83rem; color: var(--text-dim);
  text-align: left; transition: all 90ms var(--ease);
}
.pop-item:hover, .pop-item.cursor { background: color-mix(in srgb, var(--text) 9%, transparent); color: var(--text); }
.pop-item.on { color: var(--accent); }
.pop-item .pi-badge {
  margin-left: auto; font-family: var(--font-mono); font-size: .66rem;
  color: var(--text-faint); letter-spacing: .04em;
}
.pop-item .pi-badge + .pi-action { margin-left: 6px; }
.pop-item .pi-action {
  margin-left: auto; display: grid; place-items: center; flex: none;
  width: 22px; height: 22px; margin-right: -4px; border-radius: 6px; color: var(--text-faint);
}
.pop-item .pi-action:hover, .pop-item .pi-action:focus-visible {
  color: var(--text); background: color-mix(in srgb, var(--text) 12%, transparent);
}
.pop-item .pi-action svg {
  width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.pop-form { display: flex; flex-direction: column; gap: 8px; padding: 4px 10px 8px; width: 250px; }
.pop-form label { display: flex; flex-direction: column; gap: 4px; font-size: .7rem; color: var(--text-faint); }
.pop-form .pf-row { display: flex; gap: 6px; }
.pop-form .pf-row input { flex: 1; min-width: 0; }
.pop-form .pf-err { font-size: .7rem; color: var(--danger); min-height: 1em; }
.pop-form .pf-btns { display: flex; justify-content: flex-end; gap: 6px; }

/* A session's goal: same quiet line as now-playing, stacked above it. */
#goal-line {
  font-size: .78rem; color: var(--text-faint); text-align: center;
  margin-top: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transition: opacity var(--dur) var(--ease);
}
#goal-line[hidden] { display: none; }
/* Stacked with now-playing: one block of two lines, not two spaced rows. The
   timer is pinned to the viewport centre, so every pixel here is one closer
   to the digits. */
#goal-line:not([hidden]) + #now-playing { margin-top: -6px; }
#goal-line.hit { color: var(--ok); }
body.focus #goal-line, body.inspecting #goal-line,
body:has(#timer-display.state-running) #goal-line { opacity: 0; }
.pop-item .pi-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex: none; }
.pop-sep { height: 1px; margin: 5px 8px; background: var(--border); }
.pop-title {
  padding: 8px 10px 4px; font-size: .6rem; font-weight: 700;
  letter-spacing: .17em; text-transform: uppercase; color: var(--text-faint);
}
.pop-cols { display: grid; grid-template-columns: repeat(2, 1fr); gap: 2px; }
/* A past solve's phase breakdown, dropped into the solve menu Ã¢â‚¬â€ the same
   chips the post-solve zone uses, just reached from an older row. */
.pop-phases { padding: 2px 10px 8px; justify-content: flex-start; }

/* ---------------- command palette ---------------- */
#palette {
  position: fixed; inset: 0; z-index: 110;
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: 14vh;
  background: color-mix(in srgb, var(--bg) 50%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  animation: fadeIn 160ms var(--ease);
}
.pal-box {
  width: min(620px, 92vw); max-height: 62vh; display: flex; flex-direction: column;
  border-radius: 18px; overflow: hidden;
  background: color-mix(in srgb, var(--surface-2) 94%, transparent);
  border: 1px solid var(--border);
  box-shadow: 0 40px 90px -30px rgba(0,0,0,.9);
  animation: palIn 280ms var(--ease-back);
}
@keyframes palIn { from { opacity: 0; transform: scale(.95) translateY(-14px); } to { opacity: 1; transform: none; } }
.pal-input-row { display: flex; align-items: center; gap: 11px; padding: 15px 17px; border-bottom: 1px solid var(--border); }
.pal-ico { width: 17px; height: 17px; color: var(--text-faint); flex: none; }
#pal-input { flex: 1; background: none; border: 0; outline: none; font-size: .98rem; color: var(--text); }
#pal-input::placeholder { color: var(--text-faint); }
.pal-esc { flex: none; }
#pal-list { overflow-y: auto; padding: 6px; }
.pal-item {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 9px 11px; border-radius: 10px; font-size: .87rem; color: var(--text-dim);
  text-align: left;
}
.pal-item.cursor { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--text); }
.pal-item .pal-kind {
  font-size: .58rem; font-weight: 700; letter-spacing: .13em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 5px; color: var(--text-faint);
  background: color-mix(in srgb, var(--text) 8%, transparent); flex: none;
}
.pal-item .pal-key { margin-left: auto; flex: none; }
.pal-empty { padding: 22px; text-align: center; color: var(--text-faint); font-size: .84rem; }

/* ---------------- toasts ---------------- */
#toasts {
  position: fixed; left: 50%; bottom: 26px; z-index: 120;
  transform: translateX(-50%);
  display: flex; flex-direction: column-reverse; gap: 8px; align-items: center;
  pointer-events: none;
}
.toast {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 14px; border-radius: 12px; pointer-events: auto;
  background: color-mix(in srgb, var(--surface-2) 94%, transparent);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--border);
  box-shadow: 0 18px 40px -16px rgba(0,0,0,.8);
  font-size: .84rem; color: var(--text);
  animation: toastIn 380ms var(--ease-back);
  max-width: 90vw;
}
.toast.out { animation: toastOut 220ms var(--ease) forwards; }
@keyframes toastIn { from { opacity: 0; transform: translateY(22px) scale(.9); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(12px) scale(.94); } }
.toast .t-act { font-size: .78rem; font-weight: 650; color: var(--accent); padding: 3px 8px; border-radius: 7px; }
.toast .t-act:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.toast.good { border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.toast.bad  { border-color: color-mix(in srgb, var(--danger) 45%, transparent); }

/* new best single / average — a ticker across the top of the digits (js/main.js celebratePB) */
.pb-marquee {
  position: absolute; left: 0; right: 0; bottom: 100%;
  overflow: hidden; white-space: nowrap; pointer-events: none;
  font: 650 .78rem/1.8 var(--font-ui, system-ui), sans-serif;
  letter-spacing: .22em; text-transform: uppercase; color: var(--accent); text-shadow: none;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 15%, #000 85%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 15%, #000 85%, transparent);
}
.pb-marquee span { display: inline-block; padding-left: 100%; animation: pbTicker 2.5s linear infinite; }
.pb-marquee.still { text-align: center; }
.pb-marquee.still span { padding-left: 0; animation: none; }
@keyframes pbTicker { to { transform: translateX(-100%); } }
@media (prefers-reduced-motion: reduce) {
  .pb-marquee { text-align: center; }
  .pb-marquee span { padding-left: 0; animation: none; }
}

/* ---------------- creator chip ---------------- */
#creator-chip {
  position: fixed; left: 14px; bottom: 14px; z-index: 40;
  display: flex; align-items: center; gap: 7px;
  padding: 6px 11px 6px 9px; border-radius: 99px;
  font-size: .72rem; font-weight: 550; color: var(--text-faint);
  background: color-mix(in srgb, var(--surface) 40%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  opacity: .5;
  transition: all var(--dur) var(--ease);
}
#creator-chip:hover {
  opacity: 1; color: var(--text); transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  box-shadow: 0 10px 26px -12px var(--accent);
}
#creator-chip .ig { width: 14px; height: 14px; flex: none; }
#creator-chip:hover .ig { color: var(--accent); }
#creator-chip i { font-style: normal; opacity: .6; margin-right: 2px; }
/* Pinned bottom-left, which below 860px is where the two tiles reach Ã¢â‚¬â€ it sat
   on the times column headings at phone width, on the trend chart at tablet
   width, and on the stats panel in landscape. The credit is in the About panel,
   where it can be read rather than squinted past.
   Has to come after the rules above, not in either mobile block further up this
   file, or the `display: flex` a few lines back simply wins Ã¢â‚¬â€ which is exactly
   why the landscape version of this rule never did anything. */
@media (max-width: 860px),
       (max-width: 940px) and (max-height: 500px) and (orientation: landscape) {
  #creator-chip { display: none; }
}

/* ---------------- mobile topbar menu (Ã‚Â§1.1) ---------------- */
/* Eight icon buttons is fine on a laptop and impossible on a phone. Below the
   breakpoint this file already uses, they fold behind one button and come back
   as a labelled list Ã¢â‚¬â€ the label is each button's own `title`, so no button
   needed a second copy of its own name in the markup. */
#btn-menu { display: none; }

@media (max-width: 860px) {
  #topbar { position: relative; }
  /* The icon row it stands in for is what used to carry `margin-left: auto`,
     and that row is display:none down here -- so the button that replaced it
     was landing 12px to the right of the wordmark, in the middle of an
     otherwise empty bar, with its own dropdown then opening from the far
     right edge it was nowhere near. It inherits the row's job here. */
  #btn-menu { display: flex; margin-left: auto; }
  .topbar-right { display: none; }
  .topbar-right.open {
    display: flex; flex-direction: column; align-items: stretch; gap: 2px;
    position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
    min-width: 210px; max-width: calc(100vw - 24px); padding: 6px;
    max-height: calc(100vh - 72px); overflow-y: auto; border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--surface) 92%, transparent);
    border: 1px solid var(--border);
    backdrop-filter: blur(var(--panel-blur)); -webkit-backdrop-filter: blur(var(--panel-blur));
    box-shadow: var(--shadow);
  }
  /* .icon-btn is a 34px grid box that centres one glyph; in the menu it has to
     be a row of icon + label instead, so the display mode is swapped rather
     than fought with. */
  .topbar-right.open .icon-btn {
    width: auto; height: auto;
    display: flex; align-items: center; justify-content: flex-start; gap: 11px;
    padding: 9px 11px; border-color: transparent; background: none;
  }
  /* The touch-target rule further down also owns `.icon-btn::after`, and it
     absolutely centres that box on the button. It loses `content` to this
     more specific rule but keeps the positioning, which is what stacked every
     label on top of its own icon on a phone. Put the label back in flow. */
  .topbar-right.open .icon-btn::after {
    content: attr(title);
    position: static; transform: none; width: auto; height: auto;
    font-size: .8rem; font-weight: 550; color: var(--text); white-space: nowrap;
  }
  /* SOTD already carries a visible label span, and its ::after is the shine
     sweep -- it wants neither a second name nor a stray gold block in a list. */
  .topbar-right.open .sotd-box { overflow: visible; }
  .topbar-right.open .sotd-box::after { content: none; }
  .topbar-right.open .icon-btn:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
  #btn-menu.on { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 34%, transparent); }
}

/* ---------------- big charts (stats drawer) ---------------- */
.chart-card {
  padding: 14px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 10px;
}
.chart-card h4 { font-size: .66rem; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: var(--text-faint); }
.chart-card svg { width: 100%; overflow: visible; }
.axis-line { stroke: var(--border); stroke-width: 1; }
.axis-txt { fill: var(--text-faint); font-size: 9px; font-family: var(--font-mono); }
.bar { fill: var(--accent); opacity: .8; transition: opacity var(--dur-fast); }
.bar:hover { opacity: 1; }
/* ---- practice heatmap ---- */
/* Not stretched to the card like the other charts: the cells are a fixed grid,
   and scaling them up turns 11px squares into soft rectangles. It keeps its
   own size and scrolls sideways on a narrow panel instead. */
.chart-card svg.heatmap { width: auto; max-width: none; height: auto; }
/* A year of weeks is wider than a narrow drawer; scroll it rather than
   squeezing the cells, and start the view at the most recent month. */
.heat-host { overflow-x: auto; overflow-y: hidden; }
.hm-cell { rx: 2; }
/* Ring rather than a fill change, so hovering never misreports the level. */
.hm-cell:hover { stroke: var(--text); stroke-width: 1.2; paint-order: stroke; }
.hm-month {
  fill: var(--text-dim); font-size: 9px; font-weight: 600;
  font-family: var(--font-ui); letter-spacing: .04em;
}
.hm-day  { fill: var(--text-faint); font-size: 8.5px; font-family: var(--font-ui); }
.hm-key  { fill: var(--text-faint); font-size: 9px; font-family: var(--font-mono); }
.hm-divider { stroke: var(--border); stroke-width: 1; opacity: .55; }

/* ---- touch ----
   Keyed off the pointer, not the viewport width: a small window on a desktop
   still has a mouse, and a large tablet still has fingers. 44px is the size a
   fingertip can actually hit; the visual box is left alone and the target is
   grown underneath it, so nothing about the layout moves. */
@media (pointer: coarse) {
  .icon-btn, .seg-btn, .ghost-btn, .btn, .view-toggle button {
    position: relative;
  }
  .icon-btn::after, .seg-btn::after, .ghost-btn::after,
  .btn::after, .view-toggle button::after {
    content: '';
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    width: max(100%, 44px); height: max(100%, 44px);
  }
  /* The scramble buttons are 35px wide and were 4px apart, so each 44px target
     had 5px of it taken by the next one along. 9px apart they each get all 44. */
  #scramble-tools { gap: 9px; }
  /* The 2D/3D switch is the bottom edge of a widget that floats over the times
     panel, and a target centred on it reached past the preview into that
     panel's top corner — a tap meant for a column pencil reset the cube's
     angle instead. It grows upward, into the preview's own box. */
  .view-toggle button::after { top: auto; bottom: 0; transform: translateX(-50%); }
  /* The column pencils are 9px glyphs with a sort label on one side and the
     newest solve right underneath, and a centred target would take taps meant
     for both. So it grows upward instead, over the panel heading, where a tap
     does nothing else. */
  .col-edit { position: relative; }
  .col-edit::after {
    content: '';
    position: absolute; left: 50%; bottom: 100%;
    width: 44px; height: 44px; transform: translateX(-50%);
  }
}

/* ---- narrow screens ----
   At 375px the pickers plus the icon buttons do not fit, and the centre group
   was being clipped by 118px -- the session picker simply could not be
   reached. Drop the wordmark; the cube still identifies the app. */
@media (max-width: 620px) {
  .brand-text { display: none; }
}

/* Sideways scrolling alone was not enough. Once the icon buttons and the brand
   had taken their width there were ~60px left for the centre group to scroll
   inside Ã¢â‚¬â€ a slot too narrow to read one picker in, let alone four. Give the
   pickers a row of their own instead: the brand and the icons keep the first
   line, the pickers get the full width of the second and still scroll if a
   long session name overruns it.

   The threshold was 620px when there were three pickers. Four of them at full
   width, plus the wordmark and the icon row, need about 1040px on one line Ã¢â‚¬â€
   below that the last picker was being cut in half by the centre group's
   `overflow: hidden`, on an ordinary laptop window. Two rows there rather than
   a button nobody can read or reach. */
@media (max-width: 1040px) {
  #topbar { flex-wrap: wrap; row-gap: 8px; }
  .topbar-right { margin-left: auto; }
  .topbar-center {
    order: 3;
    flex: 1 0 100%;
    margin: 0;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .topbar-center::-webkit-scrollbar { display: none; }
  .topbar-center .seg { flex: none; }
  .seg-btn { max-width: none; }
  /* Full width to scroll in now, so nothing has to be truncated to 62px. */
  .seg-btn span { max-width: none; }
}

/* ---- now playing (sidebar) ----
   Deliberately quiet: it sits under the times list and must never pull the eye
   away from the timer. The cover is the only saturated thing in it, and during
   a solve the whole panel is hidden with the rest of the chrome anyway. */
#panel-spotify { flex: none; gap: 9px; }
#panel-spotify[hidden] { display: none; }

.panel-sub.live::before {
  content: ''; display: inline-block; width: 6px; height: 6px;
  border-radius: 50%; background: var(--ok); margin-right: 6px;
  vertical-align: middle;
  animation: npPulse 2.4s var(--ease) infinite;
}
@keyframes npPulse { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }

#np-card {
  display: flex; gap: 11px; align-items: center; min-width: 0;
  border-radius: var(--radius-sm);
  transition: background var(--dur-fast) var(--ease);
}
#np-card.has-track:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
#np-card:not(.has-track) { pointer-events: none; }

.np-art-wrap {
  position: relative; flex: none;
  width: 56px; height: 56px; border-radius: 8px; overflow: hidden;
  background: color-mix(in srgb, var(--text) 7%, transparent);
  box-shadow: 0 6px 18px -8px rgba(0,0,0,.7);
}
#np-art { width: 100%; height: 100%; object-fit: cover; display: block; }
#np-art[hidden] { display: none; }
/* The placeholder is the Spotify mark, so an empty card still says what it is
   waiting for rather than looking broken. */
.np-art-empty {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--text-faint);
}
#np-art:not([hidden]) + .np-art-empty { display: none; }
.np-art-empty svg { width: 22px; height: 22px; }

.np-meta { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.np-title {
  font-size: calc(.82rem * var(--sidebar-text)); font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.np-artist {
  font-size: calc(.73rem * var(--sidebar-text)); color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The two colours actually pulled out of this cover. This is the whole reason
   the tint reads as "synced to the music" rather than "the theme changed". */
.np-swatches { display: flex; gap: 4px; margin-top: 2px; }
.np-swatches i {
  width: 13px; height: 5px; border-radius: 3px; display: block;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--text) 12%, transparent);
}

/* ---- transport ----
   Centred under the card and deliberately small: this is a convenience for the
   thing you are already looking at, not a media player. It is inside
   .fade-layer with the rest of the chrome, so it disappears during a solve
   along with everything else. */
.np-controls { display: flex; align-items: center; justify-content: center; gap: 6px; }
.np-controls[hidden] { display: none; }
.np-btn {
  width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: 50%; color: var(--text-dim);
  border: 1px solid transparent;
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.np-btn svg { width: 15px; height: 15px; }
.np-btn:hover { color: var(--text); background: color-mix(in srgb, var(--text) 9%, transparent); }
.np-btn:active { transform: scale(.9); }
/* The play button is the one you aim for, so it gets the accent and the size. */
.np-play {
  width: 34px; height: 34px;
  color: var(--on-accent); background: var(--accent);
}
.np-play svg { width: 17px; height: 17px; }
.np-play:hover {
  color: var(--on-accent);
  background: var(--accent);
  box-shadow: 0 6px 18px -6px var(--accent);
}
.np-note {
  font-size: .7rem; color: var(--text-faint); line-height: 1.45; text-align: center;
  padding: 6px 8px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 10%, transparent);
}
.np-note[hidden] { display: none; }
[data-motion="off"] .np-btn { transition: none; }

.np-progress { display: flex; flex-direction: column; gap: 4px; }
.np-progress[hidden] { display: none; }
.np-bar {
  height: 3px; border-radius: 2px; overflow: hidden;
  background: color-mix(in srgb, var(--text) 12%, transparent);
}
.np-fill {
  height: 100%; width: 0%; border-radius: 2px;
  background: var(--accent);
  /* Matches the 500ms tick that drives it, so the bar glides instead of
     stepping. Re-anchored by every poll, so it never drifts. */
  transition: width 500ms linear;
}
.np-times {
  display: flex; justify-content: space-between;
  font-size: .66rem; color: var(--text-faint);
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
}
[data-motion="off"] .np-fill { transition: none; }
[data-motion="off"] .panel-sub.live::before { animation: none; }

/* ---- the Spotify drawer ---- */
.spot-hero {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 14px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid var(--border);
}
.spot-hero > div:nth-child(2) { flex: 1; min-width: 0; }
.spot-hero-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--text-faint);
}
.spot-hero.on { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.spot-hero.on .spot-hero-dot { background: var(--ok); }
.spot-hero-title { font-size: .9rem; font-weight: 600; color: var(--text); }
.spot-hero-sub { font-size: .76rem; color: var(--text-faint); margin-top: 2px; line-height: 1.45; }
.spot-hero-warn {
  font-size: .74rem; color: var(--warn); margin-top: 6px; line-height: 1.45;
}

/* ---- advanced disclosure ----
   Native <details>: the escape hatch for the handful of people past Spotify's
   user cap should cost the rest of them one line of text, not a section. */
.adv {
  border: 1px solid color-mix(in srgb, #1db954 35%, var(--border));
  border-radius: var(--radius-sm);
  background: linear-gradient(135deg, color-mix(in srgb, #1db954 12%, transparent), transparent 75%);
}
.adv > summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 11px;
  padding: 12px 14px;
}
.adv > summary::-webkit-details-marker { display: none; }
.adv > summary::before {
  content: ''; width: 0; height: 0; flex: none;
  border-left: 6px solid #1db954;
  border-top: 5px solid transparent; border-bottom: 5px solid transparent;
  transition: transform var(--dur-fast) var(--ease);
}
.adv[open] > summary::before { transform: rotate(90deg); }
.adv > summary:hover { background: color-mix(in srgb, #1db954 7%, transparent); }
.adv-title { font-size: .88rem; font-weight: 700; color: var(--text); }
.adv-sub { font-size: .74rem; color: var(--text-faint); margin-top: 2px; }
.adv-body { display: flex; flex-direction: column; gap: 12px; padding: 2px 14px 14px; }

/* The one place anyone has to go. Spotify green, bold, so it cannot be missed. */
.spot-dash {
  color: #1db954; font-weight: 700; text-decoration: none;
  background: color-mix(in srgb, #1db954 16%, transparent);
  padding: 0 5px; border-radius: 4px; white-space: nowrap;
}
.spot-dash:hover { text-decoration: underline; }
.btn.spot-go {
  display: flex; justify-content: center; text-decoration: none;
  background: #1db954; border-color: #1db954; color: #0b0b12; font-weight: 700;
}
.btn.spot-go:hover { background: #1ed760; box-shadow: 0 8px 24px -8px #1db954; }

.setup-steps { display: flex; flex-direction: column; gap: 11px; }
.setup-step { display: flex; gap: 11px; align-items: flex-start; }
.ss-n {
  flex: none; width: 21px; height: 21px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: .7rem; font-weight: 700;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
}
.ss-t { font-size: .84rem; font-weight: 600; color: var(--text); }
.ss-d { font-size: .74rem; color: var(--text-faint); line-height: 1.5; margin-top: 1px; }

/* ---- album theming (Appearance) ---- */
.copy-field { display: flex; align-items: center; gap: 8px; min-width: 0; }
.copy-field code {
  font-family: var(--font-mono); font-size: .74rem; color: var(--text-dim);
  background: color-mix(in srgb, var(--text) 6%, transparent);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 4px 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.btn-pair { display: flex; gap: 8px; flex: none; }
/* A note that is a blocker, not background reading. */
.hint-note.warn-note { border-left-color: var(--warn); color: var(--text-dim); }
.spot-status { font-size: .8rem; color: var(--text-faint); text-align: right; }
/* The dot is the state; the words are the detail. Colour alone never carries it. */
.spot-status::before {
  content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-faint); margin-right: 7px; vertical-align: middle;
}
.spot-status.on { color: var(--text-dim); }
.spot-status.on::before { background: var(--ok); }

/* The now-playing line under the scramble: present but never competing with it. */
#now-playing {
  font-size: .78rem; color: var(--text-faint); text-align: center;
  margin-top: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#now-playing[hidden] { display: none; }

/* ---- chart legend ----
   Labels wear text ink, never the series colour; the swatch beside them is
   what carries identity, so the words stay legible against any theme. */
.chart-legend {
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  font-size: .68rem; color: var(--text-faint); font-family: var(--font-ui);
}
.lg-item { display: inline-flex; align-items: center; gap: 6px; }
.lg-swatch {
  width: 14px; height: 3px; border-radius: 2px; flex: none;
  background: var(--lg-color);
}
/* The dash pattern is the second channel Ã¢â‚¬â€ the same one the line uses Ã¢â‚¬â€ so the
   key still reads when the two colours are hard to tell apart. */
.lg-swatch[data-dash="dashed"] {
  background: repeating-linear-gradient(to right,
    var(--lg-color) 0 4px, transparent 4px 7px);
}
.lg-swatch[data-dash="dotted"] {
  background: repeating-linear-gradient(to right,
    var(--lg-color) 0 2px, transparent 2px 6px);
}

.big-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(104px, 1fr)); gap: 7px; }
.bs {
  padding: 10px 12px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  display: flex; flex-direction: column; gap: 2px;
}
.bs-k { font-size: .58rem; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; color: var(--text-faint); }
.bs-v { font-family: var(--font-mono); font-size: 1.02rem; font-weight: 650; font-variant-numeric: tabular-nums; }
.bs-sub { font-size: .64rem; color: var(--text-faint); font-family: var(--font-mono); }

/* solve table (all-solves drawer) */
.solve-table { display: flex; flex-direction: column; gap: 2px; }
.st-row {
  display: grid; grid-template-columns: 34px 78px 1fr auto; gap: 9px; align-items: center;
  padding: 7px 9px; border-radius: 8px; font-size: .78rem; cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.st-row:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }
.st-row .st-i { color: var(--text-faint); font-family: var(--font-mono); font-size: .7rem; }
.st-row .st-t { font-family: var(--font-mono); font-weight: 650; font-variant-numeric: tabular-nums; }
.st-row .st-s { font-family: var(--font-mono); font-size: .66rem; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.st-row .st-d { font-size: .66rem; color: var(--text-faint); }
.st-row.dnf .st-t { color: var(--danger); text-decoration: line-through; }
.st-row.plus2 .st-t { color: var(--warn); }
.st-row.pb .st-t { color: var(--gold); }

/* case picker grid */
.case-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 6px; }
.case-cell {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 7px 4px; border-radius: 9px; cursor: pointer;
  border: 1.5px solid transparent;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  transition: all var(--dur-fast) var(--ease);
}
.case-cell:hover { transform: translateY(-2px); background: color-mix(in srgb, var(--text) 9%, transparent); }
.case-cell.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.case-cell .cc-name { font-size: .66rem; font-weight: 600; color: var(--text-dim); }
.case-cell .cc-stat { font-size: .58rem; font-family: var(--font-mono); color: var(--text-faint); }
.case-cell.on .cc-name { color: var(--text); }

/* shortcut help table */
.kbd-list { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 26px; }
.kbd-col { display: flex; flex-direction: column; gap: 7px; }
.kbd-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: .8rem; color: var(--text-dim); }
.kbd-row .keys { display: flex; gap: 3px; flex: none; }
@media (max-width: 620px) { .kbd-list { grid-template-columns: 1fr; } }

.hint-note {
  font-size: .74rem; color: var(--text-faint); line-height: 1.55;
  padding: 10px 12px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border-left: 2px solid var(--accent);
}

/* ---------------- stat detail drawer ---------------- */
.stat-detail-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
.sd-label {
  font-size: .64rem; font-weight: 700; letter-spacing: .18em;
  text-transform: uppercase; color: var(--text-faint);
}
.sd-value {
  font-family: var(--font-mono); font-size: 2.1rem; font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: -.02em; line-height: 1.15;
}
.sd-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

.sd-list { display: flex; flex-direction: column; gap: 4px; }
.sd-row {
  display: grid; grid-template-columns: 26px 74px 1fr auto;
  gap: 10px; align-items: start;
  padding: 9px 10px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid transparent;
}
.sd-row:hover { border-color: var(--border); }
/* A trimmed solve does not count towards the average, so it should not read
   with the same weight as the ones that do. */
.sd-row.trimmed { opacity: .58; }
.sd-row.dnf .sd-t { color: var(--danger); }
.sd-i {
  font-family: var(--font-mono); font-size: .7rem; color: var(--text-faint);
  text-align: right; padding-top: 3px;
}
.sd-t {
  font-family: var(--font-mono); font-size: 1rem; font-weight: 650;
  font-variant-numeric: tabular-nums; padding-top: 1px;
}
.sd-body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.sd-scramble {
  font-family: var(--font-mono); font-size: .76rem; line-height: 1.5;
  color: var(--text-dim); word-spacing: .22em;
  overflow-wrap: anywhere;              /* long big-cube scrambles must wrap */
}
.sd-meta { font-size: .64rem; color: var(--text-faint); }

@media (max-width: 620px) {
  .sd-row { grid-template-columns: 22px 64px 1fr; }
  .sd-row > .ghost-btn { grid-column: 2 / -1; justify-self: start; }
}

/* ---------------- collapsed peek: three numbers, not one ---------------- */
/* Full width on its own rows. Figures beside a title could never be made to
   fit at every rail width, and truncating the last one is the worst of the
   available outcomes. Two rows of three: the averages you watch while a
   session runs, then the ones that say how it went. */
.stats-peek {
  flex: 1 0 100%; margin-left: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px;
}
.stats-peek > span:nth-child(n + 4) {
  background: color-mix(in srgb, var(--text) 3%, transparent);
}
.stats-peek > span:nth-child(n + 4) i { color: var(--text-dim); }
.stats-peek > span {
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
  padding: 4px 7px; border-radius: 7px;
  background: color-mix(in srgb, var(--text) 5%, transparent);
}
.stats-peek b {
  font-size: .5rem; font-weight: 700; letter-spacing: .1em;
  color: var(--text-faint); text-transform: uppercase;
}
.stats-peek i {
  font-style: normal; font-family: var(--font-mono);
  font-size: calc(.78rem * var(--sidebar-text)); font-weight: 650;
  color: var(--text); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis;
}
.stats-title { flex: 0 0 auto; }
.panel-head.as-btn .chev { margin-left: auto; }
/* Expanded, every figure is on show below; the peek would only repeat them. */
#panel-stats[data-collapsed="false"] .stats-peek { display: none; }

/* ---------------- session bests ---------------- */
.bests-block { display: flex; flex-direction: column; gap: 5px; flex: 0 0 auto; }
.bests-head {
  font-size: .55rem; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--text-faint);
}
/* Five bests, and an auto-fit with a 52px floor let only four of them onto a
   sidebar row -- so ao100 sat alone under a full row of four, reading as an
   afterthought rather than the fifth of a set. Worse, whether it happened at
   all depended on a dozen pixels: the block is ~12px narrower once the stats
   body has a scrollbar, so the ragged row came and went as the session grew.
   Five is not a count that changes, so it is written down rather than guessed
   at, and the tiles take whatever fifth of the row they are given. The bottom
   dock overrides this to a flex row (see the [data-dock] block below), which
   is where a different count per row was ever wanted. */
.bests-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 4px; }
.bst {
  display: flex; flex-direction: column; gap: 1px;
  /* A fifth of a 242px sidebar is 45px, and "AO100" has to fit inside it. */
  padding: 5px 4px; border-radius: 7px;
  background: color-mix(in srgb, var(--gold) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 18%, transparent);
}
.bst span {
  font-size: .52rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-faint);
}
.bst b {
  font-family: var(--font-mono); font-weight: 650;
  font-size: calc(.76rem * var(--sidebar-text));
  color: var(--gold); font-variant-numeric: tabular-nums;
}
/* Neither line has ever been allowed to be wider than its tile, but nothing
   said so, so a best that ran past a minute pushed its own tile wide and shunted
   the other four along -- five tiles that are meant to be a set, and one of them
   a different size for the rest of the session. Same answer as the solve list
   gives a long time: it ends early rather than printing over its neighbour. */
.bst span, .bst b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------------- the trend chart is a way into the full charts --------- */
#mini-chart-btn {
  /* The flexible part. min-height 0 rather than a floor: with a floor, a short
     window pushed the total past the panel cap and the chart got clipped by the
     panel's overflow instead of simply becoming shorter. */
  position: relative; flex: 1 1 70px; min-height: 0; overflow: hidden;
  display: flex; padding: 0; border-radius: var(--radius-sm);
  border: 1px solid transparent; cursor: pointer;
  transition: all var(--dur-fast) var(--ease);
}
#mini-chart-btn:hover {
  background: color-mix(in srgb, var(--text) 5%, transparent);
  border-color: color-mix(in srgb, var(--accent) 34%, transparent);
}
.mini-chart-cue {
  position: absolute; right: 5px; top: 4px;
  font-size: .52rem; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--accent);
  opacity: 0; transition: opacity var(--dur-fast) var(--ease);
}
#mini-chart-btn:hover .mini-chart-cue { opacity: 1; }

/* Below this the panel cap leaves the chart only a few pixels, and a 2px
   sliver of a graph is worse than no graph. The figures stay; the full charts
   are still one click away on the Statistics button (A). */
@media (max-height: 700px) { #mini-chart-btn { display: none; } }

/* ---------------- about ---------------- */
.about-hero { display: flex; align-items: center; gap: 13px; }
.about-avatar {
  width: 52px; height: 52px; flex: none; border-radius: 16px;
  display: grid; place-items: center;
  font-family: var(--font-ui); font-size: 1.5rem; font-weight: 800; color: var(--on-accent);
  background: linear-gradient(140deg, var(--accent), var(--accent-2));
  box-shadow: 0 10px 26px -12px var(--accent);
  /* It is an <img> now; the gradient stays underneath as the placeholder for
     the moment before it decodes, or forever if it fails to load. */
  object-fit: cover;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
}
.about-name { font-size: 1.18rem; font-weight: 700; letter-spacing: -.02em; }
.about-handle { font-size: .82rem; color: var(--accent-2); font-weight: 550; }
.about-bio { font-size: .86rem; line-height: 1.65; color: var(--text-dim); }

.about-link {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 14px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid var(--border);
  transition: all var(--dur-fast) var(--ease);
}
.about-link:hover {
  transform: translateY(-1px);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.about-link b { display: block; font-size: .88rem; font-weight: 600; }
.about-link .sub { display: block; font-size: .72rem; color: var(--text-faint); margin-top: 2px; }
.about-arrow { width: 16px; height: 16px; flex: none; color: var(--text-faint); }
.about-link:hover .about-arrow { color: var(--accent); }

/* ---------------- owner shine & card ----------------
   Marks whichever row's raceName is the site owner's, in both the race
   room and the daily leaderboard — see js/ownercard.js. The sweep reuses
   sotdShine above rather than a second keyframe for the same gesture. */
.owner-shine {
  position: relative; display: inline-block; overflow: hidden;
  /* !important: this sits inside .race-name > b, .db-name and .race-board-name,
     each of which sets its own color at equal or higher specificity. */
  color: var(--gold) !important;
  cursor: pointer;
}
.owner-shine::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(105deg,
    transparent 38%, color-mix(in srgb, var(--gold) 55%, transparent) 50%, transparent 62%);
  transform: translateX(-120%);
  animation: sotdShine 6s var(--ease) infinite;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) { .owner-shine::after { animation: none; opacity: 0; } }
[data-motion="off"] .owner-shine::after { animation: none !important; }

.race-av.owner, .db-face.owner {
  box-shadow: 0 0 0 1.5px var(--gold); cursor: pointer;
}

.owner-card { display: flex; flex-direction: column; gap: 10px; width: 250px; max-width: 100%; padding: 2px; }
.owner-card .about-link { padding: 9px 10px; }

.reel-card { display: flex; flex-direction: column; gap: 8px; }
.reel-empty {
  font-size: .78rem; line-height: 1.55; color: var(--text-faint);
  padding: 12px 14px; border-radius: var(--radius-sm);
  border: 1px dashed var(--border);
}

/* ---------------- metronome window ----------------
   Same floating-widget family as the mascot and the scramble preview: fixed,
   draggable, pinned with left/top once it has been moved. */
#metro {
  position: fixed; right: 26px; top: 120px; z-index: 46;
  width: 186px; padding: 8px 10px 10px;
  display: flex; flex-direction: column; gap: 8px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--surface-2) 92%, transparent);
  border: 1px solid var(--border);
  backdrop-filter: blur(calc(14px * var(--glass-strength))); -webkit-backdrop-filter: blur(calc(14px * var(--glass-strength)));
  box-shadow: 0 14px 30px rgba(0,0,0,.4);
  touch-action: none;
}
#metro[data-placed="true"] { right: auto; top: auto; }
#metro-grip {
  display: flex; align-items: center; gap: 6px; cursor: move;
  font-size: .62rem; letter-spacing: .12em; text-transform: uppercase;
  font-weight: 700; color: var(--text-faint);
}
#metro-grip svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; }
#metro-grip span { flex: 1; }
#metro-close {
  width: 18px; height: 18px; border-radius: 6px; cursor: pointer;
  display: grid; place-items: center; font-size: .9rem; line-height: 1;
  color: var(--text-faint); background: none; border: 0;
}
#metro-close:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 16%, transparent); }
#metro-beats { display: flex; gap: 6px; justify-content: center; }
#metro-beats i {
  width: 14px; height: 14px; border-radius: 50%;
  background: color-mix(in srgb, var(--text) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
#metro-beats i.on { background: var(--accent); border-color: transparent; }
/* The flash is the beat: it is restarted per hit from JS, so it has to be an
   animation rather than a transition. */
#metro-beats i.hit { animation: metroHit 220ms var(--ease); }
@keyframes metroHit { from { transform: scale(1.5); } to { transform: scale(1); } }
#metro-bpm { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
#metro-bpm button {
  width: 24px; height: 24px; border-radius: 7px; cursor: pointer;
  display: grid; place-items: center; font-size: .9rem; font-weight: 700; line-height: 1;
  color: var(--text-dim); background: color-mix(in srgb, var(--text) 8%, transparent); border: 0;
}
#metro-bpm button:hover { color: var(--text); background: color-mix(in srgb, var(--text) 16%, transparent); }
#metro-read { display: flex; align-items: baseline; gap: 3px; color: var(--text); }
#metro-read b { font-size: 1.15rem; font-weight: 700; font-variant-numeric: tabular-nums; }
#metro-read small { font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; color: var(--text-faint); }
#metro-range { width: 100%; }
#metro-play { width: 100%; }
#metro.ticking #metro-play { color: var(--accent); border-color: var(--accent); }

/* ---------------- mascot ---------------- */
#mascot {
  position: fixed; right: 26px; bottom: 210px; z-index: 45;
  width: 200px; height: 200px;
  cursor: move; touch-action: none;
  filter: drop-shadow(0 18px 34px rgba(0,0,0,.55));
  animation: mascotIn 420ms var(--ease-back);
}
#mascot[data-placed="true"] { right: auto; bottom: auto; }
@keyframes mascotIn { from { opacity: 0; transform: scale(.7) translateY(18px); } to { opacity: 1; transform: none; } }
#mascot-img {
  width: 100%; height: 100%; object-fit: contain;
  user-select: none; -webkit-user-drag: none; pointer-events: none;
}
#mascot-bar {
  position: absolute; top: -6px; right: -6px;
  display: flex; gap: 3px; padding: 3px; border-radius: 9px;
  background: color-mix(in srgb, var(--surface-2) 92%, transparent);
  border: 1px solid var(--border);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  opacity: 0; transition: opacity var(--dur) var(--ease);
}
#mascot:hover #mascot-bar { opacity: 1; }
#mascot-bar button {
  width: 22px; height: 22px; border-radius: 6px; cursor: pointer;
  display: grid; place-items: center;
  font-size: .86rem; line-height: 1; font-weight: 700;
  color: var(--text-dim);
  transition: all var(--dur-fast) var(--ease);
}
#mascot-bar button:hover { color: var(--text); background: color-mix(in srgb, var(--text) 12%, transparent); }
#mascot-close:hover { color: var(--danger) !important; background: color-mix(in srgb, var(--danger) 16%, transparent) !important; }
/* He is a toy, not part of the timer Ã¢â‚¬â€ he leaves during a solve like everything else. */
body.focus #mascot { opacity: 0; pointer-events: none; transition: opacity 220ms var(--ease); }

/* the brand is a <button> now */
.brand { cursor: pointer; background: none; border: 0; padding: 0; }
.brand:hover .brand-mark { transform: scale(1.08) rotate(-6deg); }
.brand-mark { transition: transform var(--dur) var(--ease-back); }

/* ===========================================================
   Custom scramble list Ã‚Â· manual entry Ã‚Â· Stackmat Ã‚Â· share card
   =========================================================== */

/* ---------------- the "these are your own scrambles" bar ---------------- */
#custom-bar {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 10px; padding: 4px 6px 4px 11px;
  border-radius: 999px;
  font-family: var(--font-ui); font-size: .68rem;
  letter-spacing: .1em; text-transform: uppercase; font-weight: 700;
  color: var(--accent-2);
  background: color-mix(in srgb, var(--accent-2) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent-2) 34%, transparent);
}
#custom-bar[hidden] { display: none; }
#custom-bar.spent {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border-color: color-mix(in srgb, var(--warn) 34%, transparent);
}
.cb-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; box-shadow: 0 0 10px currentColor;
}
.cb-x {
  width: 20px; height: 20px; border-radius: 50%; cursor: pointer;
  display: grid; place-items: center; font-size: .95rem; line-height: 1;
  color: inherit; background: none; border: 0;
  transition: background var(--dur-fast) var(--ease);
}
.cb-x:hover { background: color-mix(in srgb, currentColor 22%, transparent); }

/* The paste box and the list preview inside the drawer. */
.scramble-box {
  width: 100%; min-height: 220px; resize: vertical;
  font-family: var(--font-mono); font-size: .78rem; line-height: 1.65;
  white-space: pre; overflow-wrap: normal; overflow-x: auto;
}
.cs-preview {
  margin-top: 12px; max-height: 320px; overflow: auto;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--bg) 55%, transparent);
}
.cs-line {
  display: grid; grid-template-columns: 34px 1fr; gap: 8px;
  padding: 6px 10px; font-family: var(--font-mono); font-size: .73rem;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}
.cs-line:last-child { border-bottom: 0; }
.cs-line.done { opacity: .38; text-decoration: line-through; }
.cs-line.now { background: color-mix(in srgb, var(--accent-2) 12%, transparent); }
.cs-line.more { display: block; text-align: center; color: var(--text-faint); font-style: italic; }
.cs-i { color: var(--text-faint); text-align: right; }
.cs-s { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------------- typed times ---------------- */
#manual-entry {
  display: flex; gap: 8px; align-items: center;
  margin-top: 14px; width: min(520px, 88vw);
}
#manual-entry[hidden] { display: none; }
#manual-input {
  flex: 1; min-width: 0;
  padding: 12px 16px; border-radius: 14px;
  font-family: var(--font-mono); font-size: 1rem; font-weight: 600;
  color: var(--text); text-align: center;
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  border: 1px solid var(--border);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
#manual-input::placeholder {
  font-family: var(--font-ui); font-weight: 500; font-size: .82rem; color: var(--text-faint);
}
#manual-input:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 26%, transparent);
}
#manual-input.bad { animation: manualShake 320ms var(--ease); border-color: var(--danger); }
@keyframes manualShake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-7px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(3px); }
}

/* ---------------- Stackmat status ---------------- */
#stackmat-bar {
  display: inline-flex; align-items: center; gap: 9px;
  margin-top: 14px; padding: 7px 14px; border-radius: 999px;
  font-family: var(--font-ui); font-size: .74rem; color: var(--text-dim);
  background: color-mix(in srgb, var(--surface) 66%, transparent);
  border: 1px solid var(--border);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
}
#stackmat-bar[hidden] { display: none; }
.sm-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--text-faint);
}
.sm-dot.wait { background: var(--warn); animation: smPulse 1.4s ease-in-out infinite; }
.sm-dot.live { background: var(--ok); box-shadow: 0 0 12px var(--ok); }
@keyframes smPulse { 50% { opacity: .25; } }

/* ---------------- share sheet ---------------- */
.sh-wrap { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; }
.sh-scrim {
  position: absolute; inset: 0;
  background: rgba(4, 4, 10, .72);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  animation: shFade 200ms var(--ease);
}
@keyframes shFade { from { opacity: 0; } }
.sh-box {
  position: relative;
  width: min(460px, 94vw); max-height: 92vh;
  display: flex; flex-direction: column; gap: 14px;
  padding: 18px; border-radius: 24px;
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  animation: shIn 300ms var(--ease-back);
  overflow: auto;
}
@keyframes shIn { from { opacity: 0; transform: translateY(22px) scale(.97); } }
.sh-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sh-head h2 { font-size: 1rem; font-weight: 700; letter-spacing: -.01em; }
.sh-ico {
  width: 17px; height: 17px; fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}
.sh-stage {
  border-radius: 18px; overflow: hidden;
  border: 1px solid var(--border);
  background: var(--bg);
}
.sh-canvas { display: block; width: 100%; height: auto; }
.sh-acts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.sh-acts.two { grid-template-columns: repeat(2, 1fr); }
.sh-act {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 11px 8px; border-radius: 13px; cursor: pointer;
  font-family: var(--font-ui); font-size: .78rem; font-weight: 600;
  color: var(--text);
  background: color-mix(in srgb, var(--text) 8%, transparent);
  border: 1px solid var(--border);
  transition: all var(--dur-fast) var(--ease);
}
.sh-act:hover { background: color-mix(in srgb, var(--text) 14%, transparent); transform: translateY(-1px); }
.sh-act:disabled { opacity: .5; cursor: progress; transform: none; }
.sh-act.primary { color: var(--on-accent); background: var(--accent); border-color: transparent; }
.sh-act.primary:hover { filter: brightness(1.08); }
.sh-share-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.sh-share-lbl {
  font-size: .66rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-faint); font-weight: 700;
}
.sh-links { display: flex; gap: 7px; flex-wrap: wrap; }
.sh-link {
  padding: 6px 12px; border-radius: 999px;
  font-size: .74rem; font-weight: 600; color: var(--text-dim);
  border: 1px solid var(--border);
  transition: all var(--dur-fast) var(--ease);
}
.sh-link:hover {
  color: var(--text); border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.sh-foot {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding-top: 4px; border-top: 1px solid var(--border);
  font-size: .74rem;
}
.sh-cred { color: var(--text-dim); font-weight: 600; }
.sh-cred:hover { color: var(--text); }
.sh-cred.insta { color: var(--accent-2); }
.sh-dot { color: var(--text-faint); }

/* Manual entry and the Stackmat both replace the spacebar, so the hint that
   talks about the spacebar has no business being on screen. */
body[data-input="manual"] #timer-hint,
body[data-input="stackmat"] #timer-hint { display: none; }
/* Focus mode clears the page around the digits Ã¢â‚¬â€ but not the field you are
   typing into, and not the connection status you are watching. */
body.focus #manual-entry,
body.focus #stackmat-bar { opacity: 1; }

/* The bottom of the times strip, once it has scrolled all the way back to the
   first solve of the session Ã¢â‚¬â€ so "nothing more is loading" is stated rather
   than guessed at. */
.hist-end {
  padding: 12px 4px 8px;
  text-align: center;
  font-family: var(--font-ui);
  font-size: .6rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--text-faint);
}

/* The times strip now runs all the way back to solve #1, which on a long
   session means thousands of rows in the DOM, and anything that forces a
   layout then has to walk all of them. Containment keeps each row's internals
   out of that walk.

   Deliberately not `content-visibility: auto`, which would be the bigger win:
   it brings size containment with it, and a row whose height comes from
   `contain-intrinsic-size` renders squashed the moment a browser does not
   accept the exact syntax used. A slower list beats a broken one. */
.solve-chip {
  contain: layout paint style;
}

/* The confetti canvas sits below the toasts, and a toast blurs whatever is
   behind it Ã¢â‚¬â€ so a moving canvas means re-blurring its backdrop every frame,
   right when the celebration is already the busiest thing on screen. The blur
   is decoration; the celebration is the point. */
body.celebrating .toast {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: var(--surface-2);
}

/* ---- reconstruct, one click from the times list ----
   It used to ride *over* the ao5 on hover, hiding it Ã¢â‚¬â€ which quietly made the
   ao5 unclickable with a mouse, since the pointer that reveals the button is
   the same pointer that would have pressed the number underneath it. It has a
   column of its own now: 20px, always reserved so nothing shifts, with the
   button itself only painted on hover. */
.solve-chip { position: relative; }
.chip-recon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; border-radius: 6px;
  background: var(--accent); color: var(--on-accent);
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}
.chip-recon svg { width: 12px; height: 12px; }
.chip-recon i { display: none; }
.solve-chip:hover .chip-recon,
.chip-recon:focus-visible { opacity: 1; pointer-events: auto; }
.chip-recon:hover { filter: brightness(1.12); }
/* No hover on a touch screen, and a permanent button would crowd every row Ã¢â‚¬â€
   the solve menu carries the same entry there, so the column goes too. */
@media (hover: none) {
  .chip-recon { display: none; }
  .solve-chip, .hist-cols { grid-template-columns: var(--hist-grid-touch); }
}

/* ---- the two topbar buttons that are about something, not everything ----
   Spotify green is Spotify's, and their guidelines ask for it. The
   reconstructor gets the accent, so both read as places rather than settings. */
#btn-spotify { color: #1db954; }
#btn-spotify:hover { color: #1ed760; background: rgba(29, 185, 84, .12); }
#btn-recon { color: var(--accent); }
#btn-recon:hover {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent) 34%, transparent);
}

/* =========================================================
   DOCKABLE TILES

   A grip on the top edge of each movable panel, three snap zones that light
   up while one is in the air, a bottom bar they can be dropped into, and a
   float layer for anything parked away from a dock.
   ========================================================= */

.tile-grip {
  position: absolute; top: -10px; left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  display: flex; align-items: center; justify-content: center;
  /* Big enough to aim at without hunting. The first version was 40x15 at .35
     opacity and people could not find it, let alone hit it. */
  width: 56px; height: 20px;
  border-radius: 99px; cursor: grab;
  color: var(--text-dim);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 3px 10px -4px rgba(0, 0, 0, .55);
  opacity: .62;
  /* The drag owns the gesture: without this a touch drag scrolls the page
     underneath instead of moving the tile. */
  touch-action: none;
  transition: opacity var(--dur) var(--ease), color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}
/* The icon rule in base.css strokes every path in every SVG, which turns six
   1.5px dots into six rings. These are meant to be filled. */
.tile-grip svg { width: 22px; height: 15px; fill: currentColor; stroke: none; }
[data-tile]:hover > .tile-grip { opacity: 1; }
.tile-grip:hover {
  opacity: 1; color: var(--text);
  background: color-mix(in srgb, var(--accent) 26%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
.tile-grip:active { cursor: grabbing; }

/* The list inside a panel must not replay its entry animation every time the
   panel changes parent Ã¢â‚¬â€ see tiles.js. */
body.tiles-moving .solve-chip { animation: none; }

/* Nothing to drag below the mobile breakpoint Ã¢â‚¬â€ see tilesEnabled() in
   tiles.js, which uses the same width. */
@media (max-width: 860px) {
  .tile-grip { display: none; }
}

/* ---- floating ---- */
#tile-float-layer {
  position: fixed; inset: 0; z-index: 13;
  pointer-events: none;
}
#tile-float-layer > * { pointer-events: auto; }
.tile-float {
  position: fixed;
  /* A floating times list still has to be able to scroll rather than run off
     the bottom of the screen. */
  max-height: min(66vh, 620px);
}
.tile-dragging {
  z-index: 95;
  cursor: grabbing;
  box-shadow: 0 30px 60px -20px rgba(0, 0, 0, .8);
  transform: scale(1.012);
  opacity: .95;
  /* The pointer is driving position directly; a transition would trail it. */
  transition: none;
}

/* ---- the bottom bar ---- */
/* Full width and centred with flexbox rather than `left: 50%` plus a
   translate. A transform makes an element the containing block for every
   `position: fixed` descendant, and a tile being dragged out of this bar is
   exactly that Ã¢â‚¬â€ with the transform here, its viewport coordinates were
   resolved against the bar instead and it shot off to the side of the screen
   the moment you picked it up. */
#dock-bottom {
  /* Exactly the strip the snap outline promises Ã¢â‚¬â€ between the two rails, not
     the whole viewport. Full width ran the bar on underneath the rail panels
     and the scramble preview. --dock-x/--dock-w come from bottomZone() in
     tiles.js, the same measurement that draws the outline. */
  position: fixed; bottom: 14px; z-index: 13;
  left: var(--dock-x, 12px);
  /* The fallback matters: on a window too narrow for a strip between the rails
     bottomZone() reports nothing, and `auto` would shrink the bar to its own
     content. Span the window instead. */
  width: var(--dock-w, calc(100% - 24px));
  right: auto;
  /* Stacked rows, not a row of cards. Two tiles sharing the width meant two
     half-width strips, and half a screen is not enough for eight figures *or*
     for a player Ã¢â‚¬â€ the whole reason to be down here is the full width. The
     player ends up on the bottom row, which is where a player bar belongs. */
  display: flex; flex-direction: column; align-items: stretch; gap: 10px;
  padding: 0 12px;
  pointer-events: none;
}
#dock-bottom:empty { display: none; }
/* The bar is a bar: whatever is docked in it spans the width of the screen,
   rather than sitting in the middle as a card you have to scroll sideways to
   read. */
#dock-bottom > * { pointer-events: auto; flex: 0 0 auto; min-width: 0; width: auto; }
/* A player bar belongs at the very bottom of the screen, under anything else
   docked down here. Only in the bar Ã¢â‚¬â€ the rails keep their own stacking. */
#dock-bottom > #panel-spotify { order: 2; }
/* The strip is a good deal narrower than the window now that it sits between
   the rails, so what fits in it is a question about the strip, not about the
   screen. `container-type` lets the rules below ask the right one. */
#dock-bottom > .panel {
  max-height: 148px;
  container-type: inline-size;
  container-name: dockbar;
}
/* Below the breakpoint the rails are tiles under the timer, there is no strip
   between them, and the bar simply spans the screen. */
@media (max-width: 860px) {
  #dock-bottom { left: 0; right: 0; width: auto; padding: 0 12px; }
}

/* ---- the statistics panel, laid on its side ----
   In a rail it is a column of figures with a chart under it. Across the bottom
   of the screen it has one short dimension and a very long one, so the three
   blocks sit side by side and the figures run in a single row. */
#panel-stats[data-dock="bottom"] { padding: 10px var(--pad); }
#panel-stats[data-dock="bottom"] #stats-body { margin-top: 8px; }
#panel-stats[data-dock="bottom"] #stats-inner {
  flex-direction: row; align-items: stretch; gap: 14px;
  overflow: hidden;
}
#panel-stats[data-dock="bottom"] .stat-grid {
  display: flex; flex-wrap: nowrap; gap: 6px;
  flex: 1 1 auto; min-width: 0;
}
#panel-stats[data-dock="bottom"] .stat { flex: 1 1 0; min-width: 0; justify-content: center; }
/* Consistency spans two columns in the grid; in a row it is just one more cell,
   and its bar sits under its figure. */
#panel-stats[data-dock="bottom"] .stat.wide { flex: 1.4 1 0; }
#panel-stats[data-dock="bottom"] .bests-block { flex: 0 0 auto; max-width: 42%; }
#panel-stats[data-dock="bottom"] .bests-grid {
  display: flex; flex-wrap: nowrap; gap: 4px;
}
#panel-stats[data-dock="bottom"] .bst { flex: 0 0 auto; }
#panel-stats[data-dock="bottom"] .mini-chart-wrap { flex: 0 0 clamp(120px, 14vw, 220px); }
/* A divider between the three blocks, so a single long row still reads as
   three groups rather than one undifferentiated strip of numbers. */
#panel-stats[data-dock="bottom"] .bests-block,
#panel-stats[data-dock="bottom"] .mini-chart-wrap {
  border-left: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  padding-left: 14px;
}
/* Collapsed, the peek row is the whole panel Ã¢â‚¬â€ let it use the width instead of
   wrapping into three cramped lines. Scoped to the collapsed state on purpose:
   `[data-collapsed="false"] .stats-peek { display: none }` has exactly the same
   specificity as this rule, so an unscoped version simply won it by coming
   later and left the peek sitting on top of the expanded figures. */
#panel-stats[data-dock="bottom"][data-collapsed="true"] .stats-peek {
  display: flex; flex-wrap: nowrap; gap: 22px; justify-content: flex-end;
}
#panel-stats[data-dock="bottom"][data-collapsed="true"] .stats-peek > span:nth-child(n + 4) { display: flex; }

/* Below the point where a row of eight figures stops being readable, drop the
   trailing blocks rather than squeezing everything Ã¢â‚¬â€ measured against the bar
   itself, not the window. */
@container dockbar (max-width: 1020px) {
  #panel-stats[data-dock="bottom"] .mini-chart-wrap { display: none; }
}
@container dockbar (max-width: 800px) {
  #panel-stats[data-dock="bottom"] .bests-block { display: none; }
}
body.focus #dock-bottom, body.focus #tile-float-layer { opacity: 0; pointer-events: none; }
#dock-bottom, #tile-float-layer { transition: opacity 420ms var(--ease) 40ms; }

/* How much room the bar is actually taking, measured and written by tiles.js Ã¢â‚¬â€
   its height depends on what is docked in it, and a guessed constant would be
   wrong for every combination but one. Everything anchored to the bottom of the
   screen steps up by it, rather than being buried underneath. */
/* Each rail stops above whichever of the two things below it reaches higher:
   the credit chip (left rail only Ã¢â‚¬â€ that is the corner it sits in) or the
   bottom bar. Both measured rather than assumed: the chip's own position moves
   when the bar is occupied, and a flat 36px left it clipping the last panel in
   the rail by a few pixels.

   The scramble preview is normally NOT one of them: reserving room for it cost
   the solve list a chunk of its height permanently, whether or not the two were
   anywhere near each other, so it moves aside instead. --cube-clear-* is only
   ever non-zero in the one case where it cannot Ã¢â‚¬â€ a panel in each rail, both
   reaching down into its corner, so there is no free side to move to. See
   placePreview() in tiles.js. */
@media (min-width: 861px) {
  #sidebar {
    /* 52px is what measureChip() lands on for the chip at its own size, so the
       rail does not lose 16px of the solve list the moment it is measured. */
    padding-bottom: max(var(--chip-h-left, 52px), var(--dock-clear-left, 0px), var(--cube-clear-left, 0px));
  }
  #sidebar-right {
    padding-bottom: max(var(--chip-h-right, 0px), var(--dock-clear-right, 0px), var(--cube-clear-right, 0px));
  }
}
/* Both of these live in bottom corners, outside the strip the bar occupies, so
   normally neither moves at all. The variables are zero unless the bar really
   is in front of them Ã¢â‚¬â€ a rail switched off and the bar widening into that
   corner, or a phone where it spans the screen. */
#creator-chip { bottom: max(14px, calc(var(--dock-clear-chip, 0px) + 6px)); }
@media (min-width: 861px) {
  body:not(.no-cube) #panel-cube:not([data-placed]) {
    bottom: max(18px, calc(var(--dock-clear-cube, 0px) + 12px));
  }
}
/* The escape hatch: placePreview() flips this on only while the preview's usual
   corner is occupied and this one is not. */
#panel-cube[data-side="left"] { right: auto; left: max(18px, var(--cube-edge-left, 0px)); }
/* ...and when both corners are occupied Ã¢â‚¬â€ a race panel down the right rail and
   the solve list down the left Ã¢â‚¬â€ the empty strip between the two rails, which
   is the timer's own floor. --cube-x is measured and written by placePreview. */
#panel-cube[data-side="centre"] { right: auto; left: var(--cube-x, 50%); }
@media (max-width: 860px) {
  #panel-cube[data-side="left"] { right: auto; left: max(10px, var(--cube-edge-left, 0px)); }
}
/* ...and the credit chip lives in that corner, so the two swap: the chip takes
   the corner the preview just left. Climbing above the preview instead would
   have cost the left rail everything the preview's own reservation used to,
   which is the whole thing this arrangement exists to avoid. */
#app:has(#panel-cube[data-side="left"]) #creator-chip {
  left: auto; right: 18px;
}

/* ---- snap zones ---- */
#snap-zones {
  position: fixed; inset: 0; z-index: 90;
  pointer-events: none;
}
.snap-zone {
  position: fixed;
  border-radius: var(--radius);
  border: 2px dashed color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  display: grid; place-items: center;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.snap-zone.on {
  border-style: solid;
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  box-shadow: inset 0 0 40px color-mix(in srgb, var(--accent) 22%, transparent);
}
.snap-tag {
  font-size: .64rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-dim);
  padding: 5px 11px; border-radius: 99px;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.snap-zone.on .snap-tag { color: var(--text); border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
[data-motion="off"] .snap-zone { transition: none; }

/* =========================================================
   NOW PLAYING, LAID ON ITS SIDE

   The same panel that lives in a rail Ã¢â‚¬â€ same markup, same handlers, same
   features Ã¢â‚¬â€ rearranged into one row when it is docked across the bottom.
   There is deliberately no second player widget: one that "looks like a bar"
   but reimplements half the controls is two things to keep in step, and the
   half it reimplemented is the half that went missing.
   ========================================================= */
/* Laid out the way a music player's bar is: the record on the left, and one
   centred column holding the transport with the scrubber directly beneath it.
   A grid rather than a row, because the two centre pieces are siblings in the
   markup Ã¢â‚¬â€ the rail version stacks them in a column and this has to reuse that
   same markup, so the layout does the arranging instead of a wrapper element. */
#panel-spotify[data-dock="bottom"] {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(280px, 1.9fr) minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 18px; row-gap: 5px;
  padding: 9px 20px;
  /* The cover's own two colours, washed across the bar the way the album
     header of a music player is Ã¢â‚¬â€ see paintBarTint() in main.js. Falls back to
     the accent pair, so it looks deliberate before anything is playing and
     when the artwork refuses to be read. */
  background:
    linear-gradient(100deg,
      color-mix(in srgb, var(--sb-1, var(--accent)) 30%, var(--surface)) 0%,
      color-mix(in srgb, var(--sb-2, var(--accent-2)) 18%, var(--surface)) 46%,
      color-mix(in srgb, var(--surface) 96%, transparent) 100%);
  transition: background 600ms var(--ease);
}
[data-motion="off"] #panel-spotify[data-dock="bottom"] { transition: none; }

/* "NOW PLAYING Ã‚Â· playing" is a heading for a card in a column. In a bar the
   artwork and the play button already say both of those things. */
#panel-spotify[data-dock="bottom"] .panel-head { display: none; }

#panel-spotify[data-dock="bottom"] #np-card {
  grid-column: 1; grid-row: 1 / 3;
  justify-self: start; gap: 12px;
  /* Both needed: `justify-self: start` sizes the card to its content, and a
     long track name would then run straight out of its column and into the
     transport. These hold it to the track so the title ellipsises instead. */
  min-width: 0; max-width: 100%;
}
#panel-spotify[data-dock="bottom"] .np-art-wrap { width: 46px; height: 46px; }
#panel-spotify[data-dock="bottom"] .np-title { font-size: .84rem; }
/* The two colours pulled out of this cover stay Ã¢â‚¬â€ they are the point of the
   whole album-tint feature, not decoration to be dropped for a few pixels. */
#panel-spotify[data-dock="bottom"] .np-swatches { margin-top: 3px; }

#panel-spotify[data-dock="bottom"] .np-controls {
  grid-column: 2; grid-row: 1;
  justify-content: center; gap: 8px;
}
#panel-spotify[data-dock="bottom"] .np-progress {
  grid-column: 2; grid-row: 2;
  flex-direction: row; align-items: center; gap: 10px;
}
#panel-spotify[data-dock="bottom"] .np-progress .np-bar { flex: 1 1 auto; height: 4px; }
/* `display: contents` dissolves the times wrapper so its two figures become
   siblings of the bar, and `order` puts the elapsed one in front of it Ã¢â‚¬â€
   elapsed | scrubber | duration, without touching the markup the rail version
   depends on. */
#panel-spotify[data-dock="bottom"] .np-times { display: contents; }
#panel-spotify[data-dock="bottom"] .np-times > span {
  flex: none; font-size: .66rem; color: var(--text-dim);
}
#panel-spotify[data-dock="bottom"] #np-elapsed { order: -1; }
/* A standing complaint about the controls gets its own row under everything. */
#panel-spotify[data-dock="bottom"] .np-note { grid-column: 1 / -1; grid-row: 3; }
/* On a coloured wash the accent fill loses its edge; white reads on every
   album. */
#panel-spotify[data-dock="bottom"] #np-fill { background: color-mix(in srgb, var(--text) 88%, transparent); }
#panel-spotify[data-dock="bottom"] .np-bar { background: color-mix(in srgb, var(--text) 22%, transparent); }

/* Narrow enough that a three-column grid leaves the centre too tight: drop the
   empty right-hand column and let the player use it. */
@media (max-width: 1180px) {
  #panel-spotify[data-dock="bottom"] {
    grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  }
}
@media (max-width: 860px) {
  #panel-spotify[data-dock="bottom"] { padding: 8px 13px; column-gap: 12px; }
  #panel-spotify[data-dock="bottom"] .np-swatches { display: none; }
}
}
}

/* The session bests open the same drawer the stat grid does, so they get the
   same affordance Ã¢â‚¬â€ they used to be figures you could read but not follow. */
.bst { cursor: pointer; }

/* ===========================================================
   RACE MODE

   Two jobs that pull in opposite directions. While you are still solving
   the panel has to convey pressure Ã¢â‚¬â€ how much of the room is already
   done Ã¢â‚¬â€ while giving away nothing at all about how fast any of them
   were. The moment you finish it becomes a leaderboard and everything
   unlocks at once. That switch is the whole visual design.
   =========================================================== */

#btn-race { position: relative; }
/* The flag picks up the accent while a room is open, so a panel that has
   been docked away or folded on a phone is never the only sign a race is
   actually running. */
#btn-race.live { color: var(--accent); }
#btn-race.live::after {
  content: ''; position: absolute; top: 5px; right: 5px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); box-shadow: 0 0 0 2px var(--bg);
}

#panel-race .race-title { display: inline-flex; align-items: center; gap: 7px; }
#panel-race .race-flag { width: 14px; height: 14px; flex: none; }
#panel-race .race-code {
  font-family: ui-monospace, monospace;
  letter-spacing: .14em; color: var(--accent);
}
/* The rail is a fixed-height flex column, so when it runs out of room it
   COMPRESSES this panel rather than scrolling Ã¢â‚¬â€ and a compressed flex column
   pushes whatever sits at its end straight out of the box. That end is the
   room controls, so a long enough chat log walked "Leave room" off the bottom
   of the screen.

   The rule that prevents it: everything in the body holds its natural height
   except the chat, which is the one part that can afford to give. min-height:0
   is what actually permits the shrink Ã¢â‚¬â€ flex items refuse to go below their
   content without it, which is why the overflow had to go somewhere. */
/* NOT a scroll container, deliberately, and this is the subtle one.
   `overflow-y: auto` here looks like a safe backstop and is the opposite: an
   overflow container is free to be as tall as its content, so flex stops
   compressing what is inside it Ã¢â‚¬â€ the chat log below never shrank again, the
   panel simply grew past its rail, and the controls got clipped instead of
   pushed. Leaving overflow alone is what keeps the shrink working. */
#panel-race { min-height: 0; }
#panel-race .race-body {
  display: flex; flex-direction: column; gap: 7px;
  min-height: 0;
}
#panel-race .race-body > * { flex: none; }
/* An explicit floor, not `0` and not `auto` Ã¢â‚¬â€ both were tried and both were
   wrong in opposite directions.
   `0` let this box shrink below its own contents: the log and composer spilled
   out while the controls were placed against the collapsed box, so the buttons
   painted on top of the messages.
   `auto` is meant to resolve to the content-based minimum, but through a
   scrolling child it resolved to the box's FULL height Ã¢â‚¬â€ so it refused to
   shrink at all and the panel overflowed its rail instead.
   96px is the composer, the log's own 44px floor and the header, added up: low
   enough to give the panel room, high enough that nothing folds through. */
#panel-race .race-chat { flex: 1 1 auto; min-height: 96px; position: relative; }
#panel-race[data-collapsed="true"] .race-body { display: none; }
#panel-race[data-collapsed="true"] .race-head .chev { transform: rotate(-90deg); }

/* ---- the pressure meter ----
   How much of the room has finished, and nothing else. This is the one
   thing you are allowed to know mid-solve, so it is the one thing drawn
   to be readable without looking away from the timer. */
.race-meter {
  height: 3px; border-radius: 3px; overflow: hidden;
  background: color-mix(in srgb, var(--border) 70%, transparent);
}
.race-meter > i {
  display: block; height: 100%; width: 0;
  background: var(--accent); border-radius: inherit;
  transition: width .45s cubic-bezier(.22,1,.36,1), background-color .3s;
}
.race-meter[data-state="pressure"] > i { background: var(--warn); }
.race-meter[data-state="revealed"] > i { background: var(--ok); }

.race-status {
  display: flex; align-items: baseline; justify-content: space-between;
  font-size: .74rem; letter-spacing: .04em;
}
.race-round { color: var(--text); font-weight: 650; }
.race-count { color: var(--text-faint); font-variant-numeric: tabular-nums; }

/* ---- rows ---- */
.race-rows { display: flex; flex-direction: column; gap: 3px; }

.race-row {
  display: grid;
  grid-template-columns: 14px 22px minmax(0, 1fr) auto;
  align-items: center; gap: 8px;
  padding: 5px 7px; border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-size: .8rem; min-height: 32px;
}
.race-row[data-me="true"] {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border-color: color-mix(in srgb, var(--accent) 26%, transparent);
}

.race-rank {
  font-size: .68rem; color: var(--text-faint);
  font-variant-numeric: tabular-nums; text-align: right;
}
.race-row:nth-child(1) .race-rank:not(:empty) { color: var(--gold); font-weight: 700; }
.race-row:nth-child(2) .race-rank:not(:empty),
.race-row:nth-child(3) .race-rank:not(:empty) { color: var(--text-dim); font-weight: 650; }

.race-av {
  width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: .58rem; font-weight: 750;
  color: #fff;
  background: hsl(var(--av-h, 260) 58% 42%);
  box-shadow: inset 0 0 0 1px hsl(var(--av-h, 260) 58% 62% / .55);
}

/* The host ring. Says the same thing a "host" chip did and takes none of
   the width the name needs. */
.race-av.host {
  box-shadow: inset 0 0 0 1px hsl(var(--av-h, 260) 58% 62% / .55),
              0 0 0 1.5px var(--gold);
}

/* overflow:hidden is the guarantee, not the plan. The children that do not
   shrink (the wins badge) were painting outside this box and straight over
   the time column; clipping means the worst case is a trimmed badge rather
   than two numbers on top of each other. */
.race-name { display: flex; align-items: center; gap: 5px; min-width: 0; overflow: hidden; }
.race-name > b {
  font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* A floor, so a crowded row truncates to "CuberÃ¢â‚¬Â¦" rather than collapsing
     to a sliver. In a 200px rail something has to give, and an ellipsis is
     a far better answer than a name nobody can read at all. */
  min-width: 2.8em;
}
.race-tag {
  font-style: normal; font-size: .58rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-faint);
  border: 1px solid var(--border); border-radius: 4px;
  padding: 1px 4px; flex: none;
}
.race-tag.me { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.race-wins {
  font-style: normal; font-size: .6rem; font-weight: 700;
  color: var(--gold); flex: none; font-variant-numeric: tabular-nums;
}

/* ---- the right-hand column ---- */
.race-val { display: flex; align-items: center; gap: 6px; justify-self: end; }
.race-time {
  font-family: ui-monospace, monospace;
  font-size: .84rem; font-weight: 650; font-variant-numeric: tabular-nums;
  color: var(--text);
}
.race-row[data-state="dnf"] .race-time { color: var(--danger); }

/* Won or lost, in one glance. The rank number says the same thing without
   relying on colour, so this is the fast read rather than the only one. */
.race-row[data-outcome="win"] .race-time { color: var(--ok); }
.race-row[data-outcome="loss"] .race-time { color: var(--danger); }
.race-row[data-outcome="win"] {
  border-color: color-mix(in srgb, var(--ok) 30%, transparent);
  background: color-mix(in srgb, var(--ok) 8%, transparent);
}
/* data-me keeps its accent fill; the winner's border still reads on top. */
.race-row[data-outcome="win"][data-me="true"] {
  background: color-mix(in srgb, var(--ok) 13%, transparent);
}

.race-row.editable { cursor: pointer; }
.race-row.editable:hover { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

/* ---- room chat ----
   Open in every phase, lobby or mid-round. */
.race-chat {
  display: flex; flex-direction: column; gap: 5px;
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--border);
}
/* A label, not a control. The chat does not fold Ã¢â‚¬â€ see the note in the
   constructor in race.js for why the fold was removed rather than fixed. */
.race-chat-head {
  display: flex; align-items: center; gap: 6px;
  font-size: .62rem; letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-faint);
}

.race-chat-log {
  display: flex; flex-direction: column; gap: 2px;
  /* Capped when there is room, and the first thing to give when there is not.
     `flex: 1 1 auto` with `min-height: 0` is the half that matters: it makes
     this the shrinkable part of the panel, so a rail running short takes the
     height out of the log instead of out of the buttons underneath it. The
     floor stops it collapsing to a sliver on the way. */
  flex: 1 1 auto;
  min-height: 44px; max-height: 152px;
  overflow-y: auto; overscroll-behavior: contain;
  font-size: .74rem;
}
.race-chat-empty { color: var(--text-faint); font-size: .7rem; padding: 2px 0; }

.race-chat-msg {
  display: flex; flex-direction: column; gap: 0;
  padding: 2px 7px; border-radius: var(--radius-sm);
  min-width: 0;
}
.race-chat-msg.run-on { padding-top: 0; }
.race-chat-msg[data-me="true"] {
  background: color-mix(in srgb, var(--accent) 9%, transparent);
}
.race-chat-who {
  font-size: .62rem; font-weight: 700; letter-spacing: .02em;
  color: hsl(var(--av-h, 260) 52% 52%);
}
.race-chat-text {
  color: var(--text); line-height: 1.35;
  /* The one rule that matters here: a 200-character run of one word must
     wrap inside the rail rather than widen it. */
  overflow-wrap: anywhere; white-space: pre-wrap;
}

/* ---- the emoji tray ----
   Opens between the log and the composer rather than floating over either.
   A popover would have to be positioned against a panel the user can dock to
   four different edges, and an inline row costs one line of height in the one
   place there is room for it. */
.race-emoji {
  /* Floated over the log rather than wedged into the column.
     Inline, it was ~50px of layout the panel had to find from somewhere, and
     what it took it from was the room controls Ã¢â‚¬â€ opening the tray pushed the
     buttons into the composer. A transient popup should not reflow the panel
     it opens in, so it costs no height at all now and the layout underneath
     does not move when it appears. */
  position: absolute; left: 0; right: 0; bottom: 33px; z-index: 6;
  display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;
  padding: 4px; border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  box-shadow: 0 8px 22px -10px rgba(0,0,0,.7);
}
.race-emoji-btn {
  border: 0; background: none; cursor: pointer;
  padding: 2px 0; border-radius: 5px;
  font-size: .92rem; line-height: 1.25;
  /* The emoji font, explicitly. Inheriting the panel's stack meant a couple
     of these rendered as the monochrome text glyph on Windows. */
  font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
.race-emoji-btn:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }

.race-chat-form { display: flex; gap: 5px; align-items: center; }
.race-chat-emoji {
  flex: none; display: grid; place-items: center;
  width: 28px; height: 28px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: none; color: var(--text-dim); cursor: pointer;
}
.race-chat-emoji svg {
  width: 15px; height: 15px; fill: none;
  stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.race-chat-emoji:hover:not(:disabled),
.race-chat-emoji[aria-expanded="true"] {
  color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.race-chat-emoji:disabled { opacity: .4; cursor: not-allowed; }
.race-chat-input {
  flex: 1; min-width: 0;
  padding: 5px 8px; font-size: .76rem;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.race-chat-input:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
.race-chat-input:disabled { opacity: .55; cursor: not-allowed; }
.race-chat-send {
  flex: none; display: grid; place-items: center;
  width: 28px; height: 28px; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: none; color: var(--text-dim); cursor: pointer;
}
.race-chat-send svg {
  width: 14px; height: 14px; fill: none;
  stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
.race-chat-send:hover { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }

/* ---- standings ---- */
.race-board {
  display: flex; flex-direction: column; gap: 2px;
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--border);
}
.race-board-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: .62rem; letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-faint); margin-bottom: 3px;
}
.race-board-row {
  display: grid;
  grid-template-columns: 14px minmax(0, 1fr) auto auto;
  align-items: center; gap: 8px;
  padding: 3px 7px; border-radius: var(--radius-sm);
  font-size: .74rem;
}
.race-board-row[data-me="true"] { background: color-mix(in srgb, var(--accent) 10%, transparent); }
/* Somebody who has left keeps their wins and stops claiming to be here. */
.race-board-row[data-gone="true"] { opacity: .5; }
.race-board-rank {
  font-size: .66rem; color: var(--text-faint);
  font-variant-numeric: tabular-nums; text-align: right;
}
.race-board-row:first-of-type .race-board-rank { color: var(--gold); font-weight: 700; }
.race-board-name {
  color: var(--text); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.race-board-best {
  font-family: ui-monospace, monospace; font-size: .7rem;
  color: var(--text-faint); font-variant-numeric: tabular-nums;
}
.race-board-wins {
  font-size: .7rem; font-weight: 700; color: var(--gold);
  font-variant-numeric: tabular-nums;
}

.race-delta {
  font-style: normal; font-size: .62rem; font-weight: 650;
  font-variant-numeric: tabular-nums;
  padding: 1px 4px; border-radius: 4px;
}
/* A glyph as well as a colour, every time. A red/green pair on its own is
   not a difference every reader can actually see. */
.race-delta.faster { color: var(--ok);     background: color-mix(in srgb, var(--ok) 14%, transparent); }
.race-delta.slower { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); }
.race-delta.tie    { color: var(--text-faint); }

.race-badge {
  font-style: normal; font-size: .64rem; letter-spacing: .06em;
  color: var(--text-faint);
}
.race-lock { display: inline-flex; color: var(--accent); flex: none; }
.race-lock svg { width: 11px; height: 11px; }
.race-flagmark { font-style: normal; font-size: .7rem; color: var(--warn); cursor: help; }

/* ---- per-state treatment ---- */
.race-row[data-state="inspecting"] .race-badge { color: var(--warn); }
.race-row[data-state="solving"] .race-badge { color: var(--accent); }
.race-row[data-state="solving"] { border-color: color-mix(in srgb, var(--accent) 22%, transparent); }
.race-row[data-state="solving"] .race-av { animation: race-pulse 1.6s ease-in-out infinite; }

/* Somebody is done and you are not. Deliberately the loudest thing on the
   panel while carrying not one digit about how fast they were Ã¢â‚¬â€ that is
   the entire feeling the mode is built on. */
.race-row[data-state="locked"] {
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
}
.race-row[data-state="locked"] .race-badge { color: var(--accent); font-weight: 650; }
.race-row[data-state="locked"] .race-av { box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent); }

@keyframes race-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .55; }
}

/* The reveal. Rows cascade in rank order rather than all repainting at
   once, which is what makes finishing read as an event rather than a
   table refresh. The per-row delay is set in race.js. */
.race-row[data-state="revealed"], .race-row[data-state="dnf"] {
  animation: race-reveal .42s cubic-bezier(.22,1,.36,1) backwards;
}
@keyframes race-reveal {
  from { opacity: 0; transform: translateX(8px); }
  to   { opacity: 1; transform: none; }
}

[data-motion="off"] .race-row,
[data-motion="off"] .race-av { animation: none !important; }
[data-motion="off"] .race-meter > i { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .race-row, .race-av { animation: none !important; }
  .race-meter > i { transition: none; }
}

/* ---- foot ---- */
.race-more {
  background: none; border: none; cursor: pointer;
  font: inherit; font-size: .68rem; letter-spacing: .04em;
  color: var(--text-faint); padding: 2px; text-align: left;
}
.race-more:hover { color: var(--accent); }

.race-foot { display: flex; flex-direction: column; gap: 6px; }
.race-note { font-size: .68rem; line-height: 1.45; color: var(--text-faint); }
.race-note.strong { color: var(--text-dim); }
.race-wait { font-size: .68rem; color: var(--text-faint); font-style: italic; }
.race-next {
  font-size: .7rem; color: var(--text-dim);
  display: flex; align-items: baseline; gap: 4px;
}
.race-next b { color: var(--accent); font-variant-numeric: tabular-nums; }
.race-local {
  font-size: .62rem; line-height: 1.4; color: var(--warn);
  border-left: 2px solid currentColor; padding-left: 6px; opacity: .85;
}

/* Ending the race and leaving the room, in the panel rather than three clicks
   away in the drawer. Short and quiet: they sit under a foot that is already
   telling you what the room is waiting for, and neither is the thing you came
   to this panel to read. */
.race-actions {
  display: flex; gap: 6px; margin-top: 8px;
  padding-top: 8px; border-top: 1px solid var(--border);
}
.race-actions .btn {
  flex: 1; min-width: 0; height: 28px; padding: 0 8px;
  font-size: .7rem; border-radius: 8px;
}

/* ---------------- the drawer ---------------- */
.race-hero {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}
.race-hero > div:nth-child(2) { flex: 1; min-width: 0; }
.race-hero-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none; margin-top: 5px;
  background: var(--text-faint);
}
.race-hero.on .race-hero-dot {
  background: var(--ok);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 22%, transparent);
}
.race-hero-title { font-size: .95rem; font-weight: 650; margin-bottom: 4px; }
.race-hero-sub { font-size: .78rem; line-height: 1.5; color: var(--text-dim); }
.race-hero-warn {
  font-size: .76rem; line-height: 1.5; color: var(--text-dim);
  border-left: 2px solid var(--warn);
  padding-left: 9px; margin-top: 9px;
}

.race-code-big {
  font-family: ui-monospace, monospace;
  font-size: 1.4rem; font-weight: 700; letter-spacing: .22em;
  color: var(--accent);
}
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-row > .btn { flex: 1; min-width: 130px; }

/* ---------------- below the tile breakpoint ----------------
   tiles.js switches dragging off under 860px, so there is nowhere
   sensible to park this and the rails are two short strips under the
   timer. It becomes a sheet across the foot of the screen instead,
   folded to its own header until it is tapped Ã¢â‚¬â€ and folded, that header
   still answers the only two questions a phone has room for: which room,
   and how much of it is already done. */
@media (max-width: 860px) {
  #panel-race {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
    border-radius: var(--radius) var(--radius) 0 0;
    border-bottom: none;
    max-height: 62vh; overflow-y: auto;
    box-shadow: 0 -14px 40px -20px rgba(0,0,0,.75);
    padding-bottom: max(10px, env(safe-area-inset-bottom));
  }
  #panel-race .tile-grip { display: none; }
  #panel-race .race-head { position: sticky; top: 0; z-index: 1; }
  #panel-race[data-collapsed="true"] { max-height: none; }
  #panel-race[data-collapsed="true"] .race-body { display: flex; }
  #panel-race[data-collapsed="true"] .race-rows,
  #panel-race[data-collapsed="true"] .race-more,
  #panel-race[data-collapsed="true"] .race-foot,
  #panel-race[data-collapsed="true"] .race-chat,
  #panel-race[data-collapsed="true"] .race-actions { display: none; }
}

/* =========================================================
   BLINDFOLDED WORKFLOW

   The breakdown sits under the scramble and is collapsed by default, so
   in its resting state all of this is one small button.
   ========================================================= */
/* Above the timer in the stack as well as on the page: where the window is
   too short for the nudge below to clear the digits entirely, the panel has
   to be the thing you can still read. */
#bld-zone {
  position: relative; z-index: 6;
  margin-top: 10px; display: flex; flex-direction: column; align-items: center; gap: 8px;
}

#bld-bar { display: flex; align-items: center; gap: 10px; }
#btn-bld-toggle {
  font-size: .72rem; letter-spacing: .04em; text-transform: lowercase;
  padding: 5px 13px; border-radius: 99px; border: 1px solid var(--border);
  color: var(--text-dim); background: color-mix(in srgb, var(--text) 4%, transparent);
}
#btn-bld-toggle:hover { color: var(--text); border-color: var(--accent); }

/* Out of the flow on purpose. In it, opening the breakdown made the scramble
   row taller, and a taller row pushed both sidebars down the screen Ã¢â‚¬â€ the
   panel you asked for should not move the rest of the page. It hangs under
   the bar instead, and bldFit() in main.js is what keeps it off the digits. */
#bld-panel {
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  margin-top: 8px;
  width: min(92vw, 640px);
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) calc(var(--panel-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--panel-blur)) saturate(1.4);
  -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(1.4);
  box-shadow: var(--shadow);
}

.bld-row { display: flex; align-items: baseline; gap: 12px; }
.bld-lbl {
  flex: 0 0 3.6em; font-size: .7rem; letter-spacing: .05em;
  color: var(--text-faint); text-transform: lowercase;
}
.bld-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.bld-none { font-size: .78rem; color: var(--text-faint); }

.bld-chip {
  display: inline-flex; gap: 1px; align-items: center;
  font-family: var(--font-mono); font-size: .84rem; font-weight: 600;
  padding: 3px 9px; border-radius: 7px; cursor: pointer;
  color: var(--text); border: 1px solid var(--border);
  background: color-mix(in srgb, var(--text) 5%, transparent);
  transition: border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.bld-chip:hover { border-color: var(--accent); transform: translateY(-1px); }
/* A lone trailing target is not a pair Ã¢â‚¬â€ say so with the shape, not a label. */
.bld-chip.odd { border-style: dashed; color: var(--warn); }

.bld-l { position: relative; }
/* A cycle break costs an extra target, so the letter that opens a new cycle
   carries a divider rather than being folded silently into the run. */
.bld-l.brk { margin-left: 6px; padding-left: 7px; border-left: 2px solid var(--accent-2); }
/* A second shot at the same piece: a flip or a twist put right in place. */
.bld-l.inplace::after {
  content: ''; position: absolute; top: -2px; right: -3px;
  width: 4px; height: 4px; border-radius: 50%; background: var(--warn);
}

/* The setup card. It stands in for the whole breakdown, so it is allowed to
   be the one thing in this panel that talks. */
#bld-setup { display: flex; flex-direction: column; gap: 8px; }
.bld-setup-title { font-size: .82rem; font-weight: 600; color: var(--text); }
.bld-setup-note { font-size: .74rem; line-height: 1.55; color: var(--text-faint); }
.bld-setup-note b { color: var(--text-dim); font-family: var(--font-mono); font-weight: 600; }
.bld-setup-actions { display: flex; gap: 8px; flex-wrap: wrap; }

.bld-foot { display: flex; align-items: center; gap: 10px; }
.bld-badge {
  font-size: .64rem; letter-spacing: .09em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 99px; font-weight: 700;
}
.bld-badge.parity { color: var(--bg); background: var(--warn); }
.bld-note { flex: 1; font-size: .7rem; color: var(--text-faint); line-height: 1.5; }
/* In the bar next to the toggle, so the letters and the settings that decide
   them are never more than one click apart Ã¢â‚¬â€ including while the panel is
   still collapsed. */
#btn-bld-settings { padding: 5px 8px; border-radius: 99px; border: 1px solid var(--border); color: var(--text-dim); }
#btn-bld-settings:hover { color: var(--text); border-color: var(--accent); }
#btn-bld-settings svg { width: 15px; height: 15px; display: block; }

/* An open breakdown wants the same room the digits are already in. main.js
   measures the real overlap and puts the difference here; it is 0 the rest of
   the time, so nothing moves unless the two actually collide.

   Written twice because the timer is positioned two different ways. Above the
   mobile breakpoint it is `fixed` and centred, so the nudge is added to the
   50%. Below it the timer sits in the flow as `relative`, where a bare `top`
   is already an offset from where it landed Ã¢â‚¬â€ and where `50%` would resolve
   against the containing block and throw it off the page. */
#timer-core { top: var(--bld-shift, 0px); }
@media (min-width: 861px) {
  #timer-core { top: calc(50% + var(--bld-shift, 0px)); }
}

/* ---------------- memo / execution phase ---------------- */
#phase-readout {
  display: flex; justify-content: center; gap: 10px; align-items: baseline;
  margin-top: 4px; font-size: .74rem; letter-spacing: .06em;
  font-family: var(--font-mono); color: var(--text-faint);
}
#phase-name { text-transform: uppercase; font-weight: 700; }
.phase-memo ~ #phase-readout #phase-name { color: var(--warn); }
.phase-exec ~ #phase-readout #phase-name { color: var(--ok); }
/* The digits carry the phase, because they are the one thing you can still
   see mid-solve with focus mode on. */
#timer-display.phase-memo #time-main { color: var(--warn); }
#timer-display.phase-exec #time-main { color: var(--ok); }

/* ---------------- multiphase (cstimer-style N-way split) ---------------- */
/* Live readout while running: a stacked cumulative/delta list at the clock's
   corner, exactly where csTimer puts it. Purely confirmation that a press
   landed Ã¢â‚¬â€ the digits stay the thing you are actually reading. */
#phase-live {
  position: absolute; top: 0; left: 100%; margin-left: 24px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
  pointer-events: none; white-space: nowrap;
}
#phase-live[hidden] { display: none; }
.phase-live-row {
  display: flex; align-items: baseline; gap: 2px;
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 1.15rem; font-weight: 600; color: var(--text-dim);
}
.phase-live-row .sign { opacity: .55; font-size: .78em; }
@media (max-width: 1100px) {
  #phase-live {
    position: static; margin: 6px 0 0;
    flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 12px;
  }
}

/* Post-solve breakdown. Same collapsed-by-default shape as #bld-zone, and
   deliberately not the same panel Ã¢â‚¬â€ a blind solve is never a multiphase one
   (syncPhaseZoneVisibility keeps the two mutually exclusive), so there is
   never a reason to show both at once. */
#phase-zone {
  position: relative; z-index: 6;
  margin-top: 10px; display: flex; flex-direction: column; align-items: center; gap: 8px;
}
#phase-bar { display: flex; align-items: center; gap: 10px; }
#btn-phase-toggle {
  font-size: .72rem; letter-spacing: .04em; text-transform: lowercase;
  padding: 5px 13px; border-radius: 99px; border: 1px solid var(--border);
  color: var(--text-dim); background: color-mix(in srgb, var(--text) 4%, transparent);
}
#btn-phase-toggle:hover { color: var(--text); border-color: var(--accent); }
#phase-panel {
  width: min(100%, 480px);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 12px 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface) calc(var(--panel-alpha) * 100%), transparent);
  backdrop-filter: blur(var(--panel-blur)) saturate(1.4);
  -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(1.4);
  box-shadow: var(--shadow);
}
.phase-row { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.phase-chip {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 6px 12px; border-radius: 7px;
  background: color-mix(in srgb, var(--text) 5%, transparent);
  border: 1px solid var(--border);
}
.phase-chip b {
  font-size: .64rem; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint);
}
.phase-chip i {
  font-style: normal; font-family: var(--font-mono); font-size: .96rem;
  font-weight: 700; color: var(--text);
}
.phase-total { font-size: .74rem; color: var(--text-faint); }

/* ---------------- letter-pair recall popover ---------------- */
.pop-img {
  display: block; max-width: 190px; max-height: 150px;
  margin: 6px auto; border-radius: var(--radius-sm); object-fit: contain;
}

/* ---------------- scheme editor ---------------- */
.scheme-block { margin-top: 12px; }
.scheme-block h4 {
  font-size: .7rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-faint); margin-bottom: 8px;
}
.scheme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(66px, 1fr)); gap: 6px; }
.scheme-cell { display: flex; align-items: center; gap: 6px; }
.scheme-cell > span {
  flex: 0 0 2.4em; font-family: var(--font-mono);
  font-size: .68rem; color: var(--text-faint);
}
.scheme-inp { width: 2.4em; text-align: center; text-transform: uppercase; padding: 4px; }
.scheme-cell.dupe .scheme-inp { border-color: var(--danger); color: var(--danger); }

/* ---------------- letter pairs ---------------- */
.lp-list { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; max-height: 46vh; overflow: auto; }
.lp-row {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 7px 10px; border-radius: var(--radius-sm); cursor: pointer;
  background: transparent; border: 1px solid transparent; color: var(--text);
}
.lp-row:hover { background: color-mix(in srgb, var(--text) 5%, transparent); border-color: var(--border); }
.lp-row > b { font-family: var(--font-mono); flex: 0 0 2.6em; }
.lp-word { flex: 1; }
.lp-alg { font-family: var(--font-mono); font-size: .72rem; color: var(--text-faint); }
.lp-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-2); }
.lp-editor:empty { display: none; }
.lp-imgrow { display: flex; gap: 8px; }
.lp-actions { display: flex; gap: 8px; }
.lp-img img { max-width: 100%; max-height: 200px; border-radius: var(--radius-sm); margin-top: 8px; }

/* ---------------- DNF post-mortem ---------------- */
.pm-net { width: 100%; display: block; cursor: pointer; border-radius: var(--radius-sm); }
.pm-picked { font-size: .74rem; color: var(--text-dim); margin: 8px 0; text-align: center; }
.pm-out { display: flex; flex-direction: column; gap: 8px; }
.pm-out:empty { display: none; }
.pm-line {
  font-size: .78rem; line-height: 1.6; padding: 10px 12px;
  border-radius: var(--radius-sm); border-left: 2px solid var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}
.pm-memo { display: flex; gap: 12px; align-items: baseline; padding: 4px 0; }
.pm-memo-k { flex: 0 0 4em; font-size: .7rem; color: var(--text-faint); }
.pm-memo-v { font-family: var(--font-mono); font-size: .82rem; letter-spacing: .08em; }

/* ---------------- memo:exec dashboard ---------------- */
.me-bar {
  display: flex; height: 10px; border-radius: 99px; overflow: hidden;
  margin: 10px 0 6px; background: color-mix(in srgb, var(--text) 6%, transparent);
}
.me-memo { background: var(--warn); }
.me-exec { background: var(--ok); }
.me-trend {
  display: flex; align-items: flex-end; gap: 2px; height: 46px;
  padding: 4px 0; border-bottom: 1px solid var(--border);
}
.me-trend i { flex: 1; min-width: 2px; background: var(--warn); opacity: .75; border-radius: 1px; }

/* ===========================================================
   Scramble of the Day Ã¢â‚¬â€ the window's bar, and both boards

   The boards (`.db-*`) are shared: the same markup renders inside the
   window's card and inside the settings drawer, so a row looks the same
   wherever you found it. That is why nothing here is scoped under
   `body.sotd` except the bar and the card that holds them.
   =========================================================== */

/* No `display` here on purpose. base.css owns whether this bar is on screen
   (`#sotd-bar { display: none }` / `body.sotd #sotd-bar { display: flex }`),
   and this file loads after it Ã¢â‚¬â€ so a `display: flex` here would have equal
   specificity, win on order, and pin the bar over the app permanently
   whether or not the window was open. Appearance here, visibility there. */
#sotd-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 90;
  align-items: center; gap: 16px;
  padding: 10px 18px;
  /* A date is not a phrase and must never break across two lines. The bar
     drops its optional parts as the width goes (see the queries at the foot
     of this file) rather than letting anything in it wrap. */
  white-space: nowrap;
  background: color-mix(in srgb, var(--surface) 82%, transparent);
  backdrop-filter: blur(24px) saturate(1.5); -webkit-backdrop-filter: blur(24px) saturate(1.5);
  border-bottom: 1px solid var(--border);
  animation: slideDown 320ms var(--ease);
}
@keyframes slideDown { from { transform: translateY(-100%) } to { transform: none } }

.sotd-id { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.sotd-kicker {
  font-size: .6rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
  color: var(--accent);
}
.sotd-day { font-size: .82rem; font-weight: 600; letter-spacing: -.01em; }
.sotd-state {
  font-size: .7rem; color: var(--text-faint);
  padding-left: 10px; margin-left: 2px; border-left: 1px solid var(--border);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Green while today's scramble is genuinely on the timer Ã¢â‚¬â€ the one state in
   which pressing space means something, and worth being able to tell apart at
   a glance from the three in which it does not. */
.sotd-state[data-state="ready"] { color: var(--ok); }
.sotd-state[data-state="done"]  { color: var(--accent); }
.sotd-clock { font-size: .74rem; color: var(--text-faint); margin-left: auto; }
.sotd-clock b { color: var(--text); font-weight: 600; }
#sotd-exit { display: flex; align-items: center; gap: 6px; font-size: .74rem; }
#sotd-exit svg { width: 13px; height: 13px; }

/* A column down the left, not a sheet across the bottom.

   The bottom sheet it replaced had to cover the middle of the screen to be
   readable, which put it straight over the timer Ã¢â‚¬â€ and the only way to keep
   both was to dim the time you had just done to the point of being unreadable.
   Standing the board up in its own column means nothing overlaps anything: the
   list is a list, the timer is still the biggest thing on screen, and the two
   can be read in the same glance instead of one at the other's expense.

   Same split as the bar above: base.css decides when this is displayed. */
#sotd-board {
  position: fixed; left: 18px; bottom: 18px; z-index: 91;
  /* Where the column starts. The default clears a bar plus a two-line
     scramble, which is the common case and keeps this correct before any
     script has run; js/dailyui.js then measures the scramble it actually got
     and sets the variable exactly. Kept as a variable rather than an inline
     `top` so the two cannot disagree about which one is in charge. */
  top: var(--sotd-board-top, 200px);
  width: var(--sotd-board-w);
  overflow-y: auto; overscroll-behavior: contain;
  animation: sotdBoardIn 420ms var(--ease);
}
@keyframes sotdBoardIn { from { transform: translateX(-18px); opacity: 0 } to { transform: none; opacity: 1 } }
@keyframes sotdBoardUp { from { transform: translateY(18px); opacity: 0 } to { transform: none; opacity: 1 } }

.sotd-board-card {
  display: flex; flex-direction: column; gap: 10px;
  padding: 18px 18px 22px;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(30px) saturate(1.5); -webkit-backdrop-filter: blur(30px) saturate(1.5);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 24px 60px -30px rgba(0,0,0,.7);
}
.sotd-board-card h3 {
  font-size: .64rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-faint);
  display: flex; align-items: baseline; gap: 10px;
}
.sotd-h3-second { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); }
.sotd-h3-note { letter-spacing: 0; text-transform: none; font-weight: 400; font-size: .66rem; opacity: .75; }

/* The past-day picker. One control, drawn in both places the board is Ã¢â‚¬â€ the
   window's card and the settings drawer Ã¢â‚¬â€ so it is styled here once rather
   than per caller. In the window it sits on the heading's own row; in the
   drawer the heading is a group title above it, hence the margin. */
.sotd-board-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.sotd-board-head h3 { margin: 0; }
.sotd-board-head .daily-daynav { margin: 0; flex: none; }

.daily-daynav { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.daily-daynav .btn {
  padding: 2px 9px; font-size: .95rem; line-height: 1.15;
  /* The arrows are the one control here that must not move under the cursor:
     they sit inches from a running timer, and the lift every other .btn gets
     on hover reads as the board itself twitching. */
  transform: none;
}
.daily-daynav .btn:hover { transform: none; }
.daily-daynav .btn[disabled] { opacity: .35; pointer-events: none; }
.daily-daynav-day {
  min-width: 5.6em; text-align: center;
  font-size: .72rem; font-weight: 600; letter-spacing: .03em;
  font-variant-numeric: tabular-nums; color: var(--text-dim);
}

/* ---------------- the boards themselves ---------------- */

.db-board { display: flex; flex-direction: column; gap: 2px; }
.db-stack { display: flex; flex-direction: column; gap: 12px; }
.db-yours-label {
  font-size: .6rem; font-weight: 700; letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-faint); margin-bottom: 4px;
}

.db-row {
  display: grid;
  grid-template-columns: 1.6em 24px minmax(0, 1fr) auto auto;
  align-items: center; gap: 10px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  /* An explicit transparent border, not none: a row that gains a border only
     when it is yours would jump one pixel wider the moment it became yours. */
  border: 1px solid transparent;
}
.db-row:nth-child(odd) { background: color-mix(in srgb, var(--text) 3%, transparent); }
.db-row[data-me="true"] {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.db-rank {
  font-size: .72rem; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--text-faint); text-align: right;
}
/* The top three read as a podium without needing a medal glyph next to a
   name, which would have cost the name a column on a phone. */
.db-row[data-rank="1"] .db-rank { color: #f2c14e; }
.db-row[data-rank="2"] .db-rank { color: #c9ccd1; }
.db-row[data-rank="3"] .db-rank { color: #cd8b52; }

.db-face {
  width: 24px; height: 24px; border-radius: 50%; object-fit: cover; flex: none;
  background: color-mix(in srgb, var(--text) 10%, transparent);
}
.db-face-letter {
  display: grid; place-items: center;
  font-size: .68rem; font-weight: 700; color: var(--text-faint);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.db-who { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.db-name {
  font-size: .8rem; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* One line, and only ever one: the cap is 80 characters but the column is
   narrower than that in most themes, so it ellipsises and carries the whole
   thing in its title. */
.db-note {
  font-size: .68rem; color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- the note composer, under today's board ---- */
.db-note-box { margin-top: 8px; }
.db-note-row { display: flex; gap: 6px; align-items: center; }
.db-note-input {
  flex: 1; min-width: 0;
  font: inherit; font-size: .76rem;
  padding: 6px 9px;
  color: var(--text);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.db-note-input:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
.db-note-emoji {
  flex: none; width: 28px; height: 28px;
  display: grid; place-items: center;
  font-size: .82rem; line-height: 1;
  background: none; color: var(--text-dim);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  cursor: pointer;
}
.db-note-emoji:hover { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.db-emoji {
  display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;
  margin-top: 6px; padding: 5px;
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.db-emoji-btn {
  aspect-ratio: 1; display: grid; place-items: center;
  font-size: .82rem; line-height: 1;
  background: none; border: 0; border-radius: 4px; cursor: pointer;
}
.db-emoji-btn:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.db-time { font-size: .84rem; font-weight: 650; font-variant-numeric: tabular-nums; }
.db-unit { font-size: .66rem; color: var(--text-faint); }
.db-flag { font-size: .7rem; color: var(--warn); cursor: help; }

/* The sign-in call to action, inside the board card where the reason for it
   is. The button is full width because it is the only thing to do here. */
.sotd-signin {
  display: flex; flex-direction: column; gap: 10px;
  margin-top: 12px; padding-top: 14px;
  border-top: 1px solid var(--border);
}
.sotd-signin-text { font-size: .78rem; line-height: 1.5; color: var(--text-faint); }

.db-empty, .db-locked-text {
  font-size: .78rem; line-height: 1.55; color: var(--text-faint);
}
.db-locked {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 16px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 4%, transparent);
  border: 1px dashed var(--border);
}
.db-locked-icon { font-size: 1rem; line-height: 1.3; flex: none; opacity: .7; }

/* Below the timer's own breakpoint there is no room for a column beside it,
   so the board lies back down along the bottom Ã¢â‚¬â€ and is kept short enough
   that the digits above it stay on screen rather than being covered. */
/* The state line is the first thing to go: it is the one part of the bar
   whose information is also on screen somewhere else Ã¢â‚¬â€ as the hold message
   where the scramble goes, or as the board being locked. */
@media (max-width: 760px) { .sotd-state { display: none; } }

@media (max-width: 860px) {
  #sotd-board {
    left: 12px; right: 12px; top: auto; bottom: 0;
    width: auto; max-height: 46vh;
  }
  /* Lifted clear of the sheet. On a wide screen the preview's usual corner is
     nowhere near the board; here the board IS the bottom of the screen, and a
     cube half-buried under a leaderboard is worse than no cube. */
  body.sotd #panel-cube:not([data-placed="true"]) { bottom: calc(46vh + 14px); }
  .sotd-board-card { border-bottom: 0; border-radius: 16px 16px 0 0; }
  /* Its own name rather than redefining sotdBoardIn inside a media block:
     same-named keyframes across conditions is the kind of thing that resolves
     differently per engine, and there is nothing to gain by finding out. */
  #sotd-board { animation-name: sotdBoardUp; }
}

@media (max-width: 520px) {
  #sotd-bar { gap: 10px; padding: 8px 12px; }
  .sotd-kicker { display: none; }
  /* The qualifier beside the heading is a sentence, and a sentence does not
     share a line with a heading at this width Ã¢â‚¬â€ it wraps into a shape that
     reads as neither. */
  .sotd-board-card h3 { flex-direction: column; align-items: flex-start; gap: 3px; }
  .db-row { grid-template-columns: 1.4em 22px minmax(0, 1fr) auto auto; gap: 8px; padding: 6px 8px; }
  .db-face { width: 22px; height: 22px; }
}

/* ---- gear log ----
   Nearly everything here is already a .group of .row, so this only adds the
   bits that have no equivalent: the card stack, and the log tucked under a
   cube so it reads as belonging to it rather than as a sibling section. */
.chart-filter { display: flex; align-items: center; gap: 10px; }
.chart-filter select { max-width: 220px; }
.gear-list { display: flex; flex-direction: column; gap: 10px; }
.gear-card { gap: 6px; }
.gear-log {
  display: flex; flex-direction: column; gap: 4px;
  margin-left: 12px; padding-left: 12px;
  border-left: 2px solid var(--border);
}
.gear-log-add { gap: 8px; align-items: center; }
.gear-log-add .inp { flex: 1; }
.gear-log-row .sub { font-variant-numeric: tabular-nums; }
.picker { display: flex; flex-direction: column; gap: 6px; min-width: 220px; }

/* ===========================================================
   CUSTOM RELAY

   The rail stands in for the single scramble line: one chip per puzzle,
   showing which is live, which are done and what each of those took. Only
   ever one puzzle is shown in full below it, because five scrambles and five
   previews at once fit on no screen -- and five 3D previews would exhaust the
   tab's WebGL contexts and paint black squares besides.
   =========================================================== */
#relay-rail {
  display: flex; align-items: center; gap: 8px;
  width: min(1320px, 95%); max-width: 100%;
  padding: 2px 10px;
  box-sizing: border-box;
}
#relay-rail[hidden] { display: none; }
/* The rail names every puzzle in the zone, so the generic heading above it is
   a second label for the same thing and a line of height a megaminx leg needs. */
body.relay .scramble-label { display: none; }

/* The chips scroll sideways rather than wrapping: a relay of ten would
   otherwise push the scramble and the preview down the page, and the rail is
   meant to cost one line however long the list is. */
.rr-track {
  display: flex; align-items: center; gap: 6px;
  flex: 1 1 auto; min-width: 0;
  overflow-x: auto; scrollbar-width: none;
  padding: 3px 2px;
}
.rr-track::-webkit-scrollbar { display: none; }

.rr-chip {
  flex: 0 0 auto;
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 4px 10px; border-radius: 999px; cursor: pointer;
  font-family: var(--font-ui); font-size: .7rem; font-weight: 700;
  letter-spacing: .06em;
  color: var(--text-faint);
  background: color-mix(in srgb, var(--text) 5%, transparent);
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.rr-chip:hover { color: var(--text); }
.rr-chip i {
  font-style: normal; font-family: var(--font-mono);
  font-size: .68rem; font-variant-numeric: tabular-nums; opacity: .9;
}
/* Done: it has a time, and the time is the point of the chip from here on. */
.rr-chip.done { color: var(--text-dim); }
.rr-chip.done i { color: var(--accent-2); }
.rr-chip.active {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: color-mix(in srgb, var(--accent) 48%, transparent);
}

.rr-count {
  flex: 0 0 auto;
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  color: var(--text-faint);
}
.rr-all {
  flex: 0 0 auto;
  padding: 3px 6px; border-radius: 6px;
  font-family: var(--font-ui); font-size: .66rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--text-faint);
  transition: color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.rr-all:hover { color: var(--text); background: color-mix(in srgb, var(--text) 8%, transparent); }
.rr-all.on { color: var(--accent-2); }

/* Every scramble at once: a list to read or print, never notation to solve at
   a glance, so it is set small, flush left and without the word spacing that
   holds moves apart on the single-puzzle line. */
.scramble.relay-all {
  font-size: calc(clamp(.72rem, 1.1vw, .86rem) * var(--scramble-scale));
  text-align: left; line-height: 1.75; word-spacing: .2em;
  /* Four big-cube scrambles are taller than a phone can spare above the
     clock, so the list scrolls inside its own box instead of pushing the
     timer into it. */
  max-height: min(34vh, 320px); overflow-y: auto; overscroll-behavior: contain;
}

/* Which puzzle the clock is counting, under the digits. */
#relay-now {
  margin-top: 6px;
  font-family: var(--font-ui); font-size: .66rem; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--accent);
}
#relay-now[hidden] { display: none; }

/* A finished relay solve, expanded out of the times list: what each puzzle
   took and the scramble it was done on. */
.relay-legs { display: flex; flex-direction: column; gap: 7px; max-width: 320px; }
.relay-leg { display: flex; flex-direction: column; gap: 2px; }
.rl-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font-size: .7rem; font-weight: 700; letter-spacing: .06em;
  color: var(--text-dim);
}
.rl-head i {
  font-style: normal; font-family: var(--font-mono);
  font-variant-numeric: tabular-nums; color: var(--accent-2);
}
.rl-scramble {
  font-family: var(--font-mono); font-size: .64rem; line-height: 1.5;
  color: var(--text-faint); word-break: break-word;
}

@media (max-width: 760px) {
  #relay-rail { width: 100%; padding: 2px 4px; gap: 6px; }
  .rr-chip { font-size: .66rem; padding: 3px 8px; }
}

/* ---------------- relay builder (drawer) ---------------- */
.relay-list { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }
.relay-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.relay-row .rr-n {
  width: 18px; flex: 0 0 auto; text-align: center;
  font-family: var(--font-mono); font-size: .72rem; color: var(--text-faint);
}
.relay-row .rr-name { flex: 1 1 auto; min-width: 0; font-size: .82rem; font-weight: 600; }
.relay-row .ghost-btn[disabled] { opacity: .3; pointer-events: none; }

.relay-add { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.relay-add .inp { flex: 1 1 140px; min-width: 0; }
.relay-step {
  display: inline-flex; align-items: center; gap: 2px;
  border-radius: 999px; padding: 2px;
  background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.relay-step .rr-count-val {
  min-width: 20px; text-align: center;
  font-family: var(--font-mono); font-size: .78rem; font-weight: 700;
}

.relay-foot { margin-top: 14px; }
.relay-preview {
  font-family: var(--font-mono); font-size: .8rem; font-weight: 600;
  color: var(--accent-2); margin-bottom: 8px; text-align: center;
}
.relay-actions { display: flex; flex-direction: column; gap: 6px; }

/* Share of the total, one segment per puzzle, in solving order. A bar rather
   than a chart for the same reason the memo/exec one is: it is a single
   division, and the only comparison that matters is inside one relay. */
.relay-bar {
  display: flex; height: 9px; border-radius: 999px; overflow: hidden;
  margin: 10px 0 4px; background: var(--surface-2);
}
.relay-bar-seg { display: block; min-width: 2px; }
.relay-bar-seg:nth-child(odd)  { background: var(--accent); }
.relay-bar-seg:nth-child(even) { background: var(--accent-2); }
