/* ---- fonts -------------------------------------------------------------
   Inter, self-hosted. Fetching it from Google held the first paint back by
   over a second on a phone; from here it's one preloaded file. Variable, so
   400 and 500 come out of the same one. ---------------------------------- */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/inter-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url(fonts/inter-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---- tokens ------------------------------------------------------------ */
:root,
html[data-theme="light"] {
  --page:        #ffffff;
  --text-strong: #0e0e0e;
  --text:        #545454;
  --text-muted:  #767676;  /* 4.54:1 on white — AA at 14px */
  --hover:       #f6f6f6;
  --rule:        rgba(0, 0, 0, 0.07);

  /* Sticker elevation. Light mode keeps these low - a white page shows
     every percent, and heavy black under a white card reads as dirt. */
  --sticker-shadow-near: 0.10;
  --sticker-shadow-far:  0.08;
  --sticker-shadow-lift: 0.16;

  /* type scale */
  --font:    13.5px;
  --lh:      21px;
  --font-sm: 12px;

  /* Held at the old scale: cards and the nav links stay put */
  --font-fixed:    12.5px;
  --font-fixed-sm: 11px;
}
html[data-theme="dark"] {
  --page:        #0e0e0e;
  --text-strong: #ffffff;
  --text:        #b0b0b0;
  --text-muted:  #7c7c7c;  /* 4.62:1 on #0e0e0e — AA at 14px */
  --hover:       #171717;
  --rule:        rgba(255, 255, 255, 0.07);

  /* On #0e0e0e a 10% shadow is invisible; these carry the separation. */
  --sticker-shadow-near: 0.55;
  --sticker-shadow-far:  0.45;
  --sticker-shadow-lift: 0.55;
}


/* ======================================================================
   Craft surface + elevation tokens
   From the craft-design-system skill. `--card`, `--muted` and
   `--custom-shadow` were being referenced by the writing demos without ever
   being declared, so those cards rendered transparent and flat. Declared
   here once, for both themes.

   The elevation rule: a card has no border. Light gets a 0.5px hairline ring
   doing the border's job plus two real drops. Dark would swallow a border
   entirely, so it takes inset white highlights catching the top edge and
   four stacked drops for depth.
   ====================================================================== */
:root,
html[data-theme="light"] {
  --card:            oklch(1 0 0);
  --card-foreground: oklch(0.205 0 0);
  --muted:           oklch(0.96 0 0);
  --muted-foreground:oklch(0.556 0 0);
  --border-token:    oklch(0.922 0 0);
  --custom-shadow:
    0px 0px 0px 0.5px rgba(0, 0, 0, 0.06),
    0px 0.5px 2px -0.5px rgba(0, 0, 0, 0.06),
    0px 1px 4px 0px rgba(0, 0, 0, 0.04);
}
html[data-theme="dark"] {
  /* Cards lift off the page by getting lighter, never darker. */
  --card:            oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --muted:           oklch(0.269 0 0);
  --muted-foreground:oklch(0.708 0 0);
  /* White at low alpha, so it picks up whatever sits behind it. */
  --border-token:    oklch(1 0 0 / 10%);
  --custom-shadow:
    inset 0 0.75px 0 0 rgba(255, 255, 255, 0.03),
    inset 0 0 0 0.75px rgba(255, 255, 255, 0.03),
    0 0 0 0.75px rgba(0, 0, 0, 0.1),
    0 2px 2px 0 rgba(0, 0, 0, 0.1),
    0 4px 4px 0 rgba(0, 0, 0, 0.1),
    0 8px 8px 0 rgba(0, 0, 0, 0.1);
}

html[data-theme="dark"]  { color-scheme: dark; }
html[data-theme="light"] { color-scheme: light; }

/* ---- base -------------------------------------------------------------- */
/* The reset sits in a layer so it loses to any library that ships its CSS
   layered (Yöte does). Unlayered, it beat their padding and flattened the
   fields; everything else on the site is unlayered and wins as before. */
@layer reset;
@layer reset {
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
}

/* Any class that sets `display` outranks the UA sheet's `[hidden]` rule, so
   `hidden` silently stopped working on flex/grid sections. This restores the
   attribute's meaning everywhere. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; scrollbar-width: none; }
/* No native scrollbar anywhere, the page included */
html::-webkit-scrollbar { display: none; }

body {
  min-height: 100dvh;
  background: var(--page);
  color: var(--text);
  font-family: Inter, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  letter-spacing: 0.01em;
  -webkit-font-smoothing: antialiased;
  /* Theme swaps shouldn't strobe */
  transition: background-color 0.3s ease, color 0.3s ease;
}

::selection { background: var(--text-strong); color: var(--page); }

a { color: inherit; }

/* Images carry their real size so the page doesn't jump as they load; the
   attribute sets the ratio, CSS still decides the size */
img[height] { height: auto; }

/* ---- layout ------------------------------------------------------------ */
.content {
  width: 540px;
  max-width: calc(100% - 48px);
  margin: 0 auto;
  padding: 88px 0 72px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 36px;
}

.avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
}

/* ---- intro ------------------------------------------------------------- */
.intro {
  display: flex;
  flex-direction: column;
  gap: 18px;
  width: 100%;
}
.greeting { color: var(--text-muted); }
.intro em {
  font-style: normal;
  color: var(--text-strong);
}
.intro a,
.connect a,
.study-content a,
.cv-contact a {
  color: var(--text-strong);
  /* A real underline, faint until you reach it: links in running text
     can't rely on colour alone */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
  transition: text-decoration-color 0.18s ease;
}
.intro a:hover,
.connect a:hover,
.study-content a:hover,
.cv-contact a:hover,
.intro a:focus-visible,
.connect a:focus-visible,
.study-content a:focus-visible,
.cv-contact a:focus-visible {
  text-decoration-color: currentColor;
}

/* ---- link lists -------------------------------------------------------- */
.link-section {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  width: 100%;
}
.link-section h2 {
  color: var(--text-muted);
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  margin-bottom: 8px;
}

.link-row {
  display: flex;
  flex-direction: column;
  /* no wash: rows sit flush with the copy above them */
  /* Negative margin keeps the wash off the text column's alignment */
  padding: 7px 10px;
  margin: 0 -10px;
  border-radius: 6px;
  text-decoration: none;
  background: transparent;
  transition: background-color 0.2s ease;
}
.link-row:hover,
.link-row:focus-visible { outline: none; }
.link-row:hover .row-name,
.link-row:focus-visible .row-name { color: var(--text-strong); }
.link-row:hover .row-meta,
.link-row:focus-visible .row-meta { color: var(--text); }
.row-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 2px 16px;
  width: 100%;
}
.row-name {
  color: var(--text-strong);
  min-width: 0;
}
.row-meta {
  color: var(--text-muted);
  white-space: nowrap;
  transition: color 0.2s ease;
}
.link-row:hover .row-meta { color: var(--text); }

/* ---- connect + footer -------------------------------------------------- */
.connect { gap: 0; }
.connect p { max-width: 100%; }

footer {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  width: 100%;
  padding-top: 8px;
  color: var(--text-muted);
}
.clock { white-space: nowrap; }
.clock .tz { margin-right: 6px; }
#clock { font-variant-numeric: tabular-nums; }

