/* =====================================================================
   Markus Lunde — Portfolio
   Plain HTML/CSS/JS · Bento-grid layout
   Built on the design system in "Tokens" below: every size, space, radius,
   colour and timing comes from a named value there. design-system.html
   shows them all on one page.
   ===================================================================== */

/* ----------------------------- Fonts ------------------------------- */
/* Served from the site itself (assets/fonts/), not Google: the same files
   Google Fonts serves, so the type looks the same, without visitors'
   browsers contacting Google. Both are variable fonts: one file covers every
   weight (and Archivo's width), split into basic Latin (with æøå) and
   extended Latin; a browser only downloads the part a page uses.
   Archivo: Copyright 2020 The Archivo Project Authors
   (https://github.com/Omnibus-Type/Archivo). Figtree: Copyright 2022 The
   Figtree Project Authors (https://github.com/erikdkennedy/figtree). Both
   are licensed under the SIL Open Font License, Version 1.1
   (https://openfontlicense.org). */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100% 125%;
  font-display: swap;
  src: url("../assets/fonts/archivo-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100% 125%;
  font-display: swap;
  src: url("../assets/fonts/archivo-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("../assets/fonts/figtree-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("../assets/fonts/figtree-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ----------------------------- Tokens ------------------------------ */
:root {
  /* Light theme: graphite, matched to the M logo (its near-black #1a1a1a on
     white and greys). No accent colour: emphasis is weight and black / white,
     and the brand tiles and project covers bring the colour. */
  --bg: #efefef;
  --card: #ffffff;
  --card-2: #f4f4f4;
  --ink: #1a1a1a;             /* the logo's black */
  --ink-soft: #646464;
  --line: rgba(0, 0, 0, 0.09);
  --accent: #1a1a1a;          /* emphasis (dots, pin, focus, buttons): ink for now; set a colour here to re-brand */
  --accent-soft: #e4e4e4;     /* halos and rings around --accent */
  --glass: rgba(255, 255, 255, 0.78);  /* translucent --card for frosted controls */
  --live: #2fbf71;            /* "now" and "open to new work" */
  --live-soft: rgba(47, 191, 113, 0.22);
  --linkedin: #0a66c2;                 /* LinkedIn brand blue */
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 6px 16px rgba(0, 0, 0, 0.07);
  --shadow-hover: 0 3px 8px rgba(0, 0, 0, 0.08), 0 12px 26px rgba(0, 0, 0, 0.11);
  /* About twice the tile shadow, so the popup's shadow matches the tile's at
     the start and end of the grow animation, when it's scaled to tile size */
  --shadow-sheet: 0 2px 5px rgba(0, 0, 0, 0.07), 0 14px 36px rgba(0, 0, 0, 0.16);
  /* Live tiles wear their service's own light / dark colours.
     Hevy light: measured on hevy.com. */
  --hv-bg: #fff;
  --hv-ink: #191a1b;
  --hv-muted: #6c7278;          /* Hevy's grey (#83898f), darkened to read clearly on white (4.9:1) */
  --hv-line: #f2f4f7;          /* dividers and shaded set rows */
  --hv-ring: #e4e6e9;          /* outline of the round exercise pictures */
  --hv-logo: brightness(0);    /* the Hevy logo file is white: black on light */
  /* Letterboxd light: from Letterboxd's logo for light backgrounds */
  --lb-bg: #fff;
  --lb-ink: #556677;           /* wordmark, headings, circle overlaps */
  --lb-muted: #667788;
  --lb-line: #e1e6eb;
  --lb-poster: #e8edf1;        /* empty poster slots */
  --lb-shimmer: #f3f5f7;
  --lb-edge: rgba(20, 24, 28, 0.08);
  --lb-brand: #556677;
  /* Oslo map: land, parks, fjord and roads in quiet greys (water a touch cool) */
  --map-land: #f0f0f0;
  --map-park: #e3e6e0;
  --map-water: #d6dde1;
  --map-road: #ffffff;
  --logo-cutout: #ffffff;      /* the M in the logo badge */
  /* The same in both themes */
  --scrim: rgba(0, 0, 0, 0.55);                   /* behind an open popup */
  --on-photo: #ffffff;                            /* text over a photo */
  --photo-shade: rgba(0, 0, 0, 0.78);             /* the dark fade behind it */
  --photo-text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
  --chip-on-photo: rgba(255, 255, 255, 0.9);      /* the corner chip on project covers */
  --chip-on-photo-ink: #1a1a1a;
  --shadow-float: 0 8px 24px rgba(0, 0, 0, 0.18); /* the phone dock, above everything */
  --shadow-icon: 0 0 0 1px var(--line), 0 2px 8px rgba(0, 0, 0, 0.16);   /* app icons */
  --hv-warmup: #f5a200;        /* Hevy's set types: warm-up, drop set, failure */
  --hv-dropset: #7a5af8;
  --hv-failure: #f04438;

  /* ----- Design system ------------------------------------------------
     Named values that every rule uses instead of raw numbers, so sizes stay
     in step with each other. The scales are fluid, made with Utopia's method
     (https://utopia.fyi): each value grows smoothly from a small screen to a
     large one, with no breakpoints.
     - Page and popups: from 360px wide (16px text, each step 1.2x the last)
       to 1280px wide (18px text, 1.25x).
     - Tiles: with the grid cell instead (--cell, a tile column's width, set
       by main.js: about 155px on a small phone, 250px on desktop, 405px on a
       tablet), so tile text fits its tile on every layout.
     Pick the roles (--text-*, --tile-*), not the steps. Structural sizes
     (hairlines, icons, buttons and tap areas) stay in px. */
  --cell: 200px;               /* until main.js measures it */

  /* Type steps: page and popups */
  --step--2: clamp(0.6944rem, 0.6844rem + 0.0444vw, 0.72rem);     /* 11.1 → 11.5px */
  --step--1: clamp(0.8333rem, 0.8072rem + 0.1159vw, 0.9rem);      /* 13.3 → 14.4px */
  --step-0: clamp(1rem, 0.9511rem + 0.2174vw, 1.125rem);          /* 16 → 18px */
  --step-1: clamp(1.2rem, 1.1193rem + 0.3587vw, 1.4063rem);       /* 19.2 → 22.5px */
  --step-2: clamp(1.44rem, 1.3156rem + 0.5527vw, 1.7578rem);      /* 23 → 28.1px */
  --step-3: clamp(1.728rem, 1.5444rem + 0.8161vw, 2.1973rem);     /* 27.6 → 35.2px */
  --step-4: clamp(2.0736rem, 1.8103rem + 1.1704vw, 2.7466rem);    /* 33.2 → 43.9px */
  --step-5: clamp(2.4883rem, 2.1186rem + 1.6433vw, 3.4332rem);    /* 39.8 → 54.9px */
  /* Type steps: tiles (14px text, 1.15x, on a 155px cell; 17px, 1.2x, on 405px) */
  --tile-step--2: clamp(0.6616rem, calc(9.83px + 0.00488 * var(--cell)), 0.7378rem);   /* 10.6 → 11.8px (desktop 11.1) */
  --tile-step--1: clamp(0.7609rem, calc(10.938px + 0.00797 * var(--cell)), 0.8854rem); /* 12.2 → 14.2px (desktop 12.9) */
  --tile-step-0: clamp(0.875rem, calc(12.14px + 0.012 * var(--cell)), 1.0625rem);     /* 14 → 17px (desktop 15.2) */
  --tile-step-1: clamp(1.0062rem, calc(13.434px + 0.0172 * var(--cell)), 1.275rem);   /* 16.1 → 20.4px (desktop 17.8) */
  --tile-step-2: clamp(1.1572rem, calc(14.817px + 0.02386 * var(--cell)), 1.53rem);   /* 18.5 → 24.5px (desktop 20.8) */
  --tile-step-3: clamp(1.3308rem, calc(16.28px + 0.03234 * var(--cell)), 1.836rem);   /* 21.3 → 29.4px (desktop 24.4) */
  --tile-step-4: clamp(1.5304rem, calc(17.812px + 0.04306 * var(--cell)), 2.2032rem); /* 24.5 → 35.3px (desktop 28.7) */

  /* Text roles */
  --text-label: var(--step--2);      /* uppercase labels: ROLE, WORK */
  --text-small: var(--step--1);      /* secondary text, menus, buttons */
  --text-body: var(--step-0);        /* reading text */
  --text-lead: var(--step-1);        /* the line under a title */
  --text-heading: var(--step-2);     /* section headings */
  --text-title: var(--step-3);       /* popup titles */
  --text-display: var(--step-5);     /* case study titles */
  --tile-label: var(--tile-step--2); /* EXPERIENCE, TOOLS I USE */
  --tile-small: var(--tile-step--1); /* dates, places, notes */
  --tile-body: var(--tile-step-0);
  --tile-title: var(--tile-step-1);  /* a role, a workout, a city */
  --tile-heading: var(--tile-step-2);
  --tile-display: var(--tile-step-3);
  --tile-figure: var(--tile-step-4); /* a big number: the clock */

  --leading-none: 1;
  --leading-tight: 1.1;              /* display type */
  --leading-snug: 1.3;               /* headings, labels, tile text */
  --leading-normal: 1.5;             /* short paragraphs */
  --leading-loose: 1.65;             /* long reading */

  --weight-regular: 400;
  --weight-medium: 500;              /* UI text */
  --weight-semibold: 600;            /* emphasis, labels, headings */
  --weight-heavy: 800;               /* display type */

  --tracking-tight: -0.015em;        /* display type and big numbers */
  --tracking-label: 0.08em;          /* uppercase labels */

  /* Space steps: multiples of the text size (16 → 18px), fluid like it */
  --space-3xs: clamp(0.25rem, 0.2378rem + 0.0543vw, 0.2813rem);   /* 4 → 4.5px */
  --space-2xs: clamp(0.5rem, 0.4755rem + 0.1087vw, 0.5625rem);    /* 8 → 9px */
  --space-xs: clamp(0.75rem, 0.7133rem + 0.163vw, 0.8438rem);     /* 12 → 13.5px */
  --space-s: clamp(1rem, 0.9511rem + 0.2174vw, 1.125rem);         /* 16 → 18px */
  --space-m: clamp(1.5rem, 1.4266rem + 0.3261vw, 1.6875rem);      /* 24 → 27px */
  --space-l: clamp(2rem, 1.9022rem + 0.4348vw, 2.25rem);          /* 32 → 36px */
  --space-xl: clamp(3rem, 2.8533rem + 0.6522vw, 3.375rem);        /* 48 → 54px */
  --space-2xl: clamp(4rem, 3.8043rem + 0.8696vw, 4.5rem);         /* 64 → 72px */
  /* Space pairs: grow from one step to a bigger one (for the page's edges) */
  --space-s-m: clamp(1rem, 0.731rem + 1.1957vw, 1.6875rem);       /* 16 → 27px */
  --space-s-l: clamp(1rem, 0.5109rem + 2.1739vw, 2.25rem);        /* 16 → 36px */
  --space-m-l: clamp(1.5rem, 1.2065rem + 1.3043vw, 2.25rem);      /* 24 → 36px */
  --space-m-xl: clamp(1.5rem, 0.7663rem + 3.2609vw, 3.375rem);    /* 24 → 54px */
  --space-l-xl: clamp(2rem, 1.462rem + 2.3913vw, 3.375rem);       /* 32 → 54px */
  --space-l-2xl: clamp(2rem, 1.0217rem + 4.3478vw, 4.5rem);       /* 32 → 72px */
  /* Space in tiles: with the grid cell */
  --tile-pad: clamp(1rem, calc(4px + 0.072 * var(--cell)), 1.5rem);   /* 16 → 22 (desktop) → 24px */
  --tile-pad-tight: calc(var(--tile-pad) * 0.8);   /* the Hevy and Letterboxd tiles, which need the room */
  --gap: var(--space-s);             /* between tiles */
  --measure: 40rem;                  /* the widest a paragraph gets (about 75 characters) */

  --radius-xs: 8px;                  /* posters */
  --radius-s: 12px;                  /* rows */
  --radius-m: 18px;                  /* images in popups */
  --radius-l: 26px;                  /* tiles and popups */
  --radius-pill: 999px;
  --radius: var(--radius-l);

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);        /* settles softly */
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);   /* overshoots a little */
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-1: 0.15s;                    /* small feedback: hover, press */
  --dur-2: 0.2s;                     /* fades, colour changes */
  --dur-3: 0.3s;                     /* things that move */
  --dur-4: 0.5s;                     /* bigger moves: reveal, menu thumb */

  --maxw: 1120px;
  --control-h: 50px;                 /* the menu pill and the round theme toggle */
}

:root[data-theme="dark"] {
  --bg: #0e0e0e;
  --card: #1a1a1a;           /* the logo's black, lifted off the page */
  --card-2: #242424;
  --ink: #f2f2f2;
  --ink-soft: #a3a3a3;
  --line: rgba(255, 255, 255, 0.1);
  --accent: #f2f2f2;
  --accent-soft: #333333;
  --glass: rgba(26, 26, 26, 0.78);     /* translucent --card for frosted controls */
  --live-soft: rgba(47, 191, 113, 0.28);
  --linkedin: #70b5f9;                 /* LinkedIn's own dark-mode blue; the standard one vanishes here */
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 6px 16px rgba(0, 0, 0, 0.45);
  --shadow-hover: 0 4px 10px rgba(0, 0, 0, 0.45), 0 12px 26px rgba(0, 0, 0, 0.55);
  --shadow-sheet: 0 2px 6px rgba(0, 0, 0, 0.45), 0 14px 36px rgba(0, 0, 0, 0.5);
  /* Hevy dark: the app's dark UI (Hevy's own store screenshots): #1c1c1e cards,
     white text, grey #9b9b9d. Lines are one step up from the card. */
  --hv-bg: #1c1c1e;
  --hv-ink: #fff;
  --hv-muted: #9b9b9d;
  --hv-line: #2c2c2e;
  --hv-ring: #3a3a3c;
  --hv-logo: none;             /* the white logo as it is */
  /* Letterboxd dark: measured on letterboxd.com (content #14181c, text #99aabb,
     section rules #445566, posters on #2c3440, white circle overlaps) */
  --lb-bg: #14181c;
  --lb-ink: #99aabb;
  --lb-muted: #99aabb;         /* their #667788 is too faint for small text on #14181c */
  --lb-line: #445566;
  --lb-poster: #2c3440;
  --lb-shimmer: #384352;
  --lb-edge: rgba(255, 255, 255, 0.05);
  --lb-brand: #fff;
  --map-land: #2b2b2b;
  --map-park: #2c332a;
  --map-water: #12181d;
  --map-road: #3b3b3b;
  --logo-cutout: #0e0e0e;      /* the page colour */
}

/* ----------------------------- Base -------------------------------- */
*,
*::before,
*::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "Archivo", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  font-size: var(--text-body);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color var(--dur-3) ease, color var(--dur-3) ease;
}

