/*
  assets/css/style.css
  All styling for the portfolio. Hybrid aesthetic: hand-drawn chrome (frames, buttons,
  underlines, annotations) over clean, readable content (sans body text, mono code).
  The CSS borders here are also the no-JS fallback — sketch.js makes them transparent
  once it has drawn real ink over the top, so nothing ever renders unbordered.
*/

/* ============================================================
   1. Tokens
   ============================================================ */
:root {
  --paper:        #f7f4ec;
  --paper-card:   #fffdf8;
  --ink:          #1c1b18;
  --ink-soft:     #56514a;   /* 7:1 on paper — passes AA and AAA for body text */
  --rule:         #b8b1a4;
  --accent:       #e2552a;   /* marker orange — display + decoration only */
  --accent-deep:  #c2431d;   /* darkened for text-bearing surfaces, keeps AA */
  --accent-2:     #2d6e8e;   /* ballpoint blue */
  --highlight:    #ffe27a;
  --grid:         rgba(28, 27, 24, 0.05);

  --font-display: 'Architects Daughter', cursive;
  --font-body:    'Karla', system-ui, sans-serif;
  --font-note:    'Caveat', cursive;
  --font-mono:    ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  --wrap: 1080px;
  --gap: 28px;
}

/* ============================================================
   2. Base
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  background-color: var(--paper);
  /* Faint graph ruling — sells "notebook" without shipping a texture image. */
  background-image:
    linear-gradient(var(--grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid) 1px, transparent 1px);
  background-size: 26px 26px;
  color: var(--ink-soft);
  font-family: var(--font-body);
  font-size: 17.5px;
  line-height: 1.72;
  -webkit-font-smoothing: antialiased;
}

/* Paper grain. feTurbulence keeps it zero-request and resolution independent. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
  opacity: 0.3;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.3'/%3E%3C/svg%3E");
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.16;
  color: var(--ink);
  margin: 0 0 0.45em;
}
h1 { font-size: clamp(2.7rem, 7.5vw, 4.6rem); letter-spacing: -0.015em; }
h2 { font-size: clamp(1.75rem, 4vw, 2.5rem); }
h3 { font-size: 1.35rem; }
p  { margin: 0 0 1.05em; }
p:last-child { margin-bottom: 0; }

a { color: var(--accent-deep); }

code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: rgba(28, 27, 24, 0.06);
  padding: 0.12em 0.4em;
  border-radius: 3px;
  color: var(--ink);
}

/* Marker highlight — angled gradient so it reads as swiped, not filled. */
mark {
  background-image: linear-gradient(100deg, transparent 1%, var(--highlight) 2%, var(--highlight) 96%, transparent 99%);
  background-repeat: no-repeat;   /* so the reveal can swipe it on from zero width */
  background-size: 100% 100%;
  padding: 0 3px;
  color: inherit;
}

:focus-visible { outline: 3px solid var(--accent-2); outline-offset: 3px; }

/* Horizontal longhand only — the padding shorthand here would out-specify `section`
   below and flatten the vertical rhythm on every `<section class="wrap">`. */
.wrap { max-width: var(--wrap); margin: 0 auto; padding-left: 24px; padding-right: 24px; }
/* Clears the sticky header when an anchor link jumps here — without it the heading
   lands underneath the bar. */
section { padding-top: 76px; padding-bottom: 76px; scroll-margin-top: 84px; }
.section-head { margin-bottom: 34px; }
.section-head p { max-width: 54ch; margin-top: -0.2em; }

/* Skip link — keyboard users shouldn't tab through the nav on every page load. */
.skip {
  position: absolute; left: -9999px;
  background: var(--ink); color: var(--paper);
  padding: 10px 18px; z-index: 999;
}
.skip:focus { left: 12px; top: 12px; }

/* ============================================================
   3. Sketch overlay plumbing
   ============================================================ */
[data-sketch] { position: relative; }

[data-sketch] > svg.sketch {
  position: absolute;
  inset: -12px;
  width: calc(100% + 24px);
  height: calc(100% + 24px);
  pointer-events: none;
  overflow: visible;
}
/* Content must be lifted above the overlay. Bare text nodes can't be, which is why
   filled elements (buttons) wrap their label in a <span> — see .btn in the markup. */
