/* joshlejeune.com - portfolio
 *
 * A single dark look, deliberately. There is no light variant: the portrait is
 * a studio headshot on a near-black seamless ground, and a light theme would
 * put a black rectangle where the face is. `color-scheme: dark` is set on html.
 *
 * The palette is almost entirely neutral on purpose. Colour on this page comes
 * from Josh's own product marks, which keep their real brand hexes. That is the
 * argument the page is making, so nothing else competes with it.
 *
 * Motion follows the house rule found across every other site in this estate:
 * a reduced-motion branch composes a deliberate resting frame, not merely
 * `animation: none`. See the block at the foot of this file.
 */

@font-face{font-family:"Inter";src:url("fonts/inter-latin-var.woff2") format("woff2");font-style:normal;font-weight:100 900;font-display:swap}
@font-face{font-family:"Space Grotesk";src:url("fonts/space-grotesk-latin-var.woff2") format("woff2");font-style:normal;font-weight:300 700;font-display:swap}

:root{
  --bg:#0a0c0f;
  --bg-2:#11151b;
  --surface:#141920;
  --ink:#f4f6f8;
  --ink-2:#c3ccd6;
  --muted:#8d97a3;
  --rule:#1f262f;
  --rule-soft:#171d24;

  /* Outcome accents, measured against --bg for AA at the sizes used. */
  --k-complete:#6fbc8d;
  --k-abandoned:#e08e72;
  --k-item:#d7ad55;
  --k-lost:#9aa0a8;
  --k-party:#79aada;
  --k-side:#a996d6;
  --k-open:#e5a44f;

  --focus:#8fb8e4;

  /* The intensity bus. assets/lume.js publishes one number per frame here and
     CSS calc() spreads it across the ambient wash. Declared with a resting
     value so the page looks composed before any script runs, and stays that
     way if none ever does. */
  --lume:0.55;

  --jl-foot-ink:#8d97a3;
  --jl-foot-rule:#1f262f;
  --jl-foot-mark:#8d97a3;
  --jl-foot-mark-opacity:.72;
}

*{box-sizing:border-box}
html{scroll-behavior:smooth;color-scheme:dark}
body{
  margin:0;
  background:var(--bg);
  color:var(--ink-2);
  font:400 1.02rem/1.7 Inter,system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

h1,h2,h3,.kind-label,.pill,.chip,.num-figure,.eyebrow,.card-name{
  font-family:"Space Grotesk",Inter,system-ui,sans-serif;
}

a{color:inherit}
:focus-visible{outline:2px solid var(--focus);outline-offset:3px;border-radius:3px}

.wrap{width:100%;max-width:64rem;margin:0 auto;padding:0 24px}

.skip{
  position:absolute;left:-9999px;
  background:var(--surface);color:var(--ink);
  padding:12px 18px;border-radius:8px;border:1px solid var(--rule);
  font:600 .8rem/1 Inter,sans-serif;z-index:50;
}
.skip:focus{left:24px;top:16px}

/* ---------- Ambient field ----------
   The concentric rings are the same element every other site in this estate
   ships: three empty <i>, behind everything, no motion at all. Atmosphere. */

.rings{
  position:fixed;z-index:-2;
  width:min(160vw,1500px);aspect-ratio:1;
  top:32%;left:50%;transform:translate(-50%,-50%);
  pointer-events:none;
}
.rings i{position:absolute;inset:0;margin:auto;border-radius:50%;border:1px solid rgba(143,184,228,.055)}
.rings i:nth-child(1){width:46%;height:46%}
.rings i:nth-child(2){width:64%;height:64%;border-color:rgba(143,184,228,.042)}
.rings i:nth-child(3){width:84%;height:84%;border-color:rgba(143,184,228,.03)}

/* The one thing --lume actually drives: a slow wash behind the hero. */
.glow{
  position:fixed;z-index:-1;pointer-events:none;
  top:-20vh;left:50%;transform:translateX(-50%);
  width:min(120vw,1100px);height:80vh;
  background:radial-gradient(
    50% 50% at 50% 50%,
    rgba(143,184,228,calc(.08 * var(--lume))) 0%,
    rgba(143,184,228,calc(.035 * var(--lume))) 45%,
    transparent 72%);
}

/* ---------- Hero ---------- */

.hero{position:relative;padding:clamp(56px,11vh,120px) 0 clamp(44px,8vh,96px)}
.hero-grid{
  display:grid;
  grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);
  gap:clamp(28px,5vw,64px);
  align-items:center;
}
.hero-grid.is-solo{grid-template-columns:minmax(0,1fr)}

