/* ============================================================================
   BIOLINK — dark, monochrome profile page
   ----------------------------------------------------------------------------
   1 tokens · 2 background · 3 layout & panel · 4 profile · 5 social buttons
   5b extras (orbit, shimmer, sweep, wave, ripple) · 6 music & footer
   7 studio · 8 boot & toast · 9 inverted tone · 10 small screens & motion
   11 named buttons

   Only black, white and grey. app.js writes the greys in from the page's
   config.js; every rule below reads them through variables.
   ========================================================================== */

/* --------------------------------- 1 tokens -------------------------------- */
:root {
  --ink: #ffffff;                 /* text, icons (theme.ink) */
  --ink-rgb: 255 255 255;
  --shade: #000000;               /* page background (theme.shade) */
  --shade-rgb: 0 0 0;
  --shade2: #0d0d0d;              /* raised surfaces (theme.shade2) */
  --shade2-rgb: 13 13 13;         /* the same, so it can be made see-through */
  --mid: #8a8a8a;                 /* muted grey (theme.mid) */

  --line: rgb(var(--ink-rgb) / 12%);
  --line2: rgb(var(--ink-rgb) / 22%);
  --edge: rgb(var(--ink-rgb) / 18%);   /* buttons and rows: hairlines you can
                                          actually see. --line is for the quiet
                                          stuff — card, dividers, the player */
  --sheen: rgb(var(--ink-rgb) / 8%);   /* 1px lit top edge on raised surfaces */
  --surface: rgb(var(--ink-rgb) / 5%);
  --surface2: rgb(var(--ink-rgb) / 9%);
  --soft: rgb(var(--ink-rgb) / 72%);
  --glyph: rgb(var(--ink-rgb) / 92%);   /* brand marks: a filled logo at grey
                                           loses its fine detail, so the glyphs
                                           sit brighter than --soft text */

  --radius: 12px;                 /* theme.ui.radius */
  --glow: 0.4;                    /* theme.ui.glow / 100 */
  --card-blur: 18px;              /* theme.ui.cardBlur */
  --grain: 0;
  --speed: 22s;
  --intensity: 1;
  --spot: 0;
  --spot-x: 50%;
  --spot-y: 0%;
  --img-blur: 8px;
  --img-dim: 0.55;

  --page: min(100% - 2rem, 420px);
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --spring: cubic-bezier(0.34, 1.56, 0.44, 1);
  --panel-w: 340px;
}

*,
*::before,
*::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

/* Every icon stroke (see assets/js/icons.js) is measured in PIXELS, not in
   viewBox units. A 20px glyph inherits a 24-unit box, so a "1.5 unit" hairline
   was really 1.25px and blurred into grey mush at the sizes this page uses;
   with non-scaling-stroke a 1.5 is exactly 1.5px whether the glyph is drawn at
   15px (the footer pair) or 22px (the social tiles). */
svg * { vector-effect: non-scaling-stroke; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--shade);
  color: var(--ink);
  font: 400 15px/1.55 var(--font);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }
[hidden] { display: none !important; }
:focus-visible { outline: 1px solid var(--ink); outline-offset: 2px; }

/* ------------------------------ 2 background ------------------------------- */
.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  background: var(--shade);
}

.bg__layer { position: absolute; inset: 0; }
.bg__canvas { width: 100%; height: 100%; }

.bg__base { background: radial-gradient(120% 80% at 50% -20%, var(--shade2), var(--shade) 72%); }
[data-bg="solid"] .bg__base { background: var(--shade2); }

.bg__aurora,
.bg__grid,
.bg__image,
.bg__video,
.bg__grain,
.bg__canvas { display: none; opacity: 0; transition: opacity 0.5s ease; }

[data-bg="aurora"] .bg__aurora,
[data-bg="grid"] .bg__grid,
[data-bg="image"] .bg__image,
[data-bg="video"] .bg__video,
[data-bg="rain"] .bg__canvas,
[data-bg="particles"] .bg__canvas,
[data-bg="mesh"] .bg__canvas,
[data-bg="waves"] .bg__canvas { display: block; opacity: 1; }

/* snow: either the whole background, or the flakes riding on top of a still picture —
   including a picture standing in for a clip in the video rotation */
[data-bg="snow"] .bg__canvas,
[data-bg="image"][data-snow="on"] .bg__canvas,
[data-bg="video"][data-snow="on"] .bg__canvas { display: block; opacity: 1; }

/* over a picture the flakes go above it, but under the veil (which pushes the whole
   background back) and under the grain (which is a texture on the page, not the sky) */
[data-bg="image"][data-snow="on"] .bg__canvas,
[data-bg="video"][data-snow="on"] .bg__canvas { z-index: 1; }
.bg__veil,
.bg__grain { z-index: 2; }

/* a still dropped into the video rotation: same veil, same blur, one picture
   instead of a clip (app.js puts `data-still` on <html> when it finds one) */
[data-bg="video"][data-still="on"] .bg__image { display: block; opacity: 1; }
[data-bg="video"][data-still="on"] .bg__video { display: none; }

[data-grain="on"] .bg__grain { display: block; opacity: var(--grain); }

[data-anim="off"] .bg__aurora .blob,
[data-anim="off"] .bg__grid { animation: none; }

/* image / video: cover, blurred, then dimmed by the veil */
.bg__image,
.bg__video {
  background-position: center;
  background-size: cover;
  filter: blur(var(--img-blur));
  transform: scale(1.06);
}
.bg__video { width: 100%; height: 100%; object-fit: cover; }

.bg__veil {
  position: absolute;
  inset: 0;
  background: var(--shade);
  opacity: 0;
  transition: opacity 0.5s ease;
}
[data-bg="image"] .bg__veil,
[data-bg="video"] .bg__veil { opacity: var(--img-dim); }

