/* ================================================================
   NJORD — Creative Digital Studio
   Design tokens, layout, and component styles.
   ================================================================ */

:root{
  /* --- color tokens (light / default) --- */
  --paper: #F4F2EA;
  --paper-raised: #FBFAF5;
  --ink: #15150F;
  --ink-soft: #57564C;
  --ink-faint: #6B6A5E;
  --line: #DBD7C9;
  --line-strong: #C3BFAE;
  --signal: #CBFF3D;
  --signal-text: #557116;
  --signal-ink: #14140A;
  --error: #B3261E;

  /* --- project accent tokens (used only inside modal/preview scopes) --- */
  /* each project page's background is the same color as its own ASCII
     visual. Aurelia's diamond-blue is light enough for dark text; Pulse's
     burgundy and Meridian's deep blue are dark enough to need light text
     instead — recomputed per background, not assumed. */
  --aurelia-bg: #3D7FF0; --aurelia-ink: #0B0F1A; --aurelia-accent: #0B0F1A;
  --pulse-bg: #7A1B33; --pulse-ink: #F6ECE8; --pulse-accent: #F6ECE8;
  --meridian-bg: #1F4C78; --meridian-ink: #E9F1F8; --meridian-accent: #E9F1F8;

  /* --- type --- */
  --f-display: 'Bricolage Grotesque', 'Helvetica Neue', Arial, sans-serif;
  --f-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;

  --fs-hero: clamp(3.4rem, 13vw, 10.5rem);
  --fs-display: clamp(2rem, 5.2vw, 4.2rem);
  --fs-h2: clamp(1.4rem, 3vw, 2rem);
  --fs-lead: clamp(1.05rem, 1.6vw, 1.35rem);
  --fs-body: 1rem;
  --fs-small: 0.8125rem;
  --fs-micro: 0.6875rem;

  --gutter: clamp(20px, 4vw, 64px);
  --maxw: 1440px;

  --ease-out: cubic-bezier(.16,.84,.32,1);
  --ease-in-out: cubic-bezier(.65,0,.35,1);
  /* declares BOTH schemes as genuinely supported (not just "light"), the
     strongest signal to Chromium-based browsers (Edge, Opera, Brave) that
     this page already handles dark mode itself — the specific thing that
     disables their own separate "force dark" / "force darken web pages"
     feature. Without it, that feature can still kick in and heuristically
     recolor the page's own light-mode CSS colors on top of what's
     rendered, which the ascii-organism canvas can't follow (it draws in
     colors read from these variables in JS, not something the browser's
     forced-dark pass can see or repaint) — producing exactly the
     wrong-contrast/garbled look reported on Edge/Opera. See also the
     matching <meta name="color-scheme"> in the dev-server wrapper
     (_dev_server.py) and index.html's own <head>, needed because this
     external stylesheet loads asynchronously and the meta tag is the one
     signal available synchronously at initial parse time. */
  color-scheme: light dark;
}

@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --paper: #100F0A;
    --paper-raised: #17160F;
    --ink: #F3F1E6;
    --ink-soft: #ACA99B;
    --ink-faint: #93917F;
    --line: #2A281E;
    --line-strong: #3C3A2C;
    --signal: #CBFF3D;
    --signal-text: #CBFF3D;
    --signal-ink: #0B0B06;
    --error: #FF6B57;
    color-scheme: dark;
  }
}

:root[data-theme="dark"]{
  --paper: #100F0A;
  --paper-raised: #17160F;
  --ink: #F3F1E6;
  --ink-soft: #ACA99B;
  --ink-faint: #6E6C60;
  --line: #2A281E;
  --line-strong: #3C3A2C;
  --signal: #CBFF3D;
  --signal-text: #CBFF3D;
  --signal-ink: #0B0B06;
  --error: #FF6B57;
  color-scheme: dark;
}

/* Every clamp() above uses a vw-based preferred value that's smaller than
   its own minimum at phone widths (e.g. --fs-micro's 11px minimum with no
   vw term at all) — meaning on a ~390px screen every one of them bottoms
   out at the same minimum tuned for "narrow desktop," not arm's-length
   phone reading. Nothing actually scales up for mobile without this.
   These aren't just bigger — most also get a real vw term so they keep
   responding to width instead of going flat again. */
@media (max-width: 640px){
  :root{
    --fs-hero: clamp(4.4rem, 18vw, 10.5rem);
    --fs-display: clamp(3rem, 10.5vw, 4.2rem);
    --fs-h2: clamp(2rem, 7vw, 2.2rem);
    --fs-lead: clamp(1.35rem, 5vw, 1.5rem);
    --fs-body: 1.25rem;
    /* another size pass up from the previous mobile minimums: bigger
       still reads as airy as long as the section padding/gaps around it
       stay generous (unchanged here) — "roomy" comes from whitespace,
       not small type. */
    --fs-small: 1.1875rem;
    --fs-micro: 1.0625rem;
  }
  /* The root tokens above only reach anything wired to var(--fs-*) — these
     seven headings/labels each hardcode their own clamp() instead and have
     the identical bottoms-out-at-minimum problem, so they need their own
     mobile values too, not just a bigger minimum inherited from above. */
  .hero-descriptor{ font-size: clamp(1.25rem, 5vw, 1.45rem); }
  .work-row__title{ font-size: clamp(2.3rem, 10.5vw, 3.4rem); }
  .contact-head h2{ font-size: clamp(2.9rem, 13.5vw, 5rem); }
  .contact-email{ font-size: clamp(1.9rem, 9vw, 2.6rem); }
  .project-modal__title{ font-size: clamp(3.8rem, 19vw, 7rem); }
  .project-modal__tag{ font-size: clamp(1.4rem, 5.6vw, 1.6rem); }
}

/* ---------------------------------------------------------------
   Reset
   --------------------------------------------------------------- */
