/* ============================================================
   Fábio Braz — Portfolio
   ============================================================ */

/* Display face. Lemony is a licensed-separately face served from the repo;
   its demo build has no glyphs for 0-9 , . & % — headings are written to
   avoid them. To go back to Bodoni Moda: swap the --serif line below AND
   restore "family=Bodoni+Moda:opsz,wght@6..96,400;6..96,500&" to the Google
   Fonts href in index.html. */
@font-face{
  font-family:"Lemony";
  src:url("../fonts/Lemony-Regular.otf") format("opentype");
  font-weight:400;
  font-style:normal;
  font-display:swap;
}

:root{
  --bg:            #060606;
  --bg-raise:      #0c0c0d;
  --line:          rgba(255,255,255,.09);
  --line-strong:   rgba(255,255,255,.16);
  --text:          #f2f2f0;
  --text-dim:      #a3a3a0;
  --text-faint:    #6b6b68;
  --accent:        #ffffff;

  --serif: "Lemony", Georgia, "Times New Roman", serif;
  /* revert: --serif: "Bodoni Moda", "Playfair Display", Georgia, "Times New Roman", serif; */
  --sans:  "Google Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  /* revert: --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
     and swap the family back in the Google Fonts href in index.html */

  --gutter: clamp(20px, 5vw, 80px);
  --maxw: 1440px;
  --r: 18px;
  --r-lg: 26px;

  --ease: cubic-bezier(.22,1,.36,1);
  /* Fast out of the gate, long settle — for things that travel. Mirrors
     SWIFT in main.js. */
  --swift: cubic-bezier(.32,.72,0,1);

  /* Durations. Feedback is fast and travel is slow: a press answers inside a
     frame or two, a hover acknowledges without making anyone wait, and only
     large surfaces take long enough to be seen moving. Slide-ins and overlay
     flights (.45–.56s) live with their own components. */
  --t-press: 110ms;
  --t-hover: 220ms;
  --t-lift:  380ms;
}

*,*::before,*::after{ box-sizing:border-box; }

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

body{
  margin:0;
  background:var(--bg);
  color:var(--text);
  font-family:var(--sans);
  font-size:16px;
  font-weight:300;
  line-height:1.6;
  letter-spacing:-.01em;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  overflow-x:hidden;
}


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

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

