/* ========== TOKENS ========== */
:root {
  --bg: #0b1221;              /* night navy */
  --surface: #16203a;         /* image placeholders while loading */
  /* Contrast on --bg / on the brightest glow rgb(43 52 96) / where the light's centre passes over the
     brightest glow. The light (.bg__light) is deliberately strong, so grey text loses contrast while it
     passes (owner's choice, 2026-10-04); white text stays well above 4.5:1. */
  --text: #eef1f6;            /* 16.5:1 / 10.5:1 / 5.3:1 */
  --text-mid: #a9b2c4;        /*  8.8:1 /  5.6:1 / 2.8:1 */
  --text-dim: #a3adc2;        /*  8.3:1 /  5.1:1 / 2.6:1 */
  --line: rgba(255, 255, 255, 0.15);
  --font-display: 'Outfit', sans-serif;
  --font-body: 'Outfit', sans-serif;
  --pad: 48px;
  --header-h: 80px;
  --ease-out: cubic-bezier(0.14, 1, 0.34, 1);
  /* The background's colours as bare RGB channels, for see-through tints: rgb(var(--glow-violet) / 0.16) */
  --bg-rgb: 11 18 33;             /* = --bg */
  --glow-indigo: 72 92 214;       /* .bg__glow--1 */
  --glow-violet: 128 96 230;      /* .bg__glow--2 */
  --glow-blue: 36 104 196;        /* .bg__glow--3 */
  --glow-lavender: 150 140 255;   /* .bg__light (the mouse light) */
}

@media (max-width: 767px) {
  :root { --pad: 24px; --header-h: 56px; }
}

/* ========== RESET ========== */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, svg, video { display: block; max-width: 100%; }
.defs { position: absolute; width: 0; height: 0; overflow: hidden; }   /* SVG filters only */

a { color: inherit; }

:focus-visible { outline: 2px solid var(--text); outline-offset: 4px; }

/* ========== BACKGROUND ==========
   Night navy with soft glows that drift slowly, a light that wanders and follows the mouse (bg.js) and a fine
   film grain (plain low-opacity noise) that breaks up banding in the dark gradients. Fixed behind
   everything. With reduced motion the glows stand still and there is no mouse light. */
.bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

.bg__glow {
  position: absolute;
  border-radius: 50%;
  will-change: transform;
}

.bg__glow--1 {   /* indigo, top left */
  width: 80vmax;
  height: 80vmax;
  left: -28vmax;
  top: -38vmax;
  background: radial-gradient(closest-side, rgb(var(--glow-indigo) / 0.34), rgb(var(--glow-indigo) / 0.12) 55%, rgb(var(--glow-indigo) / 0));
  animation: bg-drift-1 26s ease-in-out infinite alternate;
}

.bg__glow--2 {   /* violet, right */
  width: 70vmax;
  height: 70vmax;
  right: -30vmax;
  top: 5vh;
  background: radial-gradient(closest-side, rgb(var(--glow-violet) / 0.26), rgb(var(--glow-violet) / 0.08) 55%, rgb(var(--glow-violet) / 0));
  animation: bg-drift-2 32s ease-in-out infinite alternate;
}

.bg__glow--3 {   /* deep blue, bottom */
  width: 75vmax;
  height: 75vmax;
  left: 5vw;
  bottom: -52vmax;
  background: radial-gradient(closest-side, rgb(var(--glow-blue) / 0.24), rgb(var(--glow-blue) / 0.07) 55%, rgb(var(--glow-blue) / 0));
  animation: bg-drift-3 38s ease-in-out infinite alternate;
}

@keyframes bg-drift-1 { to { transform: translate(10vw, 8vh) scale(1.12); } }
@keyframes bg-drift-2 { to { transform: translate(-12vw, 10vh) scale(0.9); } }
@keyframes bg-drift-3 { to { transform: translate(14vw, -8vh) scale(1.15); } }

.bg__light {     /* lavender, wanders on its own and trails the mouse (moved by bg.js) */
  position: absolute;
  left: 0;
  top: 0;
  width: 46vmax;
  height: 46vmax;
  margin: -23vmax 0 0 -23vmax;
  border-radius: 50%;
  /* 0.45: chosen to be clearly visible; under its centre the grey text tokens drop below 4.5:1 (see the tokens) */
  background: radial-gradient(closest-side, rgb(var(--glow-lavender) / 0.45), rgb(var(--glow-lavender) / 0.25) 40%, rgb(var(--glow-lavender) / 0));
  opacity: 0;
  transition: opacity 0.6s;
  will-change: transform;
}
.bg__light.is-on { opacity: 1; }