*, *::before, *::after{ box-sizing: border-box; }
html{ -webkit-text-size-adjust: 100%; scroll-behavior: auto; }
body{
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--f-display);
  font-size: var(--fs-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
ul{ list-style: none; margin: 0; padding: 0; }
a{ color: inherit; text-decoration: none; }
button{ font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
h1,h2,h3,p{ margin: 0; }
h1,h2,h3{ text-wrap: balance; font-weight: 500; }
img,canvas{ display: block; max-width: 100%; }

::selection{ background: var(--signal); color: var(--signal-ink); }

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

.skip-link{
  position: fixed; top: -100%; left: var(--gutter); z-index: 1000;
  background: var(--ink); color: var(--paper); padding: .75em 1.25em;
  font-family: var(--f-mono); font-size: var(--fs-small); transition: top .2s var(--ease-out);
}
.skip-link:focus{ top: var(--gutter); }

@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------
   The ASCII organism — one persistent, page-fixed particle canvas.
   Sits behind everything; `main` is lifted into its own positioned
   stacking context (below) so ordinary in-flow content still paints
   on top of it despite the canvas's explicit z-index.
   --------------------------------------------------------------- */
#ascii-organism{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
}
/* Low-power fallback (main.js's enableOrganismLowPowerFallback) — a
   static pre-rendered frame swapped in behind the canvas's own draw
   loop when a device benchmarks too slow to keep it live. Same box as
   #ascii-organism so it sits in exactly the same place. */
#organism-fallback{
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
  object-fit: cover;
}
main{
  position: relative;
  z-index: 2;
}

/* ---------------------------------------------------------------
   Layout helpers
   --------------------------------------------------------------- */
.wrap{
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.section{
  position: relative;
  padding-block: clamp(64px, 10vw, 140px);
  border-top: 1px solid var(--line);
}
.eyebrow{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  display: flex;
  align-items: center;
  gap: .6em;
  /* the eyebrow's own text is a bare text node, which inside a flex
     container becomes an anonymous flex item — those default to an
     automatic minimum width equal to their full UNWRAPPED content width
     (flexbox's min-width:auto), so a long enough label (or Albanian
     translation) can overflow the viewport at mobile widths instead of
     wrapping like ordinary block text would. overflow-wrap is inherited,
     so this lets that anonymous box actually shrink/wrap. */
  overflow-wrap: anywhere;
}
.eyebrow::before{
  content: "";
  width: 6px; height: 6px;
  background: var(--signal);
  flex: none;
}
.section-head{
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: 1.5rem 3rem;
  margin-bottom: clamp(2.5rem, 6vw, 5rem);
}
.section-head h2{
  font-size: var(--fs-display);
  font-weight: 340;
  letter-spacing: -0.01em;
  max-width: 18ch;
}
.section-head .section-note{
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  color: var(--ink-soft);
  max-width: 30ch;
  padding-bottom: .3em;
}

/* registration marks — quiet print/technical-drawing motif */
.reg-marks{
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 5;
}
.reg-marks span{
  position: absolute;
  width: 13px; height: 13px;
  opacity: .45;
}
.reg-marks span::before,
.reg-marks span::after{
  content: ""; position: absolute; background: var(--ink-faint);
}
.reg-marks span::before{ width: 100%; height: 1px; top: 50%; }
.reg-marks span::after{ height: 100%; width: 1px; left: 50%; }
.reg-marks .tl{ top: 14px; left: 14px; }
.reg-marks .tr{ top: 14px; right: 14px; }
.reg-marks .bl{ bottom: 14px; left: 14px; }
.reg-marks .br{ bottom: 14px; right: 14px; }
@media (max-width: 640px){ .reg-marks{ display:none; } }

/* ---------------------------------------------------------------
   Sound toggle — the only control for NJORD's generative audio.
   Stays above the project modal (z-index 600) so sound can always
   be switched off, on any page.
   --------------------------------------------------------------- */
.sound-toggle{
  position: fixed;
  left: var(--gutter);
  bottom: calc(clamp(18px, 3vw, 32px) + env(safe-area-inset-bottom, 0px));
  z-index: 650;
  display: inline-flex;
  align-items: center;
  gap: .65em;
  border: 1px solid var(--line-strong);
  padding: .55em .95em;
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  background: color-mix(in srgb, var(--paper) 72%, transparent);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  transition: color .25s, border-color .25s;
}
.sound-toggle:hover{ color: var(--ink); border-color: var(--ink-faint); }
.sound-toggle__dot{
  width: 6px; height: 6px;
  background: var(--line-strong);
  flex: none;
  transition: background .25s, box-shadow .25s;
}
.sound-toggle.is-on{ color: var(--ink); }
.sound-toggle.is-on .sound-toggle__dot{
  background: var(--signal);
  box-shadow: 0 0 6px var(--signal);
}
/* mobile: text label + dot both give way to a single monospace glyph —
   the .is-on color change above still carries the on/off state, so the
   icon needs no state of its own beyond inheriting `color`. Moved to
   the right — sound is the only control meant to sit on that side (see
   .lang-toggle below, which moves to the left to make room for it). */
.sound-toggle__icon{ display: none; font-size: 1rem; line-height: 1; }
@media (max-width: 640px){
  .sound-toggle{
    left: auto;
    right: var(--gutter);
    padding: .65em;
  }
  .sound-toggle__label{ display: none; }
  .sound-toggle__dot{ display: none; }
  .sound-toggle__icon{ display: block; }
}

/* ---------------------------------------------------------------
   Language toggle — EN / SHQP (Albanian). Shqip is the default;
   mirrors the sound toggle's placement/treatment on the opposite
   corner so the two persistent controls read as one family.
   --------------------------------------------------------------- */
.lang-toggle{
  position: fixed;
  right: var(--gutter);
  bottom: calc(clamp(18px, 3vw, 32px) + env(safe-area-inset-bottom, 0px));
  z-index: 650;
  display: inline-flex;
  align-items: center;
  gap: .4em;
  border: 1px solid var(--line-strong);
  padding: .55em .95em;
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  background: color-mix(in srgb, var(--paper) 72%, transparent);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  transition: color .25s, border-color .25s;
}
.lang-toggle:hover{ color: var(--ink); border-color: var(--ink-faint); }
.lang-toggle__sep{ opacity: .5; }
.lang-toggle__opt{ opacity: .45; transition: opacity .25s, color .25s; }
.lang-toggle__opt.is-active{ opacity: 1; color: var(--ink); }

@media (max-width: 640px){
  /* sound is the only control meant to occupy the right corner at
     mobile widths (see .sound-toggle above) — language moves back to
     the left, in its own corner rather than stacked with anything. */
  .lang-toggle{ left: var(--gutter); right: auto; }
}

/* ---------------------------------------------------------------
   Custom cursor
   --------------------------------------------------------------- */
.cursor-dot{
  position: fixed;
  top: 0; left: 0;
  width: 10px; height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: 50%;
  background: var(--ink);
  mix-blend-mode: difference;
  pointer-events: none;
  z-index: 999;
  transform: translate3d(-100px,-100px,0) scale(1);
  transition: width .25s var(--ease-out), height .25s var(--ease-out), margin .25s var(--ease-out), background-color .25s;
  will-change: transform;
}
.cursor-dot.is-hover{
  width: 46px; height: 46px; margin: -23px 0 0 -23px;
  background: var(--signal);
}
.cursor-dot.is-down{ background: var(--signal); }
@media (pointer: coarse){ .cursor-dot{ display: none; } }

/* ---------------------------------------------------------------
   Header / nav
   --------------------------------------------------------------- */
.site-header{
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 200;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  /* the hamburger (and logo) sit in this top strip — on a real phone
     that strip can be under a notch/Dynamic Island or a host app's own
     chrome, which visually looks fine (this bar's own background still
     paints) but silently eats taps there. env(safe-area-inset-top)
     pushes the actual interactive content down clear of that band; it's
     0 on anything without a safe area, so this is a no-op elsewhere. */
  padding: calc(clamp(18px,3vw,32px) + env(safe-area-inset-top, 0px)) var(--gutter) clamp(18px,3vw,32px);
  pointer-events: none;
}
.site-header::before{
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, color-mix(in srgb, var(--paper) 78%, transparent), transparent);
  z-index: -1;
}
.site-header > *{ pointer-events: auto; }

