/* ==========================================================================
   base.css — reset + the lab's shared "chrome"

   Everything here is the gallery itself (header, cards, specimen stages,
   margin notes). The components being studied live in their own files
   under components/.
   ========================================================================== */

/* --- Reset ----------------------------------------------------------------
   Browsers ship with inconsistent default margins and box models.
   These four rules give every page the same clean starting point. */

*,
*::before,
*::after {
  box-sizing: border-box; /* width includes padding+border — the sane model */
  -webkit-tap-highlight-color: transparent; /* mobile browsers flash a blue
     overlay on anything tappable; the lab's own :active states are the
     touch feedback here */
}

body,
h1, h2, h3, p, ul, figure {
  margin: 0; /* we add space back deliberately, from the spacing scale */
}

img {
  max-width: 100%;
  display: block;
}

/* --- Page ---------------------------------------------------------------- */

body {
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: 1.6; /* body text breathes; headings tighten this locally */
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* The loaded weights (400–600 body/mono, 400–800 display) are the only
     ones allowed to render: a missing file must fail visibly, not as a
     browser-faked bold or slanted italic. */
  font-synthesis: none;
}

.container {
  max-width: 960px; /* comfortable reading width — full-bleed text is tiring */
  margin: 0 auto;
  padding: 0 var(--space-5);
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

::selection {
  background: var(--accent-soft);
}

/* --- Header (shared across all lab pages) --------------------------------- */

.lab-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5) 0;
  border-bottom: 1px solid var(--line);
}

.wordmark {
  /* Flex keeps the three pieces (name, tick, "lab") on one shared
     baseline even while the last two animate their widths below. */
  display: inline-flex;
  position: relative; /* anchors the stars that fly out of the logo on hover */
  align-items: baseline;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
  letter-spacing: 0.02em;
}

/* The wordmark holds three links ("mihneaptu" → GitHub profile, "lab" →
   home, the hidden cat → the repo) that should read as one quiet mark,
   so the links inherit the ink color and only reveal themselves with an
   underline on hover — except the cat, which is a logo, not text. */
.wordmark a {
  color: inherit;
  text-decoration: none;
}

/* One 15px mono line is a ~24px-tall tap target. Vertical padding grows
   the two text links to ~44px, and the negative margin hands the space
   straight back so the header's layout doesn't move a pixel. Padding
   rather than an ::after halo: the home link's overflow-hidden (its
   clipping window for the act) would clip a halo, but padding sits
   inside the clip. The cat is excluded — its box is choreography
   (max-width 0 → 1.3em), and it only exists for pointers and keyboards
   anyway. */
.wordmark a:not(.octo) {
  padding: 10px 0;
  margin: -10px 0;
}

.wordmark a:not(.octo):hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* Hovering the name plays a staged act for the rest of the mark:
   1. the "/" stands up into a "|" and stays there,
   2. "lab" slides left INTO the line and is swallowed by it — the link
      box clips the text at its left edge (right where the line stands),
      so letters vanish as they reach it and nothing comes out the other
      side. No fade: the text physically travels out of view.
   3. then the GitHub mark slides out of the line the opposite way,
      left to right, into the space "lab" left behind,
   ending on "mihneaptu | octocat". The ~ sibling selector is what lets
   the first link style the elements after it; the extra <span> inside
   the "lab" link is the part that slides while the link stays put as
   the clipping window, and .octo is the same trick for the logo.

   The durations here are hand-picked, not the token speeds — those are
   for micro feedback (80–120ms), too fast for a sequence the eye is
   meant to follow. Each element carries TWO transition rules: the one
   in its hover selector times the exit, the one in its base selector
   times the return (so on un-hover everything plays back in reverse
   order: logo back into the line first, then "lab" slides back out). */

.wordmark .tick {
  color: var(--accent); /* the one colored character in the wordmark */
  display: inline-block; /* inline elements can't be transformed */
  cursor: default; /* decorative glyph — no text I-beam */
  user-select: none;
  /* return timing: the line lies back down after the logo is gone */
  transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1) 0.08s;
}