[data-sketch] > *:not(svg.sketch) { position: relative; z-index: 1; }

/* Fallback border: paints instantly and survives JS failure. Once sketch.js draws real
   ink it sets [data-drawn] and we hide this by colour only — never by width, which
   would change the content box and reflow the page. */
[data-sketch]:not([data-sketch="underline"]):not([data-sketch="vline"]):not([data-sketch="dot"]) {
  border: 2.4px solid var(--ink);
  border-radius: 200px 14px 190px 14px / 14px 190px 14px 200px;
}
[data-sketch][data-drawn] { border-color: transparent !important; }

/* ============================================================
   4. Header
   ============================================================ */
header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(247, 244, 236, 0.86);
  backdrop-filter: blur(6px);
}
.bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px; flex-wrap: wrap;
  /* Long-hand, not `padding: 16px 0 20px`. This element is `.wrap .bar`, and the
     shorthand zeroed the 24px side padding .wrap sets — so under 1080px the logo sat
     flush to the screen edge while every heading below it started 24px in. */
  padding-top: 16px; padding-bottom: 20px;
}
.logo {
  font-family: var(--font-display);
  font-size: 1.3rem; color: var(--ink); text-decoration: none;
}
.logo span { color: var(--accent); }

nav ul { display: flex; gap: 24px; list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
nav a {
  position: relative;
  text-decoration: none;
  color: var(--ink-soft);
  font-size: 1rem;
  padding-bottom: 7px;
}
/* Must out-specify the generic [data-sketch] > svg.sketch overlay rule, which scores
   (0,2,1) — a plain `nav a svg.sketch` is only (0,1,3) and loses. When it lost, the
   14-unit viewBox got stretched over a 49px box and preserveAspectRatio="none" turned
   the squiggle into a blob sitting on top of the label instead of a line under it. */
a[data-sketch="underline"] > svg.sketch {
  inset: auto; left: -5px; bottom: -3px;
  width: calc(100% + 10px); height: 14px;
}
/* Draw-on rather than fade-in — a squiggle should look like it was just drawn.
   Translucent so it reads as marker laid over paper rather than a solid rule. */
nav a svg.sketch path {
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
  opacity: 0.55;
  transition: stroke-dashoffset 0.3s ease-out;
}
nav a:hover svg.sketch path,
nav a:focus-visible svg.sketch path,
nav a.current svg.sketch path { stroke-dashoffset: 0; }
nav a.current { color: var(--ink); }

/* ============================================================
   5. Hero
   ============================================================ */
.hero { padding-top: 46px; padding-bottom: 60px; }
.hero-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 56px; align-items: start; }
.eyebrow {
  font-family: var(--font-note);
  font-size: 1.45rem; color: var(--accent);
  display: inline-block; transform: rotate(-2deg); margin-bottom: 2px;
}
.lede { font-size: 1.15rem; max-width: 48ch; }

