/* Klocked — a full-bleed typographic clock.
   Design: Figma "Website Design System" › Desktop - 1 (node 6006:23649)

   Source geometry: a 1440x960 frame divided into four 360px columns, each
   holding one white digit set in Inter Semi Bold at 1000px, cap-height
   trimmed and pinned to the column's bottom (cols 1, 3) or top (cols 2, 4).

   Every length below is derived from one number — the stage width — so the
   whole composition is a proportional scale of that frame at any size. */

/* Transitions.dev — Card resize, verbatim.
   Usage: put .t-resize on an element and change its width/height (directly,
   or via a state class). Used below by the credit chip only — see the note
   on .dock__surface below for how the whole dock now takes it instead. */
:root {
  --resize-dur: 300ms;
  --resize-ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.t-resize {
  transition:
    width  var(--resize-dur) var(--resize-ease),
    height var(--resize-dur) var(--resize-ease);
  will-change: width, height;
}

/* Inter Semi Bold, self-hosted (Google Fonts' standard `latin` cut, v20).
   Serving it ourselves pins the version — a font from someone else's server
   can change metrics underneath a layout measured this tightly — and lets
   the page render with no network at all.

   Not the 3KB digits-only cut, tempting as it was. Google's `text=` subsetter
   returns a file whose cap-height metric no longer matches Inter's: it moved
   the trimmed box from 727.5 to 704.6 per 1000px, which is the exact number
   `text-box-edge: cap` and the --trim fallback below are built on, so every
   digit sat 127px off its edge. 24KB with correct metrics beats 3KB without. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: block;   /* no flash of a fallback at a different width */
  src: url('fonts/inter-600-latin.woff2') format('woff2');
}

:root {
  /* One ground for all four columns. The design alternates #000000 and
     #101010 between them; holding a single tone instead leaves the digits
     clipping at the column edges as the only thing marking them. */
  --bg: #000000;
  --ink: #ffffff;

  /* Glass for the theme switch. What it has to refract is the digits, and
     those are pure black and pure white — so the fill is a real tint rather
     than a 7% veil. A veil would let a passing glyph decide the panel's
     brightness and the icons would disappear into it half the time. At 0.7
     over blur and saturation the shapes still read through, but the panel
     keeps its own ground and the icons stay legible over either extreme. */
  --glass-bg: rgba(20, 20, 20, 0.68);
  --glass-ink: rgba(255, 255, 255, 0.62);
  --glass-ink-on: #ffffff;

  /* Same material, nearly opaque. 68% is enough behind 18px icon strokes;
     small text needs more, because the panel's brightness swings wherever a
     giant glyph passes behind it and thin letterforms lose the contest. */
  --glass-bg-strong: rgba(18, 18, 18, 0.93);

  /* The specular rim. A 1px ring lit from the top-left and fading through
     nothing to a fainter bounce at the bottom-right — the edge highlight
     that makes the panel read as a solid piece of glass with thickness. */
  --glass-rim: linear-gradient(145deg,
    rgba(255, 255, 255, 0.75),
    rgba(255, 255, 255, 0.10) 38%,
    rgba(255, 255, 255, 0) 55%,
    rgba(255, 255, 255, 0.32));

  /* A flat stand-in for that same rim, for the one place a gradient cannot
     go: the divider between the theme segment and the full-screen icon is
     a single 1px line, not a ring, so it reads off this instead — tuned to
     the rim's own weight rather than the stronger --glass-ink, which read
     as a harsher, more deliberate line than the hairline the rim implies. */
  --glass-divider: rgba(255, 255, 255, 0.24);

  /* Light gathering on the upper surface, and the inner walls catching it. */
  --glass-sheen: linear-gradient(to bottom,
    rgba(255, 255, 255, 0.20), rgba(255, 255, 255, 0) 58%);
  --glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.24),
                 inset 0 -1px 0 rgba(255, 255, 255, 0.05);

  --glass-lift: 0 10px 28px rgba(0, 0, 0, 0.34), 0 2px 8px rgba(0, 0, 0, 0.22);

  /* The selected option is a solid disc of ink with its icon knocked out of
     it. Stated in terms of --ink and --bg so it inverts with the theme by
     itself: black disc / white icon in light, white disc / black icon in
     dark. --glass-ink-on stays the *hover* colour for the unselected ones,
     which must not follow the thumb or it would go invisible against it. */
  --glass-thumb: var(--ink);
  --glass-ink-active: var(--bg);
  --glass-thumb-rim: transparent;
  --glass-thumb-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);


  /* Inter (unitsPerEm 2048): ascender 1984, descender 494, capHeight 1490.
     With line-height 1 the cap band sits 0.13623em below the line-box top
     and the baseline 0.13623em above its bottom — the same number either
     way. That is the nudge that stands in for text-box-trim where it is
     not supported. */
  --trim: 0.13623em;

  /* How far a digit travels as it swaps, and the curve it rides.
     clock.js reads --travel and --blur so the tokens stay defined here. */
  --travel: 0.07em;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Slight overshoot, for the controls rather than the digits. */
  --spring: cubic-bezier(0.34, 1.4, 0.64, 1);

  /* Motion blur through the swap — Figma "Desktop - 2" (node 6013:31479) is
     this composition mid-change, blurred 20px at the frame's 1000px digit.
     Held as an em so it scales with the digits rather than staying 20px. */
  --blur: 0.02em;
}