a { color: inherit; text-decoration: none; }
h1, h2, h3 { margin: 0; font-weight: var(--weight-semibold); letter-spacing: var(--tracking-tight); line-height: var(--leading-tight); }
p { margin: 0; }

/* Display type: the big headings (the intro line, case study titles and
   their section heads) in Archivo set wide and heavy, like the logo's blocky
   shapes. Everything else is Archivo at its normal width. */
.hero__name, .case__title, .case__story h3 { font-stretch: 118%; letter-spacing: var(--tracking-tight); }

.muted { color: var(--ink-soft); font-size: var(--text-small); }
.accent-text { color: var(--accent); }

/* Visually hidden, still read by screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.eyebrow {
  font-size: var(--text-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ----------------------------- Top bar ----------------------------- */
/* Sticky and transparent. It slides out of view while scrolling down and
   back in on scroll up (JS toggles .topbar--hidden). The empty strip ignores
   the pointer so it never blocks clicks on tiles scrolling underneath. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);   /* the menu stays centred */
  align-items: center;
  gap: var(--space-xs);
  /* Full page width so the logo sits in the page's far corner, not the content's. */
  width: 100%;
  margin: 0;
  padding: var(--space-s) var(--space-s-l);
  pointer-events: none;
  transition: transform var(--dur-3) var(--ease-standard);
}
.topbar--hidden { transform: translateY(-110%); }
.logo, .nav, .theme-toggle { pointer-events: auto; }

/* Menu wrapper: invisible on desktop (the menu sits in the top bar's grid),
   the fixed bottom dock on phones. */