.logo-mark{
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  display: flex;
  align-items: center;
  gap: .45em;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}
.logo-mark::after{ content: ""; width: 6px; height: 6px; background: var(--signal); }
body.is-past-hero .logo-mark{ opacity: 1; transform: none; }

.main-nav ul{ display: flex; flex-direction: column; align-items: flex-end; gap: .5rem; }
.main-nav a{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .12em;
  text-transform: uppercase;
  position: relative;
  padding: .15em 0;
  display: inline-block;
}
.main-nav a .fill{
  position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
  background: var(--line-strong);
}
.main-nav a .fill::after{
  content: ""; position: absolute; inset: 0; background: var(--signal-text);
  transform: scaleX(0); transform-origin: left; transition: transform .35s var(--ease-out);
}
.main-nav a:hover .fill::after, .main-nav a:focus-visible .fill::after{ transform: scaleX(1); }

/* Hamburger — hidden on desktop where the nav links are always visible
   inline. Three thin bars matching the line-driven visual language used
   elsewhere (.fill underlines, .reg-marks); animates into an X when open
   rather than swapping icons. */
.nav-toggle{
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 34px; height: 34px;
  background: none; border: none;
}
.nav-toggle__bar{
  display: block;
  width: 100%; height: 1px;
  background: var(--ink);
  transition: transform .3s var(--ease-out), opacity .3s var(--ease-out);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1){ transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2){ opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3){ transform: translateY(-6px) rotate(-45deg); }

.nav-scrim{
  position: fixed; inset: 0;
  z-index: 620;
  background: rgba(0,0,0,.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity .35s var(--ease-out);
}
.nav-scrim.is-visible{ opacity: 1; pointer-events: auto; }

@media (max-width: 640px){
  .nav-toggle{ display: flex; }
  /* off-canvas drawer: the nav used to always sit inline in the header
     at this width (flex-direction:row on its own <ul>) — replaced with
     a fixed panel sliding in from the right, since always-on top nav
     was the exact clutter this change was meant to remove. */
  .main-nav{
    position: fixed;
    inset: 0 0 0 auto;
    z-index: 630;
    width: min(78vw, 320px);
    background: var(--paper);
    border-left: 1px solid var(--line-strong);
    padding: clamp(90px, 24vw, 120px) var(--gutter) 40px;
    transform: translateX(100%);
    transition: transform .4s var(--ease-out);
    overflow-y: auto;
  }
  .main-nav.is-open{ transform: translateX(0); }
  .main-nav ul{ flex-direction: column; align-items: flex-start; gap: 1.5rem; }
  .main-nav a{ font-size: var(--fs-body); }
  body.nav-open{ overflow: hidden; }
}

/* ---------------------------------------------------------------
   Hero
   --------------------------------------------------------------- */
.hero{
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  border-top: none;
}
.hero-inner{
  position: relative;
  z-index: 2;
  padding-inline: var(--gutter);
  width: 100%;
}
.hero-eyebrow{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: .6em;
}
.hero-word{
  font-family: var(--f-display);
  font-weight: 240;
  font-size: var(--fs-hero);
  line-height: .86;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--ink);
  transition: opacity .6s var(--ease-out), transform .6s var(--ease-out);
}
/* same mark as the small header wordmark (.logo-mark::after), at the
   exact same size-to-font-size and gap-to-font-size ratio (6px / 1.1rem
   ≈ 0.34em, gap .45em there) — sized in em so it scales with --fs-hero's
   own responsive clamp() automatically, desktop and mobile alike,
   rather than needing its own breakpoint */
.hero-word::after{
  content: "";
  display: inline-block;
  width: 0.34em;
  height: 0.34em;
  margin-left: 0.45em;
  background: var(--signal);
  vertical-align: middle;
}
.hero-foot{
  position: absolute;
  left: var(--gutter); right: var(--gutter); bottom: clamp(24px,4vw,48px);
  z-index: 2;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 2rem;
}
.hero-descriptor{
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(.95rem, 1.6vw, 1.2rem);
  max-width: 34ch;
  color: var(--ink-soft);
}
.hero-descriptor strong{ color: var(--ink); font-weight: 500; }
.scroll-cue{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  display: flex;
  align-items: center;
  gap: .75em;
  flex: none;
}
.scroll-cue .track{
  width: 1px; height: 34px;
  background: var(--line-strong);
  position: relative;
  overflow: hidden;
}
.scroll-cue .track::after{
  content: ""; position: absolute; left: 0; top: -100%; width: 100%; height: 100%;
  background: var(--signal-text);
  animation: cue-fall 2.2s var(--ease-in-out) infinite;
}
@keyframes cue-fall{
  0%{ top: -100%; } 60%{ top: 100%; } 100%{ top: 100%; }
}
@media (prefers-reduced-motion: reduce){ .scroll-cue .track::after{ animation: none; top: 0; } }