.wordmark a.home {
  max-width: 4ch; /* mono font: wide enough for the resting text */
  overflow: hidden;
  /* return timing: the box reopens after the logo has retreated */
  transition: max-width 0.25s cubic-bezier(0.32, 0.72, 0, 1) 0.12s;
}

.wordmark a.home span {
  display: inline-block;
  /* return timing: the text slides back out of the line last */
  transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1) 0.18s;
}

/* The logo's clipping window: closed at rest, and its left edge sits
   right against the line — the same door "lab" leaves through. It's a
   link to the repo, clickable once revealed; the resets keep it looking
   like part of the wordmark rather than a control. */
.wordmark .octo {
  align-self: center; /* the logo is centered on the text, not baseline-set */
  position: relative; /* lifts the cat above the hold box so it stays clickable */
  background: none;
  border: 0;
  padding: 0;
  color: inherit; /* the svg draws with currentColor */
  cursor: pointer;
  max-width: 0;
  overflow: hidden;
  /* return timing: the window closes as the logo slides back in */
  transition: max-width 0.35s cubic-bezier(0.32, 0.72, 0, 1);
}

.wordmark .octo:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

.wordmark .octo svg {
  display: block;
  width: 1.15em;
  height: 1.15em;
  transform: translateX(-110%); /* parked inside the line, out of view */
  /* return timing: the logo retreats into the line first */
  transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
}

/* The safety net for fast pointers: everything between the name and
   the cat is a MOVING target while the act plays — the "/" rotates its
   own hit-box out from under a pointer parked on its edge, and a sweep
   toward the cat crosses the collapsing "lab" box, which was never a
   trigger. Either way :has() goes false mid-act and the whole thing
   reverses under the cursor, which reads as flicker.

   The hold box is the fix: an invisible span pinned over the "/" → cat
   corridor that NEVER moves. At rest it's pointer-events: none, so it
   can't block clicking "lab" or start the act by itself; the moment a
   real trigger opens the act (rule further down) it turns hit-testable
   and latches the open state for as long as the pointer stays anywhere
   in the corridor — a stable floor under all the moving scenery. It
   sits above "lab" (absolute > static) but below the cat (relative,
   later sibling), so the cat keeps its hover and its click.

   Geometry is in the wordmark's own mono ch: the name is 9 characters,
   so the corridor starts just before the "/" and runs a bit past where
   the tail of "lab" rests — the whole stretch a pointer crosses. The
   letter-spacing makes 9ch land a hair short, which only tucks the
   start deeper under the name: covering the name's tail while open is
   harmless, the hold itself keeps the act latched there. */
.octo-hold {
  position: absolute;
  top: -0.2em;    /* a little taller than the text line — forgiving, */
  bottom: -0.2em; /* like the fanned-out star lanes above the logo   */
  left: 8.5ch;
  width: 7ch;
  pointer-events: none;
}

/* The open state holds while the pointer is on the name, the line,
   the cat itself, or the hold box — otherwise the act would reverse
   the moment you leave "mihneaptu" and the cat could never be
   reached, let alone clicked. :has() lets the container watch all
   the triggers at once.

   The cat's :focus-visible is a trigger too: it's in the tab order
   (it's a link), and without it a keyboard user would land on a
   0-width box — focused, invisible, unusable. Tabbing to the cat
   now plays the same act a hover does. */
.wordmark:has(a:first-child:hover, .tick:hover, .octo:hover, .octo:focus-visible, .octo-hold:hover) .octo-hold {
  pointer-events: auto; /* arm the latch only while the act is open */
}

.wordmark:has(a:first-child:hover, .tick:hover, .octo:hover, .octo:focus-visible, .octo-hold:hover) .tick {
  transform: rotate(-20deg); /* counter-clockwise stands "/" up into "|" */
  /* exit timing: straighten right away */
  transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1);
}

