/* ============================================================
   Chris Almaguer - portfolio design system
   Concept: the interface is monochrome. Colour appears only
   inside the data. Colour is information, not decoration.
   ============================================================ */

/* ---- 1. Tokens ------------------------------------------- */

:root {
  /* Type. SF Pro only, with no webfont download.

     -apple-system is the correct way to get it: on macOS and iOS the
     browser resolves it to the real SF Pro, and picks Display or Text
     at the right optical size on its own. Apple's licence does not
     permit self-hosting SF as a webfont, so naming the files directly
     is not an option - this is both the legal route and the fast one.
     Off Apple platforms it falls through to the platform UI face.

     SF Mono carries the label voice: .meta, .num, and anything that
     has to align in a column. That pairing is the point of choosing
     SF - it ships with a matching monospace, which Helvetica does not
     have, so labels no longer lean on caps and tracking alone. */
  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text",
    "SF Pro Display", system-ui, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo,
    "Liberation Mono", monospace;

  /* Greys. Interface only. */
  /* Never pure black: a dark grey at 14.5:1 on paper reads as considered
     rather than harsh, and loses nothing that matters. */
  --ink:        #26262b;
  --ink-2:      #45454c;
  --ink-3:      #686872;
  --ink-4:      #8f9096;
  --rule:       rgba(38, 38, 43, 0.13);
  --rule-soft:  rgba(38, 38, 43, 0.07);
  --paper:      #fbfbfa;
  --paper-2:    #f2f2f0;
  /* Tile hover ground. Mid-dark grey rather than the near-black --ink;
     paper text still clears 11.9:1 on it. */
  --hover:      #f2f2f0;
  --hover-lift: 0 2px 10px rgba(38, 38, 43, 0.07), 0 10px 28px rgba(38, 38, 43, 0.06);

  /* Space - 4px base, named by use not size. */
  --gap-s:  1rem;
  --gap-m:  2rem;
  --gap-l:  4rem;
  --gap-xl: 7rem;
  --gap-xxl: 11rem;

  /* Measure */
  --measure: 34rem;
  --shell: 78rem;
  --rail: 16.5rem;
  --rail-pad: 1.75rem;

  /* Motion */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --t-fast: 180ms;
  --t-med: 420ms;
  --t-slow: 760ms;
}

/* Dark is opt-in only. The site enters light regardless of OS setting;
   js/theme.js stamps data-theme on <html> and remembers the choice. */
:root[data-theme="dark"] {
  --ink:       #f4f4f2;
  --ink-2:     #c6c6c2;
  --ink-3:     #8f8f97;
  --ink-4:     #75777c;
  --rule:      rgba(244, 244, 242, 0.14);
  --rule-soft: rgba(244, 244, 242, 0.07);
  --paper:     #17171a;
  --paper-2:   #212126;
  --hover:     #212126;
  --hover-lift: inset 0 0 0 1px rgba(244, 244, 242, 0.11);
}

/* ---- 2. Reset -------------------------------------------- */

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

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

/* The masthead is sticky at 4rem, so in-page anchors have to clear it
   or the section heading lands underneath the bar. */
/* Only needed while the nav is a bar overhead; the rail does not overlap. */
:target,
section[id] { scroll-margin-top: 1.5rem; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 1.0625rem;
  line-height: 1.6;
  letter-spacing: 0;
  font-synthesis-weight: none;
}

img, svg { display: block; max-width: 100%; }
/* Deter drag-out and long-press save. Text stays selectable - only
   media is guarded. */
img, video { -webkit-user-drag: none; user-drag: none; -webkit-touch-callout: none; user-select: none; }
img { height: auto; }

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

h1, h2, h3, h4 { font-weight: 700; margin: 0; text-wrap: balance; }
p { margin: 0; text-wrap: pretty; }

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

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 0;
}

/* ---- 3. Type scale --------------------------------------- */

.h1 {
  font-size: clamp(1.9rem, 4vw, 3rem);
  line-height: 1.08;
  letter-spacing: -0.03em;
}

.lede {
  font-size: clamp(1.15rem, 1.9vw, 1.5rem);
  line-height: 1.42;
  letter-spacing: -0.019em;
  color: var(--ink-2);
  text-wrap: pretty;
}

.body { max-width: var(--measure); color: var(--ink-2); }
.body + .body { margin-top: 1.15em; }
.body strong { color: var(--ink); font-weight: 700; }

/* Metadata / labels. SF Mono, so the label voice comes from a genuine
   change of family rather than caps and tracking doing all the work. */
.meta {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  /* No negative tracking: SF Mono is fixed-advance, so tightening only
     crowds the glyphs without buying back any width. */
  letter-spacing: 0;
}

/* ---- 4. Shell -------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 3.5rem);
}

/* Narrow sections keep the reading measure but stay flush with the left
   content edge instead of centring inside the shell. */
.shell--narrow { max-width: var(--shell); }
.shell--narrow > * { max-width: 52rem; }

section { padding-block: var(--gap-xl); }

.rule { border: 0; border-top: 1px solid var(--rule); margin: 0; }