::selection{ background:#fff; color:#000; }

/* ── Intro loader ──────────────────────────────────────── */
.intro{ display:none; }
/* No overflow:hidden here: hiding the scrollbar during the intro and bringing
   it back after made the page reflow. main.js blocks scrolling instead. */
.is-intro .intro{
  position:fixed; inset:0;
  z-index:1000;
  display:grid;
  place-items:center;
  background:#000;
  transition:background-color .7s ease;
}
/* Two beats: draw in big while easing back very slightly (the .logo-draw
   timeline, ends 2.2s), then main.js flies it straight onto the nav logo. */
.intro__logo{
  width:var(--intro-big, min(80vw, 82vh));
  height:auto;
  overflow:visible;
  filter:drop-shadow(0 0 28px rgba(255,255,255,.2));
  transform-origin:50% 50%;
  animation:intro-zoom 2.4s cubic-bezier(.25,.1,.25,1) forwards;   /* drifts back as it draws */
}
/* The outline is drawn at full size, so it's thinned to stay a fine line there */
.intro .intro__logo path{ stroke-width:var(--intro-stroke, .12); }
@keyframes intro-zoom{
  from{ transform:scale(1); }
  to{ transform:scale(.92); }
}
/* Drawn but the page is still loading: breathe until it's ready. The zoom is
   repeated in the list so adding the breathe doesn't restart or drop it. */
.intro.is-waiting .intro__logo{
  animation:
    intro-zoom 2.4s cubic-bezier(.25,.1,.25,1) forwards,
    intro-breathe 1.8s ease-in-out infinite;
}
@keyframes intro-breathe{ 50%{ opacity:.55 } }
/* Handoff: the screen clears while main.js flies the logo into the nav */
.intro.is-leaving{ background-color:transparent; pointer-events:none; }
.intro.is-leaving .intro__logo{
  animation:none;
  transform-origin:0 0;
  transition:filter .9s ease;
  filter:drop-shadow(0 0 10px rgba(255,255,255,.18));
}

/* ── Film grain ─────────────────────────────────────────── */
.grain{
  position:fixed; inset:-50%;
  z-index:9999;
  pointer-events:none;
  opacity:.16;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
  animation:grain 8s steps(6) infinite;
  mix-blend-mode:overlay;
}
@keyframes grain{
  0%,100%{ transform:translate(0,0) }
  10%{ transform:translate(-3%,-4%) }
  30%{ transform:translate(2%,-3%) }
  50%{ transform:translate(-2%,3%) }
  70%{ transform:translate(3%,2%) }
  90%{ transform:translate(-3%,1%) }
}

/* ── Cursor spotlight ───────────────────────────────────── */
.cursor-glow{
  position:fixed;
  top:0; left:0;
  width:640px; height:640px;
  margin:-320px 0 0 -320px;
  border-radius:50%;
  pointer-events:none;
  z-index:1;
  opacity:0;
  background:radial-gradient(circle, rgba(255,255,255,.055) 0%, rgba(255,255,255,0) 62%);
  transition:opacity .6s ease;
  will-change:transform;
}
@media (pointer:fine){ .cursor-glow.on{ opacity:1; } }

/* ── Typography ─────────────────────────────────────────── */
.display{
  font-family:var(--serif);
  font-weight:400;
  font-optical-sizing:auto;
  line-height:.95;
  letter-spacing:-.02em;
  margin:0;
  background:linear-gradient(180deg,#fff 6%,#e6e6e2 36%,#606060 100%);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  text-wrap:balance;
}

.pill{
  display:inline-flex;
  align-items:center;
  gap:8px;
  padding:7px 16px;
  border-radius:999px;
  border:1px solid var(--line);
  background:rgba(255,255,255,.045);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  font-size:12.5px;
  font-weight:400;
  letter-spacing:.04em;
  color:var(--text-dim);
  white-space:nowrap;
}
.pill--sm{ padding:4px 11px; font-size:11px; }

.btn{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:8px;
  height:46px;
  padding:0 26px;
  border-radius:999px;
  font-size:14px;
  font-weight:400;
  letter-spacing:-.005em;
  border:1px solid transparent;
  transition:transform var(--t-hover) var(--ease), background var(--t-hover) ease, border-color var(--t-hover) ease, color var(--t-hover) ease;
  white-space:nowrap;
}
.btn--solid{ background:#f2f2f0; color:#0a0a0a; }
@media (hover:hover){ .btn--solid:hover{ transform:translateY(-2px); background:#fff; } }
.btn--ghost{
  border-color:var(--line-strong);
  color:var(--text);
  background:rgba(255,255,255,.02);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
}
@media (hover:hover){ .btn--ghost:hover{ transform:translateY(-2px); border-color:rgba(255,255,255,.34); background:rgba(255,255,255,.06); } }

/* ── Nav ────────────────────────────────────────────────── */
.nav{
  position:fixed;
  top:0; left:0; right:0;
  z-index:100;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:24px;
  padding:22px var(--gutter);
  transition:padding .5s var(--ease), background .5s ease, backdrop-filter .5s ease, border-color .5s ease;
  border-bottom:1px solid transparent;
}
.nav.is-stuck{
  padding:14px var(--gutter);
  background:rgba(6,6,6,.62);
  backdrop-filter:blur(22px) saturate(140%);
  -webkit-backdrop-filter:blur(22px) saturate(140%);
  border-bottom-color:var(--line);
}

.nav__mark{
  display:inline-flex;
  align-items:center;
  gap:10px;
  font-size:15px;
  font-weight:400;
  letter-spacing:-.015em;
  color:var(--text-dim);
  transition:color .3s ease;
}
@media (hover:hover){ .nav__mark:hover{ color:var(--text); } }
.nav__logo{
  width:25px; height:auto;
  filter:drop-shadow(0 0 10px rgba(255,255,255,.18));
  transition:filter .4s ease, transform .5s var(--ease);
}
/* Draw-in on load (nav logo and intro logo): each piece's outline traces itself (dash slides from hidden
   to drawn), then the fill floods in. "1 2" (not "1 1") keeps a zero-length
   dash off the ends. */
.logo-draw path{
  fill:#fff;
  fill-opacity:0;
  stroke:#fff;
  stroke-width:1.4;
  stroke-linejoin:round;
  stroke-dasharray:1 2;
  stroke-dashoffset:1;
  animation:
    logo-trace 1.1s cubic-bezier(.65,0,.35,1) forwards,
    logo-fill .6s ease forwards;
}
.logo-draw path:nth-child(1){ animation-delay:.2s,  1.25s; }
.logo-draw path:nth-child(2){ animation-delay:.45s, 1.4s; }
.logo-draw path:nth-child(3){ animation-delay:.6s,  1.5s; }
.logo-draw path:nth-child(4){ animation-delay:.75s, 1.6s; }
@keyframes logo-trace{ to{ stroke-dashoffset:0 } }
@keyframes logo-fill{ to{ fill-opacity:1 } }
@media (prefers-reduced-motion: reduce){
  .logo-draw path{ animation:none; stroke-dashoffset:0; fill-opacity:1; }
}
/* With the intro running, the nav logo arrives already drawn (the intro logo
   lands on it), so it sits hidden until the handoff and skips its own draw-in. */
.has-intro .nav__logo path{ animation:none; stroke-dashoffset:0; fill-opacity:1; }
.is-intro .nav__logo{ visibility:hidden; }
@media (hover:hover){ .nav__mark:hover .nav__logo{
  filter:drop-shadow(0 0 14px rgba(255,255,255,.45));
  transform:translateY(-1px);
} }

.nav__links{
  position:relative;              /* the sliding pill measures from here */
  display:flex;
  align-items:center;
  gap:4px;
  padding:5px;
  border-radius:999px;
  border:1px solid var(--line);
  background:rgba(255,255,255,.035);
  backdrop-filter:blur(18px);
  -webkit-backdrop-filter:blur(18px);
}
.nav__links a{
  position:relative;
  padding:7px 15px;
  border-radius:999px;
  font-size:13px;
  color:var(--text-dim);
  transition:color .3s ease, background .3s ease;
}
@media (hover:hover){ .nav__links a:hover{ color:var(--text); } }
.nav__links a.is-active{ color:var(--text); }

/* ── Sliding selection (makeGlide in main.js) ───────────────
   A copy of the control in its selected colours, laid exactly over it and
   clipped to the chosen option. Moving the clip is what slides the pill. */
.glide{
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  gap:inherit;
  padding:inherit;
  border-radius:inherit;
  background:#f2f2f0;
  pointer-events:none;
  opacity:0;
  clip-path:inset(0 100% 0 0 round 999px);
  transition:clip-path .45s var(--swift), opacity .3s ease;
}
.glide .glide__item{ color:#0a0a0a; background:none; }

.nav__burger{
  display:none;
  width:44px; height:44px;
  border-radius:999px;
  border:1px solid var(--line);
  background:rgba(255,255,255,.035);
  backdrop-filter:blur(18px);
  -webkit-backdrop-filter:blur(18px);
  cursor:pointer;
  padding:0;
  place-items:center;
  align-content:center;
  gap:5px;
}
.nav__burger span{
  display:block;
  width:16px; height:1.5px;
  background:var(--text);
  margin:0 auto;
  transition:transform .4s var(--ease), opacity .3s ease;
}
.nav__burger[aria-expanded="true"] span:first-child{ transform:translateY(3.25px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:last-child{ transform:translateY(-3.25px) rotate(-45deg); }

.mobile-menu{
  position:fixed;
  inset:0;
  z-index:99;
  display:flex;
  flex-direction:column;
  justify-content:center;
  gap:6px;
  padding:0 var(--gutter);
  background:rgba(6,6,6,.94);
  backdrop-filter:blur(26px);
  -webkit-backdrop-filter:blur(26px);
}
.mobile-menu[hidden]{ display:none; }
.mobile-menu a{
  font-family:var(--serif);
  font-size:clamp(30px,8vw,46px);
  line-height:1.25;
  color:var(--text-dim);
  transition:color .3s ease, transform .5s var(--ease);
}
.mobile-menu a:hover,.mobile-menu a:active{ color:#fff; }

/* ── Hero ───────────────────────────────────────────────── */
.hero{
  position:relative;
  min-height:100svh;
  display:flex;
  flex-direction:column;
  justify-content:flex-end;
  padding:0 var(--gutter) clamp(56px,9vh,110px);
  overflow:hidden;
  isolation:isolate;
}

.hero__media{ position:absolute; inset:0; z-index:-2; background:#050505; }

/* The film. Its poster is frame 0 of the same file, so nothing shifts when the
   video data arrives. Scrubbed by scroll — see main.js. */
.hero__video{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  object-position:center 38%;
  opacity:.9;
  filter:saturate(.85) contrast(1.05);
}
/* Fallback lighting when no video file is present yet */
.hero__media.is-empty .hero__video{ display:none; }
.hero__media.is-empty{
  background:
    radial-gradient(120% 78% at 50% -8%, rgba(255,255,255,.19) 0%, rgba(255,255,255,.05) 26%, rgba(0,0,0,0) 58%),
    radial-gradient(70% 55% at 50% 42%, rgba(255,255,255,.055) 0%, rgba(0,0,0,0) 70%),
    #050505;
}

.hero__spot{
  position:absolute; inset:0;
  background:radial-gradient(105% 68% at 50% 0%, rgba(255,255,255,.14) 0%, rgba(0,0,0,0) 55%);
  mix-blend-mode:screen;
  pointer-events:none;
}
.hero__vignette{
  position:absolute; inset:0;
  background:
    linear-gradient(to top, var(--bg) 1%, rgba(6,6,6,.86) 20%, rgba(6,6,6,.34) 46%, rgba(6,6,6,0) 72%),
    linear-gradient(to right, rgba(6,6,6,.80) 0%, rgba(6,6,6,.42) 30%, rgba(6,6,6,0) 58%),
    radial-gradient(130% 100% at 50% 42%, rgba(0,0,0,0) 34%, rgba(0,0,0,.74) 100%);
  pointer-events:none;
}

.hero__inner{
  position:relative;
  width:100%;
  max-width:var(--maxw);
  margin:0 auto;
}

.hero__title{
  margin:22px 0 0;
  font-size:clamp(50px,10.8vw,158px);
  background:none;               /* the lines carry it, see below */
  /* Flex, so the masks' negative margins cancel their padding exactly; in
     block flow they collapse into each other and the h1 grew by 11px */
  display:flex;
  flex-direction:column;
}
.hero__title.reveal{ opacity:1; transform:none; filter:none; }

/* Each line rises out of its own mask. The mask is padded past the line box
   (and pulled back with negative margins, so layout is unchanged) because
   line-height .95 puts the g's descender and the caps' tops outside it.
   The gradient lives on each moving line, not on the h1: text clipped to a
   parent's background stops tracking a child that is transformed, in Safari
   especially. At 200% height the two halves join into the one gradient the
   title had. The hero runs it at a slight angle where the other headings
   run it straight down. */
.hero__title .line{
  display:block;
  overflow:hidden;
  padding:.04em 0 .14em;
  margin:-.04em 0 -.14em;
}
.hero__title .line:last-child{ padding-left:.09em; }
.hero__title .line__in{
  display:block;
  background:linear-gradient(183deg,#fff 6%,#e6e6e2 36%,#616060 100%) 0 0 / 100% 200%;
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
  transform:translateY(125%);    /* clear of the padded mask */
  transition:transform 1.25s var(--swift);
}
.hero__title .line:last-child .line__in{
  background-position:0 100%;
  transition-delay:.1s;
}
.hero__title.in .line__in{ transform:none; }

.hero__lede{
  max-width:52ch;
  margin:28px 0 0;
  font-size:clamp(15px,1.35vw,18.5px);
  line-height:1.65;
  color:var(--text-dim);
}
.hero__lede-lead{
  display:block;
  margin-bottom:7px;
  color:var(--text);
  letter-spacing:-.015em;
}

.hero__actions{
  display:flex;
  flex-wrap:wrap;
  gap:12px;
  margin-top:34px;
}

.hero__scroll{
  position:absolute;
  right:var(--gutter);
  bottom:clamp(56px,9vh,110px);
  display:flex;
  align-items:center;
  gap:12px;
  font-size:11.5px;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--text-faint);
}
.hero__scroll-line{
  display:block;
  width:52px; height:1px;
  background:linear-gradient(to right, rgba(255,255,255,0), rgba(255,255,255,.55));
  position:relative;
  overflow:hidden;
}
.hero__scroll-line::after{
  content:"";
  position:absolute; inset:0;
  background:#fff;
  transform:translateX(-100%);
  animation:sweep 2.6s var(--ease) infinite;
}
@keyframes sweep{ 60%,100%{ transform:translateX(100%) } }

/* ── Marquee ────────────────────────────────────────────── */
.marquee{
  position:relative;
  padding:26px 0;
  border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);
  overflow:hidden;
  background:linear-gradient(to bottom, rgba(255,255,255,.018), rgba(255,255,255,0));
  -webkit-mask-image:linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
  mask-image:linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}
.marquee__track{
  display:flex;
  align-items:center;
  /* No flex gap. `gap` sits *between* children, so the space where the track
     wraps from its second copy back to its first is one gap short — at
     translateX(-50%) that leaves a half-gap seam and the loop visibly jumps.
     A right margin on every child, the last one included, makes the pattern
     exactly periodic, so -50% lands on a seamless repeat. */
  gap:0;
  width:max-content;
  /* Duration tracks the distance travelled, not the item count: -50% of an
     eight-copy track is ~2844px, and 120s holds the original ~24px/s. */
  animation:scroll 120s linear infinite;
  font-family:var(--serif);
  font-size:clamp(18px,2.3vw,30px);
  color:var(--text-faint);
  white-space:nowrap;
}
.marquee__track > *{ margin-right:26px; }
.marquee__track .sep{ font-size:.5em; color:rgba(255,255,255,.28); }
@keyframes scroll{ to{ transform:translateX(-50%) } }

/* ── Work sections ──────────────────────────────────────── */
.work{
  position:relative;
  max-width:var(--maxw);
  margin:0 auto;
  padding:clamp(90px,13vh,170px) var(--gutter);
  display:grid;
  grid-template-columns:minmax(280px,.82fr) 1.5fr;
  gap:clamp(40px,6vw,90px);
  align-items:start;
}
.work + .work{ border-top:1px solid var(--line); }

/* The head has to span the full row, or the sticky block has no room to travel */
.work__head{ align-self:stretch; }
.work__sticky{ position:sticky; top:120px; }

.work__title{
  margin:20px 0 0;
  font-size:clamp(38px,4.6vw,68px);
}
.work__copy{
  margin:22px 0 0;
  max-width:40ch;
  font-size:15.5px;
  color:var(--text-dim);
}

.tools{
  display:flex;
  flex-wrap:wrap;
  gap:8px;
  margin:28px 0 0;
  padding:0;
  list-style:none;
}
.tools li{
  padding:6px 14px;
  border-radius:999px;
  border:1px solid var(--line);
  background:rgba(255,255,255,.035);
  font-size:12px;
  color:var(--text-faint);
  letter-spacing:.01em;
}

/* ── Bento grid ─────────────────────────────────────────── */
.bento{
  display:grid;
  grid-template-columns:repeat(6,1fr);
  gap:clamp(12px,1.4vw,20px);
}

.card{
  position:relative;
  margin:0;
  grid-column:span 3;
  border-radius:var(--r-lg);
  border:1px solid var(--line);
  background:linear-gradient(158deg, rgba(255,255,255,.055), rgba(255,255,255,.014));
  overflow:hidden;
  transition:transform var(--t-lift) var(--ease), border-color var(--t-lift) ease, box-shadow var(--t-lift) ease;
}
@media (hover:hover){ .card:hover{
  transform:translateY(-5px);
  border-color:var(--line-strong);
  box-shadow:0 26px 60px -28px rgba(0,0,0,.9), 0 0 0 1px rgba(255,255,255,.04) inset;
} }

.card--lead{ grid-column:span 6; }
.card--half{ grid-column:span 3; }
.card--two-thirds{ grid-column:span 4; }
.card--third{ grid-column:span 2; }

.card__media{
  height:clamp(200px,22vw,310px);
  background:#0b0b0c;
  overflow:hidden;
}
.card--lead .card__media{ height:clamp(265px,33vw,470px); }

/* Project cards carry a scene render (the design shot on a laptop and phone in a
   lit environment), all exported 16:9. The card takes the image's ratio rather
   than the other way round — at the half cards' native 1.22 `cover` sliced the
   phone off the right edge. Keep new scene renders at 16:9. */
/* The ratio belongs to the artwork, not to whether the card opens: a scene
   render still needs 16/9 while a project is waiting on its screenshots. */
.card--openable .card__media,
.card--scene .card__media{ height:auto; aspect-ratio:16/9; }

/* The 16:9 rule above exists for the web scene renders, where the phone sits
   hard against the right edge and `cover` would slice it off. A render with
   nothing load-bearing at its edges can take a normal card slot instead and let
   `cover` crop the overflow, so it matches the row it shares. */
.card--openable.card--crop .card__media{ height:clamp(200px,22vw,310px); aspect-ratio:auto; }

/* A portrait scene render (an app shot on a phone) gets a portrait card — 5:6 is
   the renders' own ratio, so nothing is cropped. Its row-mate stretches to match,
   which is the bento reading: one tall card beside one wide one. */
.card--tall .card__media,
.card--openable.card--tall .card__media{ height:auto; aspect-ratio:5/6; }

.card figcaption{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:14px;
  padding:16px 20px 18px;
  border-top:1px solid var(--line);
  font-size:13.5px;
}
.card figcaption span{ color:var(--text); }
.card figcaption em{ font-style:normal; font-size:11.5px; color:var(--text-faint); letter-spacing:.02em; }

/* ── Sound / tracks ─────────────────────────────────────── */
.tracks{ display:flex; flex-direction:column; gap:10px; }

.track{
  display:grid;
  /* Real track titles are longer than the placeholders' "Track title": at
     1fr the meta column came out 196px against the 237px this one needs,
     wrapping to two lines and making the row 30px taller than its
     neighbours. 1.5fr gives it 252px. */
  grid-template-columns:auto minmax(130px,1.5fr) 2fr auto;
  align-items:center;
  gap:clamp(14px,2vw,26px);
  padding:16px 22px;
  border-radius:var(--r);
  border:1px solid var(--line);
  background:linear-gradient(158deg, rgba(255,255,255,.05), rgba(255,255,255,.012));
  transition:transform var(--t-lift) var(--ease), border-color var(--t-hover) ease, background var(--t-hover) ease;
}
@media (hover:hover){ .track:hover{ transform:translateX(4px); border-color:var(--line-strong); } }

.track__play{
  display:grid;
  place-items:center;
  width:42px; height:42px;
  flex:none;
  border-radius:50%;
  border:1px solid var(--line-strong);
  background:rgba(255,255,255,.05);
  color:var(--text);
  cursor:pointer;
  transition:background var(--t-hover) ease, transform var(--t-hover) var(--ease);
}
@media (hover:hover){ .track__play:hover{ background:#f2f2f0; transform:scale(1.06); } }
@media (hover:hover){ .track__play:hover .tri{ border-left-color:#0a0a0a; } }
.tri{
  width:0; height:0;
  margin-left:3px;
  border-left:9px solid var(--text);
  border-top:6px solid transparent;
  border-bottom:6px solid transparent;
  transition:border-color .3s ease;
}
.track__play.is-playing .tri{
  width:9px; height:11px;
  margin-left:0;
  border:0;
  background:linear-gradient(to right,var(--text) 0 3px, transparent 3px 6px, var(--text) 6px 9px);
}
@media (hover:hover){ .track__play.is-playing:hover .tri{
  background:linear-gradient(to right,#0a0a0a 0 3px, transparent 3px 6px, #0a0a0a 6px 9px);
} }

.track__meta h3{
  margin:0;
  font-family:var(--serif);
  font-weight:400;
  font-size:19px;
  letter-spacing:-.01em;
}
.track__meta p{ margin:2px 0 0; font-size:12px; color:var(--text-faint); }

.wave{
  display:flex;
  align-items:center;
  gap:3px;
  height:38px;
  overflow:hidden;
}
.wave i{
  flex:1;
  min-width:2px;
  border-radius:2px;
  background:rgba(255,255,255,.2);
  transition:background .3s ease, transform .3s ease;
  transform-origin:center;
}
@media (hover:hover){ .track:hover .wave i{ background:rgba(255,255,255,.34); } }

.track__time{ font-size:12.5px; color:var(--text-faint); font-variant-numeric:tabular-nums; }

/* ── AI projects ────────────────────────────────────────── */
.projects{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(300px,1fr));
  gap:clamp(12px,1.4vw,20px);
}

.project{
  display:flex;
  flex-direction:column;
  border-radius:var(--r-lg);
  border:1px solid var(--line);
  background:linear-gradient(158deg, rgba(255,255,255,.055), rgba(255,255,255,.014));
  overflow:hidden;
  transition:transform var(--t-lift) var(--ease), border-color var(--t-lift) ease, box-shadow var(--t-lift) ease;
}
@media (hover:hover){ .project:hover{
  transform:translateY(-5px);
  border-color:var(--line-strong);
  box-shadow:0 26px 60px -28px rgba(0,0,0,.9);
} }
.project--soon{ opacity:.62; }
@media (hover:hover){ .project--soon:hover{ transform:none; } }

.project__media{
  /* 16/9, matching the openable project cards in the work sections, so a
     project card and a work card read at the same scale. */
  aspect-ratio:16/9;
  background:#0b0b0c center/cover no-repeat;
  background-image:
    radial-gradient(70% 60% at 26% 14%, rgba(255,255,255,.12), rgba(255,255,255,0) 60%),
    radial-gradient(60% 70% at 84% 90%, rgba(255,255,255,.06), rgba(255,255,255,0) 60%);
  border-bottom:1px solid var(--line);
}

.project__body{ padding:24px 26px 28px; }
.project__top{ display:flex; align-items:center; gap:12px; }
.project__top h3{
  margin:0;
  font-family:var(--serif);
  font-weight:400;
  font-size:clamp(24px,2.6vw,32px);
  letter-spacing:-.015em;
}
.project__body p{
  margin:12px 0 0;
  max-width:60ch;
  font-size:14.5px;
  color:var(--text-dim);
}
.project__link{
  display:inline-flex;
  align-items:center;
  gap:7px;
  margin-top:20px;
  font-size:13.5px;
  color:var(--text);
}
.project__link i{ transition:transform var(--t-hover) var(--ease); font-style:normal; }
@media (hover:hover){ .project:hover .project__link i{ transform:translate(3px,-3px); } }
.project__link--muted{ color:var(--text-faint); }

/* ── About ──────────────────────────────────────────────── */
.about{
  max-width:var(--maxw);
  margin:0 auto;
  padding:clamp(90px,13vh,170px) var(--gutter);
  border-top:1px solid var(--line);
}
.about__grid{
  display:grid;
  grid-template-columns:minmax(280px,.82fr) 1.5fr;
  gap:clamp(40px,6vw,90px);
}
.about__title{ margin:20px 0 0; font-size:clamp(36px,4.4vw,64px); }
.about__copy p{
  margin:0 0 22px;
  font-size:clamp(16px,1.5vw,19px);
  line-height:1.7;
  color:var(--text-dim);
  max-width:62ch;
}
.about__copy p:last-child{ margin-bottom:0; }
.about__copy strong{ color:var(--text); font-weight:400; }
.about__copy a{
  color:var(--text);
  border-bottom:1px solid var(--line-strong);
  padding-bottom:1px;
  transition:border-color .3s ease;
}
@media (hover:hover){ .about__copy a:hover{ border-color:#fff; } }

/* ── Contact ────────────────────────────────────────────── */
.contact{
  position:relative;
  text-align:center;
  padding:clamp(110px,17vh,210px) var(--gutter);
  border-top:1px solid var(--line);
  overflow:hidden;
  isolation:isolate;
}
.contact__glow{
  position:absolute;
  left:50%; bottom:-58%;
  width:min(1100px,120vw); aspect-ratio:1;
  transform:translateX(-50%);
  z-index:-1;
  background:radial-gradient(circle, rgba(255,255,255,.10) 0%, rgba(255,255,255,.03) 34%, rgba(0,0,0,0) 66%);
  pointer-events:none;
}
.contact__title{ margin:22px 0 0; font-size:clamp(40px,7vw,104px); }
.contact__copy{
  margin:22px auto 0;
  max-width:46ch;
  font-size:16px;
  color:var(--text-dim);
}
.mailto{
  display:inline-block;
  margin-top:44px;
  font-family:var(--sans);
  font-weight:300;
  letter-spacing:-.02em;
  color:var(--text);
  font-size:clamp(21px,3vw,38px);
  border-bottom:1px solid rgba(255,255,255,.22);
  padding-bottom:6px;
  transition:border-color var(--t-hover) ease, transform var(--t-hover) var(--ease);
}
@media (hover:hover){ .mailto:hover{ border-color:rgba(255,255,255,.7); transform:translateY(-2px); } }

.socials{
  display:flex;
  flex-wrap:wrap;
  justify-content:center;
  gap:10px;
  margin-top:44px;
}
.socials a{
  padding:9px 20px;
  border-radius:999px;
  border:1px solid var(--line);
  background:rgba(255,255,255,.03);
  font-size:13px;
  color:var(--text-dim);
  transition:color var(--t-hover) ease, border-color var(--t-hover) ease, transform var(--t-hover) var(--ease), background var(--t-hover) ease;
}
@media (hover:hover){ .socials a:hover{ color:#fff; border-color:var(--line-strong); background:rgba(255,255,255,.07); transform:translateY(-2px); } }

/* ── Footer ─────────────────────────────────────────────── */
.footer{
  /* Equal side columns keep the button on the page's centre line whatever
     the caption measures */
  display:grid;
  grid-template-columns:1fr auto 1fr;
  align-items:center;
  gap:14px;
  max-width:var(--maxw);
  margin:0 auto;
  padding:28px var(--gutter) 40px;
  border-top:1px solid var(--line);
  font-size:12.5px;
  color:var(--text-faint);
}


/* Back to top: a ring that traces itself shut as the page runs out (--p,
   from main.js). On hover the arrow leaves through the top and comes back up
   from below. */
.totop{
  position:relative;
  display:grid;
  place-items:center;
  width:52px;
  height:52px;
  border-radius:50%;
  color:var(--text-dim);
  background:rgba(255,255,255,.035);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  overflow:hidden;
  transition:transform var(--t-hover) var(--ease), background var(--t-hover) ease, color var(--t-hover) ease;
}
.totop__ring{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  fill:none;
  stroke-width:1;
  transform:rotate(-90deg);   /* start the trace at twelve o'clock */
}
.totop__track{ stroke:var(--line); }
.totop__trace{
  stroke:rgba(255,255,255,.22);
  stroke-linecap:round;
  stroke-dasharray:1 2;
  stroke-dashoffset:calc(1 - var(--p, 1));
}
.totop__arrow{
  width:16px;
  height:16px;
  fill:none;
  stroke:currentColor;
  stroke-width:1.4;
  stroke-linecap:round;
  stroke-linejoin:round;
}
@media (hover:hover){
  .totop:hover{ transform:translateY(-2px); background:rgba(255,255,255,.07); color:var(--text); }
  .totop:hover .totop__arrow{ animation:totop-loop .7s var(--swift); }
}
.totop:focus-visible{ outline:none; color:var(--text); box-shadow:0 0 0 3px rgba(255,255,255,.28); }
@keyframes totop-loop{
  0%   { transform:translateY(0); opacity:1; }
  42%  { transform:translateY(-30px); opacity:0; }
  43%  { transform:translateY(30px); opacity:0; }
  100% { transform:translateY(0); opacity:1; }
}
@media (max-width:560px){
  .footer{ grid-template-columns:1fr; justify-items:center; text-align:center; }
  .totop{ order:-1; margin-bottom:6px; }
}

/* ── Reveal on scroll ───────────────────────────────────── */
.reveal{
  opacity:0;
  transform:translateY(26px);
  filter:blur(7px);
  transition:opacity .95s var(--ease), transform .95s var(--ease), filter .95s var(--ease);
  transition-delay:calc(var(--d,0) * 90ms);
}
.reveal.in{ opacity:1; transform:none; filter:none; }
/* Image-heavy blocks skip the blur: blurring a whole card's pixels on every
   frame is the most expensive thing on the page, and at card size the rise
   and fade carry the entrance on their own. */
.card.reveal, .project.reveal, .track.reveal{ filter:none; }

/* ── Responsive ─────────────────────────────────────────── */
@media (max-width:1080px){
  .work, .about__grid{ grid-template-columns:1fr; }
  .work__sticky{ position:relative; top:auto; }
}

@media (max-width:860px){
  /* Narrow frames crop hard — pull the framing up so the desk stays in shot */
  .hero__video{ object-position:center 30%; }
  .nav__links, .nav__cta{ display:none; }
  .nav__burger{ display:grid; }
  .hero__scroll{ display:none; }
  .track{
    grid-template-columns:auto 1fr auto;
    grid-template-areas:"play meta time" "wave wave wave";
    row-gap:14px;
  }
  .track__play{ grid-area:play; }
  .track__meta{ grid-area:meta; }
  .track__time{ grid-area:time; }
  .wave{ grid-area:wave; height:30px; }
}

@media (max-width:560px){
  .card, .card--lead, .card--half, .card--two-thirds, .card--third{ grid-column:span 6; }
  .card--lead .card__media{ height:clamp(200px,58vw,300px); }
  .hero__actions .btn{ flex:1 1 auto; }
}

/* ── Reduced motion ─────────────────────────────────────── */
@media (prefers-reduced-motion:reduce){
  html{ scroll-behavior:auto; }
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
  }
  .reveal{ opacity:1; transform:none; filter:none; }
  .hero__title .line__in{ transform:none; }
  .grain{ display:none; }
}

/* ── Waveform playing state ─────────────────────────────── */
.track.is-playing .wave i{
  background:rgba(255,255,255,.5);
  animation:bar 1.15s ease-in-out infinite alternate;
}

/* A live track draws its real RMS envelope, so the bars must keep the heights
   the audio gave them — the placeholder tracks animate because they have no
   shape of their own to show. Progress is carried by colour instead. */
.track--live .wave{ cursor:pointer; }
.track--live.is-playing .wave i{ animation:none; background:rgba(255,255,255,.22); }
.track--live .wave i.on{ background:var(--text); }
.track--live.is-playing .wave i.on{ background:var(--text); }
.track--live .wave:focus-visible{ outline:2px solid var(--line-strong); outline-offset:4px; border-radius:4px; }
@keyframes bar{
  from{ transform:scaleY(.42) }
  to{   transform:scaleY(1.12) }
}
.track.is-playing{ border-color:var(--line-strong); background:linear-gradient(158deg, rgba(255,255,255,.085), rgba(255,255,255,.02)); }

/* ── Media images ───────────────────────────────────────── */
.card__media img,
.project__media img{
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:center top;
  transition:transform .9s var(--ease);
}
@media (hover:hover){ .card:hover .card__media img{ transform:scale(1.04); } }
@media (hover:hover){ .project:hover .project__media img{ transform:scale(1.03); } }

/* ============================================================
   Openable cards + project viewer
   ============================================================ */

.card--openable{ cursor:pointer; }
.card--openable:focus-visible{
  outline:none;
  border-color:rgba(255,255,255,.5);
  box-shadow:0 0 0 3px rgba(255,255,255,.18);
}
.card__media{ position:relative; }

.card__view{
  position:absolute;
  left:50%; top:50%;
  transform:translate(-50%,-50%) scale(.96);
  display:inline-flex;
  align-items:center;
  gap:8px;
  padding:11px 22px;
  border-radius:999px;
  border:1px solid rgba(255,255,255,.22);
  background:rgba(10,10,10,.55);
  backdrop-filter:blur(16px);
  -webkit-backdrop-filter:blur(16px);
  font-size:13px;
  color:#fff;
  white-space:nowrap;
  opacity:0;
  transition:opacity var(--t-lift) var(--ease), transform var(--t-lift) var(--ease);
  pointer-events:none;
}
.card__view i{ font-style:normal; }
@media (hover:hover){ .card--openable:hover .card__view{ opacity:1; transform:translate(-50%,-50%) scale(1); } }
.card--openable:focus-visible .card__view{ opacity:1; transform:translate(-50%,-50%) scale(1); }

/* ── Viewer shell ───────────────────────────────────────── */
.viewer{
  position:fixed; inset:0;
  z-index:200;
  display:grid;
  place-items:center;
  padding:clamp(12px,2.5vw,32px);
  /* Scrolls on small screens; reaching its end must not scroll the page
     (see the page lock in main.js) */
  overscroll-behavior:contain;
}
.viewer[hidden]{ display:none; }

.viewer__backdrop{
  position:fixed; inset:0;
  background:rgba(4,4,4,.82);
  backdrop-filter:blur(20px);
  -webkit-backdrop-filter:blur(20px);
}
/* Opening and closing are Web Animations in main.js: the panel flies out of
   the card that was clicked and back into it. While it lands it is inert, so
   a click reaches the page (and can reopen the card it is homing on). */
.viewer.is-closing{ pointer-events:none; }

.viewer__panel{
  position:relative;
  width:100%;
  /* `max-width:1560px` alone let the panel exceed the viewport on narrow
     screens: the stage is `flex:none` there, so it sized to its content and the
     panel grew with it. Capping at 100% too keeps the whole chain in bounds. */
  max-width:min(1560px, 100%);
  /* main.js computes the card-to-viewer flight about this corner */
  transform-origin:0 0;
}

.viewer__body{
  display:grid;
  grid-template-columns:minmax(250px,340px) minmax(0,1fr);
  gap:clamp(24px,3.5vw,64px);
  align-items:center;
}

/* Title and description sit beside the work rather than above it, which is
   what lets the mockups take the full height of the window. */
/* Grid so the device toggle can sit beside the title on wide screens and drop
   below the description on narrow ones, without duplicating it in the markup. */
.viewer__side{
  display:grid;
  grid-template-columns:auto minmax(0,1fr);
  grid-template-areas:
    "title  toggle"
    "sub    sub"
    "client client"
    "tools  tools"
    "desc   desc"
    "link   link"
    "ctrl   ctrl";
  align-items:center;
  column-gap:12px;
  min-width:0;
}
.viewer__title{ grid-area:title; }
.viewer__side > .seg--icons{ grid-area:toggle; justify-self:start; }
.viewer__subtitle{ grid-area:sub; }
.viewer__client{ grid-area:client; }
.viewer__tools{ grid-area:tools; }
.viewer__desc{ grid-area:desc; }
.viewer__controls{ grid-area:ctrl; }

.viewer__controls{
  display:flex;
  align-items:baseline;
  flex-wrap:wrap;
  gap:14px;
  margin-top:clamp(24px,3.5vh,40px);
}

.viewer__desc{
  margin:12px 0 0;
  max-width:52ch;
  font-size:14px;
  line-height:1.6;
  color:var(--text-dim);
  /* Descriptions may carry their own line breaks (&#10; in data-desc); pre-line
     honours those and collapses every other run of whitespace. */
  white-space:pre-line;
}
.viewer__desc:empty{ display:none; }

/* Segmented controls: design variant (text) + device (icons) */
.viewer__foot{ display:block; }
.viewer__page{
  margin:0;
  font-size:11.5px;
  line-height:1.3;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--text-faint);
}
.viewer__page[hidden]{ display:none; }

/* "Website  Dapp  /  Dashboard" — the rule only appears when tabs precede it.
   Keyed off the tabs themselves, not the foot: a single-design project keeps the
   foot (it still carries the page label) but has no tabs to separate from. */
.viewer__page.has-tabs::before{
  content:"/";
  margin-right:14px;
  color:rgba(255,255,255,.22);
}

/* When a project has pages at all, hold the line's space permanently: the
   column is vertically centred, so a label appearing would shove everything
   above it upward. Only the text comes and goes. */
.viewer__page.is-reserved{ min-height:1.3em; }
.viewer__page.is-reserved[hidden]{ display:block; visibility:hidden; }

/* Screen arrows, flanking the device */
.arrow{
  display:grid;
  place-items:center;
  width:44px; height:60px;
  flex:none;
  padding:0;
  border:0;
  background:none;
  color:var(--text-dim);
  cursor:pointer;
  transition:color var(--t-hover) ease, transform var(--t-hover) var(--ease), opacity var(--t-hover) ease;
}
/* Keep the arrows' space reserved when they are not needed, so the mockup is
   the same size on a single-screen design as on a multi-screen one. */
.arrow[hidden]{ display:grid; visibility:hidden; pointer-events:none; }
.arrow svg{
  width:26px; height:26px;
  fill:none;
  stroke:currentColor;
  stroke-width:1.4;
  stroke-linecap:round;
  stroke-linejoin:round;
}
@media (hover:hover){ .arrow:hover:not(:disabled){ color:#fff; } }
@media (hover:hover){ .arrow--prev:hover:not(:disabled){ transform:translateX(-3px); } }
@media (hover:hover){ .arrow--next:hover:not(:disabled){ transform:translateX(3px); } }
.arrow:disabled{ opacity:.2; cursor:default; }
.arrow:focus-visible{ outline:none; color:#fff; box-shadow:0 0 0 2px rgba(255,255,255,.35); border-radius:6px; }
.viewer__foot[hidden]{ display:none; }
.seg{
  position:relative;
  display:flex;
  align-items:center;
  gap:4px;
  padding:5px;
  border-radius:999px;
  border:1px solid var(--line);
  background:rgba(255,255,255,.045);
  backdrop-filter:blur(18px);
  -webkit-backdrop-filter:blur(18px);
}
.seg[hidden]{ display:none; }
.seg__btn{
  display:grid;
  place-items:center;
  height:36px;
  padding:0;
  border:0;
  border-radius:999px;
  background:transparent;
  color:var(--text-dim);
  font-family:inherit;
  font-size:13px;
  font-weight:400;
  letter-spacing:-.005em;
  white-space:nowrap;
  cursor:pointer;
  transition:background .3s ease, color .3s ease;
}
.seg--icons .seg__btn{ width:38px; }

/* Design picker: text tabs, no container — deliberately unlike the pill above
   it, since the two sit together and read as one control otherwise. */
.tabs{ position:relative; display:flex; align-items:baseline; gap:20px; }
.tabs__btn{
  position:relative;
  padding:0 0 6px;
  border:0;
  background:none;
  font-family:inherit;
  font-size:14px;
  font-weight:400;
  letter-spacing:-.005em;
  color:var(--text-faint);
  cursor:pointer;
  transition:color .3s ease;
}
/* One underline for the row, slid and sized by main.js (placeLine) */
.tabs__line{
  position:absolute;
  left:0; bottom:0;
  width:1px; height:1px;
  background:var(--text);
  transform-origin:0 0;
  transform:scaleX(0);
  transition:transform .45s var(--swift);
  pointer-events:none;
}
@media (hover:hover){ .tabs__btn:hover{ color:var(--text-dim); } }
.tabs__btn.is-active{ color:var(--text); }
.tabs__btn:focus-visible{ outline:none; color:#fff; box-shadow:0 0 0 3px rgba(255,255,255,.28); border-radius:4px; }
.seg__btn svg{
  width:20px; height:20px;
  fill:none;
  stroke:currentColor;
  stroke-width:1.5;
  stroke-linecap:round;
  stroke-linejoin:round;
}
@media (hover:hover){ .seg__btn:hover{ color:var(--text); } }
.seg__btn.is-active{ color:var(--text); }
.seg__btn:focus-visible{ outline:none; box-shadow:0 0 0 3px rgba(255,255,255,.3); }

.u-sr{
  position:absolute;
  width:1px; height:1px;
  padding:0; margin:-1px;
  overflow:hidden;
  clip:rect(0 0 0 0);
  white-space:nowrap;
  border:0;
}
.viewer__title{
  margin:0;
  font-family:var(--serif);
  font-weight:400;
  font-size:clamp(22px,2.4vw,32px);
  line-height:1.05;
  letter-spacing:-.02em;
  background:linear-gradient(172deg,#fff 8%,#d8d8d4 100%);
  -webkit-background-clip:text; background-clip:text; color:transparent;
}
/* A subtitle names the artefact where the title names the thing it belongs to:
   "The Picks" / "Single Pick Article Page". Collapses when absent. */
.viewer__subtitle{
  margin:6px 0 0;
  font-size:15px;
  font-weight:300;
  color:var(--text-dim);
  letter-spacing:-.005em;
}
.viewer__subtitle:empty{ display:none; }

.viewer__client{
  margin:12px 0 0;
  font-size:10.5px;
  letter-spacing:.13em;
  text-transform:uppercase;
  color:var(--text-faint);
}
.viewer__client:empty{ display:none; }

/* With a client line present the tools sit under it, so they need less lead */
.viewer__client:not(:empty) + .viewer__tools{ margin-top:5px; }

.viewer__tools{
  margin:10px 0 0;
  font-size:12.5px;
  color:var(--text-faint);
  letter-spacing:.02em;
}

/* A sibling of .viewer__panel, never a child: the panel animates with a
   transform, and a transformed ancestor becomes the containing block for
   position:fixed — the button anchored to the panel for the length of the
   flight and then snapped 173px to the viewport corner. */
.viewer__close{
  position:fixed;
  top:clamp(12px,2.2vw,30px);
  right:clamp(12px,2.2vw,30px);
  z-index:5;
  display:grid;
  place-items:center;
  width:44px; height:44px;
  flex:none;
  border-radius:999px;
  border:1px solid var(--line-strong);
  background:rgba(255,255,255,.05);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  color:var(--text);
  font-size:15px;
  cursor:pointer;
  transition:background var(--t-hover) ease, transform var(--t-hover) var(--ease), color var(--t-hover) ease;
}
@media (hover:hover){ .viewer__close:hover{ background:#f2f2f0; color:#0a0a0a; transform:rotate(90deg); } }

.viewer__stage{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:clamp(8px,2vw,26px);
  flex:none;
  min-width:0;
  max-width:100%;
}
.viewer__stage[data-device="desktop"] .mock--phone,
.viewer__stage[data-device="mobile"] .mock--laptop{ display:none; }

/* ── Device mockups ─────────────────────────────────────── */
.mock{ margin:0; display:flex; flex-direction:column; min-height:0; }

.mock__frame{
  position:relative;
  background:linear-gradient(170deg,#3a3a3d,#141416 42%,#232326);
  box-shadow:0 40px 90px -40px rgba(0,0,0,.95), 0 0 0 1px rgba(255,255,255,.07) inset;
}
.mock__frame{ flex:none; }
.mock__screen{
  position:relative;
  overflow:hidden;
  background:#0a0a0b;
  box-shadow:0 0 0 1px rgba(0,0,0,.9), 0 0 0 2px rgba(255,255,255,.05);
}
.mock__scroll{
  height:100%;
  overflow-y:auto;
  overscroll-behavior:contain;
  scrollbar-width:none;
  -ms-overflow-style:none;
}
.mock__scroll::-webkit-scrollbar{ width:0; height:0; display:none; }

/* Overlay scrollbar: sits on the design, translucent, only while scrolling */
.mock__bar{
  position:absolute;
  top:0; right:0; bottom:0;
  width:7px;
  background:rgba(255,255,255,.09);
  opacity:0;
  transition:opacity .45s ease;
  pointer-events:none;
  z-index:3;
}
.mock__screen.is-scrolling .mock__bar{
  opacity:1;
  transition-duration:.12s;
  pointer-events:auto;
}
.mock__thumb{
  position:absolute;
  top:0; left:0; right:0;
  border-radius:99px;
  background:rgba(255,255,255,.42);
  transition:background .25s ease;
  cursor:grab;
}
@media (hover:hover){ .mock__thumb:hover{ background:rgba(255,255,255,.62); } }
.mock__screen.is-dragging .mock__thumb{ background:rgba(255,255,255,.78); cursor:grabbing; }
.mock__scroll img{ width:100%; height:auto; display:block; }
.mock__scroll:focus-visible{ outline:2px solid rgba(255,255,255,.5); outline-offset:-2px; }

/* Laptop */
.mock--laptop{
  /* The frame is fixed for the whole project, never per page — a ratio that
     shifted between pages made the mockup visibly resize mid-flip. Which machine
     a project was drawn on comes from `data-laptop` on the card; add a line here
     to teach the viewer a new one. */
  --ar:1.5397;                  /* MacBook Pro 14"  1512x982 (default) */
  flex:0 1 auto;
  min-width:0;
  max-width:100%;
  width:min(1240px, calc((100vh - 120px) * var(--ar)));
}
.mock--laptop .mock__frame{
  border-radius:14px 14px 5px 5px;
  padding:11px 11px 15px;
}
.mock--laptop .mock__screen{ border-radius:5px; aspect-ratio:var(--ar); }

/* MacBook Air 13.6" (1280x832). Beyond the ratio, the Air reads as a different
   machine in three ways worth drawing: it is a third thinner in the body
   (11.3mm against the Pro's 15.5mm), its display bezels are a touch wider, and
   Midnight is a blue-black where Space Black is neutral. */
.viewer__stage[data-laptop="air"] .mock--laptop{ --ar:1.5385; }
.viewer__stage[data-laptop="air"] .mock--laptop .mock__frame{
  border-radius:11px 11px 4px 4px;
  padding:13px 13px 17px;
  background:linear-gradient(170deg,#34363f,#101218 42%,#1c1e26);
}
.viewer__stage[data-laptop="air"] .mock--laptop .mock__screen{ border-radius:4px; }
.viewer__stage[data-laptop="air"] .mock--laptop .mock__base{
  height:9px;
  border-radius:0 0 8px 8px;
  background:linear-gradient(180deg,#25272f,#0e1014 62%,#08090c);
  box-shadow:0 14px 26px -16px rgba(0,0,0,.9);
}
.viewer__stage[data-laptop="air"] .mock--laptop .mock__base span{ width:13%; height:4px; }

/* ── Plates: work that is not a website ─────────────────── */
/* 3D renders, print, artwork. Same stage and the same arrows, but the laptop's
   frame, base and scrollbar are stripped so the image stands on its own. The
   render fills the screen box exactly, so there is nothing to scroll. */
.viewer__stage[data-kind="plate"] .mock--laptop{
  /* A plate takes the work's own ratio. `data-ar` on the card sets --plate-ar on
     the stage and it inherits down; 16:9 is the default. One ratio per project,
     never per screen — a frame that resizes between arrow presses looks broken. */
  --ar:var(--plate-ar, 1.7778);
  width:min(1400px, calc((100vh - 96px) * var(--ar)));
}
.viewer__stage[data-kind="plate"] .mock__frame{
  padding:0;
  border-radius:10px;
  background:none;
  box-shadow:0 40px 90px -40px rgba(0,0,0,.95);
}
.viewer__stage[data-kind="plate"] .mock__screen{
  border-radius:10px;
  box-shadow:0 0 0 1px rgba(255,255,255,.07);
}
.viewer__stage[data-kind="plate"] .mock__base,
.viewer__stage[data-kind="plate"] .mock__bar{ display:none; }

/* Component cards are deliberately small — the section is meant to fill up, and
   a wall of large cards is scroll fatigue. At that width a title and subtitle
   cannot share a baseline, so the caption stacks instead of wrapping raggedly. */
#snippets .card figcaption{
  display:block;
  padding:13px 16px 15px;
}
#snippets .card figcaption span{
  display:block;
  line-height:1.3;
}
#snippets .card figcaption em{
  display:block;
  margin-top:2px;
  line-height:1.3;
}

/* ── Code sketches ──────────────────────────────────────── */
/* A pen replaces the devices on the stage: a browser-less frame holding either
   the running sketch or the source of the same file. The white ground is
   deliberate — it is the surface these were designed against. */
.viewer__stage[data-kind="pen"] .mock--laptop,
.viewer__stage[data-kind="pen"] .mock--phone{ display:none; }
.viewer__stage:not([data-kind="pen"]) .mock--pen{ display:none; }

.mock--pen{
  flex:0 1 auto;
  min-width:0;
  max-width:100%;
  width:min(1240px, 100%);
  /* Gutter for the resize handle, so it sits outside the frame without ever
     falling off the edge of a narrow window. */
  padding-right:18px;
  box-sizing:border-box;
}
/* No fixed ratio: this card is 186px tall at 1240 wide and 339px at 400, so any
   aspect-ratio is wrong at some width. The frame is resizable instead — dragging
   it narrow is how you read a responsive component, and it makes the point of
   the section better than a still ever could. */
.pen__frame{
  position:relative;
  /* Height is driven by the sketch's own content (syncPenHeight in main.js);
     --pen-h is only the placeholder before the iframe reports back. Width is
     dragged by .pen__grip — the native `resize` grip can only sit inside the
     element, where it was invisible against the sketch. */
  height:min(var(--pen-h, 240px), 62vh);
  /* 375px — the iPhone 7/8 viewport, and the narrowest device this work targets.
     Every pen is clean well before it: two of them (Single Pick Article Page,
     Picks of The Day) overflow by 18px at 280 and 8px at 300, and come right at
     320, so the floor sits clear of any breakage. */
  min-width:375px;
  max-width:100%;
  max-height:calc(100vh - 150px);
}

/* The clipping box. The frame itself must not clip, or the grip beside it would
   be cut off; this inner layer carries the rounding and the white ground. */
.pen__view{
  position:absolute;
  inset:0;
  overflow:hidden;
  border-radius:12px;
  background:#fff;
  box-shadow:0 40px 90px -40px rgba(0,0,0,.95), 0 0 0 1px rgba(255,255,255,.08);
}

/* Resize handle: a hairline down the frame's right edge with a knob at its
   middle. Full height so the axis is unmistakable, slim so it never competes
   with the work. .mock--pen reserves the gutter so it sits outside at any width. */
.pen__grip{
  position:absolute;
  top:0;
  bottom:0;
  right:-18px;
  width:18px;
  padding:0;
  border:0;
  background:none;
  cursor:ew-resize;
  touch-action:none;
}
/* the line */
.pen__grip::before{
  content:"";
  position:absolute;
  top:0; bottom:0; left:50%;
  width:2px;
  margin-left:-1px;
  border-radius:2px;
  background:rgba(255,255,255,.16);
  transition:background .3s ease;
}
/* the knob — a real element, so its two hairlines centre by layout rather than
   by background offsets, which were half a pixel off against the 1px border */
.pen__knob{
  position:absolute;
  top:50%; left:50%;
  transform:translate(-50%,-50%);
  width:12px; height:46px;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:3px;
  border-radius:999px;
  border:1px solid rgba(255,255,255,.26);
  background:rgba(16,16,18,.92);
  box-sizing:border-box;
  transition:border-color .3s ease, background-color .3s ease, transform .3s var(--ease);
}
.pen__knob::before,
.pen__knob::after{
  content:"";
  width:1px;
  height:14px;
  border-radius:1px;
  background:rgba(255,255,255,.5);
  transition:background .3s ease;
}
@media (hover:hover){ .pen__grip:hover::before{ background:rgba(255,255,255,.4); } }
.pen__grip:focus-visible::before{ background:rgba(255,255,255,.4); }
@media (hover:hover){ .pen__grip:hover .pen__knob{
  border-color:rgba(255,255,255,.55);
  background-color:rgba(28,28,32,.96);
  transform:translate(-50%,-50%) scaleY(1.12);
} }
.pen__grip:focus-visible .pen__knob{
  border-color:rgba(255,255,255,.55);
  background-color:rgba(28,28,32,.96);
  transform:translate(-50%,-50%) scaleY(1.12);
}
@media (hover:hover){ .pen__grip:hover .pen__knob::before,
.pen__grip:hover .pen__knob::after{ background:#fff; } }
.pen__grip:focus-visible .pen__knob::before,
.pen__grip:focus-visible .pen__knob::after{ background:#fff; }
.pen__grip:focus-visible{ outline:none; }
.pen__grip:focus-visible .pen__knob{ box-shadow:0 0 0 3px rgba(255,255,255,.2); }

/* ── Code view ──────────────────────────────────────────
   The width rail is meaningless against source, and the source wants height as
   much as width — so the native two-axis grip is the right tool here, and the
   frame clips again to give it something to sit in. */
.mock--pen.is-code .pen__grip,
.mock--pen.is-code .pen__hint{ display:none; }
.mock--pen.is-code .pen__frame{
  height:min(62vh, calc(100vh - 150px));
  resize:both;
  overflow:auto;
}
/* The browser's grip is near-invisible on this ground; mark the corner */
.mock--pen.is-code .pen__frame::after{
  content:"";
  position:absolute;
  right:4px; bottom:4px;
  width:11px; height:11px;
  border-right:2px solid rgba(255,255,255,.3);
  border-bottom:2px solid rgba(255,255,255,.3);
  border-bottom-right-radius:3px;
  pointer-events:none;
  z-index:2;
}

/* Below the phone breakpoint the frame is already at the width it will ever
   have, so a width handle has nothing to say. */
@media (max-width:560px){
  .mock--pen{ padding-right:0; }
  .mock--pen .pen__grip,
  .mock--pen .pen__hint{ display:none; }
}

.mock--pen.is-resizing{ cursor:ew-resize; }
.mock--pen.is-resizing .pen__view{ pointer-events:none; }
.mock--pen.is-resizing .pen__grip::before{ background:rgba(255,255,255,.5); }
.mock--pen.is-resizing .pen__knob{ border-color:rgba(255,255,255,.6); }

.pen__hint{
  margin:10px 2px 0;
  font-size:11.5px;
  letter-spacing:.02em;
  color:var(--text-faint);
}
.pen__iframe{
  width:100%;
  height:100%;
  border:0;
  display:block;
  background:#fff;
}
.pen__iframe[hidden]{ display:none; }

.pen__code{
  position:absolute;
  inset:0;
  margin:0;
  overflow:auto;
  padding:20px 22px 20px 0;
  overscroll-behavior:contain;
  background:#0c0c0e;
  color:#cfcfca;
  font:12.5px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  tab-size:2;
  scrollbar-width:thin;
  scrollbar-color:rgba(255,255,255,.22) transparent;
}
.pen__code[hidden]{ display:none; }
.pen__code::-webkit-scrollbar{ width:6px; height:6px; }
.pen__code::-webkit-scrollbar-thumb{ background:rgba(255,255,255,.2); border-radius:99px; }
.pen__code:focus-visible{ outline:2px solid rgba(255,255,255,.4); outline-offset:-2px; }

/* Two columns, one row per line — the only way the numbers stay aligned with
   wrapped-off long lines and with a horizontal scroll. */
.pen__code code{
  display:grid;
  grid-template-columns:max-content 1fr;
  column-gap:16px;
  min-width:max-content;
}
.pen__code .ln{
  position:sticky;
  left:0;
  z-index:1;
  padding:0 10px 0 22px;
  text-align:right;
  color:#45453f;
  background:#0c0c0e;
  user-select:none;
  -webkit-user-select:none;
}
.pen__code .cl{ white-space:pre; }

/* HTML cool, CSS warm — the style block should not read as more markup */
.pen__code i{ font-style:normal; }
.pen__code .c  { color:#5c5c58; }   /* comments, both languages */
.pen__code .s  { color:#c3b58a; }   /* strings, both languages  */
.pen__code .t  { color:#8fb8c9; }   /* html tag                 */
.pen__code .a  { color:#9c9c96; }   /* html attribute           */
.pen__code .sel{ color:#d2a0a0; }   /* css selector             */
.pen__code .at { color:#d2a0a0; }   /* css at-rule              */
.pen__code .p  { color:#86b3c4; }   /* css property             */
.pen__code .n  { color:#a8a2cf; }   /* css number, unit, colour */

/* Outbound link under the description — CodePen, a repo, a live site */
.viewer__link{
  grid-area:link;
  justify-self:start;
  margin-top:14px;
  font-size:12.5px;
  color:var(--text-dim);
  border-bottom:1px solid rgba(255,255,255,.22);
  padding-bottom:2px;
  transition:color .3s ease, border-color .3s ease;
}
.viewer__link[hidden]{ display:none; }
@media (hover:hover){ .viewer__link:hover{ color:var(--text); border-bottom-color:rgba(255,255,255,.5); } }
.mock__base{
  position:relative;
  flex:none;
  width:104%;
  margin-left:-2%;
  height:13px;
  border-radius:0 0 11px 11px;
  background:linear-gradient(180deg,#2b2b2e,#121214 62%,#0b0b0c);
  box-shadow:0 18px 34px -18px rgba(0,0,0,.9);
}
.mock__base span{
  position:absolute;
  left:50%; top:0;
  transform:translateX(-50%);
  width:15%; height:5px;
  border-radius:0 0 6px 6px;
  background:rgba(0,0,0,.55);
}

/* Phone */
.mock--phone{
  flex:0 0 auto;
  max-width:100%;
  width:clamp(220px, calc((100vh - 130px) / 2.164), 400px);
}
.mock--phone .mock__frame{
  border-radius:24px;
  padding:12px;
}
.mock--phone .mock__screen{ border-radius:12px; aspect-ratio:390/844; }

/* ── Loading state ──────────────────────────────────────── */
.mock__scroll.is-loading{
  background:
    linear-gradient(100deg, rgba(255,255,255,0) 30%, rgba(255,255,255,.05) 50%, rgba(255,255,255,0) 70%)
    #0d0d0e;
  background-size:220% 100%;
  animation:shimmer 1.5s linear infinite;
}
@keyframes shimmer{ to{ background-position:-220% 0 } }

/* Paging: the outgoing screen, laid over the scroll box while it fades
   (handOver in main.js), and the held screen dimmed if the next is slow. */
.mock__ghost{
  position:absolute;
  display:block;
  max-width:none;
  height:auto;
  z-index:2;                      /* under the overlay scrollbar (3) */
  pointer-events:none;
}
.mock__scroll img{ transition:opacity .3s ease; }
.mock__scroll.is-swapping img{ opacity:.45; }

/* ── Responsive ─────────────────────────────────────────── */
/* Small or short screens: pin the dialog to the top and let it scroll, so
   nothing is vertically re-centred (and jumps) as screens change height. */
@media (max-width:900px), (max-height:720px){
  .viewer{ align-items:start; overflow-y:auto; }
}

/* Short screens scroll anyway, so let the device claim more of the height */
@media (max-height:720px){
  .mock--laptop{ width:min(1240px, calc((100vh - 110px) * var(--ar))); }
  .viewer__stage[data-kind="plate"] .mock--laptop{ width:min(1400px, calc((100vh - 86px) * var(--ar))); }
  .mock--phone{ width:clamp(220px, calc((100vh - 120px) / 2.164), 400px); }
}

@media (max-width:900px){
  .viewer__body{
    grid-template-columns:1fr;
    gap:22px;
    align-items:start;
  }
  /* The toggle stays on the title line here too — dropping it to its own
     centred row cost a whole line of height, which is scarce in one column. */
  .viewer__side{ padding-right:56px; }        /* clear of the close button */
  .viewer__controls{ margin-top:18px; gap:12px; }
  .mock{ flex:none; }
  .mock--laptop{ flex:none; width:100%; max-width:calc(100% - 84px); }
  /* `flex:none` stops the shrink that kept the plate in bounds on wide screens,
     and its width is set from 100vh, which a 390px column cannot hold — it ran
     1330px wide off the left edge. Percentages are measured against the column,
     so they cannot run away the way a vh-derived width can. */
  .viewer__stage[data-kind="plate"] .mock--laptop{ width:100%; max-width:calc(100% - 84px); }
  .mock--phone{ width:min(320px,62vw); }
  .viewer__stage{ gap:6px; }
  .arrow{ width:34px; height:52px; }
  .arrow svg{ width:22px; height:22px; }
}

@media (max-width:560px){
  /* Phone takes whatever the arrows leave rather than a fixed 260px cap —
     a phone mockup on a phone should be close to life size. */
  .mock--laptop{ max-width:calc(100% - 60px); }
  .viewer__stage[data-kind="plate"] .mock--laptop{ max-width:calc(100% - 60px); }
  .mock--phone{ width:min(340px, calc(100% - 60px)); }
  .viewer__stage{ gap:2px; }
  .arrow{ width:28px; }
  .arrow svg{ width:20px; height:20px; }
}

@media (prefers-reduced-motion:reduce){
  .mock__scroll.is-loading{ animation:none; }
}

.viewer__close:focus-visible{
  outline:none;
  background:rgba(255,255,255,.14);
  box-shadow:0 0 0 3px rgba(255,255,255,.3);
}

.viewer__panel::-webkit-scrollbar{ display:none; }

/* ============================================================
   Full-screen image zoom
   ============================================================ */
.mock__scroll img{ cursor:zoom-in; }

.zoom{
  position:fixed;
  inset:0;
  z-index:300;
  display:grid;
  place-items:center;
  overflow:hidden;
  cursor:zoom-out;
  touch-action:none;
}
.zoom[hidden]{ display:none; }
.zoom.is-closing{ pointer-events:none; }
/* Its own layer so it can fade while the picture flies at full opacity */
.zoom__backdrop{
  position:absolute;
  inset:0;
  background:rgba(4,4,4,.95);
  backdrop-filter:blur(12px);
  -webkit-backdrop-filter:blur(12px);
}

.zoom.is-pannable .zoom__img{ cursor:grab; }
.zoom.is-pannable:active .zoom__img{ cursor:grabbing; }
.zoom__img{
  position:relative;              /* paint above the absolute backdrop */
  display:block;
  transform-origin:center center;
  will-change:transform;
  user-select:none;
  -webkit-user-drag:none;
  border-radius:2px;
}

.zoom__hint{
  position:absolute;
  left:50%;
  bottom:24px;
  transform:translateX(-50%);
  padding:8px 18px;
  border-radius:999px;
  border:1px solid var(--line);
  background:rgba(10,10,10,.6);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
  font-size:12px;
  letter-spacing:.02em;
  color:var(--text-dim);
  pointer-events:none;
  transition:opacity .5s ease;
}
.zoom.is-touched .zoom__hint{ opacity:0; }

/* ── Hover is for pointers ──────────────────────────────────
   Every :hover rule above is wrapped in @media (hover:hover). On touch a tap
   leaves :hover stuck on, so a card stayed lifted after its viewer closed and
   the closing flight landed 5px off it.

   ── Press ─────────────────────────────────────────────────
   A press answers on the way down, faster than any hover, and the release
   rides the element's own slower transition back. Last in the file so it
   wins over the hover states it presses into. */
.btn:active,
.socials a:active{ transform:scale(.97); transition-duration:var(--t-press); }
.card--openable:active{ transform:scale(.985); transition-duration:var(--t-press); }
.project:not(.project--soon):active{ transform:scale(.985); transition-duration:var(--t-press); }
.track__play:active{ transform:scale(.92); transition-duration:var(--t-press); }
.totop:active{ transform:scale(.92); transition-duration:var(--t-press); }
.arrow:active:not(:disabled){ transform:scale(.9); transition-duration:var(--t-press); }
.viewer__close:active{ transform:scale(.92); transition-duration:var(--t-press); }
@media (hover:hover){
  .viewer__close:hover:active{ transform:rotate(90deg) scale(.92); }
}
