/* =========================================================================
   Editorial portfolio — design tokens

   Layout geometry (720px measure, 56px header, clamp() section rhythm) is
   inherited from the original build. The surface is not: warm paper ground,
   Inter throughout, teal accent, dotted column rules, tinted brand chips.
   ========================================================================= */

:root {
  /* Colour */
  --color-ink:        #1c1b19;
  --color-muted:      #1c1b19;
  --color-soft:       #6f6a63;
  --color-ground:     #faf9f7;   /* warm paper, not the reference's pure white */
  --color-raised:     #ffffff;   /* cards that need to lift off the paper */
  --color-sunken:     #f1efea;
  --color-hairline:   #e3dfd8;
  --color-accent:     #1e40af;   /* deep blue, against the warm paper ground */
  --color-on-accent:  #ffffff;
  --color-highlight:  #fd2;
  --color-live:       #146c2e;   /* the "Live" pill: dark enough to clear 4.5:1
                                    against its own 13% tint on warm paper */
  /* Hover wash. Over light paper the tint has to be LIGHTER than the paper
     in value, not darker: a dark hue at low alpha just greys the panel down
     to a dingy taupe (rgb(229,212,203) at 52%/44%). High lightness adds the
     colour without dropping the value. Dark mode overrides all four. */
  --glow-s:   90%;
  --glow-l:   78%;
  --glow-top: 0.38;
  --glow-mid: 0.13;
  --color-rule:       rgba(39, 29, 22, 0.1);
  --color-header-bg:  rgba(250, 249, 247, 0.82);
  --color-linkedin:   var(--color-accent);

  /* Claude Code token-volume ramp */
  --tok-1: #f2d9cd;
  --tok-2: #e0a98f;
  --tok-3: #c96442;
  --tok-4: #8f4325;

  --status-live-fg:   #1d6b45;
  --status-live-bg:   #eaf7ef;
  --status-live-bd:   #9ce0bb;
  --status-wip-fg:    #8b5200;
  --status-wip-bg:    #fff3de;
  --status-wip-bd:    #ffd58a;
  --status-off-fg:    #5d5d5d;
  --status-off-bg:    #f1f1f1;
  --status-off-bd:    #d6d6d6;

  /* Type */
  /* One family, hierarchy carried by weight, size and tracking. */
  --font-body:    "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: var(--font-body);
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --display-wght: 640;

  --hero-size:     clamp(2.4rem, 5.2vw, 3.9rem);
  --hero-leading:  1.06;
  /* Inter needs real negative tracking at display sizes or it reads loose. */
  --hero-track:    -0.032em;
  --hero-measure:  660px;
  --section-size:  clamp(1.625rem, 3.1vw, 2.25rem);

  /* Layout */
  --measure:      720px;
  --header-h:     56px;
  --gutter:       clamp(1.25rem, 7vw, 6rem);
  --rail:         210px;   /* career left column */

  /* Motion — three tiers, shared by every component rather than each one
     inventing its own timing.
       tap   immediate feedback: a press, a state flip
       ui    ordinary transitions: hover, reveal, panel
       scene larger moves: a section entrance, a mode change */
  --dur-tap:   150ms;
  --dur-ui:    240ms;
  --dur-scene: 420ms;
  --ease-calm:    cubic-bezier(0.16, 1, 0.3, 1);
  --ease-exit:    cubic-bezier(0.4, 0, 1, 1);

  /* Role timeline */
  --role-dot-size: 5px;
  --role-dot-top:  7px;
  --role-gap:      14px;
}

/* ---------- Dark theme (added on top of the reference, same token set) ---- */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-ink:       #ece9e4;
    --color-muted:     #ece9e4;
    --color-soft:      #9a948c;
    --color-ground:    #131514;
    --color-raised:    #1b1e1d;
    --color-sunken:    #232725;
    --color-hairline:  #2e3331;
    --color-accent:    #6d9bf6;
    --color-live:      #58cc8f;
    --glow-s:   88%;
    --glow-l:   56%;
    --glow-top: 0.34;
    --glow-mid: 0.12;
    --color-on-accent: #0a1730;
    --color-rule:      rgba(236, 233, 228, 0.09);
    --color-header-bg: rgba(19, 21, 20, 0.82);
    --color-linkedin:  var(--color-accent);

    --status-live-fg:  #7ee2ab; --status-live-bg: rgba(29, 107, 69, 0.22);  --status-live-bd: rgba(126, 226, 171, 0.32);
    --status-wip-fg:   #ffca7a; --status-wip-bg:  rgba(139, 82, 0, 0.26);   --status-wip-bd:  rgba(255, 202, 122, 0.3);
    --status-off-fg:   #a9a49d; --status-off-bg:  rgba(255, 255, 255, 0.06); --status-off-bd: rgba(255, 255, 255, 0.12);

    --tok-1: #40251a;
    --tok-2: #6f3b25;
    --tok-3: #a8542f;
    --tok-4: #e08a5c;
  }
}

:root[data-theme="dark"] {
  --color-ink:       #ece9e4;
  --color-muted:     #ece9e4;
  --color-soft:      #9a948c;
  --color-ground:    #131514;
  --color-raised:    #1b1e1d;
  --color-sunken:    #232725;
  --color-hairline:  #2e3331;
  --color-accent:    #6d9bf6;
  --color-live:      #58cc8f;
  --glow-s:   88%;
  --glow-l:   56%;
  --glow-top: 0.34;
  --glow-mid: 0.12;
  --color-on-accent: #0a1730;
  --color-rule:      rgba(236, 233, 228, 0.09);
  --color-header-bg: rgba(19, 21, 20, 0.82);
  --color-linkedin:  var(--color-accent);

  --status-live-fg:  #7ee2ab; --status-live-bg: rgba(29, 107, 69, 0.22);  --status-live-bd: rgba(126, 226, 171, 0.32);
  --status-wip-fg:   #ffca7a; --status-wip-bg:  rgba(139, 82, 0, 0.26);   --status-wip-bd:  rgba(255, 202, 122, 0.3);
  --status-off-fg:   #a9a49d; --status-off-bg:  rgba(255, 255, 255, 0.06); --status-off-bd: rgba(255, 255, 255, 0.12);

  --tok-1: #40251a;
  --tok-2: #6f3b25;
  --tok-3: #a8542f;
  --tok-4: #e08a5c;
}

/* =========================================================================
   Reset & base
   ========================================================================= */

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

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

body {
  margin: 0;
  position: relative;
  background: var(--color-ground);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: 18px;
  line-height: 1.65;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* The two faint vertical rules that frame the measure. */
body::before {
  content: "";
  position: absolute;
  inset-block: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(100% - 2rem, 784px);
  /* Dotted rather than the solid hairlines of the site this started from. */
  background:
    linear-gradient(var(--color-rule) 50%, transparent 50%) left  top / 1px 7px repeat-y,
    linear-gradient(var(--color-rule) 50%, transparent 50%) right top / 1px 7px repeat-y;
  pointer-events: none;
  z-index: -1;
}

h1, h2, h3, h4, p, ul, figure { margin: 0; }
ul { padding: 0; list-style: none; }

a {
  color: inherit;
  text-decoration: none;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

img, svg { display: block; max-width: 100%; }
mark { background: none; color: inherit; }

::selection { background: var(--color-highlight); color: #212121; }

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

.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 8px 14px;
  background: var(--color-ground);
  border: 1px solid var(--color-hairline);
  border-radius: 8px;
  font-size: 13px;
  transform: translateY(-160%);
  transition: transform 0.2s var(--ease-calm);
}
.skip-link:focus-visible { transform: none; }

.arrow { font-size: 0.86em; line-height: 1; }

/* =========================================================================
   Layout primitives
   ========================================================================= */

.gutter { padding-inline: var(--gutter); }

.col-wide {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
}

.section {
  padding-block: clamp(3rem, 7vw, 4.5rem);
  scroll-margin-top: 5rem;
}
.section + .section { border-top: 1px solid var(--color-hairline); }

/* =========================================================================
   Typography scale
   ========================================================================= */

.t-display,
.home-title {
  font-family: var(--font-display);
  font-weight: var(--display-wght);
  font-optical-sizing: auto;
  font-size: var(--hero-size);
  line-height: var(--hero-leading);
  letter-spacing: var(--hero-track);
  max-width: var(--hero-measure);
  text-wrap: balance;
}

/* In the split hero the headline is bounded by its column, not the old cap. */
.home-copy .home-title { max-width: none; }

.t-section {
  font-family: var(--font-display);
  font-weight: var(--display-wght);
  font-optical-sizing: auto;
  font-size: var(--section-size);
  line-height: 1.18;
  letter-spacing: -0.026em;
  text-wrap: balance;
}

.t-lede {
  font-size: clamp(1.0625rem, 1.7vw, 1.25rem);
  line-height: 1.6;
}

/* Emphasis carried by colour rather than a highlighter band. Still <mark>,
   so the "this is the part that matters" semantics survive the restyle.
   background: none also clears the UA default yellow on <mark>. */
.highlight {
  background: none;
  color: var(--color-accent);
}

/* =========================================================================
   Header
   ========================================================================= */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  border-bottom: 1px solid var(--color-hairline);
  background: var(--color-header-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.header-bar {
  display: flex;
  align-items: center;
  height: var(--header-h);
}

.site-signature {
  position: relative;
  display: grid;
  align-items: center;
  flex-shrink: 0;
  width: 172px;
  min-height: 40px;
}

.site-signature-mark,
.site-signature-name {
  grid-area: 1 / 1;
  transition: opacity 0.18s ease, transform 0.22s var(--ease-calm);
}

.site-signature-mark {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-size: 17px;
  line-height: 1.2;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.site-signature-name {
  display: flex;
  align-items: center;
  gap: 10px;
  font: 600 16px/1.2 var(--font-display);
  letter-spacing: -0.015em;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(3px);
}

.site-signature-photo {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: url("assets/portrait.jpg") center / cover var(--color-sunken);
}

.site-signature:hover .site-signature-mark,
.site-signature:focus-visible .site-signature-mark { opacity: 0; transform: translateY(-3px); }
.site-signature:hover .site-signature-name,
.site-signature:focus-visible .site-signature-name { opacity: 1; transform: none; }

/* --- nav --- */

.site-nav {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-left: auto;
  font-size: 13px;
  line-height: 1.65;
}

.site-nav > a,
.site-nav > button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 24px;
  text-underline-offset: 5px;
  white-space: nowrap;
}

.site-nav > a:hover,
.site-nav > a:focus-visible,
.site-nav > button:hover,
.site-nav > button:focus-visible {
  text-decoration: underline;
  text-decoration-color: var(--color-accent);
}

.nav-ask svg { width: 15px; height: 15px; }

.agent-launcher-mark {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  width: 22px;
  color: var(--color-accent);
}
.agent-launcher-mark i {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  animation: thinking 1.4s var(--ease-calm) infinite;
}
.agent-launcher-mark i:nth-child(2) { animation-delay: 0.16s; }
.agent-launcher-mark i:nth-child(3) { animation-delay: 0.32s; }

@keyframes thinking {
  0%, 60%, 100% { opacity: 0.32; transform: translateY(0); }
  30%           { opacity: 1;    transform: translateY(-2px); }
}

.theme-toggle {
  display: inline-grid;
  place-items: center;
  width: 24px;
  height: 24px;
  color: var(--color-ink);
}
.theme-toggle svg { width: 16px; height: 16px; }
.theme-toggle .icon-moon { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle .icon-sun  { display: none; }
  :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: block; }
}
:root[data-theme="dark"] .theme-toggle .icon-sun  { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
:root[data-theme="light"] .theme-toggle .icon-sun  { display: block; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: none; }

/* =========================================================================
   Hero
   ========================================================================= */

.hero-section {
  position: relative;
  padding-block: 44px 77px;
  /* Dot grid, the same one the case study hero uses. */
  background-image: radial-gradient(var(--color-rule) 1px, transparent 1px);
  background-size: 26px 26px;
}

/* Copy on the left, portrait on the right. The portrait is allowed to reach
   into the right gutter so the text column keeps the page's 720px measure. */
.home-heading {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(16px, 2vw, 24px);
}
.home-copy { min-width: 0; }

/* Small circular slot above the copy. A photo fills it; swap in an
   illustration and set object-fit: contain instead. */
.hero-widget {
  display: grid;
  place-items: center;
  width: clamp(66px, 7vw, 88px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--color-sunken);
  overflow: hidden;
}
.hero-widget > * { max-width: 100%; max-height: 100%; }
.hero-widget img { width: 100%; height: 100%; object-fit: cover; }

.home-intro {
  max-width: 530px;
  margin-top: 20px;
  font-size: clamp(14.5px, 0.98vw + 0.5px, 15.4px);
  line-height: 1.95;
}
.home-intro + .home-intro { margin-top: 20px; }

/* Inline company chips */
.inline-brand {
  display: inline-flex;
  align-items: baseline;
  gap: 0.24em;
  white-space: nowrap;
}

.company-brand {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 1px 0.45em;
  border: 1px solid var(--brand-color, var(--color-hairline));
  border-radius: 999px;
  line-height: 1.45;
  background: color-mix(in srgb, var(--brand-color, var(--color-ink)) 9%, var(--color-raised));
  backdrop-filter: blur(22px) saturate(190%);
  -webkit-backdrop-filter: blur(22px) saturate(190%);
  box-shadow:
    0 1px 1px rgba(33, 33, 33, 0.05),
    0 6px 12px -6px rgba(33, 33, 33, 0.25),
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 -1px 0 rgba(33, 33, 33, 0.05);
  transition: box-shadow 0.32s var(--ease-calm), transform 0.52s var(--ease-calm);
}
/* On dark, a very dark brand colour would leave the chip with an invisible
   border and dot, so both are lifted toward the foreground. */
.theme-dark .company-brand {
  background: color-mix(in srgb, var(--brand-color, var(--color-ink)) 20%, var(--color-raised));
  border-color: color-mix(in srgb, var(--brand-color) 55%, var(--color-ink) 45%);
  box-shadow:
    0 1px 1px rgba(0, 0, 0, 0.2),
    0 6px 12px -6px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.theme-dark .company-brand:hover {
  box-shadow:
    0 1px 1px rgba(0, 0, 0, 0.2),
    0 10px 22px -8px rgba(0, 0, 0, 0.65),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.theme-dark .brand-dot { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22); }

.company-brand::after {
  content: "";
  position: absolute;
  inset: -60% auto -60% -40%;
  width: 30%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.65), transparent);
  transform: rotate(16deg);
  animation: brand-sheen 16.4s ease-in-out 1.6s infinite;
  pointer-events: none;
}
@keyframes brand-sheen {
  0%, 88%  { transform: translateX(0) rotate(16deg); }
  96%,100% { transform: translateX(560%) rotate(16deg); }
}

.company-brand:hover { box-shadow: 0 1px 1px rgba(33,33,33,.05), 0 10px 22px -8px rgba(33,33,33,.33), inset 0 1px 0 #fff; }

/* Brand mark inside a chip. Real logos where I have them, a monogram tile
   where I don't — swap either for an <img> and it keeps the same box. */
.brand-logo {
  position: relative;
  z-index: 1;
  align-self: center;
  flex-shrink: 0;
  width: 1.05em;
  height: 1.05em;
  border-radius: 3px;
  overflow: hidden;
}
.brand-logo { display: block; }
.brand-logo svg,
.brand-logo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.brand-logo--mono {
  display: grid;
  place-items: center;
  background: var(--brand-color, var(--color-ink));
  color: #fff;
  font-size: 0.66em;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0;
  border-radius: 3px;
}
.brand-name { position: relative; z-index: 1; }
.home-intro .inline-brand.company-brand { font-size: 1em; }

/* --- hero actions --- */

.home-contact-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 24px;
  margin-top: 24px;
}

.home-contact-button,
.home-resume-button,
.home-booking-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 44px;
  font-size: 15px;
  font-weight: 500;
}

.home-contact-button {
  padding: 10px 20px;
  border-radius: 999px;
  background: var(--color-accent);
  color: var(--color-on-accent);
  transition: background 0.2s var(--ease-calm);
}
.home-contact-button:hover { background: color-mix(in srgb, var(--color-accent) 84%, var(--color-ink)); }


.home-resume-button,
.home-booking-button {
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 5px;
  transition: text-decoration-color 0.2s;
}
.home-resume-button:hover,
.home-booking-button:hover { text-decoration-color: var(--color-accent); }

.home-contact-actions :focus-visible { outline: 2px solid var(--color-ink); outline-offset: 5px; }

/* --- resume hover preview ------------------------------------------------ *
   A peek at page 1 on hover or keyboard focus. The image is only fetched on
   first reveal, so the render costs nothing on load. The card takes the
   pointer only while it is open: it holds a full-screen button and a
   download link now, and closed it must not swallow clicks meant for the
   link underneath.                                                           */

.resume-hint { position: relative; display: inline-flex; }

.resume-peek {
  position: absolute;
  /* Beside the button and hanging DOWN from its top edge. Centred on the
     button it reached 179px above and sat squarely on the headline, which
     is the one thing in the hero worth not covering. */
  top: 0;
  left: calc(100% + 14px);
  z-index: 60;
  display: block;
  width: 300px;
  padding: 8px;
  border: 1px solid var(--color-hairline);
  border-radius: 12px;
  background: var(--color-raised);
  box-shadow: 0 20px 44px -20px rgba(33, 33, 33, 0.42);
  opacity: 0;
  /* The -50% y stays in every state, so the entrance only slides on x.
     --peek-shift is set by script.js to nudge the card back into the
     viewport when centring on the button would hang it off an edge. */
  transform: translate(-8px, var(--peek-shift, 0px)) scale(0.98);
  transform-origin: 0 20%;
  pointer-events: none;
  visibility: hidden;
  transition: opacity 0.18s var(--ease-calm), transform 0.22s var(--ease-calm), visibility 0s linear 0.2s;
}
.resume-hint.is-peeking .resume-peek {
  opacity: 1;
  transform: translate(0, var(--peek-shift, 0px));
  visibility: visible;
  /* The card holds buttons now, so it has to accept the pointer. Only once
     open: a non-interactive card would still swallow clicks meant for the
     page behind it. */
  pointer-events: auto;
  transition-delay: 0s;
}

.resume-peek img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  background: #fff;
}