.dock { display: contents; }

/* The logo, on its own. Shrink-wrapped so its grid column's empty space isn't
   clickable. */
.logo {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  padding: 6px;
  margin: -6px;                /* a bigger tap area, without moving the logo */
  border-radius: 50%;
}

/* The ML logo: a round badge as big as the theme toggle, in the text colour.
   On light it's near-black with a white M (filled, so it stands out from the
   grey page); on dark it's near-white with the M in the page colour. */
.logo__mark { display: block; flex: none; width: var(--control-h); height: var(--control-h); }
.logo__disc { fill: var(--ink); }
.logo__ml { fill: var(--logo-cutout); }

/* Logo animation (main.js): while the logo moves, it's drawn from its parts.
   While rising, that's the separate left and right pieces. While only the
   square moves, it's the two as one shape, so they never show a seam. The
   rest of the time, it's the one original shape. Until the script has
   started, the logo stays hidden, so it never shows whole and then jumps to
   its first frame. While rising, the pieces are clipped at the M's baseline
   (#logoRise), so they rise up out of it. */
.logo__parts { display: none; }
.logo__mark.is-moving .logo__parts { display: inline; }
.logo__mark.is-moving .logo__whole { display: none; }
.logo__part--a, .logo__part--b { display: none; }
.logo__mark.is-rising .logo__part--a,
.logo__mark.is-rising .logo__part--b { display: inline; }
.logo__mark.is-rising .logo__part--m { display: none; }
.logo__mark.is-rising .logo__parts { clip-path: url(#logoRise); }
.js .logo__mark:not(.is-ready) { visibility: hidden; }

.nav {
  position: relative;
  justify-self: center;
  display: flex;
  gap: var(--space-3xs);
  height: var(--control-h);
  padding: 5px;                /* the pill's inner edge, fixed like its height */
  background: var(--glass);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow);
}
.nav__tab {
  position: relative;
  z-index: 1;               /* above the sliding indicator */
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-soft);
  font: inherit;
  font-size: var(--text-small);
  font-weight: var(--weight-medium);
  padding: var(--space-2xs) var(--space-s);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: color var(--dur-2) ease, background-color var(--dur-2) ease;
}
.nav__tab:hover { color: var(--ink); }
/* No-JS fallback: the active tab carries its own dark fill */
.nav__tab.is-active {
  background: var(--ink);
  color: var(--bg);
}
/* With JS, one dark thumb slides between tabs instead (positioned by JS) */
.nav__indicator {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  border-radius: var(--radius-pill);
  background: var(--ink);
  opacity: 0;
  pointer-events: none;
  transition: transform var(--dur-4) var(--ease-spring),
              width var(--dur-4) var(--ease-spring);
}
.nav.has-indicator .nav__indicator { opacity: 1; }
.nav.has-indicator .nav__tab.is-active { background: transparent; color: var(--bg); }

/* A circle exactly as tall as the menu pill, with just the sun or moon */
.theme-toggle {
  justify-self: end;
  display: inline-grid;
  place-items: center;
  width: var(--control-h);
  height: var(--control-h);
  padding: 0;
  border: 1px solid var(--line);
  background: var(--glass);
  color: var(--ink);
  border-radius: 50%;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: transform var(--dur-1) ease;
}
.theme-toggle:hover { transform: translateY(-1px); }
.theme-toggle__icon { display: inline-flex; }
.theme-toggle__icon--moon { display: none; }
:root[data-theme="dark"] .theme-toggle__icon--sun { display: none; }
:root[data-theme="dark"] .theme-toggle__icon--moon { display: inline-flex; }

/* ----------------------------- Bento grid -------------------------- */
.bento {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* Square base unit: row height = column width (--cell, set by main.js).
     Rows never grow with content; see the tile rules below. */
  grid-auto-rows: var(--cell, 200px);
  grid-auto-flow: row;
  gap: var(--gap);
  max-width: var(--maxw);
  margin: var(--space-2xs) auto var(--space-l);
  padding: 0 var(--space-s-m);
}

/* ----------------------------- Card base --------------------------- */
.card {
  position: relative;
  overflow: hidden;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--tile-pad);
  display: flex;
  flex-direction: column;
  font-size: var(--tile-body);       /* tiles use the tile type scale */
  line-height: var(--leading-snug);
  transition: transform var(--dur-3) var(--ease-out),
              box-shadow var(--dur-3) ease, opacity var(--dur-3) ease, filter var(--dur-3) ease;
}

/* Only interactive tiles lift on hover — static info tiles stay put, and so
   does a placeholder tile (a link with no href yet) */
.card.is-expandable:not(.is-dimmed):hover,
a.card--tile[href]:not(.is-dimmed):hover,
a.card--letterboxd[href]:not(.is-dimmed):hover,
button.card--tile:not(.is-dimmed):hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-hover);
  transition: transform var(--dur-2) ease, box-shadow var(--dur-2) ease;
  transition-delay: 0s;
}

/* ----------------------------- Tile system ------------------------- */
/* TILE RULES. Every tile follows these, so tiles keep their shape and the
   grid never has holes.
   1. Four shapes only: S (1x1), W (2x1), T (1x2) and L (2x2). A tile's shape
      comes only from its data-shape attribute, optionally changed per layout
      with data-shape-tablet (<=900px, also used on phones) and
      data-shape-phone (<=560px). Nothing else sets grid spans.
   2. Rows are exactly one cell tall, and a cell is square (--cell = column
      width, set by main.js). A tile's size never depends on its content, so
      no tile can stretch its neighbours.
   3. Content adapts to the tile instead: every tile is a size container, text
      follows the tile type scale (--tile-*, sized from the grid cell), long
      text is clamped, and lists show what fits ("See N more").
   4. The About / Work filters move tiles; they never change a tile's shape.
   5. A tile changes shape between layouts only where it would be unusable
      otherwise (the hero, workout tile and the last project become L on phones).
   6. Every order must fill complete rows in All, About and Work on desktop
      (4 columns), tablet and phone (2 columns). After adding, removing or
      moving a tile, run .claude/layout-check.js in the browser. */
.card { container-type: size; }

[data-shape="S"] { grid-column: span 1; grid-row: span 1; }
[data-shape="W"] { grid-column: span 2; grid-row: span 1; }
[data-shape="T"] { grid-column: span 1; grid-row: span 2; }
[data-shape="L"] { grid-column: span 2; grid-row: span 2; }
@media (max-width: 900px) {
  [data-shape-tablet="S"] { grid-column: span 1; grid-row: span 1; }
  [data-shape-tablet="W"] { grid-column: span 2; grid-row: span 1; }
  [data-shape-tablet="T"] { grid-column: span 1; grid-row: span 2; }
  [data-shape-tablet="L"] { grid-column: span 2; grid-row: span 2; }
}
@media (max-width: 560px) {
  [data-shape-phone="S"] { grid-column: span 1; grid-row: span 1; }
  [data-shape-phone="W"] { grid-column: span 2; grid-row: span 1; }
  [data-shape-phone="T"] { grid-column: span 1; grid-row: span 2; }
  [data-shape-phone="L"] { grid-column: span 2; grid-row: span 2; }
}

/* ----------------------------- Hero -------------------------------- */
/* Intro tile: circular portrait (invisible circle) left, brief text right */
.card--hero { padding: 0 calc(var(--tile-pad) * 1.5); }
.hero { display: flex; flex-direction: row; align-items: center; gap: calc(var(--tile-pad) * 1.25); height: 100%; }
.hero__avatar {
  flex: 0 0 auto;
  width: clamp(120px, calc(var(--cell) * 0.66), 168px);
  height: clamp(120px, calc(var(--cell) * 0.66), 168px);
  border-radius: 50%;
  overflow: hidden;
  background: none;
}
/* Transparent square photo clipped to a circle — the shirt curves along the edge */
.avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}
.hero__text { flex: 1; align-self: center; }
.hero__wave { color: var(--ink-soft); font-weight: var(--weight-medium); margin-bottom: var(--space-2xs); font-size: var(--tile-body); }
/* The intro line is the page's headline: the tile scale's display size, the
   biggest text on the front page. */