/* ---- theme toggle ------------------------------------------------------ */
.theme-toggle {
  position: fixed;
  top: max(20px, env(safe-area-inset-top, 0px));
  right: max(20px, env(safe-area-inset-right, 0px));
  z-index: 10;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  opacity: 0.55;
  cursor: pointer;
  transition: opacity 0.2s ease, color 0.2s ease, transform 0.14s cubic-bezier(0.22, 1, 0.36, 1);
}
.theme-toggle:hover,
.theme-toggle:focus-visible {
  opacity: 1;
  color: var(--text-strong);
  outline: none;
}
.theme-toggle:active { transform: scale(0.92); }
.theme-toggle svg {
  width: 16px;
  height: 16px;
  display: block;
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
html[data-theme="dark"] .theme-toggle svg { transform: rotate(180deg); }

/* ---- entrance ---------------------------------------------------------- */
.appear {
  opacity: 0;
  animation: appear 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: calc(var(--i, 0) * 70ms);
}
@keyframes appear {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
/* What a visitor reads first is painted on the first frame and only rises
   into place: holding it at opacity 0 made the page count as blank for the
   length of the stagger. Everything after it still fades in. */
.intro.appear:first-of-type,
.study-title.appear,
.study-head.appear,
.cv-head.appear,
.cv-contact.appear,
.cv-section.appear {
  opacity: 1;
  animation-name: rise;
}
@keyframes rise {
  from { transform: translateY(8px); }
  to   { transform: none; }
}

/* ---- narrow ------------------------------------------------------------ */
@media (max-width: 600px) {
  .content { padding: 64px 0 56px; gap: 32px; }
  footer { flex-direction: column; align-items: flex-start; gap: 6px; }
}

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

/* ======================================================================
   Nav — avatar, signature, links
   ====================================================================== */
.nav {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.brand { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }

/* Ring, carried over from v4 */
.avatar-wrap {
  position: relative;
  flex-shrink: 0;
  width: 46px;
  height: 46px;
  padding: 3px;
  border: none;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  overflow: hidden;
  transition: background 0.2s ease;
}
.avatar-wrap:hover {
  background: conic-gradient(from 135deg, #0F6DFF, #00C6FF, #0040FF, #006FFF, #0F6DFF);
  outline: none;
}
/* Gap between ring and photo — picks up the page colour in either theme */
.avatar-wrap::after {
  content: '';
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  background: var(--page);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.avatar-wrap:hover::after { opacity: 1; }

.avatar-wrap:hover::before {
  animation: none;
  background-position: -200% 0;
  transition: background-position 0.5s ease;
}
.avatar {
  position: relative;
  z-index: 1;
  display: block;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
}

.sig {
  /* Framed to his numbers, not guessed. His ink is 69x44.6 inside a 90x90
     box: it fills 49.6% of the height and his stroke is 0.0222 of the box
     side. Our viewBox is padded to the same fractions, so the mark renders
     26.8px tall with a 1.20px stroke - identical to his beside a 40px
     avatar. Filling the box edge to edge is what made it look oversized. */
  width: 54px;
  height: 54px;
  display: block;
  flex-shrink: 0;
  color: var(--text-strong);
  /* Clockwise, i.e. to the right. The earlier attempt used -6deg, which is
     counter-clockwise - it tipped the mark the wrong way. */
  transform: rotate(8deg);
  transform-origin: 50% 60%;
  transition: color 0.3s ease;
}
/* Every stroke carries pathLength="1", so one dash unit is the whole stroke
   whatever its real length. Offset 1 is undrawn, 0 is drawn. */
.sig-path { stroke-dasharray: 1; stroke-dashoffset: 1; }

.nav-links { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.nav-links a {
  color: var(--text-muted);
  font-size: var(--font-fixed);
  text-decoration: none;
  white-space: nowrap;
  padding: 6px 8px;
  border-radius: 6px;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.nav-links a:hover,
.nav-links a:focus-visible {
  color: var(--text-strong);
  outline: none;
}

/* ======================================================================
   Now playing
   ====================================================================== */
/* The now-playing row and its vinyl were removed with the markup:
   they showed the previous owner's Spotify history. */

/* ======================================================================
   Lightbox — constrained, not full-bleed
   ====================================================================== */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--page) 78%, transparent);
  backdrop-filter: blur(20px) saturate(1.1);
  -webkit-backdrop-filter: blur(20px) saturate(1.1);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.lightbox.is-open { opacity: 1; }

/* ======================================================================
   Back link
   ====================================================================== */
/* A single back link instead of a breadcrumb trail. The trail restated the
   page title the <h1> was about to say anyway.

   It lives in the left margin, level with the avatar, rather than in the
   column above the title: sitting in the flow it read as the first line of
   the page, which is exactly what it is not. Absolute against .brand so it
   costs the column no height and never shifts the heading.

   The arrow nudges left on hover, which is the only motion it needs. */
.brand { position: relative; }

.back {
  position: absolute;
  /* Clear of the column entirely, out in the margin, so it reads as page
     chrome rather than the first line of the article. */
  right: calc(100% + 64px);
  /* Stretched top-to-bottom and centred by flex instead of translateY: .back
     also carries .appear, whose entrance animation writes `transform`, and a
     translate here loses to it. That is why it sat 11px low. */
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  color: var(--text-muted);
  text-decoration: none;
  transition: color 0.15s ease;
}
.back:hover, .back:focus-visible { color: var(--text-strong); outline: none; }
.back-arrow {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  transition: transform 0.2s var(--ease-snappy, cubic-bezier(0.23, 1, 0.32, 1));
}
.back:hover .back-arrow, .back:focus-visible .back-arrow { transform: translateX(-2px); }

/* Below this width the margin is too thin to hold it, so it rejoins the row
   and sits inline ahead of the avatar instead of hanging off the page. */
/* Below this the margin cannot hold it, so it rejoins the row inline ahead
   of the avatar rather than hanging off the page. */
@media (max-width: 1040px) {
  .back {
    position: static;
    margin-right: 10px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .back, .back-arrow { transition: none; }
  .back:hover .back-arrow { transform: none; }
}

.section-label {
  color: var(--text-muted);
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  margin-bottom: -6px;
}
.movie {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 136px;
  flex-shrink: 0;
  text-decoration: none;
}
.movie img {
  width: 136px;
  height: 196px;
  object-fit: cover;
  border-radius: 6px;
  display: block;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.movie:hover img { transform: translateY(-4px); }
.movie-title { color: var(--text-muted); font-size: var(--font-sm); line-height: 1.35; transition: color 0.15s ease; }
.movie:hover .movie-title { color: var(--text-strong); }

@media (max-width: 600px) {
  .nav { flex-direction: column; align-items: flex-start; gap: 12px; }
  .nav-links { margin-left: -8px; }
}


/* ======================================================================
   Greeting — rotates through languages, click to advance
   ====================================================================== */
.greeting {
  display: inline-block;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  font: inherit;
  letter-spacing: inherit;
  text-align: left;
  cursor: pointer;
  transition: color 0.15s ease;
}
.greeting:hover, .greeting:focus-visible { color: var(--text-strong); outline: none; }
.greeting-text {
  display: inline-block;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.greeting-text.is-out { opacity: 0; transform: translateY(-3px); }

/* ======================================================================
   Chrome cluster — audio + theme
   ====================================================================== */
.chrome {
  position: fixed;
  top: max(20px, env(safe-area-inset-top, 0px));
  right: max(20px, env(safe-area-inset-right, 0px));
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 4px;
}
.chrome .theme-toggle { position: static; top: auto; right: auto; }

.audio-toggle,
.sticker-reset {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  opacity: 0.55;
  cursor: pointer;
  transition: opacity 0.2s ease, color 0.2s ease, transform 0.14s cubic-bezier(0.22, 1, 0.36, 1);
}
.audio-toggle:hover, .audio-toggle:focus-visible,
.sticker-reset:not(:disabled):hover, .sticker-reset:focus-visible { opacity: 1; color: var(--text-strong); outline: none; }
.audio-toggle:active, .sticker-reset:not(:disabled):active { transform: scale(0.92); }
.audio-toggle svg, .sticker-reset svg { width: 16px; height: 16px; display: block; }
/* Nothing to put back until something has moved */
.sticker-reset:disabled { opacity: 0.22; cursor: default; }
/* its display:grid would otherwise beat the hidden attribute on a phone */
.sticker-reset[hidden] { display: none; }
.audio-toggle .ico-on  { display: none; }
.audio-toggle .ico-off { display: block; }
.audio-toggle[aria-pressed="true"] .ico-on  { display: block; }
.audio-toggle[aria-pressed="true"] .ico-off { display: none; }

@media (prefers-reduced-motion: reduce) {
}


/* ======================================================================
   Project rows — the icon is the hover indicator
   ====================================================================== */
/* Names start flush with the section labels; the mark hangs outside the
   column in the left margin, so nothing indents and nothing shifts */
.row-head { position: relative; }

.row-mark {
  position: absolute;
  left: -26px;
  top: 0;
  width: 16px;
  height: var(--lh);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 0.22s ease, transform 0.28s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.row-mark img {
  width: 16px;
  height: 16px;
  object-fit: contain;
  display: block;
}
.is-project:hover .row-mark,
.is-project:focus-visible .row-mark { opacity: 1; transform: none; }

/* Avatar keeps its ring while the strip is open */
.avatar-wrap.is-active {
  background: conic-gradient(from 135deg, #0F6DFF, #00C6FF, #0040FF, #006FFF, #0F6DFF);
}
.avatar-wrap.is-active::after { opacity: 1; }

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

/* The left margin runs out on narrow screens, and there's no hover there
   anyway, so the mark simply doesn't appear */
@media (max-width: 640px) {
  .row-mark { display: none; }
}


/* ======================================================================
   Project cards - layered oklch gradients + grain
   Method from jakub.kr/work/gradients: stack a couple of soft radial
   lights over a linear body and blend them, interpolating `in oklch` so
   the hue travels angularly and picks up tones between the stops.
   Each card's three hues are its brand hue plus two neighbours.
   ====================================================================== */
.cards-section { width: 100%; }

.cards {
  display: flex;
  gap: 14px;
  width: 100vw;
  max-width: 100vw;
  margin-left: calc(50% - 50vw);
  padding-inline: max(24px, calc(50vw - 270px));
  padding-block: 4px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.cards::-webkit-scrollbar { display: none; }

.card {
  position: relative;
  flex-shrink: 0;
  width: 208px;
  aspect-ratio: 5 / 6;
  border-radius: 10px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 16px;
  text-decoration: none;
  isolation: isolate;
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.card:hover { transform: translateY(-4px); }

/* Grain. feTurbulence rendered once as a data URI, tiled and blended so it
   sits in the colour rather than on top of it. */
.card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.6;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='5' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}

/* Everything readable sits above the grain */
.card-mark, .card-body { position: relative; z-index: 2; }

.card-mark { width: 26px; height: 26px; display: block; }
.card-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* Marks go monochrome white so five different brand palettes don't
     fight the five different card gradients */
  filter: brightness(0) invert(1);
  opacity: 0.95;
}

.card-body { display: flex; flex-direction: column; gap: 6px; }
.card-name {
  color: #fff;
  font-size: var(--font-fixed);
  font-weight: 500;
  line-height: 1.25;
}
.card-desc {
  color: rgba(255, 255, 255, 0.95);
  font-size: var(--font-fixed-sm);
  line-height: 1.38;
}


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

.cards-label {
  color: var(--text-muted);
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 400;
  margin-bottom: 8px;
}

/* ======================================================================
   Drifting lights
   Registered so the browser can interpolate them; without @property these
   resolve as static values and the gradient simply sits still.
   ====================================================================== */
@property --l1x { syntax: '<percentage>'; inherits: true; initial-value: 76%; }
@property --l1y { syntax: '<percentage>'; inherits: true; initial-value: 6%; }
@property --l2x { syntax: '<percentage>'; inherits: true; initial-value: 8%; }
@property --l2y { syntax: '<percentage>'; inherits: true; initial-value: 30%; }
@property --l3x { syntax: '<percentage>'; inherits: true; initial-value: 52%; }
@property --l3y { syntax: '<percentage>'; inherits: true; initial-value: 22%; }

:root {
  --l1x: 76%; --l1y: 6%;
  --l2x: 8%;  --l2y: 30%;
  --l3x: 52%; --l3y: 22%;
}

/* Three different paths per light. Four stops rather than two, so a light
   traces a loop around the card instead of sliding back and forth along
   the same line. */
@keyframes l1-a {
  0%,100% { --l1x: 78%; --l1y: 8%; }
  25%     { --l1x: 30%; --l1y: 26%; }
  50%     { --l1x: 12%; --l1y: 62%; }
  75%     { --l1x: 58%; --l1y: 40%; }
}
@keyframes l1-b {
  0%,100% { --l1x: 14%; --l1y: 18%; }
  33%     { --l1x: 86%; --l1y: 44%; }
  66%     { --l1x: 46%; --l1y: 66%; }
}
@keyframes l1-c {
  0%,100% { --l1x: 50%; --l1y: 64%; }
  30%     { --l1x: 88%; --l1y: 12%; }
  70%     { --l1x: 18%; --l1y: 34%; }
}

@keyframes l2-a {
  0%,100% { --l2x: 10%; --l2y: 34%; }
  30%     { --l2x: 62%; --l2y: 66%; }
  65%     { --l2x: 90%; --l2y: 16%; }
}
@keyframes l2-b {
  0%,100% { --l2x: 88%; --l2y: 58%; }
  25%     { --l2x: 44%; --l2y: 10%; }
  50%     { --l2x: 6%;  --l2y: 46%; }
  75%     { --l2x: 70%; --l2y: 30%; }
}
@keyframes l2-c {
  0%,100% { --l2x: 34%; --l2y: 12%; }
  40%     { --l2x: 8%;  --l2y: 60%; }
  75%     { --l2x: 76%; --l2y: 52%; }
}

@keyframes l3-a {
  0%,100% { --l3x: 54%; --l3y: 20%; }
  35%     { --l3x: 16%; --l3y: 52%; }
  70%     { --l3x: 84%; --l3y: 62%; }
}
@keyframes l3-b {
  0%,100% { --l3x: 20%; --l3y: 62%; }
  25%     { --l3x: 72%; --l3y: 24%; }
  55%     { --l3x: 92%; --l3y: 54%; }
  80%     { --l3x: 40%; --l3y: 6%; }
}
@keyframes l3-c {
  0%,100% { --l3x: 90%; --l3y: 36%; }
  45%     { --l3x: 24%; --l3y: 14%; }
  75%     { --l3x: 56%; --l3y: 68%; }
}

/* Each card gets its own combination of paths, its own three periods, and a
   negative delay so it starts mid-loop. Nothing shares a period with
   anything else, and none of the periods divide evenly into another, so the
   three lights on one card never resynchronise and no two cards match. */
.card {
  animation:
    var(--a1, l1-a) var(--d1, 19s) ease-in-out var(--o1, 0s) infinite,
    var(--a2, l2-a) var(--d2, 23s) ease-in-out var(--o2, 0s) infinite,
    var(--a3, l3-a) var(--d3, 29s) ease-in-out var(--o3, 0s) infinite;
}


@media (prefers-reduced-motion: reduce) {
  .card { animation: none; }
}



/* Greeting stays lowercase whatever language it lands on, without
   mangling the source text or its lang attribute */
.greeting-text { text-transform: lowercase; }

/* ======================================================================
   Case study pages
   Same shell as the homepage. The hero sits the real product screenshot
   on that project's own gradient, so the page opens on the work itself.
   ====================================================================== */


.study-head { display: flex; flex-direction: column; gap: 12px; width: 100%; }

.study-hero {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 10px;
  overflow: hidden;
  display: grid;
  place-items: center;
  padding: 26px 26px 0;
}
.study-hero img,
.study-hero video {
  width: 100%;
  max-height: 100%;
  object-fit: cover;
  object-position: top center;
  border-radius: 6px 6px 0 0;
  box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.45);
  display: block;
}

.study-pills { display: flex; gap: 4px; flex-wrap: wrap; }
.study-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 8px;
  background: var(--hover);
  color: var(--text-strong);
  font-size: var(--font-sm);
  line-height: 1.4;
  text-decoration: none;
  white-space: nowrap;
}
a.study-pill { padding-left: 6px; }
a.study-pill:hover { background: color-mix(in srgb, var(--hover) 68%, var(--text-muted)); }
.study-pill-mark { width: 14px; height: 14px; flex-shrink: 0; }
.study-pill-mark img { width: 100%; height: 100%; object-fit: contain; display: block; }

.study-section { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.study-label { color: var(--text-muted); font-size: var(--font); line-height: var(--lh); font-weight: 400; }

/* Read by screen readers, not shown */
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.study-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
  color: var(--text-strong);
  max-width: 620px;
}
.study-content ul { padding-left: 20px; display: flex; flex-direction: column; gap: 4px; }


@media (max-width: 600px) {
  .study-hero { aspect-ratio: 4 / 3; padding: 16px 16px 0; }
}

/* Sub-headings inside a study, so a long section reads as parts rather
   than one run of prose */
.study-subhead {
  font-size: var(--font);
  line-height: var(--lh);
  font-weight: 500;
  color: var(--text-strong);
  margin-top: 10px;
}
.study-content > .study-subhead:first-child { margin-top: 0; }

/* A staged figure, as the case studies have it: a neutral platform with one
   or two shots sitting on it. Height is fixed and images are contained, so a
   tall mobile screenshot sits inside the stage instead of running to 1600px
   at full column width. */
.study-figure {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  width: 100%;
  max-width: none;
  height: 300px;
  margin: 6px 0;
  padding: 18px;
  background: var(--hover);
  border-radius: 8px;
  overflow: hidden;
}
.study-figure img,
.study-figure video {
  height: 100%;
  width: auto;
  max-width: 100%;
  object-fit: contain;
  border-radius: 4px;
  display: block;
}
/* A lone wide shot can use the full stage rather than being boxed in */
.study-figure[data-n="1"] img,
.study-figure[data-n="1"] video { max-width: 100%; }

@media (max-width: 600px) {
  .study-figure { height: 220px; padding: 12px; gap: 10px; }
}

/* SVG feature panels are drawn on white, so the grey screenshot stage shows
   as a halo around them */
.study-figure:has(img[src$=".svg"]) { background: transparent; padding: 0; height: auto; align-items: stretch; }
/* Panels are shown whole at their own aspect: a fixed-height stage cropped
   them mid-word, and side by side each takes an equal share of the row */
.study-figure:has(img[src$=".svg"]) img { border-radius: 8px; flex: 1 1 0; min-width: 0; width: 100%; height: auto; }

/* ======================================================================
   Study header
   A study used to open on a screenshot: no name, no one-liner, no role or
   year above the fold, and the site link arriving small and late. This puts
   all four first, which also means the intro no longer has to explain what
   the thing is before the voice can start.
   ====================================================================== */
.study-title { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.study-title h1 {
  font-size: 26px;
  line-height: 1.2;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text-strong);
}
.study-lede {
  max-width: 560px;
  color: var(--text);
}
.study-title .study-pills { margin-top: 2px; }

/* A study used to stop dead and drop into the articles block. */
.study-next {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  padding: 16px 0;
  border-top: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
}
.study-next-label { color: var(--text-muted); }
.study-next-name {
  color: var(--text-strong);
  font-weight: 500;
  transition: opacity 0.15s ease;
}
.study-next:hover .study-next-name { opacity: 0.65; }

@media (max-width: 600px) {
  .study-title h1 { font-size: 22px; }
}

/* ======================================================================
   Stickers
   Peeking in from the far edges, loose on the page. The die-cut border
   and the rim shade are drawn once per sticker on a canvas (app.js), so
   the white follows each shape exactly at the screen's pixel density.
   ====================================================================== */
body { position: relative; }

.stickers {
  position: absolute;
  inset: 0;
  z-index: 5;                 /* over the page, under the toggles and the note */
  overflow: hidden;           /* hanging off the edge must never widen the page */
  overflow: clip;
  pointer-events: none;
}

.sticker {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  will-change: transform;
  /* Two shadows: a tight contact shadow where the sticker meets the board,
     and a soft one for how far it sits off it.

     Shadow strength is a token, not a constant. A near-black board swallows
     anything faint, so dark mode needs real weight; the same values on a
     white page read as grime under every sticker. */
  filter: drop-shadow(0 2px 4px rgb(0 0 0 / var(--sticker-shadow-near)))
          drop-shadow(0 12px 22px rgb(0 0 0 / var(--sticker-shadow-far)));
  transition: filter 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
.sticker.is-held {
  cursor: grabbing;
  filter: drop-shadow(0 4px 8px rgb(0 0 0 / var(--sticker-shadow-near)))
          drop-shadow(0 26px 40px rgb(0 0 0 / var(--sticker-shadow-lift)));
}
/* The canvas runs a couple of px past the sticker's own box so the art's
   anti-aliased edge isn't clipped. There is no die-cut border to carry. */
.sticker canvas {
  display: block;
  max-width: none;
  pointer-events: none;
}

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

/* ======================================================================
   Playground (explorations.html)
   A dense mosaic, as the old page had it: two columns of 4:3 tiles that
   break out of the 540px column to 980px, one column on a phone. Tiles
   open in the lightbox (explorations.js), which steps through the lot.
   ====================================================================== */
.explore-sites a.study-pill { padding-left: 10px; }
.explore-sites svg { color: var(--text-muted); flex-shrink: 0; }

.explore-section { width: 100%; }
.explore-grid {
  --explore-w: min(980px, calc(100vw - 32px));
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  width: var(--explore-w);
  /* centred on the column, whatever the column's own width */
  margin-left: calc((100% - var(--explore-w)) / 2);
}
.explore-cell {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: 6px;
  overflow: hidden;
  background: var(--hover);
  isolation: isolate;
}
.explore-open {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: zoom-in;
}
.explore-open:focus-visible { outline: 2px solid var(--text-strong); outline-offset: -2px; }
.explore-open img,
.explore-open video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s cubic-bezier(0.2, 0.88, 0.24, 1);
}
.explore-open:hover img,
.explore-open:hover video { transform: scale(1.02); }

/* Shimmer until the tile's image (or a video's poster) arrives. Only once
   the script is there to take it away again. */
.explore-grid.is-enhanced .explore-cell::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(90deg, transparent 0%,
    color-mix(in srgb, var(--page) 55%, transparent) 50%, transparent 100%);
  background-size: 200% 100%;
  animation: explore-shimmer 1.4s infinite;
  transition: opacity 0.3s ease;
}
.explore-grid.is-enhanced .explore-cell.is-loaded::before { opacity: 0; animation: none; }
@keyframes explore-shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}

@media (max-width: 680px) {
  .explore-grid { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  .explore-grid.is-enhanced .explore-cell::before { animation: none; }
  .explore-open img, .explore-open video { transition: none; }
}

/* ======================================================================
   Writing (articles/*.html)
   The old article hero: a figure on a yellow gradient with four textures
   soft-lit over it. Positions are the original pixel layout (572 x 300)
   as percentages, so it scales with the column instead of cropping.
   ====================================================================== */
.article-hero {
  position: relative;
  width: 100%;
  aspect-ratio: 572 / 300;
  border-radius: 10px;
  overflow: hidden;
  isolation: isolate;
  /* The yellow ramp is gone; the hero sits on the page's own surface so
     the overlays read as artwork rather than a colour block. */
  background: var(--hover);
}
.article-hero img {
  position: absolute;
  max-width: none;
  object-fit: cover;
  display: block;
  pointer-events: none;
  user-select: none;
}
.article-hero-overlay { mix-blend-mode: soft-light; }
.article-hero-a { left: -9.79%;  top: 0;       width: 56.99%; height: 109%; }
.article-hero-b { left: -4.72%;  top: -1.67%;  width: 98.78%; height: 94.33%; }
.article-hero-c { left: 61.54%;  top: -15.33%; width: 56.64%; height: 137.67%; }
.article-hero-d { left: 37.41%;  top: 23%;     width: 69.23%; height: 154%; }
.article-hero-main { left: 16.96%; top: 10%;   width: 78.32%; height: 93.33%; }

/* ======================================================================
   Earlier work
   A role row with one line under it. The note sits on the row's text
   column, and a row with nowhere to go (Volla) keeps its colour still.
   ====================================================================== */
.row-note {
  color: var(--text);
  margin: -2px 0 10px;
}
.row-note:last-child { margin-bottom: 0; }
.link-row-static .row-name { color: var(--text-strong); }

/* ======================================================================
   Resume
   The same page doubles as the PDF: print hides the site around it and
   sets it as a document. resume.pdf is rendered from this page
   (scripts/resume/pdf.mjs), so the two can't drift apart.
   ====================================================================== */
.cv-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; width: 100%; }
.cv-title h1 { font-size: 24px; line-height: 1.2; font-weight: 500; letter-spacing: -0.01em; color: var(--text-strong); }
.cv-sub { margin-top: 4px; color: var(--text-muted); }

.cv-contact { display: flex; flex-wrap: wrap; gap: 4px 8px; color: var(--text-muted); margin-top: -18px; }

.cv-section { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.cv-section .study-label { font-size: var(--font); font-weight: 400; }
.cv-body { display: flex; flex-direction: column; gap: 18px; color: var(--text-strong); }
.cv-item { display: flex; flex-direction: column; gap: 6px; }
.cv-role { display: block; text-decoration: none; color: inherit; }
.cv-role .row-name { font-weight: 500; }
a.cv-role:hover .row-name { box-shadow: inset 0 -1px 0 0 currentColor; }
.cv-item ul { padding-left: 18px; display: flex; flex-direction: column; gap: 4px; color: var(--text); }
.cv-code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.9em; }
.cv-skills { gap: 6px; }
.cv-skills p { color: var(--text); }
.cv-skills span { display: inline-block; min-width: 64px; color: var(--text-muted); }

@media (max-width: 600px) {
  .cv-head { flex-direction: column; align-items: flex-start; }
}

@media print {
  @page { size: A4; margin: 13mm 16mm; }
  html, html[data-theme="dark"] {
    --page: #fff; --text-strong: #111; --text: #3a3a3a; --text-muted: #6b6b6b; --rule: rgba(0,0,0,.12);
    color-scheme: light;
  }
  body { background: #fff; font-size: 9pt; line-height: 1.4; transition: none; }
  .appear { opacity: 1 !important; animation: none !important; transform: none !important; }
  .content { width: auto; max-width: none; padding: 0; gap: 12px; }
  .cv-title h1 { font-size: 20pt; }
  .cv-contact { margin-top: -10px; }
  .cv-contact a { text-decoration: none; }
  .cv-item { break-inside: avoid; }
  .cv-section { gap: 6px; }
  .cv-section .study-label { break-after: avoid; }
  .cv-body { gap: 8px; }
  .cv-item { gap: 3px; }
  .cv-item ul { gap: 1px; }
  .cv-skills { gap: 2px; }
}

/* ======================================================================
   Card to case study
   Each work card on the homepage and the hero on its case study share a
   view-transition-name, so following the card grows it into the hero, and
   going back shrinks the hero into the card. Cross-document, no JS; browsers
   without support just navigate.
   ====================================================================== */
@view-transition { navigation: auto; }
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 0.24s; }

html.vt-in .study-head.appear,
html.vt-in .cards-section.appear { animation: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ======================================================================
   Framed hero
   The case study's cover: the project's mark and name, one line, and the
   product rising from the bottom edge, cut off by the card, on the same
   gradient as its homepage card.
   ====================================================================== */
.study-hero-framed {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding: 6% 0 0;
  text-align: center;
  color: #fff;
}
.study-hero-framed .hero-brand {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 15px;
  line-height: 1;
  font-weight: 500;
  letter-spacing: -0.01em;
}
.study-hero-framed .hero-brand img {
  width: 18px;
  height: 18px;
  object-fit: contain;
  border-radius: 0;
  box-shadow: none;
  filter: brightness(0) invert(1);
}
.study-hero-framed .hero-line {
  position: relative;
  z-index: 2;
  margin-top: 3.2%;
  max-width: 72%;
  font-size: clamp(16px, 3.7vw, 21px);
  line-height: 1.15;
  font-weight: 500;
  letter-spacing: -0.025em;
  text-wrap: balance;
  text-shadow: 0 1px 12px rgb(0 0 0 / 0.12);
}
/* The shot is wider than the space left and runs off the bottom edge */
.study-hero-framed .hero-shot {
  flex: none;
  width: 80%;
  height: auto;
  max-height: none;
  margin-top: 5.5%;
  background: #fff;
  object-fit: cover;
  object-position: top center;
  border-radius: 8px 8px 0 0;
  box-shadow: 0 18px 50px -12px rgb(0 0 0 / 0.45), 0 0 0 1px rgb(255 255 255 / 0.12);
}

@media (max-width: 600px) {
  .study-hero-framed { aspect-ratio: 4 / 3.4; padding: 8% 0 0; }
  .study-hero-framed .hero-brand { font-size: 13px; }
  .study-hero-framed .hero-brand img { width: 15px; height: 15px; }
  .study-hero-framed .hero-shot { width: 86%; margin-top: 6%; }
}

/* ======================================================================
   Case-study reading size
   The site's 13.5px works for a homepage of short lines; a 1,000-word case
   study read on a phone needs more. Labels, pills and captions stay small.
   ====================================================================== */
.study-content { font-size: 15px; line-height: 24px; }
.study-content .study-subhead { font-size: inherit; line-height: inherit; }
.study-lede { font-size: 15px; line-height: 23px; }

/* ======================================================================
   Work row
   The row starts on the text column at every width and runs off the right
   edge. Two step buttons sit beside the label at every width, since the
   scrollbar is hidden and a wheel only scrolls down; they grey out at
   either end, and when a very wide screen fits all five.
   ====================================================================== */
.cards-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.cards-head .cards-label { margin-bottom: 0; }
.cards-nav { display: flex; gap: 2px; margin-right: -6px; }
.cards-nav[hidden] { display: none; }
.cards-step {
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border: none; border-radius: 6px;
  background: transparent;
  color: var(--text-strong);
  cursor: pointer;
  transition: background-color 0.15s ease, opacity 0.15s ease;
}
.cards-step svg { width: 14px; height: 14px; display: block; }
.cards-step:hover:not(:disabled), .cards-step:focus-visible { background: var(--hover); outline: none; }
.cards-step:disabled { opacity: 0.28; cursor: default; }
.cards { scroll-behavior: smooth; scroll-snap-type: x proximity; }
.cards .card { scroll-snap-align: start; scroll-margin-inline-start: max(24px, calc(50vw - 270px)); }
@media (prefers-reduced-motion: reduce) { .cards { scroll-behavior: auto; } }


/* ---- brand block: photo + signature, stacked above the intro ------------ */
/* Hidden, not removed — flip this off to bring the nav links back. */
.nav-links { display: none; }

/* With the links hidden the nav is just the brand, so let it sit hard left
   and give the intro the same top-left origin as the photo. */
.nav { align-items: flex-start; }
.brand { gap: 10px; align-items: center; }

/* A rounded square rather than a circle: at this size a circle crops the
   jaw and shoulders, a squircle keeps the whole head in frame. */
.avatar-wrap {
  width: 48px;
  height: 48px;
  padding: 3px;
  border-radius: 13px;
}
.avatar-wrap::after { inset: 2px; border-radius: 11px; }
.avatar {
  width: 42px;
  height: 42px;
  border-radius: 11px;
}
/* The conic ring follows the new shape. */
.avatar-wrap:hover,
.avatar-wrap:focus-visible { border-radius: 13px; }

/* Signature size lives with the rest of the .sig rule near the nav styles.
   A second declaration down here silently won twice during this rebuild, so
   it is deliberately not repeated. */

/* The reference gives the photo real air before the copy starts. */
.intro { margin-top: -26px; }

/* The wrap carries 3px of ring padding, which pushes the visible photo 3px
   right of the text column. Pull the lockup back so the photo's left edge and
   the copy share one optical margin. */
.brand { margin-left: -3px; }

/* ======================================================================
   Work cards — brand gradients
   Same recipe as the originals: three drifting radials over one linear
   base. The rule those follow, which I missed first time: each card stays
   inside ONE narrow hue band. Eldora runs 205->236, TextCortex 313->258.
   Travelling pink->purple->blue inside a single 208px card is mud.
   ====================================================================== */

/* Eldora UI — cyan, the accent its own docs site uses. One hue, 200-230. */
.card-eldora {
  --vg: 0.22;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.82 0.09 205 / 0.3), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.72 0.143 215 / 0.7), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.55 0.11 232 / 0.66), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.7 0.13 208) 0%, oklch(0.54 0.12 222) 52%, oklch(0.33 0.08 236) 100%);
}
/* TextCortex — restored to the original: exactly the three logo colours,
   nothing else. #FF805F coral, #B74BDD purple, #0379FF blue, as three
   drifting radials over a linear base. Flattening it was wrong; this card
   was the one worth keeping. */
.card-textcortex {
  --vg: 0.24;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.77 0.128 39 / 0.34), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.62 0.203 313 / 0.62), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.58 0.221 258 / 0.6), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.62 0.203 313) 0%, oklch(0.58 0.221 258) 100%);
}
/* Dialux — grey. Near-zero chroma, 0.016-0.03, so it reads as a
   neutral beside the two coloured cards rather than a third hue. */