.wordmark:has(a:first-child:hover, .tick:hover, .octo:hover, .octo:focus-visible, .octo-hold:hover) a.home {
  max-width: 0;
  /* exit timing: the box starts closing while the tail of "lab" is
     still being absorbed, so the logo emerges right at the line
     instead of a box-width away from it */
  transition: max-width 0.2s cubic-bezier(0.32, 0.72, 0, 1) 0.28s;
}

.wordmark:has(a:first-child:hover, .tick:hover, .octo:hover, .octo:focus-visible, .octo-hold:hover) a.home span {
  transform: translateX(-110%); /* fully past the clip edge, i.e. the line */
  /* exit timing: the visible journey through the line. An almost-even
     curve so the travel can be seen, not a snap. */
  transition: transform 0.4s cubic-bezier(0.45, 0.05, 0.35, 0.95) 0.03s;
}

.wordmark:has(a:first-child:hover, .tick:hover, .octo:hover, .octo:focus-visible, .octo-hold:hover) .octo {
  max-width: 1.3em;
  /* exit timing: opens while the last letter is still being swallowed —
     overlapping the stages is what keeps the act tight without losing
     the story */
  transition: max-width 0.2s cubic-bezier(0.32, 0.72, 0, 1) 0.3s;
}

.wordmark:has(a:first-child:hover, .tick:hover, .octo:hover, .octo:focus-visible, .octo-hold:hover) .octo svg {
  transform: translateX(0);
  /* exit timing: the logo walks out of the line, left to right, starting
     right as the tail of "lab" disappears into it */
  transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1) 0.32s;
}

/* Hovering the cat itself earns a bonus scene: three golden stars
   fly out of the logo, left to right, swinging gently — one pass,
   then the sky empties until the next hover. The cast of <span>s and
   each star's flight plan (lane, speed, size, delay — the custom
   properties read below) come from scripts/octo-stars.js, which
   starts the show by adding .playing; this stylesheet owns
   everything the stars actually DO.

   They live OUTSIDE the .octo link (whose overflow:hidden is busy
   being the reveal's clipping window and would trap them) as a
   sibling pinned to the wordmark's right edge — exactly where the
   logo sits.

   Each star is two elements doing one job each: the outer span flies
   steadily rightward (linear), while its ::before — the star shape
   itself — swings up and down on its own clock (ease-in-out, like a
   pendulum). Two axes on two mismatched clocks combine into a smooth
   swinging glide where no two passes look quite the same. */

.octo-stars {
  position: absolute;
  /* The horizontal anchor is set per-show by the script, in px from
     the wordmark's LEFT edge — the stable one. Anchoring to the right
     edge (left: 100%) would glue the stars to a moving target: that
     edge travels whenever the reveal act plays back, dragging a
     still-fading show sideways with it. The 100% below is only the
     resting value for the no-JS case, where no stars ever fly. */
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  width: 3.2em;  /* the flight corridor */
  height: 1.4em; /* a bit taller than the logo, so the lanes fan out */
  pointer-events: none; /* scenery — never blocks a click */
  /* The whole scene fades as one: .playing (added by the script) only
     fades this container in. When the pointer leaves mid-show, the
     class drops and the container fades back out while the stars KEEP
     FLYING underneath — the show dissolves in motion rather than
     freezing, the same soft exit the stars get at the end of the
     corridor. */
  opacity: 0;
  transition: opacity 0.35s ease;
}

/* One star. Its whole flight plan arrives as custom properties set by
   the script; the fallbacks only matter if a star is ever created by
   hand. `1` iteration, not infinite: each star flies its corridor
   once and is done, so the show ends by itself — the sky empties even
   if the cursor stays on the logo, until the next hover restages it. */
.octo-stars span {
  position: absolute;
  left: 0;
  top: var(--lane, 36%);
  opacity: 0; /* covers the delay phase before the star's launch */
  animation: octo-star-flight var(--flight, 1.3s) linear var(--wait, 0s) 1;
}