/* grayscale glow blobs (aurora) */
.bg__aurora .blob {
  position: absolute;
  width: 46vmax;
  height: 46vmax;
  border-radius: 50%;
  filter: blur(80px);
  will-change: transform;
}
.blob--a { top: -18%; left: -12%; background: radial-gradient(circle, rgb(var(--ink-rgb) / 22%), transparent 68%); animation: drift-a var(--speed) var(--ease) infinite alternate; }
.blob--b { bottom: -22%; right: -14%; background: radial-gradient(circle, rgb(var(--ink-rgb) / 15%), transparent 68%); animation: drift-b calc(var(--speed) * 1.25) var(--ease) infinite alternate; }
.blob--c { top: 24%; right: -24%; background: radial-gradient(circle, rgb(var(--ink-rgb) / 11%), transparent 70%); animation: drift-c calc(var(--speed) * 1.6) var(--ease) infinite alternate; }
.blob--d { bottom: 6%; left: -22%; background: radial-gradient(circle, rgb(var(--ink-rgb) / 9%), transparent 70%); animation: drift-b calc(var(--speed) * 1.9) var(--ease) infinite alternate-reverse; }

@keyframes drift-a { from { transform: scale(var(--intensity)) translate3d(0, 0, 0); } to { transform: scale(calc(var(--intensity) * 1.18)) translate3d(9%, 7%, 0); } }
@keyframes drift-b { from { transform: scale(var(--intensity)) translate3d(0, 0, 0); } to { transform: scale(calc(var(--intensity) * 1.12)) translate3d(-8%, -6%, 0); } }
@keyframes drift-c { from { transform: scale(var(--intensity)) translate3d(0, 0, 0); } to { transform: scale(calc(var(--intensity) * 1.22)) translate3d(-6%, 9%, 0); } }

.bg__grid {
  background-image:
    linear-gradient(rgb(var(--ink-rgb) / 12%) 1px, transparent 1px),
    linear-gradient(90deg, rgb(var(--ink-rgb) / 12%) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: linear-gradient(to top, #000, transparent 62%);
  mask-image: linear-gradient(to top, #000, transparent 62%);
  animation: grid-pan calc(var(--speed) * 1.4) linear infinite;
}
@keyframes grid-pan { from { background-position: 0 0, 0 0; } to { background-position: 0 46px, 46px 0; } }

.bg__grain {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: var(--spot);
  transition: opacity 0.4s ease;
  background: radial-gradient(520px 380px at var(--spot-x) var(--spot-y), rgb(var(--ink-rgb) / 8%), transparent 70%);
}


/* --------------------------- 3 layout & content panel ---------------------- */
.page {
  position: relative;
  z-index: 1;
  width: var(--page);
  min-height: 100dvh;
  margin: 0 auto;
  padding: 46px 0 38px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12px;
}

/* theme.ui.tabs puts everything on one panel, guns.lol style */
.shell {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* the card: a hairline edge, one soft light line along the top and a bloom
   behind the avatar, so it reads as a surface instead of a flat rectangle.
   `--card-alpha` (theme.ui.cardAlpha) lets the background show through it. */
[data-tabs="on"] .shell {
  --pad-t: 24px;
  --pad-x: 18px;
  --card-radius: calc(var(--radius) + 8px);
  padding: var(--pad-t) var(--pad-x) 20px;
  border: 1px solid var(--line);
  border-radius: var(--card-radius);
  background:
    radial-gradient(130% 85% at 50% -25%, rgb(var(--ink-rgb) / 6%), transparent 60%),
    rgb(var(--shade2-rgb) / var(--card-alpha, 100%));
  box-shadow:
    inset 0 1px 0 rgb(var(--ink-rgb) / 7%),
    0 26px 55px -36px rgb(0 0 0 / 85%);
}
[data-tabs="on"][data-card="on"] .shell {
  /* Tempered glass: the background diffuses through a blur, the pane picks up a
     little light and a little less contrast the way real glass does, and the edge
     stays crisp — a lit top line, a hairline just inside it, and the drop shadow
     behind. The 55% floor keeps text legible when `cardAlpha` asks for a clearer
     panel than that. */
  background:
    linear-gradient(150deg, rgb(var(--ink-rgb) / 7%), transparent 38%),
    radial-gradient(130% 85% at 50% -25%, rgb(var(--ink-rgb) / 6%), transparent 60%),
    rgb(var(--shade-rgb) / min(var(--card-alpha, 100%), 55%));
  backdrop-filter: blur(var(--card-blur)) saturate(130%) brightness(1.06);
  -webkit-backdrop-filter: blur(var(--card-blur)) saturate(130%) brightness(1.06);
  box-shadow:
    inset 0 1px 0 rgb(var(--ink-rgb) / 13%),
    inset 0 0 0 1px rgb(var(--ink-rgb) / 5%),
    0 30px 60px -34px rgb(0 0 0 / 90%);
}

/* a soft highlight that follows the pointer across the panel (behind content) */
[data-tabs="on"] .shell::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease;
  background: radial-gradient(
    300px 240px at var(--mx, 50%) var(--my, 0%),
    rgb(var(--ink-rgb) / 7%),
    transparent 72%
  );
}
[data-tilt="on"][data-tabs="on"] .shell:hover::before { opacity: 1; }

/* --- 3D: the card leans towards the pointer (app.js writes the custom props) --- */
[data-tilt="on"] .page { perspective: 1100px; }
[data-tilt="on"] .shell {
  transform:
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg))
    translate3d(0, var(--lift, 0px), 0)
    scale(var(--pop, 1));
  transform-style: preserve-3d;
  transition: transform 0.6s var(--ease), box-shadow 0.4s ease;
  will-change: transform;
}
/* while the pointer is inside, app.js lerps the values itself — no easing lag */
[data-tilt="on"] .shell.is-live { transition: transform 0.07s linear, box-shadow 0.4s ease; }
[data-tilt="on"][data-tabs="on"] .shell:hover {
  box-shadow: 0 0 0 1px var(--line2), 0 34px 60px -34px rgb(var(--ink-rgb) / 14%);
}

/* the contents sit on their own layers, so the tilt reads as real depth.
   Only with a panel to pop out of, and only when it is not frosted —
   backdrop-filter would flatten the 3D context. */
[data-tilt="on"] .shell .profile { transform-style: preserve-3d; }
[data-tilt="on"][data-tabs="on"]:not([data-card="on"]) .shell .avatar {
  transform: translateZ(var(--z-avatar, 34px)) scale(var(--s-avatar, 1));
}
[data-tilt="on"][data-tabs="on"]:not([data-card="on"]) .shell .name-row,
[data-tilt="on"][data-tabs="on"]:not([data-card="on"]) .shell .handle,
[data-tilt="on"][data-tabs="on"]:not([data-card="on"]) .shell .bio,
[data-tilt="on"][data-tabs="on"]:not([data-card="on"]) .shell .meta {
  transform: translateZ(18px);
}
[data-tilt="on"][data-tabs="on"]:not([data-card="on"]) .shell .socials { transform: translateZ(26px); }
[data-tilt="on"][data-tabs="on"] .shell:hover .avatar { --s-avatar: 1.05; }

/* ------------------ 3b banner — appears with img/cover.* ------------------ */
/* Bleeds to the card edges and the avatar sits on the seam, ringed in the card
   colour so it stays readable over whatever the picture is. */
.shell__cover {
  position: relative;
  height: 104px;
  margin: calc(-1 * var(--pad-t, 0px)) calc(-1 * var(--pad-x, 0px)) 0;
  border-radius: var(--card-radius, 0) var(--card-radius, 0) 0 0;
  background: var(--surface2) center / cover no-repeat;
  overflow: hidden;
}
.shell__cover::before {          /* the same pointer highlight the card gets */
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.4s ease;
  background: radial-gradient(
    260px 200px at var(--mx, 50%) var(--my, 0%),
    rgb(var(--ink-rgb) / 10%),
    transparent 72%
  );
}
[data-tilt="on"] .shell:hover .shell__cover::before { opacity: 1; }
.shell__cover::after {           /* melt the picture into the card */
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgb(var(--shade-rgb) / 22%), transparent 46%, var(--shade2));
}
[data-card="on"] .shell__cover::after {
  background: linear-gradient(180deg, rgb(var(--shade-rgb) / 22%), transparent 46%, rgb(var(--shade-rgb) / 55%));
}