.card-dialux {
  --vg: 0.3;
  background:
    radial-gradient(ellipse 66% 32% at var(--l3x) var(--l3y), oklch(0.78 0.02 250 / 0.34), transparent 64%),
    radial-gradient(ellipse 96% 52% at var(--l1x) var(--l1y), oklch(0.56 0.022 255 / 0.7), transparent 60%),
    radial-gradient(ellipse 88% 48% at var(--l2x) var(--l2y), oklch(0.42 0.018 258 / 0.66), transparent 58%),
    linear-gradient(in oklch 165deg, oklch(0.54 0.02 252) 0%, oklch(0.39 0.016 258) 52%, oklch(0.25 0.012 262) 100%);
}
/* Drawings — flat, for the same reason. One value from the illustrations' own
   warm accent, darkened until white text sits on it safely. */
.card-drawings {
  --vg: 0;
  background: oklch(0.47 0.086 48);
}
/* Drifting-light timings — coprime so the row never re-syncs. Drawings has
   none: it is a flat fill, so there are no radials to move. */
.card-eldora     { --a1: l1-a; --a2: l2-b; --a3: l3-c; --d1: 23s; --d2: 37s; --d3: 29s; --o1: -5s;  --o2: -19s; --o3: -11s; }
.card-textcortex { --a1: l1-c; --a2: l2-a; --a3: l3-b; --d1: 31s; --d2: 21s; --d3: 43s; --o1: -14s; --o2: -7s;  --o3: -24s; }
.card-dialux     { --a1: l1-b; --a2: l2-c; --a3: l3-a; --d1: 27s; --d2: 41s; --d3: 35s; --o1: -22s; --o2: -3s;  --o3: -16s; }