/* Polaroid — the clean-content rule applied to imagery: real photo, sketchy frame. */
.polaroid {
  background: var(--paper-card);
  padding: 14px 14px 0;
  transform: rotate(2deg);
  max-width: 270px;
  margin-left: auto;
}
.polaroid .shot {
  aspect-ratio: 4 / 5;
  background: repeating-linear-gradient(45deg, #ece7db, #ece7db 12px, #e4dece 12px, #e4dece 24px);
  display: grid; place-items: center;
  text-align: center; font-family: var(--font-note);
  font-size: 1.15rem; color: #8d8577; padding: 12px;
}
/* me.jpg is pre-cropped to 4:5 to match .shot, so cover has nothing left to trim and
   the subject stays centred. A photo of another ratio will be centre-cropped instead. */
.polaroid .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.polaroid .caption {
  font-family: var(--font-note); font-size: 1.25rem;
  color: var(--ink-soft); text-align: center; padding: 8px 4px 12px;
}

/* Doodled margin note with a hand-drawn arrow. */
.note {
  position: relative;
  font-family: var(--font-note); font-size: 1.3rem;
  color: var(--accent-2); transform: rotate(-3deg);
  max-width: 230px; line-height: 1.35;
}
.note svg { position: absolute; overflow: visible; }

/* ============================================================
   6. Buttons
   ============================================================ */
.btns { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 30px; }
.btn {
  font-family: var(--font-display);
  font-size: 1.02rem;
  padding: 12px 26px;
  background: var(--paper-card);
  color: var(--ink);
  text-decoration: none;
  display: inline-block;
  cursor: pointer;
  transition: transform 0.16s ease;
}
.btn.primary { background: var(--accent-deep); color: #fff; }
.btn:hover { transform: translate(-2px, -2px) rotate(-0.5deg); }
.btn:active { transform: translate(1px, 1px); }

/* ============================================================
   7. Cards / work
   ============================================================ */
/* The lead project takes the full row as .featured, so the remaining three need to sit
   in one row rather than 2 + 1 with one stranded. Column count is floor((C + gap) /
   (min + gap)) against the wrap's *content* box — 1080px max-width less 48px padding,
   so 1032px, not 1080. Three columns therefore need a min of 320px or less; 300px is
   used instead of that exact fit to leave room for rounding and scrollbar width.
   Revisit it if the card count changes. align-items: start then lets each card size to
   its own copy; stretching them to a shared row height leaves dead space under the
   shorter ones, which reads as an unfinished card rather than a deliberately ragged
   pinboard. */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: start;
  gap: 36px;
}
.card {
  background: var(--paper-card);
  padding: 26px 26px 22px;
  transition: transform 0.2s ease;
}
/* Uneven rotation so the grid never reads as machine-aligned. */
.card:nth-child(3n+1) { transform: rotate(-0.7deg); }
.card:nth-child(3n+2) { transform: rotate(0.5deg); }
.card:nth-child(3n+3) { transform: rotate(-0.3deg); }
.card:hover { transform: rotate(0deg) translateY(-5px); }
.card h3 { margin-bottom: 0.3em; }
.card p { font-size: 0.98rem; }
.kicker {
  font-family: var(--font-note); font-size: 1.25rem;   /* Caveat needs more size than a sans to stay legible */
  color: var(--accent-2); letter-spacing: 0.01em;
}
.card-links { display: flex; gap: 16px; margin-top: 14px; font-size: 0.94rem; }
.card-links a { text-decoration-thickness: 1px; text-underline-offset: 3px; }

/* Result strip — the numbers that keep a sketchy site from reading as vague. */
/* Painted with a gradient rather than a flat background so the fill can be swiped on
   from zero width — see the reveal section. */
.result {
  font-family: var(--font-mono); font-size: 0.82rem;
  color: var(--ink);
  background-image: linear-gradient(var(--highlight), var(--highlight));
  background-repeat: no-repeat;
  background-size: 100% 100%;
  display: inline-block; padding: 2px 8px; margin-top: 4px;
}

/* --- Featured card -----------------------------------------------------------
   The lead project spans the whole row so its screenshot has somewhere to live.
   Rotation is nearly flat here: the same 0.7deg that reads as a pinned card at
   360px wide reads as a crooked page at full width. */
.card.featured { grid-column: 1 / -1; transform: rotate(-0.25deg); }
.card.featured:hover { transform: rotate(0deg) translateY(-5px); }

.featured-grid {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: 30px;
  align-items: center;
  margin-top: 18px;
}
.featured-shot {
  background: var(--paper-card);
  padding: 10px;
  transform: rotate(0.6deg);   /* counter to the card, so the print looks laid on top */
}
/* The capture's own ratio, declared so the box reaches its final height before the
   JPEG arrives. sketch.js measures on load and redraws via ResizeObserver, but a
   late reflow would still flash a frame at the wrong size. */
.featured-shot img {
  display: block; width: 100%;
  /* height:auto is required, not optional. The width/height attributes on the <img>
     map to CSS presentation hints, so without this the 834px height attribute wins
     and the picture renders at full pixel height instead of scaling to the column. */
  height: auto;
  aspect-ratio: 1456 / 834;
  object-fit: cover;
}

/* --- Learning repos ----------------------------------------------------------
   Deliberately lighter than .card: these are pointers to working-out, not finished
   work, and giving them card frames would make them compete with Selected work.
   Two columns at desktop, one below — three would shrink the descriptions to
   fragments. */
.learn-grid {
  display: grid;
  /* 340px resolves to two columns against the 1032px content box, so four repos sit
     2x2 rather than 3 + 1 stranded. Same arithmetic as .cards — floor((C + gap) /
     (min + gap)) — but a different answer because the item count differs. */
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: 26px 40px;
}
/* Glyph left of the copy, not above it. Eye-tracking puts a vertical scan down the
   left edge of a content block, so that column is where a silhouette actually gets
   seen — four distinct shapes (cycle, chain, flask, layered net) let someone tell the
   four repos apart without reading a word. */
.learn-item {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
.learn-glyph { width: 62px; height: 50px; overflow: visible; }
/* Stroke width is in viewBox units, and this renders at roughly half the timeline
   glyphs' scale — so it has to be set proportionally heavier to land at the same
   visual weight on screen. */
.learn-glyph path {
  fill: none;
  stroke: var(--accent-2);
  stroke-width: 4.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.learn-item a {
  font-family: var(--font-mono); font-size: 0.92rem;
  color: var(--accent-deep); text-decoration: none;
  /* Wraps rather than overflowing: these repo names are long and unbreakable. */
  overflow-wrap: anywhere;
  /* Room for the squiggle to sit below the text rather than through it. */
  display: inline-block;
  padding-bottom: 7px;
}
/* These names are mono and full of underscores, which render on the baseline exactly
   where the default squiggle lands — the two cross and the name stops being readable.
   The nav doesn't hit this because it's sans with no underscores. Push the overlay
   clear of the descender line; the padding above reserves the space it moves into. */
.learn-item a[data-sketch="underline"] > svg.sketch { bottom: -7px; }
.learn-item p {
  font-size: 0.94rem; color: var(--ink-soft);
  margin-top: 7px; max-width: 46ch;
}

/* ============================================================
   8. Timeline — the "progress and growth" section
   ============================================================ */
.timeline-wrap { position: relative; }
.timeline { list-style: none; margin: 0; padding: 0 0 0 52px; }
.spine { position: absolute; left: 8px; top: 6px; width: 12px; bottom: 6px; }
/* Text left, era glyph right — the glyph column is what fills the dead space that the
   60ch measure used to leave. */
.tl-item {
  position: relative;
  margin-bottom: 48px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 140px;
  gap: 34px;
  align-items: start;
}
.tl-item:last-child { margin-bottom: 0; }
.tl-dot { position: absolute; left: -52px; top: 8px; width: 15px; height: 15px; }

/* Entries lift in via the shared [data-anim="lift"] rule in section 11. Only the
   marker is timeline-specific: it lands a beat after its text, like it was inked in
   afterwards rather than arriving with the words. */
.anim .tl-dot {
  scale: 0.3;
  opacity: 0;
  transition: scale 0.4s cubic-bezier(0.34, 1.7, 0.5, 1) 0.12s, opacity 0.3s ease-out 0.12s;
}
.anim .tl-item.in-view .tl-dot { scale: 1; opacity: 1; }

/* Hand-drawn era glyph. Drawn solid by default — the dash trick that hides it lives
   behind .anim for the same reason the entry fade does, so a glyph can never end up
   hidden by a stylesheet that arrived without the script able to reveal it. */
.glyph { width: 140px; height: 112px; overflow: visible; }
.glyph path {
  fill: none;
  stroke: var(--accent-2);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* --len is measured by main.js from getTotalLength(). Eases out hard so a stroke
   leaves fast and settles, the way a pen does; the base 0.22s delay lets the entry
   finish lifting before the drawing starts. */
.anim .glyph path {
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
  transition: stroke-dashoffset 0.55s cubic-bezier(0.25, 0.8, 0.35, 1) 0.22s;
}
.anim .tl-item.in-view .glyph path { stroke-dashoffset: 0; }
/* Stagger so a glyph assembles stroke by stroke rather than appearing all at once.
   Steps are tight enough that consecutive strokes overlap and read as one hand. */
.anim .glyph path:nth-child(2) { transition-delay: 0.32s; }
.anim .glyph path:nth-child(3) { transition-delay: 0.42s; }
.anim .glyph path:nth-child(4) { transition-delay: 0.52s; }
.anim .glyph path:nth-child(5) { transition-delay: 0.62s; }
.anim .glyph path:nth-child(6) { transition-delay: 0.72s; }
.anim .glyph path:nth-child(7) { transition-delay: 0.82s; }
.anim .glyph path:nth-child(8) { transition-delay: 0.92s; }
.anim .glyph path:nth-child(9) { transition-delay: 1.02s; }
.tl-year {
  font-family: var(--font-display); font-size: 1.05rem;
  color: var(--accent); margin-bottom: 2px;
}
.tl-item h3 { margin-bottom: 0.2em; }
.tl-item p { font-size: 1rem; max-width: 60ch; }

/* ============================================================
   9. Tags
   ============================================================ */
.tag-group { margin-bottom: 28px; }
.tag-group h4 {
  font-family: var(--font-note); font-size: 1.3rem;
  color: var(--ink-soft); margin: 0 0 12px;
}
.tags { display: flex; flex-wrap: wrap; gap: 15px 16px; }
.tag {
  padding: 6px 16px; font-size: 0.95rem;
  background: var(--paper-card); color: var(--ink);
}
.tag:nth-child(even) { transform: rotate(1deg); }
.tag:nth-child(odd)  { transform: rotate(-0.9deg); }

/* ============================================================
   10. Contact
   ============================================================ */
.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 30px; }
.contact-card { background: var(--paper-card); padding: 22px 24px; }
.contact-card .kicker { display: block; margin-bottom: 2px; }
.contact-card a {
  font-family: var(--font-display); font-size: 1.15rem;
  color: var(--ink); text-decoration: none; word-break: break-word;
}
.contact-card a:hover { color: var(--accent-deep); }

footer {
  padding: 46px 0 70px; text-align: center;
  font-family: var(--font-note); font-size: 1.25rem;
}
footer span { color: var(--accent); }

/* ============================================================
   11. Reveal
   ============================================================ */
/* Content fades up on scroll. Gated behind .js so a JS failure can never leave the page
   blank — without the gate, one broken script hides every section below the fold. */
.js .reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.5s ease, transform 0.5s ease; }
.js .reveal.in { opacity: 1; transform: none; }

/* --- Per-element reveal ------------------------------------------------------
   [data-anim] marks something that arrives rather than being there already;
   [data-anim-group] releases its children together on a stagger, so a row of cards
   reads as one gesture instead of four unrelated ones. main.js writes the per-child
   delay into --d.

   All of it hangs off .anim, which main.js sets from the same block that releases
   these elements. Never gate a hidden state on .js: that class is set by every build
   of main.js ever shipped, so an older cached script would apply the hiding with
   nothing able to clear it and blank the section.

   Movement uses the independent translate/scale/rotate properties rather than
   transform, so it composes with the resting tilt already on .card, .tag, .polaroid
   and .eyebrow instead of overwriting it. */
.anim [data-anim] {
  opacity: 0;
  transition:
    opacity 0.5s ease-out var(--d, 0ms),
    translate 0.6s cubic-bezier(0.22, 0.9, 0.3, 1) var(--d, 0ms),
    scale 0.6s cubic-bezier(0.22, 0.9, 0.3, 1) var(--d, 0ms),
    rotate 0.6s cubic-bezier(0.22, 0.9, 0.3, 1) var(--d, 0ms),
    clip-path 0.85s cubic-bezier(0.3, 0.8, 0.3, 1) var(--d, 0ms),
    background-size 0.7s cubic-bezier(0.4, 0.8, 0.3, 1) var(--d, 0ms);
}
.anim [data-anim="lift"]  { translate: 0 20px; }              /* body copy, list rows */
.anim [data-anim="rise"]  { translate: 0 32px; }              /* cards — further, heavier */
.anim [data-anim="pop"]   { scale: 0.82; }                    /* chips and tags */
.anim [data-anim="tilt"]  { translate: 0 24px; rotate: 6deg; }/* the polaroid, dropped in */
/* Wipes left to right so the name reads as being written, not faded up. The negative
   vertical inset keeps descenders and the display font's overshoot from clipping. */
.anim [data-anim="write"] { clip-path: inset(-18% 100% -18% 0); }
.anim [data-anim="swipe"] { background-size: 0% 100%; }       /* marker over paper */

.anim [data-anim].in-view { opacity: 1; translate: none; scale: none; rotate: none; }
/* Scoped, not folded into the rule above: a blanket clip-path would crop the sketch
   overlays, which deliberately overflow their host by 12px. */
.anim [data-anim="write"].in-view { clip-path: inset(-18% -2% -18% 0); }
.anim [data-anim="swipe"].in-view { background-size: 100% 100%; }

/* The polaroid is solo rather than in the hero group, so nothing writes --d for it.
   Hold it back until the name has started writing — the photo being tossed down after
   the greeting reads better than both arriving together. */
.anim .polaroid[data-anim] { --d: 240ms; }

/* And it should develop, not arrive finished. The image comes up from a washed-out
   ghost to full colour once the frame has landed, which is the one thing a polaroid
   does that no other photo does. Opacity starts above zero so the ghost is visible. */
.anim .polaroid .shot img {
  opacity: 0.12;
  filter: saturate(0.15) contrast(0.75) brightness(1.2);
  transition: opacity 0.9s ease-out 0.6s, filter 1.2s ease-out 0.6s;
}
.anim .polaroid.in-view .shot img { opacity: 1; filter: none; }

/* Same develop-in treatment for the featured screenshot, so the two photographic
   elements on the page behave alike rather than each having their own idea. */
.anim .featured-shot img {
  opacity: 0.12;
  filter: saturate(0.15) contrast(0.75) brightness(1.2);
  transition: opacity 0.9s ease-out 0.5s, filter 1.2s ease-out 0.5s;
}
.anim .card.featured.in-view .featured-shot img { opacity: 1; filter: none; }

/* The result chip is a marker stroke, so it swipes rather than fades — and it waits
   for its card to land first. */
.anim .card .result { background-size: 0% 100%; transition: background-size 0.6s cubic-bezier(0.4, 0.8, 0.3, 1) 0.4s; }
.anim .card.in-view .result { background-size: 100% 100%; }

/* ============================================================
   12. Responsive
   ============================================================ */
@media (max-width: 860px) {
  .hero-grid { grid-template-columns: 1fr; gap: 36px; }
  .featured-grid { grid-template-columns: 1fr; gap: 24px; }
  .polaroid { margin-left: 0; }
  section { padding: 56px 0; }
}
/* Below this the text column is too narrow to give up 140px, and the glyphs are
   decorative — drop them rather than shrink them into illegibility. */
@media (max-width: 780px) {
  .tl-item { grid-template-columns: 1fr; gap: 0; }
  .glyph { display: none; }
}
@media (max-width: 560px) {
  body { font-size: 16.5px; }
  /* No nav links on phones. Five of them sit at roughly 14px with 20px gaps — well under
     a comfortable tap target — and they duplicate the hero's two CTAs, which already
     cover the destinations anyone actually wants. The logo stays as the way back to the
     top; everything else is a scroll away on a single page. */
  nav { display: none; }
  .bar { padding-bottom: 12px; gap: 10px; }
  .timeline { padding-left: 40px; }
  .tl-dot { left: -40px; }
  .spine { left: 4px; }
}

/* ============================================================
   13. Preferences
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  html { scroll-behavior: auto; }
  /* Must match .js .reveal specificity or the fade-out rule keeps winning here. */
  .js .reveal { opacity: 1; transform: none; }
  /* Covers reduced motion being switched on after load, when .anim is already set and
     content below the fold would otherwise stay hidden with no transition to reveal it. */
  .anim [data-anim], .anim .tl-dot {
    opacity: 1; translate: none; scale: none; rotate: none;
    clip-path: none; background-size: 100% 100%;
  }
  .anim .card .result { background-size: 100% 100%; }
  .anim .polaroid .shot img { opacity: 1; filter: none; }
  .anim .featured-shot img { opacity: 1; filter: none; }
}

@media print {
  body::after, header, .note svg { display: none; }
  body { background: #fff; }
  [data-sketch] { border-color: var(--ink) !important; }
  svg.sketch { display: none; }
  /* These only reach their end state once scrolled into view — force them, or printing
     a page nobody scrolled through emits blank boxes and missing entries. */
  .glyph path { stroke-dashoffset: 0 !important; }
  .anim [data-anim], .anim .tl-dot {
    opacity: 1 !important; translate: none !important; scale: none !important;
    rotate: none !important; clip-path: none !important; background-size: 100% 100% !important;
  }
  .anim .card .result { background-size: 100% 100% !important; }
  .anim .polaroid .shot img { opacity: 1 !important; filter: none !important; }
  .anim .featured-shot img { opacity: 1 !important; filter: none !important; }
}
