/*
  DESIGN
  One typeface, JetBrains Mono, carries the whole identity; hierarchy comes from size and
  weight only. Each discipline owns a colour field (same lightness, different hue; software is sky blue) that
  fills the intro and the hero and eases between pages; everything else is quiet ink on
  grey paper. Work is shown as aligned rows on the monospace grid, not as cards.

  Layout: left-aligned single column, max 1200px.
    [ hey, im xavier ...................... software  games  music  ui/ux ]
    [ hero colour field: page name / "i am a" / role (huge) / one-line note ]
    [ section title                                                         ]
    [ name          description                                       meta ]  ← striped rows
    [ about / contact / footer                                              ]
*/
:root {
  --paper: #EDEDEA;             /* page background */
  --stripe: #E4E4E0;            /* every other row */
  --rule: #D3D3CE;              /* nav edge */
  --ink: #17181C;               /* text, the dot, the highlight */
  --ink-2: #55585F;             /* secondary text (6:1 on paper) */
  --nav-bg: rgba(237, 237, 234, 0.88);

  /* the current page's colour; pages.js sets and animates it from the nav links' data-color */
  --field: #8CC8FF;

  --font: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* type scale (px): 13 · 16 · 21 · 28 · display */
  --t-small: 0.8125rem;
  --t-body: 1rem;
  --t-lede: 1.3125rem;
  --t-title: 1.75rem;
  --t-display: clamp(1.5rem, 8vw, 6rem);

  --hero-size: clamp(2rem, 7vw, 5.5rem);    /* the intro greeting */
  --nav-size: 1.125rem;
  --tagline-size: 1.25rem;
  --nav-h: 64px;
  --max-w: 1200px;

  --gutter-l: max(clamp(16px, 4vw, 40px), env(safe-area-inset-left, 0px));
  --gutter-r: max(clamp(16px, 4vw, 40px), env(safe-area-inset-right, 0px));

  color-scheme: light;
}

*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html { background: var(--paper); }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.6;
  font-variant-ligatures: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
html.js:not(.intro-done),
html.js:not(.intro-done) body { overflow: hidden; }

:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; border-radius: 2px; }
::selection { background: var(--field); color: var(--ink); }

/* ---------- Top bar ---------- */
.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 40;
  padding-top: env(safe-area-inset-top, 0px);
  background: var(--nav-bg);
  -webkit-backdrop-filter: saturate(1.2) blur(10px);
  backdrop-filter: saturate(1.2) blur(10px);
  border-bottom: 1px solid var(--rule);
}
.nav__inner {
  max-width: var(--max-w);
  height: var(--nav-h);
  margin: 0 auto;
  padding-left: var(--gutter-l);
  padding-right: var(--gutter-r);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.nav__brand { display: flex; align-items: center; min-width: 0; color: inherit; text-decoration: none; }
.nav__slot { display: flex; align-items: center; }

/* Page links. The active page is full ink; a dot sits under it and travels between pages. */
.nav__links { position: relative; display: flex; gap: clamp(16px, 2.6vw, 32px); }
.nav__links a {
  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 500;
  transition: color 150ms ease;
}
@media (hover: hover) {
  .nav__links a:hover { color: var(--ink); }
}
.nav__links a[aria-current="page"] { color: var(--ink); font-weight: 700; }
.nav__indicator {
  position: absolute;
  left: 0;
  top: calc(100% + 4px);
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--ink);
  pointer-events: none;
  will-change: transform, width;
}

/* ---------- Greeting (shared type for the h1 and its invisible nav twin) ---------- */
.greeting-type {
  margin: 0;
  font-family: var(--font);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
}
.greeting { position: relative; display: block; font-size: var(--hero-size); }
.greeting.is-docked { font-size: var(--nav-size); }
.nav__ghost { display: block; font-size: var(--nav-size); visibility: hidden; }

.greeting__word { display: inline-block; }
.greeting__char { display: inline-block; }
.greeting__name { position: relative; }
.greeting__probe { display: inline-block; width: 0; height: 0; vertical-align: baseline; }

/* Inverted highlight over "xavier": an ink box with the word in the background colour.
   script.js sizes this overlay (in em) and shows only part of it with clip-path, which is
   how the dot morphs into the box. In the top bar it's black and white. */