[data-cover="on"] .profile { margin-top: -46px; }
[data-cover="on"] .avatar { box-shadow: 0 0 0 4px var(--shade2); }
[data-cover="on"][data-ring="on"] .avatar {
  box-shadow:
    0 0 0 4px var(--shade2),
    0 0 0 5px var(--line2),
    0 0 34px rgb(var(--ink-rgb) / calc(var(--glow) * 0.24));
}

/* entrance animation (theme.ui.motion) — `backwards` fill means the content
   always settles visible, even if a frame or two is missed. It waits for the
   curtain: `is-cleared` goes on <body> as the intro screen starts to go, so this
   plays to an audience rather than to the back of the intro. */
[data-motion="on"] body.is-cleared .reveal {
  animation: rise 0.55s var(--ease) backwards;
  animation-delay: var(--delay, 0ms);
}
@keyframes rise { from { opacity: 0; transform: translateY(6px); } }

/* ------------------------------ 3c the arrival ------------------------------
   The other half of the intro: the curtain goes, and then the page comes up to
   meet it — no cloud, no frost. The page fades up and the card floats in a beat
   later, its contents rising in the stagger above (theme.ui.motion).

   While the curtain is *there*, the page is not: the veil is a strong tint but it
   is still a tint, and a card behind it reads as a card. This is keyed off the
   presence of #intro rather than off a flag, so it cannot get stuck — and the
   animation is the same expiry the curtain itself uses (9s, section 8), so a
   visitor whose intro.js never arrived still gets a page rather than a hole.

   None of the rest is load-bearing either. Without `is-cleared` (script missing,
   or an older browser) the page is simply there; `theme.ui.motion: false` and a
   visitor who asked for less motion get the same plain result — the animations
   sit behind [data-motion="on"], and the settled state of each element is its base
   one, so only the keyframes hide anything. */
body:has(#intro) .page,
body:has(#intro) .dock {
  opacity: 0;
  animation: page-hold 0.001s 9s forwards;
}
@keyframes page-hold { to { opacity: 1; } }

[data-motion="on"] body.is-cleared .page {
  animation: page-in 0.65s var(--ease) both;
}
@keyframes page-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The card leads, a beat behind the page. `translate` and `scale` are their own
   properties rather than `transform`, so this can never fight the 3D lean (which
   owns `transform` on .shell). */
[data-motion="on"] body.is-cleared .shell {
  animation: card-in 0.95s var(--ease) both;
  animation-delay: 90ms;
}
@keyframes card-in {
  from { opacity: 0; translate: 0 22px; scale: 0.985; }
  to { opacity: 1; translate: none; scale: none; }
}

/* -------------------------------- 4 profile -------------------------------- */
.profile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-align: center;
}