.resume-peek-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 10px;
  padding: 8px 2px 1px;
}
.resume-peek-meta {
  color: var(--color-soft);
  font-family: var(--font-mono);
  font-size: 11px;
}
.resume-peek-actions {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.resume-peek-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 8px;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--color-ink);
  font: 500 11.5px/1 var(--font-body);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur-tap) var(--ease-calm),
              color var(--dur-tap) var(--ease-calm);
}
.resume-peek-btn svg { width: 13px; height: 13px; flex: none; }
.resume-peek-btn:hover {
  background: color-mix(in srgb, var(--color-accent) 10%, transparent);
  color: var(--color-accent);
}
.resume-peek-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* --- resume, full screen ------------------------------------------------- */

.resume-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(12px, 3vw, 36px);
}
.resume-modal[hidden] { display: none; }

.resume-modal-scrim {
  position: absolute;
  inset: 0;
  background: rgba(14, 13, 12, 0.62);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--dur-ui) var(--ease-calm);
}
.resume-modal.is-open .resume-modal-scrim { opacity: 1; }

.resume-modal-card {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: min(980px, 100%);
  height: min(100%, 94vh);
  overflow: hidden;
  border: 1px solid var(--color-hairline);
  border-radius: 14px;
  background: var(--color-raised);
  box-shadow: 0 32px 80px -28px rgba(0, 0, 0, 0.6);
  opacity: 0;
  transform: translateY(8px) scale(0.99);
  transition: opacity var(--dur-ui) var(--ease-calm),
              transform var(--dur-ui) var(--ease-calm);
}
.resume-modal.is-open .resume-modal-card { opacity: 1; transform: none; }

.resume-modal-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 14px;
  padding: 10px 12px 10px 16px;
  border-bottom: 1px solid var(--color-hairline);
}
.resume-modal-title {
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-ink);
}
.resume-modal-tools { display: inline-flex; align-items: center; gap: 6px; }

.resume-modal-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 0 11px;
  border: 1px solid var(--color-hairline);
  border-radius: 8px;
  background: none;
  color: var(--color-ink);
  font: 500 13px/1 var(--font-body);
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--dur-tap) var(--ease-calm),
              color var(--dur-tap) var(--ease-calm);
}
.resume-modal-btn svg { width: 15px; height: 15px; flex: none; }
.resume-modal-btn:hover { border-color: var(--color-accent); color: var(--color-accent); }
.resume-modal-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* The PDF needs a light sheet behind it in either theme: the file has no
   background of its own and renders black-on-transparent. */
.resume-modal-frame {
  flex: 1;
  width: 100%;
  min-height: 0;
  border: 0;
  background: #fff;
}

/* On a phone the dialog is the whole screen, and the touch targets grow. */
@media (max-width: 600px) {
  .resume-modal { padding: 0; }
  .resume-modal-card { height: 100%; border: 0; border-radius: 0; }
  .resume-modal-btn { min-height: 44px; }
}

@media (prefers-reduced-motion: reduce) {
  .resume-modal-scrim,
  .resume-modal-card { transition: none; }
  .resume-modal-card { transform: none; }
}

/* Touch has no hover, and the popover would just cover the page. */
@media (hover: none), (max-width: 600px) {
  .resume-peek { display: none; }
}

/* --- ask launcher: straddles the hero / next-section rule --- */

.ask-launcher {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translate(-50%, 50%);
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 12px;
  width: calc(100% - (2 * var(--gutter)));
  max-width: var(--measure);
  min-height: 44px;
  padding: 9px 18px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  background: var(--color-raised);
  color: var(--color-ink);
  font: 400 14px/1.5 var(--font-body);
  text-align: left;
  transition: border-color 0.25s;
}
.ask-launcher:hover { border-color: var(--color-ink); }
.ask-launcher > svg { flex-shrink: 0; width: 18px; height: 18px; }

.ask-launcher-copy {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 12px;
  font-size: 14px;
  line-height: 1.5;
}
.ask-hint { color: var(--color-soft); font-size: 12px; }
.ask-go { flex-shrink: 0; color: var(--color-soft); }

/* =========================================================================
   Token calendar
   ========================================================================= */

.activity-cal {
  min-height: 170px;
  margin-top: 28px;
  font-size: 13px;
}

.activity-summary,
.activity-key,
.linkedin-summary,
.linkedin-key {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}

.activity-summary a,
.linkedin-summary a {
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 4px;
  white-space: nowrap;
}
.activity-summary a:hover,
.linkedin-summary a:hover { text-decoration-color: var(--color-accent); }

.activity-count {
  margin-top: 18px;
  color: var(--color-soft);
  font-size: 12px;
}

/* The graph keeps a readable cell size and scrolls sideways when the
   column is too narrow for 53 weeks. */
.activity-scroll {
  margin-top: 8px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.activity-scroll::-webkit-scrollbar { display: none; }

.activity-canvas { min-width: 634px; }

.activity-months {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  color: var(--color-soft);
  font-size: 10px;
}

.activity-legend i {
  display: block;
  aspect-ratio: 1;
  border-radius: 2px;
  background: var(--color-sunken);
  outline: 1px solid var(--color-rule);
  outline-offset: -1px;
}
.activity-legend i[data-level="1"] { background: var(--tok-1); }
.activity-legend i[data-level="2"] { background: var(--tok-2); }
.activity-legend i[data-level="3"] { background: var(--tok-3); }
.activity-legend i[data-level="4"] { background: var(--tok-4); }

.activity-key {
  margin-top: 12px;
  color: var(--color-soft);
  font-size: 11px;
}
.activity-legend { display: inline-flex; align-items: center; gap: 3px; }
.activity-legend i { width: 10px; height: 10px; }

/* =========================================================================
   How I build
   ========================================================================= */

.build-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  gap: clamp(28px, 4.5vw, 56px);
  align-items: start;
}

.build-intro { position: sticky; top: 92px; }
.build-lede { margin-top: 18px; max-width: 34ch; color: var(--color-soft); }

/* The rail is one gradient line down the left of the list. */
.build-steps {
  position: relative;
  margin: 0;
  padding: 4px 0 4px 28px;
  list-style: none;
  counter-reset: build;
}
.build-steps::before {
  content: "";
  position: absolute;
  inset-block: 0;
  left: 0;
  width: 4px;
  border-radius: 999px;
  background: linear-gradient(
    to bottom,
    transparent,
    color-mix(in srgb, var(--color-accent) 55%, transparent) 12%,
    color-mix(in srgb, var(--color-accent) 55%, transparent) 88%,
    transparent
  );
}

.build-steps li { position: relative; counter-increment: build; }
.build-steps li + li { margin-top: clamp(22px, 2.6vw, 34px); }

.build-steps h3 {
  font-size: clamp(1rem, 1.25vw, 1.15rem);
  font-weight: 620;
  letter-spacing: -0.014em;
}
.build-steps p {
  margin-top: 7px;
  max-width: 46ch;
  font-size: 14.5px;
  line-height: 1.65;
  color: var(--color-soft);
}

/* A dot on the rail, aligned to each step title. */
.build-steps li::before {
  content: "";
  position: absolute;
  left: -30px;
  top: 0.55em;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 4px var(--color-ground);
}

@media (max-width: 860px) {
  .build-grid { grid-template-columns: minmax(0, 1fr); gap: 28px; }
  .build-intro { position: static; }
  .build-lede { max-width: none; }
}

/* =========================================================================
   Career grid
   ========================================================================= */

.career-grid {
  margin-top: 36px;
  border-top: 1px solid var(--color-hairline);
}

.career-entry {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  border-bottom: 1px solid var(--color-hairline);
  scroll-margin-top: 88px;
}

.career-position {
  padding: 24px 24px 32px 0;
  border-right: 1px solid var(--color-hairline);
}

@media (min-width: 601px) {
  .career-position-inner { position: sticky; top: 88px; }
}

.career-company { display: flex; align-items: center; gap: 10px; }
.career-logo {
  /* Block, not grid: an auto grid row would size itself to the image and then
     height:100% would resolve against that row instead of this 30px box. */
  display: block;
  flex-shrink: 0;
  width: 30px;
  height: 30px;
}
/* Logos are not all square, so the image fills the well and object-fit
   letterboxes it inside. */
.career-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.career-position h3 { font-size: 23px; font-weight: 500; line-height: 1.65; }

.career-origin {
  display: block;
  margin-top: 4px;
  font: 400 13px/1.6 var(--font-body);
  color: var(--color-soft);
}

.career-period { margin-top: 8px; font-size: 13px; color: var(--color-soft); }

.career-roles {
  margin-top: 24px;
  font-size: 13px;
  line-height: 1.6;
}
.career-roles li {
  position: relative;
  padding-left: 15px;
}
.career-roles li + li { margin-top: var(--role-gap); }

.career-roles li::before {
  content: "";
  position: absolute;
  left: 0;
  top: var(--role-dot-top);
  z-index: 1;
  width: var(--role-dot-size);
  height: var(--role-dot-size);
  border-radius: 50%;
  background: var(--color-ink);
}
.career-roles li:not(:last-child)::after {
  content: "";
  position: absolute;
  left: calc((var(--role-dot-size) - 1px) / 2);
  top: calc(var(--role-dot-top) + var(--role-dot-size));
  bottom: calc(-1 * (var(--role-gap) + var(--role-dot-top)));
  width: 1px;
  background: var(--color-hairline);
}
.career-roles small {
  display: block;
  margin-top: 3px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--color-soft);
}