.eyebrow{
  font:600 .68rem/1.6 "Space Grotesk",Inter,sans-serif;
  letter-spacing:.24em;text-transform:uppercase;
  color:var(--muted);margin:0 0 16px;
}

/* The name rises a letter at a time, the house rise pattern. Each span carries
   its own --i so the stagger is data, not a hand-written delay list. */
.hero h1{
  color:var(--ink);
  font-size:clamp(2.4rem,7.5vw,4.2rem);
  font-weight:700;letter-spacing:-.03em;line-height:1.02;
  margin:0 0 6px;
}
.hero h1 .ch{
  display:inline-block;
  animation:rise .62s cubic-bezier(.2,.7,.3,1) backwards;
  animation-delay:calc(var(--i) * 42ms);
}
.hero h1 .sp{display:inline-block;width:.28em}
@keyframes rise{from{opacity:0;transform:translateY(.42em)}to{opacity:1;transform:none}}

/* A hairline that draws itself under the name. pathLength="1" makes the dash
   maths unit-free: no getTotalLength(), no JS. */
.hero-rule{display:block;width:min(22rem,100%);height:2px;margin:20px 0 24px;overflow:visible}
.hero-rule path{
  fill:none;stroke:var(--focus);stroke-width:2;
  stroke-dasharray:1;stroke-dashoffset:1;
  animation:draw 1.1s .5s cubic-bezier(.35,.8,.4,1) forwards;
  opacity:.5;
}
@keyframes draw{to{stroke-dashoffset:0}}

.hero-lede{max-width:34rem;margin:0 0 18px;font-size:1.06rem;color:var(--ink-2)}
.hero-lede:last-child{margin-bottom:0}
.hero-lede strong{color:var(--ink);font-weight:600}

/* ---------- Portrait ----------
   The subject is isolated with real alpha by tools/cutout.swift, so no rectangle
   of photographic ground is sitting on the page.

   **No outline, and no drop-shadow of any kind.** Both were tried and both were
   wrong: a blurred one is a halo that pools where the shoulders fade, and a
   zero-blur one is a hard sticker edge that makes the cut more obvious, not
   less. The brief is that he fades into the background, so the only treatment
   is a mask.

   One radial does all of it. It holds the face at full opacity and falls away
   in every direction, so the crown, the shoulders and the bottom crop all
   dissolve into --bg with no boundary anywhere. A single gradient rather than
   two composited ones, because mask-composite still needs a -webkit- prefixed
   spelling with different keywords and this needs no such fragility.

   The AVIF and WebP sources carry the alpha. The JPEG fallback is the original
   photograph, whose studio ground is already near-black, so a browser too old
   for either format still gets something that sits correctly on this page. */