.greeting__hl {
  position: absolute;
  left: var(--ov-x, 0em);
  top: var(--ov-y, 0em);
  width: var(--ov-w, 100%);
  height: var(--ov-h, 1em);
  background: var(--ink);
  color: var(--field);
  pointer-events: none;
}
.greeting.is-docked .greeting__hl { color: #FFFFFF; }
.greeting__hl-text {
  position: absolute;
  left: calc(-1 * var(--ov-x, 0em));
  top: calc(-1 * var(--ov-y, 0em));
  line-height: 1;
  white-space: nowrap;
}
.greeting__hl-text::before { content: attr(data-text); }

.greeting__dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 0.22em;
  height: 0.22em;
  border-radius: 999px;
  background: var(--ink);
  opacity: 0;
  pointer-events: none;
  will-change: transform;
  transform-origin: 50% 50%;
}
html:not(.js) .greeting__dot,
.greeting.is-docked .greeting__dot { display: none; }

/* ---------- Intro stage ---------- */
/* Without JS this is a static hero; with JS it becomes a full-screen overlay on the page's colour. */
.intro {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 70vh;
  padding-top: calc(var(--nav-h) + env(safe-area-inset-top, 0px));
  padding-left: var(--gutter-l);
  padding-right: var(--gutter-r);
}
.js .intro {
  position: fixed;
  inset: 0;
  z-index: 50;
  min-height: 0;
  padding-top: 0;
  pointer-events: none;
}
.intro__bg { position: absolute; inset: 0; background: var(--field); }
.intro__stage {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(14px, 2.2vw, 26px);
  text-align: center;
  max-width: 100%;
}
.tagline {
  margin: 0;
  font-size: var(--tagline-size);
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink);
  white-space: nowrap;
}
/* Each word sits in its own mask and rises out of it. */
.tagline__word {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-block: 0.12em;
  margin-block: -0.12em;
}
.tagline__inner { display: inline-block; will-change: transform; }

/* The role that rolls. JS sizes it to the current word; letters roll through its mask. */
.tagline__slot { font-weight: 800; }
.tagline__slot.is-rolling {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow: hidden;
  height: calc(1.35em + 0.24em);
  padding-block: 0.12em;
  margin-block: -0.12em;
  text-align: left;
}
.roller.is-rolling.is-highlight::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: scaleX(var(--hl, 1));
  transform-origin: left center;
}
.tagline__role { position: absolute; left: 0; top: 0.12em; white-space: nowrap; }
.tagline__letter { display: inline-block; will-change: transform; }

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

/* ---------- States while the intro is running ---------- */
.js:not(.intro-done) #greeting .greeting__word,
.js:not(.intro-done) .tagline,
.js:not(.intro-done) .nav__links a,
.js:not(.intro-done) .nav__indicator,
.js:not(.intro-done) .block { opacity: 0; }
.js:not(.intro-done) .nav { transform: translateY(-100%); }

/* ---------- Page ---------- */
.page {
  max-width: var(--max-w);
  margin: 0 auto;
  padding-left: var(--gutter-l);
  padding-right: var(--gutter-r);
  padding-top: calc(var(--nav-h) + env(safe-area-inset-top, 0px) + clamp(16px, 3vw, 32px));
  padding-bottom: 32px;
  display: grid;
  gap: clamp(56px, 8vw, 96px);
}
html:not(.js) .page { padding-top: 24px; }

/* ---------- Hero: the page's colour field ---------- */
.hero {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 48px;
  min-height: clamp(340px, 44vw, 580px);
  padding: clamp(20px, 4vw, 48px);
  background: var(--field);
  border-radius: 6px;
}
.hero__label {
  --roller-lh: 1.6;
  margin: 0;
  font-size: var(--t-small);
  font-weight: 500;
}
.hero__title {
  --roller-lh: 1.12;
  /* the role sits in an inverted box with an even gap around its ink (see pages.js) */
  --hl-top: 0em;                 /* fallbacks; pages.js sets these per word */
  --hl-bot: 0.1em;
  --hl-gx: 0.2em;                /* gap left and right of the ink */
  --hl-gy: 0.12em;               /* gap above and below the ink */
  margin: 0;
  font-size: var(--t-display);
  font-weight: 800;
  line-height: 1.12;
  letter-spacing: -0.03em;
}
.hero__lead { display: block; }
/* the role always gets its own line, so it never jumps lines while it rolls */
.hero__title .roller.is-rolling { display: block; }
.hero__notes { display: grid; margin-top: 20px; }
.hero__notes p {
  margin: 0;
  max-width: 56ch;
  font-size: var(--t-body);
  font-weight: 500;
}