.career-projects { min-width: 0; }

.career-project-list li { padding: 15px 24px; }
.career-project-list li + li { border-top: 1px solid var(--color-hairline); }
.career-project-list .career-study-row { padding: 0; }

.career-project-heading {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
}
.career-project-list h4 { font-size: 16px; font-weight: 500; }
.career-project-list p { margin-top: 6px; font-size: 14px; line-height: 1.6; }
.career-project-metric { margin-top: 10px; font-weight: 500; }

/* --- featured case-study card --- */

.project-card {
  display: block;
  padding: 18px 24px;
  transition: background 0.24s var(--ease-calm);
}
.project-card:hover { background: color-mix(in srgb, var(--color-sunken) 55%, transparent); }
.project-card:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }

.project-card .home-project-name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
  margin: 0 0 12px;
}

.project-card .home-project-title {
  margin: 0;
  font: var(--display-wght) clamp(1.35rem, 1.9vw, 1.65rem) / 1.3 var(--font-display);
  font-optical-sizing: auto;
  letter-spacing: -0.022em;
  max-width: 36rem;
}

.project-card .project-card-description {
  margin-top: 10px;
  font-size: 15px;
  line-height: 1.6;
}

.home-project-action {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 10px;
  font-size: 14px;
  text-decoration: underline;
  text-decoration-color: var(--color-accent);
  text-underline-offset: 5px;
}
.project-card:hover .home-project-action { text-decoration-thickness: 2px; }

/* --- card tags --- */

/* Flat and squared rather than stadium pills: these cards are plain blocks
   separated by rules, so a rounded bubble reads as borrowed app chrome. */
.project-card-tag,
.project-card-status,
.writing-read {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  border: 0;
  border-radius: 4px;
  background: var(--color-sunken);
  color: var(--color-soft);
  font: 500 11px/1.45 var(--font-body);
  letter-spacing: 0.012em;
  white-space: nowrap;
}
/* Both tags carry the same weight: this is metadata above the headline, not a
   badge competing with it. The fill is mixed from ink rather than using
   --color-sunken directly, because .project-card:hover tints the whole card
   with sunken and the chip would dissolve into it. */
.project-card-tag {
  background: color-mix(in srgb, var(--color-ink) 9%, var(--color-raised));
  color: var(--color-ink);
}

.project-card-status--live   { color: var(--status-live-fg); background: var(--status-live-bg); }
.project-card-status--wip    { color: var(--status-wip-fg);  background: var(--status-wip-bg);  }
.project-card-status--sunset { color: var(--status-off-fg);  background: var(--status-off-bg);  }

/* =========================================================================
   Writings
   ========================================================================= */

.writing-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}
.writing-all {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--color-soft);
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 4px;
}
.writing-all:hover { color: var(--color-ink); text-decoration-color: var(--color-accent); }

.writing-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  margin-top: 28px;
  list-style: none;
}

/* Same shell as .li-card next door, so the two sections read as one system. */
.writing-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 18px;
  border: 1px solid var(--color-hairline);
  border-radius: 14px;
  background: var(--color-raised);
  transition: border-color 0.2s var(--ease-calm), transform 0.24s var(--ease-calm);
}
.writing-card:hover { border-color: color-mix(in srgb, var(--color-accent) 45%, var(--color-hairline)); transform: translateY(-2px); }
.writing-card:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Kept as a column wrapper so cover art can go back above the text without
   restructuring the card. */
.writing-top { display: flex; flex-direction: column; }

/* Currently unused: the generated cover art was removed from the cards. Left
   in place because restoring real artwork is then a one-line markup change.
   A tinted block stands in for artwork. Drop an <img> in and it fills. */
.writing-thumb {
  flex-shrink: 0;
  width: 100%;
  /* The source art is square at 168px. A letterbox crop keeps the upscale
     modest and stops the card being swallowed by a full-width square. */
  aspect-ratio: 16 / 10;
  border-radius: 10px;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 22% 14%, color-mix(in srgb, var(--color-accent) 20%, transparent), transparent 62%),
    var(--color-sunken);
}
.writing-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }

.writing-lede { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; min-width: 0; }
.writing-read { font-family: var(--font-mono); letter-spacing: 0; }

.writing-title {
  font: 600 clamp(1.0625rem, 1.4vw, 1.1875rem) / 1.35 var(--font-display);
  letter-spacing: -0.014em;
  text-wrap: balance;
}
.writing-card:hover .writing-title { color: var(--color-accent); }

.writing-excerpt {
  margin-top: 16px;
  color: var(--color-soft);
  font-size: 14px;
  line-height: 1.6;
}

.writing-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  /* margin-top:auto pins the footer to the bottom, so cards of unequal
     excerpt length still line their dates up across the row. */
  margin-top: auto;
  padding-top: 16px;
  border-top: 1px solid var(--color-hairline);
}
.writing-date { font-family: var(--font-mono); font-size: 12px; color: var(--color-soft); }
.writing-action {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 5px;
}
.writing-card:hover .writing-action { text-decoration-color: var(--color-accent); }

/* =========================================================================
   LinkedIn feed
   ========================================================================= */

.linkedin-feed { margin-top: 28px; font-size: 13px; }

.li-track {
  display: flex;
  gap: 8px;
  margin-top: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
.li-track::-webkit-scrollbar { display: none; }

.li-card {
  flex: 0 0 min(464px, 86%);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-hairline);
  border-radius: 14px;
  background: var(--color-raised);
  overflow: hidden;
}

.li-head { display: flex; gap: 10px; padding: 14px 14px 0; }

.li-avatar {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: url("assets/portrait.jpg") center / cover var(--color-sunken);
}

.li-who { min-width: 0; display: block; }
.li-who strong { display: block; font-size: 13px; font-weight: 600; line-height: 1.4; }
.li-who small {
  display: block;
  color: var(--color-soft);
  font-size: 11px;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.li-date { margin-top: 2px; }

.li-body {
  padding: 12px 14px;
  font-size: 13px;
  line-height: 1.55;
}
.li-more {
  color: var(--color-soft);
  font-size: 13px;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.li-more:hover { color: var(--color-linkedin); }

.li-media {
  height: 210px;
  background:
    radial-gradient(120% 90% at 18% 12%, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent 60%),
    var(--color-sunken);
}
/* Without this an <img> renders at its natural size and bursts out of the
   block. The tinted gradient above stays as the backdrop while it loads. */
.li-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.li-foot {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  /* The media block used to carry margin-top:auto and push this down. It is
     only rendered when a post has an image now, so the footer pins itself. */
  margin-top: auto;
  padding: 10px 14px;
  border-top: 1px solid var(--color-hairline);
  color: var(--color-soft);
  font-size: 11px;
}

.linkedin-key { padding-top: 12px; font-size: 11px; }

.linkedin-dots { display: flex; gap: 6px; }
.linkedin-dots button {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--color-hairline);
  transition: background 0.2s, transform 0.25s var(--ease-calm);
}
.linkedin-dots button.is-active { transform: scale(1.3); }

.linkedin-dots button.is-active { background: var(--color-linkedin); }

.linkedin-arrows { display: flex; gap: 8px; }
.linkedin-arrows button {
  min-width: 28px;
  height: 28px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  background: var(--color-raised);
  font-size: 14px;
  line-height: 1;
  transition: border-color 0.2s, color 0.2s, background 0.2s;
}
.linkedin-arrows button:hover:not(:disabled) {
  border-color: var(--color-accent);
  color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 8%, var(--color-raised));
}
.linkedin-arrows button:disabled { opacity: 0.35; cursor: default; }

/* Touch sizing. This has to sit after the rules above: a media query adds no
   specificity, so declared first it simply lost to them.

   The dots stay 7px; what you can hit does not. The hit box is absolute, so
   it costs no layout, but two 44px boxes 29px apart overlap and a tap near
   the seam picks the wrong post. Vertical room is free, horizontal is not,
   so it goes tall and takes only the pitch the row can spare. */
@media (hover: none) {
  .linkedin-dots { gap: 26px; }
  .linkedin-dots button { position: relative; }
  .linkedin-dots button::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 32px;
    height: 44px;
    transform: translate(-50%, -50%);
  }
  .linkedin-arrows button { min-width: 44px; height: 44px; }
}

.li-track:focus-visible,
.linkedin-dots button:focus-visible,
.linkedin-arrows button:focus-visible,
.li-more:focus-visible { outline: 2px solid var(--color-linkedin); outline-offset: 2px; }

/* =========================================================================
   Footer
   ========================================================================= */

.home-footer {
  border-top: 1px solid var(--color-hairline);
  padding-block: 32px 48px;
  font-size: 14px;
}
.home-footer a {
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 4px;
}
.home-footer a:hover { text-decoration-color: var(--color-accent); }
.home-footer span { color: var(--color-soft); padding-inline: 2px; }

/* =========================================================================
   Responsive
   ========================================================================= */

/* Tablet — the rail narrows before it collapses. */
@media (max-width: 860px) {
  :root { --rail: 178px; }
  .career-position { padding-right: 20px; }
  .career-project-list li,
  .project-card { padding-inline: 20px; }
}