.avatar {
  position: relative;
  width: 88px;
  height: 88px;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--surface2);
  border-radius: 30%;
  font-size: 24px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--mid);
  transition: border-radius 0.25s var(--ease);
}
[data-avatar="round"] .avatar { border-radius: 50%; }
[data-avatar="square"] .avatar { border-radius: max(6px, calc(var(--radius) * 0.6)); }
[data-ring="on"] .avatar {
  box-shadow:
    0 0 0 1px var(--line2),
    0 0 0 7px rgb(var(--ink-rgb) / 3%),
    0 0 34px rgb(var(--ink-rgb) / calc(var(--glow) * 0.24));
}
.avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.avatar img.is-dropped { animation: img-in 0.45s var(--ease) both; }
@keyframes img-in { from { opacity: 0; transform: scale(1.05); } }
.avatar::after {                 /* holds the picture inside its shape */
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px rgb(var(--ink-rgb) / 8%);
}

.name-row {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
}
.name {
  margin: 0;
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.2;
}
.badges { display: inline-flex; flex-wrap: wrap; gap: 5px; }
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px 2px 6px;
  border: 1px solid var(--line2);
  border-radius: 999px;
  background: var(--surface);
  color: var(--mid);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 1.6;
}
.badge svg { width: 11px; height: 11px; }
/* strokes are pixel weights now (see the note at the top of this file), so the
   smallest icons get their own lighter hairline instead of the 1.5px default */
.badge svg * { stroke-width: 1.1px; }

.handle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  color: var(--mid);
}
.copy {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--mid);
  opacity: 0.55;
  cursor: pointer;
  transition: opacity 0.18s ease, color 0.18s ease, background 0.18s ease;
}
.copy svg { width: 15px; height: 15px; }
.copy:hover { opacity: 1; color: var(--ink); background: var(--surface2); }

.bio {
  margin: 0;
  max-width: 34ch;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--soft);
}
.bio.is-typing::after {
  content: "";
  display: inline-block;
  width: 7px;
  height: 1em;
  margin-left: 2px;
  vertical-align: -0.15em;
  background: var(--ink);
  animation: caret 1s steps(1) infinite;
}
@keyframes caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

.meta {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 7px;
  font-size: 11.5px;
  letter-spacing: 0.02em;
  color: var(--mid);
  font-variant-numeric: tabular-nums;
}
.meta span { display: inline-flex; align-items: center; gap: 4px; }
.meta svg { width: 12px; height: 12px; }
.meta i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.6;
}


/* -------------------------------- 5 socials -------------------------------- */
/* icon-only buttons: the page's links live here */
.socials {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding-top: 2px;
}
/* on the card the row gets its own shelf, so the profile block reads as one
   thing and the links as another */
[data-tabs="on"] .socials {
  margin-top: 2px;
  padding-top: 15px;
  border-top: 1px solid var(--line);
}
/* hovering the row quietens the rest so the one under the pointer stands out */
#socials:hover .social { color: rgb(var(--ink-rgb) / 42%); }
#socials:hover .social:hover { color: var(--ink); }
.footer__actions:hover .social { color: rgb(var(--ink-rgb) / 42%); }
.footer__actions:hover .social:hover { color: var(--ink); }

.social {
  position: relative;
  overflow: hidden;                /* keeps the tap ripple inside the tile */
  width: 40px;
  height: 40px;
  padding: 0;
  display: grid;
  place-items: center;
  /* The tile is visible at rest, not only on hover: a phone never hovers, so a
     bare glyph there reads as an icon rather than something you can tap. Lit
     from the top (--sheen) so the 1px edge reads crisp even where it lands on a
     half pixel. */
  border: 1px solid var(--edge);
  border-radius: 11px;
  background: linear-gradient(180deg, rgb(var(--ink-rgb) / 7%), var(--surface));
  box-shadow: inset 0 1px 0 var(--sheen);
  color: var(--glyph);
  cursor: pointer;
  transition:
    color 0.2s ease,
    background 0.2s ease,
    transform 0.32s var(--spring);
}
.social svg {
  width: 22px;
  height: 22px;
  transition: transform 0.4s var(--spring);
}
/* the ring pops in as you reach for the button */
.social::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--line2);
  border-radius: inherit;
  opacity: 0;
  transform: scale(0.76);
  transition: opacity 0.22s ease, transform 0.4s var(--spring);
  pointer-events: none;
}
.social:hover {
  color: var(--ink);
  background: var(--surface2);
  transform: translateY(-3px) scale(1.06);
}
.social:hover svg { transform: translateY(-1px) scale(1.16) rotate(-8deg); }
.social:hover::before { opacity: 1; transform: scale(1); }
.social:active { transform: translateY(-1px) scale(0.96); }
.social:active svg { transform: scale(0.9); transition-duration: 0.12s; }

/* A tile that copies its handle instead of opening a page (`copy` in config.js).
   The chip in the corner is what tells you so before you tap it; the glyph inside
   is re-set here because the rules above would otherwise spin and scale it with
   the tile's own icon. */
.social--copy { font: inherit; }
.social__badge {
  position: absolute;
  right: 2px;
  bottom: 2px;
  width: 13px;
  height: 13px;
  display: grid;
  place-items: center;
  border-radius: 4px;
  background: var(--ink);
  color: var(--shade);
  opacity: 0;
  transform: scale(0.5);
  transition: opacity 0.2s ease, transform 0.34s var(--spring);
  pointer-events: none;
}
.social__badge svg {
  width: 9px;
  height: 9px;
  transition: none;
  transform: none;
}
.social__badge svg * { stroke-width: 1.75px; }
.social--copy:hover .social__badge svg,
.social--copy:active .social__badge svg,
.social--copy:focus-visible .social__badge svg { transform: none; }
.social--copy:hover .social__badge,
.social--copy:active .social__badge,
.social--copy:focus-visible .social__badge { opacity: 1; transform: scale(1); }