/* Light is the design inverted: the same 6% separation between the two
   column tones, read the other way up. On a near-white ground a white
   translucent panel would vanish, so the glass tints dark and the active
   thumb goes solid white with a shadow to lift it off the track. */
:root[data-theme="light"] {
  --bg: #ffffff;
  --ink: #000000;

  --glass-bg: rgba(255, 255, 255, 0.68);
  --glass-ink: rgba(0, 0, 0, 0.55);
  --glass-ink-on: #000000;
  --glass-bg-strong: rgba(255, 255, 255, 0.93);

  /* Same rim, but a white-only ring would disappear against a white ground,
     so the lower half of it darkens instead of fading out. */
  --glass-rim: linear-gradient(145deg,
    rgba(255, 255, 255, 0.95),
    rgba(0, 0, 0, 0.05) 42%,
    rgba(0, 0, 0, 0.10) 60%,
    rgba(0, 0, 0, 0.16));

  /* Same reasoning as the rim above: a white divider would vanish on a
     white ground, so this flips dark for light mode too. */
  --glass-divider: rgba(0, 0, 0, 0.14);

  --glass-sheen: linear-gradient(to bottom,
    rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 60%);
  --glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.85),
                 inset 0 -1px 0 rgba(0, 0, 0, 0.04);

  --glass-lift: 0 10px 28px rgba(0, 0, 0, 0.13), 0 2px 8px rgba(0, 0, 0, 0.08);

  /* --glass-thumb and --glass-ink-active are not repeated here: they are
     written in terms of --ink and --bg, which this block already flips. */
  --glass-thumb-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  overflow: hidden;
}

/* Crossing between themes, including the automatic flip at 06:00 and 18:00.
   The cells are transparent now, so this one ground is the only thing to
   cross-fade. */
html, body { transition: background-color 420ms ease; }
.digit { transition: color 420ms ease; }

body {
  display: grid;
  place-items: center;
  font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  color: var(--ink);
}

/* ---------------------------------------------------------------- layout */

/* Landscape only, by decision — see the rotate hint further down. The grid
   is a fixed 4x1, so unlike an earlier version of this file there is no
   --cols/--rows to generalise over: that machinery existed to support a
   portrait fold and a seconds mode, and both are gone now. */