@media (max-width: 760px) {
  .li-card { flex-basis: min(420px, 88%); }
  .writing-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Phone */
@media (max-width: 600px) {
  :root { --header-h: auto; }

  .header-bar {
    min-height: 56px;
    padding-block: 6px;
  }

  .site-signature { width: auto; min-width: 104px; }
  .site-signature-mark { font-size: 19px; }
  .site-signature-name { display: none; }

  /* Single condensed row — labels collapse to icons rather than wrapping. */
  .site-nav {
    flex-wrap: nowrap;
    gap: 14px;
    font-size: 12px;
  }
  /* 44px is the touch minimum. Width cannot reach it at this size without
     dropping an item, but the height is free and vertical slop is what a
     horizontal row of small icons actually gets mis-tapped on. */
  .site-nav > a,
  .site-nav > button { min-height: 44px; }
  .nav-ask .nav-label,
  .agent-mode-link .nav-label { display: none; }
  .nav-ask svg { width: 17px; height: 17px; }

  .home-heading { gap: 14px; }
  .home-title { font-size: clamp(1.9rem, 7.5vw, 2.75rem); }
  /* No padding: the photo is object-fit:cover, so any padding here shows the
     widget's own background as a ring around it. */
  .hero-widget { width: 58px; }

  .home-intro { font-size: 15px; line-height: 1.85; }

  .ask-launcher { padding: 9px 14px; gap: 10px; }
  .ask-launcher-copy { display: block; }
  /* The hint is four topics long and wrapped to three lines in a pill this
     narrow. "Ask me anything." carries the affordance on its own, and dropping
     it from the a11y tree shortens the button's name rather than padding it. */
  .ask-hint { display: none; }

  .activity-months { font-size: 9px; }

  .career-entry { grid-template-columns: 1fr; }
  .career-position {
    padding: 24px 0;
    border-right: 0;
    border-bottom: 1px solid var(--color-hairline);
  }
  .career-roles { margin-top: 16px; }
  .career-project-list li,
  .project-card { padding: 24px 0; }
  .project-card:hover { background: none; }

  .li-card { flex-basis: 86%; }
  .li-media { height: 180px; }
}

@media (max-width: 380px) {
  .site-nav { gap: 11px; }
  .home-contact-actions { gap: 10px 18px; }
}

/* =========================================================================
   Interactions
   Ported from the reference: energy packets, scroll reveal, cursor preview,
   magnetic chips and their tooltips, the docked ask pill and the ask sheet.
   ========================================================================= */

@keyframes content-enter      { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: translateY(0); } }
@keyframes brand-tooltip-enter{ from { opacity: 0; transform: translateY(9px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes brand-tooltip-exit { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(5px); } }
@keyframes ask-enter          { from { opacity: 0; transform: translateY(100px); } to { opacity: 1; transform: translateY(0); } }
@keyframes ask-dock-enter     { from { opacity: 0; transform: translate(-50%, 20px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes document-in        { from { opacity: 0; } to { opacity: 1; } }

/* --- energy packets travelling the rules --------------------------------- */

.grid-energy {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
}
.grid-energy-packet {
  position: absolute;
  border-radius: 999px;
  background: var(--color-accent);
  opacity: 0;
}

/* --- scroll reveal ------------------------------------------------------- */

[data-reveal-pending="true"] { opacity: 0; }

/* --- cursor preview over a project --------------------------------------- */

.project-cursor-preview {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  width: 320px;
  height: 213px;
  border-radius: 10px;
  background: var(--color-sunken);
  box-shadow: 0 16px 36px -14px rgba(33, 33, 33, 0.33);
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  transition: opacity 0.16s;
}
.project-cursor-preview.is-visible { opacity: 1; }
.project-cursor-preview img,
.project-cursor-preview video { width: 100%; height: 100%; object-fit: cover; }

.project-preview-placeholder {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 10px;
  width: 100%;
  height: 100%;
  padding: 16px;
  color: var(--color-ink);
  text-align: center;
}
.project-preview-placeholder strong { font: 400 24px/1.1 var(--font-display); }
.project-preview-placeholder > span { color: var(--color-soft); font: 400 12px/1.4 var(--font-body); }

/* Card contents drift very slightly toward the cursor. */
.project-card > *,
.career-project-list li > * {
  transform: translate(var(--project-x, 0px), var(--project-y, 0px));
  transition: transform 0.52s var(--ease-calm);
}
.career-study-row > .project-card { transform: none; transition: none; }

@media (hover: none), (max-width: 759px) {
  .project-cursor-preview { display: none; }
}

/* --- magnetic brand chips ------------------------------------------------ */

.brand-wrap { display: inline-flex; }

.company-brand {
  --tx: 0px;
  --ty: 0px;
  --mx: 50%;
  --my: 0%;
  transform: translate3d(var(--tx), var(--ty), 0);
  transition: transform 0.52s var(--ease-calm), box-shadow 0.32s var(--ease-calm);
  font: inherit;
  /* The `font` shorthand above resets line-height to the paragraph's 1.95, which
     makes the chip taller than the line box and springs the leading apart.
     Restore the chip's own leading after it. */
  line-height: 1.45;
  cursor: pointer;
}

/* Spotlight that tracks the pointer across the chip. */
.company-brand::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(80px 40px at var(--mx) var(--my), #fff, transparent 70%);
  opacity: 0;
  transition: opacity 0.32s var(--ease-calm);
  pointer-events: none;
}
.brand-wrap:hover .company-brand::before { opacity: 1; }
.theme-dark .company-brand::before {
  background: radial-gradient(80px 40px at var(--mx) var(--my), rgba(255, 255, 255, 0.14), transparent 70%);
}

/* --- brand tooltip ------------------------------------------------------- */

.brand-tooltip {
  position: fixed;
  z-index: 100;
  width: max-content;
  max-width: min(310px, 100vw - 32px);
  padding: 18px 20px;
  border-radius: 12px;
  background: var(--color-raised);
  color: var(--color-ink);
  font: 400 13px/1.5 var(--font-body);
  text-align: left;
  transform-origin: var(--arrow-x) bottom;
  box-shadow: 0 8px 32px -8px rgba(33, 33, 33, 0.22), 0 2px 6px rgba(33, 33, 33, 0.05);
  animation: brand-tooltip-enter 0.48s var(--ease-calm) both;
}
.theme-dark .brand-tooltip {
  box-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--color-hairline);
}
.brand-tooltip.is-closing { animation: brand-tooltip-exit 0.2s ease-in both; pointer-events: none; }

.brand-tooltip::after {
  content: "";
  position: absolute;
  bottom: -5px;
  left: var(--arrow-x);
  width: 10px;
  height: 10px;
  background: var(--color-raised);
  transform: translateX(-50%) rotate(45deg);
}
/* Bridges the gap so the pointer can travel into the tooltip. */
.brand-tooltip::before { content: ""; position: absolute; inset: auto 0 -16px; height: 16px; }

.brand-tooltip ul { margin: 0; padding: 0; list-style: none; }
.brand-tooltip li { position: relative; padding-left: 16px; }
.brand-tooltip li + li { margin-top: var(--role-gap); }
.brand-tooltip li::before {
  content: "";
  position: absolute;
  left: 0;
  top: var(--role-dot-top);
  z-index: 1;
  width: var(--role-dot-size);
  height: var(--role-dot-size);
  border-radius: 50%;
  background: var(--brand-color, var(--color-ink));
}
.brand-tooltip li:not(:last-child)::after {
  content: "";
  position: absolute;
  left: calc((var(--role-dot-size) - 1px) / 2);
  top: calc(var(--role-dot-top) + var(--role-dot-size));
  bottom: calc(-1 * (var(--role-gap) + var(--role-dot-top)));
  width: 1px;
  background: var(--color-hairline);
}
.tooltip-title { display: block; font-weight: 500; }
.tooltip-date  { display: block; margin-top: 3px; font-size: 12px; color: var(--color-soft); }

/* =========================================================================
   Proof band + career timeline
   Two rule-ruled blocks that borrow the page's existing gutter alignment:
   the first number and the first period both start where the headline does,
   so the hairlines read as a table rather than a floating card.
   ========================================================================= */

.eyebrow {
  color: var(--color-soft);
  font-size: 11.5px;
  font-weight: 640;
  letter-spacing: 0.13em;
  line-height: 1.4;
  text-transform: uppercase;
}

/* ---- proof band ---- */

/* Both blocks stay on --measure. body::before draws the dotted verticals at
   784px, 32px outside that column, and a table that crosses them reads as a
   mistake rather than emphasis — so the layout adapts to the measure instead
   of the measure widening for the layout. */

/* Only ul is reset globally, and both of these are ol. */
.proof-grid, .timeline { padding: 0; list-style: none; }

.proof-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
  align-items: start;
  gap: clamp(20px, 4vw, 72px);
}
.proof-title .t-section { margin-top: 10px; }
.proof-note {
  max-width: 44ch;
  color: var(--color-soft);
  font-size: clamp(0.9375rem, 1.3vw, 1.0625rem);
  line-height: 1.62;
}

/* Two across rather than the four of the reference this came from: inside a
   720px measure a quarter-column is ~180px, which wraps "1 month" onto two
   lines and stacks the captions four deep. Half-columns keep the numerals
   full size, which is the whole point of the band. */
.proof-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-top: clamp(34px, 5vw, 60px);
  border-top: 1px solid var(--color-hairline);
  border-bottom: 1px solid var(--color-hairline);
}
.proof-grid > li { padding: clamp(22px, 3vw, 32px) clamp(18px, 2.5vw, 34px); }
/* The left column sits on the gutter, in line with the headline above it. */
.proof-grid > li:nth-child(odd) { padding-left: 0; }
.proof-grid > li:nth-child(even) { border-left: 1px solid var(--color-hairline); }
.proof-grid > li:nth-child(n + 3) { border-top: 1px solid var(--color-hairline); }
.proof-grid b {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--display-wght);
  font-optical-sizing: auto;
  font-size: clamp(2.125rem, 4.2vw, 3.25rem);
  line-height: 1;
  letter-spacing: -0.032em;
}
.proof-grid span {
  display: block;
  margin-top: 13px;
  color: var(--color-soft);
  font-size: 13.5px;
  line-height: 1.5;
}

/* ---- career timeline ---- */

.career-head .t-section { margin-top: 10px; }
.career-lede { max-width: 58ch; margin-top: 14px; color: var(--color-soft); }

.timeline {
  margin-top: clamp(30px, 4.5vw, 54px);
  border-top: 1px solid var(--color-hairline);
}
.timeline > li {
  display: grid;
  /* Tighter than the reference's proportions, because the whole row now has
     720px to spend instead of 1250. The first column is a measured constant,
     not a fraction: "Sep 2023 — Nov 2023" sets at 151px in the eyebrow's size
     and tracking, and a date range that wraps mid-range reads as broken. */
  grid-template-columns: 152px minmax(0, 1fr) minmax(0, 1.3fr);
  align-items: start;
  gap: clamp(12px, 2vw, 26px);
  padding: clamp(22px, 3vw, 34px) 0;
  border-bottom: 1px solid var(--color-hairline);
}
.timeline-period {
  padding-top: 7px;
  color: var(--color-accent);
  font-size: 11.5px;
  font-weight: 640;
  letter-spacing: 0.1em;
  line-height: 1.4;
  text-transform: uppercase;
}
.timeline-role { display: flex; align-items: flex-start; gap: 12px; }
.timeline-role h3 {
  font-family: var(--font-display);
  font-weight: var(--display-wght);
  font-optical-sizing: auto;
  font-size: clamp(1.0625rem, 1.75vw, 1.375rem);
  line-height: 1.22;
  letter-spacing: -0.022em;
  text-wrap: balance;
}
.timeline-org { margin-top: 5px; color: var(--color-soft); font-size: 13px; line-height: 1.45; }
.timeline-note {
  padding-top: 5px;
  max-width: 46ch;
  color: var(--color-soft);
  font-size: 14.5px;
  line-height: 1.6;
}

/* ---- narrower: the third column folds under the role ---- */
/* Below this the head stacks and the timeline drops to two columns, with the
   note folded under the role it belongs to. */
@media (max-width: 900px) {
  .proof-head { grid-template-columns: minmax(0, 1fr); gap: 18px; }

  .timeline > li { grid-template-columns: 152px minmax(0, 1fr); }
  .timeline-note { grid-column: 2; padding-top: 0; max-width: none; }
}

@media (max-width: 560px) {
  .proof-grid { grid-template-columns: minmax(0, 1fr); }
  .proof-grid > li,
  .proof-grid > li:nth-child(odd) { padding: 20px 0; }
  .proof-grid > li:nth-child(even) { border-left: 0; }
  .proof-grid > li + li { border-top: 1px solid var(--color-hairline); }

  .timeline > li { grid-template-columns: minmax(0, 1fr); gap: 10px; }
  .timeline-period { padding-top: 0; }
  .timeline-note { grid-column: 1; }
}

/* --- docked ask pill ----------------------------------------------------- */

/* Retired. The sticky header already shows "Ask me anything" at every width
   above 600px, and the bottom bar carries Ask below it, so the pill was a
   third copy of the same entry point, parked over the content. The markup
   and its script stay so it can come back by deleting this one line. */
.ask-docked { display: none !important; }

.ask-docked {
  position: fixed;
  left: 50%;
  bottom: max(20px, env(safe-area-inset-bottom));
  z-index: 60;
  display: flex;
  align-items: center;
  /* The face sits close to the label; the arrow keeps its original 40px of
     air via its own margin, so the pill reads as one thought plus an action. */
  gap: 10px;
  min-height: 44px;
  padding: 11px 20px 11px 11px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  background: var(--color-raised);
  color: var(--color-ink);
  font: 400 14px/1.5 var(--font-body);
  white-space: nowrap;
  box-shadow: 0 8px 30px -8px rgba(33, 33, 33, 0.27);
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 20px);
  transition: opacity 0.4s, transform 0.5s var(--ease-calm), visibility 0.4s;
}
.ask-docked.is-visible { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
.ask-docked.is-visible:hover { transform: translate(-50%, 0) scale(1.045); border-color: var(--color-ink); }
.ask-docked > svg { flex-shrink: 0; width: 18px; height: 18px; margin-left: 30px; }

.ask-docked-face {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: url("assets/portrait.jpg") center / cover var(--color-sunken);
}

@media (max-width: 600px) {
  .ask-docked { gap: 8px; padding: 8px 18px 8px 8px; }
  .ask-docked > svg { margin-left: 16px; }
  .ask-docked-face { width: 24px; height: 24px; }
}

/* --- ask widget: a side panel, not a takeover ---------------------------- */

@keyframes ask-panel-in  { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: none; } }
@keyframes ask-panel-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(100%); } }
/* Phones get a sheet instead: it rises from the edge the thumb is nearest. */
@keyframes ask-sheet-in  { from { opacity: 0; transform: translateY(100%); } to { opacity: 1; transform: none; } }
@keyframes ask-sheet-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(100%); } }
/* ask-card-in still drives the phone sheet, which rises rather than slides. */
@keyframes ask-card-in { from { opacity: 0; transform: translateY(14px) scale(0.975); } to { opacity: 1; transform: none; } }
@keyframes ask-msg-in  { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes ask-dot     { 0%, 80%, 100% { opacity: .3; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }

.ask-widget[hidden] { display: none; }
.ask-widget {
  position: fixed;
  inset: 0;
  z-index: 80;
  pointer-events: none;   /* only the card takes events on desktop */
}

/* Scrim exists for phones only, where a floating card has nowhere to float. */
.ask-scrim { display: none; }

.ask-card {
  /* A local value rather than --header-h: that token is `auto` on phones, and
     an `auto` top against a fixed bottom would collapse the panel to nothing.
     This branch only runs above 600px, where the header is exactly this tall,
     so the panel tucks under it and leaves the nav clickable. */
  --ask-top: 56px;
  --ask-shadow: -26px 0 60px -34px rgba(28, 27, 25, 0.45);
  position: absolute;
  top: var(--ask-top);
  right: 0;
  bottom: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  width: clamp(340px, 30vw, 440px);
  border-left: 1px solid var(--color-hairline);
  background: var(--color-raised);
  overflow: hidden;
  pointer-events: auto;
  animation: ask-panel-in 0.36s cubic-bezier(0.16, 1, 0.3, 1) both;
  box-shadow: var(--ask-shadow);
}
/* JS holds the panel open for the length of this, then sets [hidden]. */
.ask-card.is-leaving { animation: ask-panel-out 0.22s var(--ease-exit) both; }
.theme-dark .ask-card { --ask-shadow: -26px 0 64px -30px rgba(0, 0, 0, 0.85); }

/* The docked pill would sit beside an open panel arguing for attention. */
.ask-open .ask-docked { opacity: 0; visibility: hidden; pointer-events: none; }

/* --- ask panel: chrome ---------------------------------------------------
   The reference this follows is a floating assistant card. Two things it does
   are worth keeping: it opens on a face and a short set of icon chips rather
   than a blank field, and it keeps a quiet utility row under the composer.
   Everything else here is the portfolio's, not the reference's: same tokens,
   same hairlines, same radii, and it stays a side panel at every width. */

.ask-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 12px 12px 16px;
  border-bottom: 1px solid var(--color-hairline);
}
.ask-head-id { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ask-head-text strong {
  display: block;
  font-size: 14.5px;
  font-weight: 620;
  line-height: 1.3;
  letter-spacing: -0.012em;
}

/* Clear and close, in that order: the destructive one first so the close
   button stays where the eye already expects it, hard against the corner. */
.ask-head-actions { display: flex; align-items: center; gap: 2px; }
.ask-close[hidden] { display: none; }

.ask-avatar {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: url("assets/portrait.jpg") center / cover var(--color-sunken);
}

.ask-close {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  color: var(--color-soft);
  transition: background 0.18s, color 0.18s;
}
.ask-close:hover { background: var(--color-sunken); color: var(--color-ink); }
.ask-close svg { width: 16px; height: 16px; }

/* ---- conversation ---- */
.ask-conversation {
  min-height: 0;
  padding: 20px 18px 8px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--color-hairline) transparent;
}
.ask-conversation::-webkit-scrollbar { width: 8px; }
.ask-conversation::-webkit-scrollbar-thumb {
  border: 2px solid transparent;
  border-radius: 8px;
  background: var(--color-hairline);
  background-clip: content-box;
}

/* ---- empty state: a face, a sentence, and somewhere to start ---- */
/* With nothing said yet the panel is mostly air, so the greeting sits in
   the middle of it rather than clinging to the top. Once a message exists
   the intro is hidden, the selector stops matching, and the log goes back
   to ordinary top-down flow. */
.ask-conversation:has(> .ask-intro:not([hidden])) {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.ask-intro { padding: 0 2px; text-align: center; }
.ask-intro[hidden] { display: none; }

.ask-orb {
  display: block;
  width: 56px;
  height: 56px;
  margin: 0 auto 16px;
  border-radius: 50%;
  background: url("assets/portrait.jpg") center / cover var(--color-sunken);
  box-shadow:
    0 0 0 1px var(--color-hairline),
    0 0 0 7px color-mix(in srgb, var(--color-accent) 11%, transparent);
}

.ask-greeting {
  font-size: 19px;
  font-weight: 620;
  letter-spacing: -0.024em;
  line-height: 1.25;
}
.ask-sub {
  max-width: 34ch;
  margin: 7px auto 0;
  color: var(--color-soft);
  font-size: 13px;
  line-height: 1.55;
}

/* Two columns of icon chips, which is what fits a panel of this width
   without the labels wrapping. */
.ask-starters {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px;
  margin-top: 20px;
  text-align: left;
}
.ask-starters button {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  min-height: 38px;
  padding: 9px 11px;
  border: 1px solid var(--color-hairline);
  border-radius: 10px;
  background: var(--color-raised);
  color: var(--color-ink);
  font: 500 12.5px/1.3 var(--font-body);
  letter-spacing: -0.006em;
  text-align: left;
  transition: border-color var(--dur-tap) var(--ease-calm),
              background var(--dur-tap) var(--ease-calm),
              transform var(--dur-tap) var(--ease-calm);
}
.ask-starters button svg {
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  color: var(--color-accent);
}
.ask-starters button:hover {
  border-color: color-mix(in srgb, var(--color-accent) 45%, var(--color-hairline));
  background: color-mix(in srgb, var(--color-accent) 6%, var(--color-raised));
  transform: translateY(-1px);
}

/* ---- messages ---- */
.ask-msg { display: flex; gap: 9px; animation: ask-msg-in 0.28s var(--ease-calm) both; }
.ask-msg + .ask-msg { margin-top: 16px; }
/* A reply stops short of the opposite edge. Running the full width of the
   panel made it read as page copy rather than something said. */
.ask-msg-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
  max-width: 92%;
}
.ask-msg--you .ask-bubble { max-width: 86%; }

.ask-msg-avatar {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: url("assets/portrait.jpg") center / cover var(--color-sunken);
}

.ask-bubble {
  padding: 11px 14px;
  border-radius: 15px 15px 15px 5px;
  /* Sunken rather than raised: the panel itself is raised, so a reply
     needs to sit into it rather than float invisibly on top. */
  background: var(--color-sunken);
  font-size: 13.5px;
  line-height: 1.62;
}
.ask-bubble p + p { margin-top: 9px; }

.ask-msg--you { justify-content: flex-end; }
.ask-msg--you .ask-bubble {
  border-radius: 15px 15px 5px 15px;
  background: var(--color-accent);
  color: var(--color-on-accent);
}

/* ---- the hover toolbar, lifted from the reference ---- */
.ask-actions {
  display: flex;
  gap: 2px;
  margin: 5px 0 0 2px;
  opacity: 0;
  transform: translateY(-2px);
  transition: opacity var(--dur-ui) var(--ease-calm), transform var(--dur-ui) var(--ease-calm);
}
.ask-msg--reply:hover .ask-actions,
.ask-actions:focus-within { opacity: 1; transform: none; }
.ask-action {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  color: var(--color-soft);
  transition: background var(--dur-tap), color var(--dur-tap);
}
.ask-action svg { width: 15px; height: 15px; }
.ask-action:hover { background: var(--color-sunken); color: var(--color-ink); }
.ask-action.is-done { color: var(--color-accent); }

/* Touch has no hover, so the toolbar simply stays. */
@media (hover: none) {
  .ask-actions { opacity: 1; transform: none; }
}

/* ---- a link is a destination, so it gets a card ---- */
.ask-source {
  display: flex;
  align-items: center;
  gap: 9px;
  align-self: stretch;
  margin-top: 9px;
  padding: 10px 12px;
  border: 1px solid var(--color-hairline);
  border-radius: 11px;
  background: var(--color-sunken);
  color: var(--color-ink);
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.35;
  transition: border-color var(--dur-tap) var(--ease-calm), background var(--dur-tap) var(--ease-calm);
}
.ask-source svg { flex-shrink: 0; width: 16px; height: 16px; color: var(--color-accent); }
.ask-source-label { flex: 1; min-width: 0; }
.ask-source-go { flex-shrink: 0; color: var(--color-soft); }
.ask-source:hover {
  border-color: color-mix(in srgb, var(--color-accent) 50%, var(--color-hairline));
  background: color-mix(in srgb, var(--color-accent) 7%, var(--color-raised));
}

/* ---- follow-ups, so the conversation always has somewhere to go ---- */
.ask-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.ask-chip {
  padding: 6px 12px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  background: var(--color-raised);
  color: var(--color-soft);
  font: 500 12px/1.35 var(--font-body);
  white-space: nowrap;
  text-align: left;
  transition: border-color var(--dur-tap) var(--ease-calm),
              color var(--dur-tap) var(--ease-calm),
              background var(--dur-tap) var(--ease-calm);
}
.ask-chip:hover {
  border-color: color-mix(in srgb, var(--color-accent) 50%, var(--color-hairline));
  background: color-mix(in srgb, var(--color-accent) 6%, var(--color-raised));
  color: var(--color-ink);
}

/* ---- typing ---- */
.ask-typing { display: inline-flex; gap: 4px; padding: 3px 0; }
.ask-typing i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-soft);
  animation: ask-dot 1.25s infinite;
}
.ask-typing i:nth-child(2) { animation-delay: 0.14s; }
.ask-typing i:nth-child(3) { animation-delay: 0.28s; }