/* ---------- Rolling text (hero role + label) ----------
   JS sizes the slot to the current word; letters roll through its mask. */
.roller { white-space: nowrap; }
.roller.is-rolling {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow: hidden;
  height: calc(var(--roller-lh, 1.2) * 1em + 0.24em);
  padding-block: 0.12em;
  margin-block: -0.12em;
}
.roller__word { position: absolute; left: 0; top: 0.12em; white-space: nowrap; }
.roller__letter { display: inline-block; will-change: transform; }

/* Highlighted roller: same roll, inside an inverted box (ink box, letters in the page colour).
   pages.js sets the width and each word's left offset from the letters' actual ink, so the
   gap is even on both sides; --hl-top / --hl-bot do the same vertically. */
.roller.is-rolling.is-highlight {
  margin-top: var(--hl-top, 0em);
  margin-bottom: calc(-1 * var(--hl-bot, 0.1em));
  height: calc(var(--roller-lh, 1.2) * 1em - var(--hl-top, 0em) + var(--hl-bot, 0.1em));
  padding: 0;
  color: var(--field);
}
.roller.is-highlight .roller__word {
  z-index: 1;
  top: calc(-1 * var(--hl-top, 0em));
}

/* ---------- Pages and sections ---------- */
.views { display: grid; min-width: 0; }
.view { display: grid; gap: clamp(56px, 8vw, 96px); min-width: 0; }
.js .view:not(.is-active) { display: none; }

.section { display: grid; gap: 20px; min-width: 0; }
.section__title {
  margin: 0;
  font-size: var(--t-title);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
}
.section__text { margin: 0; max-width: 64ch; color: var(--ink-2); }
.section__lede {
  margin: 0;
  max-width: 52ch;
  font-size: var(--t-lede);
  line-height: 1.5;
}

/* Work as rows on the monospace grid: name, description, meta. */
.rows {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}
.row {
  display: grid;
  grid-template-columns: minmax(0, 20ch) minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 8px 32px;
  padding: 14px 16px;
  border-radius: 4px;
}
.row:nth-child(odd) { background: var(--stripe); }
.row__name { font-weight: 700; min-width: 0; }
.row__desc { color: var(--ink-2); min-width: 0; }
.row__meta { color: var(--ink-2); font-variant-numeric: tabular-nums; text-align: right; }

/* A tracklist is a real sequence, so it's numbered. */
.rows--numbered { counter-reset: row; }
.rows--numbered .row { grid-template-columns: 3ch minmax(0, 20ch) minmax(0, 1fr) auto; counter-increment: row; }
.rows--numbered .row::before { content: counter(row, decimal-leading-zero); color: var(--ink-2); }
.rows--contact .row { grid-template-columns: minmax(0, 20ch) minmax(0, 1fr); }

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 28px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--t-lede);
  font-weight: 500;
}

/* ---------- Live visits (games page) ----------
   The total sits in the same inverted box as the highlights; each digit is a reel that
   rolls like an odometer (visits.js). */