.clock {
  /* Overscan. The drift below moves the whole composition by a few pixels,
     and in an edge-to-edge layout the outermost digits are cut off by the
     viewport itself — so shifting left would pull ink away from the right
     edge and leave a bare strip. Building the stage slightly larger than the
     viewport and letting body's centring hang the excess off both sides
     means there is always ink beyond the edge to shift into. Must exceed the
     drift radii in clock.js. */
  --overscan-x: 16px;
  --overscan-y: 12px;
  --fill-w: calc(100vw + var(--overscan-x));
  --fill-h: calc(100dvh + var(--overscan-y));

  /* The sizing, as the design's own four ratios against the stage:

       0.6944444  digit bleeds past its column exactly as drawn (1000/360/4)
       1.0416667  cap fills the same 75.7% of cell height as drawn
       1.3744898  cap exactly fills the cell — past here it clips vertically
       0.8680556  1.25x the drawn bleed — past here neighbouring digits
                  collide too much to read as a time

     Taken against --fill-w rather than raw 100vw so the overscan widens the
     columns and the digit together: the bleed ratio stays exactly the drawn
     one instead of drifting by the overscan.

     max() of the first two is never sparser than the design on either axis.
     The two ceilings bind on opposite extremes and never together: the
     vertical one on wide, short viewports, the bleed one on narrow, tall
     ones. At the frame's own 3:2 none of them bind and the size is exact.

     dvh throughout, deliberately without a vh fallback — a custom property
     is a token stream, so a second declaration in vh would not survive as
     one anyway. It would only look like a safety net. */
  --fs-fill: min(
    max(calc(var(--fill-h) * 1.0416667), calc(var(--fill-w) * 0.6944444)),
    calc(var(--fill-h) * 1.3744898),
    calc(var(--fill-w) * 0.8680556)
  );

  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: 1fr;
  font-size: var(--fs);

  /* Burn-in drift. Left on an OLED for hours this page is close to the worst
     case: enormous, unmoving, maximum-contrast shapes. clock.js walks these
     two values around a small ellipse, one step a minute, and the whole
     composition creeps with them so no edge sits on the same pixels for
     long. The ground is a single colour, so the sliver uncovered at the
     trailing edge is the same colour as everything else — invisible. */
  translate: var(--drift-x, 0px) var(--drift-y, 0px);
  transition: translate 6s ease-in-out;
}

/* Still drift — it protects the panel — but arrive without the slow slide. */
@media (prefers-reduced-motion: reduce) {
  .clock { transition: none; }
}

/* data-fit="contain" — the design, scaled.
   The stage keeps the frame's 3:2 aspect and grows to whichever viewport
   axis runs out first, so column width, digit size and the amount each
   digit bleeds past its column all stay in exactly the proportions they
   were drawn in. One design cell is 360x960, so four of them side by side
   is 1440x960 — 3:2. Leftover viewport is the ground colour. */
.clock[data-fit="contain"] {
  --stage-w: min(100vw, 150dvh);
  width: var(--stage-w);
  aspect-ratio: 1.5;
  --fs: calc(var(--stage-w) * 0.6944444);
}

/* data-fit="fill" — edge to edge, no letterboxing. Slightly larger than the
   viewport, hung off all four sides by half the overscan each way, so the
   drift always has ink to move into.

   The offset is an explicit negative margin rather than body's centring:
   a single auto-sized grid track grows to fit an oversized item and then
   starts it at zero, which would put the whole overscan on one side and
   leave the other edge bare — the exact strip this is here to prevent. */
.clock[data-fit="fill"] {
  width: var(--fill-w);
  height: var(--fill-h);
  margin: calc(var(--overscan-y) / -2) calc(var(--overscan-x) / -2);
  --fs: var(--fs-fill);
}

/* Genuinely phone-shaped screens (and narrow desktop windows past the same
   threshold) get the rotate hint below instead of a folded layout — this
   composition is landscape, full stop, and a compromise fold is not the
   same thing as the design. The clock itself keeps running underneath
   (time, drift, theme all continue), just covered by the hint at a higher
   z-index, so nothing needs to reset when the viewport widens back out. */

.rotate-hint {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1000;   /* above .cursor (100) too — a mouse can resize a window
                       narrow enough to trigger this without ever leaving */

  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 24px;
  text-align: center;

  background: var(--bg);
  color: var(--ink);
  font-family: inherit;

  /* Same crossfade as the theme switch itself, so the one moment this can
     appear alongside a theme change (loading a narrow window at night, say)
     does not show two different colours mid-transition. */
  transition: background-color 420ms ease, color 420ms ease;
}

.rotate-hint svg {
  width: 40px;
  height: 40px;
  flex: none;
}

.rotate-hint__title {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
}