/* Section heading: mono label sitting on a hairline. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-m);
  padding-bottom: var(--gap-s);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--gap-l);
}

/* ---- 5. Site header -------------------------------------- */

/* Navigation lives in a fixed rail down the left rather than a bar across
   the top. On a page that is mostly a grid of work, a top bar competes with
   the grid for the same horizontal band; a rail gets out of its way and
   leaves the nav permanently in view without occupying reading width.

   Below 64rem there is no room for a rail, so it folds back into a bar. */

.masthead {
  position: fixed;
  inset: 0 auto 0 0;
  width: var(--rail);
  z-index: 50;
  background: var(--paper);
  border-right: 1px solid var(--rule);
}

/* Base nav. Also used by the footer link rows, so it lives outside the rail. */
.nav {
  display: flex;
  gap: clamp(1rem, 2.5vw, 2rem);
  align-items: center;
}

.nav a {
  font-size: 0.8125rem;
  color: var(--ink-3);
  transition: color var(--t-fast) var(--ease);
}

.nav a:hover, .nav a[aria-current="page"] { color: var(--ink); }

/* The shell's max-width and centring are meaningless inside the rail. */
.masthead .shell {
  max-width: none;
  padding-inline: var(--rail-pad);
}

.masthead .nav {
  flex: 0 0 auto;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.99rem;                 /* 15.8px + 21.1px line box = the comp\'s 37px step */
  margin-top: 2.03rem;           /* drops nav1 baseline from 405.5 to 438 */
  width: 100%;
}

.masthead .nav a { font-size: 0.9438rem; line-height: 1.4; }

/* Elsewhere links: same column, quieter voice, held apart from the
   primary set by a clear step. */
.masthead .nav--social { flex-direction: row; margin-top: 1.6rem; gap: 0.9rem; }
.masthead .nav--social a { color: var(--ink-4); transition: color var(--t-fast) var(--ease); }
.masthead .nav--social a:hover { color: var(--ink); }
.masthead .nav--social svg { width: 15px; height: 15px; display: block; }

/* At the foot of the rail, away from the links it is not one of. */
.masthead .theme-toggle { margin-left: auto; margin-right: -0.35rem; }

/* ---- Rail contents ---------------------------------------
   Mark, then name and role stacked, then the sections, with
   About set apart because it is not one of them, and the ways
   to reach him at the foot.                                */

/* The mark now runs full width, flush to the top edge, cropped to a
   landscape frame rather than sitting inside the rail's padding. The
   image is oversized inside it so the glow fills the frame the way the
   Figma comp does. */
.rail__mark {
  width: calc(100% + 2 * var(--rail-pad));
  margin: 0 calc(-1 * var(--rail-pad)) 0;
  aspect-ratio: 264 / 228;
  overflow: hidden;
  display: block;
}

.rail__mark img {
  /* Figma places the 440px source at 241x241 inside the 264x228 frame,
     offset (-5, -13). Expressed against the frame width so it scales:
     241/264 = 91.29%, -5/264 = -1.894%, -13/264 = -4.924%. Margin
     percentages resolve against width, which is what keeps both true. */
  width: 91.29%;
  height: auto;
  display: block;
  margin: -4.924% 0 0 -1.894%;
}

.railmark { display: block; }

.railmark__name {
  display: block;
  font-size: 1.7936rem;
  font-weight: 700;
  line-height: 0.871;
  letter-spacing: -0.022em;
  text-transform: uppercase;
}

.railmark__role {
  display: block;
  margin-top: 0.7125rem;
  font-size: 0.7563rem;
  font-weight: 700;
  letter-spacing: 0.075em;
  text-transform: uppercase;
  line-height: 1.3;
  color: var(--ink-3);
}

.rail__foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

.rail__social { display: flex; align-items: center; gap: 0.7rem; }

.rail__social a {
  color: var(--ink-4);
  display: grid;
  place-items: center;
  transition: color var(--t-fast) var(--ease);
}

.rail__social a:hover { color: var(--ink); }
.rail__social svg { width: 16px; height: 16px; fill: currentColor; }



.masthead__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  gap: clamp(2rem, 4vw, 2.75rem);
  height: 100%;
  padding-block: 0 calc(var(--gap-m) + 0.15rem);
}

/* Content clears the rail. */
body { padding-left: var(--rail); }

/* ---- 5b. Segmented control -------------------------------
   Shared by the masthead theme switch and every in-page demo
   control, so the interface has one button language.       */

.seg {
  display: inline-flex;
  border: 1px solid var(--rule);
  border-radius: 0;
  overflow: hidden;
}

.seg button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.45rem 0.7rem;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}

.seg button + button { border-left: 1px solid var(--rule); }
.seg button:hover { color: var(--ink); }
.seg button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }

/* ---- 5c. Theme toggle ------------------------------------
   A half-filled circle: the light/dark relationship itself, so
   it needs no label and reads in any language.              */