/* --------------- 5b extras — the studio's party switches ------------------ */
/* ui.orbit — a lit arc sweeping around the avatar */
[data-orbit="on"] .avatar::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1.5px solid transparent;
  border-radius: inherit;
  background: conic-gradient(
    from 0deg,
    transparent 0deg 185deg,
    rgb(var(--ink-rgb) / 90%) 288deg,
    transparent 360deg
  ) border-box;
  -webkit-mask: linear-gradient(#000 0 0) padding-box exclude, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) padding-box exclude, linear-gradient(#000 0 0);
  animation: orbit 4.5s linear infinite;
  pointer-events: none;
}
@keyframes orbit { to { transform: rotate(1turn); } }

/* ui.shimmer — a soft band travelling across the name. The gradient image is 3×
   the text box, so both ends of the travel are solid ink: the name is white at
   rest and only the band moves. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  [data-shimmer="on"] .name {
    background: linear-gradient(
      100deg,
      var(--ink) 0 46%,
      rgb(var(--ink-rgb) / 50%) 50%,
      var(--ink) 54% 100%
    );
    background-size: 300% 100%;
    background-repeat: no-repeat;
    background-position: 0% 0;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    animation: shimmer 5.5s linear infinite;
  }
}
@keyframes shimmer {
  from { background-position: 0% 0; }
  to { background-position: 100% 0; }
}

/* ui.sweep — a slow band of light crossing the card */
[data-sweep="on"] .shell::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(102deg, transparent 38%, rgb(var(--ink-rgb) / 6%) 50%, transparent 62%);
  background-size: 260% 100%;
  background-position: 140% 0;
  animation: sweep 7.5s ease-in-out infinite;
}
@keyframes sweep {
  0% { background-position: 140% 0; }
  45%, 100% { background-position: -80% 0; }
}

/* ui.wave — enter the icon row and they bob in sequence. Uses the standalone
   `translate` property, so it never fights the hover lift's `transform`. */
[data-wave="on"] #socials:hover .social {
  animation: wave 0.72s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 55ms);
}
@keyframes wave {
  0% { translate: 0 0; }
  42% { translate: 0 -5px; }
  100% { translate: 0 0; }
}

/* ui.ripple — the dot app.js drops inside a button when you tap it */
.ripple {
  position: absolute;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: rgb(var(--ink-rgb) / 28%);
  pointer-events: none;
  animation: ripple-out 0.62s var(--ease) forwards;
}
@keyframes ripple-out {
  to { transform: scale(9); opacity: 0; }
}

/* ------------------------------ 6 music & footer --------------------------- */
.music {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: calc(var(--radius) + 2px);
  background:
    radial-gradient(120% 110% at 50% -40%, rgb(var(--ink-rgb) / 5%), transparent 70%),
    var(--surface);
  box-shadow: inset 0 1px 0 rgb(var(--ink-rgb) / 5%);
  transition: border-color 0.25s ease;
}
.music:hover { border-color: var(--line2); }
/* The panel's row: a speaker glyph and the volume, the whole of what a visitor can
   change. The slider is drawn from scratch — a hairline track, the travelled part
   lit up to the value (--fill, written by app.js), a small crisp thumb that lifts
   as you reach for it. The input itself is 14px tall so there is something to hit;
   the line you see is only the track inside it. Firefox gets the same thing through
   ::-moz-range-progress, which fills without any help from JavaScript. */
.music__row { flex: 1 0 100%; display: flex; align-items: center; gap: 12px; }
.music__volume { flex: 0 0 auto; width: 104px; display: flex; align-items: center; gap: 7px; }
.music__vol {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  min-width: 0;
  height: 14px;
  margin: 0;
  --lit: rgb(var(--ink-rgb) / 60%);     /* the travelled part of the track */
  background: transparent;
  cursor: pointer;
}
.music.is-muted .music__vol { --lit: var(--line2); }   /* muted reads grey */
.music input[type="range"]::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--lit) var(--fill, 0%), var(--line2) var(--fill, 0%));
}
.music input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -4px;                   /* centres an 11px thumb on a 3px track */
  border: 0;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 0 rgb(var(--ink-rgb) / 0%);
  transition: box-shadow 0.22s ease, transform 0.22s var(--spring);
}
.music input[type="range"]::-moz-range-track {
  height: 3px;
  border-radius: 999px;
  background: var(--line2);
}
.music input[type="range"]::-moz-range-progress {
  height: 3px;
  border-radius: 999px;
  background: var(--lit);
}
.music input[type="range"]::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border: 0;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 0 rgb(var(--ink-rgb) / 0%);
  transition: box-shadow 0.22s ease, transform 0.22s var(--spring);
}
.music input[type="range"]:hover::-webkit-slider-thumb,
.music input[type="range"]:hover::-moz-range-thumb {
  transform: scale(1.18);
  box-shadow: 0 0 0 4px rgb(var(--ink-rgb) / 12%);
}
.music input[type="range"]:focus-visible::-webkit-slider-thumb,
.music input[type="range"]:focus-visible::-moz-range-thumb {
  transform: scale(1.18);
  box-shadow: 0 0 0 4px rgb(var(--ink-rgb) / 22%);
}
.music input[type="range"]:active::-webkit-slider-thumb,
.music input[type="range"]:active::-moz-range-thumb { transform: scale(1.04); }
.music input[type="range"]:focus { outline: none; }          /* the ring on the thumb is the focus mark */
/* ------------------------------ 6b the music dock ---------------------------
   The player, reduced to its one control and tucked in the corner: a tile that
   pops a slider out. It only exists when there is a track (app.js builds it), it
   rides the player's own styling for the slider, and it goes with the page while
   the intro is up. There is no play button and no seek bar on purpose — the track
   plays on its own and the visitor's say is the level, nothing more. */