.rotate-hint__body {
  margin: 0;
  max-width: 22em;
  font-size: 14px;
  font-weight: 500;
  opacity: 0.6;
}

@media (max-aspect-ratio: 3 / 4) {
  .rotate-hint { display: flex; }
}

/* No fill of their own — the page's single ground shows through. The cells
   still clip, which is what carries the design now that the columns are no
   longer tinted apart.

   The clip is a half-pixel *outside* the box rather than `overflow: hidden`
   on it. Two neighbours' clips meet on a shared edge, and when that edge
   lands mid-pixel — which any odd viewport width or a fractional drift
   guarantees — each side antialiases to about half coverage. Half plus half
   does not make one, so a hairline of the ground colour shows straight
   through the ink. Overlapping the two clips by a pixel closes it. The cost
   is a digit bleeding half a pixel into its neighbour, which is invisible;
   the seam was not. */
.cell {
  position: relative;
  clip-path: inset(-0.5px);
}

/* ---------------------------------------------------------------- digits */

/* One layer per digit. Two coexist briefly during a swap, so they are taken
   out of flow and pinned to the cell edge the design anchors them to. */
.layer {
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
}

.cell[data-align="bottom"] .layer { bottom: 0; }
.cell[data-align="top"]    .layer { top: 0; }

.digit {
  position: relative;
  font-weight: 600;
  font-size: inherit;
  line-height: 1;
  letter-spacing: 0;
  white-space: nowrap;
  color: var(--ink);
  user-select: none;
}

/* Pull the line box off the edge so the *cap band* meets it instead. */
.cell[data-align="bottom"] .digit { top: var(--trim); }
.cell[data-align="top"]    .digit { top: calc(-1 * var(--trim)); }

/* Where the real thing exists, use it and drop the manual nudge. */
@supports (text-box-trim: trim-both) and (text-box-edge: cap alphabetic) {
  .cell[data-align="bottom"] .digit,
  .cell[data-align="top"] .digit {
    text-box-trim: trim-both;
    text-box-edge: cap alphabetic;
    top: 0;
  }
}

/* `1` renders narrower here than the same digit does in Figma's own Inter —
   422.9px against Figma's 481px at an identical 1000px font-size (the same
   font-vendor deviation noted in the README's Known deviation table). Every
   other digit bleeds past its column enough to touch its neighbour;
   at its natural width `1` falls short and leaves a visible gap where the
   others show ink meeting ink.

   Scaled back up to Figma's own ratio for this glyph specifically —
   481/422.9, about 1.137 — not to the average width of the other nine
   digits, which would overshoot by a lot (their average is ~633px) and
   fatten `1` into a shape that no longer reads as a `1`. This is a
   correction for a font substitution, not a redesign.

   transform, not letter-spacing: the goal is more ink reaching the column
   edge, and letter-spacing only adds blank space around a glyph — it does
   not widen the glyph itself. Centred by default, so the extra width lands
   evenly on both sides — every `1` has this regardless of which of the
   four cells it lands in. */
.digit--one {
  scale: 1.137 1;
}

/* A further rightward nudge, but only in the one cell where it cannot cost
   anything: data-lead has no real left neighbour, just the screen edge, so
   shifting its `1` right only adds bleed where the design wants it — into
   the digit on its right. Anywhere else a `1` has a real neighbour on
   *both* sides (proven wrong the first time this shipped: a leading `2`
   followed by `1` opened a gap on `1`'s left exactly as wide as the shift,
   because the same nudge that closes a gap on one side by definition opens
   one on the other unless there is no neighbour there to open a gap with).
   Scale alone, centred, is what closes the gap symmetrically everywhere
   else — this is additive polish for the one safe spot, not the fix
   itself. Cell 0 is unambiguously that spot now that the layout is fixed
   at 4x1 — an earlier version of this rule had to hedge around a portrait
   fold that also put a no-left-neighbour cell at position 2, but that
   fold is gone (see the rotate hint), so there is only the one true
   leading position to mark. */
.cell[data-lead] .digit--one {
  translate: 0.03em 0;
}

/* ----------------------------------------------------------- the dock */