/* ---- composer ---- */
.ask-foot {
  border-top: 1px solid var(--color-hairline);
  background: var(--color-raised);
}
.ask-composer {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  /* Nothing sits below it now, so it carries the home indicator clearance. */
  padding: 12px 12px max(12px, env(safe-area-inset-bottom));
}
.ask-composer textarea {
  flex: 1;
  min-width: 0;
  /* One line of text plus the padding and borders, since box-sizing is
     border-box. Without this the field collapses to padding height if the
     autosize ever measures it while the panel is hidden. */
  min-height: 42px;
  max-height: 104px;
  padding: 11px 14px;
  border: 1px solid var(--color-hairline);
  border-radius: 14px;
  outline: none;
  background: var(--color-ground);
  color: inherit;
  font: inherit;
  font-size: 13.5px;
  line-height: 1.5;
  resize: none;
  overflow-y: auto;
  scrollbar-width: none;
  transition: border-color var(--dur-tap) var(--ease-calm);
}
.ask-composer textarea::-webkit-scrollbar { display: none; }
.ask-composer textarea:focus {
  border-color: color-mix(in srgb, var(--color-accent) 55%, var(--color-hairline));
}
.ask-composer textarea::placeholder { color: var(--color-soft); }

.ask-composer button {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  margin-bottom: 2px;
  border-radius: 50%;
  background: var(--color-accent);
  color: var(--color-on-accent);
  transition: background var(--dur-tap) var(--ease-calm), transform var(--dur-tap) var(--ease-calm);
}
.ask-composer button:hover:not(:disabled) {
  background: color-mix(in srgb, var(--color-accent) 84%, var(--color-ink));
  transform: scale(1.06);
}
.ask-composer button:disabled { background: var(--color-sunken); color: var(--color-soft); cursor: default; }
.ask-composer button svg { width: 17px; height: 17px; }

.ask-card button:focus-visible,
.ask-card a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ---- phones: full screen, rising from the bottom ----
       A side entrance on a 375px viewport fights the platform: sheets on a
       phone rise from the bottom, where the thumb already is. The panel then
       takes the whole screen, so the conversation gets every pixel rather
       than competing with a page nobody is reading at that point. */
@media (max-width: 600px) {
  .ask-widget { pointer-events: auto; }
  .ask-scrim {
    display: block;
    position: absolute;
    inset: 0;
    background: rgba(28, 27, 25, 0.28);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    animation: document-in 0.28s both;
  }
  .ask-card {
    /* top:0 against bottom:0 fills the screen, so no explicit height is
       needed and the base rule's collapse warning does not apply. */
    --ask-top: 0px;
    --ask-shadow: none;
    left: 0;
    width: 100%;
    border-left: 0;
    /* Nothing is left behind it to separate from, so the edge treatment and
       the corner radius both go. */
    border-radius: 0;
    animation: ask-sheet-in 0.34s cubic-bezier(0.16, 1, 0.3, 1) both;
  }
  .ask-card.is-leaving { animation: ask-sheet-out 0.22s var(--ease-exit) both; }
  .theme-dark .ask-card { --ask-shadow: none; }
  .ask-starters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 380px) {
  .ask-starters { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  /* .is-leaving outranks a bare .ask-card, so it has to be named. JS already
     skips the exit class here, but the rule should not depend on that. */
  .ask-card, .ask-card.is-leaving, .ask-msg, .ask-scrim { animation: none; transition: none; }
  .ask-typing i { animation: none; opacity: 0.5; }
  .ask-starters button:hover { transform: none; }
  .ask-composer button:hover:not(:disabled) { transform: none; }
}


/* --- token calendar days ------------------------------------------------ */

.activity-days {
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(7, 1fr);
  grid-auto-columns: minmax(10px, 1fr);
  gap: 2px;
  margin-top: 4px;
}
.activity-days button {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: 2px;
  background: var(--color-sunken);
  outline: 1px solid var(--color-rule);
  outline-offset: -1px;
}
.activity-days button[data-level="1"] { background: var(--tok-1); }
.activity-days button[data-level="2"] { background: var(--tok-2); }
.activity-days button[data-level="3"] { background: var(--tok-3); }
.activity-days button[data-level="4"] { background: var(--tok-4); }
.activity-days button:hover { outline: 1px solid var(--color-ink); outline-offset: 0; }
.activity-days button:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.activity-scroll:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; }

/* =========================================================================
   Motion preferences
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .company-brand::after,
  .grid-energy { display: none; }
  .company-brand { transform: none; }
  .project-card > *,
  .career-project-list li > * { transform: none; }
  [data-reveal-pending="true"] { opacity: 1; }
}

@media print {
  .site-header, .ask-launcher, .linkedin-key, .theme-toggle { display: none; }
  body::before { display: none; }
}

/* =========================================================================
   Cursor

   The page draws its own pointer. One family, one silhouette treatment: every
   glyph is an accent shape carrying a white halo, so the same drawing stays
   legible on the cream, on the ink cards, over video, and in dark mode.

   The halo is not painted per shape. Each glyph is stamped twice from one
   definition: once with a fat white stroke, which welds a multi-part glyph
   like the hand into a single outline, and once filled in the accent on top.
   That is why a four-fingered hand has no seams where the fingers meet.

   The native cursor is hidden only after the script has confirmed a fine
   pointer and built the glyph, so a failed script, a touch screen, or a
   reduced-motion preference all leave the real cursor exactly where it was.
   ========================================================================= */

html.cursor-custom,
html.cursor-custom * { cursor: none !important; }

/* An iframe paints its own cursor from its own document and we cannot track
   inside it, so the real one comes back at the boundary. */
html.cursor-custom iframe,
html.cursor-custom iframe * { cursor: auto !important; }

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  /* Above everything, including the ask panel. A pointer that can be covered
     is not a pointer. */
  z-index: 90;
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.16s var(--ease-calm);
  will-change: transform;
}
.cursor.is-hidden { opacity: 0; }

.cursor-svg {
  display: block;
  width: 26px;
  height: 26px;
  /* Every glyph declares its hotspot in viewBox units. The drawing is 26px
     wide on a 32 unit canvas, so 0.8125 converts one to the other, and the
     hotspot lands on the pixel the pointer actually reports. */
  margin-left: calc(var(--hx, 4) * -0.8125px);
  margin-top: calc(var(--hy, 2.4) * -0.8125px);
  transform-origin: calc(var(--hx, 4) * 0.8125px) calc(var(--hy, 2.4) * 0.8125px);
  transition: transform 0.13s var(--ease-calm);
  filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.3));
}

/* The two stamps. Fill and stroke inherit into a <use> clone, which is what
   lets one definition serve as both the halo and the body. */