.hero__name {
  font-size: var(--tile-display);
  font-weight: var(--weight-heavy);
  line-height: var(--leading-tight);
  color: var(--ink);
  margin: 0;
}
.hero__status { margin-top: var(--space-xs); }
.wave { display: inline-block; transform-origin: 70% 70%; animation: wave 2.4s ease-in-out infinite; }
@keyframes wave {
  0%, 60%, 100% { transform: rotate(0); }
  10%, 30% { transform: rotate(14deg); }
  20% { transform: rotate(-8deg); }
}

/* ----------------------------- Tile (icon links) ------------------- */
.card--tile {
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-xs);
}
.card--tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Keyboard focus: the same ring on every link and button as on the tiles */
a:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.nav__tab:focus-visible { outline-offset: 0; }   /* stays inside the menu's pill */

/* ----------------------------- Logo tiles (Gmail / LinkedIn) ------- */
/* Just the logo, centred. Sized from the grid cell so it scales with the tile.
   The pieces are animated from JS (assemble / draw on) on reveal and hover. */
.card--logo {
  align-items: center;
  justify-content: center;
  width: 100%;
  font: inherit;              /* the Gmail tile is a <button> */
  color: inherit;
  cursor: pointer;
}
.logo-art { display: block; flex: none; width: auto; }
.logo-art--gmail {
  height: calc(var(--cell, 200px) * 0.27);
  overflow: hidden;           /* pieces slide in from the logo's own edges */
}
.logo-art--linkedin,
.logo-art--instagram {
  height: calc(var(--cell, 200px) * 0.34);
  overflow: visible;          /* room for the dot's springy overshoot */
}
/* Logos that draw themselves (main.js): stroked lines and a dot that pops in */
.logo-dot { transform-box: fill-box; transform-origin: center; }
.logo-stroke { fill: none; stroke-linecap: round; stroke-linejoin: round; }
.li-dot { fill: var(--linkedin); }
.li-stroke { stroke: var(--linkedin); stroke-width: 3.2; }
.ig-dot { fill: url(#ig-gradient); }
.ig-stroke { stroke: url(#ig-gradient); stroke-width: 2.1; }

/* Corner chip (same look as the expand button): copy icon on Gmail,
   external-link arrow on LinkedIn. Gmail's grows into a "Copied ✓" pill. */
.expand-btn.tile-action {
  border-radius: var(--radius-pill);
  overflow: hidden;
  /* The hidden "Copied ✓" label is wider than the collapsed chip and sets the
     grid cell's width; centring the cell keeps the icon in the middle. */
  place-content: center;
  transition: opacity var(--dur-2) ease, transform var(--dur-2) ease,
              width var(--dur-3) var(--ease-spring),
              background-color var(--dur-2) ease, color var(--dur-2) ease, border-color var(--dur-2) ease;
}
.tile-action__icon,
.tile-action__done { grid-area: 1 / 1; transition: opacity var(--dur-1) ease; }
.tile-action__done {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  font-size: var(--tile-small);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
  opacity: 0;
}
.tile-action__check {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 21;
  stroke-dashoffset: 21;
}
.card--gmail.is-copied .tile-action {
  width: 96px;
  opacity: 1;
  transform: none;
  background: var(--ink);
  color: var(--bg);
  border-color: transparent;
}
.card--gmail.is-copied .tile-action__icon { opacity: 0; }
.card--gmail.is-copied .tile-action__done { opacity: 1; transition-delay: 0.12s; }
.card--gmail.is-copied .tile-action__check {
  animation: check-draw var(--dur-3) var(--ease-standard) var(--dur-2) forwards;
}
@keyframes check-draw { to { stroke-dashoffset: 0; } }

/* ----------------------------- Hevy / latest workout -------------- */
/* Styled like Hevy's own workout card: Figtree, grey labels, Duration / Volume /
   Records, round exercise pictures and "See N more exercises", in Hevy's light
   or dark colours to match the site theme (--hv-* tokens at the top). Filled in
   by js/hevy.js; the list shows the exercises that fit the tile. The expanded
   view uses the same .hevy-skin. The expand button stays the site's own. */
.hevy-skin {
  background: var(--hv-bg);
  color: var(--hv-ink);
  font-family: "Figtree", system-ui, sans-serif;
}
.card--hevy { padding: var(--tile-pad-tight); }

/* Avatar + username + date (also used by the expanded view) */
.hevy-user {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  min-width: 0;
  padding-right: 40px;        /* clears the expand chip */
}
.hevy-user__avatar {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--hv-line);
}
.hevy-user__avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.hevy-user__text { display: flex; flex-direction: column; min-width: 0; }
.hevy-user__name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The date's day and time (two spans) each stay whole: on a narrow tile the
   time goes to a second line instead of being cut off. */
.hevy-user__date > span { white-space: nowrap; }
.hevy-user__name { font-size: var(--tile-body); font-weight: var(--weight-semibold); line-height: var(--leading-snug); color: var(--hv-ink); }
.hevy-user__date { color: var(--hv-muted); font-size: var(--tile-small); line-height: var(--leading-snug); }

.hevy__title {
  margin-top: var(--space-xs);
  font-size: var(--tile-title);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Duration / Volume / Records: small grey labels over the values */
.hevy-stats { display: flex; gap: var(--space-s); margin: var(--space-2xs) 0 0; }
.hevy-stats dt { color: var(--hv-muted); font-size: var(--tile-label); line-height: var(--leading-snug); }
.hevy-stats dd {
  display: flex;
  align-items: center;
  gap: 3px;
  margin: 2px 0 0;
  font-size: var(--tile-body);
  line-height: var(--leading-snug);
  white-space: nowrap;
}
.hevy-stats [hidden] { display: none; }

/* Hevy's gold record medal (sprite #ic-medal) */
.hevy-medal { width: 1.1em; height: 1.1em; flex: none; }

/* Exercise rows: round picture + "3 sets Incline Bench Press (Dumbbell)". The
   list takes the tile's remaining height; js/hevy.js hides the rows that don't
   fit and fills in "See N more exercises". */
.hevy-exercises {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  display: grid;
  align-content: start;
  gap: var(--space-2xs);
  margin: var(--space-xs) 0 0;
  padding: var(--space-xs) 0 0;
  border-top: 1px solid var(--hv-line);
  list-style: none;
}
.hevy-ex { display: flex; align-items: center; gap: var(--space-2xs); min-width: 0; }
.hevy-ex[hidden] { display: none; }
.hevy-ex__text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  min-width: 0;
  font-size: var(--tile-body);
  line-height: var(--leading-snug);
}
.hevy-ex__ghost { display: block; height: 12px; border-radius: var(--radius-pill); background: var(--hv-line); flex: 1; }

/* Round exercise picture, or Hevy's dumbbell for exercises without one */
.hevy-thumb {
  --size: 40px;
  flex: none;
  display: grid;
  place-items: center;
  width: var(--size);
  height: var(--size);
  border-radius: 50%;
  border: 0.8px solid var(--hv-ring);
  overflow: hidden;
  background: var(--hv-bg);
  color: var(--hv-muted);
}
.hevy-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.hevy-thumb svg { width: 55%; height: 55%; }

.hevy__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs);
  margin-top: auto;           /* sits at the bottom of the tile */
  padding-top: var(--space-xs);
}
.hevy__more { color: var(--hv-muted); font-size: var(--tile-small); }

/* Shorter tile (the big square on phones): tighter spacing and smaller
   pictures, so three exercises fit, as on Hevy's card */
@container (max-height: 420px) {
  .hevy__title { margin-top: var(--space-2xs); }
  .hevy-stats { margin-top: var(--space-3xs); }
  .hevy-exercises { margin-top: var(--space-2xs); padding-top: var(--space-2xs); gap: var(--space-2xs); }
  .hevy-exercises .hevy-thumb { --size: 34px; }
  .hevy__foot { padding-top: var(--space-2xs); }
}

/* Hevy mark + wordmark, like the header on hevy.com */
.hevy-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  color: var(--hv-ink);
  font-family: "Figtree", system-ui, sans-serif;
  font-size: var(--tile-small);
  font-weight: var(--weight-heavy);
  letter-spacing: 0.04em;      /* as on hevy.com */
}
.hevy-brand img { width: 1.3em; height: 1.3em; filter: var(--hv-logo); }   /* the logo file is white */