.theme-toggle {
  appearance: none;
  background: none;
  border: 0;
  padding: 0.35rem;
  margin-left: 0.25rem;
  cursor: pointer;
  color: var(--ink-3);
  display: grid;
  place-items: center;
  transition: color var(--t-fast) linear, transform var(--t-fast) var(--ease);
}

.theme-toggle svg { width: 16px; height: 16px; }
.theme-toggle:hover { color: var(--ink); }
.theme-toggle[aria-pressed="true"] svg { transform: rotate(180deg); }

@media (max-width: 34rem) {
  .nav { gap: 0.85rem; }
  .nav a { font-size: 0.75rem; }
}

/* ---- 6. Nameplate ----------------------------------------
   The masthead of a newspaper, not the hero of a landing page.
   Name at full width between two rules; everything else is
   filed underneath it as specification.                     */

.nameplate {
  padding-block: 0 clamp(0.75rem, 2vw, 1.5rem);
}

.nameplate h1 {
  font-size: clamp(1.4rem, 5.4vw, 3.9rem);
  line-height: 0.98;
  /* SF Pro Display sets wider than Helvetica at this size, so the nameplate
     needs more negative tracking to keep the same density. */
  letter-spacing: -0.03em;
  font-weight: 700;
  text-transform: uppercase;
  margin: 0;
}

.nameplate__sub {
  /* An eyebrow above the name: small, uppercase and tracked, so it labels
     the discipline without competing with the nameplate underneath. */
  margin-bottom: 0.55rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  line-height: 1.4;
  color: var(--ink-3);
}

/* Short bio under the nameplate. Held to the prose measure so it reads as
   a paragraph, not a banner. */
.nameplate__bio {
  margin-top: clamp(1rem, 2vw, 1.5rem);
  max-width: 40rem;
  font-size: clamp(0.9375rem, 1.15vw, 1.0625rem);
  line-height: 1.55;
  color: var(--ink-2);
  text-wrap: pretty;
}

/* Below the width where it fits on one line, let it wrap rather than
   overflow the page. */
@media (max-width: 46rem) {
  .nameplate__sub { white-space: normal; }
}


/* Specification block - label / value rows, the way a spec sheet
   or a chart's metadata reads. Replaces the "availability pill". */

.spec {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  border-bottom: 1px solid var(--rule);
}

.spec > dt {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding-block: 0.6rem;
  border-right: 1px solid var(--rule);
}

.spec > dd {
  margin: 0;
  padding-block: 0.6rem;
  padding-left: 1rem;
  font-size: 0.9375rem;
  color: var(--ink);
}

.spec > dt:not(:first-of-type),
.spec > dd:not(:first-of-type) { border-top: 1px solid var(--rule); }

@media (max-width: 34rem) {
  .spec { grid-template-columns: 6.5rem minmax(0, 1fr); }
  .spec > dd { padding-left: 0.75rem; }
}

/* Square hairline button. No radius, no fill, no motion trickery. */
.btn {
  display: inline-block;
  padding: 0.5rem 0.85rem;
  border: 1px solid var(--ink-4);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  transition: background var(--t-fast) linear, color var(--t-fast) linear;
}

.btn:hover { background: var(--ink); color: var(--paper); }

/* ---- 7. Work ledger -------------------------------------- */
/* An index that reads like a masthead contents page, not a
   moodboard. Numbered, dated, scannable in one pass.        */

.ledger { border-top: 1px solid var(--rule); }

.entry {
  display: grid;
  grid-template-columns: 3.25rem minmax(0, 1fr) auto;
  gap: var(--gap-s) var(--gap-m);
  align-items: baseline;
  padding: clamp(1.4rem, 3vw, 2.1rem) clamp(0.5rem, 1.5vw, 1rem);
  border-bottom: 1px solid var(--rule);
  position: relative;
  transition: background 110ms linear, box-shadow 160ms ease;
}

/* Hard invert. No fade, no slide - the row is either off or on. */
.entry:hover,
.entry:focus-within {
  background: var(--ink);
  color: var(--paper);
}

.entry:hover .entry__num,
.entry:hover .entry__sub,
.entry:hover .meta,
.entry:focus-within .entry__num,
.entry:focus-within .entry__sub,
.entry:focus-within .meta { color: var(--paper); }

.entry:hover .tag,
.entry:focus-within .tag {
  font-family: var(--font-mono);
  /* 9px. Small, but these are secondary labels beside a title that already
     names the thing, not copy anyone has to read to understand the tile.
     The inks still clear 4.5:1, so shrinking costs legibility, not contrast. */
  font-size: 0.34375rem;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.08rem 0.2rem;
  white-space: nowrap;
}

.entry__num { color: var(--ink-3); font-size: 0.75rem; }

.entry__title {
  font-size: clamp(1.25rem, 2.6vw, 1.9rem);
  letter-spacing: -0.02em;
  line-height: 1.1;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}

/* The index number does the pointing; nothing needs to slide in. */
.entry__title .arrow { display: none; }

.entry__sub {
  margin-top: 0.45rem;
  color: var(--ink-3);
  font-size: 0.9375rem;
  max-width: 44ch;
}