/* One glass surface, two layers, one shape — not two elements taking turns
   in the same corner. .dock itself carries no fill and no clipping: it
   exists only to anchor the corner and to give the tooltips somewhere to
   sit that .dock__surface's own overflow: hidden (needed for the resize)
   cannot reach. Put the tooltips inside the surface and they would be
   exactly as clipped as everything else in it. */
.dock {
  position: fixed;
  inset-block-end: calc(env(safe-area-inset-bottom, 0px) + 20px);
  inset-inline-end: calc(env(safe-area-inset-right, 0px) + 20px);
  z-index: 10;
}

/* The morph. Width tweens between the credit chip's content (133px,
   default) and the controls' (163px, .is-awake) — both measured directly
   (131px and 161px rendered, plus 2px of safety margin against subpixel
   clipping): a CSS width transition needs a real number on both ends,
   `auto`/`fit-content` will not tween.

   Measuring this needed a trick the first time round did not use: the two
   layers are position: absolute, inset: 0, so their own scrollWidth just
   reports the surface's width back — out-of-flow descendants do not
   contribute to a container's intrinsic size, and inset: 0 stretches them
   to match it regardless of what their content actually needs. The honest
   number comes from freeing a layer of both constraints for a moment
   (position: static; width: max-content) and reading its real flow width,
   then restoring it. That first, wrong measurement undershot the credit
   chip by 15px — invisible in the numbers, but not on screen: the label
   was landing hard against the clipped edge with almost none of its
   intended 16px of right padding left to sit in.

   .dock__surface.glass, not .dock__surface: .glass declares its own
   `transition` later in this file, .t-resize declares another, and
   `transition` is a shorthand — two rules that both set it do not merge,
   the more specific one replaces the other's value wholesale. The compound
   selector outranks both single-class rules regardless of source order, so
   its own list (background/shadow from .glass, width/height from
   .t-resize, combined by hand) is the one that actually survives. */
.dock__surface.glass {
  --btn: 34px;
  --gap: 2px;
  --pad: 4px;

  display: flex;
  align-items: center;
  height: 42px;
  overflow: hidden;
  width: 133px;

  transition: background-color 420ms ease, box-shadow 420ms ease,
              width var(--resize-dur) var(--resize-ease),
              height var(--resize-dur) var(--resize-ease);
}

.dock__surface.is-awake {
  width: 163px;
}

/* Both layers stacked exactly on top of each other — position: absolute
   inset: 0 rather than flow, so the crossfade never reflows the other one
   mid-transition. Whichever is inactive is opacity: 0 *and*
   pointer-events: none, not just visually gone: without that a hidden
   layer's buttons would still be tabbable and clickable underneath the
   visible one. */
.dock__layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  opacity: 0;
  pointer-events: none;
  margin: 0;
  transition: opacity 200ms ease;
}

.dock__layer--credit {
  opacity: 1;
  pointer-events: auto;
  gap: 8px;
  padding: 6px 16px 6px 6px;
}

.dock__layer--controls {
  padding-inline-end: 4px;
}

.dock__surface.is-awake .dock__layer--credit {
  opacity: 0;
  pointer-events: none;
}

.dock__surface.is-awake .dock__layer--controls {
  opacity: 1;
  pointer-events: auto;
}

.credit__avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
  flex: none;
}

.credit__label {
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--glass-ink-on);
  white-space: nowrap;
}

/* A piece of glass. Four things together do the work: a tint that holds its
   own ground, a lensing backdrop, a specular rim, and light gathered on the
   upper surface. Shared by the theme switch and the full-screen button so
   they read as the same material. */
.glass {
  position: relative;
  border-radius: 999px;
  background: var(--glass-bg);
  box-shadow: var(--glass-lift), var(--glass-inset);
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.08);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.08);
  transition: background-color 420ms ease, box-shadow 420ms ease,
              transform 240ms var(--spring);
}

:root[data-theme="light"] .glass {
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.02);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.02);
}

/* Light pooling on the top face. */
.glass::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--glass-sheen);
  pointer-events: none;
}

/* The rim: a 1px gradient ring, drawn by masking out everything but the
   border box so the gradient survives as a hairline rather than a fill. */