/* Hidden sprite holding the medal and dumbbell icons (used via <use href="#...">) */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ----------------------------- Letterboxd / recently watched ------- */
/* In Letterboxd's own colours, light or dark to match the site theme (--lb-*
   tokens at the top): the three-circle mark, a section heading with a rule
   under it as on letterboxd.com, four posters in a row with your rating over
   them. Filled in by js/letterboxd.js. Posters are ALWAYS 2:3 (never
   stretched): their height comes from the tile, capped so all four fit its
   width. The corner button stays the site's own. */
.card--letterboxd {
  padding: var(--tile-pad-tight);
  background: var(--lb-bg);
  color: var(--lb-ink);
}
.card--letterboxd:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.card--letterboxd .eyebrow { color: var(--lb-ink); }
.card--letterboxd:not(.is-dimmed):hover .expand-btn,
.card--letterboxd:focus-visible .expand-btn { opacity: 1; transform: scale(1); }

.letterboxd__head,
.letterboxd__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs);
  flex: none;
}
.letterboxd__head {
  margin-right: 38px;         /* clears the corner chip */
  padding-bottom: var(--space-2xs);
  border-bottom: 1px solid var(--lb-line);
}
.letterboxd__foot {
  margin-top: var(--space-2xs);
  color: var(--lb-muted);
  font-size: var(--tile-small);
  line-height: var(--leading-snug);
}
.letterboxd__brand { color: var(--lb-brand); font-weight: var(--weight-heavy); }

/* Letterboxd's mark: three overlapping circles; where they overlap shows the
   wordmark colour (slate on light, white on dark), as in their logos */
.letterboxd-mark { display: block; flex: none; width: 30px; height: auto; }
.letterboxd-mark__overlap { fill: var(--lb-brand); }

.letterboxd-posters {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2xs);
  margin-top: var(--space-2xs);
}
.letterboxd-film {
  position: relative;
  display: block;
  flex: none;
  /* As tall as the row allows, but never so tall that four won't fit across */
  height: min(100%, (100cqi - 24px) / 4 * 1.5);
  aspect-ratio: 2 / 3;                        /* DO NOT stretch posters */
  overflow: hidden;
  border-radius: var(--radius-xs);
  background: var(--lb-poster);
  box-shadow: inset 0 0 0 1px var(--lb-edge);
}
.letterboxd-film__poster { width: 100%; height: 100%; object-fit: cover; display: block; }
.letterboxd-film__fallback {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  color: var(--lb-ink);
  font-size: var(--tile-display);
  font-weight: var(--weight-heavy);
  background: var(--lb-poster);
}
.letterboxd-film__shade {
  position: absolute;
  inset: auto 0 0;
  height: 55%;
  background: linear-gradient(to top, var(--photo-shade), transparent);
  pointer-events: none;
}
.letterboxd-film__meta {
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 5px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  color: var(--on-photo);
  font-size: var(--tile-label);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-none);
  text-shadow: var(--photo-text-shadow);
}
.letterboxd-film__rewatch { color: var(--on-photo); font-size: 1.15em; }
.letterboxd-film--ghost {
  background: linear-gradient(110deg, var(--lb-poster) 20%, var(--lb-shimmer) 38%, var(--lb-poster) 56%);
  background-size: 220% 100%;
  animation: letterboxd-shimmer 1.6s linear infinite;
}
@keyframes letterboxd-shimmer { to { background-position-x: -220%; } }

/* Short strip (phones): less chrome around the posters so they stay readable.
   (A container query can't style the tile itself, hence the media query.) */
@container (max-height: 200px) {
  .letterboxd-posters { margin-top: var(--space-2xs); }
  .letterboxd__foot { margin-top: var(--space-2xs); }
}

/* ----------------------------- Small print on tiles ---------------- */
/* Tile text uses the tile type scale, so the same copy fits a small phone
   square and a desktop square alike */
.card .eyebrow { font-size: var(--tile-label); }

/* "Open to new work": a green dot with a soft pulse */
.status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  font-size: var(--tile-small);
  color: var(--ink-soft);
  font-weight: var(--weight-medium);
}
.status__dot {
  flex: none;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--live);
  box-shadow: 0 0 0 3px var(--live-soft);
  animation: status-pulse 2.4s ease-out infinite;
}
@keyframes status-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--live) 45%, transparent); }
  70%, 100% { box-shadow: 0 0 0 8px transparent; }
}

/* ----------------------------- Oslo map ---------------------------- */
/* A map of central Oslo from OpenStreetMap data (--map-* colours) fills the
   tile. The place sits at the top and the local time at the bottom, each on a
   soft fade of the card's own colour so they read over the map. main.js ("Oslo
   clock") keeps the time and sets data-sky="day" / "night" (sun or moon). */
.card--map {
  justify-content: space-between;
  padding: var(--tile-pad);
}
.map-art { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.card--map::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to bottom, color-mix(in srgb, var(--card) 85%, transparent), transparent 34%, transparent 62%, color-mix(in srgb, var(--card) 85%, transparent));
}
.map-land { fill: var(--map-land); }
.map-park { fill: var(--map-park); }
.map-water { fill: var(--map-water); }
.map-river { fill: none; stroke: var(--map-water); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.map-road { fill: none; stroke: var(--map-road); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.map-road--minor { stroke-width: 1.6; }
.map-pin path { fill: var(--accent); }
.map-pin__halo { fill: var(--accent); opacity: 0.2; }
.map-pin__eye { fill: var(--card); }

.map__place,
.map__clock { position: relative; z-index: 1; }    /* above the map and its fades */
.map__place { display: flex; flex-direction: column; gap: 2px; }
.map__city { font-size: var(--tile-title); font-weight: var(--weight-semibold); letter-spacing: var(--tracking-tight); line-height: var(--leading-snug); }
.map__clock { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--space-2xs); }
.map__time {
  font-size: var(--tile-figure);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-none);
}
.map__sky svg { display: none; width: clamp(16px, 10cqi, 22px); height: clamp(16px, 10cqi, 22px); }
.card--map[data-sky="day"] .map__sun,
.card--map[data-sky="night"] .map__moon { display: block; }
.map__sun circle { fill: var(--accent); }
.map__sun path { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linecap: round; }
.map__moon path { fill: var(--ink-soft); }
.map__zone { flex-basis: 100%; margin-top: var(--space-3xs); font-size: var(--tile-small); color: var(--ink-soft); }

/* ----------------------------- Tools ------------------------------- */
/* The apps as their own icons in a row, each with its name under it, sized
   from the tile (as big as its width and height allow). */
.card--tools { justify-content: space-between; gap: var(--space-2xs); }
.tools {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(4, auto);
  justify-content: space-evenly;
  align-content: center;
}
.tool { display: flex; flex-direction: column; align-items: center; gap: var(--space-2xs); }
.tool__icon {
  display: block;
  width: min(17cqi, 50cqh);
  height: min(17cqi, 50cqh);
  border-radius: 23%;          /* an app icon's rounded square */
  box-shadow: var(--shadow-icon);   /* the ring keeps dark icons (Figma) apart from a dark card */
}
.tool__icon text { font: 700 19px/1 "Archivo", system-ui, sans-serif; letter-spacing: -0.5px; }
.tool__name { font-size: var(--tile-small); font-weight: var(--weight-semibold); line-height: var(--leading-snug); white-space: nowrap; }

/* ----------------------------- Experience (career) ----------------- */
/* Work and school in one timeline, newest first, as dots on a line: the role,
   then its years and where. Only what's happening now is green (live, like
   "open to new work"); degrees get a filled dot. The list fills the tile and
   fades out towards the bottom, so older entries show but step back; the full
   timeline (and the CV, once it's in) opens from here. */
.card--career { gap: var(--space-xs); }
.career {
  position: relative;
  flex: 1;
  min-height: 0;
  overflow: hidden;
  list-style: none;
  margin: -6px 0 0 -6px;                       /* a 6px gutter, pulled back out, so the dots */
  padding: 6px 0 0 28px;                       /* and the "now" halo aren't clipped at the edge */
  -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent);
  mask-image: linear-gradient(to bottom, #000 60%, transparent);
}
.career::before {                              /* the line through the dots */
  content: "";
  position: absolute;
  left: 10px;
  top: 12px;
  bottom: 0;
  width: 2px;
  border-radius: 2px;
  background: var(--line);
}
.career__item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: calc(var(--tile-pad) * 0.75);
}
.career__item::before {                        /* the dot */
  content: "";
  position: absolute;
  left: -22px;
  top: 0.3em;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--card);
  box-shadow: inset 0 0 0 2px var(--ink-soft);
}
.career__item--now::before { background: var(--live); box-shadow: 0 0 0 4px var(--live-soft); }   /* live, like "open to new work" */
.career__item--degree::before { background: var(--ink); box-shadow: none; }          /* degrees: filled */
.career__role { font-size: var(--tile-title); font-weight: var(--weight-semibold); line-height: var(--leading-snug); }
.career__meta { font-size: var(--tile-small); color: var(--ink-soft); line-height: var(--leading-snug); }
.career__when { font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }
.career__item--now .career__when { color: var(--ink); }
.career__more { font-size: var(--tile-small); font-weight: var(--weight-semibold); color: var(--ink-soft); }