.cursor-halo {
  fill: #fff;
  stroke: #fff;
  stroke-width: 5;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.cursor-body { fill: var(--color-accent); }

/* ---- hotspots, per state ---- */
.cursor[data-state="default"],
.cursor[data-state="external"],
.cursor[data-state="download"],
.cursor[data-state="copy"],
.cursor[data-state="help"],
.cursor[data-state="busy"]      { --hx: 4;  --hy: 2.4; }
.cursor[data-state="pointer"]   { --hx: 12; --hy: 2.2; }
.cursor[data-state="text"]      { --hx: 16; --hy: 16; }
.cursor[data-state="disabled"]  { --hx: 16; --hy: 16; }
.cursor[data-state="grab"],
.cursor[data-state="grabbing"]  { --hx: 16; --hy: 15; }

/* ---- press: every glyph answers the same way ---- */
.cursor.is-pressed .cursor-svg { transform: scale(0.84); }

/* ---- busy: the only moving part, kept outside the stamped glyph so it can
        be animated directly ---- */
.cursor-spin { display: none; }
.cursor[data-state="busy"] .cursor-spin { display: block; }
.cursor-spin circle {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-dasharray: 13 30;
  transform-origin: 21.5px 21.5px;
  animation: cursor-spin 0.75s linear infinite;
}
.cursor-spin .cursor-spin-halo {
  stroke: #fff;
  stroke-width: 7.5;
  stroke-dasharray: none;
  animation: none;
}
@keyframes cursor-spin { to { transform: rotate(360deg); } }

/* --- the label that trails it -------------------------------------------
   Once the glyph is the pointer, the label is the only thing left that
   should lag. It keeps its own easing and its own stand-down rules: it
   steps aside for the ask panel, the glyph never does. */

.buzz {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 64;
  /* Hangs below and right of the trailing point, clear of the glyph. */
  margin: 19px 0 0 17px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.22s var(--ease-calm);
  will-change: transform;
}
.buzz.is-on { opacity: 1; }

.buzz-label {
  display: block;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: -0.005em;
  white-space: nowrap;
  box-shadow: 0 4px 14px -4px rgba(0, 0, 0, 0.35);
  /* The label swaps text between sections rather than sliding around. */
  transition: opacity 0.18s ease;
}

/* A quip fits on one line. The work cards carry a sentence instead, so it
   wraps into a block and gives up the stadium radius. */
.buzz-label.is-long {
  max-width: min(330px, 72vw);
  padding: 9px 13px;
  border-radius: 12px;
  white-space: normal;
  text-wrap: pretty;
}
.buzz-label.is-swapping { opacity: 0; }

/* Coarse pointers have no cursor to replace, and reduced motion should get
   neither a drawn pointer nor a second thing chasing the first. Repeated
   here in case the query flips after load. */
@media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) {
  html.cursor-custom,
  html.cursor-custom * { cursor: auto !important; }
  .cursor, .buzz { display: none; }
}


/* =========================================================================
   Selected work — the editorial arrangement: one lead project, two beside it

   No product screenshots exist in the repository yet, so these are text-led.
   The markup reserves a .work-visual slot on each card; drop an image in and
   it takes over without the layout moving.
   ========================================================================= */

/* The heading keeps the arrows company, same as More projects. */
.work-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}
.work-head .t-section { margin-bottom: 0; }
.work-arrows { flex: none; }

/* Three across, the fourth reached by scrolling. Same mechanics as the
   More projects rail so the page has one sideways behaviour, not two. */
.work-grid {
  list-style: none;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(290px, calc((100% - 2 * 26px) / 3));
  gap: 26px;
  margin: clamp(26px, 3vw, 38px) 0 0;
  padding: 4px;
  margin-inline: -4px;   /* cancels the padding the focus ring needs */
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.work-grid::-webkit-scrollbar { width: 0; height: 0; display: none; }
.work-grid:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
.work-item { scroll-snap-align: start; }

/* Unboxed: the panel carries the frame, the words sit on the page under it.
   A border around the whole card put a second frame around a thing that
   already had one. */
.work-card {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
}

/* --- the panel ----------------------------------------------------------
   Grid paper, with the screenshot tilted and running off the right edge, so
   it reads as an object laid down rather than a picture pasted in. The tilt
   is per card (--rot) because four identical angles look like a template. */
.work-shot {
  position: relative;
  display: block;
  aspect-ratio: 4 / 2.75;
  overflow: hidden;
  border: 1px solid var(--color-hairline);
  border-radius: 16px;
  background-color: var(--color-sunken);
  /* Hover wash. --glow is a hue set per card from its own screenshot, so a
     card lights up in its artwork's colour rather than one house accent.
     The panel's own background paints below its children, which is what
     puts the wash under both the motifs and the screenshot.

     Linear, not radial: a radial ending at 78% left a faint curved edge
     arcing across the panel, which on light paper read as the boundary of
     a stain. A top-to-bottom fade reaching transparent exactly at the
     bottom edge has no boundary to see. */
  --glow: 215;
  background-image:
    linear-gradient(to bottom,
      hsl(var(--glow) var(--glow-s) var(--glow-l) / calc(var(--glow-a) * var(--glow-top))) 0%,
      hsl(var(--glow) var(--glow-s) var(--glow-l) / calc(var(--glow-a) * var(--glow-mid))) 48%,
      transparent 100%);
  transition: border-color var(--dur-ui) var(--ease-calm),
              transform var(--dur-ui) var(--ease-calm),
              --glow-a var(--dur-ui) var(--ease-calm);
}
/* Registered so the wash can fade in. An unregistered custom property is
   just a string to the engine, and jumps from 0 to 1 with no transition. */
@property --glow-a {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* --- motifs ---------------------------------------------------------------
   The panel's ground: line icons of what the product actually deals with,
   scattered and revealed on hover. Decorative, so the whole layer is
   aria-hidden in the markup and inert to the pointer; the card's single
   click target is unaffected. It sits before .work-shot-img in the DOM, so
   it paints under the screenshot. */
/* The sprite only defines shapes. Taking it out of flow stops it reserving
   a line box at the foot of the page. */
.wm-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.work-motifs {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-ui) var(--ease-calm);
}
.work-card:hover .work-motifs,
.work-card:focus-within .work-motifs { opacity: 1; }

.wm {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--s);
  height: var(--s);
  rotate: var(--r);
  fill: none;
  stroke: var(--color-ink);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Low enough to stay a ground rather than compete with the role
     statement sitting over it. */
  opacity: 0.22;
}

.work-card:hover .work-shot,
.work-card:focus-within .work-shot {
  border-color: color-mix(in srgb, hsl(var(--glow) 70% 50%) 40%, var(--color-hairline));
  transform: translateY(-2px);
  --glow-a: 1;
}

/* Anchored off the right edge and oversized, so the crop is deliberate:
   the shot continues past the card rather than stopping inside it. */
.work-shot-img {
  position: absolute;
  top: 9%;
  left: 57%;
  right: -15%;
  bottom: -13%;
  overflow: hidden;
  border-radius: 7px;
  transform: rotate(var(--rot, -5deg));
  box-shadow: 0 18px 34px -18px rgba(0, 0, 0, 0.45),
              0 2px 8px -2px rgba(0, 0, 0, 0.25);
}
.work-shot-img img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top left;
}

/* Project name, top left, out of the way of the shot. */
.work-kicker {
  position: absolute;
  z-index: 2;
  top: clamp(14px, 1.6vw, 19px);
  left: clamp(15px, 1.7vw, 20px);
  max-width: 42%;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-soft);
}

/* The role, bottom left, in display type. */
.work-role-tag {
  position: absolute;
  z-index: 2;
  left: clamp(15px, 1.7vw, 20px);
  bottom: clamp(14px, 1.6vw, 19px);
  /* Stops short of where the shot begins, so the two never overlap.
     "System Design" is the longest line of the four and sets this. */
  max-width: 52%;
}
.work-role-tag b {
  display: block;
  font: var(--display-wght) 20px / 1.14 var(--font-display);
  letter-spacing: -0.028em;
  color: var(--color-ink);
}
.work-role-tag i {
  display: block;
  margin-top: 7px;
  font-style: normal;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-soft);
}
/* A real highlighter band here, not the page's colour-only emphasis: over
   grid paper a block of yellow is what reads, and the panel is the one place
   on the site that carries artwork behind its words. box-decoration-break
   keeps the band whole if the line ever wraps. */
.work-mark {
  display: inline;
  padding: 0 0.16em;
  background: var(--color-highlight);
  color: #212121;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* --- the words under the panel ------------------------------------------ */
.work-name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 9px;
  margin: 18px 0 0;
  font-size: 14.5px;
  font-weight: 500;
  color: var(--color-soft);
}
.work-live {
  padding: 2px 9px;
  border: 1px solid color-mix(in srgb, var(--color-live) 45%, transparent);
  border-radius: 99px;
  background: color-mix(in srgb, var(--color-live) 13%, transparent);
  color: var(--color-live);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
}

/* Two lines reserved whether the copy fills them or not. Without this a
   one-line sub or a two-line title rides up and the body text starts 35px
   higher on some cards than others, which reads across the row as cards set
   in different sizes. The em value is the fallback for no lh support. */
.work-headline {
  min-height: 2.4em;
  min-height: 2lh;
  margin: 7px 0 0;
  font: var(--display-wght) clamp(1.3rem, 2.1vw, 1.8rem) / 1.2 var(--font-display);
  letter-spacing: -0.026em;
  text-wrap: balance;
  color: var(--color-ink);
}
.work-sub {
  min-height: 3.1em;
  min-height: 2lh;
  margin: 9px 0 0;
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--color-soft);
}



@media (max-width: 760px) {
  /* A plain percentage, not minmax: with a 258px floor the card came out
     NARROWER on a phone than in the 290px desktop column, which cropped the
     shot down to a blank strip of page margin and clipped the role line. */
  .work-grid { grid-auto-columns: 86%; }
  .work-shot { aspect-ratio: 4 / 3; }
  .work-role-tag { max-width: 50%; }
}

/* "System Design" is the longest role line and sets the floor here. */
@media (max-width: 380px) {
  .work-role-tag b { font-size: 18px; }
  .work-kicker { font-size: 9.5px; }
  .work-role-tag i { font-size: 9.5px; }
  /* 237px card: the shot starts at 135px and the text box has to end before
     it, inset and gap included. "System Design" wraps to two lines here. */
  .work-role-tag { max-width: 46%; }
}

@media (prefers-reduced-motion: reduce) {
  .work-grid { scroll-behavior: auto; }
  .work-motifs { transition: none; }
  .work-card:hover .work-shot,
  .work-card:focus-within .work-shot { transform: none; }
  .work-shot { transition: border-color var(--dur-ui) var(--ease-calm); }
}

.work-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
  margin-bottom: 12px;
}
.work-kind {
  padding: 3px 9px;
  border-radius: 4px;
  background: color-mix(in srgb, var(--color-ink) 9%, var(--color-raised));
  color: var(--color-ink);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.012em;
}



.work-result { margin-top: 10px; font-size: 15px; line-height: 1.6; }

.work-context {
  /* Follows the headline directly, so it needs a touch more air than it did
     when it sat under the result line. */
  margin-top: 12px;
  color: var(--color-soft);
  font-size: 14px;
  line-height: 1.5;
}
.work-role { color: var(--color-soft); }
.work-role b { color: var(--color-ink); font-weight: 500; }
.work-result { font-weight: 500; }

/* The image slot, empty for now. Reserving it keeps the card from jumping
   when a screenshot finally arrives. */
/* --- work card diagrams --------------------------------------------------
   Not screenshots. Each card carries a drawing of how its system is put
   together, in the page's own tokens so it themes with everything else.
   Every value in them is one the card already states in words.            */

.work-visual {
  display: block;
  /* The negative inline margin pays back the card's padding so the shot
     reaches both edges, and the top edge too. No padding of its own: the
     image is the content here, not something mounted inside a frame. */
  margin: 0 -24px 20px;
  padding: 0;
  border: 0;
  border-bottom: 1px solid var(--color-hairline);
  border-radius: 0;
  background: var(--color-sunken);
  aspect-ratio: 344 / 130;
  overflow: hidden;
}
.work-visual--lib { aspect-ratio: 400 / 130; }
.work-visual svg { display: block; width: 100%; height: 100%; }
.work-visual img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Shared marks */
.wv-t {
  fill: var(--color-soft);
  font: 500 9.5px/1 var(--font-body);
  letter-spacing: 0.01em;
}
.wv-t--mid { text-anchor: middle; }
.wv-t--end { text-anchor: end; }
.wv-t--on { fill: var(--color-raised); font-weight: 600; }
.wv-big {
  fill: var(--color-ink);
  font: 640 19px/1 var(--font-display);
  letter-spacing: -0.02em;
  text-anchor: middle;
}

/* 1. section library */
.wv-panel { fill: none; stroke: var(--color-hairline); stroke-dasharray: 3 3; }
.wv-chip { fill: color-mix(in srgb, var(--color-ink) 13%, transparent); }
.wv-chip.is-on { fill: var(--color-accent); }
.wv-arrow { stroke: var(--color-soft); stroke-width: 1.2; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.wv-page { fill: var(--color-raised); stroke: var(--color-hairline); }
.wv-block { fill: color-mix(in srgb, var(--color-ink) 11%, transparent); }

/* 2. time to first useful action */
.wv-tick { fill: color-mix(in srgb, var(--color-ink) 9%, transparent); }
.wv-axis { stroke: var(--color-hairline); stroke-width: 2; stroke-linecap: round; }
.wv-axis-fill { stroke: var(--color-accent); stroke-width: 2; stroke-linecap: round; }
.wv-stem { stroke: var(--color-hairline); stroke-width: 1; stroke-dasharray: 2 3; }
.wv-dot { fill: var(--color-accent); stroke: var(--color-sunken); stroke-width: 2; }

/* 3. the journey */
.wv-route { stroke: var(--color-hairline); stroke-width: 2; stroke-linecap: round; }
.wv-stop { fill: var(--color-raised); stroke: var(--color-hairline); stroke-width: 2; }
.wv-stop.is-on { fill: var(--color-accent); stroke: var(--color-accent); }
.wv-bubble { fill: var(--color-accent); }
.wv-bubble-tail { fill: var(--color-accent); }

.work-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
  /* The image no longer holds the foot, so the link does: it keeps the
     call to action on one line across a row of uneven cards. */
  margin-top: auto;
  padding: 16px 0 0;
}
.work-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 5px;
  transition: text-decoration-color var(--dur-ui);
}
.work-link:hover { text-decoration-color: var(--color-accent); }
.work-link .arrow { transition: transform var(--dur-tap) var(--ease-calm); }
/* The one card that opens the assistant rather than a page says so up front. */
.work-link-icon { width: 15px; height: 15px; flex: none; }
.work-link:hover .arrow { transform: translateX(2px); }
.work-note { color: var(--color-soft); font-size: 13px; }
/* A note sits on its own line above the action, so the action itself stays
   on the same baseline as its neighbours' across the row. */
.work-actions > .work-note { flex-basis: 100%; }