.entry__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  justify-content: flex-end;
  max-width: 20rem;
}

.tag {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.16rem 0.38rem;
  white-space: nowrap;
}

/* ---- Tag colour families ---------------------------------
   The one place colour enters the interface. It is safe here
   because it is REDUNDANT: the chip already reads "Live" or
   "Tableau", so hue reinforces a label rather than carrying
   meaning on its own, which is what WCAG 1.4.1 asks.

   Derived, not picked. Every ink clears 4.5:1 against BOTH its
   own fill and the page - so a label stays readable even if the
   fill never paints. Measured: text/chip 5.0-5.1, text/page
   5.4-6.2 in light, 6.0-6.2 in dark.

   The three families sit at matched lightness, so they are
   deliberately NOT separable in greyscale. That is the correct
   trade here - they are siblings, and the word does the work.
   Anywhere colour is the only channel, use the tile ladder
   instead, which is built to survive greyscale.            */

:root {
  --tag-live-fill: #defaed;  --tag-live-line: #addcc7;  --tag-live-ink: #037757;
  --tag-tool-fill: #d6ebfe;  --tag-tool-line: #b1d6f5;  --tag-tool-ink: #036793;
  --tag-stat-fill: #ffe2d8;  --tag-stat-line: #f6c7b8;  --tag-stat-ink: #a14425;
}

:root[data-theme="dark"] {
  --tag-live-fill: #0b261c;  --tag-live-line: #133e2f;  --tag-live-ink: #08a479;
  --tag-tool-fill: #062232;  --tag-tool-line: #0d3b53;  --tag-tool-ink: #2597d1;
  --tag-stat-fill: #301a12;  --tag-stat-line: #502e22;  --tag-stat-ink: #d07657;
}

.tag--live {
  background: var(--tag-live-fill);
  border-color: var(--tag-live-line);
  color: var(--tag-live-ink);
}

.tag--tool {
  background: var(--tag-tool-fill);
  border-color: var(--tag-tool-line);
  color: var(--tag-tool-ink);
}

.tag--status {
  background: var(--tag-stat-fill);
  border-color: var(--tag-stat-line);
  color: var(--tag-stat-ink);
}

/* Chart type leads every tile, so it is the one facet that carries weight
   rather than hue - a fifth colour would turn the grid into confetti, and
   monochrome keeps the emphasis where the eye lands first. */
.tag--type {
  color: var(--ink);
  border-color: var(--ink);
  font-weight: 700;
}

/* Local AI. Violet reads as the machine-learning family by convention and
   sits well clear of the other three inks - minimum ΔE 21.4 in light,
   26.3 in dark, so it never reads as a shade of the tool blue. */
:root {
  --tag-ai-fill: #f0e7fe;  --tag-ai-line: #d9ccf0;  --tag-ai-ink: #6c54aa;
}
:root[data-theme="dark"] {
  --tag-ai-fill: #251e31;  --tag-ai-line: #3c324f;  --tag-ai-ink: #9d81dc;
}

.tag--ai {
  background: var(--tag-ai-fill);
  border-color: var(--tag-ai-line);
  color: var(--tag-ai-ink);
}

/* Full-card link target without nesting interactive elements */
.entry__link::before {
  content: "";
  position: absolute;
  inset: 0;
}

@media (max-width: 46rem) {
  .entry { grid-template-columns: 2.25rem minmax(0, 1fr); }
  .entry__tags { grid-column: 2; justify-content: flex-start; max-width: none; }
}

/* ---- 7b. Work tiles --------------------------------------
   A grid, not a reading list. Each thumbnail is a vector
   reduction of the actual work rather than a screenshot.   */

/* Both ladders are verified: every pair clears ΔE 10 in normal vision, all
   three CVD types and greyscale, and every colour clears 3:1 against the tile
   background it sits on. Light measures ΔE 10.2 minimum, dark ΔE 10.3 - the
   binding pair is greyscale in both, as it usually is.

   --tile-4 is the tightest colour on the site at 3.03:1 against --paper-2, the
   thumbnail surface these marks sit on, and that is forced
   rather than careless: a 3:1 floor on light paper caps a usable series at
   about L* 58, and the full L* 16-58 span is needed for four series to stay
   apart in greyscale. Re-deriving all four jointly buys ΔE 10.4 instead of
   10.2 - not worth changing four colours for. Anything lighter here fails
   contrast; anything darker collapses into --tile-3 in greyscale. */

:root {
  --tile-1: #003353;
  --tile-2: #a1002a;
  --tile-3: #107c7f;
  --tile-4: #c87900;
}

:root[data-theme="dark"] {
  --tile-1: #1372b0;
  --tile-2: #fc5562;
  --tile-3: #21c9ce;
  --tile-4: #fedab0;
}

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
}

/* Fixed column counts - galleries that sit beside each other need to read
   as matching blocks rather than reflowing independently. */
.tiles--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.tiles--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.tiles--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }


@media (max-width: 74rem) {
  .tiles--4 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 64rem) {
  .tiles--3, .tiles--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 30rem) {
  .tiles--2, .tiles--3, .tiles--4 { grid-template-columns: minmax(0, 1fr); }
}