/* The star shape: a classic five-pointed star cut from a square with
   clip-path — crisper and more portable than hoping a ★ glyph
   renders the same in every font. Gold is the lab's only hue,
   reserved in tokens.css for exactly this moment. */
.octo-stars span::before {
  content: "";
  display: block;
  width: var(--size, 0.5em);
  height: var(--size, 0.5em);
  background: var(--gold);
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
  /* The swing may loop — it's invisible whenever the flight isn't
     mid-corridor, so only the flight's iteration count matters. */
  animation: octo-star-swing var(--swing, 0.6s) ease-in-out infinite alternate;
}

/* The flight: pop out from behind the logo, glide right, and fade
   before the corridor ends — the disappearance reads as distance
   traveled, not as hitting a wall. */
@keyframes octo-star-flight {
  0%   { transform: translateX(-0.3em); opacity: 0; }
  12%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { transform: translateX(2.6em); opacity: 0; }
}

/* The swing: an easy pendulum around the star's own lane. `alternate`
   plays it back and forth so the turnarounds are soft, never a snap. */
@keyframes octo-star-swing {
  from { transform: translateY(-0.16em); }
  to   { transform: translateY(0.16em); }
}

/* The script adds .playing when the cat is hovered (hovering the name
   merely reveals it) and removes it when the cursor leaves or the last
   star has landed. The stars are already flying the moment they're
   staged — this only lifts the curtain. */
.octo-stars.playing {
  opacity: 1;
}

/* Under reduced motion the global rule below would collapse these
   infinite loops into a flicker, so the stars simply stay home. */
@media (prefers-reduced-motion: reduce) {
  .octo-stars {
    display: none;
  }
}

/* The light/dark switch. Styled like a token chip, not like a .btn —
   it's part of the lab's chrome, not one of the specimens on display. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  position: relative; /* anchors the invisible hit-area extension below */
  color: var(--ink-muted);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: var(--space-2);
  cursor: pointer;
  transition: color var(--speed-color) ease,
              border-color var(--speed-color) ease,
              transform var(--speed-move) ease;
}

/* The visible chip is ~35px — under the 44px touch target. The halo
   extends the clickable area without growing the chip; it has no box
   of its own, so nothing changes visually. */
.theme-toggle::after {
  content: "";
  position: absolute;
  inset: -5px;
}

/* The chip's icon — the sun-moon exhibit's morph in miniature, and
   the chip's ONLY content: the icon is the current sky. Clicking the
   sun sinks its rays into the swelling disc while the mask's bite
   circle carves the crescent, and the lab darkens around it.
   All three parts share one ease; the delays choreograph the handoff,
   flipped per direction: going to the moon the rays sink in first and
   the bite carves last, coming back the crescent heals first and the
   rays climb out last. The rays run a touch faster so the sweeping
   bite hole never catches them. */

.tt-sky {
  display: block;
  width: 1.25em;
  height: 1.25em;
  /* the sunrise overshoot pokes the ray caps a hair past the
     viewBox — let them, instead of clipping them flat */
  overflow: visible;
}

.tt-disc,
.tt-rays {
  transform-box: view-box;
  transform-origin: center;
}

/* The chip runs the exhibit's exact choreography at ~60% of its clock:
   motion across 16px reads far slower than the same motion across
   176px, so at full duration the little moon feels like it's dragging.
   Same sequence, same proportions, smaller stage, faster tempo. */

/* Every delay below rides on --tt-hold (default 0ms): a whole-morph
   hold that shifts the choreography in time without touching its
   internal stagger. theme.js sets it per icon during a theme reveal,
   to the moment the reveal's sweep arrives at THAT icon — so a
   miniature far from the toggle (the homepage card) is uncovered
   mid-morph instead of hiding its whole transformation under the
   old-theme snapshot. The chip's own hold computes to 0ms: it sits
   at the reveal's epicenter and morphs the moment it's clicked. */