/* --- The whole card is the click target ---------------------------------
   Wrapping the card in an <a> would nest the two buttons inside a link:
   invalid, and it breaks keyboard and screen-reader use. Instead the case
   study link stays a single real <a> and its ::after stretches over the
   card. One link in the accessibility tree, one tab stop, whole card
   clickable.

   Every card gets it. On the section library card the one action is the
   "Ask about this project" button, so the overlay stretches that instead
   and a click anywhere opens the assistant with the question ready. */
.work-actions > .work-link:not(.ask-about)::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: 16px;
}
/* The whole card is one target, so the keyboard ring belongs on the card,
   not on the link's few words. The overlay already traces the card exactly. */
.work-actions > .work-link:not(.ask-about):focus-visible::after {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}
/* Anything else interactive has to sit above that overlay or the card
   swallows it. The panel is lifted too: it holds its own content once
   expanded. (The card's own action must NOT be lifted: positioning it would
   shrink its ::after back to the link's few words.) */
.work-card .think-toggle,
.work-card .think-panel {
  position: relative;
  z-index: 2;
}
/* The overlay covers the card, so the hover underline needs the card as its
   trigger rather than the link's own few words. */
.work-actions > .work-link { cursor: pointer; }
.work-card:hover .work-link { text-decoration-color: var(--color-accent); }
.work-card:hover .work-link .arrow { transform: translateX(2px); }


/* --- "See my thinking" --------------------------------------------------- */

.think-toggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  /* A quiet second option beside the destination, not a competing pill:
     at this card width two pills wrapped onto separate lines. */
  padding: 6px 0;
  border: 0;
  border-radius: 999px;
  color: var(--color-soft);
  font-size: 13px;
  font-weight: 500;
  transition: background var(--dur-tap), border-color var(--dur-tap), transform var(--dur-tap);
}
.think-toggle:hover { color: var(--color-ink); }
.think-toggle:active { transform: scale(0.97); }
.think-toggle[aria-expanded="true"] { color: var(--color-accent); }
.think-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
}
.think-toggle[aria-expanded="true"] .think-dot { opacity: 1; }

/* Visible by default, so the decisions are readable with no JS at all. The
   script hides them on load and the toggle takes over.

   This uses [hidden] rather than an animated max-height on purpose: the
   reveal system runs Web Animations on these cards, and a running animation
   outranks even an inline style, so a height-based collapse silently lost. */
.think-panel { opacity: 1; transition: opacity var(--dur-ui) var(--ease-calm); }
.think-panel[hidden] { display: none; }
.think-panel.is-entering { opacity: 0; }
.think-list { margin: 14px 0 0; padding: 0; list-style: none; counter-reset: think; }
.think-list li {
  position: relative;
  padding: 0 0 0 30px;
  counter-increment: think;
}
.think-list li + li { margin-top: 12px; }
.think-list li::before {
  content: counter(think);
  position: absolute;
  left: 0;
  top: 1px;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--color-sunken);
  color: var(--color-soft);
  font-family: var(--font-mono);
  font-size: 11px;
}
.think-list strong { display: block; font-size: 14px; font-weight: 600; }
.think-list span { display: block; margin-top: 3px; color: var(--color-soft); font-size: 14px; line-height: 1.6; }

@media (prefers-reduced-motion: reduce) {
  .think-panel { transition: none; }
}

/* =========================================================================
   Contact — the closing section
   ========================================================================= */

.contact-lede { max-width: 54ch; margin-top: 16px; }

.contact-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 20px;
  margin-top: 26px;
}

.contact-copy {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  font-size: 15px;
  font-weight: 500;
  transition: border-color var(--dur-ui), background var(--dur-ui), transform var(--dur-tap);
}
.contact-copy:hover { border-color: var(--color-ink); background: var(--color-sunken); }
.contact-copy:active { transform: translateY(1px); }
.contact-copy.is-done { border-color: var(--status-live-bd); background: var(--status-live-bg); color: var(--status-live-fg); }

.contact-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 15px;
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 5px;
  transition: text-decoration-color var(--dur-ui);
}
.contact-link:hover { text-decoration-color: var(--color-accent); }
.contact-link .arrow { transition: transform var(--dur-tap) var(--ease-calm); }
/* The LinkedIn mark rides with the label. currentColor rather than the brand
   blue, because --color-linkedin is aliased to the site accent everywhere
   else: this page deliberately does not borrow other companies' palettes. */
/* The LinkedIn mark, wherever a link sends someone to LinkedIn. currentColor
   rather than the brand blue, because --color-linkedin is aliased to the site
   accent everywhere else: this page does not borrow other companies' palettes. */
.li-mark {
  flex: none;
  display: inline-block;
  width: 16px;
  height: 16px;
  vertical-align: -0.17em;
  margin-right: 5px;
}
.contact-link .li-mark, .about-btn .li-mark { margin-right: 0; }

.contact-link:hover .arrow { transform: translate(1px, -1px); }

.contact-status {
  /* Two lines reserved: the failure message is longer than the success one. */
  min-height: 2.8em;
  margin-top: 12px;
  color: var(--color-soft);
  font-size: 13px;
}

/* =========================================================================
   Toolkit marquee
   ========================================================================= */

.tk-title { margin-top: 10px; }

.tk-lede {
  margin-top: 14px;
  max-width: 58ch;
}

/* The row is framed by the same dotted verticals as everything else, so it
   clips on the 784px rail rather than running to the viewport edges. That
   width is body::before's own; if the rail moves, move this with it. */
/* The wrapper cancels the section gutter so its box is the body's, which is
   what body::before measures against. Without it "100%" is the padded content
   box and the row sits inside the lines at every width below about 912px. */
.tk-bleed {
  margin-inline: calc(var(--gutter) * -1);
  margin-top: clamp(1.6rem, 3.2vw, 2.4rem);
}

.tk-marquee {
  --tk-gap: clamp(0.65rem, 1.4vw, 1.05rem);
  /* Identical formula to body::before, so the clip lands on the lines. */
  width: min(100% - 2rem, 784px);
  margin-inline: auto;
  /* Hard clip on the rail: tiles are cut at the lines, not faded out. */
  overflow: hidden;
}

.tk-row {
  display: flex;
  width: max-content;
  gap: var(--tk-gap);
  animation: tk-scroll 42s linear infinite;
}

/* Pause on hover so a name can actually be read, and on focus-within so the
   same is true for keyboard users once a tile ever becomes focusable. */
.tk-marquee:hover .tk-row,
.tk-marquee:focus-within .tk-row { animation-play-state: paused; }

.tk-track {
  display: flex;
  gap: var(--tk-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Two identical tracks sit in the row, so the row is 2T + gap wide. A seam-free
   loop has to travel T + gap, and -50% only covers T + gap/2, hence the
   extra half gap. */
@keyframes tk-scroll {
  to { transform: translateX(calc(-50% - (var(--tk-gap) / 2))); }
}

.tk-item {
  display: flex;
  flex: none;
  align-items: center;
  gap: 10px;
  padding: 11px 18px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  background: var(--color-raised);
  white-space: nowrap;
}

.tk-mark {
  display: grid;
  flex: none;
  place-items: center;
  width: 22px;
  height: 22px;
}
.tk-mark svg { width: 100%; height: 100%; display: block; }

.tk-name {
  font-size: 14.5px;
  font-weight: 560;
  letter-spacing: -0.008em;
}

@media (prefers-reduced-motion: reduce) {
  /* No animation means the duplicate track is just the list said twice, so it
     goes, and the row becomes an ordinary horizontal scroller. */
  .tk-row { animation: none; }
  .tk-marquee {
    overflow-x: auto;
    scrollbar-width: thin;
  }
  .tk-track[aria-hidden="true"] { display: none; }
}

/* =========================================================================
   About
   ========================================================================= */

/* Deliberately NOT two columns. The site's measure is 720px, which left the
   copy column at 346px, wrapping every five words, and widening the section to
   fix that pushed its left edge off the rail every other section sits on. So
   the copy keeps the full measure and the facts go underneath. */
.about-grid { display: block; }

.about-title { margin-top: 10px; }
.about-lede { margin-top: 18px; max-width: 64ch; }

.about-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 28px;
}

/* Same shape as the contact buttons, outlined rather than filled: this is a
   secondary invitation, the primary one is still "Email me". */
.about-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 20px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 14.5px;
  cursor: pointer;
  text-decoration: none;
  transition: border-color var(--dur-ui), transform var(--dur-tap) var(--ease-calm);
}
.about-btn:hover { border-color: var(--color-accent); transform: translateY(-1px); }
.about-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.about-btn .arrow { transition: transform var(--dur-tap) var(--ease-calm); }
.about-btn:hover .arrow { transform: translateY(1px); }

/* ---- the fact list ---- */

.about-meta {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  margin: clamp(2rem, 4vw, 2.75rem) 0 0;
  border: 1px solid var(--color-hairline);
  border-radius: 16px;
  background: var(--color-raised);
  /* so the row dividers stop cleanly at the rounded corners */
  overflow: hidden;
}

.about-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: clamp(1rem, 2vw, 1.3rem) clamp(1.05rem, 2.2vw, 1.45rem);
}
.about-row { border-top: 1px solid var(--color-hairline); }
.about-row:first-child { border-top: 0; }

.about-icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 38px;
  height: 38px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--color-accent) 13%, transparent);
  color: var(--color-accent);
}
.about-icon svg { width: 19px; height: 19px; }

.about-label {
  color: var(--color-soft);
  font-size: 11.5px;
  font-weight: 640;
  letter-spacing: 0.13em;
  line-height: 1.4;
  text-transform: uppercase;
}
.about-value {
  margin: 3px 0 0;
  font-size: 15.5px;
  line-height: 1.45;
}

/* The two columns stop earning their keep once the fact values start wrapping
   onto three lines, which happens well before the usual tablet breakpoint. */
/* Two facts abreast once there is room: every value then fits on one line,
   which four stacked full-width rows did not achieve any better. */
@media (min-width: 560px) {
  .about-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .about-row:nth-child(-n + 2) { border-top: 0; }
  .about-row:nth-child(even) { border-left: 1px solid var(--color-hairline); }
}

/* =========================================================================
   Contact actions, small screens

   Four items in one wrapping flex row broke two ways on a phone: the wrap
   point moved with the viewport (at 390px Resume was orphaned on its own
   line), and the two text links rendered 25px tall against a 44px minimum
   touch target. A 2x2 grid of identical pills fixes both, and the primary
   action stays primary through its accent fill rather than through size.
   ========================================================================= */

@media (max-width: 600px) {
  .contact-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }

  .contact-actions > * {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 46px;
    padding: 0 12px;
    border-radius: 999px;
    font-size: 14.5px;
    text-align: center;
  }

  /* The links become buttons here, so they take the border and drop the
     underline that distinguished them when they sat in a text row. */
  .contact-actions .contact-link {
    gap: 6px;
    border: 1px solid var(--color-hairline);
    text-decoration: none;
  }
  .contact-actions .contact-link:hover { border-color: var(--color-accent); }
}

/* Bulleted evidence under an answer's opening verdict. */
.ask-list {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 6px;
}
.ask-list li {
  position: relative;
  padding-left: 15px;
  line-height: 1.55;
}
.ask-list li::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 0.68em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--color-accent);
}
.ask-msg--reply strong { font-weight: 650; }

/* ---- pinned shortcuts under the conversation ---- */

.ask-cta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  /* 12px matches .ask-composer, so the row sits on the panel rhythm. The
     rule runs full bleed on purpose: inset content, edge-to-edge divider. */
  padding: 11px 12px;
  border-bottom: 1px solid var(--color-hairline);
}

.ask-cta-label {
  margin-right: 2px;
  color: var(--color-soft);
  font-size: 10.5px;
  font-weight: 640;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

.ask-cta-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  color: inherit;
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--dur-ui), background var(--dur-ui);
}
.ask-cta-btn:hover { border-color: var(--color-accent); background: var(--color-sunken); }
.ask-cta-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.ask-cta-btn svg { width: 14px; height: 14px; flex: none; }

/* ---- voice input ---- */

.ask-mic {
  display: grid;
  place-items: center;
  flex: none;
  width: 38px;
  height: 38px;
  border: 1px solid var(--color-hairline);
  border-radius: 999px;
  background: none;
  color: var(--color-soft);
  cursor: pointer;
  transition: border-color var(--dur-ui), color var(--dur-ui), background var(--dur-ui);
}
.ask-mic:hover { border-color: var(--color-ink); color: var(--color-ink); }
.ask-mic:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.ask-mic svg { width: 17px; height: 17px; }

/* Listening: the only state where the control is doing something the visitor
   cannot otherwise see, so it is the only one that animates. */
.ask-mic.is-live {
  border-color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
  color: var(--color-accent);
  animation: ask-mic-pulse 1.4s ease-in-out infinite;
}
@keyframes ask-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-accent) 38%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-accent) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .ask-mic.is-live { animation: none; }
}

/* JKLU ships with white baked into the file, and unlike Maxlence its wordmark
   is dark, so keying the white out would leave it invisible on the dark
   ground. Framing it as a deliberate rounded tile is the honest fix: the white
   then reads as a chip rather than as a stray rectangle behind the mark. */
.career-logo img[src*="jklu"] {
  padding: 2px;
  border-radius: 6px;
  background: #fff;
}

/* =========================================================================
   Shipped for
   ========================================================================= */

/* No heading, so the section does not need a heading's worth of air. */
.sh-section { padding-block: clamp(1.75rem, 3.2vw, 2.75rem); }

/* Same bleed trick as the toolkit: cancel the gutter first so the rail's own
   width formula has the body to measure, not the padded content box. */
.sh-bleed { margin-inline: calc(var(--gutter) * -1); }

.sh-marquee {
  --sh-gap: clamp(2rem, 4.5vw, 3.5rem);
  width: min(100% - 2rem, 784px);
  margin-inline: auto;
  overflow: hidden;
}

