/* Page-level styles that used to live in each page's inline <style>.
 *
 * They had to move. Barba swaps the container, never <head> — so whichever page
 * the visitor landed on supplies the <head> for the whole session. Home-only
 * rules like the bento board simply vanished after navigating in from another
 * page, and same-named keyframes defined differently per page silently applied
 * the wrong one.
 *
 * Rules whose selectors are already unique to one page just live here. Rules
 * that would otherwise leak across pages are scoped with html[data-page="..."],
 * which router.js sets from the active barba namespace.
 */

/* ---- keyframes ---------------------------------------------------------- */
/* Shared by home, blog and projects card grids. */
@keyframes fade-up {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

/* The Work page animates an element that is also horizontally centred by a
 * transform, so its variant has to carry the -50% through. It used to be a
 * second, conflicting definition of `fade-up`; under one shared <head> only one
 * could win, so it is renamed rather than merged. */
@keyframes fade-up-centered {
  from { opacity: 0; transform: translate(-50%, 12px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes blink { 50% { opacity: 0; } }
@keyframes pulse-ring {
  0%   { transform: scale(1);    opacity: 0.7; }
  100% { transform: scale(1.35); opacity: 0; }
}
@keyframes ellipsis {
  0%   { content: '.';   }
  33%  { content: '..';  }
  66%  { content: '...'; }
}

/* ---- Work page ---------------------------------------------------------- */
/* This page alone is a fixed, full-viewport canvas, so it needs a full-height
 * root. Scoped, or every other page inherits a height it does not want. */
html[data-page="work"], html[data-page="work"] body { height: 100%; }
/* Scoped too: stripping focus outlines site-wide would be an accessibility
 * regression on the pages that rely on them. */
html[data-page="work"] input:focus { outline: none; }

.dots::after {
  content: '';
  display: inline-block;
  width: 18px; text-align: left;
  animation: ellipsis 1.4s steps(4) infinite;
}

/* ---- home: project dashboard (bento case-study morph) ------------------- */
/* No board of its own: the cards sit directly on the page so this section
   reads as part of the same sheet as the hero rather than a panel pasted onto
   it. That means the shell's own foreground is the normal, theme-flipping
   --ink, and every colour inside a card is derived from a theme token or
   currentColor -- there are no fixed light-board literals left to strand. */
.pf-root{background:transparent;padding:0;font-family:inherit;color:var(--ink);position:relative;overflow:hidden}
.pf-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;position:relative;z-index:10}
/* Muted derived from --ink now that the board is gone: this label sits on
   --paper like any other page text, so the flipping ink is the correct base. */
.pf-title{font-family:'JetBrains Mono',monospace;font-size:12px;letter-spacing:0.22em;color:color-mix(in srgb, var(--ink) 62%, transparent);text-transform:uppercase;transition:all 0.25s ease}
/* Hint that replaces the old "<- Overview" button: the exit from a focused
   case study is now a click anywhere on the board (or Escape), so this is a
   label, not a control. */
.pf-hint{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.08em;color:color-mix(in srgb, var(--ink) 72%, transparent);user-select:none;opacity:0;transition:opacity 0.25s ease}
.pf-hint.pf-show{opacity:1}
.pf-canvas{position:relative}
.pf-overlay{position:absolute;inset:0;z-index:50;pointer-events:none;clip-path:circle(0px at 0 0);background:var(--ink);border-radius:8px}
/* Equal columns and a symmetric first/last row are what make the pairs match.
   With the areas below, A spans cols 1-2 and E spans cols 2-3, so their widths
   are equal only when col1 == col3; B spans rows 1-2 and C spans rows 2-3, so
   their heights are equal only when row1 == row3. Satisfy both and the layout
   is a pinwheel: A == E (wide) and B == C (tall), with D the remainder. */
.pf-bento{display:grid;grid-template-columns:1fr 1fr 1fr;grid-template-rows:216px 186px 216px;grid-template-areas:"A A B" "C D B" "C E E";gap:12px;perspective:900px;perspective-origin:50% 50%}
.pf-slot{cursor:pointer;position:relative;transition:transform 0.15s ease;transform-style:preserve-3d}
.pf-slot:hover{transform:translateY(-2px)}
.pf-slot[data-slot="A"]{grid-area:A}
.pf-slot[data-slot="B"]{grid-area:B}
.pf-slot[data-slot="C"]{grid-area:C}
.pf-slot[data-slot="D"]{grid-area:D}
.pf-slot[data-slot="E"]{grid-area:E}
.pf-flipper{position:absolute;inset:0;transform-style:preserve-3d;will-change:transform}
.pf-face{position:absolute;inset:0;border-radius:14px;padding:22px;display:flex;flex-direction:column;justify-content:space-between;overflow:hidden;backface-visibility:hidden;-webkit-backface-visibility:hidden;transition:background-color 0.3s ease 0.05s,color 0.3s ease 0.05s;box-shadow:0 1px 2px rgba(30,30,28,0.06)}
.pf-face-back{transform:rotateY(180deg)}
.pf-body{flex:1;display:flex;flex-direction:column;justify-content:space-between;position:relative;z-index:1;transition:opacity 160ms ease}
.pf-brand-badge{position:absolute;top:12px;right:14px;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.2em;text-transform:uppercase;font-weight:600;opacity:0.5;z-index:2;transition:opacity 160ms ease}
.pf-brand-badge.pf-brand-active{opacity:1}
.pf-lbl{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;opacity:0.75}
.pf-root.pf-busy .pf-slot{pointer-events:none;cursor:default}
.pf-root.pf-busy .pf-slot:hover{transform:none}
.pf-root.pf-focused .pf-slot{cursor:pointer}
.pf-cta-mark{position:absolute;top:12px;left:14px;font-size:15px;opacity:0.6;z-index:2;line-height:1}
.pf-bento-clone{position:absolute;inset:0;pointer-events:none;z-index:40;-webkit-mask:url(#pf-diffuse-mask);mask:url(#pf-diffuse-mask)}
.pf-bento-clone .pf-slot,.pf-bento-clone .pf-body,.pf-bento-clone .pf-brand-badge,.pf-bento-clone .pf-cta-mark{transition:none}

/* ---- home: services (hover-to-open list with per-name ornaments) --------- */
/* Every selector is namespaced .sv-*: the original mock used .name / .body /
   .label / .tags / .rule / .on, all of which already mean something else in
   this codebase (.detail-body, --rule, .tag-row) and would have collided the
   moment they shared one stylesheet. */
/* ---- home: project dashboard overview cards ----------------------------- */
/* Each tile reads top to bottom: the domain worked in, the product mark, the
   product name. .pf-body is already a space-between column, so the domain pins
   itself to the top and .pf-mark takes the rest. */
.pf-domain {
  font-family: 'JetBrains Mono', monospace; font-size: 11px;
  letter-spacing: .18em; text-transform: uppercase;
  color: color-mix(in srgb, currentColor 62%, transparent);
}
.pf-mark {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px;
  text-align: center;
}
/* Height comes from --pf-logo-h, computed per logo in home.jsx so every mark
   covers the same ink AREA rather than sharing one height. A common height is
   what made the narrow ResearchSoup mark read as a quarter the size of the wide
   Femverse one. max-width is only a guard for a narrow tile. */
.pf-logo {
  display: block;
  height: var(--pf-logo-h, 56px); width: auto;
  max-width: 100%;
  object-fit: contain; object-position: center;
}
.pf-name {
  font-size: clamp(22px, 2.6vw, 34px);
  font-weight: 700; letter-spacing: -.025em; line-height: 1.04;
  text-wrap: balance;
}
/* Slot D only. E used to be short too, but it is now the same size as A, so it
   takes the standard mark; D is the one remaining tile short enough that a
   full-size logo would crowd the name. Desktop only: below 900px the bento stops
   being a grid and every tile gets a similar height. */
@media (min-width: 901px) {
  .pf-slot[data-slot="D"] .pf-logo { height: var(--pf-logo-h-sm, 40px); }
  .pf-slot[data-slot="D"] .pf-name { font-size: clamp(19px, 1.9vw, 25px); }
  .pf-slot[data-slot="D"] .pf-mark { gap: 10px; }
}

.sv-rule { height: 1px; background: var(--rule); position: relative; }
.sv-dot {
  position: absolute; top: -4px; left: 34%;
  width: 8px; height: 8px; border-radius: 50%; background: var(--ink);
}

.sv-list {
  display: grid;
  /* grid-template-rows is set inline by services.jsx -- one row per service, so
     the panels' `grid-row: 1 / -1` has an explicit grid to resolve -1 against.
     Without it the rows are implicit, -1 collapses to row 1, and the first
     heading's row stretches to fit the tallest panel. */
  grid-template-columns: minmax(0, 46%) minmax(0, 50%);
  column-gap: 4%;
  align-items: start;
  padding: 78px 0 96px;
}
/* display:contents so .sv-name and .sv-panel become direct grid items of
   .sv-list -- names stack in column 1, the open panel spans column 2. */
.sv-item { display: contents; }

.sv-name {
  /* The index slot and the flex gap are variables because three separate rules
     have to agree on them exactly: the slot's own width, the negative margin
     that cancels it while idle, and where .sv-track starts. Both are plain px
     so any descendant can consume them -- a ch/em derivation cannot, because
     those units resolve against whichever element uses them.
     16px fits "01".."04" at 12px JetBrains Mono (measured 15.85px). */
  --sv-idx-w: 16px;
  --sv-gap: 20px;
  grid-column: 1;
  display: flex; align-items: baseline; gap: var(--sv-gap);
  position: relative;
  margin: 0; padding: 30px 0;
  background: none; border: 0;
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: clamp(38px, 5vw, 70px);
  font-weight: 400; line-height: 1.02; letter-spacing: -.035em;
  color: var(--idle-ink);
  text-align: left; cursor: pointer;
  transition: color .3s ease;
}
/* The index is pulled entirely out of the flow while idle, by a negative margin
   equal to its own slot plus the flex gap, so the heading sits flush left with
   nothing reserved beside it. Handing that margin back on hover/turn is what
   slides the heading right -- the number does not so much appear beside the word
   as push it over.
   min-width:0 matters: flex items default to min-width:auto, which floors the
   used width at the content size. A ch-derived slot tripped over that -- `ch`
   resolved to about 6.6px inside this item while measuring 7.2px elsewhere, the
   floor then widened the box past the margin meant to cancel it, and every idle
   heading sat 1.2px off the margin. A fixed px slot plus this override keeps the
   two exactly equal. */
.sv-idx {
  flex: none;
  width: var(--sv-idx-w); min-width: 0;
  font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 400;
  letter-spacing: .06em; color: var(--idle-ink);
  margin-left: calc(-1 * var(--sv-idx-w) - var(--sv-gap));
  opacity: 0;
  transition: margin-left .38s cubic-bezier(.22, 1, .36, 1),
              opacity .26s ease, color .3s ease;
}
.sv-name:hover .sv-idx,
.sv-item.is-on .sv-idx { margin-left: 0; opacity: 1; }
.sv-word { position: relative; display: inline-block; white-space: pre; }
/* SplitText's charsClass. It sets display:inline-block inline as well, but
   stating it here means a yPercent tween cannot silently no-op if that
   behaviour ever changes -- transforms do nothing on an inline box. */
.sv-ch { display: inline-block; }
.sv-base { position: relative; z-index: 2; display: inline-block; }
.sv-arrow {
  margin-left: auto; font-size: 22px; color: var(--accent-ink);
  opacity: 0; transform: translate(-6px, 4px);
  transition: opacity .3s ease, transform .3s ease;
}
/* the underline starts exactly where the shifted word does */
/* No rail: the track is just the invisible extent the bar grows along, so only
   the running part is ever drawn. A visible full-width rule behind it read as a
   permanent underline on the heading rather than as progress. */
.sv-track {
  position: absolute; left: calc(var(--sv-idx-w) + var(--sv-gap)); right: 0; bottom: 18px;
  height: 2px; background: none;
  opacity: 0; transition: opacity .3s ease;
}
.sv-bar {
  display: block; height: 100%; width: 100%;
  /* not --mustard: that is a panel fill and reads 1.85:1 on --paper, which is
     less than the track behind it -- the countdown was invisible in light mode */
  background: var(--accent-ink);
  transform: scaleX(0); transform-origin: left;
}
.sv-name:hover { color: var(--ink-soft); }
.sv-name:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 6px; }

.sv-item.is-on .sv-name { color: var(--ink); }
.sv-name:hover .sv-idx,
.sv-item.is-on .sv-idx { color: var(--accent-ink); }
.sv-item.is-on .sv-arrow { opacity: 1; transform: none; }
.sv-item.is-on .sv-track { opacity: 1; }

/* ornament layers. All pointer-events:none -- the button underneath has to stay
   clickable through them, and overflow:visible because a trace or a lens ring
   deliberately extends past the word's own box. */
.sv-plot, .sv-wires, .sv-glass, .sv-sats, .sv-ghosts {
  position: absolute; inset: 0; overflow: visible; pointer-events: none;
}
.sv-plot, .sv-wires, .sv-ghosts { z-index: 1; }
.sv-glass, .sv-sats { z-index: 3; }
.sv-sat {
  position: absolute;
  font-family: 'Space Grotesk', system-ui, sans-serif; font-weight: 500;
  line-height: 1; color: var(--ink); opacity: 0;
}
.sv-ghost {
  position: absolute; left: 0; top: 0; white-space: pre;
  color: var(--ink); opacity: 0; user-select: none;
}
.sv-mag {
  position: absolute; left: 0; top: 0; white-space: pre;
  pointer-events: none; user-select: none;
  color: var(--ink); opacity: 0; z-index: 2;
  -webkit-text-stroke-width: 0; -webkit-text-stroke-color: currentColor;
}
.sv-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.sv-panel {
  grid-column: 2; grid-row: 1 / -1;
  align-self: start; padding-top: 36px;
  opacity: 0; visibility: hidden; transform: translateY(10px);
  transition: opacity .35s ease,
              transform .45s cubic-bezier(.22, 1, .36, 1),
              visibility 0s .35s;
}
.sv-item.is-on .sv-panel {
  opacity: 1; visibility: visible; transform: none; transition-delay: 0s;
}
.sv-panel h3 {
  margin: 0; max-width: 24ch;
  font-size: clamp(20px, 1.85vw, 27px); font-weight: 500;
  letter-spacing: -.022em; line-height: 1.24;
}
/* 52ch, not 42: the copy runs to four paragraphs now, and a narrower measure
   made the panel tall enough to stretch the heading rows it spans. Still inside
   the comfortable 45-75ch range for body text. */
.sv-panel p {
  margin: 18px 0 0; max-width: 52ch;
  font-size: 15.5px; line-height: 1.62; color: var(--ink-soft);
}
/* The load-bearing phrase. Bold is animated up from 400 by services.jsx; the
   colour lift to full --ink is static, so the emphasis survives with gsap absent
   or reduced-motion set. font-synthesis:none keeps the browser from faking a
   bold on top of the variable axis mid-tween. */
.sv-em {
  font-weight: 600;
  color: var(--ink);
  font-synthesis-weight: none;
}
.sv-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 28px; }
.sv-tags span {
  font-family: 'JetBrains Mono', monospace; font-size: 10.5px;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent-ink);
  border: 1px solid color-mix(in srgb, var(--accent-ink) 45%, transparent);
  border-radius: 999px; padding: 7px 15px;
}

/* Phones get a plain accordion: the grid, the progress track and every ornament
   are dropped rather than shrunk. The ornaments are measured off live glyph
   metrics and simply have nowhere to go at this width. */
@media (max-width: 900px) {
  .sv-list { display: block; padding: 30px 0 56px; }
  .sv-item { display: block; border-bottom: 1px solid var(--rule); }
  .sv-item:first-child { border-top: 1px solid var(--rule); }
  .sv-name { --sv-gap: 14px; width: 100%; padding: 22px 0; font-size: clamp(30px, 8vw, 42px); }
  .sv-track, .sv-plot, .sv-wires, .sv-sats, .sv-ghosts, .sv-mag, .sv-glass { display: none; }
  .sv-arrow { transform: rotate(90deg); opacity: .45; }
  .sv-item.is-on .sv-arrow { transform: rotate(180deg); opacity: 1; }
  /* padding moves onto the open state: with border-box a `height: 0` box still
     reserves its padding, so a closed panel kept 32px of dead space -- four of
     them put 128px of nothing between the headings. */
  .sv-panel { display: block; height: 0; padding: 0; overflow: hidden; transform: none; }
  .sv-item.is-on .sv-panel { height: auto; padding-bottom: 32px; }
}

@media (prefers-reduced-motion: reduce) {
  .sv-plot, .sv-wires, .sv-sats, .sv-ghosts, .sv-mag, .sv-glass { display: none; }
}