/* ----------------------------- Projects ---------------------------- */
/* A project tile is only its cover, filling the whole tile (no title bar): an
   image or looping video as .project__cover, cropped to the tile's shape.
   Until real covers are in, placeholder images (assets/projects/placeholder-*.svg).
   The gradient only shows while a cover loads. When a project opens, its cover
   flies into the case study's frame (main.js). */
.card--project { padding: 0; }
.project__media {
  position: relative;
  flex: 1;
  min-height: 0;
  background: var(--cover-bg, linear-gradient(150deg, var(--card-2), var(--accent-soft)));
  overflow: hidden;
}
/* A project can set its own colours behind its cover (--cover-light and
   --cover-dark on the tile, for cut-out photos, e.g. Moen Marin's pale blue
   and near-black navy). The case study (its frame and the images in its
   story) and the cover's flight between tile and frame get the same (main.js
   copies them over). Without them it's the soft grey. */
.card--project, .case, .case__frame, .media-flight { --cover-bg: var(--cover-light); }
:root[data-theme="dark"] :is(.card--project, .case, .case__frame, .media-flight) { --cover-bg: var(--cover-dark); }

/* A project's logo, small in the tile's bottom-left corner: an inline SVG in
   currentColor, in the project's colours per theme (--logo-light and
   --logo-dark on it), else the text colour. It's only on the tile: it fades
   out as the tile opens (main.js), and the case study doesn't have it. */
.project__logo {
  position: absolute;
  left: var(--tile-pad);
  bottom: var(--tile-pad);
  z-index: 2;
  height: calc(clamp(13px, 4cqi, 20px) * var(--logo-scale, 1));   /* --logo-scale evens out logos that look smaller */
  width: auto;                                  /* from the SVG's own proportions */
  color: var(--logo-light, var(--ink));
  pointer-events: none;
}
:root[data-theme="dark"] .project__logo { color: var(--logo-dark, var(--ink)); }
.project__logo--top { top: var(--tile-pad); bottom: auto; }       /* when the cover fills the bottom-left */
.project__logo--right { right: var(--tile-pad); left: auto; }     /* bottom-right, e.g. on a dark part of a photo */
.logo-flight { position: fixed; z-index: 121; margin: 0; }
.project__cover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ----------------------------- Town tile --------------------------- */
/* The tile that turns the page into a little 3D town (css/town.css and
   js/town/ load only when it's clicked): a picture of the town, a "Play"
   badge and a line about it. */
.card--play .play__media { background: var(--card-2); }
.play__cover--night { display: none; }
:root[data-theme="dark"] .play__cover--day { display: none; }
:root[data-theme="dark"] .play__cover--night { display: block; }
/* The picture fades into the tile at the bottom, under the text */
.play__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, var(--card) 12%, color-mix(in srgb, var(--card) 70%, transparent) 30%, transparent 55%);
  pointer-events: none;
}
.play__text {
  position: absolute;
  left: var(--tile-pad);
  right: var(--tile-pad);
  bottom: var(--tile-pad);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3xs);
  pointer-events: none;
}
.play__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  margin-bottom: var(--space-2xs);
  padding: var(--space-3xs) var(--space-xs) var(--space-3xs) var(--space-2xs);
  border-radius: var(--radius-pill);
  background: var(--ink);
  color: var(--bg);
  font-size: var(--tile-small);
  font-weight: var(--weight-semibold);
}
.play__title { font-size: var(--tile-display); font-weight: var(--weight-heavy); font-stretch: 118%; letter-spacing: var(--tracking-tight); line-height: var(--leading-tight); }
.play__sub { font-size: var(--tile-small); color: var(--ink-soft); }
.card--play.is-loading .play__badge::after { content: "…"; }
.card--play.is-unavailable .play__sub::after { content: " (not in this browser)"; }
/* The town is for computers: on the 2-column layout the tile only says to
   open it on one (main.js doesn't open the town there) */
.play__sub--small { display: none; }
@media (max-width: 900px) {
  .card--play .play__badge,
  .card--play .expand-btn,
  .card--play .play__sub:not(.play__sub--small) { display: none; }
  .card--play .play__sub--small { display: block; }
  .card--play.is-expandable { cursor: default; }
  .card--play.is-expandable:not(.is-dimmed):hover { transform: none; box-shadow: var(--shadow); }
}

/* While the town opens and closes (main.js puts town-lift on <html>), the
   top bar with its menu and the back-to-top button get out of the way, and
   come back once the page is back. On phones the bar stays put (the dock
   is fixed inside it) and only fades; the dock slides down. */
.town-lift .topbar { transform: translateY(-110%); opacity: 0; transition: transform var(--dur-3) var(--ease-standard), opacity var(--dur-2) ease; }
.town-lift .to-top { opacity: 0; pointer-events: none; }
@media (max-width: 640px) {
  .dock { transition: transform var(--dur-3) var(--ease-standard); }
  .town-lift .topbar { transform: none; }
  .town-lift .dock { transform: translateY(160%); }
}

/* ----------------------------- Button ------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  padding: var(--space-xs) var(--space-m);
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--card);
  font-weight: var(--weight-semibold);
  font-size: var(--text-small);
  transition: transform var(--dur-1) ease;
  white-space: nowrap;
}
.btn:hover { transform: translateY(-2px); }

/* ----------------------------- Footer ------------------------------ */
.footer {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--space-m) var(--space-s-m) var(--space-xl);
  display: flex;
  justify-content: space-between;
  gap: var(--space-xs);
  flex-wrap: wrap;
  font-size: var(--text-small);
  color: var(--ink-soft);
}
.footer__credit { flex-basis: 100%; font-size: var(--text-label); }
.footer__credit:hover { text-decoration: underline; }

/* ----------------------------- Back to top ------------------------- */
.to-top {
  position: fixed;
  /* --sbc offsets the scrollbar that disappears while a tile is open */
  right: calc(var(--space-m) + var(--sbc, 0px));
  bottom: var(--space-m);
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--glass);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow);
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px) scale(0.9);
  transition: opacity var(--dur-2) ease, transform var(--dur-3) var(--ease-out);
  z-index: 65;
}
.to-top.is-visible { opacity: 1; pointer-events: auto; transform: none; }
.to-top:hover { background: var(--card); }

/* ----------------------------- Frosted controls -------------------- */
/* Menu, theme toggle and back-to-top float over tiles, so they blur what's
   behind them. Small elements only; the full-screen overlay stays blur-free. */
.nav, .theme-toggle, .to-top {
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
  backdrop-filter: blur(14px) saturate(1.3);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav, .theme-toggle, .to-top { background: var(--card); }
}

/* ----------------------------- Expandable cards -------------------- */
.is-expandable { cursor: pointer; }
.is-expandable:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.expand-btn {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 3;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  opacity: 0;
  transform: scale(0.8);
  transition: opacity var(--dur-2) ease, transform var(--dur-2) ease, background-color var(--dur-2) ease;
  box-shadow: var(--shadow);
}
.is-expandable:hover .expand-btn,
.is-expandable:focus-visible .expand-btn,
.card--logo:not(.is-dimmed):hover .expand-btn,
.card--logo:focus-visible .expand-btn { opacity: 1; transform: scale(1); }
.expand-btn:hover { background: var(--card-2); }
/* On dark project cards the icon needs a light chip to stay visible */
.card--project .expand-btn { background: var(--chip-on-photo); color: var(--chip-on-photo-ink); border-color: transparent; }