.visits { gap: 14px; }
.visits__number { margin: 0; }
.visits__count {
  display: inline-flex;
  align-items: flex-start;
  padding: 0.06em 0.14em;
  background: var(--ink);
  color: var(--field);
  font-size: clamp(2.5rem, 9vw, 7rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  transition: opacity 300ms ease;
}
.odo__digit { display: inline-block; height: 1em; overflow: hidden; }
.odo__reel { display: flex; flex-direction: column; will-change: transform; }
.odo__reel > span { display: block; height: 1em; }
.odo__sep { display: inline-block; }
.visits__meta {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0;
  color: var(--ink-2);
}
/* a small dot that pulses once each time fresh numbers arrive */
.visits__meta::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--ink);
  margin-top: calc((1.6em - 8px) / 2);   /* centred on the first line */
}
.visits.is-pulse .visits__meta::before { animation: visits-pulse 1.2s ease-out; }
@keyframes visits-pulse {
  from { box-shadow: 0 0 0 0 rgba(23, 24, 28, 0.4); }
  to   { box-shadow: 0 0 0 10px rgba(23, 24, 28, 0); }
}
.visits.is-loading .visits__count { opacity: 0.25; }
.visits.is-error .visits__number { display: none; }
@media (prefers-reduced-motion: reduce) {
  .visits.is-pulse .visits__meta::before { animation: none; }
}

/* A process is a sequence, so its steps are numbered. */
.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16ch), 1fr));
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}
.steps li {
  display: grid;
  gap: 4px;
  padding: 14px 16px;
  background: var(--stripe);
  border-radius: 4px;
  font-weight: 700;
  counter-increment: step;
}
.steps li::before {
  content: counter(step);
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-2);
}

.footer {
  max-width: var(--max-w);
  margin: 0 auto;
  padding-left: var(--gutter-l);
  padding-right: var(--gutter-r);
  padding-block: 40px 48px;
  font-size: var(--t-small);
  color: var(--ink-2);
}
.footer p { margin: 0; }

/* ---------- Small screens ---------- */
@media (max-width: 640px) {
  .row { grid-template-columns: minmax(0, 1fr) auto; padding: 12px 14px; }
  .row__desc { grid-column: 1 / -1; grid-row: 2; }
  .rows--numbered .row { grid-template-columns: 3ch minmax(0, 1fr) auto; }
  .rows--numbered .row__desc { grid-column: 2 / -1; }
  .rows--contact .row { grid-template-columns: minmax(0, 10ch) minmax(0, 1fr); }
  .rows--contact .row__desc { grid-column: auto; grid-row: auto; }
}

/* Phones: the page links move under the name so both fit. */
@media (max-width: 560px) {
  :root { --nav-size: 1rem; --nav-h: 92px; --tagline-size: 1rem; }
  .nav__inner {
    height: auto;
    min-height: var(--nav-h);
    flex-wrap: wrap;
    align-content: center;
    row-gap: 10px;
    padding-block: 14px 16px;
  }
  .nav__brand { width: 100%; }
  .nav__links { gap: 20px; }
  .hero { gap: 32px; min-height: 0; }
  .hero__notes { margin-top: 14px; }
}

/* ---------- Coming soon page (coming-soon.html) ----------
   Sits on the intro's stage; script.js rolls the greeting out and this in. */
.soon {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: clamp(14px, 2.2vw, 26px);
  margin: 0;
  text-align: center;
  pointer-events: none;
}
.js .soon { visibility: hidden; }
.is-soon-final .soon { visibility: visible; }
.is-soon-final .greeting,
.is-soon-final .tagline { visibility: hidden; }
html:not(.js) .soon-page .greeting,
html:not(.js) .soon-page .tagline { display: none; }
.soon__line {
  margin: 0;
  font-size: var(--hero-size);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
}
/* the "..." is three of the intro's dots, sitting on the baseline like full stops */
.soon__dots { display: inline-flex; gap: 0.1em; margin-left: 0.08em; }
.soon__dots i {
  display: inline-block;
  width: 0.22em;
  height: 0.22em;
  border-radius: 999px;
  background: var(--ink);
}
.soon__note {
  margin: 0;
  font-size: var(--tagline-size);
  font-weight: 500;
  pointer-events: auto;
}
.soon__note a { color: inherit; text-underline-offset: 0.2em; }
/* the copyright line, bottom of the screen; it rises out of its own mask */
.soon__footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  padding-bottom: max(24px, calc(env(safe-area-inset-bottom, 0px) + 16px));
  padding-left: var(--gutter-l);
  padding-right: var(--gutter-r);
  text-align: center;
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink);
}
.soon__footer-mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-block: 0.12em;
  margin-block: -0.12em;
}
.soon__footer-inner { display: inline-block; }
.js .soon__footer { visibility: hidden; }
.is-soon-final .soon__footer { visibility: visible; }

