/* Shared by index.html and artefacts.html: tokens, base type and grid, the
   sticky header with its theme switch, the case rail layout, the footer and
   the paper grain. Anything only one page uses stays in that page. */

/* FONT-STACK */
:root {
  --display-font: "Fraunces", Georgia, serif;
  --body-font: "Newsreader", Georgia, "Times New Roman", serif;
  --body-size: 19px;
  --body-leading: 1.6;
  --label-font: "Segoe UI", Helvetica, Arial, sans-serif;
}
/* Dark for everyone. Light is opt-in via the toggle, which sets
   data-theme="light" on <html> and remembers it in localStorage. There is
   deliberately no prefers-color-scheme rule: a device set to light would
   otherwise override the choice this page is making. */
:root {
  --ink: #e9eaec;
  --steel: #99a2a9;
  --line: #333a40;
  --accent: #71c294;
  --rule: var(--accent);
  --bg: #16191c;
  --route: var(--accent);
  --time: #99a1ab;
  --slow: #8b939d;
  --label: var(--accent);
  --after: var(--ink);
  --focus: var(--accent);
  /* the mat behind a screenshot, and how much the screenshot is calmed down */
  --mat: #1c2025;
  --shot-dim: 0.88;
}
:root[data-theme="light"] {
  --ink: #15181c;
  --steel: #6b7280;
  --line: #dcdfe3;
  --accent: #1c5a3a;
  --bg: #ffffff;
  --time: #5f6771;
  --slow: #7a828c;
  --after: var(--ink);
  /* In light the mat is a hair off-white: enough to separate the screenshot
     from the page without turning it back into a card. No dimming: there is
     no glare to take off a light screenshot on a light page. */
  --mat: #f4f5f6;
  --shot-dim: 1;
}
/* THEME-END */
:root {
  --page-max: 1400px;
  --side: 210px;
  --main: 640px;
  --gutter: clamp(28px, 4.5vw, 68px);
  --edge-l: clamp(20px, 9vw, 168px);
  --edge-r: clamp(20px, 4vw, 56px);
}
* { box-sizing: border-box; }
body {
  /* Past 1400px the grid stops growing and centres, so the asymmetry stays
     inside a fixed measure instead of stranding the text in the left quarter
     of a wide window while the rules run the full viewport. */
  max-width: var(--page-max);
  margin: 0 auto;
  background: var(--bg);
  color: var(--ink);
  font: var(--body-size)/var(--body-leading) var(--body-font);
  padding: 0 var(--edge-r) 0 var(--edge-l);
}
h1, h2, h3 {
  font-family: var(--display-font);
  font-optical-sizing: auto;
  line-height: 1.12;
  margin: 0;
  font-weight: 600;
}
/* Sized just above the case study headings, not far above them: the work
   should carry the emphasis, and the name only has to read as the top of
   the page. The artefacts page's title uses the same size. */
h1 {
  font-size: clamp(1.9rem, 2.6vw, 2.2rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.04;
}
p { margin: 0 0 1em; }
p:last-child { margin-bottom: 0; }
a { color: var(--route); text-underline-offset: 3px; }
a:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }

/* Two-track band. Headings and labels sit left, prose sits right. */
.band {
  display: grid;
  grid-template-columns: minmax(120px, var(--side)) minmax(0, var(--main));
  column-gap: var(--gutter);
}
.band > .side { grid-column: 1; text-align: left; }
.band > .main { grid-column: 2; }
.band > .full { grid-column: 1 / -1; }
.rule-top { border-top: 2px solid var(--rule); margin-top: clamp(48px, 8vh, 90px); }

/* ---- header ---- */
/* Only the name line is pinned (name and LinkedIn on the left, email and the
   theme switch on the right): the tagline and intro would cover most of a
   phone screen if they stayed too. The space above the name is a margin,
   not padding, so it scrolls away with the page instead of being pinned.
   The negative side margins carry the solid background out over the body's
   edge padding, so text can't show beside the bar either. */