/* ----------------------------- Overlay + sheet (popup) ------------- */
/* A tile grows into its popup and back (main.js, "Expandable modal"):
   - .sheet is the growing box: background, corners and shadow. main.js sets
     its width and height when it opens: on desktop the tile's own proportions
     scaled up as far as the screen allows (projects: one large card size for
     all), on tablets and phones a near-full-screen card.
   - .sheet__body gets the inverse scale while the box grows, so the content
     keeps its real size; it's also the container the popup layouts query.
   - .sheet__ghost is a copy of the tile that fades out (and back in on close).
   - .portrait-flight is the About portrait flying between tile and popup, and
     .media-flight a project's cover flying between tile and case study frame. */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
}
.overlay[hidden] { display: none; }

.overlay__backdrop {
  position: absolute;
  inset: 0;
  /* Plain dim (no live backdrop-blur); faded in and out by main.js */
  background: var(--scrim);
}

.sheet {
  position: relative;
  background: var(--card);
  border-radius: var(--radius-l);    /* the same as a tile's, so it grows from it seamlessly */
  box-shadow: var(--shadow-sheet);
  overflow: hidden;
  transform-origin: 0 0;
}
.sheet__body {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  transform-origin: 0 0;
  container: sheet / size;
}
.sheet__ghost.card {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 6;
  margin: 0;
  pointer-events: none;
  background: transparent;    /* the sheet behind it has the same colour */
  box-shadow: none;
}
.portrait-flight {
  position: fixed;
  z-index: 120;
  margin: 0;
  border-radius: 50%;
  overflow: hidden;
  pointer-events: none;
  transform-origin: 0 0;
}
.portrait-flight img { display: block; width: 100%; height: 100%; object-fit: cover; }
.media-flight {
  position: fixed;
  z-index: 120;
  margin: 0;
  overflow: hidden;
  pointer-events: none;
  transform-origin: 0 0;
  background: var(--cover-bg, var(--card-2));
}
.media-flight > * {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  max-width: none;
  transform-origin: 0 0;
}

.sheet__close {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 5;
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: transform var(--dur-1) ease, background-color var(--dur-2) ease;
}
.sheet__close:hover { transform: scale(1.06); background: var(--card-2); }

.sheet__content {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Hide the scrollbar while a tile is open (still scrollable) */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.sheet__content::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ----------------------------- Popup layouts ---------------------- */
/* A popup has its tile's shape (see "Overlay + sheet"), so its layout follows
   that shape: the "sheet" container (the popup body) is queried by aspect
   ratio. Wide popups (from wide tiles) put things side by side; square and
   tall ones stack them. Text and space use the page scale (--text-*,
   --space-*); a few sizes use cqh, the popup's own height. */

/* ----------------------------- Detail: About ---------------------- */
.detail--about {
  min-height: 100cqh;          /* short content sits centred in the popup */
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--space-m-xl);
  text-align: center;
}
/* The portrait with "Open to new work" under it, as on the tile */
.detail__me { display: flex; flex-direction: column; align-items: center; gap: var(--space-xs); margin-bottom: var(--space-m); }
.detail__avatar { width: 128px; height: 128px; overflow: hidden; background: none; border-radius: 50%; }
.detail__status { font-size: var(--text-small); }
@container sheet (min-aspect-ratio: 3 / 2) {
  .detail--about {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    align-content: center;
    gap: var(--space-l-2xl);
    padding: var(--space-l-2xl);
  }
  .detail--about .detail__me { gap: var(--space-s); margin: 0; }
  .detail--about .detail__avatar { width: min(48cqh, 260px); height: min(48cqh, 260px); }
  .detail--about .detail__prose { margin: 0; max-width: var(--measure); }
}
.detail__avatar .avatar-img { object-position: center; }
.detail__prose { text-align: left; max-width: var(--measure); margin: 0 auto; display: flex; flex-direction: column; gap: var(--space-s); }
.detail__prose p { font-size: var(--text-body); line-height: var(--leading-loose); color: var(--ink-soft); }
.detail__prose strong { color: var(--ink); font-weight: var(--weight-semibold); }
.hl { color: var(--accent); font-weight: var(--weight-medium); }

/* ----------------------------- Detail: Experience timeline -------- */
.detail--exp { padding: var(--space-m-xl); }
/* Wide popup: Work and Education side by side */
@container sheet (min-aspect-ratio: 3 / 2) {
  .detail--exp .timeline {
    display: grid;
    grid-auto-flow: column;
    grid-template-rows: auto 1fr;
    grid-auto-columns: minmax(0, 1fr);
    column-gap: var(--space-l-xl);
    row-gap: var(--space-2xs);
  }
  .detail--exp .timeline__group { margin-top: 0; }
}
.exp__header { margin-bottom: var(--space-m); }
.exp__title { font-size: var(--text-title); margin-bottom: var(--space-2xs); }
.exp__sub { color: var(--ink-soft); font-size: var(--text-body); }
.exp__cv { margin-top: var(--space-s); }
.exp__cv[hidden] { display: none; }        /* .btn's display would override hidden */

.timeline { display: flex; flex-direction: column; gap: var(--space-2xs); }
.timeline__group {
  font-size: var(--text-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: var(--space-m) 0 var(--space-2xs);
}
.timeline__group:first-child { margin-top: 0; }

/* Vertical rule + node dots. Same rules as the Experience tile: only what's
   happening now is green (a filled dot with a soft ring, its dates in full
   ink); degrees get a filled dot in the text colour; everything else is a
   grey ring with grey dates. */
.tl { list-style: none; margin: 0; padding: 0 0 0 26px; position: relative; }
.tl::before {
  content: "";
  position: absolute;
  left: 6px;
  top: 6px;
  bottom: 6px;
  width: 2px;
  background: var(--line);
  border-radius: 2px;
}
.tl__item { position: relative; padding: 0 0 var(--space-m); }
.tl__item:last-child { padding-bottom: 0; }
.tl__item::before {
  content: "";
  position: absolute;
  left: -26px;
  top: 5px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--card);
  border: 2px solid var(--ink-soft);
  box-shadow: 0 0 0 3px var(--card);
}
.tl__item--now::before { background: var(--live); border-color: var(--live); box-shadow: 0 0 0 4px var(--live-soft); }
.tl__item--degree::before { background: var(--ink); border-color: var(--ink); }        /* degrees: filled */
.tl__period {
  display: block;
  font-size: var(--text-small);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  margin-bottom: var(--space-3xs);
}
.tl__item--now .tl__period { color: var(--ink); }
.tl__role { font-size: var(--text-body); font-weight: var(--weight-semibold); line-height: var(--leading-snug); margin: 0; }
.tl__org { font-size: var(--text-small); color: var(--ink-soft); margin: 2px 0 var(--space-2xs); }
.tl__desc { font-size: var(--text-small); line-height: var(--leading-normal); color: var(--ink-soft); }

/* ----------------------------- Detail: Latest workout ------------- */
/* Laid out like the workout page on hevy.com: every exercise with its round
   picture, then SET / WEIGHT & REPS rows ("50kg x 10 reps"), every other row
   shaded, record sets marked with the gold medal. Same Hevy colours as the tile. */
.sheet:has(.hevy-skin) { background: var(--hv-bg); border-color: transparent; }

.workout-detail { padding: var(--space-m-xl); }
.hevy-user--lg { padding-right: 46px; }             /* clears the close button */
.hevy-user--lg .hevy-user__avatar { width: 48px; height: 48px; }
.hevy-user--lg .hevy-user__name { font-size: var(--text-body); }
.hevy-user--lg .hevy-user__date { font-size: var(--text-small); white-space: normal; }
.workout-detail__title { margin-top: var(--space-s); font-size: var(--text-lead); font-weight: var(--weight-semibold); letter-spacing: 0; }
.hevy-stats--lg { gap: var(--space-m); margin-top: var(--space-xs); }
.hevy-stats--lg dt { font-size: var(--text-label); }
.hevy-stats--lg dd { font-size: var(--text-body); }

.workout-exercises {
  display: grid;
  gap: var(--space-m);
  margin-top: var(--space-s);
  padding-top: var(--space-m);
  border-top: 1px solid var(--hv-line);
}
.workout-exercise__head { display: flex; align-items: center; gap: var(--space-xs); margin-bottom: var(--space-2xs); }
.workout-exercise__head .hevy-thumb { --size: 48px; }
.workout-exercise__title { font-size: var(--text-body); font-weight: var(--weight-semibold); line-height: var(--leading-snug); letter-spacing: 0; }
.workout-exercise__notes { margin: 0 0 var(--space-2xs); color: var(--hv-muted); font-size: var(--text-small); }