/* Sun pose (light theme) — these transitions time the RETURN to sun.
   The rays' scale keeps the sunrise overshoot (a hair past resting
   length, same ease as the exhibit's rays). Their rotation rides its
   own shorter clock — the individual scale/rotate properties let the
   two motions transition separately — symmetric with nightfall's
   100ms twist, so the unwind finishes while the rays are still under
   the shrinking disc. On one shared clock the 45° unwind stretched
   across the whole 200ms emergence, visibly outlasting the quick
   twist that wound it up: readable on the exhibit's 176px stage,
   drag at chip size. The exhibit itself keeps the classic combined
   transform on purpose. */
.tt-rays {
  rotate: 0deg;
  scale: 1;
  transition: scale 200ms cubic-bezier(0.34, 1.3, 0.64, 1)
              calc(var(--tt-hold, 0ms) + 85ms),
              rotate 100ms cubic-bezier(0.65, 0, 0.35, 1)
              calc(var(--tt-hold, 0ms) + 85ms);
}

.tt-disc {
  transition: transform 240ms cubic-bezier(0.65, 0, 0.35, 1)
              calc(var(--tt-hold, 0ms) + 25ms);
}

.tt-bite {
  transition: transform 240ms cubic-bezier(0.65, 0, 0.35, 1)
              var(--tt-hold, 0ms);
}

/* Moon pose (dark theme) + the arrival timing: rays lead and land,
   THEN the bite carves — held back so its notch doesn't show on the
   right rim while the left rays still have the longest journey.
   The durations are the exhibit's nightfall numbers (170/360/240ms)
   scaled by the same ~60% as the sunrise rules above — nightfall is
   the snappy direction on the exhibit, and it must stay the snappy
   direction here too. */
[data-theme="dark"] .tt-rays {
  rotate: 45deg;
  scale: 0.25;
  /* one duration/delay covers both clocks: going dark the twist and
     the plunge travel together */
  transition-duration: 100ms;
  transition-delay: var(--tt-hold, 0ms);
}

[data-theme="dark"] .tt-disc {
  transform: scale(1.75);
  /* outlasts the bite so the crescent is carved from a still-growing
     body — grow-and-carve as one event, not grow, stop, carve */
  transition-duration: 215ms;
  transition-delay: calc(var(--tt-hold, 0ms) + 15ms);
}

[data-theme="dark"] .tt-bite {
  transform: translate(-15.7px, 0);
  /* starts at half the rays' clock, same beat as the exhibit: the
     rays are most of the way home before the notch appears */
  transition-duration: 145ms;
  transition-delay: calc(var(--tt-hold, 0ms) + 50ms);
}

/* The exhibit's stars stay on the exhibit — at chip size they read
   as specks, and the chrome should whisper. The crescent alone says
   "night" just fine here. */

.theme-toggle:hover {
  color: var(--ink);
  border-color: var(--line-strong);
}

/* Pressing the sky, in miniature — same give as the exhibit's button
   (0.97, sun-moon/index.html). Anything below ~0.95 reads exaggerated
   on a chip this small. */
.theme-toggle:active {
  transform: scale(0.97);
}

.theme-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A quiet way back home from a component page. Pinned to the top-left
   corner and styled like the rest of the chrome (mono, muted) — visible
   when you look for it, invisible when you don't. */
.back-link {
  position: fixed;
  top: var(--space-5);
  left: var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-muted);
  text-decoration: none;
  transition: color var(--speed-color) ease;
}

/* One line of 13px mono is a ~21px-tall target. The halo brings it to
   ~44px each way; the corner around it is empty, so it overlaps nothing. */
.back-link::after {
  content: "";
  position: absolute;
  inset: -12px;
}

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

/* --- Type roles ------------------------------------------------------------ */

.page-title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  /* At this size the size IS the emphasis — bold on top of it shouts,
     especially as light-on-dark, which already renders heavier. */
  font-weight: 500;
  line-height: 1.05; /* big type needs tight lines or it falls apart */
  letter-spacing: -0.02em;
  text-wrap: balance; /* a two-line title splits evenly, never 90/10 */
}