header {
  position: sticky; top: 0; z-index: 10;
  background: var(--bg);
  margin: calc(clamp(56px, 11vh, 118px) - 14px) calc(-1 * var(--edge-r)) 0 calc(-1 * var(--edge-l));
  padding: 14px var(--edge-r) 18px var(--edge-l);
  transition: padding 0.25s ease, box-shadow 0.25s ease;
}
/* The same grain as the page, so the bar doesn't read as a darker strip. */
header::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  opacity: var(--grain); background-image: var(--grain-img);
}
header.scrolled {
  padding-top: 9px; padding-bottom: 9px;
  box-shadow: 0 1px 0 var(--line), 0 6px 16px rgba(0, 0, 0, 0.18);
}
/* The name. On the front page it is the h1; on the artefacts page, where the
   page has its own title, it is a plain line. Both get the same look. */
.hdr-name {
  font-family: var(--display-font);
  font-optical-sizing: auto;
  margin: 0;
  font-size: clamp(1.9rem, 2.6vw, 2.2rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.04;
  transition: font-size 0.25s ease;
}
header.scrolled .hdr-name { font-size: calc(clamp(1.9rem, 2.6vw, 2.2rem) * 0.62); }

/* The name starts flush with the headings' left edge, full size or shrunk;
   the second track runs to the right edge for the email and switch. */
header.band {
  grid-template-columns: minmax(120px, var(--side)) minmax(0, 1fr);
  align-items: center;
}
header .side { display: flex; align-items: baseline; justify-content: flex-start; }
header .side .hdr-name { white-space: nowrap; }
/* The icon takes the name's font size, so it scales and shrinks with it.
   Its bottom sits on the name's baseline; the ::after widens the tap area
   without moving anything. */
.hdr-in {
  position: relative; margin-left: 0.28em; color: var(--ink);
  font-size: clamp(1.9rem, 2.6vw, 2.2rem); line-height: 1;
  transition: font-size 0.25s ease;
}
.hdr-in svg { display: block; width: 0.6em; height: 0.6em; fill: currentColor; }
/* The left edge of the tap area stops short of the name, so the two links
   never overlap. */
.hdr-in::after { content: ""; position: absolute; inset: -8px -8px -8px -4px; }
/* The name is a link (back to the top on the front page, home from the
   artefacts page) but keeps the heading's look: no colour or underline, just
   a slight fade on hover. Keyboard focus uses the site's usual outline. */
.hdr-top { color: inherit; text-decoration: none; transition: opacity 0.15s ease; }
.hdr-top:hover { opacity: 0.75; }
/* Set only while the name's back-to-top scroll runs: the header growing back
   near the top would otherwise make the browser push the page down again,
   so it stopped short of the top. */
html.to-top, html.to-top body { overflow-anchor: none; }
.hdr-in:hover { color: var(--route); }
header.scrolled .hdr-in { font-size: calc(clamp(1.9rem, 2.6vw, 2.2rem) * 0.62); }
.hdr-end {
  grid-column: 2; justify-self: end;
  display: flex; align-items: center; gap: 22px;
}
.hdr-mail { display: inline-flex; align-items: center; font-size: 0.92rem; white-space: nowrap; }
.hdr-mail[hidden] { display: none; }
.hdr-mail svg {
  display: none; width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
}

/* Theme switch. A real button with role="switch", so Enter and Space work
   and it sits in the tab order; aria-checked carries the state and the label
   stays "Light theme" rather than swapping under the user. Square-cornered
   like everything else on the page. The mark for the current theme brightens. */
.themeswitch {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px; margin: -4px; border: 0; background: transparent;
  color: var(--steel); cursor: pointer;
}
.themeswitch svg { display: block; width: 13px; height: 13px; }
.ts-track {
  position: relative; display: block; width: 34px; height: 18px;
  border: 1px solid var(--steel);
}
.ts-thumb {
  position: absolute; top: 2px; left: 2px; width: 12px; height: 12px;
  background: var(--steel); transition: transform 0.2s ease;
}
.themeswitch[aria-checked="true"] .ts-thumb { transform: translateX(16px); background: var(--ink); }
.themeswitch[aria-checked="false"] .ts-moon,
.themeswitch[aria-checked="true"] .ts-sun { color: var(--ink); }
.themeswitch:hover .ts-track { border-color: var(--ink); }
.themeswitch:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }

@media (max-width: 860px) {
  header.band { grid-template-columns: auto minmax(0, 1fr); }
}
/* The full address doesn't fit beside the name on a phone, so it becomes an
   envelope; the link keeps the address as its accessible name. */
@media (max-width: 600px) {
  .hdr-mail-text { display: none; }
  .hdr-mail svg { display: block; transition: width 0.25s ease, height 0.25s ease; }
  header.scrolled .hdr-mail svg { width: 18px; height: 18px; }
  .hdr-mail { color: var(--ink); }
  .hdr-end { gap: 18px; }
}
@media (max-width: 360px) {
  .hdr-end { gap: 12px; }
  .themeswitch { gap: 4px; }
}
@media (prefers-reduced-motion: reduce) { header, .hdr-name, .hdr-in, .hdr-top, .ts-thumb { transition: none; } }
@media print {
  header { position: static; box-shadow: none; }
  header.scrolled .hdr-name, header.scrolled .hdr-in { font-size: clamp(1.9rem, 2.6vw, 2.2rem); }
  .themeswitch { display: none; }
}

/* ---- case rails ---- */
/* On desktop each case is a rail and a story. The rail (label, heading, key
   points) is wider than the page's side column so the points read as lines
   rather than a narrow ragged strip. It pins below the shrunk header while
   its case is on screen and lets go at the end of the case; the script only
   adds .pin when the rail fits in the window, so a tall one just scrolls. */
@media (min-width: 861px) {
  .study { grid-template-columns: clamp(210px, 24vw, 300px) minmax(0, var(--main)); }
  .study > .side { align-self: start; }
  .study > .side.pin { position: sticky; top: calc(var(--hdr-h, 42px) + 28px); }
}

/* ---- footer ---- */
footer {
  border-top: 2px solid var(--rule);
  margin-top: clamp(56px, 9vh, 100px);
  padding: 26px 0 80px;
  color: var(--steel);
  font-size: 0.88rem;
}
footer p { max-width: 62ch; }
/* The footer's only line: copyright, and the note on screenshots and code. */
.copyright { margin-top: 0; font-size: 0.8rem; line-height: 1.5; color: var(--steel); }

/* ---- narrow screens: one column ---- */
@media (max-width: 860px) {
  :root { --edge-l: clamp(18px, 5vw, 32px); }
  body { font-size: 18px; }
  .band { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
  .band > .side, .band > .main, .band > .full { grid-column: 1; text-align: left; }
}

/* Paper grain. Generated by an SVG filter in a data URI, so nothing external
   loads: about 498 bytes of CSS, no request. It sits on a fixed layer behind
   the content rather than over it, so text contrast is untouched, and the
   opacity is a theme token because the same grain reads much stronger on
   white than on charcoal. */
:root {
  --grain: 0.055;
  --grain-img: url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27180%27%20height%3D%27180%27%3E%3Cfilter%20id%3D%27n%27%20x%3D%270%27%20y%3D%270%27%3E%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%270.82%27%20numOctaves%3D%273%27%20stitchTiles%3D%27stitch%27%2F%3E%3CfeColorMatrix%20type%3D%27saturate%27%20values%3D%270%27%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%27180%27%20height%3D%27180%27%20filter%3D%27url%28%23n%29%27%2F%3E%3C%2Fsvg%3E");
}
:root[data-theme="light"] { --grain: 0.025; }
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain);
  background-image: var(--grain-img);
  background-repeat: repeat;
}
@media print { body::before, header::before { display: none; } }