.tile {
  position: relative;
  background: var(--paper);
  display: flex;
  flex-direction: column;
  transition: background 110ms linear, box-shadow 160ms ease;
}

/* Hover lifts the card off the page instead of inverting it. The earlier
   dark fill forced every label to white and flattened the tag families to
   plain outlines, so the one place colour is allowed to carry meaning went
   grey exactly when someone was paying attention to it. A tint plus a lift
   keeps the ink, the hues and every contrast ratio the page was verified
   against, and reads as clearly as the slab did. */
.tile:hover, .tile:focus-within {
  background: var(--hover);
  box-shadow: var(--hover-lift);
}
.tile__thumb {
  aspect-ratio: 16 / 10;
  border-bottom: 1px solid var(--rule);
  overflow: hidden;
  background: var(--paper-2);
  transition: border-color 90ms linear;
}

.tile__thumb svg { width: 100%; height: 100%; display: block; }

/* Video thumbs. The Thunderhill capture carries a black window surround and
   a Tableau tab bar; scaling past the frame and nudging up drops both and
   leaves the plot centred. Poster and video share the transform so the
   hand-off when playback starts is invisible. */
.tile__thumb video {
  width: 100%;
  height: 100%;
  /* Contain, not cover: show the recording whole rather than cropping to
     hide its black surround - every attempt at that clipped the chart title
     or the value axis. The source is 1.548:1 against a 1.6:1 tile, so the
     letterbox is ~3% and disappears against the matched black ground. */
  object-fit: contain;
  display: block;
  background: #000;
}


/* Real screenshots fill the thumb; vector stand-ins sit on the panel tint. */
.tile__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}

/* NOTE: this must stay below `.tile__thumb img` - same specificity, so
   whichever comes last wins. Placed above it, the cover rule silently
   overrode contain and these tiles kept cropping. */
/* THE RULE, so the grid does not look accidental: captured software - app
   windows, dashboards, recordings - is contained on white with an inset, so
   it reads as a screenshot presented on the panel. Device mockups and
   photography fill the tile edge to edge, because insetting a photograph
   just looks like a mistake. Sorting by what the picture IS keeps the two
   treatments legible rather than arbitrary.

   App captures sit as objects on the panel rather than bleeding to the tile
   edge. Contain shows the window whole; the inset stops it touching the
   border, so a 1.3:1 screenshot in a 1.6:1 slot reads as deliberate framing
   instead of a crop that went wrong. */
.tile__thumb--contain { background: #fff; }

.tile__thumb--contain img {
  object-fit: contain;
  object-position: center;
  padding: 2%;
}

/* Marks a tile whose thumbnail is an illustration, not a capture. */
.tile--novisual .tile__thumb { display: grid; place-items: center; }
.tile--novisual .tile__thumb svg { width: 78%; height: 78%; }


.tile__body {
  padding: 1.1rem clamp(1rem, 2vw, 1.35rem) 1.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  flex: 1;
}

.tile__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

/* The year/status label sits beside the title but should not compete with
   it - a step down from the base .meta size keeps it as an annotation. */
.tile__head .meta { font-size: 0.625rem; flex: 0 0 auto; }

.tile__title {
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.018em;
  line-height: 1.2;
}

.tile__sub {
  font-size: 0.875rem;
  color: var(--ink-3);
  line-height: 1.45;
}

/* Case-study header pills. Same vocabulary as the index tiles, so a project
   carries one identity across the site instead of a tile of pills and a
   separate table of fields saying the same thing more slowly. */
.case-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: var(--gap-m);
}

.tile__tags { display: flex; flex-wrap: wrap; gap: 0.2rem; margin-top: auto; padding-top: 0.7rem; }

.tile__link::before { content: ""; position: absolute; inset: 0; }

/* Reserved slot. Holds the grid square until real work fills it - visibly
   empty rather than a fake card, and skipped by assistive tech. */
.tile--empty { background: var(--paper); }
.tile--empty:hover { background: var(--paper); color: inherit; }
.tile--empty .tile__thumb {
  background: transparent;
  background-image: repeating-linear-gradient(
    45deg, var(--rule-soft) 0 7px, transparent 7px 14px);
  border-bottom-color: var(--rule-soft);
}
.tile--empty .tile__body { min-height: 5.5rem; }

/* Past work has no live data behind it yet - a ruled field holds the
   slot until real assets land, rather than a fake image. */
.tile--quiet .tile__thumb {
  background-image: repeating-linear-gradient(
    45deg, var(--rule-soft) 0 8px, transparent 8px 16px);
}

/* ---- 7c. Live embeds -------------------------------------
   The real tool, running in the page. Sandboxed in an iframe
   so its own stylesheet can't leak into the portfolio's.    */

.embed {
  border: 1px solid var(--rule);
  background: var(--paper-2);
  margin-block: var(--gap-m);
}

.embed__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-s);
  flex-wrap: wrap;
  padding: 0.6rem 0.85rem;
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}