.bg__grain {
  position: absolute;
  inset: 0;
  opacity: 0.09;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Star dust: shown instead of the light where there is something to read or look at closely (html.in-sky,
   stars.js: the home page's projects and About, a project page's story).
   Two depth layers (far: small and many, near: bigger and fewer) drift slower than the page as it
   scrolls; each star twinkles and floats a few px on its own. They only move while shown. */
.in-sky .bg__light.is-on { opacity: 0; }

.bg__stars {
  position: absolute;
  left: 0;
  right: 0;
  top: -20%;
  height: 140%;           /* room for the scroll drift */
  opacity: 0;
  transition: opacity 1.2s;
}
.in-sky .bg__stars { opacity: 1; }

.bg__stars i {
  position: absolute;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: #e8e4ff;
  box-shadow: 0 0 6px 1px rgb(var(--glow-lavender) / 0.8);
  /* --d, --dd, --delay, --dx, --dy: per star, set by home.js */
  animation: bg-twinkle var(--d) ease-in-out var(--delay) infinite alternate,
             bg-float var(--dd) ease-in-out var(--delay) infinite alternate;
  animation-play-state: paused;
}
.bg__stars--near i { width: 3px; height: 3px; }
.in-sky .bg__stars i { animation-play-state: running; }

@keyframes bg-twinkle { from { opacity: 0.15; scale: 0.6; } to { opacity: 1; scale: 1; } }
@keyframes bg-float { from { translate: 0 0; } to { translate: var(--dx) var(--dy); } }

@media (prefers-reduced-motion: reduce) {
  .bg__glow { animation: none; }
  .bg__light { display: none; }
  .bg__stars i { animation: none; opacity: 0.6; }
}

/* ========== LIGHT BUTTON ==========
   A pill whose edge a thin lavender light runs around on hover and keyboard focus — the site's
   'light along a line' motif (like the scroll cue). With reduced motion the edge just turns lavender. */
@property --run {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.btn-light {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 24px;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: inherit;
  font-size: 14px;
  text-decoration: none;
  transition: box-shadow 0.4s var(--ease-out);
}
.btn-light::before,
.btn-light::after {        /* the running light: a conic sweep, cut to a 1px ring by the mask */
  content: '';
  position: absolute;
  inset: -1px;
  padding: 1px;
  border-radius: inherit;
  pointer-events: none;
  background: conic-gradient(from var(--run), transparent 0deg, rgb(var(--glow-lavender) / 0) 30deg,
              rgb(var(--glow-lavender)) 70deg, #fff 80deg, rgb(var(--glow-lavender) / 0) 110deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.4s;
  animation: btn-run 2.6s linear infinite paused;
}
.btn-light::after { padding: 2px; filter: blur(6px); }   /* its glow */
.btn-light:hover::before, .btn-light:hover::after,
.btn-light:focus-visible::before, .btn-light:focus-visible::after { opacity: 1; animation-play-state: running; }
.btn-light:hover, .btn-light:focus-visible { box-shadow: 0 0 24px rgb(var(--glow-violet) / 0.25); }
.btn-light .arrow { display: inline-block; transition: transform 0.5s var(--ease-out); }
.btn-light:hover .arrow { transform: translateX(4px); }
@keyframes btn-run { to { --run: 360deg; } }

@media (prefers-reduced-motion: reduce) {
  .btn-light::before, .btn-light::after { animation: none; background: rgb(var(--glow-lavender) / 0.7); }
}

/* Round version for icon buttons (the hero's down arrow, back to top): the arrow nudges the way it points */
.btn-light--round {
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
}
.btn-light--round svg { width: 20px; height: 20px; transition: translate 0.5s var(--ease-out); }

/* ========== BACK TO TOP ==========
   A round light button, bottom right, in the same frosted night as the header's pill. Shown once the
   hero is passed. Its ring fills with lavender light as the page is read, a bright head at the front
   (--p, 0 to 1); near the end it rises so it never covers the footer (--lift). It glides back to the
   top, so on the home page the media shrinks back into the headline on the way. All in to-top.js. */
.to-top {
  position: fixed;
  right: var(--pad);
  bottom: 32px;
  z-index: 90;
  background: rgb(var(--bg-rgb) / 0.55);
  box-shadow: 0 8px 32px rgb(0 0 0 / 0.28);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  transform: translateY(calc(var(--lift, 0) * -1px));
  opacity: 0;
  visibility: hidden;
  translate: 0 16px;
  transition: opacity 0.5s var(--ease-out), translate 0.6s var(--ease-out), visibility 0.5s, box-shadow 0.4s var(--ease-out);
}
.to-top.is-shown { opacity: 1; visibility: visible; translate: none; }
.to-top:hover svg.to-top__arrow { translate: 0 -3px; }
.to-top:hover, .to-top:focus-visible { box-shadow: 0 8px 32px rgb(0 0 0 / 0.28), 0 0 24px rgb(var(--glow-violet) / 0.25); }

.to-top .to-top__ring {   /* over the 1px edge: the light that fills it (.to-top: beats .btn-light--round svg) */
  position: absolute;
  inset: -1px;
  width: 48px;
  max-width: none;  /* not the 46px inside the edge */
  height: 48px;
  rotate: -90deg;   /* starts at the top */
  fill: none;
  stroke: rgb(var(--glow-lavender));
  stroke-width: 1;
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p, 0));
  filter: drop-shadow(0 0 2px rgb(var(--glow-lavender) / 0.9));
}
.to-top__head {   /* the bright head at the front of the light */
  position: absolute;
  inset: -1px;
  rotate: calc(var(--p, 0) * 360deg);
  opacity: min(1, calc(var(--p, 0) * 20));
}
.to-top__head::after {
  content: '';
  position: absolute;
  left: 50%;
  top: -1px;
  width: 3px;
  height: 3px;
  margin-left: -1.5px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 4px 1px rgb(var(--glow-lavender) / 0.95), 0 0 10px 3px rgb(var(--glow-violet) / 0.55);
}
.to-top__arrow { position: relative; }

@media (max-width: 767px) {
  .to-top { bottom: max(24px, env(safe-area-inset-bottom)); }
}
@media (prefers-reduced-motion: reduce) {
  .to-top { translate: none; transition: opacity 0.3s, visibility 0.3s; }
  .btn-light--round svg { transition: none; }
}

/* ========== LIGHT LINE KEYFRAMES ==========
   The drop's fade and the landing twinkle, shared by the home page's scroll cue and the light under the
   contact email. */
@keyframes cue-fade {
  0% { opacity: 0; }
  10%, 58% { opacity: 1; }
  70%, 100% { opacity: 0; }
}
@keyframes cue-glint {
  0%, 54% { opacity: 0; transform: scale(0.3) rotate(-45deg); }
  62% { opacity: 1; transform: scale(1) rotate(0deg); }
  84%, 100% { opacity: 0; transform: scale(0.4) rotate(45deg); }
}

/* ========== CONTACT ==========
   A big closing headline like the opening one, the email with a light running along its line, the light buttons.
   The mouse light is back here (the star sky ends with About). The headline's two lines rise from
   below their mask as the section comes in (contact.js adds .is-armed, then .is-in). Shared by the home
   page and the project pages. */
.contact {
  padding: 160px var(--pad) 120px;
  text-align: center;
}

.contact__label {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.contact__title {
  margin: 22px 0 56px;
  font-family: var(--font-display);
  font-size: clamp(48px, 10.3vw, 190px);
  font-weight: 800;
  line-height: 0.92;
  letter-spacing: -0.01em;
  text-transform: uppercase;
}
.contact__line { display: block; overflow: hidden; }   /* the mask */
.contact__line > span { display: inline-block; }
.contact.is-armed .contact__line > span { transform: translateY(110%); transition: transform 1.1s var(--ease-out); }
.contact.is-armed.is-in .contact__line > span { transform: none; }
.contact.is-armed.is-in .contact__line + .contact__line > span { transition-delay: 0.08s; }
@media (prefers-reduced-motion: reduce) {
  .contact.is-armed .contact__line > span { transform: none; transition: none; }
}

.contact__mail { margin: 0; }
.contact__email {
  position: relative;
  display: inline-block;
  padding-bottom: 10px;
  color: inherit;
  font-size: clamp(20px, 2.8vw, 40px);
  font-weight: 500;
  text-decoration: none;
}

/* The underline is the scroll cue's wire laid flat: a faint line with a lavender light running along it
   from left to right, and a small twinkle where it ends. Same drop, timing curve and twinkle as the cue
   (cue-fade, cue-glint), only longer. Runs only while the email is on screen (.is-live, contact.js). */
.contact__wire {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: rgb(238 241 246 / 0.24);
  clip-path: inset(-12px 0);   /* the light's tail stays on the line, its glow may spill above and below */
  transition: background-color 0.4s var(--ease-out);
}
.contact__email:hover .contact__wire,
.contact__email:focus-visible .contact__wire { background-color: rgb(238 241 246 / 0.45); }

.contact__spark {   /* as wide as the line; the light is drawn at its right end, then the whole span slides */
  position: absolute;
  inset: 0;
  background: linear-gradient(to right, rgb(var(--glow-lavender) / 0) calc(100% - 72px), rgb(var(--glow-lavender) / 0.9) calc(100% - 20px), #fff);
  opacity: 0;
  transform: translateX(-100%);
}
.contact__spark::before {   /* bloom along the tail */
  content: '';
  position: absolute;
  right: -2px;
  top: -3px;
  bottom: -3px;
  width: 64px;
  border-radius: 4px;
  background: linear-gradient(to right, rgb(var(--glow-lavender) / 0), rgb(var(--glow-lavender) / 0.45));
  filter: blur(3px);
}
.contact__spark::after {    /* head */
  content: '';
  position: absolute;
  right: -1.5px;
  top: -1px;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 4px 1px rgb(var(--glow-lavender) / 0.95), 0 0 10px 3px rgb(var(--glow-violet) / 0.55);
}
.contact__glint {   /* the twinkle at the end of the line, like the cue's */
  position: absolute;
  right: -10px;
  bottom: -10px;
  width: 21px;
  height: 21px;
  opacity: 0;
  background:
    radial-gradient(circle, #fff 0 1.2px, rgb(var(--glow-lavender) / 0.85) 2px, rgb(var(--glow-lavender) / 0.25) 4.5px, rgb(var(--glow-lavender) / 0) 8px),
    linear-gradient(90deg, rgb(var(--glow-lavender) / 0), rgb(232 228 255 / 0.95) 50%, rgb(var(--glow-lavender) / 0)) 50% 50% / 100% 1px no-repeat,
    linear-gradient(rgb(var(--glow-lavender) / 0), rgb(232 228 255 / 0.95) 50%, rgb(var(--glow-lavender) / 0)) 50% 50% / 1px 100% no-repeat;
}
.contact__email.is-live .contact__spark {
  animation: mail-run 3.6s cubic-bezier(0.65, 0, 0.35, 1) 0.5s infinite, cue-fade 3.6s linear 0.5s infinite;
}
.contact__email.is-live .contact__glint { animation: cue-glint 3.6s linear 0.5s infinite; }
@keyframes mail-run {
  0% { transform: translateX(-100%); }
  60%, 100% { transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {   /* a still frame: the light resting at the end, a small twinkle */
  .contact__wire { transition: none; }
  .contact__spark, .contact__email.is-live .contact__spark { animation: none; opacity: 1; transform: none; }
  .contact__glint, .contact__email.is-live .contact__glint { animation: none; opacity: 0.9; transform: scale(0.7); }
}

.contact__links {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 14px;
  margin-top: 48px;
}

@media (max-width: 767px) {   /* the three buttons, a little narrower, share one row from about 360px wide */
  .contact { padding-top: 80px; }
  .contact__links { gap: 8px; }
  .contact__links .btn-light { gap: 8px; padding-left: 16px; padding-right: 16px; }
}

/* ========== INK (ink.js) ==========
   The drop that carries a project's colour from one page to the next. The page that opens starts under
   the ink's colour (html.ink-in, set by the inline script in its <head>), which then sinks away into the
   page's own night. */
.ink { position: fixed; inset: 0; z-index: 400; width: 100%; height: 100%; }
html.ink-in body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 400;
  background: rgb(var(--ink, var(--bg-rgb)));
  pointer-events: none;
  animation: ink-sink 1s 0.25s var(--ease-out) forwards;
}
@keyframes ink-sink { to { opacity: 0; visibility: hidden; } }

/* ========== SITE FOOTER ========== */
.site-footer {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin: 0 var(--pad);
  padding: 20px 0 32px;
  border-top: 1px solid var(--line);
  font-size: 13px;
  color: var(--text-mid);
}
@media (max-width: 600px) {
  .site-footer { flex-direction: column; align-items: center; gap: 6px; text-align: center; }
}

/* ========== SKIP LINK ========== */
.skip-link {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 200;
  padding: 10px 16px;
  background: var(--text);
  color: var(--bg);
  text-decoration: none;
  transform: translateY(-200%);
}
.skip-link:focus { transform: none; }

/* ========== SITE HEADER ==========
   At the top: the wide header (name, menu, year). Scrolling down slides it away; scrolling up brings
   back only the menu, as a pill of frosted night, with a small light (the scroll cue's drop head) under
   the item of the section on screen. Hidden over the home hero. Driven by header.js (.is-compact,
   .is-hidden, .has-spot); keyboard focus always shows it. With reduced motion it fades instead of sliding.
   The slide uses `translate`, so it never fights the load intro, which moves `transform` (home.js). */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: var(--header-h);
  padding: 0 var(--pad);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.02em;
  pointer-events: none;          /* the bar itself lets clicks through to the page; its contents don't */
  transition: translate 0.6s var(--ease-out), opacity 0.3s;
}
.site-header > * { pointer-events: auto; }
.site-header:focus { outline: none; }   /* focused as a whole by Back to top (to-top.js); its links keep their ring */
.site-header.is-hidden:not(:focus-within) { translate: 0 calc(-100% - 48px); }   /* 48px: the pill's shadow too */

.site-header a { text-decoration: none; }

.site-header__nav {
  position: relative;
  display: flex;
  gap: 32px;
  padding: 12px 24px;            /* the pill; the same at the top (transparent), so nothing shifts */
  border: 1px solid transparent;
  border-radius: 999px;
  text-transform: uppercase;
  font-size: 13px;
  letter-spacing: 0.08em;
  transition: background-color 0.4s, border-color 0.4s, box-shadow 0.4s, -webkit-backdrop-filter 0.4s, backdrop-filter 0.4s;
}
.site-header.is-compact .site-header__nav {
  background: rgb(var(--bg-rgb) / 0.55);
  border-color: var(--line);
  box-shadow: 0 8px 32px rgb(0 0 0 / 0.28);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
}

.site-header__nav a,
.site-header__name { transition: opacity 0.3s; }
.site-header__nav a:hover,
.site-header__name:hover { opacity: 0.6; }

.site-header__name,
.site-header__year { transition: opacity 0.3s, visibility 0.3s; }
.site-header.is-compact .site-header__name,
.site-header.is-compact .site-header__year { opacity: 0; visibility: hidden; }

.site-header__year { justify-self: end; font-weight: 400; }

.site-header__spot {   /* slides to the lit item (--spot-x, set by header.js) */
  position: absolute;
  left: 0;
  bottom: 5px;
  width: 3px;
  height: 3px;
  margin-left: -1.5px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 4px 1px rgb(var(--glow-lavender) / 0.95), 0 0 10px 3px rgb(var(--glow-violet) / 0.55);
  translate: var(--spot-x, 0) 0;
  opacity: 0;
  transition: opacity 0.4s, translate 0.6s var(--ease-out);
}
.site-header.has-spot .site-header__spot { opacity: 1; }

@media (max-width: 767px) {
  .site-header { grid-template-columns: 1fr; justify-items: center; }
  .site-header__name, .site-header__year { display: none; }
  .site-header__nav { gap: 24px; padding: 10px 20px; transition-property: background-color, border-color, box-shadow, -webkit-backdrop-filter, backdrop-filter, translate; }
  .site-header.is-compact .site-header__nav { translate: 0 6px; }   /* the pill a little off the screen's top edge */
}

@media (prefers-reduced-motion: reduce) {
  .site-header.is-hidden:not(:focus-within) { translate: none; opacity: 0; }
  .site-header.is-hidden:not(:focus-within) > * { pointer-events: none; }
  .site-header__spot { transition: opacity 0.4s; }
}