.sh-row {
  display: flex;
  width: max-content;
  gap: var(--sh-gap);
  animation: sh-scroll 34s linear infinite;
}
.sh-marquee:hover .sh-row { animation-play-state: paused; }

.sh-track {
  display: flex;
  align-items: center;
  gap: var(--sh-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Two identical tracks make the row 2T + gap, so a seamless loop travels
   T + gap. -50% only covers T + gap/2, hence the extra half gap. */
@keyframes sh-scroll {
  to { transform: translateX(calc(-50% - (var(--sh-gap) / 2))); }
}

.sh-item { flex: none; display: flex; align-items: center; }

/* Matched on height, not box: these marks have very different aspect ratios,
   and a shared optical height is what makes them read as one set. */
.sh-item img {
  display: block;
  width: auto;
  height: clamp(19px, 2.2vw, 24px);
}
.sh-item img[src*="bigoh"] { height: clamp(21px, 2.5vw, 27px); }

/* A hue-rotated invert shifted every colour, so the three marks that cannot
   survive the dark ground get real variants instead: only their near-black
   wordmark is recoloured (#2D2D2D and #1E1E1E / #231F20 -> the site's dark
   ink), and every brand colour is left exactly as supplied. The other four
   are already legible on both grounds and are never touched.

   content: on a replaced element swaps the rendered image while the <img>
   keeps its alt, so the accessible name does not change with the theme. */
:root[data-theme="dark"] .sh-item img[src$="dgsign.svg"],
.theme-dark .sh-item img[src$="dgsign.svg"] {
  content: url("assets/logos/clients/dgsign-dark.svg");
}
:root[data-theme="dark"] .sh-item img[src$="digilawyer.svg"],
.theme-dark .sh-item img[src$="digilawyer.svg"] {
  content: url("assets/logos/clients/digilawyer-dark.svg");
}
:root[data-theme="dark"] .sh-item img[src$="igx.webp"],
.theme-dark .sh-item img[src$="igx.webp"] {
  content: url("assets/logos/clients/igx-dark.webp");
}

@media (prefers-reduced-motion: reduce) {
  /* No animation means the duplicate track is just the list said twice. */
  .sh-row { animation: none; }
  .sh-marquee { overflow-x: auto; scrollbar-width: thin; }
  .sh-track[aria-hidden="true"] { display: none; }
}

/* =========================================================================
   Bottom navigation, phones only
   ========================================================================= */

.bottom-nav { display: none; }

@media (max-width: 600px) {
  .bottom-nav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 70;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 2px;
    /* The bar sits on the home indicator, so it carries that clearance
       itself rather than leaving a dead strip under it. */
    padding: 6px 6px max(6px, env(safe-area-inset-bottom));
    border-top: 1px solid var(--color-hairline);
    background: color-mix(in srgb, var(--color-raised) 92%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }

  .bn-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    /* 48px, not 44: this is the thumb's primary target on the page. */
    min-height: 48px;
    padding: 4px 2px;
    border: 0;
    border-radius: 12px;
    background: none;
    color: var(--color-soft);
    font: inherit;
    font-size: 10.5px;
    letter-spacing: 0.01em;
    text-decoration: none;
    cursor: pointer;
  }
  .bn-item:active { background: var(--color-sunken); }
  .bn-item:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
  .bn-icon { display: grid; place-items: center; }
  .bn-icon svg { width: 21px; height: 21px; }
  .bn-label { line-height: 1.2; }

  /* Ask is the one thing on this page that is not a destination, so colour
     carries the difference rather than position. */
  .bn-item--accent { color: var(--color-accent); }

  /* Content has to clear the bar. */
  body { padding-bottom: calc(62px + env(safe-area-inset-bottom)); }

  /* This bar already carries Work, Playground and Ask, with labels. The
     header's icon-only copies of the same three said it twice, and less
     clearly, so on phones the header keeps only the theme toggle. */
  .site-nav > a { display: none; }
}

/* =========================================================================
   Footer
   ========================================================================= */

.home-footer { padding-block: clamp(2.5rem, 5vw, 4rem) clamp(1.75rem, 3vw, 2.5rem); }

.ft-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: clamp(1.75rem, 4vw, 3rem);
}

.ft-col { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }

.ft-head {
  margin-bottom: 2px;
  color: var(--color-soft);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 640;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

.ft-col a {
  font-size: 14.5px;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 4px;
  transition: text-decoration-color var(--dur-ui);
}
.ft-col a:hover { text-decoration-color: var(--color-accent); }
.ft-col .arrow { font-size: 0.85em; color: var(--color-soft); }

/* The legal note is the one thing here nobody reads on purpose, so it sits
   below the rule at the smallest size the contrast floor still allows. */
.ft-legal {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-top: clamp(1.25rem, 2.5vw, 1.75rem);
  border-top: 1px solid var(--color-hairline);
  color: var(--color-soft);
  font-size: 12.5px;
  line-height: 1.6;
}
.ft-legal p { max-width: 68ch; }
.ft-copy { margin-top: 10px; }

/* Marks in the Ask-an-AI column. currentColor rather than brand palettes:
   four saturated logos in a quiet footer column would outshout the links. */
/* The link becomes its own flex row: an earlier rule sets svg to
   display:block, and as a block child inside a shrink-to-fit link the mark
   pushed the label onto a second line. */
.ft-col a:has(.ft-mark) { display: inline-flex; align-items: center; }

.ft-mark {
  flex: none;
  display: block;
  width: 15px;
  height: 15px;
  margin-right: 8px;
  opacity: 0.75;
}
.ft-col a:hover .ft-mark { opacity: 1; }
/* Brand marks carry their own colour, and dimming a brand colour just
   muddies it, so they sit at full strength. */
.ft-mark--brand { opacity: 1; }

/* The bar is shared across pages, so the current one has to say so: colour
   alone would be invisible to anyone who cannot distinguish it. */
@media (max-width: 600px) {
  .bn-item.is-current { color: var(--color-ink); }
  .bn-item.is-current .bn-icon { position: relative; }
  .bn-item.is-current .bn-icon::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: -5px;
    width: 14px;
    height: 2px;
    border-radius: 2px;
    transform: translateX(-50%);
    background: var(--color-accent);
  }
}

/* =========================================================================
   Footer closing bar
   ========================================================================= */

/* The band closes the page, so the footer stops padding below it. */
.home-footer { padding-bottom: 0; }

/* Full bleed: cancel the gutter the footer sets, then pay it back as padding
   so the text still lines up with the columns above. */
.ft-bar {
  margin-inline: calc(var(--gutter) * -1);
  margin-top: clamp(1.75rem, 3.5vw, 2.75rem);
  padding: 16px var(--gutter) clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--color-hairline);
  color: var(--color-soft);
}

.ft-bar-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 24px;
  font-size: 12.5px;
  line-height: 1.5;
}

.ft-bar-row p { margin: 0; }

.ft-bar a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--color-hairline);
  text-underline-offset: 3px;
}
.ft-bar a:hover { color: var(--color-ink); text-decoration-color: var(--color-accent); }

/* Three parts will not sit side by side on a phone, and a ragged two-and-one
   row reads worse than a centred stack. */
@media (max-width: 720px) {
  .ft-bar-row { justify-content: center; text-align: center; gap: 4px 16px; }
}

/* --- work card screenshot slot ------------------------------------------
   The diagrams were replaced by real captures. Until one lands the slot
   holds its shape, so the card does not jump when the image arrives. */

.work-visual--empty {
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 9;
  color: var(--color-soft);
}

.work-slot {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  text-align: center;
}
.work-slot svg { width: 22px; height: 22px; opacity: 0.6; }

/* --- mobile: keep the composer out of iOS's zoom ------------------------
   Safari zooms the whole page in when a field under 16px takes focus. That
   pushed the send button off the right edge mid-sentence, so 16px here is a
   threshold rather than a type choice. */
@media (max-width: 600px) {
  .ask-composer textarea { font-size: 16px; }
}

/* The card image slot keeps one shape whether it holds a capture or is still
   waiting for one, so a row of cards does not go ragged while some are
   filled and some are not. */
.work-visual, .work-visual--empty { aspect-ratio: 16 / 9; }
/* The shots are framed browser mockups, so they fill the slot and anchor to
   the top: the window chrome and the headline are the part worth showing, and
   the foot of the page is the part that can go. */
.work-visual img {
  object-fit: cover;
  object-position: top center;
}

/* =========================================================================
   More projects
   The card is the picture. Everything else sits on top of it: who and when
   at the top, the number and its one line at the bottom, both held legible
   by a scrim rather than by panels, which is what made the first attempt
   read as a component instead of a piece of work.
   ========================================================================= */

.mp-rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(270px, 31%);
  gap: 22px;
  margin: clamp(26px, 3vw, 38px) 0 0;
  padding: 4px;
  margin-inline: -4px;   /* cancels the padding the focus ring needs */
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
}
/* No scrollbar. The half card at the edge already says the row keeps going,
   and the bar was the loudest thing in the section. Scrolling itself is
   untouched, and the rail still takes focus, so the keyboard keeps its
   arrow keys. */
.mp-rail { scrollbar-width: none; }
.mp-rail::-webkit-scrollbar { width: 0; height: 0; display: none; }

.mp-rail:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
.mp-item { scroll-snap-align: start; }

.mp-card {
  position: relative;
  display: block;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: 18px;
  /* The text is always over artwork, so it does not follow the page theme. */
  background: #121110;
  color: #f4f1ec;
  text-decoration: none;
  isolation: isolate;
}

.mp-card img {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* Dark at both ends, clear through the middle, so the picture keeps the
   part of itself worth looking at. */
.mp-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* The bottom stops are set from the worst case, not by eye: over the
     near-white phone screens on three of these cards the old ramp left only
     0.31 alpha under the metric, which measured 2:1. The text sits around a
     third of the way up, so the ramp has to still be heavy there. */
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0) 22%),
    linear-gradient(to top,
      rgba(0, 0, 0, 0.95) 0%,
      rgba(0, 0, 0, 0.85) 22%,
      rgba(0, 0, 0, 0.55) 42%,
      rgba(0, 0, 0, 0) 66%);
}

.mp-meta,
.mp-foot { position: absolute; z-index: 2; left: clamp(16px, 1.8vw, 22px); right: clamp(16px, 1.8vw, 22px); }
.mp-meta {
  top: clamp(14px, 1.6vw, 20px);
  margin: 0;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
}
.mp-foot { bottom: clamp(16px, 1.8vw, 22px); }

.mp-metric {
  margin: 0;
  font-size: clamp(1.5rem, 2.4vw, 2rem);
  font-weight: 680;
  letter-spacing: -0.025em;
  line-height: 1.08;
}
.mp-line {
  margin: 7px 0 0;
  font-size: 14px;
  line-height: 1.45;
  color: rgba(244, 241, 236, 0.74);
}

/* ---- the hover label ----
   Only on a card that is a link, so a placeholder never offers it. */
.mp-peek {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 46%;
  transform: translate(-50%, -50%) scale(0.94);
  padding: 10px 18px;
  border-radius: 999px;
  background: #faf8f5;
  color: #17161a;
  font-size: 13.5px;
  font-weight: 550;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  box-shadow: 0 14px 30px -16px rgba(0, 0, 0, 0.7);
  transition: opacity 0.22s var(--ease-calm, ease), transform 0.22s var(--ease-calm, ease);
}
a.mp-card { transition: transform 0.35s var(--ease-calm, ease); }
a.mp-card:hover .mp-peek,
a.mp-card:focus-visible .mp-peek,
a.mp-card:focus-within .mp-peek { opacity: 1; transform: translate(-50%, -50%) scale(1); }
a.mp-card:hover img { transform: scale(1.03); }
a.mp-card img { transition: transform 0.5s var(--ease-calm, ease); }
a.mp-card:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }


@media (prefers-reduced-motion: reduce) {
  .mp-rail { scroll-behavior: auto; }
  .mp-peek, a.mp-card, a.mp-card img { transition: none; }
  a.mp-card:hover img { transform: none; }
}

@media (max-width: 720px) {
  .mp-rail { grid-auto-columns: minmax(230px, 76%); gap: 16px; }
}

/* The rail's arrows sit on the heading's line, right aligned. */
.mp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}
.mp-head .t-section { margin-bottom: 0; }
.mp-arrows { flex: none; }
/* Chevrons, not arrows: no shaft, just the turn. Both carousels use them,
   so the rule sits on the class they share. */
.linkedin-arrows button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.linkedin-arrows svg { width: 15px; height: 15px; }

/* One outcome has a project on this page to go to. The tile keeps its
   shape; the link just makes the whole of it the target. */
.proof-grid a {
  display: block;
  color: inherit;
  text-decoration: none;
}
.proof-grid a:hover b,
.proof-grid a:focus-visible b { color: var(--color-accent); }
.proof-grid a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 6px; }

/* Anchors parked the target at the top of the viewport, which is behind the
   57px sticky header: following a link to a section hid its number and often
   its heading. The header is 57px at every width, so one value covers all,
   plus room so the number is not flush against the header's edge. */
html { scroll-padding-top: 76px; }

/* The marks run 19 to 24px on a phone, which asks the reader to recognise a
   logo at a size where several of these are unreadable. Each carries its
   name below it there. Above 600px the marks are large enough to stand
   alone, and the row reads as a row of marks. The text repeats the alt the
   image already has, so it is aria-hidden: announcing it twice helps nobody. */
.sh-name { display: none; }

@media (max-width: 600px) {
  .sh-item {
    flex-direction: column;
    gap: 7px;
  }
  .sh-name {
    display: block;
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.02em;
    white-space: nowrap;
    color: var(--color-soft);
  }
}

/* The cards carry a cursor label saying why they do not click, which a
   touch screen never shows. This says it once, for everyone. */
.mp-note {
  margin: 16px 0 0;
  font-size: 13.5px;
  color: var(--color-soft);
}