@media (max-width: 720px){
  .hero-foot{ flex-direction: column; align-items: flex-start; gap: 1.5rem; }
}

/* ---------------------------------------------------------------
   Work — index rows
   --------------------------------------------------------------- */
.work-index{ padding-top: 0; }
.work-row{
  position: relative;
  display: grid;
  grid-template-columns: 90px 1fr auto;
  align-items: center;
  gap: clamp(1rem,3vw,3rem);
  padding-block: clamp(1.6rem, 4vw, 3rem);
  border-top: 1px solid var(--line);
  cursor: pointer;
  overflow: hidden;
}
.work-row:last-child{ border-bottom: 1px solid var(--line); }
.work-row + .work-row{ margin-top: clamp(6rem, 16vw, 14rem); }
/* Aurelia and Pulse each command a generous gap before the NEXT row via
   the rule above — Meridian, being last, has no following .work-row to
   trigger that margin, so without this it only got the section's own
   (much smaller) bottom padding. That's what made its "section" read as
   tiny next to the other two: not its own box, but the empty space
   around it not matching theirs. Give it the same total trailing space —
   subtracting .work-index's own bottom padding (which stacks on top of
   this margin, they don't collapse, since non-zero padding sits between
   them) so the total comes out equal to the others' gap, not bigger. */
.work-row:last-child{
  /* started at 180px (measured from a screenshot against Pulse's row),
     increased further per follow-up feedback that it still needed more.
     Deliberately padding-bottom, not margin-bottom: this needs to land
     BETWEEN the text and this row's own border-bottom line (inside the
     box), not after the line. Adjust this single number up/down based
     on how it actually looks live. */
  padding-bottom: 230px;
}

.work-row__index canvas{ width: 100%; height: 100%; }
.work-row__index{ position: relative; z-index: 1; width: 72px; height: 72px; display: flex; align-items: center; }
.work-row__index.is-pulsing{ animation: glyph-pulse .7s var(--ease-out); }
/* the plain numeral fallback (mobile only, see max-width:720px below) —
   hidden on desktop where the animated ASCII glyph canvas is the real
   index mark */
.work-row__index-num{
  display: none;
  font-family: var(--f-mono);
  font-size: 1.4rem;
  font-weight: 500;
  color: var(--ink-faint);
  letter-spacing: .02em;
}
@keyframes glyph-pulse{
  0%{ transform: scale(1); opacity: 1; }
  40%{ transform: scale(1.18); opacity: .7; }
  100%{ transform: scale(1); opacity: 1; }
}
/* a shared floor, not a per-row guess — description lengths differ (and
   differ again once translated), so wrap-to-2-vs-3-lines can vary per
   row/language in ways that are impractical to hand-tune text for; this
   keeps every row the same height regardless, rather than chasing exact
   pixel matches */
.work-row__body{ position: relative; z-index: 1; min-width: 0; }
.work-row__title{
  font-size: clamp(1.8rem, 4.4vw, 3.4rem);
  font-weight: 350;
  letter-spacing: -0.01em;
  transition: transform .4s var(--ease-out);
}
.work-row:hover .work-row__title{ transform: translateX(.2em); }
.work-row__desc{
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  color: var(--ink-soft);
  margin-top: .5em;
  max-width: 46ch;
}
.work-row__tags{
  display: flex; flex-wrap: wrap; gap: .5em 1em; margin-top: .9em;
}
.work-row__tags span{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  border: 1px solid var(--line-strong);
  padding: .3em .6em;
}
.work-row__meta{
  position: relative; z-index: 1;
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  color: var(--ink-faint);
  text-align: right;
  display: flex; flex-direction: column; align-items: flex-end; gap: .6em;
}
.work-row__enter{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink);
  display: inline-flex; align-items: center; gap: .5em;
}
.work-row__enter .arrow{ transition: transform .3s var(--ease-out); }
.work-row:hover .work-row__enter .arrow{ transform: translateX(4px); }

@media (max-width: 720px){
  .work-row{ grid-template-columns: 56px 1fr; }
  .work-row__meta{ grid-column: 1 / -1; flex-direction: row; justify-content: space-between; align-items: center; }
  .work-row__index{ width: 48px; height: 48px; }
  /* the animated ASCII glyph reads as noisy clutter at this size — swap
     it for a plain static numeral instead of just trying to calm the
     animation down further */
  .work-row__index canvas{ display: none; }
  .work-row__index-num{ display: block; }
  /* gap between Meridian's meta row (year + Enter) and its own
     border-bottom line, mobile only — tuned live by feel, not a fixed
     measurement; adjust this one number directly if it's ever off */
  .work-row:last-child{ padding-bottom: 161px; }
}

/* ---------------------------------------------------------------
   Services — procedural ASCII roadmap (V2). The connecting path is
   drawn by the organism itself (ascii-engine.js, 'roadmap' formation)
   on the full-bleed canvas already behind this section — everything
   here only lays out and types the real DOM text for each node.
   --------------------------------------------------------------- */
.roadmap-nodes{
  position: relative;
  /* tall enough that scrolling through 5 nodes' worth of path-growth +
     typing reads as deliberate rather than instant — retune this
     multiplier directly if the pacing feels rushed or too slow */
  min-height: 420vh;
  margin-top: clamp(2rem, 6vw, 4rem);
}
.roadmap-node{
  position: absolute;
  transform: translate(-50%, -50%);
  max-width: 38ch;
  opacity: 0;
  transition: opacity .3s var(--ease-out);
  pointer-events: none;
}
.roadmap-node.is-visible{ opacity: 1; }
.roadmap-node__num{
  display: block;
  font-family: var(--f-mono);
  font-size: clamp(1.4rem, 3vw, 2rem);
  font-weight: 700;
  letter-spacing: .1em;
  color: var(--signal);
  margin-bottom: .3em;
}
.roadmap-node__heading{
  font-family: var(--f-mono);
  font-size: clamp(1.6rem, 3.6vw, 2.4rem);
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 .45em;
}
.roadmap-node__support{
  font-family: var(--f-mono);
  font-size: clamp(1.05rem, 1.8vw, 1.25rem);
  color: var(--ink-soft);
  line-height: 1.55;
  margin: 0;
}
.roadmap-node__cursor{
  display: inline-block;
  color: var(--signal);
  animation: roadmap-cursor-blink 1s step-end infinite;
}
@keyframes roadmap-cursor-blink{
  0%, 100%{ opacity: 1; }
  50%{ opacity: 0; }
}
/* visually hidden but announced — the always-complete text for screen
   readers, independent of the sighted character-by-character reveal */