.dock {
  position: fixed;
  left: 18px;
  bottom: 18px;
  z-index: 40;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}
.dock__tile { box-shadow: 0 12px 30px -18px rgb(0 0 0 / 90%); }
.dock__panel {
  width: min(232px, calc(100vw - 36px));
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  padding: 12px 13px;
  box-shadow: 0 22px 45px -30px rgb(0 0 0 / 95%);
  animation: dock-in 0.26s var(--ease) both;
}
.dock__panel .music__row { flex: 0 0 auto; }  /* the panel is a column, so no 100% basis */
.dock__panel .music__volume { flex: 1; width: auto; }
.dock__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  color: var(--mid);
}
.dock__icon svg { width: 15px; height: 15px; }
.dock__icon svg * { stroke-width: 1.3px; }
@keyframes dock-in { from { opacity: 0; transform: translateY(8px) scale(0.985); } }

/* the count is live — it ticks up on its own — so the number gets a beat when it moves */
#views-count.is-ticked { animation: views-tick 0.7s var(--ease); }
@keyframes views-tick { from { color: var(--ink); opacity: 0.45; } }

.footer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 11.5px;
  letter-spacing: 0.03em;
  color: var(--mid);
}
.footer__actions { display: inline-flex; gap: 2px; }
.footer .social { width: 28px; height: 28px; border-radius: 7px; }
.footer .social svg { width: 16px; height: 16px; }


/* --------------------------------- 7 studio -------------------------------- */
.fab {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 40;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface2);
  color: var(--mid);
  cursor: pointer;
  transition: color 0.18s ease, border-color 0.18s ease, transform 0.18s var(--ease);
}
.fab svg { width: 17px; height: 17px; }
.fab:hover { color: var(--ink); border-color: var(--line2); transform: rotate(35deg); }

.scrim {
  position: fixed;
  inset: 0;
  z-index: 45;
  background: rgb(0 0 0 / 55%);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}
.scrim.is-open { opacity: 1; visibility: visible; }

.panel {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 50;
  width: min(var(--panel-w), 100%);
  height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--shade2);
  border-left: 1px solid var(--line);
  transform: translateX(101%);
  visibility: hidden;
  transition: transform 0.35s var(--ease), visibility 0.35s ease;
}
.panel.is-open { transform: none; visibility: visible; }

.panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px 18px 11px;
}
.panel__title { margin: 0; font-size: 13.5px; font-weight: 600; letter-spacing: 0.01em; }
.panel__close {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: none;
  color: var(--mid);
  cursor: pointer;
}
.panel__close:hover { color: var(--ink); background: var(--surface); }
.panel__close svg { width: 14px; height: 14px; }

.panel__body {
  flex: 1;
  overflow-y: auto;
  padding: 0 18px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.block { display: flex; flex-direction: column; gap: 10px; }
.block__title {
  margin: 0;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--mid);
}
.block__note {
  margin: -2px 0 0;
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--mid);
}
.block__note code,
.drops code {
  padding: 1px 4px;
  border-radius: 5px;
  background: var(--surface);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10.5px;
  color: var(--soft);
}
/* the files you can drop into assets/, listed where you'd look for them */
.drops {
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.drops li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.drops li span {
  flex: none;
  max-width: 46%;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--mid);
  text-align: right;
}

.swatches { display: flex; flex-wrap: wrap; gap: 7px; }
.swatch {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line2);
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.18s var(--ease);
}
.swatch:hover { transform: scale(1.06); }
.swatch[aria-checked="true"] { box-shadow: 0 0 0 2px var(--shade2), 0 0 0 3px var(--ink); }

.row2 { display: flex; gap: 10px; }
.field { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--mid); }
.field--color { flex: 1; }
.field__label { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.field__label b { font-weight: 500; color: var(--ink); font-variant-numeric: tabular-nums; }

.field input[type="color"] {
  width: 100%;
  height: 28px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  cursor: pointer;
}
.field select,
.field input[type="url"],
.field input[type="text"] {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 12.5px;
}

.sliders { display: flex; flex-direction: column; gap: 13px; }
.sliders input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 3px;
  border-radius: 2px;
  background: var(--surface2);
  cursor: pointer;
}
.sliders input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border: 0;
  border-radius: 50%;
  background: var(--ink);
}
.sliders input[type="range"]::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: 0;
  border-radius: 50%;
  background: var(--ink);
}

.switches { display: flex; flex-direction: column; }
.switch {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 7px 0;
  border: 0;
  background: none;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.switch__track {
  flex: none;
  position: relative;
  width: 30px;
  height: 18px;
  border: 1px solid var(--line2);
  border-radius: 999px;
  background: var(--surface2);
  transition: background 0.2s ease;
}
.switch__track i {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--mid);
  transition: transform 0.2s var(--ease), background 0.2s ease;
}
.switch[aria-checked="true"] .switch__track { background: var(--ink); border-color: var(--ink); }
.switch[aria-checked="true"] .switch__track i { transform: translateX(12px); background: var(--shade); }

.panel__foot { padding: 13px 18px 18px; border-top: 1px solid var(--line); }
.panel__actions { display: flex; gap: 8px; }
.btn {
  flex: 1;
  padding: 9px 10px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  font-size: 12.5px;
  cursor: pointer;
  transition: background 0.18s ease, opacity 0.18s ease;
}
.btn:hover { background: var(--surface2); }
.btn--primary { background: var(--ink); border-color: var(--ink); color: var(--shade); font-weight: 500; }
.btn--primary:hover { opacity: 0.88; }
.panel__hint { margin: 10px 0 0; font-size: 11px; line-height: 1.5; color: var(--mid); }
.panel__hint code { font-size: 10.5px; }