.embed__frame {
  position: relative;
  height: clamp(30rem, 76vh, 46rem);
  background: var(--paper-2);
}

.embed__frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* Fit-to-width variant.

   Both embedded tools are laid out for one viewport size - no media
   queries, several position:fixed panels - so narrowing the frame clips
   them rather than reflowing. Instead the iframe is given its natural
   size and scaled down to whatever width is available, so the whole
   interface is always visible and nothing is cut off. Interaction still
   works normally; only the rendered scale changes. */
.embed--fit .embed__frame {
  height: auto;
  overflow: hidden;
}

.embed--fit .embed__frame iframe {
  width: var(--embed-w, 1440px);
  height: var(--embed-h, 900px);
  transform: scale(var(--embed-scale, 1));
  transform-origin: top left;
}

/* Full-bleed: escape the reading column so the scale factor stays high. */
.embed--bleed {
  width: min(96vw, 92rem);
  margin-inline: calc(50% - min(48vw, 46rem));
}

@media (max-width: 46rem) {
  .embed--bleed { width: 100%; margin-inline: 0; }
}

/* Shown until the iframe reports load - these are heavy documents. */
.embed__loading {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  transition: opacity var(--t-med) linear;
}

.embed[data-loaded="true"] .embed__loading { opacity: 0; }

.embed__note {
  padding: 0.65rem 0.85rem;
  border-top: 1px solid var(--rule);
  background: var(--paper);
  font-size: 0.8125rem;
  color: var(--ink-3);
  line-height: 1.5;
}

@media (max-width: 46rem) {
  /* Both tools are laid out for a desktop viewport; below that, send
     people to the full-screen version rather than a squeezed frame. */
  .embed__frame { height: 26rem; }
}

/* ---- 8. Tool cards --------------------------------------- */

.grid-tools {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15.5rem, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
}

.tool {
  background: var(--paper);
  padding: var(--gap-m) clamp(1.25rem, 2.5vw, 1.75rem);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-height: 12rem;
  transition: background var(--t-med) var(--ease);
  position: relative;
}

.tool:hover { background: var(--paper-2); }
.tool__name { font-size: 1.0625rem; font-weight: 700; letter-spacing: -0.02em; }
.tool__desc { font-size: 0.875rem; color: var(--ink-3); line-height: 1.5; }
.tool__foot { margin-top: auto; padding-top: var(--gap-s); }

/* ---- 9. Case study layout -------------------------------- */

.case-hero { padding-block: clamp(4rem, 12vw, 9rem) var(--gap-l); }

.case-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--gap-m);
  padding-block: var(--gap-m);
  border-block: 1px solid var(--rule);
}

.fact__label { margin-bottom: 0.3rem; }
.fact__value { font-size: 0.9375rem; color: var(--ink); line-height: 1.4; }

/* Prose column for the written case study */
.prose { max-width: var(--measure); }
.prose > * + * { margin-top: 1.15em; }
.prose h2 {
  font-size: clamp(1.35rem, 2.2vw, 1.7rem);
  letter-spacing: -0.017em;
  line-height: 1.24;
  margin-top: 2.6em;
  margin-bottom: 0.1em;
}
/* The section flag sits close above its heading, not a full block away. */
.prose .meta + h2 { margin-top: 0.55em; }
.prose h3 {
  font-size: 1.0625rem;
  letter-spacing: -0.015em;
  margin-top: 2em;
}
.prose p { color: var(--ink-2); }
.prose strong { color: var(--ink); font-weight: 700; }
.prose ul { margin: 0; padding-left: 1.1rem; color: var(--ink-2); }
.prose li + li { margin-top: 0.5em; }

/* A pulled-out claim - used sparingly, for the decision that
   actually mattered in the project. */
.pull {
  border-left: 2px solid var(--ink);
  padding-left: 1.25rem;
  font-size: clamp(1.1rem, 1.7vw, 1.3rem);
  line-height: 1.38;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin-block: 2.2em;
  max-width: 32rem;
}

/* Mark grid - logos shown as a set, two up, each in its own frame. */
.marks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--gap-s);
}
.marks figure { margin: 0; }
.marks .figure__frame { aspect-ratio: 4 / 3; }
.marks .figure__frame img { width: 100%; height: 100%; object-fit: cover; }
.marks figcaption {
  margin-top: 0.6rem;
  font-size: 0.8125rem;
  color: var(--ink-3);
  line-height: 1.5;
}
.marks figcaption b { color: var(--ink-2); font-weight: 600; }

/* Figure with mono caption, newsroom style */
figure { margin: 0; }
.figure { margin-block: var(--gap-l); }
.figure__frame {
  border: 1px solid var(--rule);
  background: var(--paper-2);
  overflow: hidden;
}
.figure figcaption {
  margin-top: 0.75rem;
  font-size: 0.8125rem;
  color: var(--ink-3);
  line-height: 1.5;
  max-width: 46ch;
}
.figure figcaption b {
  color: var(--ink-2);
  font-weight: 700;
}