.roadmap-node__sr{
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
@media (max-width: 720px){
  /* shortened along with the node copy itself (i18n.js) — the old
     length needed 340vh of scroll to let long paragraphs finish typing
     before the next node; much shorter one-liners don't need nearly
     as much runway */
  /* trimmed again — even after the first cut (340vh->260vh) to match
     the shortened node copy, the section still scrolled well past where
     the last node finished typing, leaving a stretch of dead empty space
     before Studio began */
  /* raised back up (200vh->300vh): node y-centers are evenly spaced
     across this height (5 nodes), so shrinking it also shrinks the
     fixed gap between adjacent nodes' centers. At 200vh that gap
     (~325px on a 812px-tall phone) was smaller than a fully-typed
     node's own rendered height (~430px, num+heading+multi-line
     support at mobile font sizes), so consecutive nodes visually
     overlapped once both were on screen. 300vh gives each node
     comfortable room without reopening the earlier dead-space problem. */
  .roadmap-nodes{ min-height: 300vh; }
  /* .roadmap-node is absolutely positioned with no left+right pair, so it
     shrink-to-fits its own content up to max-width regardless of its
     containing block's actual width — a plain ch value doesn't know how
     wide the viewport actually is. At mobile font sizes, 28ch worked out
     to ~430px, comfortably WIDER than a 390px phone screen, so node text
     ran off both edges instead of wrapping. Capping with vw as well
     guarantees it never exceeds the viewport no matter how the ch/font-size
     math works out. */
  .roadmap-node{ max-width: min(28ch, 84vw); }
  .roadmap-node__num{ font-size: clamp(1.6rem, 5.5vw, 2rem); }
  .roadmap-node__heading{ font-size: clamp(1.85rem, 7vw, 2.4rem); }
  .roadmap-node__support{ font-size: clamp(1.2rem, 4vw, 1.25rem); }
  /* the intro copy right under "What we do." was reading as long and
     heavy on phones even after trimming its actual word count (i18n.js)
     — dropping it back down (rather than letting it inherit the bumped-up
     --fs-small like every other section-note) keeps it a quiet aside
     under the big heading instead of competing with it */
  #services .section-note{ font-size: .9375rem; max-width: 26ch; }
}
@media (prefers-reduced-motion: reduce){
  .roadmap-node{ opacity: 1; transition: none; }
  .roadmap-node__cursor{ animation: none; opacity: 0; }
}

/* ---------------------------------------------------------------
   Studio
   --------------------------------------------------------------- */
.studio-copy p{
  font-size: var(--fs-lead);
  font-weight: 380;
  color: var(--ink-soft);
  max-width: 58ch;
}
.studio-copy p + p{ margin-top: 1.1em; }
.studio-copy strong{ color: var(--ink); font-weight: 550; }
.studio-principles{
  margin-top: 2.2rem;
  display: grid;
  gap: .75rem;
}
.studio-principles li{
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  letter-spacing: .04em;
  color: var(--ink);
  display: flex; gap: .8em; align-items: baseline;
  /* same anonymous-flex-item overflow as .eyebrow above — this is the
     one actually causing the Studio section's text to run off the
     screen on mobile: longer lines (esp. the Albanian translations)
     have no wrap points respected inside a flex row by default. */
  overflow-wrap: anywhere;
}
.studio-principles li::before{
  content: "";
  width: 4px; height: 4px;
  background: var(--signal);
  flex: none;
  transform: translateY(-2px);
}
/* ---------------------------------------------------------------
   Contact
   --------------------------------------------------------------- */
.contact{ overflow: hidden; }
.contact-inner{ position: relative; z-index: 1; }
/* fades out while the mascot conversation is open (mascot-convo.js
   toggles this class) so the heading doesn't compete with him for
   attention — room for the conversation to breathe */
.contact-head{ transition: opacity .4s var(--ease-out); }
.contact-head.is-convo-open{ opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce){
  .contact-head{ transition: none; }
}
.contact-head h2{
  font-size: clamp(2.2rem, 6vw, 5rem);
  font-weight: 320;
  max-width: 18ch;
  letter-spacing: -0.01em;
}
@media (max-width: 640px){
  /* explicit request: give the section real room below the title before
     the row/mascot begins, rather than relying only on his own resting
     position (mascotFloor in ascii-engine.js) to create that space */
  .contact-head{ padding-bottom: 200px; }
}
.contact-row{
  position: relative;
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end;
  gap: 2rem;
  margin-top: clamp(2rem, 6vw, 4rem);
  padding-top: 2rem;
  border-top: 1px solid var(--line);
}
.contact-email{
  font-family: var(--f-display);
  font-weight: 420;
  font-size: clamp(1.4rem, 4vw, 2.6rem);
  letter-spacing: -0.01em;
  display: inline-flex;
  align-items: center;
  gap: .5em;
  /* same anonymous-flex-item overflow as .eyebrow/.studio-principles li
     — the address itself has no spaces to wrap at, so at the bigger
     mobile font size it could run past the viewport edge instead of
     breaking; overflow-wrap:anywhere allows a mid-word break as a
     last resort rather than overflowing. */
  overflow-wrap: anywhere;
}
.contact-email .status{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--signal-text);
  opacity: 0;
  transition: opacity .3s;
}
.contact-email.is-copied .status{ opacity: 1; }
.contact-meta{
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  color: var(--ink-soft);
  text-align: right;
}
@media (max-width: 640px){
  /* .contact-row's own flex-wrap left these two stacking in an odd
     halfway state at phone widths: the email alone was wide enough to
     fill the row (forcing a wrap) but the meta paragraph then landed as
     a lone item on its own flex line, which sizes to its own content —
     making its `text-align:right` a no-op (nothing to align against)
     and leaving it stuck flush-left, out of step with the address
     above it. Forcing a real column stack with matching left alignment
     reads as one deliberate block instead of a layout accident. */
  .contact-row{ flex-direction: column; align-items: flex-start; gap: 1.25rem; }
  .contact-meta{ text-align: left; }
}
.site-footer{
  position: relative;
  display: flex; justify-content: space-between; align-items: center;
  gap: 1rem; flex-wrap: wrap;
  padding-top: clamp(2rem, 5vw, 3.5rem);
  margin-top: clamp(2rem, 5vw, 3.5rem);
  border-top: 1px solid var(--line);
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
@media (max-width: 640px){
  /* the sound/lang toggles are fixed to the viewport's bottom corners
     (see .sound-toggle/.lang-toggle) — once the page is scrolled all the
     way down, that's exactly where the footer's own last line of text
     otherwise sits too. Reserving real room below the footer (its own
     height, plus the safe-area inset those toggles already account for)
     stops them ever landing on top of it. */
  .site-footer{ padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
}
/* MASCOT — a real point marker sitting exactly on .contact-row's own
   top border line: its rect (near-zero size) is what AsciiOrganism's
   anchor formula uses as the mascot formation's pivot, so he reads as
   straddling this line the same way the old standalone version
   straddled the footer's. He now RENDERS on the shared #ascii-organism
   canvas (see ascii-engine.js), not a canvas of his own — this marker
   only exists to give the organism something to measure. The real
   hit-target button is positioned every frame from JS (main.js reads
   AsciiOrganism.getMascotFrame()), since his idle sway/glances keep
   him moving slightly even when the page isn't scrolling. */
.mascot-anchor{
  position: absolute;
  top: 0; right: 30px;
  width: 1px; height: 1px;
  pointer-events: none;
}
.mascot-hit{
  position: fixed;
  top: 0; left: 0;
  background: transparent;
  border: 0;
  padding: 0;
  pointer-events: auto;
  cursor: pointer;
  z-index: 3;
  /* browsers show a default blue tap-highlight/focus glow on a plain
     button click — exactly the "button chrome" this element must not
     have, since his own click reaction (looking at you, straightening)
     is the only feedback that should read */
  -webkit-tap-highlight-color: transparent;
}

/* MASCOT HINT — a one-time "click here" nudge with an arrow curling
   toward his head, positioned every frame from the same
   getMascotFrame() data as the hit-target above (see main.js). Shown
   once per visitor (tracked via localStorage, same pattern as
   njord-lang) until they actually click him, then dismissed for good. */
.mascot-hint{
  position: fixed;
  top: 0; left: 0;
  z-index: 4;
  display: flex;
  align-items: baseline;
  gap: .35em;
  pointer-events: none;
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  letter-spacing: .04em;
  color: var(--signal-text);
  white-space: nowrap;
  opacity: 0;
  transition: opacity .5s var(--ease-out);
}
.mascot-hint.is-visible{ opacity: 1; }
@media (max-width: 640px){
  /* positioned via translate(-100%,0%) from his head (main.js) — on a
     narrow phone, a long nowrap "click here"-style string could still
     push its own left edge past the viewport if he happens to be near
     the left side when it appears. Letting it wrap removes the one
     thing (a hard nowrap width) that made that possible. */
  .mascot-hint{ white-space: normal; max-width: min(200px, 62vw); }
}
.mascot-hint__arrow{
  display: inline-block;
  font-size: 1.2em;
  animation: mascotHintNudge 1.6s ease-in-out infinite;
}
@keyframes mascotHintNudge{
  0%, 100%{ transform: translate(0, 0); }
  50%{ transform: translate(3px, -3px); }
}
@media (prefers-reduced-motion: reduce){
  .mascot-hint{ transition: none; }
  .mascot-hint__arrow{ animation: none; }
}

/* MASCOT CONVERSATION — a guided, conversational project intake
   opened by clicking the mascot (see mascot-convo.js). Grows in place
   right below the contact-row (where he sits) rather than a modal, so
   it reads as "he opened this up," not a page overlay. Restrained
   fade + slight rise on open/close, per the brief. */
/* No box — floating text/options positioned by JS (main.js reads
   AsciiOrganism.getMascotFrame() every frame and calls
   mascotConvo.updatePosition()) to the left and slightly above the
   mascot's own on-screen position, so it reads as him talking rather
   than a form anchored to the page. The outer element owns POSITION
   only (JS sets its transform every frame); the inner __panel owns
   the open/close fade — keeping these on separate elements avoids the
   two transforms fighting each other. */
.mascot-convo{
  position: fixed;
  top: 0; left: 0;
  z-index: 500;
  /* matches the page's own gutter on both sides, same as every other
     mobile-clamped width on the site, instead of its own hardcoded 32px */
  width: min(720px, calc(100vw - 2 * var(--gutter)));
}
/* On phones the floating-panel-next-to-him treatment doesn't have room to
   work — it takes over as a full screen instead, closer to a dedicated
   page than a bubble. JS's per-frame updatePosition() skips this element
   entirely below the same 720px threshold (see mascot-convo.js), so
   nothing fights this layout; the two !important rules below only guard
   against any inline style briefly set before that width check runs. */
@media (max-width: 720px){
  .mascot-convo{
    inset: 0;
    width: 100%;
    height: 100dvh;
    z-index: 900;
    transform: none !important;
    background: var(--paper);
    display: flex;
    padding: max(1.1rem, env(safe-area-inset-top)) var(--gutter) max(1.1rem, env(safe-area-inset-bottom));
    box-sizing: border-box;
  }
  .mascot-convo[hidden]{ display: none; }
  .mascot-convo__panel{
    display: flex;
    flex-direction: column;
    width: 100%;
    gap: 1rem;
  }
  .mascot-convo__top{ flex: 0 0 auto; }
  .mascot-convo__stage{
    flex: 1 1 auto;
    max-height: none !important;
    overflow-y: auto;
  }
  /* An × reads as dismissing a popup; full-screen this now reads as its
     own screen, so the same close control becomes a plain back-arrow —
     same click handler (close()), just a different affordance for a
     different context. */
  .mascot-convo__close{ font-size: 0; padding: .5em; }
  .mascot-convo__close::before{ content: '←'; font-size: 1.5rem; }
}
.mascot-convo__panel{
  display: grid; gap: .65rem;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .3s var(--ease-out), transform .3s var(--ease-out);
}
.mascot-convo.is-open .mascot-convo__panel{ opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce){
  .mascot-convo__panel{ transition: none; }
}
.mascot-convo__top{
  display: flex; align-items: center; gap: .75rem;
}
.mascot-convo__back,
.mascot-convo__close{
  font-family: var(--f-mono);
  font-size: 1rem;
  line-height: 1;
  color: var(--ink-faint);
  padding: .3em .4em;
  transition: color .2s;
}
.mascot-convo__back:hover,
.mascot-convo__close:hover{ color: var(--ink); }
.mascot-convo__progress{
  flex: 1;
  height: 1px;
  background: var(--line);
}
.mascot-convo__progress-fill{
  display: block;
  height: 100%;
  width: 0%;
  background: var(--signal-text);
  transition: width .3s var(--ease-out);
}
/* Panel grows up-and-left from the mascot (see updatePosition() in
   mascot-convo.js) — a long option list (e.g. the problem/goal choices)
   could otherwise push its top edge past the section's own border line
   above #contact, forcing the visitor to scroll the PAGE up just to
   see the first option. Capping height and scrolling internally here
   instead keeps the whole thing within reach of the mascot, and keeps
   back/progress/close (in __top, outside this element) reachable
   regardless of scroll position within the content itself. */
.mascot-convo__stage{
  display: grid; gap: .75rem;
  max-height: min(48vh, 380px);
  overflow-y: auto;
  padding-inline: 8px .3rem;
}
.mascot-convo__prompt{ display: grid; gap: .15em; }
.mascot-convo__prompt p{
  font-family: var(--f-display);
  font-weight: 380;
  font-size: clamp(1.1rem, 2vw, 1.4rem);
  letter-spacing: -0.01em;
  line-height: 1.15;
}
.mascot-convo__sr{
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0; overflow: hidden;
  clip: rect(0,0,0,0); white-space: nowrap;
}
.mascot-convo__helper{
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  color: var(--ink-faint);
  margin-top: -.6em;
}
.mascot-convo__stage input,
.mascot-convo__stage textarea{
  width: 100%;
  font: inherit;
  font-family: var(--f-display);
  font-size: 1.05rem;
  color: var(--ink);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--line-strong);
  padding: .35em .45em;
  box-sizing: border-box;
  transition: border-color .25s;
  resize: vertical;
}
.mascot-convo__stage textarea{ min-height: 3.5rem; }
.mascot-convo__stage input:focus-visible,
.mascot-convo__stage textarea:focus-visible{ border-color: var(--signal-text); }
.mascot-convo__field--important{
  font-size: 1.2rem;
  border-bottom-width: 2px;
}
.mascot-convo__choices{
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: .45rem;
}
.mascot-convo__choice{
  border: 1px solid var(--line-strong);
  padding: .5em .8em;
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  text-align: left;
  transition: background .2s, color .2s, border-color .2s;
}
.mascot-convo__choice:hover{ border-color: var(--ink); }
.mascot-convo__choice.is-selected,
.mascot-convo__choice--primary{
  background: var(--ink); color: var(--paper); border-color: var(--ink);
}
.mascot-convo__contact-row{ display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.mascot-convo__contact-field{ display: flex; flex-direction: column; gap: .35em; }
.mascot-convo__contact-field > span{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
@media (max-width: 640px){
  .mascot-convo__contact-row{ grid-template-columns: 1fr; }
  .mascot-convo__prompt p{ font-size: clamp(1.25rem, 5.5vw, 1.4rem); }
  .mascot-convo__stage input,
  .mascot-convo__stage textarea{ font-size: 1.2rem; }
  .mascot-convo__field--important{ font-size: 1.35rem; }
  /* the desktop cap (48vh/380px) was tuned for a wide sidebar-style
     panel next to him — paired with the bigger mobile type above and
     less horizontal room to spread choices across, stages ran out of
     vertical space and had to scroll internally mid-sentence. Combined
     with pushing his own resting position further down the viewport
     (see the mobile mascotFloor in ascii-engine.js's _timelineSegment),
     there's real headroom to actually use here now. */
  .mascot-convo__stage{ max-height: min(62vh, 520px); }
}
.mascot-convo__foot{ display: flex; align-items: center; gap: .8rem; flex-wrap: wrap; }
.mascot-convo__primary{
  border: 1px solid currentColor;
  padding: .65em 1.2em;
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  letter-spacing: .03em;
  display: inline-flex; align-items: center; gap: .6em;
  transition: background .25s, color .25s, opacity .25s;
}
.mascot-convo__primary:hover{ background: var(--ink); color: var(--paper); }
.mascot-convo__primary:disabled{ opacity: .4; pointer-events: none; }
.mascot-convo__skip{
  font-family: var(--f-mono);
  font-size: var(--fs-small);
  color: var(--ink-faint);
  transition: color .2s;
}
.mascot-convo__skip:hover{ color: var(--ink); }
.mascot-convo__note{
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  opacity: 0;
  transition: opacity .3s;
}
.mascot-convo__note.is-visible{ opacity: .75; }
.mascot-convo__note.is-error{ color: var(--error); opacity: 1; }

/* ---------------------------------------------------------------
   Project preview overlay
   --------------------------------------------------------------- */
.project-modal{
  position: fixed; inset: 0; z-index: 600;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  opacity: 0;
  visibility: hidden;
  transform: translateY(12px);
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out), visibility 0s linear .5s;
  background: var(--paper);
  color: var(--ink);
  will-change: clip-path;
}
.project-modal.is-open{
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .5s var(--ease-out), transform .5s var(--ease-out);
}

/* full-bleed ASCII texture behind every project page — dense and faint,
   a watermark the text sits on top of, never competing for attention */
.project-modal__bg{
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.project-modal__bg canvas{
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: .16;
}

.project-modal__bar{
  position: relative; z-index: 1;
  display: flex; justify-content: space-between; align-items: center;
  /* same reasoning as .site-header above — this bar sits flush against
     the real viewport top (the modal itself is position:fixed; inset:0),
     so its own close/brand button needs the same safe-area clearance */
  padding: calc(clamp(18px,3vw,32px) + env(safe-area-inset-top, 0px)) var(--gutter) clamp(18px,3vw,32px);
  font-family: var(--f-mono);
  font-size: var(--fs-micro);
  letter-spacing: .1em;
  text-transform: uppercase;
}
/* the wordmark itself is the close control now — no separate "back"
   button/label — it simply grows on hover/focus to read as clickable */
.project-modal__brand{
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  transform-origin: left center;
  transition: transform .3s var(--ease-out);
}
.project-modal__brand:hover,
.project-modal__brand:focus-visible{
  transform: scale(1.18);
}
.project-modal__body{
  position: relative; z-index: 1;
  flex: 1;
  display: flex;
  align-items: center;
  padding: clamp(1rem,4vw,3rem) var(--gutter) clamp(3rem,6vw,5rem);
  max-width: var(--maxw);
  margin-inline: auto;
  width: 100%;
}
.project-modal__body > div{ max-width: 60ch; }
.project-modal__eyebrow{ font-family: var(--f-mono); font-size: var(--fs-micro); letter-spacing: .14em; text-transform: uppercase; opacity: .7; margin-bottom: .8em; }
.project-modal__title{ font-size: clamp(3rem, 9vw, 7rem); line-height: .9; margin-bottom: .3em; }
.project-modal__tag{ font-size: clamp(1.1rem, 2vw, 1.5rem); opacity: .85; max-width: 22ch; margin-bottom: 1.1em; }
.project-modal__desc{ font-size: 1.02rem; max-width: 52ch; opacity: .85; line-height: 1.65; }
.project-modal__meta{
  margin-top: 2rem; display: grid; gap: .5rem;
  font-family: var(--f-mono); font-size: var(--fs-small); opacity: .75;
}

.project-modal__highlights{
  position: relative; z-index: 1;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.2rem, 3vw, 2rem);
  max-width: var(--maxw);
  margin-inline: auto;
  width: 100%;
  padding: clamp(1.5rem,4vw,2.5rem) var(--gutter) clamp(2rem,5vw,3rem);
  border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent);
}
.project-modal__highlights > div{ display: flex; flex-direction: column; gap: .7em; }
.project-modal__highlight-num{
  font-family: var(--f-mono); font-size: var(--fs-micro); letter-spacing: .1em; opacity: .5;
}
.project-modal__highlights p{ font-size: .95rem; line-height: 1.55; max-width: 30ch; opacity: .9; }

.project-modal__cta-row{
  position: relative; z-index: 1;
  display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap;
  max-width: var(--maxw); margin-inline: auto; width: 100%;
  padding: 0 var(--gutter) clamp(3rem,6vw,4.5rem);
}
.project-modal__cta{
  border: 1px solid currentColor;
  padding: .85em 1.6em;
  font-family: var(--f-mono); font-size: var(--fs-small); letter-spacing: .03em;
  display: inline-flex; align-items: center; gap: .6em;
  transition: background .25s, color .25s, border-color .25s;
}
.project-modal__cta-note{
  font-family: var(--f-mono); font-size: var(--fs-micro); letter-spacing: .06em; text-transform: uppercase;
  opacity: 0; transition: opacity .3s;
}
.project-modal__cta-note.is-visible{ opacity: .65; }

/* right-middle link out to the project's real, eventual live site —
   inert for now (no build to send it to), interactive feedback stands
   in until there is one to link to */
.project-modal__sitelink-wrap{
  position: fixed; z-index: 40;
  right: var(--gutter); top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; align-items: flex-end; gap: .6em;
}
.project-modal__sitelink{
  border: 1px solid currentColor;
  padding: .7em 1.2em;
  font-family: var(--f-mono); font-size: var(--fs-small); letter-spacing: .04em; text-transform: uppercase;
  display: inline-flex; align-items: center; gap: .6em;
  white-space: nowrap;
  transition: background .25s, color .25s;
}
.project-modal__sitelink-note{
  font-family: var(--f-mono); font-size: var(--fs-micro); letter-spacing: .06em; text-transform: uppercase;
  opacity: 0; transition: opacity .3s;
}
.project-modal__sitelink-note.is-visible{ opacity: .65; }

.project-modal--aurelia{ background: var(--aurelia-bg); color: var(--aurelia-ink); font-family: 'Fraunces', serif; }
.project-modal--aurelia .project-modal__title{ font-style: italic; font-weight: 500; }
.project-modal--aurelia .project-modal__desc{ font-family: 'Fraunces', serif; font-weight: 400; }
.project-modal--aurelia .project-modal__cta{ border-color: var(--aurelia-accent); }
.project-modal--aurelia .project-modal__cta:hover{ background: var(--aurelia-accent); color: var(--aurelia-bg); }
.project-modal--aurelia .project-modal__sitelink{ border-color: var(--aurelia-accent); }
.project-modal--aurelia .project-modal__sitelink:hover{ background: var(--aurelia-accent); color: var(--aurelia-bg); }

.project-modal--pulse{ background: var(--pulse-bg); color: var(--pulse-ink); font-family: 'Archivo', sans-serif; }
.project-modal--pulse .project-modal__title{ font-family: 'Bebas Neue', sans-serif; letter-spacing: .01em; text-transform: uppercase; }
.project-modal--pulse .project-modal__desc{ font-weight: 500; }
.project-modal--pulse .project-modal__cta{ border-color: var(--pulse-accent); }
.project-modal--pulse .project-modal__cta:hover{ background: var(--pulse-accent); color: var(--pulse-bg); }
.project-modal--pulse .project-modal__sitelink{ border-color: var(--pulse-accent); }
.project-modal--pulse .project-modal__sitelink:hover{ background: var(--pulse-accent); color: var(--pulse-bg); }

.project-modal--meridian{ background: var(--meridian-bg); color: var(--meridian-ink); font-family: 'IBM Plex Sans', sans-serif; }
.project-modal--meridian .project-modal__title{ font-family: 'IBM Plex Mono', monospace; letter-spacing: -0.02em; }
.project-modal--meridian .project-modal__eyebrow, .project-modal--meridian .project-modal__meta{ font-family: 'IBM Plex Mono', monospace; }
.project-modal--meridian .project-modal__cta{ border-color: var(--meridian-accent); }
.project-modal--meridian .project-modal__cta:hover{ background: var(--meridian-accent); color: var(--meridian-bg); }
.project-modal--meridian .project-modal__sitelink{ border-color: var(--meridian-accent); }
.project-modal--meridian .project-modal__sitelink:hover{ background: var(--meridian-accent); color: var(--meridian-bg); }

@media (max-width: 860px){
  .project-modal__highlights{ grid-template-columns: 1fr; }
}
@media (max-width: 720px){
  .project-modal__sitelink-wrap{ display: none; }
}

body.modal-open{ overflow: hidden; }