/* ------------------------ 8 boot, intro screen & toast ---------------------
   `.boot` is the flat black curtain that hides the page until app.js has built
   it. The intro sits on top of that: the page itself, blurred, with the wordmark
   on it. Its markup is the first thing in <body> (so it is painted before any
   script runs), the look and the timings are here, and assets/js/intro.js
   decides when it leaves. The letters live in one place — the `data-text`
   attribute — so the sweep and the slices always match the visible word. */
.boot { position: fixed; inset: 0; z-index: 70; background: var(--shade); }
.boot span {
  position: absolute;
  top: 0;
  left: 0;
  width: 32%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--ink), transparent);
  animation: boot-run 1.2s ease-in-out infinite;
}
@keyframes boot-run { from { transform: translateX(-100%); } to { transform: translateX(320%); } }

#intro {
  --intro-blur: 110px;
  --intro-veil: 76%;
  opacity: 1;
  /* No JavaScript — or intro.js never arrives? The overlay expires on its own
     rather than holding the page hostage for ever. (The <noscript> block in
     index.html hides it outright when scripting is off.) */
  animation: intro-expire 0.7s var(--ease) 9s forwards;
}
/* the blurred page: --intro-veil is how solid the black cover is, so the video or
   aurora behind stays alive and soft instead of being replaced by a colour. Both
   numbers are deliberately heavy — a blur this size smears everything back there
   into a wash, and the tint is what stops the smear from still reading as words,
   because a soft edge is unmistakably an edge however far it is spread. The extra
   saturation keeps the colours from going muddy once they are smeared that far. */
.intro__cover {
  background: rgb(var(--shade-rgb) / var(--intro-veil));
  backdrop-filter: blur(var(--intro-blur)) saturate(160%);
  -webkit-backdrop-filter: blur(var(--intro-blur)) saturate(160%);
}
/* Tempered glass rather than a hole: a breath of light across the top of the pane
   and the faintest lift over all of it. Deliberately tiny — the veil is what stops
   the page behind reading as words, and this must not undo that. */
.intro__cover::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgb(var(--ink-rgb) / 6%), transparent 30%),
    radial-gradient(130% 90% at 50% -10%, rgb(var(--ink-rgb) / 4%), transparent 62%);
}
/* The landing screen's own video — `intro.video` in config.js, or a video/intro.*
   dropped next to index.html. It sits above the cover and below the glow, so the
   blur behind it never touches it; intro.js dims it to `intro.videoDim` and stops
   it with the dissolve. Optional: without one, this is simply not there. */
.intro__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}
.intro__glow {
  background: radial-gradient(36% 28% at 50% 50%, rgb(var(--ink-rgb) / 8%), transparent 72%);
  animation: intro-glow 1.5s var(--ease) both;
}
/* The wordmark is its own layer and stays unblurred: the entrance is a plain fade
   so the letters are crisp from the first frame, and the typewriter in
   assets/js/intro.js does the rest. */
.intro__word {
  color: var(--ink);
  text-shadow: 0 0 28px rgb(var(--ink-rgb) / 12%);
  animation: intro-word 0.5s var(--ease) both;
}
@keyframes intro-word { from { opacity: 0; transform: scale(0.985); } }
/* The letters stay full ink from the first one to the last: they are the point, so
   nothing dims them. That also means a brighter band across them would show
   nothing — white on white — so the gliss reads through the bloom below and the
   sweep of shade, not through the letter colour. */
@keyframes intro-bloom {
  0% { text-shadow: 0 0 0 rgb(var(--ink-rgb) / 0%); }
  42% { text-shadow: 0 0 34px rgb(var(--ink-rgb) / 58%), 0 0 92px rgb(var(--ink-rgb) / 20%); }
  100% { text-shadow: 0 0 28px rgb(var(--ink-rgb) / 12%); }
}

/* the typewriter's cursor: blinks while the letters arrive, then fades away */
.intro__caret {
  display: block;
  width: 0.09em;
  min-width: 2px;
  height: 1.04em;
  margin-left: 0.1em;
  border-radius: 1px;
  background: var(--ink);
  animation: intro-caret 1.06s steps(1, end) infinite;
}
@keyframes intro-caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
#intro.is-typed .intro__caret {
  animation: intro-caret-off 0.42s var(--ease) 0.5s both;
  opacity: 1;
}
@keyframes intro-caret-off { to { opacity: 0; } }
.intro__word::before,
.intro__word::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* the gliss waits for the last letter: intro.js adds `is-typed` when the typing
   is done, and the sweep and the slices run from there */
.intro__word::after { opacity: 0; }              /* the slices are the hidden layer */
#intro.is-typed .intro__word { animation: intro-bloom 0.9s var(--ease) both; }
#intro.is-typed .intro__word::before { animation: intro-sheen 0.84s var(--ease) both; }
#intro.is-typed .intro__word::after { animation: intro-slices 0.36s steps(1, end) both; }
/* the gliss, half one: the edge of the light travelling across the letters, which
   on full-ink text reads as a sweep of shade (theme-aware: it darkens dark ink and
   lifts light ink) */