.glass::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: var(--glass-rim);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* Without backdrop-filter the tint alone would read as a flat veil over
   whatever it sits on, so fall back to something opaque. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background: rgb(26, 26, 26); }
  :root[data-theme="light"] .glass { background: rgb(247, 247, 247); }
}

/* ---------------------------------------------------------- theme switch */

.dock__seg {
  position: relative;
  display: flex;
  gap: var(--gap);
  padding: var(--pad);
}

/* The lit pill behind the active option. Driven by --i (0, 1, 2) from JS,
   and it overshoots very slightly on arrival — the flex that makes the
   material feel liquid rather than a sliding rectangle. */
.dock__thumb {
  position: absolute;
  inset-block-start: var(--pad);
  inset-inline-start: var(--pad);
  width: var(--btn);
  height: var(--btn);
  border-radius: 999px;
  background: var(--glass-thumb);
  border: 1px solid var(--glass-thumb-rim);
  box-shadow: var(--glass-thumb-shadow);
  transform: translateX(calc(var(--i, 0) * (var(--btn) + var(--gap))));
  transition: transform 420ms var(--spring), background-color 420ms ease,
              border-color 420ms ease, box-shadow 420ms ease;
}

.dock__opt {
  position: relative;   /* above the thumb */
  width: var(--btn);
  height: var(--btn);
}

/* A hairline between the segment and the full-screen icon — the seam where
   two formerly separate pills became one continuous surface. --glass-ink
   (the icon colour) read as a harsher, more deliberate line than a seam
   should; --glass-divider matches the outer rim's own subtle weight
   instead, so the internal seam and the pill's own edge read as the same
   material rather than two different decisions. */
.dock__divider {
  width: 1px;
  height: 20px;
  margin-inline: 4px;
  background: var(--glass-divider);
  flex: none;
}

/* --------------------------------------------------- full-screen button */

.dock__icon {
  width: var(--btn);
  height: var(--btn);
  padding: 0;
  flex: none;
}

/* ------------------------------------------------------- shared controls */

.dock__opt,
.dock__icon {
  display: grid;
  place-items: center;
  margin: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--glass-ink);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 220ms ease, transform 240ms var(--spring);
}

.dock__opt svg,
.dock__icon svg { width: 18px; height: 18px; }

/* Only one of the two glyphs shows, chosen by the pressed state. These come
   after the sizing rule above and out-specify it, so the hidden one stays
   hidden — a bare `.iconbtn__exit` would lose to `.dock__icon svg`. */
.dock__icon .iconbtn__enter,
.dock__icon .iconbtn__exit { display: block; }
.dock__icon .iconbtn__exit { display: none; }
.dock__icon[aria-pressed="true"] .iconbtn__enter { display: none; }
.dock__icon[aria-pressed="true"] .iconbtn__exit { display: block; }

.dock__opt:hover,
.dock__icon:hover { color: var(--glass-ink-on); }
/* After :hover on purpose — the selected icon sits on the ink disc and must
   keep the knocked-out colour even while the pointer is over it. */
.dock__opt[aria-checked="true"] { color: var(--glass-ink-active); }

/* Glass gives a little under a press, then springs back. */
.dock__opt:active { transform: scale(0.9); }
.dock__icon:active { transform: scale(0.94); }

.dock__opt:focus-visible,
.dock__icon:focus-visible {
  outline: 2px solid var(--glass-ink-on);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  html, body, .cell, .digit, .glass, .dock__surface, .dock__layer,
  .dock__thumb, .dock__opt, .dock__icon { transition: none; }
  .dock__opt:active, .dock__icon:active { transform: none; }
}

/* -------------------------------------------------------------- tooltip */

/* Transitions.dev — tooltip open/close, adapted three ways from the
   original: opens *above* the trigger rather than below (these controls
   live in the bottom-right corner, so below would run off-screen); skinned
   onto the page's own --glass-bg-strong / --glass-ink-on rather than a
   fixed white pill, so it reads as this page's material; and positioned
   from .dock's own stable right edge rather than centred on its trigger —
   the trigger-sibling trick the original relies on no longer applies once
   the trigger sits inside .dock__surface's clipped, resizing box and the
   tooltip has to live outside it (see .dock above). --tt-end is measured
   per tooltip, not guessed, the same way .dock__surface's own widths are:
   .dock's right edge is fixed regardless of which width it is mid-tween
   between, so each tooltip can anchor to a fixed offset from it and still
   land above its own trigger, not just above the dock's midpoint. */