/* The headline's hover italic (scripts/title-lean.js): the script splits
   the title into these spans and skews each on its own spring — Bricolage
   ships no italic and font-synthesis is off, so the slant is a transform.
   Pivoting from the bottom edge keeps the feet planted while the tops
   swing over, which is how type actually leans; inline-block because
   inline elements can't be transformed. No transition here on purpose —
   the motion is integrated per-frame by the script, and a CSS transition
   underneath it would fight every step. */

/* On touch the finger drives the same physics, so the word must keep the
   gesture: pan-y hands vertical swipes to the scroll but lets horizontal
   drags comb the letters instead of being eaten as a failed scroll.
   touch-action is ignored on non-replaced inline elements, so the word
   must be an inline-block for pan-y to mean anything — which also keeps
   the split word from ever wrapping mid-word, like unsplit text. The
   split title was never really selectable text anyway (a drag across it
   would grab letters one span at a time), so selection and the tap
   flash are turned off rather than left to misfire. These live on
   .lean-word, which only exists once the script is running — a title
   the script left alone stays ordinary selectable text. */
.page-title .lean-word {
  display: inline-block;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.page-title .lean-letter {
  display: inline-block;
  transform-origin: 50% 100%;
}

.section-title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.2; /* headings tighten the body's 1.6 */
  letter-spacing: -0.01em;
  text-wrap: balance;
}

/* --- Specimen stage --------------------------------------------------------
   The signature element of the lab: a white artboard with a faint dot
   grid, like a canvas in a design tool. Components are pinned to it and
   annotated in mono. */

.stage {
  background-color: var(--surface);
  background-image: radial-gradient(var(--line) 1px, transparent 1px);
  background-size: 18px 18px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6);
}

.stage-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
}

/* A pinned specimen: the component plus its mono label underneath */
.specimen {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
}

.specimen-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

/* --- Component cards (homepage grid) --------------------------------------- */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--space-5);
}

.component-card {
  display: block;
  position: relative; /* anchors the optional corner tag */
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  color: var(--ink);
  overflow: hidden; /* clips the preview's dot grid to the rounded corners */
  transition: box-shadow var(--speed-color) ease,
              transform var(--speed-move) ease;
}

.component-card:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px); /* a gentle lift — the card is pickable */
}

.component-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* An optional status tag pinned to the card's top-right corner —
   quiet chrome in the mono face, not part of the specimen itself. */
.card-tag {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 1; /* the preview's dot layer is positioned too and comes later
                 in the DOM — without this the dots paint over the tag */
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: 1;
  color: var(--ink-muted);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: var(--space-1) var(--space-2);
  /* No opacity dimming here: --ink-muted is already the quiet voice, and
     at 13px it sits just above the 4.5:1 contrast floor — fading it to
     80% dropped it under. */
}

/* The "interactive" tag hints that it responds: hovering it lets a
   faint rainbow drift through the LETTERS, flowing left to right.
   The gradient is clipped to the text and slides underneath it the
   whole time — it never pauses, so whenever the tint fades in the
   colors are already mid-flight, and on the way out they keep moving
   under the returning ink instead of freezing (the same dissolve-in-
   motion exit the octo stars use). What actually animates on hover is
   the solid ink painted on top: opaque at rest, half-clear on hover,
   so the rainbow shows through as a tint, never full saturation.
   The fade runs on its own 400ms clock — the token speeds are micro
   feedback, too quick to read as an entrance. The gradient starts and
   ends on the same hue, so each 200% slide loops seamlessly. */