/* Set rows: number (or W / D / F) and "50kg x 10 reps", plus any records */
.workout-sets__head,
.workout-set {
  display: grid;
  grid-template-columns: 64px 1fr;
  align-items: center;
}
.workout-sets__head {
  padding-bottom: var(--space-2xs);
  color: var(--hv-muted);
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
}
.workout-sets__head span:first-child,
.workout-set__n { text-align: center; }
.workout-set { min-height: 46px; padding: var(--space-2xs) 0; border-radius: var(--radius-s); }
.workout-set:nth-child(odd) { background: var(--hv-line); }   /* child 1 is the heading */
.workout-set__n { font-size: var(--text-body); font-weight: var(--weight-semibold); }
.workout-set__n--warmup { color: var(--hv-warmup); }
.workout-set__n--dropset { color: var(--hv-dropset); }
.workout-set__n--failure { color: var(--hv-failure); }
.workout-set__val {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3xs) var(--space-xs);
  font-size: var(--text-body);
  font-variant-numeric: tabular-nums;
}
.workout-set__record {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  color: var(--hv-muted);
  font-size: var(--text-small);
}

/* Your username links to your Hevy profile, "View on Hevy" to the workout */
a.hevy-user__name[href]:hover,
.workout-detail__profile:hover { text-decoration: underline; }
.workout-detail__profile { margin-left: auto; color: var(--hv-ink); font-weight: var(--weight-semibold); white-space: nowrap; }
.workout-detail__source {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs);
  margin-top: var(--space-l);
  padding-top: var(--space-s);
  border-top: 1px solid var(--hv-line);
  color: var(--hv-muted);
  font-size: var(--text-small);
}

/* ----------------------------- Detail: Case study ----------------- */
/* Every project opens to the same large card (main.js, popupSize) with the
   same page layout: one scrolling column with one left edge, holding the
   title and one line, the cover in a rounded frame, the facts, then the
   story. Text keeps a comfortable reading width; the cover and the images
   use the full width. The frame gets the tile's cover (copied by main.js,
   which flies it in from the tile). It's the column's width and 3:2, but
   never taller than 62% of the card, so it's always in the first view (on
   short screens the cover is cropped a little wider). */
.case {
  width: min(100%, 900px);
  margin: 0 auto;
  padding: var(--space-m-xl) var(--space-m-xl) var(--space-l-2xl);
}
.case__head { padding-right: 48px; }          /* clears the close button */
.case__title {
  font-weight: var(--weight-heavy);
  font-size: var(--text-display);
  line-height: var(--leading-tight);
}
.case__lede {
  margin-top: var(--space-xs);
  max-width: 48ch;
  color: var(--ink-soft);
  font-size: var(--text-lead);
  line-height: var(--leading-normal);
}

.case__frame {
  position: relative;
  width: 100%;                  /* stays full width when max-height kicks in */
  margin: var(--space-m-l) 0 0;
  aspect-ratio: 3 / 2;
  max-height: 62cqh;
  border-radius: var(--radius-m);
  overflow: hidden;
  background: var(--cover-bg, var(--card-2));
}
/* A hairline ring drawn over the image, so light covers keep an edge */
.case__frame::after,
.case__figure::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--line);
  pointer-events: none;
}

.case__facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-s) var(--space-m);
  margin: var(--space-s-m) 0 0;
  padding-bottom: var(--space-s-m);
  border-bottom: 1px solid var(--line);
}
.case__facts dt {
  font-size: var(--text-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-soft);
}
.case__facts dd { margin: var(--space-2xs) 0 0; font-size: var(--text-small); font-weight: var(--weight-medium); line-height: var(--leading-snug); }
@container sheet (max-width: 560px) {
  .case__facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The story: "About" and "What I did", each a heading with its text below
   (at a reading width, on the page's left edge) and a wide image after it. */
.case__story section { max-width: var(--measure); margin-top: var(--space-l-xl); }
.case__story h3 {
  font-size: var(--text-heading);
  font-weight: var(--weight-heavy);
  line-height: var(--leading-tight);
  margin-bottom: var(--space-xs);
}
.case__story p { font-size: var(--text-body); line-height: var(--leading-loose); }
.case__story p + p { margin-top: var(--space-xs); }

/* Images in the story: an <img> or <video> inside .case__figure, the full
   width of the page and cropped to 16:10, on the project's cover colours (so
   cut-out mockups match the cover). Without one, it's a dashed placeholder
   with a note of what goes there. A <section> with only text (no heading)
   continues the story after an image. */
.case__figure {
  position: relative;
  margin: var(--space-m-l) 0 0;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-m);
  overflow: hidden;
  background: var(--cover-bg, var(--card-2));
}
.case__figure > img,
.case__figure > video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.case__figure:not(:has(img, video)) {
  background: var(--card-2);
  display: grid;
  place-items: center;
  padding: var(--space-s);
  text-align: center;
  color: var(--ink-soft);
  font-size: var(--text-small);
}
.case__figure:not(:has(img, video))::after { box-shadow: none; border: 1px dashed var(--line); }

/* Lock scroll when a sheet is open */
body.is-locked { overflow: hidden; }

/* ----------------------------- Reveal & filter animations ---------- */
/* Hidden start-state only applies when JS is running; otherwise tiles show. */
.js .card {
  opacity: 0;
  transform: translateY(16px) scale(0.98);
}
.card.is-visible {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-4) ease, transform var(--dur-4) var(--ease-out);
  transition-delay: calc(var(--i, 0) * 55ms);
}
/* Filtered-out tiles stay in place but read as plainly "off" — muted & flat */
.card.is-dimmed {
  opacity: 0.5;
  filter: saturate(0.3) contrast(0.9);
  pointer-events: none;
  cursor: default;
  box-shadow: none;
}

/* ----------------------------- Responsive -------------------------- */
/* Tile shapes per layout come from the tile system above (data-shape*). */
@media (max-width: 900px) {
  .bento { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  /* Two-column grid so small tiles pair up (and stay square via the cell unit) */
  .bento { grid-template-columns: repeat(2, 1fr); }
}

/* Phones and narrow tablets (up to 640px): the header is one row that scrolls
   away, and the menu moves to a dock at the bottom. (640px, not the grid's
   560px, dates from when my name sat next to the logo and needed the room.) */
@media (max-width: 640px) {
  /* Top bar: the same logo and toggle as on desktop, in one row that scrolls
     away with the page. No sticky and no transform, so the fixed dock inside it
     stays anchored to the screen. */
  .topbar,
  .topbar.topbar--hidden {
    position: static;
    transform: none;
    grid-template-columns: minmax(0, 1fr) auto;
    pointer-events: auto;
  }

  /* Bottom dock: the menu, pinned and always visible. It spans the same
     gutters as the grid, and stretches into equal tabs. */
  .dock {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--space-s) + env(safe-area-inset-bottom, 0px));
    z-index: 60;
    display: flex;
    align-items: center;
    gap: var(--space-2xs);
    padding: 0 var(--space-s-m);
    pointer-events: none;
  }
  .nav { flex: 1; height: 56px; padding: 4px; box-shadow: var(--shadow-float), var(--shadow); }   /* thumb-sized */
  .nav__tab { flex: 1; padding: 15px 12px; line-height: var(--leading-none); font-size: var(--text-body); }

  /* Back-to-top sits just above the dock's right end */
  .to-top {
    right: var(--space-s-m);
    bottom: calc(var(--space-s) + env(safe-area-inset-bottom, 0px) + 56px + var(--space-xs));
  }

  /* Leave room so the last row / footer clears the dock */
  body { padding-bottom: 96px; }
}

/* Small phones: tighten the tabs so the menu fits a 320px screen */
@media (max-width: 360px) {
  .nav__tab { padding: 15px 8px; font-size: calc(var(--text-body) * 0.95); }
}

/* ----------------------------- Reduced motion ---------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .card { opacity: 1; transform: none; }
  .wave { animation: none; }
  .status__dot { animation: none; }
  .letterboxd-film--ghost { animation: none; }                  /* the loading shimmer */
  .card--gmail.is-copied .tile-action__check { animation: none; stroke-dashoffset: 0; }
  * { transition-duration: 0.01ms !important; }
}