/* ---- 10. Live chart shell -------------------------------- */

.viz {
  border: 1px solid var(--rule);
  background: var(--paper);
  padding: clamp(1rem, 3vw, 2rem);
}

.viz__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-s);
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
}

.viz__title { font-size: 0.9375rem; font-weight: 700; letter-spacing: -0.015em; }
.viz__note { font-size: 0.8125rem; color: var(--ink-3); margin-top: 0.2rem; }

.viz figcaption { margin-top: 1rem; }

/* Data-source honesty line - every live chart carries one. */
.source {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-top: 0.9rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--rule-soft);
}

/* Reconstructed / placeholder data must announce itself. Greyscale only - weight and a glyph carry the signal, never hue. */
.source[data-placeholder="true"] {
  color: var(--ink-2);
  font-weight: 700;
  border-top-color: var(--ink-4);
}

.source[data-placeholder="true"]::before {
  content: "▲ ";
}

/* ---- 12. Footer / contact -------------------------------- */

.contact-line {
  font-size: clamp(1.6rem, 4.5vw, 3rem);
  letter-spacing: -0.024em;
  line-height: 1.06;
  font-weight: 700;
  display: inline-block;
  position: relative;
}

.contact-line::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0.06em;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--t-med) var(--ease);
}

.contact-line:hover::after { transform: scaleX(1); }

.colophon {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-s) var(--gap-m);
  /* Only the link nav lives here now, so flex-end keeps it on the right
     rather than collapsing to the left the way space-between would. */
  justify-content: flex-end;
  padding-block: var(--gap-m);
  border-top: 1px solid var(--rule);
}

/* ---- 13. Reveal on scroll -------------------------------- */

/* Elements arrive; they don't drift in. Short, flat, almost mechanical - a long floaty fade-up is the house style of every template on the web. */
[data-reveal] {
  opacity: 0;
  transform: translateY(5px);
  transition:
    opacity 200ms linear,
    transform 200ms cubic-bezier(0.2, 0.7, 0.3, 1);
  transition-delay: var(--reveal-delay, 0ms);
}

[data-reveal][data-shown="true"] { opacity: 1; transform: none; }

/* ---- 14. Reduced motion ---------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  [data-reveal] { opacity: 1; transform: none; }
  .step { opacity: 1; }
}

/* ---- 15. Utilities --------------------------------------- */

.muted { color: var(--ink-3); }
.tight { max-width: var(--measure); }

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: 1rem; top: 1rem;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  padding: 0.6rem 1rem;
  border-radius: 0;
}

/* ---- 16. Locked section ----------------------------------
   Client work that can't be shown openly. The card states what
   is behind it; the page itself decrypts on the right password
   rather than merely hiding markup. */

.locked {
  display: flex;
  gap: clamp(1rem, 3vw, 2rem);
  align-items: flex-start;
  border: 1px solid var(--rule);
  border-left: 2px solid var(--ink);
  background: var(--paper-2);
  padding: clamp(1.25rem, 3vw, 2rem);
}

.locked__mark { flex: none; color: var(--ink-3); padding-top: 0.15rem; }
.locked__mark svg { width: 26px; height: 26px; }
.locked__title { font-size: 1.0625rem; letter-spacing: -0.018em; }
.locked__sub {
  margin-top: 0.5rem;
  max-width: 52ch;
  font-size: 0.9375rem;
  color: var(--ink-3);
  line-height: 1.5;
}

/* Gate shown in place of the protected page */
.gate { max-width: 30rem; }
.gate__row { display: flex; gap: 0.5rem; margin-top: 1.25rem; flex-wrap: wrap; }

.gate input[type="password"] {
  flex: 1 1 14rem;
  font: inherit;
  font-size: 0.9375rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--ink-4);
  border-radius: 0;
  background: var(--paper);
  color: var(--ink);
}

.gate input[type="password"]:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

.gate button {
  font: inherit;
  font-size: 0.8125rem;
  padding: 0.6rem 1rem;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  cursor: pointer;
  border-radius: 0;
}

.gate__msg { margin-top: 0.75rem; min-height: 1.2rem; font-size: 0.875rem; color: var(--ink-2); }
.gate__msg[data-state="bad"] { color: var(--ink); font-weight: 700; }
.gate__msg[data-state="bad"]::before { content: "✕ "; }

/* ---- 14. Awaiting-copy placeholder ------------------------ */

/* Marks a section whose text the author still has to supply. Deliberately
   looks unfinished so it cannot be mistaken for published copy. */
.impact-todo {
  border: 1px dashed var(--rule);
  background-image: repeating-linear-gradient(
    45deg, var(--rule-soft) 0 8px, transparent 8px 16px);
  padding: 1rem 1.1rem;
  color: var(--ink-3);
  font-size: 0.875rem;
}
.impact-todo::before {
  content: "To fill";
  display: inline-block;
  margin-right: 0.6rem;
  padding: 0.1rem 0.4rem;
  border: 1px solid var(--rule);
  background: var(--paper);
  font-size: 0.625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}