.portrait{
  position:relative;justify-self:end;
  width:100%;max-width:24rem;
  animation:portrait-in 1s .15s cubic-bezier(.2,.7,.3,1) backwards;
}
.portrait img{
  display:block;width:100%;height:auto;
  filter:grayscale(1) contrast(.97) brightness(1.02);
  -webkit-mask-image:radial-gradient(66% 60% at 50% 34%,#000 42%,rgba(0,0,0,.6) 72%,transparent 100%);
  mask-image:radial-gradient(66% 60% at 50% 34%,#000 42%,rgba(0,0,0,.6) 72%,transparent 100%);
}
@keyframes portrait-in{from{opacity:0;transform:translateY(14px) scale(.985)}to{opacity:1;transform:none}}

/* ---------- Sections ---------- */

.section{padding:clamp(52px,9vh,104px) 0;border-top:1px solid var(--rule-soft);position:relative}
.section-head h2{
  color:var(--ink);
  font-size:clamp(1.4rem,3.6vw,2rem);
  font-weight:700;letter-spacing:-.02em;margin:0;
}
.section-intro{max-width:42rem;margin:12px 0 34px;color:var(--muted)}

/* ---------- The Work ---------- */

.cards{
  list-style:none;margin:0;padding:0;
  display:grid;gap:16px;
  grid-template-columns:repeat(auto-fill,minmax(16.5rem,1fr));
}
.card{
  position:relative;
  background:linear-gradient(180deg,var(--surface),var(--bg-2));
  border:1px solid var(--rule);border-radius:14px;
  padding:20px 20px 17px;
  transition:border-color .25s ease,transform .25s ease;
}
.card:hover{border-color:#2b3540;transform:translateY(-2px)}

.card-mark{
  display:flex;align-items:center;justify-content:center;
  width:46px;height:46px;margin-bottom:14px;
  border-radius:11px;
  background:rgba(255,255,255,.028);
  border:1px solid var(--rule);
}
.card-mark .mark-svg{width:28px;height:28px;display:block;transform-box:fill-box;transform-origin:center;will-change:transform,opacity}
.card-mark[data-mark="chronicles"] .mark-svg,
.card-mark[data-mark="peptide"] .mark-svg{width:30px;height:30px}
.card-mark.is-letter{font:700 1rem/1 "Space Grotesk",Inter,sans-serif;color:var(--muted)}

/* Each mark draws itself once, when its card first enters view. Scale and fade
   only: these are ten different drawings, and a per-mark choreography would be
   ten maintenance problems. */
.js-mark .mark-svg{opacity:0;transform:scale(.72) rotate(-14deg)}
.js-mark.is-in .mark-svg{
  opacity:1;transform:none;
  transition:opacity .5s ease,transform .62s cubic-bezier(.2,.8,.3,1);
}

.card-name{display:block;color:var(--ink);font-size:1.02rem;font-weight:700;letter-spacing:-.01em;margin:0 0 2px}
.card-expand{display:block;font:500 .68rem/1.4 Inter,sans-serif;color:var(--muted);margin-bottom:10px}
.card p{margin:0;font-size:.92rem;color:var(--ink-2)}
.card-foot{
  display:flex;flex-wrap:wrap;gap:9px;align-items:center;
  margin-top:15px;padding-top:12px;border-top:1px solid var(--rule-soft);
}
.pill{
  font:700 .57rem/1 "Space Grotesk",Inter,sans-serif;
  letter-spacing:.14em;text-transform:uppercase;
  padding:5px 8px;border-radius:5px;
  background:rgba(255,255,255,.03);border:1px solid currentColor;
  color:var(--muted);
}
.pill.is-live{color:var(--k-complete)}
.pill.is-private{color:var(--k-item)}
.pill.is-dead{color:var(--k-lost)}
.card-stack{font:400 .72rem/1.4 Inter,sans-serif;color:var(--muted)}

/* ---------- Filters ---------- */

.filters{
  position:sticky;top:0;z-index:5;
  margin:0 0 30px;padding:14px 0;
  background:linear-gradient(var(--bg) 72%,transparent);
  backdrop-filter:blur(6px);
}
.filter-row{display:flex;flex-wrap:wrap;gap:7px;align-items:center}
.filter-row + .filter-row{margin-top:9px}
.filter-legend{
  font:600 .62rem/1 "Space Grotesk",Inter,sans-serif;
  letter-spacing:.2em;text-transform:uppercase;color:var(--muted);
  margin-right:4px;min-width:4.2rem;
}
.chip{
  appearance:none;cursor:pointer;
  font:600 .72rem/1 "Space Grotesk",Inter,sans-serif;letter-spacing:.04em;
  color:var(--ink-2);background:var(--surface);
  border:1px solid var(--rule);border-radius:999px;
  padding:8px 13px;min-height:32px;
  transition:background .15s,border-color .15s,color .15s;
}
.chip:hover{border-color:#333e4a;color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--bg)}
.chip-count{opacity:.55;margin-left:5px;font-variant-numeric:tabular-nums}

/* ---------- The Log ---------- */

.log{list-style:none;margin:0;padding:0;position:relative}

.entry{position:relative;padding:0 0 6px 28px;margin:0 0 26px}
@media(min-width:760px){.entry{padding-left:calc(8rem + 30px)}}
.entry[hidden]{display:none}

/* The spine is drawn per entry rather than as one long rule: each segment
   scales up from its top as its entry reveals, so the line appears to draw
   itself down the page as you scroll, with no scroll listener anywhere. */
.entry::after{
  content:"";position:absolute;
  left:0;top:14px;bottom:-26px;width:1px;
  background:var(--rule);transform-origin:top;
}
@media(min-width:760px){.entry::after{left:8rem}}
.entry:last-child::after{display:none}
.js-reveal::after{transform:scaleY(0)}
.js-reveal.is-in::after{transform:scaleY(1);transition:transform .7s .1s cubic-bezier(.4,.7,.3,1)}

.entry::before{
  content:"";position:absolute;
  left:-4px;top:9px;width:9px;height:9px;border-radius:50%;
  background:var(--bg);border:2px solid var(--dot,var(--muted));z-index:1;
}
@media(min-width:760px){.entry::before{left:calc(8rem - 4px)}}

.entry-when{
  display:block;
  font:600 .66rem/1.5 "Space Grotesk",Inter,sans-serif;
  letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
  margin-bottom:5px;
}
@media(min-width:760px){.entry-when{position:absolute;left:0;top:3px;width:7rem;text-align:right;margin:0}}

.kind-label{
  display:inline-flex;align-items:center;gap:6px;
  font:700 .61rem/1 "Space Grotesk",Inter,sans-serif;
  letter-spacing:.16em;text-transform:uppercase;
  color:var(--dot,var(--muted));margin-bottom:5px;
}
.kind-glyph{font-size:.8em}

.entry h3{color:var(--ink);font-size:1.05rem;font-weight:700;letter-spacing:-.01em;margin:0 0 3px}
.entry-line{margin:0;color:var(--ink-2)}

.entry-toggle{
  appearance:none;background:none;border:0;padding:7px 0 0;cursor:pointer;
  font:600 .7rem/1 Inter,sans-serif;letter-spacing:.03em;color:var(--muted);
  display:inline-flex;align-items:center;gap:5px;min-height:24px;
}
.entry-toggle:hover{color:var(--ink)}
.entry-toggle .caret{transition:transform .18s}
.entry-toggle[aria-expanded="true"] .caret{transform:rotate(90deg)}

.entry-detail{
  margin:11px 0 0;padding:13px 15px;
  background:rgba(255,255,255,.022);
  border-left:2px solid var(--dot,var(--rule));
  border-radius:0 8px 8px 0;
  font-size:.95rem;color:var(--ink-2);
}
.entry-detail[hidden]{display:none}

/* Entries render visible; JS opts them into the animation only when motion is
   welcome and IntersectionObserver exists, so a missing feature can never
   leave content invisible. */
.js-reveal{opacity:0;transform:translateY(10px)}
.js-reveal.is-in{opacity:1;transform:none;transition:opacity .5s ease,transform .5s ease}

.log-empty{margin:0;padding:24px 0;color:var(--muted)}
.log-empty[hidden]{display:none}

/* ---------- Numbers ---------- */

.numbers{
  list-style:none;margin:0;padding:0;
  display:grid;gap:16px;
  grid-template-columns:repeat(auto-fit,minmax(8.5rem,1fr));
}
.number{
  background:linear-gradient(180deg,var(--surface),var(--bg-2));
  border:1px solid var(--rule);border-radius:12px;
  padding:18px 18px 16px;
}
.num-figure{
  display:block;color:var(--ink);
  font-size:1.9rem;font-weight:700;letter-spacing:-.03em;line-height:1.1;
  font-variant-numeric:tabular-nums;
}
.num-label{display:block;margin-top:5px;font:500 .7rem/1.4 Inter,sans-serif;color:var(--muted)}

/* ---------- Motion toggle ----------
   A visible switch alongside the OS setting, the same idea as the peptide 404.
   An animated page should be stoppable by someone who has not set a system
   preference. Rendered hidden and revealed by script, so it is never a dead
   control. */
.motion-toggle{
  position:fixed;right:16px;bottom:16px;z-index:20;
  appearance:none;cursor:pointer;
  display:inline-flex;align-items:center;gap:7px;
  min-height:36px;padding:9px 13px;
  font:600 .66rem/1 "Space Grotesk",Inter,sans-serif;letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted);background:rgba(20,25,32,.9);
  border:1px solid var(--rule);border-radius:999px;
  backdrop-filter:blur(8px);
  transition:color .18s,border-color .18s;
}
.motion-toggle:hover{color:var(--ink);border-color:#333e4a}
.motion-toggle[hidden]{display:none}

/* One line freezes the whole page. */
html.motion-paused *,
html.motion-paused *::before,
html.motion-paused *::after{animation-play-state:paused!important}

/* ---------- Footer ---------- */

.jl-foot{
  display:flex;align-items:center;justify-content:center;gap:12px;
  width:100%;padding:32px 24px clamp(30px,5vh,52px);
  border-top:1px solid var(--rule-soft);
  margin-top:clamp(44px,7vh,80px);
}
.jl-foot span.jl-line{
  color:var(--jl-foot-ink);
  font:500 .63rem/1.7 Inter,system-ui,-apple-system,sans-serif;
  letter-spacing:.24em;text-transform:uppercase;
}
.jl-foot i{width:1px;height:17px;background:var(--jl-foot-rule)}
.jl-foot .jl-mark{
  /* Brand Kit capsule at its own 1.981:1. Height is the one knob; width follows
     so the JL is never stretched. Not a link here: this page is where the mark
     points from every other site. */
  --jw-mark-h:1.2em;
  display:block;height:var(--jw-mark-h);width:calc(var(--jw-mark-h) * 1.9811);
  background:currentColor;
  -webkit-mask:url('brand/joshworks-capsule.svg') center/contain no-repeat;
  mask:url('brand/joshworks-capsule.svg') center/contain no-repeat;
  color:var(--jl-foot-mark);opacity:var(--jl-foot-mark-opacity);
}

/* ---------- Small screens ---------- */

@media(max-width:860px){
  .hero-grid{grid-template-columns:minmax(0,1fr);gap:32px}
  .portrait{justify-self:start;max-width:17rem;order:-1}
}
@media(max-width:560px){
  .wrap{padding:0 18px}
  .filter-legend{min-width:100%;margin-bottom:2px}
  .jl-foot{flex-direction:column;gap:9px}
  .jl-foot i{width:80px;height:1px}
  .motion-toggle{right:12px;bottom:12px;padding:8px 11px}
}

/* ---------- Reduced motion ----------
   The house rule: compose a resting frame, do not just switch animation off.
   Every element below is given the state it would have finished in. */

@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}

  .hero h1 .ch{animation:none;opacity:1;transform:none}
  .portrait{animation:none;opacity:1;transform:none}

  /* The rule sits fully drawn rather than invisible. */
  .hero-rule path{animation:none;stroke-dashoffset:0}

  /* Marks at their final state, spine fully extended, entries in place. */
  .js-mark .mark-svg,
  .js-mark.is-in .mark-svg{opacity:1;transform:none;transition:none}
  .js-reveal,.js-reveal.is-in{opacity:1;transform:none;transition:none}
  .js-reveal::after,.js-reveal.is-in::after{transform:scaleY(1);transition:none}

  /* --lume rests mid-range so the wash still reads as lit, not switched off. */
  :root{--lume:.55}

  .entry-toggle .caret,.chip,.card,.motion-toggle{transition:none}
  .card:hover{transform:none}
}

/* ---------- Print ---------- */

@media print{
  .filters,.motion-toggle,.rings,.glow{display:none}
  .entry-detail{display:block!important}
  .entry-toggle{display:none}
  body{background:#fff;color:#000}
  .card,.number{border-color:#ccc;background:none}
  .hero h1,.entry h3,.card-name,.num-figure,.section-head h2{color:#000}
}