.t-tt {
  position: absolute;
  bottom: calc(100% + 10px);
  inset-inline-end: var(--tt-end);
  z-index: 20;
  transform: translate(50%, 0) scale(0.98);
  transform-origin: 50% 100%;

  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border-radius: 10px;
  background: var(--glass-bg-strong);
  color: var(--glass-ink-on);

  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.01em;
  white-space: nowrap;
  box-shadow: var(--glass-lift);

  opacity: 0;
  pointer-events: none;
  transition: opacity 50ms ease-out, transform 50ms ease-out;
}

#tt-fullscreen { --tt-end: 23px; }   /* centred over the icon, measured */
#tt-theme      { --tt-end: 106px; }  /* centred over the segment, measured */

/* The 80ms delay belongs only to the hover/focus rule, so leaving the
   trigger snaps it to 0 and the tooltip clears without lag.

   Keyed off :focus-visible, not :focus-within — a mouse click focuses the
   button too, and :focus-within cannot tell that apart from a real Tab
   press. That left the tooltip stuck open after every click until focus
   moved elsewhere. :focus-visible is the browser's own answer to exactly
   that question: it matches keyboard navigation and not a pointer click.

   Both keyed off :has() on .dock now, not the old wrap-is-hover-target
   trick — that needed the tooltip to be a DOM sibling of its trigger, which
   is exactly what moving it outside the clipped surface breaks. :has()
   reaches across the gap instead: DOM relationships are unaffected by
   overflow, only painting is, so this still works even though the trigger
   is visually clipped by an ancestor the tooltip itself sits outside of. */
.dock:has(.dock__seg:hover) #tt-theme,
.dock:has(.dock__seg :focus-visible) #tt-theme,
.dock:has(#fullscreen:hover) #tt-fullscreen,
.dock:has(#fullscreen:focus-visible) #tt-fullscreen {
  opacity: 1;
  transform: translate(50%, 0) scale(1);
  transition-duration: 150ms;
  transition-timing-function: ease-out;
  transition-delay: 80ms;
}

.t-tt kbd {
  display: inline-grid;
  place-items: center;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 5px;
  background: var(--glass-thumb);
  color: var(--glass-ink-active);
  font: inherit;
  font-size: 10px;
  font-weight: 600;
}

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

/* Touch has no hover to trigger this from, and the controls already wake
   on tap — a tooltip that can never be dismissed by moving away would just
   sit there. */
@media (hover: none) {
  .t-tt { display: none; }
}

/* --------------------------------------------------------------- cursor */

/* A dot, and nothing else. The page is pure black and white with giant
   glyphs of the opposite colour bleeding across it, so a dot of any single
   colour would be invisible over roughly half the screen. Difference
   blending sidesteps the problem entirely: the dot is always the exact
   inverse of whatever sits behind it.

   The blend has to live on this element rather than an inner one — an
   ancestor with a z-index forms a stacking context, and mix-blend-mode only
   reaches the backdrop of its own. Position therefore rides the `translate`
   property and scale rides `scale`, so the two animate independently and
   following the pointer stays instant while the hover swell can ease. */
.cursor { display: none; }

@media (hover: hover) and (pointer: fine) {
  html,
  .dock__opt,
  .dock__icon { cursor: none; }

  .cursor {
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    width: 12px;
    height: 12px;
    margin: -6px 0 0 -6px;      /* centre the dot on the point */
    border-radius: 50%;
    background: #ffffff;
    mix-blend-mode: difference;
    pointer-events: none;
    z-index: 100;
    translate: -100px -100px;   /* parked until the first pointer move */
    scale: 1;
    transition: scale 220ms var(--spring), opacity 200ms ease;
    will-change: translate;
  }

  /* Swells over anything clickable. */
  .cursor.is-over { scale: 2.6; }
  .cursor.is-away { opacity: 0; }
}

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

/* --------------------------------------------------------------- a11y */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