/* ---- Editorial page gallery -------------------------------
   A weekly page is a body of work, not a series of one-offs, so
   these sit in a grid at a size you can read the headline from
   rather than as a stack of full-width figures.             */

.pages {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--gap-m) var(--gap-s);
  margin-top: var(--gap-l);
}

.pages figure { margin: 0; }

.pages img {
  width: 100%;
  border: 1px solid var(--rule);
  background: #fff;
}

.pages figcaption {
  margin-top: 0.55rem;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--ink-3);
}

.pages figcaption b { color: var(--ink-2); font-weight: 600; display: block; }


/* ---- Rail folds to a bar --------------------------------- */

@media (max-width: 64rem) {
  body { padding-left: 0; }

  /* No room for the full-bleed mark in a 4rem bar, and its negative-margin
     crop breaks at these widths - the nameplate text leads instead. */
  .rail__mark { display: none; }

  /* Compact one-line nameplate for the bar. */
  .railmark__name { font-size: 1.05rem; line-height: 1.1; white-space: nowrap; }
  .railmark__role { display: none; }

  .masthead {
    position: sticky;
    inset: 0 0 auto 0;
    width: auto;
    border-right: 0;
    border-bottom: 1px solid transparent;
    transition: box-shadow var(--t-med) var(--ease),
                border-color var(--t-med) var(--ease);
  }

  .masthead .shell { padding-inline: clamp(1.25rem, 5vw, 3.5rem); }

  .masthead__inner {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: clamp(2rem, 4vw, 2.75rem);
    height: 4rem;
    padding-block: 0;
  }

  .masthead .nav {
    flex: 0 1 auto;
    flex-direction: row;
    align-items: center;
    gap: clamp(0.85rem, 2.5vw, 1.75rem);
    width: auto;
  }

  .masthead .nav a { font-size: 0.9438rem; line-height: 1.4; }
  .masthead .nav--social { margin-top: 0; }
  .masthead .theme-toggle { margin-top: 0; margin-left: 0.25rem; }

  :target, section[id] { scroll-margin-top: 5.5rem; }
}


/* ---- "Right now" feature ---------------------------------
   Replaces the old nameplate. The rail already carries the
   name, so repeating it at 4rem said nothing twice; a single
   current piece of work says more and gets the reader into
   the grid faster.                                        */

.now { margin: 0; }

.now__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-m);
  padding-bottom: 0.85rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--gap-m);
}

.now__frame {
  border: 1px solid var(--rule);
  background: var(--paper-2);
  overflow: hidden;
  aspect-ratio: 16 / 7;
}

.now__frame img { width: 100%; height: 100%; object-fit: cover; display: block; }

.now figcaption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1.25rem;
  margin-top: 1rem;
}

.now__title {
  font-size: clamp(1.15rem, 2vw, 1.5rem);
  font-weight: 700;
  letter-spacing: -0.024em;
}

.now__note { color: var(--ink-3); font-size: 0.9375rem; max-width: 46ch; }

/* Pills sit on their own line under the caption; the tile row's
   margin-top: auto is meaningless outside a flex column. */
.now__tags { flex-basis: 100%; margin-top: 0.25rem; padding-top: 0; }

@media (max-width: 46rem) {
  .now__frame { aspect-ratio: 4 / 3; }
}


/* ---- Carousel strip ------------------------------------------
   For bodies of work too large to stack. Scroll-snap does the
   real work; the buttons are a convenience layered on top, so
   without JS the strip is still a plain scrollable row. Slides
   are height-capped so portrait pages and wide spreads share
   one line. */

.car { position: relative; }

.car__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.85rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 1rem;
}

.car__meta { display: flex; align-items: baseline; gap: 1.1rem; }

.car__track {
  display: flex;
  gap: 0.9rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  padding: 2px 2px 0.9rem;
  scrollbar-width: thin;
  scrollbar-color: var(--ink-4) transparent;
}
.car__track::-webkit-scrollbar { height: 5px; }
.car__track::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 3px; }
.car__track::-webkit-scrollbar-track { background: transparent; }

.car__item { flex: 0 0 auto; scroll-snap-align: start; }

.car__item img {
  height: clamp(15rem, 32vw, 24rem);
  width: auto;
  display: block;
  border: 1px solid var(--rule);
  background: #fff;
}

/* Slides that name what they are: a caption sits under the frame and is
   held to the slide's own width so the strip keeps its rhythm. */
.car__item figure { margin: 0; }
.car__item figcaption {
  margin-top: 0.55rem;
  font-size: 0.8125rem;
  color: var(--ink-3);
  line-height: 1.45;
  max-width: 22rem;
}
.car__item figcaption b { color: var(--ink-2); font-weight: 600; }

.car__btns { display: flex; gap: 0.4rem; }

.car__btn {
  width: 1.9rem;
  height: 1.9rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: var(--paper);
  color: var(--ink-2);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.car__btn svg { width: 12px; height: 12px; }
.car__btn:hover { color: var(--ink); border-color: var(--ink-3); }
.car__btn:disabled { opacity: 0.3; cursor: default; }