/* Three cards and no step buttons any more, so the row must not scroll:
   without an affordance a clipped third card just looks broken. Drop the
   full-bleed scroller and let the cards share the measure.
   (.cards is flex, so the old grid-auto-columns override here did nothing.) */
.cards {
  width: 100%;
  max-width: 100%;
  margin-left: 0;
  padding-inline: 0;
  overflow-x: visible;
}
.cards .card {
  width: auto;
  flex: 1 1 0;
  min-width: 0;
  aspect-ratio: 4 / 5;
}

/* Sharing the measure works at the 540px column: three cards land near 170px
   each and the body still fits. On a phone the same rule gives 105px, and the
   copy is clipped mid-sentence. Below the breakpoint they stop sharing and
   stack, full width, with the height driven by the text so nothing can be
   cut off whatever the card says. */
@media (max-width: 600px) {
  .cards { flex-direction: column; gap: 10px; }
  .cards .card {
    width: 100%;
    flex: 0 0 auto;
    aspect-ratio: auto;
    min-height: 132px;
  }
}

/* ======================================================================
   Craft surface — the card system from craft.gustavofior.com, hand-ported
   to this stylesheet. The rules that make it that system rather than a
   generic card:

   1. Elevation is a shadow token, never a border. Light is a 0.5px hairline
      plus two soft drops; dark is inset white highlights plus four stacked
      drops, because a border is invisible on a dark surface.
   2. Radii are gated PAIRS. `corner-shape: squircle` is Chromium-only, and a
      squircle reads less round than a circular arc at the same radius, so the
      large value sits behind @supports and everyone else gets the small one.
   3. Hover changes BACKGROUND ONLY. No lift, no scale, no shadow change.
   4. The card surface is LIGHTER than the page in light mode and the lift is
      carried by the shadow, not by fill contrast.
   ====================================================================== */

:root {
  --craft-card: #ffffff;
  --craft-card-hover: #f7f7f7;
  --craft-shadow:
    0 0 0 0.5px rgba(0, 0, 0, 0.06),
    0 0.5px 2px -0.5px rgba(0, 0, 0, 0.06),
    0 1px 4px 0 rgba(0, 0, 0, 0.04);
  /* The drawings' own accent, carried through to the card chrome. */
  --craft-accent: #FEC799;
  --craft-radius: 10px;
  --craft-radius-squircle: 22px;
}
html[data-theme="dark"] {
  --craft-card: #171717;
  --craft-card-hover: #1c1c1c;
  --craft-shadow:
    inset 0 0.75px 0 0 rgba(255, 255, 255, 0.03),
    inset 0 0 0 0.75px rgba(255, 255, 255, 0.03),
    0 0 0 0.75px rgba(0, 0, 0, 0.1),
    0 2px 2px 0 rgba(0, 0, 0, 0.1),
    0 4px 4px 0 rgba(0, 0, 0, 0.1),
    0 8px 8px 0 rgba(0, 0, 0, 0.1);
}

.craft-surface { width: 100%; }

.craft-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px;
  width: 100%;
}

.craft-card {
  margin: 0;
  display: flex;
  flex-direction: column;
  background: var(--craft-card);
  box-shadow: var(--craft-shadow);          /* never a border as well */
  border-radius: var(--craft-radius);
  overflow: hidden;
  transition: background-color 0.15s var(--ease-snappy, cubic-bezier(0.23, 1, 0.32, 1));
}
/* The gate: only browsers that can actually draw a squircle get the big radius. */
@supports (corner-shape: squircle) {
  .craft-card { border-radius: var(--craft-radius-squircle); corner-shape: squircle; }
}
/* Hover moves background and nothing else. */
.craft-card:hover { background: var(--craft-card-hover); }

.craft-card-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 34px 28px 26px;
  min-height: 168px;
}
.craft-card-stage svg {
  width: 100%;
  height: auto;
  max-height: 150px;
  overflow: visible;
}

.craft-card-foot {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 18px 16px;
}
.craft-card-name {
  font-size: var(--font-fixed);
  color: var(--text-strong);                 /* headings only */
  font-weight: 500;
}
.craft-card-note {
  font-size: var(--font-fixed-sm);
  line-height: 1.6;
  color: var(--text-muted);                  /* body copy is muted */
}

@media (prefers-reduced-motion: reduce) {
  .craft-card { transition: none; }
  /* SMIL keeps running otherwise; stop it at the source. */
  .craft-card-stage svg animate,
  .craft-card-stage svg animateTransform { display: none; }
}

/* Link variant of the craft card: no image stage, just the label block and a
   quiet affordance that appears on hover. Press is the system's scale-down;
   hover still moves background only. */
/* Component cards: the wide grid's alignment, but compact cells. They hold a
   name and a sentence, so a 4/3 stage would be an empty box. When there are
   real previews to show they take .craft-card-stage like the drawings do. */
.craft-grid-wide.craft-grid-tight {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin-top: 16px;
}
.craft-grid-wide.craft-grid-tight .craft-card-foot { padding: 0; }

.craft-card-link {
  position: relative;
  text-decoration: none;
  padding: 15px 16px 34px;
  gap: 4px;
  transition: background-color 0.15s var(--ease-snappy, cubic-bezier(0.23, 1, 0.32, 1)),
              transform 0.15s var(--ease-snappy, cubic-bezier(0.23, 1, 0.32, 1));
}
.craft-card-link .craft-card-foot { padding: 0; }
.craft-card-link:active { transform: scale(0.97); }
/* Absolute so the affordance costs no height: reserving a row for it left
   every card with a band of dead space under the text. */
