/* ============================================================
   Al Jubaer — Portfolio & WebGIS Portal
   site.css

   Design direction: WHITE. One accent — teal, because the work is
   geospatial. Near-black text, hairline rules, generous whitespace,
   and no decoration that is not doing a job.

   Two accent systems, deliberately:
     --brand-*   the site's own teal. Every button, link and heading
                 accent. This is the "one accent colour".
     --accent    the project's category colour, injected inline by
                 _card.php. Used ONLY for the small category chip and
                 the card's hover title, so a grid of thirty projects
                 reads as one calm page with a hint of category —
                 not thirty competing brands.

   On white, restraint is the whole design. Shadows are almost
   invisible, borders are 1px at low contrast, and colour appears
   only where it means something.

   Contents
     1  tokens
     2  reset & base
     3  animated mesh background
     4  buttons, chips, pills
     5  header & footer
     6  hero & stats
     7  sections & headings
     8  category rail
     9  cards & grid
    10  filters & search
    11  project detail
    12  gallery & lightbox
    13  forms
    14  panels, notices, empty states
    15  motion & reveal
    16  responsive
   ============================================================ */

/* ---------- 1 · tokens ----------
   LIGHT, ALWAYS. There is no dark variant of this file and no
   prefers-color-scheme branch. color-scheme is pinned to light so
   the browser paints its own furniture — form controls, scrollbars,
   autofill — light too, rather than inverting them on a user whose
   OS is set to dark.

   Colour: a six-stop spectrum, not one accent. The six seeded
   categories each own a hue (cyan, emerald, violet, amber, pink,
   sky) and those hues arrive per-card as --accent / --accent-2 from
   the database. The spectrum below is the site's own voice; the
   category hue is the project's. They are allowed to sit together
   because both are pinned to the same lightness band.
   ------------------------------------------------------------- */