.intro__word::before {
  background: linear-gradient(100deg, transparent 42%, rgb(var(--shade-rgb) / 30%) 50%, transparent 58%) 0 0 / 240% 100% no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
/* config.js → intro.gliss picks one half of the effect. The attribute is in the
   markup and only changed when a page asks for something else. */
#intro[data-gliss="sweep"] .intro__word::after,
#intro[data-gliss="slices"] .intro__word::before { animation: none; opacity: 0; }

@keyframes intro-glow { from { opacity: 0; } }
@keyframes intro-sheen { from { background-position: 190% 0; } to { background-position: -170% 0; } }
@keyframes intro-slices {
  0%, 100% { opacity: 0; clip-path: inset(0 0 100% 0); transform: translateX(0); }
  14% { opacity: 0.85; clip-path: inset(44% 0 44% 0); transform: translateX(-4px); }
  30% { opacity: 0; }
  46% { opacity: 0.7; clip-path: inset(16% 0 70% 0); transform: translateX(3px); }
  62% { opacity: 0; }
  74% { opacity: 0.55; clip-path: inset(66% 0 20% 0); transform: translateX(-2px); }
  88% { opacity: 0; }
}
@keyframes intro-expire { to { opacity: 0; visibility: hidden; } }

/* The dissolve, added by intro.js once the wordmark, the boot and `hold` have
   all lined up; the node leaves the DOM a frame after the fade. */
#intro.is-out {
  opacity: 0;
  filter: blur(12px);
  transform: scale(1.05);
  transition:
    opacity 0.66s var(--ease),
    filter 0.66s var(--ease),
    transform 0.66s var(--ease);
  pointer-events: none;
}
/* `animation: none` is what lets the word itself fade: the entrance animation
   holds its end state with `fill: both`, and an animation outranks a plain
   declaration, so the transition below would otherwise never be seen. */
#intro.is-out .intro__word {
  animation: none;
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 0.5s ease, transform 0.5s var(--ease);
}

/* Nothing is being typed for someone who asked for less motion, so no cursor. */
@media (prefers-reduced-motion: reduce) {
  .intro__caret { display: none; }
}

.toast {
  position: fixed;
  left: 50%;
  bottom: 20px;
  z-index: 80;
  max-width: min(92vw, 340px);
  padding: 8px 14px;
  border: 1px solid var(--line2);
  border-radius: 999px;
  background: rgb(var(--shade-rgb) / 88%);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-size: 12.5px;
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 8px);
  transition: opacity 0.25s ease, transform 0.25s var(--ease), visibility 0.25s ease;
}
.toast.is-on { opacity: 1; visibility: visible; transform: translate(-50%, 0); }

[data-tabs="on"] .shell:hover { border-color: var(--line2); }

/* ----------------------------- 9 inverted tone ---------------------------- */
[data-tone="light"] { color-scheme: light; }
[data-tone="light"] .scrim { background: rgb(0 0 0 / 25%); }
[data-tone="light"] .bg__veil { background: #fff; }
[data-tone="light"] .toast { background: rgb(255 255 255 / 90%); }
[data-tone="light"] .swatch[aria-checked="true"] { box-shadow: 0 0 0 2px var(--shade2), 0 0 0 3px var(--ink); }

/* ------------------------- 11 named buttons (`links`) --------------------- */
/* Icon row for places with a logo, labelled rows for the rest — a shop, an
   account, anything people need to read. Same card language as everything
   else, so both share the layout. */
.links {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  margin-top: 2px;
}
.linkrow {
  position: relative;
  overflow: hidden;                /* keeps the tap ripple inside the row */
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 13px;
  border: 1px solid var(--edge);
  border-radius: calc(var(--radius) + 2px);
  background: linear-gradient(180deg, rgb(var(--ink-rgb) / 6%), var(--surface));
  box-shadow: inset 0 1px 0 var(--sheen);
  font-size: 13.5px;
  transition:
    border-color 0.22s ease,
    background 0.22s ease,
    transform 0.34s var(--spring);
}
.linkrow__icon { display: flex; color: var(--glyph); transition: color 0.22s ease; }
.linkrow__icon svg { width: 20px; height: 20px; }
.linkrow__label {
  flex: 1;
  min-width: 0;
  text-align: left;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.linkrow__go { display: flex; color: var(--soft); transition: transform 0.34s var(--spring), color 0.22s ease; }
.linkrow__go svg { width: 17px; height: 17px; }
.linkrow:hover { border-color: var(--line2); background: var(--surface2); transform: translateY(-2px); }
.linkrow:hover .linkrow__icon,
.linkrow:hover .linkrow__go { color: var(--ink); }
.linkrow:hover .linkrow__go { transform: translate(2px, -2px); }
.linkrow:active { transform: translateY(0) scale(0.99); }

/* -------------------- 10 small screens & reduced motion ------------------- */
@media (max-width: 460px) {
  :root { --page: min(100% - 1.5rem, 420px); }
  .panel { width: 100%; }
  .name { font-size: 20px; }
  .avatar { width: 76px; height: 76px; }
  .social { width: 38px; height: 38px; }
  .shell__cover { height: 84px; }
  [data-cover="on"] .profile { margin-top: -38px; }
  [data-cover="on"] .avatar { box-shadow: 0 0 0 3px var(--shade2); }
  [data-cover="on"][data-ring="on"] .avatar {
    box-shadow: 0 0 0 3px var(--shade2), 0 0 0 4px var(--line2);
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  [data-motion="on"] .reveal { animation: none; }
  /* no leaning, no depth: the card stays flat and still */
  [data-tilt="on"] .shell,
  [data-tilt="on"] .shell .avatar,
  [data-tilt="on"] .shell .socials,
  [data-tilt="on"] .shell .name-row,
  [data-tilt="on"] .shell .handle,
  [data-tilt="on"] .shell .bio,
  [data-tilt="on"] .shell .meta { transform: none; }
}