.craft-card-go {
  position: absolute;
  right: 16px;
  bottom: 12px;
  font-size: var(--font-fixed-sm);
  color: var(--text-muted);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.craft-card-link:hover .craft-card-go,
.craft-card-link:focus-visible .craft-card-go { opacity: 1; }
.craft-card-link:focus-visible {
  outline: none;
  box-shadow: var(--craft-shadow), 0 0 0 1.5px color-mix(in srgb, var(--text-strong) 32%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .craft-card-link, .craft-card-go { transition: none; }
  .craft-card-link:active { transform: none; }
}

/* Wide craft grid — the explorations page's alignment: the grid breaks out of
   the text measure and centres on it, so the work is bigger than the prose
   that introduces it. Same trick, same widths.

   Cells hold a fixed aspect so a row never goes ragged when one drawing is
   wider than another; the SVG letterboxes inside it. */
.craft-grid-wide {
  /* Three up in a narrower band. At two up on 980px each drawing rendered
     ~485px wide, which is demo-reel size - these are line illustrations, not
     product videos, and they read better as a set than as posters. */
  --craft-w: min(820px, calc(100vw - 32px));
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  width: var(--craft-w);
  margin-left: calc((100% - var(--craft-w)) / 2);
}
.craft-grid-wide .craft-card-stage {
  aspect-ratio: 4 / 3;
  min-height: 0;
  padding: 20px 22px;
}
.craft-grid-wide .craft-card-stage svg {
  max-height: 100%;
  max-width: 100%;
  width: auto;
  height: auto;
}
.craft-grid-wide .craft-card-foot { padding: 0 14px 13px; }

@media (max-width: 860px) { .craft-grid-wide { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .craft-grid-wide { grid-template-columns: minmax(0, 1fr); } }

/* ======================================================================
   Contents — case study pages
   Parked in the left margin, opposite the reading column.

   Fixed, not sticky. A sticky element still occupies a slot in .content's
   flex column, and the negative margin used to pull it out of that slot
   dragged the whole article up over the nav. Fixed takes no space at all,
   and "stays put while you scroll" is the only behaviour wanted here.

   The column is 540px wide and auto-centred, so its left edge is
   50vw - 270px. Sit 64px clear of that, 180px wide: 50vw - 514px.
   ====================================================================== */
.toc {
  position: fixed;
  left: calc(50vw - 514px);
  top: 184px;
  width: 180px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  z-index: 3;
}
.toc-label {
  font-size: var(--font-fixed-sm);
  color: var(--text-muted);
  opacity: 0.7;
  margin-bottom: 2px;
}
.toc a {
  font-size: var(--font-fixed);
  line-height: 1.45;
  color: var(--text-muted);
  text-decoration: none;
  transition: color 0.15s ease;
}
.toc a:hover,
.toc a:focus-visible { color: var(--text-strong); outline: none; }
/* Current section. Colour only — no bar, no weight change, nothing that
   moves the text and makes the list jitter as you read. */
.toc a.is-current { color: var(--text-strong); }

/* Below this the margin cannot hold 180px + 64px of clearance. */
@media (max-width: 1140px) { .toc { display: none; } }
@media (prefers-reduced-motion: reduce) { .toc a { transition: none; } }

/* ======================================================================
   Social rail
   Pinned at the foot of the viewport on every page, the way the reference
   keeps its rail there while you read. A pill rather than bare icons so it
   survives scrolling over photographs and light cards alike.
   ====================================================================== */
.social {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 5px;
  border-radius: 999px;
  /* Translucent, so it sits on the page rather than punching a hole in it.
     The blur is what keeps the icons legible over a sticker or a screenshot. */
  background: color-mix(in srgb, var(--page) 72%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  box-shadow: var(--craft-shadow);
}
.social a {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  color: var(--text-muted);
  /* Background only on hover, per the system. No lift, no scale up. */
  transition: color 0.15s ease, background-color 0.15s ease;
}
.social a:hover,
.social a:focus-visible {
  color: var(--text-strong);
  background: var(--hover);
  outline: none;
}
.social a:active { transform: scale(0.94); }
.social svg { width: 15px; height: 15px; display: block; }

/* Printing a floating rail makes no sense. */
@media print { .social { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .social a { transition: none; }
  .social a:active { transform: none; }
}

/* ======================================================================
   Crafts — an isolated page
   No nav, no signature, no connect block. The back link therefore has no
   .brand to anchor against, so it hangs off .content instead, at the same
   64px clearance the other pages use.
   ====================================================================== */
.content-bare { padding-top: 96px; }
/* Anchored to the header, not the top of the column, so it sits level with
   the title the way it sits level with the avatar on every other page. */
.study-title { position: relative; }
.back-bare {
  top: 0;
  bottom: auto;
  height: var(--lh, 21px);
  right: calc(100% + 64px);
}
@media (max-width: 1040px) {
  .back-bare { position: static; height: auto; margin-bottom: 10px; }
}

/* The card grid. Cards are supplied per component; the grid is what holds
   them in Craft's shape: shadow, no border, gated squircle. */


/* ======================================================================
   Colour sweep on brand links
   Craft's technique: a registered custom property driving a color-mix inside
   a gradient that is clipped to the glyphs. A plain custom property is an
   uninterpolatable token, so @property is what makes the colour actually
   move rather than jump.

   Craft fires it once, as a nav dot lands. A hover wants something else: its
   version washed over 13.5px of inline text in 242ms and was gone before you
   registered it. So the tint fades in and HOLDS while the pointer is there,
   with the three colours travelling across the glyphs, and drains when you
   leave. The transition on --name-tint is what smooths the exit.

   The safety property is unchanged and is the reason this replaced the old
   shimmer: at 0% tint every stop resolves to var(--text-strong), so the text
   can never be invisible.
   ====================================================================== */
@property --name-tint {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

@supports (background-clip: text) and (color: color-mix(in oklch, red, blue)) {
  /* `a.sweep`, not `.sweep`: the prose-link rule is `.intro a` (0,1,1) and
     would otherwise win on specificity, leaving `color` opaque so the text
     painted solid over the gradient. That was the whole bug. */
  a.sweep {
    --sweep-1: var(--text-strong);
    --sweep-2: var(--text-strong);
    --sweep-3: var(--text-strong);
    color: transparent;
    -webkit-background-clip: text;
    background-clip: text;
    background-image: linear-gradient(90deg,
      color-mix(in oklch, var(--sweep-1) var(--name-tint), var(--text-strong)) 0%,
      color-mix(in oklch, var(--sweep-2) var(--name-tint), var(--text-strong)) 25%,
      color-mix(in oklch, var(--sweep-3) var(--name-tint), var(--text-strong)) 50%,
      color-mix(in oklch, var(--sweep-2) var(--name-tint), var(--text-strong)) 75%,
      color-mix(in oklch, var(--sweep-1) var(--name-tint), var(--text-strong)) 100%);
    background-size: 220% 100%;
    background-repeat: repeat-x;
    /* currentColor is transparent once the glyphs are a gradient, so the
       underline has to be given a colour of its own or it disappears. */
    text-decoration-color: color-mix(in srgb, var(--text-strong) 30%, transparent);
    /* registered properties transition, which is how the colour drains */
    transition: --name-tint 0.42s ease, text-decoration-color 0.18s ease;
  }
  a.sweep:hover,
  a.sweep:focus-visible {
    --name-tint: 100%;
    /* background-clip:text paints the glyphs only - the underline is a
       separate ink and keeps its own colour. Running it on the same 2.4s
       clock, through the same stop order, makes the rule travel with the
       letters instead of staying grey underneath them. */
    animation: sweep-run 2.4s linear infinite,
               sweep-rule 2.4s linear infinite;
  }
}

@keyframes sweep-run {
  from { background-position: 0 0; }
  to   { background-position: -220% 0; }
}

@keyframes sweep-rule {
  0%   { text-decoration-color: var(--sweep-1); }
  25%  { text-decoration-color: var(--sweep-2); }
  50%  { text-decoration-color: var(--sweep-3); }
  75%  { text-decoration-color: var(--sweep-2); }
  100% { text-decoration-color: var(--sweep-1); }
}

/* TextCortex: the three logo colours, the same three its work card carries. */
a.sweep[data-sweep="textcortex"] { --sweep-1: #FF805F; --sweep-2: #B74BDD; --sweep-3: #0379FF; }
/* Eldora UI: cyan. Three values from one family so the run is still legible. */
a.sweep[data-sweep="eldora"] { --sweep-1: #06B6D4; --sweep-2: #67E8F9; --sweep-3: #22D3EE; }

@media (prefers-reduced-motion: reduce) {
  a.sweep { transition: none; }
  a.sweep:hover, a.sweep:focus-visible { animation: none; }
}
/* ======================================================================
   Writing demos — ported from craft-main/src/components/demos
   nested-radius.tsx and tabular-nums.tsx. Same structure, same numbers,
   same sky-blue dimension lines; Tailwind classes resolved to real CSS.
   ====================================================================== */

/* ---- Segmented control (app/segmented-control.tsx) -------------------- */
.seg {
  position: relative;
  display: inline-flex;
  padding: 3px;
  border-radius: 999px;
  background: var(--hover);
}
.seg button {
  position: relative;
  z-index: 1;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--font-fixed-sm);
  color: var(--text-muted);
  padding: 6px 14px;
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.2s ease;
}
.seg button[aria-selected="true"] { color: var(--text-strong); }
.seg-ind {
  position: absolute;
  top: 3px;
  left: 0;
  height: calc(100% - 6px);
  border-radius: 999px;
  background: var(--craft-card);
  box-shadow: var(--craft-shadow);
  transition: transform 0.25s cubic-bezier(0.25, 0.1, 0.25, 1), width 0.25s cubic-bezier(0.25, 0.1, 0.25, 1);
}

/* ---- Sources (app/resources.tsx) -------------------------------------- */
.src-list { display: flex; flex-direction: column; }
.src-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-inline: -10px;
  padding: 10px;
  border-radius: 8px;
  text-decoration: none;
  transition: background-color .15s ease;
}
.src-row:hover { background: var(--muted); }
.src-fav { width: 16px; height: 16px; flex-shrink: 0; border-radius: 3px; }
/* One line, truncated - the title reads as the link, the rest as context. */
.src-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14px;
  color: var(--text-muted);
}
.src-title { color: var(--text-strong); }


/* ---- Demo frame (app/demo.tsx: min-h-48, centred, gap) ---------------- */
.nr-demo, .tn-demo, .nr-calc, /* ---- Real components ---------------------------------------------------
   Card: 16px outer, 8px padding, so the media nests to 8px. Menu: 12px
   outer, 4px padding, rows nest to 8px. Both cross-fade on 200ms ease-out. */
.nr-examples { display: flex; flex-direction: column; align-items: center; gap: 40px; width: 100%; }
.nr-ex-row {
  display: grid;
  gap: 32px;
  width: 100%;
  max-width: 512px;
  grid-template-columns: 1.1fr 0.9fr;
  align-items: start;
}
@media (max-width: 620px) { .nr-ex-row { grid-template-columns: 1fr; } }

.nr-ex-card { background: var(--card); padding: 8px; border-radius: 16px; box-shadow: var(--custom-shadow); }
.nr-ex-media {
  position: relative;
  height: 128px;
  overflow: hidden;
  background: var(--muted);
  border-radius: 16px;
  transition: border-radius .2s ease-out;
}
.nr-ex-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Inset hairline on a sibling so it follows the container radius, not the
   image - the image-outline rule. */
.nr-ex-edge {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  outline: 1px solid rgb(0 0 0 / 0.1);
  outline-offset: -1px;
}
[data-theme="dark"] .nr-ex-edge { outline-color: rgb(255 255 255 / 0.1); }
.nr-ex-meta { display: flex; flex-direction: column; padding: 12px 8px 4px; }
.nr-ex-title { font-size: 12px; color: var(--text-strong); }
.nr-ex-sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; }

.nr-ex-menu {
  align-self: center;
  background: var(--card);
  padding: 4px;
  border-radius: 12px;
  box-shadow: var(--custom-shadow);
}
.nr-ex-item {
  display: flex;
  height: 32px;
  align-items: center;
  gap: 10px;
  padding-inline: 10px;
  font-size: 12px;
  color: var(--text-muted);
  border-radius: 12px;
  transition: border-radius .2s ease-out;
}
.nr-ex-item svg { width: 14px; height: 14px; flex-shrink: 0; }
.nr-ex-item.is-active { background: var(--muted); color: var(--text-strong); }

/* Nested: media 16 -> 8, rows 12 -> 8. */
.nr-examples[data-mode="nested"] .nr-ex-media,
.nr-examples[data-mode="nested"] .nr-ex-item { border-radius: 8px; }

@media (prefers-reduced-motion: reduce) {
  .nr-ex-media, .nr-ex-item { transition: none; }
}


/* ---- NestedRadiusDemo -------------------------------------------------- */
.nr-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; width: 100%; max-width: 440px; }
.nr-case { display: flex; flex-direction: column; align-items: center; gap: 18px; min-width: 0; }
.nr-verdict { display: inline-flex; align-items: center; gap: 6px; font-size: var(--font-fixed); font-weight: 500; }
.nr-verdict svg { width: 15px; height: 15px; }
.nr-wrong { color: #E5484D; }
.nr-right { color: #30A46C; }
.nr-outer { width: 100%; padding: 12px; background: var(--hover); box-shadow: var(--craft-shadow); }
.nr-inner {
  display: grid;
  place-items: center;
  gap: 5px;
  height: 116px;
  background: var(--craft-card);
  box-shadow: var(--craft-shadow);
}
.nr-nums { font-size: var(--font-fixed-sm); color: var(--text-strong); font-variant-numeric: tabular-nums; }
.nr-sep { margin: 0 6px; color: var(--text-muted); }
.nr-legend { font-size: 10px; color: var(--text-muted); }
.nr-legend span { display: inline-block; width: 26px; }
.nr-foot { max-width: 340px; text-align: center; font-size: var(--font-fixed-sm); color: var(--text-muted); }
.nr-formula { padding: 10px 14px; border-left: 2px solid var(--rule); color: var(--text-strong); }

/* ---- RadiusCalculatorDemo --------------------------------------------- */
.nr-stage { display: flex; justify-content: center; width: 100%; }
.nr-box {
  position: relative;
  padding: var(--inset, 12px);
  border-radius: var(--outer, 32px);
  background: var(--hover);
  box-shadow: var(--craft-shadow);
  transition: border-radius 0.2s ease-out, padding 0.2s ease-out;
}
.nr-card {
  position: relative;
  width: 264px;
  height: 132px;
  border-radius: var(--inner, 20px);
  background: var(--craft-card);
  box-shadow: var(--craft-shadow);
  transition: border-radius 0.2s ease-out;
}
/* Dimension lines: a 1px rule in sky blue with a tick at each end, exactly
   as Craft draws them with before/after pseudo-elements. */
.nr-dim { position: absolute; pointer-events: none; background: #38bdf8; transition: width 0.2s ease-out, height 0.2s ease-out; }
.nr-dim::before, .nr-dim::after { content: ''; position: absolute; background: #38bdf8; }
.nr-dim-y { width: 1px; height: var(--len); }
.nr-dim-y::before { top: 0; left: 50%; height: 1px; width: 6px; transform: translateX(-50%); }
.nr-dim-y::after  { bottom: 0; left: 50%; height: 1px; width: 6px; transform: translateX(-50%); }
.nr-dim-x { height: 1px; width: var(--len); }
.nr-dim-x::before { top: 50%; left: 0; width: 1px; height: 6px; transform: translateY(-50%); }
.nr-dim-x::after  { top: 50%; right: 0; width: 1px; height: 6px; transform: translateY(-50%); }
.nr-dim-inset { --len: var(--inset, 12px); top: 0; left: 50%; transform: translateX(-50%); }
.nr-dim-outer { --len: var(--outer, 32px); right: 0; bottom: 0; transform: translateY(50%); }
.nr-dim-inner { --len: var(--inner, 20px); left: 0; bottom: 0; transform: translateY(50%); }
.nr-lab { position: absolute; white-space: nowrap; font-size: 10px; color: #38bdf8; pointer-events: none; }
.nr-lab b { font-weight: 400; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.nr-lab-inset { bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 6px; }
.nr-lab-outer { top: 100%; right: 0; margin-top: 6px; }
.nr-lab-inner { bottom: 10px; left: 12px; }

.nr-sliders { display: grid; gap: 18px; width: 100%; max-width: 280px; }
.nr-sliders label { display: grid; gap: 8px; }
.nr-sliders span { display: flex; justify-content: space-between; font-size: var(--font-fixed-sm); color: var(--text-muted); }
.nr-sliders b { font-weight: 400; font-size: 10px; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.nr-sliders input { width: 100%; accent-color: var(--text-strong); }

/* ---- NestedRadiusExamplesDemo ----------------------------------------- */
.nr-ex-row { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 26px; width: 100%; max-width: 440px; align-items: center; }
.nr-ex-card { padding: 8px; border-radius: 16px; background: var(--craft-card); box-shadow: var(--craft-shadow); }
.nr-ex-media {
  height: 120px;
  border-radius: var(--ex-inner, 16px);
  background: linear-gradient(140deg, #6aa9c9, #3f6f5d 55%, #234a3c);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1);
  transition: border-radius 0.2s ease-out;
}
.nr-ex-meta { display: flex; flex-direction: column; gap: 2px; padding: 12px 8px 4px; }
.nr-ex-title { font-size: var(--font-fixed-sm); color: var(--text-strong); }
.nr-ex-sub { font-size: 10px; color: var(--text-muted); }
.nr-ex-menu { align-self: center; padding: 4px; border-radius: 12px; background: var(--craft-card); box-shadow: var(--craft-shadow); }
.nr-ex-item {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 32px;
  padding: 0 10px;
  font-size: var(--font-fixed-sm);
  color: var(--text-muted);
  border-radius: var(--ex-menu, 12px);
  transition: border-radius 0.2s ease-out;
}
.nr-ex-item.is-active { background: var(--hover); color: var(--text-strong); }

/* ---- TabularNumsDemo --------------------------------------------------- */
.tn-digits { display: flex; align-items: flex-end; gap: 1px; font-variant-numeric: proportional-nums; }
.tn-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 56px;
  font-size: 40px;
  font-weight: 500;
  line-height: 1;
  color: var(--text-strong);
  background: color-mix(in srgb, var(--text-strong) 5%, transparent);
}
.tn-cell.tn-alt { background: color-mix(in srgb, var(--text-strong) 10%, transparent); }

/* ---- TabularTimerDemo -------------------------------------------------- */
.tn-clock-wrap { position: relative; display: flex; align-items: center; }
.tn-clock { font-size: 44px; font-weight: 500; letter-spacing: -0.02em; color: var(--text-strong); font-variant-numeric: proportional-nums; }
.tn-rule { margin-left: 8px; height: 44px; border-left: 1px dashed #7dd3fc; }
html[data-theme="dark"] .tn-rule { border-left-color: #0c4a6e; }
.tn-edge-label { position: absolute; right: -10px; bottom: -18px; font-size: 9px; color: #38bdf8; }

/* ---- TabularTableDemo -------------------------------------------------- */
.tn-table { width: 100%; max-width: 440px; border-radius: 10px; overflow: hidden; background: var(--craft-card); box-shadow: var(--craft-shadow); }
.tn-thead, .tn-tr { display: grid; grid-template-columns: 1fr auto auto; gap: 34px; padding: 10px 16px; font-size: var(--font-fixed-sm); }
.tn-thead { color: var(--text-muted); border-bottom: 1px solid var(--rule); }
.tn-tr { align-items: center; border-bottom: 1px solid var(--rule); }
.tn-tr:last-child { border-bottom: 0; }
.tn-td-name { color: var(--text-strong); }
.tn-td-num { min-width: 52px; text-align: right; color: var(--text-muted); font-variant-numeric: proportional-nums; }
.tn-td-chg { min-width: 48px; text-align: right; font-variant-numeric: proportional-nums; }
.tn-td-chg.is-up { color: #30A46C; }
.tn-td-chg.is-down { color: #E5484D; }

/* The one thing every tabular demo is actually about. */
[data-figures].is-tabular { font-variant-numeric: tabular-nums; }

@media (max-width: 560px) {
  .nr-pair, .nr-ex-row { grid-template-columns: minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .nr-box, .nr-card, .nr-dim, .nr-ex-media, .nr-ex-item, .seg-ind, .seg button { transition: none; }
}


/* ======================================================================
   Crafts
   One big light stage per component, caption underneath - the component is
   the whole card, nothing chrome competes with it.
   ====================================================================== */
.cp-list { display: flex; flex-direction: column; gap: 56px; width: 100%; }
.cp-item { margin: 0; display: flex; flex-direction: column; gap: 14px; }
.cp-stage {
  display: grid;
  place-items: center;
  width: 100%;
  /* The stage is sized by what it holds, not by a fixed block: a 332px card
     inside 560px left 114px of dead air above and below it. The floor only
     stops a very small component from looking cramped, and the stage grows
     on its own as rows open. */
  min-height: 0;
  padding: 44px 24px;
  background: var(--sch-surface);
  border-radius: 24px;
  box-shadow: var(--custom-shadow);
  transition: background-color .2s ease;
}
.cp-cap { display: flex; flex-direction: column; gap: 4px; }
.cp-title {
  align-self: flex-start;
  font-size: 15px;
  color: var(--text-strong);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  transition: text-decoration-color .18s ease;
}
.cp-title:hover, .cp-title:focus-visible { text-decoration-color: currentColor; outline: none; }
.cp-desc { font-size: 15px; color: var(--text-muted); margin: 0; }
.cp-solo { display: flex; width: 100%; }

/* ---- Scheduler palette ------------------------------------------------
   The port arrived with the source's literals baked in, which made it a
   light-only component. They are tokens now, so the same markup themes.

   Dark follows the craft rules rather than inverting the hexes: surfaces
   lift by getting lighter (chrome 0.205 -> surface 0.269), the ring becomes
   white-at-10% instead of a flat gray, the accent gets lighter and loses
   chroma, and the elevation swaps the hairline for inset highlights. */
.sch, .cp-stage {
  --sch-chrome:    #f7f7f7;
  --sch-surface:   #ffffff;
  --sch-fill:      #f7f7f7;
  --sch-fill-hi:   #efefef;
  --sch-line:      #ebebeb;
  --sch-ink:       #171717;
  --sch-ink-2:     #5c5c5c;
  --sch-ink-3:     #a3a3a3;
  --sch-ink-4:     #c4c4c4;
  --sch-track:     #dadadb;
  --sch-thumb:     #ffffff;
  --sch-thumb-on:  #fcfcfc;
  --sch-accent:    #3592f9;
  --sch-inv-bg:    #171717;
  --sch-inv-fg:    #ffffff;
  /* The mark shipped in the source's brand blue, the one chromatic thing on
     a zero-chroma page. Graphite instead, so it reads as ink like everything
     else; the white face and dark hands still give it a dial. */
  --sch-logo-a:    #333333;
  --sch-logo-b:    #6b6b6b;
  --sch-logo-rim-a:#f8f8f8;
  --sch-logo-rim-b:#d0d0d0;
  --sch-logo-face: #ffffff;
  --sch-logo-hand: #222222;
  --sch-elev:
    0 0 0 1px rgba(51,51,51,.04),
    0 16px 8px -8px rgba(51,51,51,.01),
    0 12px 6px -6px rgba(51,51,51,.02),
    0 5px 5px -2.5px rgba(51,51,51,.08),
    0 1px 3px -1.5px rgba(51,51,51,.16);
}
html[data-theme="dark"] .sch,
html[data-theme="dark"] .cp-stage {
  --sch-chrome:    oklch(0.205 0 0);
  --sch-surface:   oklch(0.269 0 0);
  --sch-fill:      oklch(0.205 0 0);
  --sch-fill-hi:   oklch(0.30 0 0);
  --sch-line:      oklch(1 0 0 / 10%);
  --sch-ink:       oklch(0.985 0 0);
  --sch-ink-2:     oklch(0.708 0 0);
  --sch-ink-3:     oklch(0.556 0 0);
  --sch-ink-4:     oklch(0.44 0 0);
  --sch-track:     oklch(0.32 0 0);
  --sch-thumb:     oklch(0.92 0 0);
  --sch-thumb-on:  oklch(0.96 0 0);
  --sch-accent:    oklch(0.72 0.15 253);
  --sch-inv-bg:    oklch(0.985 0 0);
  --sch-inv-fg:    oklch(0.205 0 0);
  /* Lifts to mid-graphite so the mark still separates from the dark card,
     and the rim becomes white-at-alpha like every other dark hairline. */
  --sch-logo-a:    oklch(0.45 0 0);
  --sch-logo-b:    oklch(0.70 0 0);
  --sch-logo-rim-a:oklch(1 0 0 / 14%);
  --sch-logo-rim-b:oklch(1 0 0 / 6%);
  --sch-logo-face: oklch(0.94 0 0);
  --sch-logo-hand: oklch(0.22 0 0);
  --sch-elev:
    inset 0 0.75px 0 0 rgba(255,255,255,.04),
    inset 0 0 0 0.75px rgba(255,255,255,.04),
    0 0 0 0.75px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.18),
    0 4px 4px 0 rgba(0,0,0,.16), 0 8px 8px 0 rgba(0,0,0,.14);
}
/* The stage sits one step below the card it holds, so the component still
   reads as an object resting on a surface rather than merging into it.
   Declared after the block above: same specificity, so order decides. */
html[data-theme="dark"] .cp-stage { --sch-surface: oklch(0.17 0 0); }

/* ---- Scheduler --------------------------------------------------------
   Ported from github.com/johntony1/SCHEDULER. Sizes, colours, shadows and
   delays are the source's own values. Framer's springs become cubic-bezier
   curves with matching overshoot, since this site ships no runtime. */
.sch { --spring: cubic-bezier(.22, 1.18, .36, 1); --fast: cubic-bezier(.3, 1.25, .45, 1); }
.sch {
  width: 350px;
  background: var(--sch-chrome);
  border-radius: 20px;
  padding: 12px 4px 4px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  font-family: var(--font), Inter, system-ui, sans-serif;
  opacity: 0;
  transform: scale(.92);
  transition: opacity .34s ease-out, transform .46s var(--spring);
}
.sch.is-in { opacity: 1; transform: scale(1); }
.sch.is-out { opacity: 0; transform: scale(.9); transition-duration: .18s; }

.sch-head {
  display: flex; align-items: center; justify-content: space-between;
  padding-inline: 12px;
  opacity: 0; transform: translateY(-6px);
  transition: opacity .22s cubic-bezier(.25,.46,.45,.94) 80ms,
              transform .22s cubic-bezier(.25,.46,.45,.94) 80ms;
}
.sch.is-in .sch-head { opacity: 1; transform: none; }
.sch-id { display: flex; align-items: center; gap: 8px; }
.sch-titles { display: flex; flex-direction: column; width: 130px; }
.sch-name { font-size: 14px; font-weight: 500; letter-spacing: -.084px; line-height: 20px; color: var(--sch-ink); }
.sch-sub  { font-size: 13px; font-weight: 400; letter-spacing: -.078px; line-height: 18px; color: var(--sch-ink-2); }
.sch-close {
  width: 20px; height: 20px; border-radius: 50%; border: none; background: var(--sch-track);
  color: var(--sch-ink-2);
  cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; transition: transform .18s var(--fast);
}
.sch-close:hover { transform: scale(1.1); }
.sch-close:active { transform: scale(.9); }

.sch-body {
  background: var(--sch-surface); border-radius: 16px; padding: 12px;
  display: flex; flex-direction: column; gap: 8px;
  box-shadow: var(--sch-elev);
  opacity: 0; transition: opacity .2s ease-out 200ms;
}
.sch.is-in .sch-body { opacity: 1; }

/* rows enter at 320ms, 75ms apart */
.sch-row { border-radius: 12px; opacity: 0; transform: translateY(10px); }
.sch.is-in .sch-row {
  opacity: 1; transform: none;
  transition: opacity .28s cubic-bezier(.25,.46,.45,.94) var(--rd),
              transform .28s cubic-bezier(.25,.46,.45,.94) var(--rd);
}
.sch-rowinner {
  border-radius: 12px; padding: 8px 0;
  background: var(--sch-fill); box-shadow: inset 0 0 0 1px transparent;
  transition: background-color .3s ease, box-shadow .3s ease, padding .42s var(--spring);
}
.sch-row.is-on .sch-rowinner {
  background: rgba(255,255,255,0); box-shadow: inset 0 0 0 1px var(--sch-line); padding: 8px;
}
.sch-rowhead {
  display: flex; align-items: center; justify-content: space-between;
  padding-inline: 12px; transition: padding .42s var(--spring);
}
.sch-row.is-on .sch-rowhead { padding-inline: 4px; }
.sch-day { font-size: 14px; font-weight: 500; letter-spacing: -.084px; line-height: 20px; white-space: nowrap; user-select: none; color: var(--sch-ink); }

/* toggle - Figma values from the source */
.sch-tog { position: relative; width: 51.406px; height: 25.703px; border-radius: 12.85px; border: none; padding: 0; cursor: pointer; flex-shrink: 0; overflow: hidden; background: transparent; transition: transform .2s var(--fast); }
.sch-tog:active { transform: scale(.95); }
.sch-tog-track { position: absolute; inset: 0; border-radius: inherit; background: var(--sch-track); opacity: .8; transition: background-color .18s ease-out, opacity .18s ease-out; }
.sch-row.is-on .sch-tog-track { background: var(--sch-accent); opacity: 1; }
.sch-tog-thumb {
  position: absolute; top: 2.57px; left: 2.57px; width: 28.273px; height: 20.563px;
  border-radius: 10.28px; background: var(--sch-thumb); pointer-events: none;
  box-shadow: 0 2.57px 5.14px rgba(0,0,0,.04), 0 1.285px 2.57px rgba(0,0,0,.06), 0 0 1.285px rgba(0,0,0,.06);
  transition: transform .3s var(--fast), background-color .2s ease, box-shadow .2s ease;
}
.sch-row.is-on .sch-tog-thumb {
  transform: translateX(17.993px); background: var(--sch-thumb-on);
  box-shadow: 0 0 1.285px rgba(0,0,0,.3), 0 2.57px 12.852px rgba(0,0,0,.06), 0 0 6.426px rgba(0,0,0,.02);
}

/* panel: height 0 -> measured */
.sch-panel { overflow: hidden; height: 0; opacity: 0; transition: height .42s var(--spring), opacity .3s ease; }
.sch-row.is-on .sch-panel { opacity: 1; }
.sch-panel-pad { padding-top: 12px; }

.sch-pill {
  background: var(--sch-fill); border-radius: 12px; padding: 8px 12px;
  display: flex; align-items: center; justify-content: space-between;
  min-width: 0; border: none; cursor: pointer;
  font-family: inherit;
  box-shadow: inset 0 0 0 0 var(--sch-accent);
  transition: box-shadow .24s var(--spring), transform .24s var(--spring), opacity .24s ease, background-color .15s ease;
}
.sch-display { cursor: default; }
.sch-time { font-size: 14px; font-weight: 500; letter-spacing: -.084px; line-height: 20px; color: var(--sch-ink); font-variant-numeric: tabular-nums; }
.sch-mer  { font-size: 14px; font-weight: 500; letter-spacing: -.084px; line-height: 20px; color: var(--sch-ink-3); }
.sch-pencil { background: none; border: none; cursor: pointer; padding: 2px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--sch-ink-3); transition: transform .22s var(--fast); }
.sch-pencil:hover { transform: scale(1.15); }
.sch-pencil:active { transform: scale(.88); }

.sch-edit { display: flex; flex-direction: column; gap: 8px; }
.sch-pills { display: flex; gap: 8px; align-items: stretch; }
.sch-pills .sch-pill { flex: 1; gap: 6px; }
.sch-pill.is-active { box-shadow: inset 0 0 0 1.5px var(--sch-accent); }
.sch-check { flex: 0 0 auto; padding: 8px 12px; color: var(--sch-ink-3); transition: transform .22s var(--fast), background-color .15s ease; }
.sch-check:hover { transform: scale(1.08); background: var(--sch-fill-hi); }
.sch-check:active { transform: scale(.92); }

/* the drum picker */
.sch-picker {
  background: var(--sch-surface); border: 1px solid var(--sch-line); border-radius: 12px; padding: 12px;
  display: flex; align-items: center; justify-content: center; gap: 4px;
  transform-origin: top center;
  transform: scaleY(0); opacity: 0;
  transition: transform .4s var(--spring), opacity .26s ease;
}
.sch-picker.is-open { transform: scaleY(1); opacity: 1; }
.sch-drum { display: flex; flex-direction: column; align-items: center; gap: 4px; user-select: none; }
.sch-arrow { background: none; border: none; cursor: pointer; padding: 4px; display: flex; color: var(--sch-ink-4); border-radius: 6px; transition: transform .2s var(--fast); }
.sch-arrow:hover { transform: scale(1.15); }
.sch-arrow:active { transform: scale(.85); }
.sch-window { position: relative; width: 52px; height: 120px; }
.sch-sel { position: absolute; top: 40px; left: 0; right: 0; height: 40px; background: var(--sch-fill); border-radius: 8px; z-index: 0; }
.sch-mask {
  position: absolute; inset: 0; overflow: hidden; z-index: 1;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 28%, #000 72%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 28%, #000 72%, transparent 100%);
}
.sch-strip { position: absolute; left: 0; right: 0; transition: transform .44s var(--spring); }
.sch-cell {
  height: 40px; display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 500; letter-spacing: -.04em; color: var(--sch-ink-4);
  transition: font-size .18s ease, color .18s ease; cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.sch-cell.is-sel { font-size: 18px; color: var(--sch-ink); }
.sch-colon { font-size: 18px; font-weight: 600; color: var(--sch-ink-4); padding-bottom: 2px; user-select: none; line-height: 1; }
.sch-gap { width: 8px; }
.sch-mers { display: flex; flex-direction: column; gap: 6px; }
.sch-merbtn {
  border: none; cursor: pointer; border-radius: 8px; padding: 6px 12px;
  font-size: 13px; font-weight: 500; letter-spacing: -.03em; min-width: 44px;
  font-family: inherit; background: var(--sch-fill); color: var(--sch-ink-3);
  transition: background-color .22s ease, color .22s ease, transform .2s var(--fast);
}
.sch-merbtn:hover { transform: scale(1.05); }
.sch-merbtn:active { transform: scale(.94); }
.sch-merbtn.is-on { background: var(--sch-inv-bg); color: var(--sch-inv-fg); }

.sch-reopen {
  background: var(--sch-fill); border: none; border-radius: 14px; padding: 12px 24px;
  cursor: pointer; font-size: 14px; font-weight: 500; color: var(--sch-ink);
  letter-spacing: -.084px; font-family: inherit;
  opacity: 0; transform: scale(.85);
  transition: opacity .26s ease, transform .4s var(--spring);
}
.sch-reopen.is-in { opacity: 1; transform: scale(1); }

/* the clock mark: hands run on real time */
.sch-clock { display: block; transform-origin: 20px 20px; animation: sch-clock-in .8s cubic-bezier(.2,1.3,.4,1) 50ms both; }
@keyframes sch-clock-in { from { transform: scale(.72); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.sch-clock .sch-body-g { animation: sch-pulse 7s ease-in-out infinite; transform-origin: 20px 20px; }
@keyframes sch-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.03); } }
.sch-hand-m { animation: sch-spin 60s linear infinite; transform-origin: 20px 20px; }
.sch-hand-h { animation: sch-spin 43200s linear infinite; transform-origin: 20px 20px; }
@keyframes sch-spin { from { rotate: var(--a, 0deg); } to { rotate: calc(var(--a, 0deg) + 360deg); } }

@media (prefers-reduced-motion: reduce) {
  .sch, .sch-head, .sch-body, .sch-row, .sch-rowinner, .sch-rowhead, .sch-panel,
  .sch-pill, .sch-picker, .sch-strip, .sch-tog-thumb, .sch-tog-track, .sch-reopen { transition: none; }
  .sch-clock, .sch-clock .sch-body-g, .sch-hand-m, .sch-hand-h { animation: none; }
}
@media (max-width: 420px) { .sch { width: 100%; max-width: 350px; } }

/* ======================================================================
   Eldora components
   Ported from karthikmudunuri/eldoraui: registry/eldoraui/clerk-otp.tsx,
   registry/blocks/logo-cloud-01, components/docs-copy-page.tsx.
   Sizes, durations and delays are each source's own.
   ====================================================================== */

/* ---- Clerk OTP --------------------------------------------------------- */
.otp {
  position: relative;
  width: 100%;
  max-width: 350px;
  height: 14rem;
  border-radius: 6px;
  border: 1px solid var(--rule);
  background: var(--otp-bg);
  box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);
}
:root, html[data-theme="light"] { --otp-bg: #fafafa; --otp-cell: linear-gradient(to bottom right, #f5f5f5, #fafafa); --otp-desc: #525252; }
html[data-theme="dark"] { --otp-bg: #171717; --otp-cell: linear-gradient(to bottom right, #262626, #262626); --otp-desc: #a3a3a3; }

.otp-digits {
  position: absolute;
  top: 25%;
  left: 50%;
  translate: -50% 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
.otp-cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 40px;
  border-radius: 6px;
  background: var(--otp-cell);
  box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);
  color: var(--text-strong);
  font-size: 14px;
  cursor: default;
}
/* Cyan is the component's own signal colour and the only chroma it uses. */
.otp-ring {
  position: absolute;
  inset: 0;
  border-radius: 6px;
  border: 1px solid #22d3ee;
  box-shadow: inset 0 0 12px rgb(34 211 238 / 0.5);
  opacity: 0;
}
/* The whole row flashes once, 2.25s in, for 0.5s. */
.otp-cell .otp-ring-all { animation: otp-flash .5s ease-in-out 2.25s both; }
@keyframes otp-flash { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
/* The cursor ring sits on the active cell and carries the underline. */
.otp-cell.is-active .otp-ring-cursor { opacity: 1; box-shadow: inset 0 0 12px rgb(34 211 238 / 0.6); transition: opacity .3s ease-in-out; }
.otp-ring-cursor svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.otp-ring-cursor path { stroke: #22d3ee; stroke-width: .4; }
.otp-digit { position: absolute; top: 50%; left: 50%; translate: -50% -50%; opacity: 0; }
.otp-cell.is-filled .otp-digit { opacity: 1; transition: opacity .2s ease-in-out var(--dd); }
.otp-digits.is-out .otp-digit { opacity: 0; transition: opacity .1s ease-in-out; }

.otp-meta { position: absolute; bottom: 16px; left: 0; width: 100%; padding-inline: 12px; }
.otp-meta h3 { font-size: 14px; font-weight: 600; color: var(--text-strong); }
.otp-meta p { margin-top: 8px; font-size: 12px; color: var(--otp-desc); }

@media (prefers-reduced-motion: reduce) {
  .otp-cell .otp-ring-all { animation: none; }
  .otp-digit, .otp-ring-cursor { transition: none; }
}

/* ---- Copy Page --------------------------------------------------------- */
.cpp { display: flex; align-items: center; justify-content: center; }
.cpp-group {
  position: relative;
  display: flex;
  align-items: center;
  border-radius: 8px;
  background: var(--hover);
}
.cpp-copy, .cpp-trigger {
  display: flex; align-items: center; gap: 6px;
  height: 32px; padding-inline: 12px;
  border: none; background: none; cursor: pointer;
  font: inherit; font-size: 13px; font-weight: 500;
  color: var(--text-strong);
  border-radius: 8px;
  transition: background-color .15s ease;
}
.cpp-trigger { padding-inline: 8px; }
.cpp-copy:hover, .cpp-trigger:hover { background: var(--rule); }
.cpp-ico { width: 15px; height: 15px; flex-shrink: 0; }
.cpp-ico-done { display: none; }
.cpp-copy.is-copied .cpp-ico-copy { display: none; }
.cpp-copy.is-copied .cpp-ico-done { display: block; }
.cpp-trigger svg { width: 15px; height: 15px; transition: rotate .2s ease; }
.cpp-trigger[aria-expanded="true"] svg { rotate: 180deg; }
/* The hairline between the two halves, as in the source. */
.cpp-rule { width: 1px; height: 20px; background: color-mix(in srgb, var(--text-strong) 10%, transparent); }

.cpp-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  min-width: 208px;
  padding: 4px;
  border-radius: 8px;
  border: 1px solid var(--rule);
  background: var(--page);
  box-shadow: var(--custom-shadow);
  display: flex;
  flex-direction: column;
  opacity: 0;
  translate: 0 -4px;
  scale: .98;
  pointer-events: none;
  transform-origin: top right;
  transition: opacity .16s ease, translate .16s ease, scale .16s ease;
}
.cpp-group.is-open .cpp-menu { opacity: 1; translate: 0 0; scale: 1; pointer-events: auto; }
.cpp-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 8px; border-radius: 6px;
  font-size: 13px; color: var(--text-strong); text-decoration: none;
  transition: background-color .15s ease;
}
.cpp-item:hover, .cpp-item:focus-visible { background: var(--hover); outline: none; }
.cpp-item svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--text-muted); }
@media (prefers-reduced-motion: reduce) { .cpp-menu, .cpp-trigger svg { transition: none; } }

/* A stage whose component opens a layer: the group sits near the top so the
   menu drops into the stage instead of escaping it. Sized for the four rows
   the Copy Page menu carries. */
.cp-stage-menu {
  align-items: start;
  min-height: 300px;
}

/* ---- Claude model selector --------------------------------------------
   zanwei/claude-model-selector, dropped in as its own web component - the
   module is unmodified. Its palette lives in --effort-* custom properties
   on :host, and outer-document rules beat :host, so dark is a retheme from
   here rather than a fork of the component. */
.cms { display: flex; align-items: center; justify-content: center; width: 100%; }
html[data-theme="dark"] claude-model-selector {
  --effort-text:        oklch(0.708 0 0);
  --effort-text-strong: oklch(0.985 0 0);
  --effort-muted:       oklch(0.62 0 0);
  --effort-track:       oklch(0.269 0 0);
  --effort-track-fill:  oklch(0.36 0 0);
  --effort-surface:     oklch(0.205 0 0);
  --effort-outline:     oklch(1 0 0 / 12%);
}

/* The effort panel opens upward, out of flow: 160px of panel sitting 168px
   above a 44px trigger. Bottom-align the element and give the stage that
   212px plus its padding, so the panel opens into the stage instead of
   over the heading above it. */
.cms-stage {
  align-items: end;
  min-height: 320px;
}

/* ---- Agent orb + widget -----------------------------------------------
   The orb paints itself; nothing here may touch its box. */
.orb { position: relative; flex-shrink: 0; border-radius: 9999px; }
.orb-canvas { position: absolute; inset: 0; height: 100%; width: 100%; display: block; }
/* no-WebGL fallback, hidden once the program links */
.orb-fallback {
  position: absolute; inset: 0; border-radius: 9999px;
  background: radial-gradient(circle at 34% 30%, #c3e7fc 0%, #74c4f2 42%, #2f9fe8 74%, #2f9fe8 100%);
  transition: opacity .2s ease;
}

.aw {
  --ground: var(--sch-surface, var(--page));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  width: 100%;
  max-width: 360px;
}
/* Fixed across both modes, or the panel collapses on switch and the toggle
   jumps out from under the cursor. */
.aw-stage { display: flex; align-items: center; justify-content: center; width: 100%; height: 320px; }

.aw-voice { display: flex; align-items: center; justify-content: center; gap: 12px; }
.aw-orbcol { display: flex; flex-direction: column; align-items: center; }
.aw-fade { transition: opacity 180ms ease; }
.aw-fade.is-out { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .aw-fade { transition: none; } }

.aw-chev {
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border: none; background: none; cursor: pointer; padding: 0;
  color: var(--text-muted);
  transition: color .15s ease;
}
/* They must never read as strongly as the orb. */
.aw-chev:hover, .aw-chev:focus-visible { color: var(--text-strong); outline: none; }
.aw-chev svg { width: 20px; height: 20px; }

.aw-call {
  position: relative;
  z-index: 10;              /* the canvas makes its own paint context: without
                               this the button's top half paints behind the orb */
  display: flex; align-items: center; justify-content: center;
  width: 56px; height: 56px;
  margin-top: -28px;        /* pull it onto the orb's bottom edge */
  border: none; border-radius: 9999px;
  background: #1b1b1b;
  color: #fff;
  cursor: pointer;
  /* The ring is a hole, not a border: it is the colour of whatever is behind
     the widget, so the button punches through the orb. Written as an explicit
     box-shadow so the var() fallback survives Tailwind-style class parsing.
     Unconditional — a ring that appears only during a call would resize the
     button on press. */
  box-shadow: 0 0 0 6px var(--ground);
  /* A colour, not an opacity: the canvas sits under the top half and the page
     under the bottom, so dimming reads as the orb coming through with a seam
     across the middle. Not a filter either — that would tint the ring off the
     ground colour. */
  transition: background-color .16s ease;
}
.aw-call:hover, .aw-call:focus-visible { background: #343434; outline: none; }
.aw-call.is-live { background: #b3261e; }
.aw-call.is-live:hover { background: #c8392f; }
.aw-call svg { width: 22px; height: 22px; }

.aw-chat { display: flex; flex-direction: column; width: 100%; height: 100%; }
.aw-greet { margin-top: auto; font-size: 15px; color: var(--text); }
.aw-form { width: 100%; margin-top: 12px; }
.aw-input {
  width: 100%; height: 44px;
  padding-inline: 14px;
  border-radius: 12px;
  border: 1px solid var(--rule);
  background: var(--page);
  color: var(--text-strong);
  font: inherit; font-size: 14px;
}
.aw-input::placeholder { color: var(--text-muted); }
.aw-input:focus { outline: none; border-color: color-mix(in srgb, var(--text-strong) 30%, transparent); }

.aw-toggle { display: flex; gap: 2px; padding: 3px; border-radius: 9999px; background: var(--hover); }
.aw-seg {
  padding: 6px 18px; border: none; border-radius: 9999px;
  background: none; color: var(--text-muted);
  font: inherit; font-size: 13px; font-weight: 500; cursor: pointer;
  transition: background-color .15s ease, color .15s ease, box-shadow .15s ease;
}
.aw-seg.is-on {
  background: var(--page);
  color: var(--text-strong);
  box-shadow: inset 0 0 0 1px var(--rule);
}

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

/* The widget owns its own 320px stage plus a toggle, so the craft stage only
   has to stop hugging. --ground is what the call button's ring punches to. */
.aw-hold { min-height: 460px; }

/* ---- Command bar ------------------------------------------------------
   bencho.dev/blocks/command (MIT). Layout figures are the block's own:
   56px bar, 28px corner, 0 8 0 20 padding, 6px gap, 36px mic, 38 -> 46px
   button. The fill is opaque because the goo's alpha contrast erases
   anything under ~0.39. */
.cb {
  --cb-fill: #26262a;
  --cb-ink: #f4f3f1;
  --cb-dim: #8e8e93;
  --cb-send-fill: #f4f3f1;
  --cb-send-ink: #17171a;
  position: relative;
  width: 100%;
  max-width: 340px;
  height: var(--cb-bar-h);
}
html[data-theme="light"] .cb {
  --cb-fill: #ececec;
  --cb-ink: #17171a;
  --cb-dim: #6e6e73;
  --cb-send-fill: #17171a;
  --cb-send-ink: #f4f3f1;
}
.cb-defs { position: absolute; width: 0; height: 0; pointer-events: none; }

/* layer 1: silhouettes, filtered */
.cb-goo { position: absolute; inset: 0; pointer-events: none; }
.cb-slot { position: absolute; top: 0; left: 0; }
.cb-slot-bar { width: 100%; height: var(--cb-bar-h); }
.cb-fill { width: 100%; height: 100%; border-radius: var(--cb-corner); background: var(--cb-fill); }
/* the button rides the bar's right edge, and morph positions it with a
   transform in layout pixels */
.cb-slot-btn {
  left: auto; right: calc(-1 * (var(--cb-btn) + var(--cb-gap)));
  top: 50%; margin-top: calc(var(--cb-btn) / -2);
  width: var(--cb-btn); height: var(--cb-btn);
  display: grid; place-items: center;
  transition: transform 420ms cubic-bezier(.22, 1.12, .36, 1);
}
.cb-dot {
  width: var(--cb-btn-idle); height: var(--cb-btn-idle);
  border-radius: 999px; background: var(--cb-fill);
  transition: width 420ms cubic-bezier(.22, 1.12, .36, 1),
              height 420ms cubic-bezier(.22, 1.12, .36, 1);
}

/* layer 2: the real controls, never filtered — the alpha contrast would
   destroy the glyphs */
.cb-ui { position: absolute; inset: 0; }
.cb-form {
  display: flex; align-items: center; gap: 6px;
  height: var(--cb-bar-h); padding: 0 8px 0 20px;
}
.cb-input {
  flex: 1; min-width: 0;
  border: none; background: none; outline: none;
  font: inherit; font-size: 15px; line-height: 19.5px;
  color: var(--cb-ink);
}
.cb-input::placeholder { color: var(--cb-dim); }

/* the mic only ever fades. No scale, no travel: the one thing it does is
   stop being there. */
.cb-mic {
  flex-shrink: 0;
  width: var(--cb-mic); height: var(--cb-mic);
  display: grid; place-items: center;
  border: none; background: none; border-radius: 999px;
  color: var(--cb-dim); cursor: pointer;
  opacity: 0; transition: opacity 150ms ease;
}
.cb-mic[data-show="true"] { opacity: 1; }
.cb-mic[data-show="false"] { pointer-events: none; }
.cb-mic svg { width: 20px; height: 20px; }

.cb-send {
  position: absolute; top: 50%; right: calc(-1 * (var(--cb-btn) + var(--cb-gap)));
  margin-top: calc(var(--cb-btn) / -2);
  width: var(--cb-btn); height: var(--cb-btn);
  display: grid; place-items: center;
  border: none; border-radius: 999px;
  background: transparent; color: var(--cb-ink);
  cursor: pointer;
  transition: transform 420ms cubic-bezier(.22, 1.12, .36, 1),
              background-color 220ms ease, color 220ms ease;
}
.cb.is-ready .cb-send { background: var(--cb-send-fill); color: var(--cb-send-ink); }
.cb-send:disabled { cursor: default; }
/* While the button is tucked under the bar its silhouette still belongs to
   the goo - that is what makes the two one body - but its glyph must not be
   there, or the arrow sits on top of the mic. The dot keeps the mass, the
   icon only ever fades, on the mic's own 150ms. */
.cb-send svg {
  width: 20px; height: 20px;
  opacity: 0; transition: opacity 150ms ease;
}
.cb.is-ready .cb-send svg { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .cb-slot-btn, .cb-dot, .cb-send, .cb-send svg, .cb-mic { transition: none; }
}

/* The button lives outside the bar's box, so the stage needs room on the
   right for it plus its gap. */
.cb-stage { min-height: 260px; }
.cbwrap { display: flex; justify-content: center; width: 100%; padding-right: 54px; }