:root{
  color-scheme: light only;

  --bg:        #ffffff;
  --bg-2:      #f6f9fc;
  --surface:   #ffffff;
  --surface-2: #f2f6fa;
  --line:      #e3e9f0;
  --line-2:    #c6d0dd;

  --text:      #0b1020;
  --text-2:    #48546b;
  --text-3:    #7d899c;

  /* Category colour. The inline style on each card overrides these;
     these are the fallback for anything uncategorised. */
  --accent:    #0891b2;
  --accent-2:  #6366f1;

  /* ---- the spectrum ---- */
  --c-cyan:    #06b6d4;
  --c-blue:    #3b82f6;
  --c-indigo:  #6366f1;
  --c-violet:  #8b5cf6;
  --c-pink:    #ec4899;
  --c-amber:   #f59e0b;
  --c-emerald: #10b981;
  --c-sky:     #0ea5e9;

  /* Text-safe versions. The spectrum above is for fills and glows;
     these are the only ones allowed to carry words on white. Each
     clears 4.5:1 against #fff. */
  --c-cyan-ink:    #0e7490;
  --c-indigo-ink:  #4338ca;
  --c-violet-ink:  #6d28d9;
  --c-pink-ink:    #be185d;
  --c-amber-ink:   #b45309;
  --c-emerald-ink: #047857;

  /* Brand ramp — now a journey across the spectrum rather than three
     stops of one teal. brand-1 is the text-safe end and is what any
     coloured word or icon uses. */
  --brand-1:   #0e7490;
  --brand-2:   #0891b2;
  --brand-3:   #22d3ee;
  --on-brand:  #ffffff;

  --grad-brand:  linear-gradient(115deg,#0e7490 0%,#4338ca 48%,#6d28d9 100%);
  --grad-vivid:  linear-gradient(115deg,#06b6d4 0%,#6366f1 40%,#8b5cf6 70%,#ec4899 100%);
  --grad-warm:   linear-gradient(115deg,#f59e0b 0%,#ec4899 100%);
  --grad-cool:   linear-gradient(115deg,#0ea5e9 0%,#10b981 100%);

  --r-sm: 10px;
  --r:    16px;
  --r-lg: 22px;
  --r-xl: 30px;

  --wrap: 1200px;
  --pad:  clamp(1.1rem, 4vw, 2.5rem);

  /* Shadows stay quiet — the colour is doing the work now, and a
     heavy shadow on white still looks like a mistake. */
  --shadow:     0 1px 2px rgba(11,16,32,.04);
  --shadow-lg:  0 18px 44px -22px rgba(11,16,32,.26);
  --shadow-hue: 0 18px 44px -22px color-mix(in srgb, var(--accent) 45%, transparent);
  --glow:       0 0 0 1px var(--line);

  --ease:   cubic-bezier(.22,1,.36,1);
  --spring: cubic-bezier(.34,1.56,.64,1);
}

/* Belt and braces: if a browser, extension or embedded webview tries
   to force a dark rendering, these keep the page white rather than
   letting it half-invert into something unreadable. */
@media (prefers-color-scheme: dark){
  :root{
    --bg:#ffffff; --bg-2:#f6f9fc; --surface:#ffffff; --surface-2:#f2f6fa;
    --line:#e3e9f0; --line-2:#c6d0dd;
    --text:#0b1020; --text-2:#48546b; --text-3:#7d899c;
  }
  html,body{background:#fff;color:#0b1020}
}

/* ---------- 2 · reset & base ---------- */
*,*::before,*::after{box-sizing:border-box}

html{
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
  scroll-padding-top:88px;
}

body{
  margin:0;
  min-height:100vh;
  background:var(--bg);
  color:var(--text);
  font-family:Inter,system-ui,-apple-system,'Segoe UI',sans-serif;
  font-size:16px;
  line-height:1.65;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;
}

img,svg,video{max-width:100%;display:block}
img{height:auto}

a{color:inherit;text-decoration:none}

h1,h2,h3,h4{margin:0;line-height:1.14;letter-spacing:-.022em;font-weight:800}
p{margin:0}
ul{margin:0;padding:0;list-style:none}

code,kbd{font-family:'JetBrains Mono',ui-monospace,Menlo,monospace;font-size:.88em}

::selection{background:var(--brand-2);color:#fff}

:focus-visible{
  outline:2px solid var(--brand-1);
  outline-offset:3px;
  border-radius:4px;
}

.wrap{
  width:100%;
  max-width:var(--wrap);
  margin-inline:auto;
  padding-inline:var(--pad);
}

.skip{
  position:absolute;left:-9999px;top:0;z-index:999;
  background:var(--brand-2);color:#fff;padding:.8rem 1.2rem;border-radius:0 0 var(--r-sm) 0;
  font-weight:700;
}
.skip:focus{left:0}

.muted{color:var(--text-2)}
.small{font-size:.88rem}
.is-hidden{display:none !important}

/* The gradient headline word. Every stop is a text-safe ink from the
   spectrum — teal, indigo, violet, magenta — so the word travels
   across four hues and still clears 4.5:1 at its lightest point.
   The light end of the raw spectrum (#22d3ee, ~2.4:1) is deliberately
   not in here. A gradient you cannot read is decoration pretending
   to be typography.

   It drifts, slowly. 12s is long enough that you notice it only if
   you look, which is the correct amount of attention for a headline
   to ask twice. */
.grad{
  background:linear-gradient(115deg,
             #0e7490 0%, #4338ca 30%, #6d28d9 55%, #be185d 80%, #0e7490 100%);
  background-size:280% 100%;
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
  animation:gradroll 12s linear infinite;
}
@keyframes gradroll{to{background-position:280% 0}}

/* Same treatment, no motion — for anywhere a moving headline would
   compete with something more important. */
.grad-still{
  background:var(--grad-brand);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}

.icon{flex:none;vertical-align:-.18em}

.dot{
  width:7px;height:7px;border-radius:50%;background:#059669;flex:none;
  box-shadow:0 0 0 0 rgba(5,150,105,.6);
  animation:ping 2.2s var(--ease) infinite;
}
@keyframes ping{
  0%  {box-shadow:0 0 0 0 rgba(5,150,105,.55)}
  70% {box-shadow:0 0 0 8px rgba(5,150,105,0)}
  100%{box-shadow:0 0 0 0 rgba(5,150,105,0)}
}

/* ---------- 3 · animated background ----------
   Three coloured blobs drifting behind a white page, plus the
   graticule. They were removed once for reading as smudges — the fix
   is not to delete them but to get the numbers right:

     · saturation high, opacity low (.28–.34, not .6)
     · blur enormous (90–120px), so there is no edge to read as dirt
     · a white veil over the top, which is what keeps text on plain
       white instead of on a tint
     · they move slowly and never cross the centre column, so nothing
       ever drifts under a paragraph

   Cost: three composited layers, transform-only animation, no
   repaint. `will-change` is deliberately absent — on a fixed,
   z-index:-1 layer it buys nothing and costs memory on phones. */
.mesh{
  position:fixed;inset:0;z-index:-1;overflow:hidden;
  background:var(--bg);
  /* layout+paint only. `contain:strict` would add size containment,
     which is unnecessary here (inset:0 already fixes the box) and
     collapses the layer to nothing in a couple of older engines. */
  contain:layout paint;
}

.mesh-blob{
  position:absolute;display:block;border-radius:50%;
  filter:blur(100px);
  opacity:.30;
  mix-blend-mode:multiply;
}

.mesh-1{
  width:46vw;height:46vw;min-width:380px;min-height:380px;
  top:-12vh;left:-8vw;
  background:radial-gradient(circle at 30% 30%, var(--c-cyan), transparent 68%);
  animation:drift-1 26s var(--ease) infinite alternate;
}
.mesh-2{
  width:40vw;height:40vw;min-width:340px;min-height:340px;
  top:8vh;right:-10vw;
  background:radial-gradient(circle at 60% 40%, var(--c-violet), transparent 68%);
  animation:drift-2 31s var(--ease) infinite alternate;
}
.mesh-3{
  width:38vw;height:38vw;min-width:320px;min-height:320px;
  bottom:-14vh;left:26vw;
  background:radial-gradient(circle at 45% 55%, var(--c-amber), transparent 66%);
  opacity:.22;
  animation:drift-3 36s var(--ease) infinite alternate;
}

@keyframes drift-1{
  from{transform:translate3d(0,0,0) scale(1)}
  to  {transform:translate3d(6vw,7vh,0) scale(1.14)}
}
@keyframes drift-2{
  from{transform:translate3d(0,0,0) scale(1.06)}
  to  {transform:translate3d(-7vw,10vh,0) scale(.92)}
}
@keyframes drift-3{
  from{transform:translate3d(0,0,0) scale(.95)}
  to  {transform:translate3d(9vw,-8vh,0) scale(1.18)}
}

/* The veil. Without it the blobs tint the type; with it they stay
   behind a sheet of white and only the corners of the page carry
   colour. This one rule is the difference between "colourful" and
   "washed out". */
.mesh::after{
  content:'';position:absolute;inset:0;
  background:
    radial-gradient(ellipse 120% 70% at 50% 42%, #fff 34%, rgba(255,255,255,.72) 62%, transparent 100%),
    linear-gradient(180deg, rgba(255,255,255,.55) 0%, transparent 26%, transparent 74%, rgba(255,255,255,.75) 100%);
}

/* faint graticule — a quiet nod to the subject matter, now tinted
   rather than grey, and drifting one cell every 40s. */
.mesh-grid{
  position:absolute;inset:0;z-index:1;opacity:1;
  background-image:
    linear-gradient(rgba(14,116,144,.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(14,116,144,.055) 1px, transparent 1px);
  background-size:64px 64px;
  mask-image:radial-gradient(ellipse 90% 60% at 50% 0%, #000 30%, transparent 78%);
  -webkit-mask-image:radial-gradient(ellipse 90% 60% at 50% 0%, #000 30%, transparent 78%);
  animation:graticule 40s linear infinite;
}
@keyframes graticule{to{background-position:64px 64px}}

/* ---------- 4 · buttons, chips, pills ---------- */
.btn{
  display:inline-flex;align-items:center;gap:.55rem;
  font-size:.945rem;font-weight:700;letter-spacing:-.01em;
  padding:.78rem 1.35rem;border-radius:999px;border:1px solid transparent;
  cursor:pointer;white-space:nowrap;
  transition:transform .2s var(--ease), box-shadow .2s var(--ease),
             background .2s var(--ease), border-color .2s var(--ease), filter .2s;
}
.btn:hover{transform:translateY(-2px)}
.btn:active{transform:translateY(0)}
.btn-sm{padding:.55rem 1rem;font-size:.875rem}
.btn-lg{padding:.95rem 1.7rem;font-size:1.03rem}
.btn-block{width:100%;justify-content:center}

.btn-primary{
  background:var(--brand-1);
  color:var(--on-brand);
  box-shadow:0 1px 2px rgba(16,24,40,.06);
}
.btn-primary:hover{background:var(--brand-2);box-shadow:0 6px 16px -8px rgba(15,118,110,.55)}

/* Was frosted glass. On white there is nothing to frost, so it is
   now what it should always have been: a plain outlined button. */
.btn-glass{
  background:#fff;
  border-color:var(--line-2);
  color:var(--text);
}
.btn-glass:hover{background:var(--surface-2);border-color:var(--text-3)}

.btn-ghost{color:var(--text-2);border-color:var(--line)}
.btn-ghost:hover{color:var(--text);border-color:var(--line-2);background:var(--surface)}

.chip{
  display:inline-flex;align-items:center;gap:.36rem;
  font-size:.775rem;font-weight:650;letter-spacing:.005em;
  padding:.32rem .68rem;border-radius:999px;
  background:var(--surface);border:1px solid var(--line);color:var(--text-2);
  transition:.18s var(--ease);
}
.chip-cat{
  color:var(--accent);
  border-color:color-mix(in srgb, var(--accent) 34%, transparent);
  background:color-mix(in srgb, var(--accent) 12%, transparent);
}
.chip-cat:hover{background:color-mix(in srgb, var(--accent) 20%, transparent);transform:translateY(-1px)}
.chip-tech{font-family:'JetBrains Mono',monospace;font-size:.715rem;font-weight:500}
.chip-link{color:var(--text);border-color:var(--line-2)}
.chip-link:hover{background:var(--surface-2);transform:translateY(-1px)}

/* Darkened for white. The dark-theme values were mid-tones chosen to
   glow on near-black; on white they fail contrast badly. */
.chip-kind.kind-hosted     {color:#046c4e;border-color:#a7e3cd;background:#e8f7f1}
.chip-kind.kind-gallery    {color:#8a5200;border-color:#f2d49a;background:#fdf4e3}
.chip-kind.kind-publication{color:#9d1f5f;border-color:#f3bdd7;background:#fdeef5}
.chip-kind.kind-external   {color:#1d4ed8;border-color:#bcd0f7;background:#eef3fe}

.chips{display:flex;flex-wrap:wrap;gap:.42rem}

.pill{
  display:inline-flex;align-items:center;gap:.42rem;
  font-size:.875rem;font-weight:650;
  padding:.52rem 1rem;border-radius:999px;
  background:var(--surface);border:1px solid var(--line);color:var(--text-2);
  transition:.2s var(--ease);
}
.pill:hover{color:var(--text);border-color:var(--line-2);transform:translateY(-1px)}
.pill.is-on{
  color:var(--on-brand);border-color:transparent;
  background:var(--brand-1);
  box-shadow:0 4px 12px -6px rgba(15,118,110,.5);
}

.tag{
  font-size:.83rem;font-weight:600;padding:.4rem .8rem;border-radius:var(--r-sm);
  color:var(--text-3);transition:.18s;
}
.tag:hover{color:var(--text-2);background:var(--surface)}
.tag.is-on{color:var(--text);background:var(--surface-2);box-shadow:inset 0 0 0 1px var(--line-2)}

/* ---------- 5 · header & footer ---------- */
.nav{
  position:sticky;top:0;z-index:60;
  background:rgba(255,255,255,.82);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  border-bottom:1px solid transparent;
  transition:background .3s, border-color .3s, box-shadow .3s;
}
.nav.is-stuck{
  background:rgba(255,255,255,.96);
  border-bottom-color:var(--line);
  box-shadow:0 1px 3px rgba(16,24,40,.05);
}

.nav-inner{display:flex;align-items:center;gap:1.4rem;height:70px}

.brand{display:flex;align-items:center;gap:.65rem;font-weight:900;font-size:1.06rem;letter-spacing:-.03em}
.brand-mark{
  display:grid;place-items:center;width:38px;height:38px;border-radius:12px;
  background:var(--brand-1);
  color:var(--on-brand);box-shadow:0 4px 12px -6px rgba(15,118,110,.55);
  transition:transform .3s var(--ease);
}
.brand:hover .brand-mark{transform:rotate(-10deg) scale(1.06)}

.nav-links{display:flex;gap:.25rem;margin-left:auto}
.nav-links a{
  position:relative;padding:.5rem .9rem;border-radius:var(--r-sm);
  font-size:.925rem;font-weight:600;color:var(--text-2);transition:.18s;
}
.nav-links a:hover{color:var(--text);background:var(--surface-2)}
.nav-links a.is-active{color:var(--text)}
.nav-links a.is-active::after{
  content:'';position:absolute;left:.9rem;right:.9rem;bottom:.24rem;height:2px;border-radius:2px;
  background:var(--brand-1);
}

.nav-right{display:flex;align-items:center;gap:.6rem}
.nav-toggle{
  display:none;background:var(--surface);border:1px solid var(--line);
  color:var(--text);width:42px;height:42px;border-radius:12px;cursor:pointer;
  align-items:center;justify-content:center;
}

.foot{
  margin-top:5rem;padding-top:3.4rem;
  border-top:1px solid var(--line);
  background:var(--bg-2);
}
.foot-inner{display:grid;grid-template-columns:1.4fr 2fr;gap:2.6rem;padding-bottom:2.6rem}
.foot-title{font-size:1.25rem;font-weight:900;letter-spacing:-.03em}
.foot-tag{color:var(--text-3);font-size:.92rem;margin-top:.4rem;max-width:32ch}
.foot-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:1.6rem}
.foot-cols h4{font-size:.76rem;text-transform:uppercase;letter-spacing:.11em;
              color:var(--text-3);margin-bottom:.85rem;font-weight:700}
.foot-cols a{display:block;font-size:.92rem;color:var(--text-2);padding:.22rem 0;transition:.16s}
.foot-cols a:hover{color:var(--brand-1);transform:translateX(3px)}
.foot-base{
  display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;
  padding-block:1.3rem;border-top:1px solid var(--line);
  font-size:.86rem;color:var(--text-3);
}
.foot-base a:hover{color:var(--brand-1)}

/* ---------- 6 · hero & stats ---------- */
.hero{padding:clamp(3.5rem,10vw,7rem) 0 clamp(2.5rem,6vw,4rem)}
.hero-inner{max-width:60rem}

.hero-kicker{
  display:inline-flex;align-items:center;gap:.6rem;
  font-size:.845rem;font-weight:650;color:var(--text-2);
  padding:.44rem 1rem;border-radius:999px;
  background:var(--surface-2);border:1px solid var(--line);
}
.pulse{width:8px;height:8px;border-radius:50%;
       background:var(--brand-1);
       animation:ping 2.4s var(--ease) infinite}

.hero-title{
  font-size:clamp(2.5rem,7.2vw,5rem);
  font-weight:900;letter-spacing:-.045em;line-height:1.03;
  margin:1.5rem 0 0;
  max-width:16ch;
}
.hero-lede{
  margin-top:1.5rem;max-width:56ch;
  font-size:clamp(1.02rem,1.6vw,1.2rem);color:var(--text-2);line-height:1.72;
}
.hero-cta{display:flex;flex-wrap:wrap;gap:.8rem;margin-top:2.2rem}

.stats{
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;
  margin:3.4rem 0 0;padding:0;
  background:var(--line);border:1px solid var(--line);
  border-radius:var(--r-lg);overflow:hidden;
}
.stat{background:#fff;padding:1.4rem 1.2rem}
.stat dt{font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;
         color:var(--text-3);font-weight:700}
/* Solid, not gradient-clipped. Clipped text on white loses too much
   contrast at the light end of the ramp, and a statistic that is hard
   to read is not doing its job. */
.stat dd{
  margin:.35rem 0 0;font-size:clamp(1.6rem,3.4vw,2.3rem);font-weight:900;letter-spacing:-.04em;
  color:var(--text);
  font-variant-numeric:tabular-nums;
}

/* ---------- 7 · sections & headings ---------- */
.section{padding-block:clamp(2.6rem,6vw,4.4rem)}
.section-tight{padding-top:clamp(1.2rem,3vw,2rem)}
.section-tall{padding-block:clamp(4rem,12vw,8rem)}

.section-head{
  display:flex;align-items:flex-end;justify-content:space-between;gap:1.2rem;
  flex-wrap:wrap;margin-bottom:1.9rem;
}
.section-head h2{font-size:clamp(1.5rem,3.2vw,2.15rem);letter-spacing:-.035em}

.link-more{
  display:inline-flex;align-items:center;gap:.4rem;
  font-size:.9rem;font-weight:650;color:var(--text-2);transition:.18s;
}
.link-more:hover{color:var(--brand-1);gap:.6rem}

.page-head{padding:clamp(2.4rem,6vw,4.2rem) 0 0}
.page-title{font-size:clamp(2.1rem,5.4vw,3.6rem);font-weight:900;letter-spacing:-.042em;margin-top:.7rem}
.page-lede{margin-top:1rem;max-width:58ch;color:var(--text-2);font-size:1.04rem;line-height:1.7}

.crumb{font-size:.85rem;color:var(--text-3);display:flex;gap:.45rem;flex-wrap:wrap}
.crumb a:hover{color:var(--brand-1)}
.crumb span{opacity:.5}

/* ---------- 8 · category rail ---------- */
.cat-rail{
  display:grid;gap:1rem;
  grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
}
.cat{
  position:relative;overflow:hidden;
  display:flex;flex-direction:column;gap:.45rem;
  padding:1.5rem 1.35rem 1.35rem;
  border-radius:var(--r-lg);
  background:var(--surface);
  border:1px solid var(--line);
  transition:transform .3s var(--ease), border-color .3s, box-shadow .3s;
}
.cat::before{
  content:'';position:absolute;inset:0;opacity:0;transition:opacity .35s;
  background:linear-gradient(150deg,
             color-mix(in srgb, var(--accent) 7%, transparent),
             transparent 62%);
}
.cat:hover{transform:translateY(-3px);border-color:var(--line-2);box-shadow:var(--shadow-lg)}
.cat:hover::before{opacity:1}
.cat > *{position:relative}

.cat-icon{
  display:grid;place-items:center;width:46px;height:46px;border-radius:14px;
  margin-bottom:.5rem;
  color:var(--accent);
  background:color-mix(in srgb, var(--accent) 10%, #fff);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
  transition:transform .3s var(--ease);
}
.cat:hover .cat-icon{transform:scale(1.08) rotate(-6deg)}
.cat-name{font-weight:800;font-size:1.06rem;letter-spacing:-.02em}
.cat-blurb{font-size:.855rem;color:var(--text-2);line-height:1.55}
.cat-count{
  margin-top:.5rem;font-size:.75rem;font-weight:700;letter-spacing:.05em;
  text-transform:uppercase;color:var(--accent);
}

/* ---------- 9 · cards & grid ---------- */
.grid{
  display:grid;gap:1.35rem;
  grid-template-columns:repeat(auto-fill,minmax(310px,1fr));
}

.card{
  position:relative;display:flex;flex-direction:column;
  border-radius:var(--r-lg);overflow:hidden;
  background:var(--surface);
  border:1px solid var(--line);
  box-shadow:var(--shadow);
  transition:transform .35s var(--ease), border-color .3s, box-shadow .35s;
}
/* The inset white highlight was a dark-theme trick — a 1px rim of
   light along the top edge to lift a card off a black page. On white
   it is invisible at best. Removed rather than left as dead weight. */
.card::after{display:none}
.card:hover{
  transform:translateY(-4px);
  border-color:var(--line-2);
  box-shadow:var(--shadow-lg);
}

.card-media{
  position:relative;display:block;aspect-ratio:16/10;overflow:hidden;
  background:linear-gradient(140deg,
             color-mix(in srgb, var(--accent) 9%, #fff),
             color-mix(in srgb, var(--accent-2) 14%, #fff));
  border-bottom:1px solid var(--line);
}
.card-media img{
  width:100%;height:100%;object-fit:cover;
  transition:transform .55s var(--ease), filter .4s;
}
.card:hover .card-media img{transform:scale(1.055)}

.card-fallback{
  position:absolute;inset:0;display:grid;place-items:center;
  color:color-mix(in srgb, var(--accent) 42%, #fff);
}

/* A white pill on the artwork, not a dark one. It sits over covers
   whose top-left corner is now light, so a dark chip would be the
   loudest thing on the card. */
.card-kind{
  position:absolute;top:.72rem;left:.72rem;
  display:inline-flex;align-items:center;gap:.36rem;
  font-size:.71rem;font-weight:750;letter-spacing:.04em;text-transform:uppercase;
  padding:.3rem .62rem;border-radius:999px;
  background:rgba(255,255,255,.92);color:var(--text-2);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  border:1px solid rgba(16,24,40,.09);
  box-shadow:0 1px 2px rgba(16,24,40,.06);
}
.card-kind.kind-hosted{color:#046c4e}
.card-kind.kind-gallery{color:#8a5200}
.card-kind.kind-publication{color:#9d1f5f}
.card-kind.kind-external{color:#1d4ed8}

.card-body{display:flex;flex-direction:column;gap:.62rem;padding:1.15rem 1.2rem 1.05rem;flex:1}
.card-title{font-size:1.08rem;font-weight:750;letter-spacing:-.022em;line-height:1.32}
.card-title a{transition:color .18s}
.card:hover .card-title a{color:var(--accent)}
.card-sum{font-size:.885rem;color:var(--text-2);line-height:1.58}
.card-tech{display:flex;flex-wrap:wrap;gap:.34rem}

.card-foot{
  display:flex;align-items:center;justify-content:space-between;gap:.6rem;
  margin-top:auto;padding-top:.9rem;border-top:1px solid var(--line);
}
.card-go{
  display:inline-flex;align-items:center;gap:.4rem;
  font-size:.865rem;font-weight:700;color:var(--accent);transition:gap .2s var(--ease);
}
.card-go:hover{gap:.62rem}
.card-views{
  display:inline-flex;align-items:center;gap:.3rem;
  font-size:.78rem;color:var(--text-3);font-variant-numeric:tabular-nums;
}

/* ---------- 10 · filters & search ---------- */
.filters{
  display:flex;flex-direction:column;gap:1rem;
  padding:1.25rem;margin-bottom:2rem;
  border-radius:var(--r-lg);
  background:var(--bg-2);border:1px solid var(--line);
}
.filter-row{display:flex;flex-wrap:wrap;gap:.5rem}
.filter-bar{
  display:flex;align-items:center;justify-content:space-between;gap:1rem;
  flex-wrap:wrap;padding-top:.9rem;border-top:1px solid var(--line);
}

.search{
  position:relative;display:flex;align-items:center;gap:.55rem;
  padding:.55rem .95rem;border-radius:999px;min-width:min(100%,20rem);
  background:#fff;border:1px solid var(--line-2);
  color:var(--text-3);transition:border-color .2s, box-shadow .2s;
}
.search:focus-within{border-color:var(--brand-1);
                     box-shadow:0 0 0 3px color-mix(in srgb, var(--brand-1) 14%, transparent)}
.search input{
  flex:1;min-width:0;background:none;border:0;outline:none;
  color:var(--text);font:inherit;font-size:.92rem;
}
.search input::placeholder{color:var(--text-3)}
.search-clear{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;
              background:var(--surface-2);color:var(--text-2);transition:.16s}
.search-clear:hover{background:var(--line-2);color:var(--text)}

.result-count{
  font-size:.85rem;color:var(--text-3);display:flex;gap:.35rem;align-items:baseline;
}
.result-count strong{color:var(--text);font-size:1.05rem;font-variant-numeric:tabular-nums}

/* ---------- 11 · project detail ---------- */
.proj-head{padding:clamp(2.2rem,5vw,3.6rem) 0 1.8rem}
.proj-badges{display:flex;flex-wrap:wrap;gap:.45rem;margin:1rem 0 0}
.proj-title{font-size:clamp(2rem,5.2vw,3.4rem);font-weight:900;letter-spacing:-.042em;margin-top:1rem}
.proj-lede{margin-top:1.1rem;max-width:62ch;font-size:1.1rem;color:var(--text-2);line-height:1.7}
.proj-actions{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;margin-top:1.9rem}
.meta-note{font-size:.85rem;color:var(--text-3)}

.proj-cover{
  margin:0;border-radius:var(--r-xl);overflow:hidden;
  border:1px solid var(--line);box-shadow:var(--shadow-lg);
  background:linear-gradient(140deg,
             color-mix(in srgb, var(--accent) 8%, #fff),
             color-mix(in srgb, var(--accent-2) 12%, #fff));
}
.proj-cover img{width:100%}

.proj-grid{
  display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:2.6rem;
  padding-block:clamp(2rem,5vw,3.4rem);
}
.proj-body{font-size:1.03rem;line-height:1.82;color:var(--text-2);max-width:68ch}
.proj-body strong{color:var(--text)}

.proj-side{display:flex;flex-direction:column;gap:1.15rem}
.side-block{
  padding:1.2rem;border-radius:var(--r);
  background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--shadow);
}
.side-block h4{
  font-size:.74rem;text-transform:uppercase;letter-spacing:.11em;
  color:var(--text-3);font-weight:700;margin-bottom:.85rem;
}
.side-block > .btn{margin-top:.7rem}

.facts{display:grid;grid-template-columns:auto 1fr;gap:.5rem 1rem;margin:0;font-size:.9rem}
.facts dt{color:var(--text-3);font-weight:600}
.facts dd{margin:0;color:var(--text);text-align:right;font-weight:600}
.facts dd code{font-size:.8em;color:var(--accent)}

.pdf-frame{margin-top:2rem;border-radius:var(--r);overflow:hidden;border:1px solid var(--line)}
.pdf-frame object{width:100%;height:min(78vh,900px);display:block;background:var(--surface-2)}

.prose-grid{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:2.6rem}
.prose{font-size:1.06rem;line-height:1.85;color:var(--text-2);max-width:68ch}

.how{display:grid;gap:1.1rem;grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}
.how-step{
  position:relative;padding:1.6rem 1.3rem 1.3rem;border-radius:var(--r-lg);
  background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--shadow);
  transition:transform .3s var(--ease),border-color .3s,box-shadow .3s;
}
.how-step:hover{transform:translateY(-4px);border-color:var(--line-2);box-shadow:var(--shadow-lg)}
.how-num{
  position:absolute;top:1rem;right:1.15rem;font-size:2.6rem;font-weight:900;
  line-height:1;color:rgba(14,17,22,.06);letter-spacing:-.05em;
}
.how-icon{
  display:grid;place-items:center;width:42px;height:42px;border-radius:12px;margin-bottom:.9rem;
  background:var(--brand-1);color:var(--on-brand);
}
.how-step h3{font-size:1.03rem;font-weight:750;letter-spacing:-.02em}
.how-step p{margin-top:.5rem;font-size:.895rem;color:var(--text-2);line-height:1.62}

.ticks li{
  position:relative;padding-left:1.5rem;font-size:.9rem;color:var(--text-2);
  line-height:1.55;margin-bottom:.55rem;
}
.ticks li::before{
  content:'';position:absolute;left:0;top:.52em;width:7px;height:7px;border-radius:50%;
  background:var(--brand-1);
}

/* ---------- 12 · gallery & lightbox ---------- */
.gallery{
  display:grid;gap:1rem;
  grid-template-columns:repeat(auto-fill,minmax(260px,1fr));
}
.shot{
  margin:0;border-radius:var(--r);overflow:hidden;cursor:zoom-in;
  border:1px solid var(--line);background:var(--surface);
  transition:transform .3s var(--ease), border-color .3s;
}
.shot:hover{transform:translateY(-4px) scale(1.008);border-color:var(--line-2)}
.shot img{width:100%;aspect-ratio:4/3;object-fit:cover}
.shot figcaption{
  padding:.7rem .9rem;font-size:.83rem;color:var(--text-2);
  border-top:1px solid var(--line);
}

/* The last dark surface in the system, and now it is white too.

   It was kept dark on the usual argument — a black surround removes
   everything except the photograph. That argument is real, but it
   only holds for photographs. What opens in here are *maps*: pale
   basemaps, white page layouts, atlas spreads. A black surround
   around a white map does not disappear, it frames the map in a
   hard edge and drags the eye to the border.

   White surround, and the separation now comes from the shadow
   under the image rather than from a change of value. */
.lightbox{
  position:fixed;inset:0;z-index:200;
  display:grid;place-items:center;
  background:rgba(248,250,252,.94);
  backdrop-filter:blur(18px) saturate(1.2);
  -webkit-backdrop-filter:blur(18px) saturate(1.2);
  padding:clamp(1rem,4vw,3rem);
  animation:fadein .22s var(--ease);
}
.lightbox[hidden]{display:none}
@keyframes fadein{from{opacity:0}to{opacity:1}}

.lb-stage{margin:0;max-width:100%;max-height:100%;display:flex;flex-direction:column;gap:.9rem}
.lb-stage img{
  max-width:100%;max-height:80vh;object-fit:contain;
  border-radius:var(--r);box-shadow:var(--shadow-lg);
}
/* On a white surround the image needs its own edge, or a map with a
   white margin bleeds into the page. A hairline plus a real shadow
   does what the black background used to do. */
.lb-stage img{
  border:1px solid var(--line);
  box-shadow:0 32px 70px -30px rgba(11,16,32,.42);
  background:#fff;
}
.lb-stage figcaption{text-align:center;color:var(--text-2);font-size:.92rem;font-weight:600}

.lb-close,.lb-prev,.lb-next{
  position:absolute;display:grid;place-items:center;
  background:#fff;border:1px solid var(--line-2);color:var(--text);
  border-radius:50%;cursor:pointer;transition:.18s;
  box-shadow:0 6px 18px -8px rgba(11,16,32,.3);
  backdrop-filter:blur(10px);
}
.lb-close{top:1.2rem;right:1.2rem;width:46px;height:46px}
.lb-prev,.lb-next{top:50%;transform:translateY(-50%);width:52px;height:52px;font-size:1.9rem;line-height:0}
.lb-prev{left:1.2rem}
.lb-next{right:1.2rem}
.lb-close:hover,.lb-prev:hover,.lb-next:hover{
  background:var(--surface-2);border-color:var(--brand-2);color:var(--brand-1);
}
.lb-prev:hover,.lb-next:hover{transform:translateY(-50%) scale(1.06)}

/* ---------- 13 · forms ---------- */
.form{display:flex;flex-direction:column;gap:1.1rem}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem}
.field{display:flex;flex-direction:column;gap:.44rem}
.field > span{font-size:.845rem;font-weight:650;color:var(--text-2)}
.field em{color:#c2185b;font-style:normal}

.field input,.field textarea,.field select{
  width:100%;padding:.78rem 1rem;
  background:#fff;
  border:1px solid var(--line-2);border-radius:var(--r-sm);
  color:var(--text);font:inherit;font-size:.95rem;
  transition:border-color .2s, box-shadow .2s;
}
.field textarea{resize:vertical;min-height:7rem;line-height:1.65}
.field input:focus,.field textarea:focus,.field select:focus{
  outline:none;
  border-color:var(--brand-1);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--brand-1) 14%, transparent);
}
.field input::placeholder,.field textarea::placeholder{color:var(--text-3)}

.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

.form-note{font-size:.83rem;color:var(--text-3);line-height:1.6}

/* ---------- 14 · panels, notices, empty ---------- */
.panel{
  padding:clamp(1.4rem,3.4vw,2.1rem);border-radius:var(--r-lg);
  background:var(--surface);border:1px solid var(--line);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
}

.contact-grid{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:2.2rem}

.notice{padding:1.15rem 1.3rem;border-radius:var(--r);border:1px solid var(--line);margin-bottom:1.2rem}
.notice h3{font-size:1.1rem;margin-bottom:.5rem}
.notice p{color:var(--text-2);font-size:.94rem;line-height:1.65}
.notice .btn{margin-top:.9rem}
.notice-ok {background:#e8f7f1;border-color:#a7e3cd}
.notice-ok h3{color:#046c4e}
.notice-bad{background:#fdecec;border-color:#f3bfbf}
.notice-bad p{color:#96201f}

.empty{
  display:flex;flex-direction:column;align-items:center;gap:.85rem;
  padding:clamp(2.6rem,7vw,4.5rem) 1.5rem;text-align:center;
  border-radius:var(--r-xl);
  background:var(--bg-2);border:1px dashed var(--line-2);
  color:var(--text-3);
}
.empty h3{font-size:1.3rem;color:var(--text)}
.empty p{max-width:44ch;color:var(--text-2);font-size:.95rem;line-height:1.68}
.empty a{color:var(--brand-1);text-decoration:underline;text-underline-offset:3px}
.empty .btn{margin-top:.7rem}
.empty .btn a{text-decoration:none}

.four-oh-four{
  font-size:clamp(4.5rem,16vw,9rem);font-weight:900;line-height:.9;letter-spacing:-.06em;
  color:var(--line-2);
}

/* The one place colour fills a whole block. A single flat panel of
   the accent, at the bottom of a white page, is the full extent of
   the "bold" in this design — and it is enough. */
.cta-panel{
  display:flex;align-items:center;justify-content:space-between;gap:2rem;flex-wrap:wrap;
  padding:clamp(1.8rem,4.5vw,3rem);border-radius:var(--r-xl);
  background:var(--brand-1);
  border:1px solid var(--brand-1);
  color:var(--on-brand);
}
.cta-panel h2{color:var(--on-brand)}
.cta-panel .btn-primary{background:#fff;color:var(--brand-1)}
.cta-panel .btn-primary:hover{background:#f0fdfa}
.cta-panel .btn-glass{background:transparent;color:#fff;border-color:rgba(255,255,255,.45)}
.cta-panel .btn-glass:hover{background:rgba(255,255,255,.14);border-color:#fff}
.cta-panel h2{font-size:clamp(1.4rem,3.2vw,2rem);letter-spacing:-.035em}
.cta-panel p{margin-top:.55rem;color:rgba(255,255,255,.86);max-width:46ch}

/* ---------- 15 · motion & reveal ---------- */
.reveal{
  opacity:0;transform:translateY(18px);
  transition:opacity .65s var(--ease), transform .65s var(--ease);
  will-change:opacity, transform;
}
.reveal.is-in{opacity:1;transform:none}

/* Stagger cards inside a grid so the page arrives rather than snaps. */
.grid .reveal:nth-child(1){transition-delay:.03s}
.grid .reveal:nth-child(2){transition-delay:.07s}
.grid .reveal:nth-child(3){transition-delay:.11s}
.grid .reveal:nth-child(4){transition-delay:.15s}
.grid .reveal:nth-child(5){transition-delay:.19s}
.grid .reveal:nth-child(6){transition-delay:.23s}

.cat-rail .reveal:nth-child(2){transition-delay:.06s}
.cat-rail .reveal:nth-child(3){transition-delay:.12s}
.cat-rail .reveal:nth-child(4){transition-delay:.18s}
.cat-rail .reveal:nth-child(5){transition-delay:.24s}
.cat-rail .reveal:nth-child(6){transition-delay:.30s}

/* If someone has asked their system for less motion, honour it
   completely — including the background, which is the worst
   offender for anyone sensitive to movement. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
  .reveal{opacity:1;transform:none}
  .mesh-blob{animation:none}
}

/* ---------- 16 · responsive ---------- */
@media (max-width:1024px){
  .proj-grid,.prose-grid,.contact-grid{grid-template-columns:1fr}
  .proj-side{flex-direction:row;flex-wrap:wrap}
  .proj-side > *{flex:1 1 260px}
}

@media (max-width:860px){
  .nav-links{
    position:fixed;inset:70px 0 auto;flex-direction:column;gap:0;
    background:rgba(255,255,255,.98);backdrop-filter:blur(20px);
    padding:.8rem var(--pad) 1.4rem;border-bottom:1px solid var(--line);
    box-shadow:0 8px 20px -12px rgba(16,24,40,.2);
    transform:translateY(-120%);transition:transform .32s var(--ease);
    margin-left:0;
  }
  .nav-links.is-open{transform:none}
  .nav-links a{padding:.85rem .5rem;font-size:1rem;border-radius:0}
  .nav-links a.is-active::after{left:.5rem;right:auto;width:22px;bottom:.55rem}
  .nav-toggle{display:flex}
  .nav-right .btn{display:none}
  .foot-inner{grid-template-columns:1fr;gap:1.8rem}
}

@media (max-width:680px){
  .stats{grid-template-columns:repeat(2,1fr)}
  .field-row{grid-template-columns:1fr}
  .foot-cols{grid-template-columns:1fr 1fr}
  .filter-bar{flex-direction:column;align-items:stretch}
  .search{min-width:100%}
  .cta-panel{flex-direction:column;align-items:flex-start}
  .grid{grid-template-columns:1fr}
  .lb-prev,.lb-next{width:42px;height:42px;font-size:1.5rem}
  .lb-prev{left:.5rem} .lb-next{right:.5rem}
}

@media (max-width:420px){
  .foot-cols{grid-template-columns:1fr}
  .hero-cta .btn{width:100%;justify-content:center}
}

/* ============================================================
   17 · COLOUR & MOTION LAYER
   ============================================================
   Everything above is the structure and it is unchanged. This
   section is the light-but-colourful, animated pass, kept in one
   block so it can be read — or removed — as a unit.

   Three rules held throughout:

     1. Colour never carries meaning on its own. Every gradient
        here is decoration on top of something that already has a
        label, a border or a position.
     2. Ambient motion is slow and hue-only — gradients rolling
        through their own stops over 9–18s, and the background
        blobs over 26–36s. Nothing ambient moves position, changes
        size or competes with reading. Everything *fast* is a
        response: a hover, a scroll, a reveal.
     3. Every text gradient is built from the *-ink stops, which are
        the only spectrum values that clear 4.5:1 on white.
   ============================================================ */

/* ---------- buttons ---------- */
.btn-primary{
  position:relative;overflow:hidden;
  background:var(--grad-brand);
  background-size:200% 100%;
  border-color:transparent;
  box-shadow:0 2px 6px -2px rgba(67,56,202,.4);
  transition:transform .2s var(--spring), box-shadow .25s var(--ease),
             background-position .5s var(--ease);
}
.btn-primary:hover{
  background-position:100% 0;
  box-shadow:0 12px 26px -12px rgba(67,56,202,.62);
}

/* The sweep. A single specular band crossing the button once per
   hover — not a loop, because a button that shimmers permanently
   reads as "loading" rather than "clickable". */
.btn-primary::after{
  content:'';position:absolute;top:0;bottom:0;left:-60%;width:45%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.42),transparent);
  transform:skewX(-18deg);
  transition:none;
}
.btn-primary:hover::after{animation:sweep .75s var(--ease) 1}
@keyframes sweep{to{left:130%}}

.btn-glass:hover,.btn-ghost:hover{
  border-color:color-mix(in srgb, var(--brand-2) 45%, var(--line-2));
  color:var(--brand-1);
}

/* ---------- header ---------- */
.brand-mark{
  background:var(--grad-brand);
  background-size:200% 100%;
  box-shadow:0 6px 16px -8px rgba(67,56,202,.65);
  animation:gradroll 14s linear infinite;
}
.brand:hover .brand-mark{transform:rotate(-12deg) scale(1.1)}

/* The active-tab underline grows from the centre instead of simply
   being there, and it carries the spectrum. */
.nav-links a.is-active::after{
  background:var(--grad-vivid);
  animation:underline .38s var(--spring) 1;
  transform-origin:50% 50%;
}
@keyframes underline{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* Hovering an inactive tab previews the same underline, faintly. */
.nav-links a:not(.is-active){position:relative}
.nav-links a:not(.is-active)::after{
  content:'';position:absolute;left:.9rem;right:.9rem;bottom:.24rem;height:2px;
  border-radius:2px;background:var(--grad-vivid);
  transform:scaleX(0);transform-origin:50% 50%;
  transition:transform .28s var(--spring);
}
.nav-links a:not(.is-active):hover::after{transform:scaleX(1)}

.nav.is-stuck{box-shadow:0 1px 0 var(--line), 0 8px 24px -20px rgba(11,16,32,.45)}

/* A hairline of spectrum along the very top of a stuck header. It is
   the smallest possible reminder that this is a colourful site, on
   the one element that is always on screen. */
.nav::before{
  content:'';position:absolute;inset:0 0 auto;height:2px;
  background:var(--grad-vivid);background-size:200% 100%;
  opacity:0;transition:opacity .3s;
  animation:gradroll 9s linear infinite;
}
.nav.is-stuck::before{opacity:1}

/* ---------- hero ---------- */
.hero-kicker{
  border-color:transparent;
  background:
    linear-gradient(#fff,#fff) padding-box,
    var(--grad-vivid) border-box;
  border:1px solid transparent;
  background-size:auto, 200% 100%;
  animation:gradroll 11s linear infinite;
}
.pulse{background:var(--c-cyan)}
.dot{background:var(--c-emerald)}

/* ---------- stats ---------- */
.stats{background:var(--line);border-color:var(--line)}
.stat{position:relative;transition:background .3s var(--ease)}
.stat::before{
  content:'';position:absolute;inset:0 0 auto;height:3px;
  background:var(--grad-vivid);
  transform:scaleX(0);transform-origin:0 50%;
  transition:transform .45s var(--ease);
}
.stat:hover::before{transform:scaleX(1)}
.stat:hover{background:var(--bg-2)}

/* Gradient numerals. Every stop is a text-safe ink, and at 900 weight
   / 2.3rem the lightest of them still reads cleanly. */
.stat dd{
  background:linear-gradient(115deg,#0e7490 0%,#4338ca 55%,#6d28d9 100%);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}
/* Counting up needs a fixed width or the grid jitters as digits
   change. tabular-nums is already set; this pins the baseline. */
.stat dd[data-count]{min-height:1.15em}

/* ---------- section headings ---------- */
.section-head h2,.page-title,.proj-title{position:relative}
.section-head h2::after{
  content:'';position:absolute;left:0;bottom:-.5rem;width:2.4rem;height:3px;border-radius:3px;
  background:var(--grad-vivid);
}
.link-more:hover{color:var(--c-indigo-ink)}

/* ---------- category rail ---------- */
/* This is where the six seeded hues finally do some work. Each card
   already receives --accent / --accent-2 inline from the database,
   so the rail is six different colours without a single hard-coded
   value in this file. */
.cat{transition:transform .35s var(--spring), border-color .3s, box-shadow .35s}
.cat::after{
  content:'';position:absolute;inset:auto 0 0;height:3px;
  background:linear-gradient(90deg,var(--accent),var(--accent-2));
  transform:scaleX(0);transform-origin:0 50%;
  transition:transform .4s var(--ease);
}
.cat:hover::after{transform:scaleX(1)}
.cat:hover{
  transform:translateY(-5px);
  border-color:color-mix(in srgb, var(--accent) 32%, var(--line));
  box-shadow:var(--shadow-hue);
}
.cat-icon{
  background:linear-gradient(140deg,var(--accent),var(--accent-2));
  color:#fff;
  box-shadow:0 8px 18px -10px color-mix(in srgb, var(--accent) 75%, transparent);
}
.cat:hover .cat-icon{transform:scale(1.12) rotate(-8deg)}
/* The category hues arrive from the database at their 500 step —
   #06b6d4, #f59e0b, #ec4899 — which is right for a fill and wrong
   for a word: on white they run 2.4:1 to 3.2:1.

   `filter: brightness()` was the obvious fix and is the wrong one:
   it scales channels multiplicatively, so it darkens an already-dark
   hue far more than a light one, and the six categories end up at
   six different contrast ratios. Mixing a fixed 35% of the page's
   ink into each instead lands all six between 4.9:1 and 6.4:1,
   whatever hue they started from. */
.cat-count{
  background:linear-gradient(90deg,
             color-mix(in srgb, var(--accent) 65%, #0b1020),
             color-mix(in srgb, var(--accent-2) 65%, #0b1020));
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}

/* ---------- cards ---------- */
.card{transition:transform .4s var(--spring), border-color .3s, box-shadow .4s}
.card:hover{
  transform:translateY(-6px);
  border-color:color-mix(in srgb, var(--accent) 30%, var(--line));
  box-shadow:var(--shadow-hue);
}
/* Re-enabled — but as the category hue along the top edge, which is
   the one place a coloured line tells you something (which category
   this project is in) rather than merely decorating. */
.card::after{
  display:block;content:'';position:absolute;inset:0 0 auto;height:3px;z-index:2;
  background:linear-gradient(90deg,var(--accent),var(--accent-2));
  transform:scaleX(0);transform-origin:0 50%;
  transition:transform .45s var(--ease);
}
.card:hover::after{transform:scaleX(1)}

.card-media{
  background:linear-gradient(140deg,
             color-mix(in srgb, var(--accent) 16%, #fff),
             color-mix(in srgb, var(--accent-2) 22%, #fff));
}
.card:hover .card-media img{transform:scale(1.07)}
.card-fallback{color:color-mix(in srgb, var(--accent) 55%, #fff)}
/* Same 65% ink mix as .cat-count, for the same reason. */
.card-go{
  background:linear-gradient(90deg,
             color-mix(in srgb, var(--accent) 65%, #0b1020),
             color-mix(in srgb, var(--accent-2) 65%, #0b1020));
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}

/* ---------- accent-as-text, corrected ---------- */
/* Every remaining place the raw category hue was used as a text
   colour. Same 65% ink mix, same reason: --accent is a 500 step and
   a 500 step is not a text colour on white. Defined once, here,
   rather than repeated at each site. */
.card:hover .card-title a,
.chip-cat,
.facts dd code{
  color:color-mix(in srgb, var(--accent) 65%, #0b1020);
}

/* ---------- pills & chips ---------- */
.pill.is-on{
  background:var(--grad-brand);
  background-size:200% 100%;
  box-shadow:0 6px 16px -8px rgba(67,56,202,.55);
  animation:gradroll 12s linear infinite;
}
.pill:hover{border-color:color-mix(in srgb, var(--brand-2) 40%, var(--line-2))}
.chip-cat{
  background:linear-gradient(120deg,
             color-mix(in srgb, var(--accent) 14%, #fff),
             color-mix(in srgb, var(--accent-2) 18%, #fff));
  border-color:color-mix(in srgb, var(--accent) 38%, transparent);
}

/* ---------- how-it-works ---------- */
.how-icon{
  background:var(--grad-vivid);background-size:200% 100%;
  box-shadow:0 8px 18px -10px rgba(99,102,241,.6);
  animation:gradroll 13s linear infinite;
}
.how-step{transition:transform .35s var(--spring),border-color .3s,box-shadow .35s}
.how-step:hover{transform:translateY(-6px);box-shadow:var(--shadow-lg)}
.how-num{
  background:var(--grad-vivid);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
  opacity:.16;
}
.ticks li::before{background:linear-gradient(120deg,var(--c-cyan),var(--c-indigo))}

/* ---------- forms & search ---------- */
.search:focus-within{
  border-color:var(--c-indigo);
  box-shadow:0 0 0 3px rgba(99,102,241,.16);
}
.field input:focus,.field textarea:focus,.field select:focus{
  border-color:var(--c-indigo);
  box-shadow:0 0 0 3px rgba(99,102,241,.16);
}
.field em{color:var(--c-pink-ink)}

/* ---------- the closing panel ---------- */
/* The one block where colour fills the whole width. It is a moving
   gradient rather than a flat fill, and it is the last thing on the
   page, which is where a site is allowed to raise its voice. */
.cta-panel{
  position:relative;overflow:hidden;
  background:var(--grad-vivid);
  background-size:220% 100%;
  border-color:transparent;
  animation:gradroll 16s linear infinite;
  box-shadow:0 24px 60px -32px rgba(99,102,241,.75);
}
.cta-panel::before{
  content:'';position:absolute;inset:0;
  background:radial-gradient(ellipse 60% 120% at 82% 50%, rgba(255,255,255,.22), transparent 60%);
  pointer-events:none;
}
.cta-panel > *{position:relative}
.cta-panel .btn-primary{background:#fff;color:var(--c-indigo-ink);box-shadow:0 8px 20px -10px rgba(11,16,32,.4)}
.cta-panel .btn-primary::after{display:none}
.cta-panel .btn-primary:hover{background:#fff;transform:translateY(-3px)}

/* ---------- 404 ---------- */
.four-oh-four{
  background:var(--grad-vivid);background-size:200% 100%;
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
  opacity:.55;
  animation:gradroll 10s linear infinite;
}

/* ---------- footer ---------- */
.foot{
  background:
    linear-gradient(180deg, #fff 0%, var(--bg-2) 40%),
    var(--bg-2);
  border-top:0;
  position:relative;
}
.foot::before{
  content:'';position:absolute;inset:0 0 auto;height:2px;
  background:var(--grad-vivid);background-size:200% 100%;
  animation:gradroll 18s linear infinite;
}
.foot-title{
  background:var(--grad-brand);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent;
}
.foot-cols a:hover{color:var(--c-indigo-ink)}
.foot-base a:hover{color:var(--c-indigo-ink)}
.crumb a:hover{color:var(--c-indigo-ink)}
.empty a{color:var(--c-indigo-ink)}
::selection{background:rgba(99,102,241,.22);color:var(--text)}
:focus-visible{outline-color:var(--c-indigo)}

/* ---------- reveal: longer stagger ---------- */
/* The original stopped at six, so a 32-project grid had 26 cards
   arriving simultaneously. Twelve steps, then it holds — past that
   the delay would be longer than the patience. */
.grid .reveal:nth-child(7) {transition-delay:.27s}
.grid .reveal:nth-child(8) {transition-delay:.31s}
.grid .reveal:nth-child(9) {transition-delay:.35s}
.grid .reveal:nth-child(10){transition-delay:.39s}
.grid .reveal:nth-child(11){transition-delay:.43s}
.grid .reveal:nth-child(n+12){transition-delay:.46s}

/* A slightly richer entrance than a plain slide: cards also come up
   from 97% scale, which reads as "settling into place". */
.grid .reveal,.cat-rail .reveal,.how .reveal{
  transform:translateY(22px) scale(.985);
}
.grid .reveal.is-in,.cat-rail .reveal.is-in,.how .reveal.is-in{
  transform:none;
}

/* ---------- scroll progress ---------- */
/* Uses scroll-driven animations where they exist and simply does not
   appear where they do not. No JavaScript, no layout cost, no
   fallback needed — a progress bar is not load-bearing. */
@supports (animation-timeline: scroll()){
  body::before{
    content:'';position:fixed;top:0;left:0;height:3px;width:100%;z-index:120;
    transform-origin:0 50%;transform:scaleX(0);
    background:var(--grad-vivid);
    animation:progress linear;
    animation-timeline:scroll(root block);
  }
  @keyframes progress{to{transform:scaleX(1)}}
}

/* ---------- pointer spotlight ---------- */
/* site.js writes --mx / --my as percentages while the cursor is over
   a card. When it is not, the properties are absent and the fallback
   puts the highlight at 50% 0% with zero opacity — so nothing shows
   and nothing has to be reset. */
.card::before{
  content:'';position:absolute;inset:0;z-index:1;pointer-events:none;
  border-radius:inherit;
  background:radial-gradient(320px circle at var(--mx,50%) var(--my,0%),
             color-mix(in srgb, var(--accent) 16%, transparent), transparent 62%);
  opacity:0;transition:opacity .35s var(--ease);
}
.card:hover::before{opacity:1}

.cat::before{
  background:radial-gradient(320px circle at var(--mx,50%) var(--my,0%),
             color-mix(in srgb, var(--accent) 15%, transparent), transparent 62%);
}

/* The card's own content has to sit above the highlight. .card-media
   and .card-body are the only two children, and both are static by
   default, so z-index alone would do nothing without this. */
.card > *{position:relative;z-index:2}

/* ============================================================
   18 · PROFILE SECTIONS
   ============================================================
   Career timeline, services, skills, publications. Each item
   carries its own --accent inline from the database, exactly like
   a project card, so these sections are coloured by their data
   rather than by anything hard-coded here.
   ============================================================ */

.section-lede{margin-top:.7rem;max-width:54ch;line-height:1.7}

/* ---------- career timeline ---------- */
.tl{
  position:relative;
  display:flex;flex-direction:column;gap:.4rem;
  padding:0;margin:0;list-style:none;
}

/* The rail. A gradient rather than a flat rule, so the eye is pulled
   down it — and masked at both ends so it fades in and out instead of
   stopping dead against whitespace. */
.tl::before{
  content:'';position:absolute;left:19px;top:8px;bottom:8px;width:2px;
  background:linear-gradient(180deg,var(--c-cyan),var(--c-indigo) 45%,var(--c-pink));
  opacity:.28;
  mask-image:linear-gradient(180deg,transparent,#000 6%,#000 94%,transparent);
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 6%,#000 94%,transparent);
}

.tl-item{
  position:relative;
  display:grid;grid-template-columns:40px minmax(0,1fr);
  gap:1.1rem;align-items:start;
  padding:1rem 1.1rem 1rem 0;
  border-radius:var(--r);
  transition:background .3s var(--ease);
}
.tl-item:hover{background:linear-gradient(90deg,
                color-mix(in srgb, var(--accent) 7%, transparent), transparent 60%)}

.tl-dot{
  position:relative;z-index:1;
  display:grid;place-items:center;width:40px;height:40px;border-radius:13px;
  background:#fff;color:var(--accent);
  box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 34%, transparent),
             0 6px 16px -8px color-mix(in srgb, var(--accent) 70%, transparent);
  transition:transform .35s var(--spring);
}
.tl-item:hover .tl-dot{transform:scale(1.1) rotate(-8deg)}

/* The current role fills its marker in and keeps a slow halo, which is
   the one piece of ambient motion in this section. */
.tl-item.is-current .tl-dot{
  background:linear-gradient(140deg,var(--accent),color-mix(in srgb,var(--accent) 55%,#6366f1));
  color:#fff;
  animation:halo 2.8s var(--ease) infinite;
}
@keyframes halo{
  0%  {box-shadow:0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent)}
  70% {box-shadow:0 0 0 12px transparent}
  100%{box-shadow:0 0 0 0 transparent}
}

.tl-when{
  font-size:.78rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  color:var(--text-3);
  display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;
}
.tl-now{
  display:inline-flex;align-items:center;gap:.35rem;
  color:color-mix(in srgb, var(--accent) 65%, #0b1020);
  text-transform:none;letter-spacing:0;font-size:.75rem;
}
.tl-role{font-size:1.12rem;font-weight:800;letter-spacing:-.022em;margin-top:.3rem}
.tl-org{margin-top:.15rem;font-size:.93rem;font-weight:650;
        color:color-mix(in srgb, var(--accent) 65%, #0b1020)}
.tl-org a{display:inline-flex;align-items:center;gap:.3rem;transition:gap .2s var(--ease)}
.tl-org a:hover{gap:.45rem;text-decoration:underline;text-underline-offset:3px}
.tl-what{margin-top:.5rem;font-size:.92rem;color:var(--text-2);line-height:1.65;max-width:62ch}

/* Timeline items arrive one after another, top to bottom. */
.tl .reveal{transform:translateX(-14px)}
.tl .reveal.is-in{transform:none}
.tl .reveal:nth-child(1){transition-delay:.02s}
.tl .reveal:nth-child(2){transition-delay:.08s}
.tl .reveal:nth-child(3){transition-delay:.14s}
.tl .reveal:nth-child(4){transition-delay:.20s}
.tl .reveal:nth-child(5){transition-delay:.26s}
.tl .reveal:nth-child(6){transition-delay:.32s}
.tl .reveal:nth-child(n+7){transition-delay:.36s}

/* ---------- services ---------- */
.svc-grid{
  display:grid;gap:1.1rem;
  grid-template-columns:repeat(auto-fit,minmax(250px,1fr));
}
.svc{
  position:relative;overflow:hidden;
  padding:1.5rem 1.35rem 1.35rem;border-radius:var(--r-lg);
  background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--shadow);
  transition:transform .35s var(--spring), border-color .3s, box-shadow .35s;
}
.svc::after{
  content:'';position:absolute;inset:auto 0 0;height:3px;
  background:linear-gradient(90deg,var(--accent),color-mix(in srgb,var(--accent) 40%,#6366f1));
  transform:scaleX(0);transform-origin:0 50%;transition:transform .4s var(--ease);
}
.svc:hover{
  transform:translateY(-5px);
  border-color:color-mix(in srgb, var(--accent) 30%, var(--line));
  box-shadow:var(--shadow-hue);
}
.svc:hover::after{transform:scaleX(1)}

.svc-icon{
  display:grid;place-items:center;width:44px;height:44px;border-radius:13px;
  margin-bottom:.9rem;color:#fff;
  background:linear-gradient(140deg,var(--accent),color-mix(in srgb,var(--accent) 45%,#6366f1));
  box-shadow:0 8px 18px -10px color-mix(in srgb, var(--accent) 80%, transparent);
  transition:transform .35s var(--spring);
}
.svc:hover .svc-icon{transform:scale(1.1) rotate(-7deg)}
.svc h3{font-size:1.03rem;font-weight:780;letter-spacing:-.02em}
.svc p{margin-top:.45rem;font-size:.885rem;color:var(--text-2);line-height:1.6}

/* ---------- skills ---------- */
.skill-cols{
  display:grid;gap:1.2rem;
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr));
}
.skill-group{
  padding:1.3rem 1.25rem;border-radius:var(--r-lg);
  background:var(--surface);border:1px solid var(--line);
  border-top:3px solid var(--accent);
  box-shadow:var(--shadow);
  transition:transform .3s var(--ease), box-shadow .3s;
}
.skill-group:hover{transform:translateY(-3px);box-shadow:var(--shadow-hue)}
.skill-group h4{
  font-size:.76rem;text-transform:uppercase;letter-spacing:.11em;font-weight:750;
  color:color-mix(in srgb, var(--accent) 65%, #0b1020);
  margin-bottom:.9rem;
}
.skill-list{display:flex;flex-wrap:wrap;gap:.4rem}
.skill-list li{
  font-size:.83rem;font-weight:600;
  padding:.34rem .7rem;border-radius:999px;
  color:var(--text-2);
  background:color-mix(in srgb, var(--accent) 8%, #fff);
  border:1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  transition:.2s var(--ease);
}
.skill-list li:hover{
  transform:translateY(-2px);
  color:color-mix(in srgb, var(--accent) 68%, #0b1020);
  border-color:color-mix(in srgb, var(--accent) 45%, transparent);
}

/* ---------- publications ---------- */
.pubs{display:flex;flex-direction:column;gap:.8rem;padding:0;margin:0;list-style:none}
.pub{
  display:grid;grid-template-columns:74px minmax(0,1fr);gap:1.1rem;align-items:start;
  padding:1.15rem 1.3rem;border-radius:var(--r-lg);
  background:var(--surface);border:1px solid var(--line);
  box-shadow:var(--shadow);
  transition:transform .3s var(--ease), border-color .3s, box-shadow .3s;
}
.pub:hover{
  transform:translateX(4px);
  border-color:color-mix(in srgb, var(--accent) 30%, var(--line));
  box-shadow:var(--shadow-hue);
}
.pub-year{
  display:grid;place-items:center;
  padding:.4rem .2rem;border-radius:var(--r-sm);
  font-size:.92rem;font-weight:850;font-variant-numeric:tabular-nums;
  color:#fff;
  background:linear-gradient(140deg,var(--accent),color-mix(in srgb,var(--accent) 45%,#6366f1));
}
.pub-body h3{font-size:1.03rem;font-weight:750;line-height:1.4;letter-spacing:-.018em}
.pub-body h3 a{display:inline-flex;align-items:baseline;gap:.35rem;transition:color .18s}
.pub-body h3 a:hover{color:color-mix(in srgb, var(--accent) 65%, #0b1020)}
.pub-where{margin-top:.3rem;font-size:.89rem;color:var(--text-2);font-style:italic}
.pub-body .chip{margin-top:.6rem}

/* ---------- responsive corrections for this layer ---------- */
/* Section 16 runs before this one, so the mobile menu's underline
   geometry has to be restated here or the hover bar spans the whole
   row instead of sitting under the word. */
@media (max-width:860px){
  .nav-links a:not(.is-active)::after{
    left:.5rem;right:auto;width:22px;bottom:.55rem;
  }
  /* Three blurred layers at 100px is a real cost on a mid-range
     phone. Two, smaller and less blurred, keeps the colour without
     the scroll jank. */
  .mesh-3{display:none}
  .mesh-blob{filter:blur(70px);opacity:.26}
  .mesh-1,.mesh-2{width:70vw;height:70vw}
}

@media (max-width:680px){
  /* The timeline keeps its rail but tightens; the marker stays because
     it is the only thing carrying the role's colour. */
  .tl::before{left:15px}
  .tl-item{grid-template-columns:32px minmax(0,1fr);gap:.85rem;padding:.85rem .5rem .85rem 0}
  .tl-dot{width:32px;height:32px;border-radius:11px}
  .tl-role{font-size:1.04rem}

  /* The year badge above the citation rather than beside it — at 74px
     wide in a 2-column grid it left the title about 12 characters. */
  .pub{grid-template-columns:1fr;gap:.7rem}
  .pub-year{justify-self:start;padding:.25rem .7rem}

  .svc-grid,.skill-cols{grid-template-columns:1fr}
}

/* ---------- reduced motion ---------- */
/* The blanket rule in section 15 already kills durations. These stop
   the infinite gradient loops from freezing mid-sweep on a colour
   that was only ever meant to be passed through. */
@media (prefers-reduced-motion:reduce){
  .grad,.brand-mark,.hero-kicker,.pill.is-on,.how-icon,
  .cta-panel,.four-oh-four,.foot::before,.nav::before,.mesh-grid{
    animation:none !important;
    background-position:0 0 !important;
  }
  .mesh-blob{animation:none !important}
  .btn-primary::after{display:none}
  body::before{display:none}
  .card::before{display:none}
  .grid .reveal,.cat-rail .reveal,.how .reveal{transform:none}
  /* The current-role halo is the only looping animation in the profile
     sections, and a pulsing marker is exactly what someone with motion
     sensitivity asked not to have. */
  .tl-item.is-current .tl-dot{animation:none !important}
  .tl .reveal{transform:none}
}

@media print{
  .mesh,.nav,.foot,.proj-actions,.cta-panel,.lightbox{display:none !important}
  body{background:#fff;color:#000}
  body::before{display:none}
  .card,.side-block,.panel{border:1px solid #ccc;background:#fff}
  /* Clipped text prints as nothing. Give every gradient word its
     ink back before it reaches paper. */
  .grad,.grad-still,.stat dd,.foot-title,.card-go,.cat-count,
  .how-num,.four-oh-four{
    -webkit-text-fill-color:#000 !important;color:#000 !important;
    background:none !important;
  }

  /* The profile sections are the part of this site most likely to be
     printed — a career timeline and a publication list is a CV. They
     get flattened rather than dropped. */
  .tl-dot,.svc-icon,.pub-year{
    background:none !important;color:#000 !important;box-shadow:none !important;
    border:1px solid #999;
  }
  .tl::before{background:#ccc !important;opacity:1 !important}
  .tl-item,.svc,.skill-group,.pub{
    break-inside:avoid;page-break-inside:avoid;
    box-shadow:none !important;border:1px solid #ddd;
  }
  .svc::after{display:none}
  .skill-list li{background:none !important;border:1px solid #ccc}
}