.card-tag-rainbow {
  background: linear-gradient(90deg,
    #ff6b6b, #ffb56b, #ffe66b, #6bff8f, #6bc8ff, #b06bff, #ff6b6b);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: var(--ink-muted); /* opaque ink hides the rainbow */
  transition: -webkit-text-fill-color 400ms ease;
  animation: tag-rainbow-drift 2.5s linear infinite;
}

/* Hover is the desktop way in, and it's gated to devices that really
   have one: touch browsers replay :hover on tap, and that sticky
   state would pin the tint on long after the show should end. */
@media (hover: hover) {
  .card-tag:hover .card-tag-rainbow {
    /* 55% ink stays on top — the rainbow reads as a tint, not a paint job */
    -webkit-text-fill-color: color-mix(in srgb, var(--ink-muted) 55%, transparent);
  }
}

/* The touch way in: rainbow-tag.js lights this on tap (instead of
   letting the card navigate) and holds it for one full drift loop,
   then the ink returns on the same 400ms fade hover uses. */
.card-tag.is-lit .card-tag-rainbow {
  -webkit-text-fill-color: color-mix(in srgb, var(--ink-muted) 55%, transparent);
}

/* Negative percentages shift the image rightward; -200% is exactly one
   image width at this background-size, so the loop has no seam. */
@keyframes tag-rainbow-drift {
  from { background-position: 0% 0; }
  to   { background-position: -200% 0; }
}

.card-preview {
  /* A miniature stage inside the card. pointer-events: none so the
     preview buttons are scenery, not controls — the whole card is the link. */
  position: relative; /* anchors the dot-grid layer below */
  border-bottom: 1px solid var(--line);
  padding: var(--space-5);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-height: 108px;
  pointer-events: none;
}

/* The dot grid on its own layer, as a solid color with the dot pattern
   punched out by a mask. A background-image gradient can't transition,
   so it would snap during the theme crossfade below; a background-COLOR
   fades like everything else, and the mask never changes. */
.card-preview::before {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--line);
  -webkit-mask-image: radial-gradient(#000 1px, transparent 1px);
  -webkit-mask-size: 14px 14px;
  mask-image: radial-gradient(#000 1px, transparent 1px);
  mask-size: 14px 14px;
}

/* Positioned pseudo-elements paint above static children, so the
   miniature specimens need a lift to stay above the dot layer. */
.card-preview > * {
  position: relative;
}

.card-body {
  padding: var(--space-4) var(--space-5) var(--space-5);
}

.card-name {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 500; /* the only text in the card body — it needs no bold to lead */
}

.card-meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-muted);
  margin-top: var(--space-1);
}

/* --- Footer ----------------------------------------------------------------- */

.lab-footer {
  border-top: 1px solid var(--line);
  margin-top: var(--space-9);
  padding: var(--space-5) 0 var(--space-7);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

/* The footer's wordmark doubles as the repo link — the path a phone
   can actually reach (the header's cat only opens on hover or keyboard
   focus). Quiet like the header links: footer ink, underline only on
   hover, so it reads as the same mark until you ask it to be a link. */
.lab-footer a {
  position: relative; /* anchors the tap halo below */
  color: inherit;
  text-decoration: none;
}

.lab-footer a:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* One line of 13px mono is a ~20px-tall target; the halo brings it to
   ~44px. The strip around the footer is empty, so it overlaps nothing. */
.lab-footer a::after {
  content: "";
  position: absolute;
  inset: -12px;
}

/* --- Theme reveal ------------------------------------------------------------
   Ink and paper reverse polarity between themes, so interpolating both
   inevitably makes them the same gray halfway through. Instead, theme.js
   captures the complete old and new pages and reveals the new one from the
   sun/moon control. Every pixel is always part of one valid theme, which
   keeps text, button labels, card borders, and backgrounds in lockstep. */

@media (prefers-reduced-motion: no-preference) {
  html.theme-transitioning::view-transition-old(root),
  html.theme-transitioning::view-transition-new(root) {
    animation: none;
    mix-blend-mode: normal;
  }

  html.theme-transitioning::view-transition-old(root) {
    z-index: 1;
  }

  html.theme-transitioning::view-transition-new(root) {
    z-index: 2;
  }
}

/* --- Accessibility ----------------------------------------------------------
   If someone has asked their OS to reduce motion, honor it: everything
   still works, it just stops animating. */

/* Present in the document outline (screen readers, reader modes) but not
   on screen — for headings a visual page doesn't need but its structure
   does. The clip dance is the standard recipe: display:none would remove
   it from the outline too. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}
