/* ───────── base ───────── */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--color-bg-page);
  color: var(--color-text-primary);
  font-family: var(--font-family-serif);
  font-weight: var(--font-weight-serif-light);   /* Body/Default = Light */
  font-size: var(--font-size-body);
  line-height: var(--line-height-body);
  -webkit-font-smoothing: antialiased;
}
img, video { display: block; max-width: 100%; }
a { color: inherit; }

.skip-link {
  position: absolute; left: -9999px;
  background: var(--color-text-primary); color: var(--color-text-inverse);
  padding: var(--space-sm) var(--space-md); z-index: 100;
}
.skip-link:focus { left: var(--layout-page-margin); top: var(--space-sm); }

:is(a, button):focus-visible {
  outline: 2px solid var(--color-text-link);
  outline-offset: 3px;
}

/* ───────── header ───────── */
.site-header {
  position: absolute; inset: 0 0 auto 0; z-index: 20;
  padding: var(--space-md) var(--layout-page-margin);
}
.logotype {
  /* 20.8, as Figma 377:24977 draws it — it had been on --font-size-label (16),
     which read a third too small against the hero beneath it */
  font-size: var(--font-size-body);
  text-decoration: none;
  font-family: var(--font-family-serif);
}

/* ───────── buttons ───────── */
.btn {
  display: inline-flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-radius: var(--radius-pill);
  font-family: var(--font-family-sans);
  font-size: var(--font-size-button);
  font-weight: var(--font-weight-sans-medium);
  line-height: var(--line-height-ui);
  text-decoration: none;
  border: var(--size-border-width) solid transparent;
  transition: background-color .2s ease, color .2s ease, border-color .2s ease,
              scale .12s ease;
}
.btn--solid {
  background: var(--color-button-solid-bg-on-light);
  color: var(--color-text-on-dark);
}
.btn--ghost { background: transparent; color: var(--color-text-on-light); }

/* Hover is a pointer affordance, and a touch device keeps :hover applied after
   a tap until you tap something else — which is what left the CTA looking
   stuck. Behind this guard, touch never enters the state at all. The press is
   tied to the gesture instead: :active ends the moment the finger lifts, so it
   clears itself and no timer has to decide when to release it. */
@media (hover: hover) {
  .btn--solid:hover { background: var(--color-button-solid-bg-on-light-hover); }
  .btn--ghost:hover { background: var(--color-button-ghost-bg-hover-on-light); }
}
.btn:active { scale: .97; }
.btn--solid:active { background: var(--color-button-solid-bg-on-light-hover); }
.btn--ghost:active { background: var(--color-button-ghost-bg-hover-on-light); }

/* Current section. A raised white pill against the 80%-white glass reads as
   selected without competing with the solid Contact CTA, and the label weight
   is unchanged so the nav never resizes as you scroll. */
.site-nav .btn--ghost {
  transition: background-color .2s ease, box-shadow .2s ease, scale .12s ease;
}
.site-nav .btn--ghost.is-current,
.site-nav .btn--ghost.is-current:hover {
  /* filled, not outlined — a white pill needed a hairline and a shadow to
     show up against near-white glass at all, and still read as a faint
     outline rather than as a selection */
  background: var(--color-nav-current-bg);
  box-shadow: none;
}
.nav-popover a.is-current,
.nav-popover a.is-current:hover { background: var(--color-nav-current-bg); }

/* Small size. Lands at 42px tall against the default's 57, so a bar built
   from them is a third shorter. The avatar variant uses a tighter pad so both
   shapes come out the same height rather than centring at different sizes. */
.btn--sm {
  padding: 10px 18px;
  font-size: var(--font-size-button-sm);
}
.btn--sm.btn--avatar { padding: 6px 6px 6px 16px; }
.btn--sm .btn__dec { width: 28px; height: 28px; }
.btn--sm .btn__icon svg { width: 16px; height: 16px; }
.btn--avatar { padding: 12px 12px 12px 20px; }
.btn__dec {
  width: 34px; height: 34px; border-radius: 50%;
  /* re-cut from the full portrait: the old 240x240 avatar.jpg already had the
     chin running off its own bottom edge, so no background-position could
     recover it. This frames the head at ~70% with clearance all round. */
  background: #fff url("../source-images/avatar-2026.jpg") center/cover;
  flex: none;
}

/* ───────── hero ───────── */
.hero {
  /* was position:sticky, which is what let the sections ride up over it. They
     butt against it now, so it scrolls like everything else. isolate keeps the
     fixed media in the hero's own stacking context, or z-index:-1 would drop
     it behind the page background. */
  position: relative;
  isolation: isolate;
  min-height: var(--layout-hero-height);
  padding: var(--layout-intro-top) var(--layout-page-margin) var(--space-5xl);
  display: flex; align-items: flex-start;
  overflow: hidden;
}
.hero__media, .hero__still {
  /* Absolute, not fixed. Fixed matched the live site's framing but escapes
     the hero's overflow and repaints against the scroll, so it flickered
     through as you moved. The hero is viewport-tall instead, which gives the
     media the same box to cover without anything leaving the flow. */
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; z-index: -1;
}

/* The hero fades into the surface below it rather than being overlapped by it
   — Figma 377:24925, where the gradient runs the bottom 141 of an 800 hero. */
@media (min-width: 751px) {
  /* covering this instead of a fixed 1000px is what keeps the crop at 90px a
     side rather than 180 — the framing the live site gets */
  .hero { min-height: 100vh; }

  /* One content column for the whole page. The sections looked
     wider-margined than the hero not because their padding differed — it is
     the same token — but because their content is capped at
     --layout-content-width and centred, which lands the edge at ~101px rather
     than the 48px page margin. The hero and the header now share that edge,
     falling back to the plain margin once the viewport is narrower than the
     cap. The footer already aligned: .footer-meta is capped and centred. */
  :root {
    --layout-content-inset: max(var(--layout-page-margin),
                                calc((100% - var(--layout-content-width)) / 2));
  }
  .hero { padding-inline: var(--layout-content-inset); }
  .site-header { padding-inline: var(--layout-content-inset); }
}

.hero::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: var(--layout-hero-fade);
  background: linear-gradient(to bottom,
              rgb(245 245 245 / 0), var(--color-surface-section));
  pointer-events: none;
}
.hero__still { display: none; }
/* An explicit width, not just a cap. As a flex item this was shrink-to-fit,
   and the line-reveal rewrites the heading into block-level spans — so its
   max-content width became the longest line already broken, and the column
   settled at 599px instead of the 685 it was mocked at. */
.hero__intro {
  width: var(--layout-intro-width);
  max-width: 100%;
}
.hero__heading {
  margin: 0 0 var(--space-2xl);
  font-size: var(--font-size-hero);
  /* Figma binds this to font/style/serif-thin — ExtraLight under the
     Substitute mode it renders in, Thin in real Edu Arizona */
  font-weight: var(--font-weight-serif-thin);
  line-height: var(--line-height-heading);
  /* the rag is orphan-free at the 780 measure, but only by arithmetic — this
     keeps it that way through a copy change */
  text-wrap: pretty;
}
.hero__currently {
  margin: 0 0 var(--space-2xl);
  font-size: var(--font-size-hero-support);
  font-weight: var(--font-weight-serif-thin);
  /* "Health" was landing alone. The nbsp in the markup keeps the brand whole
     on its own; this is the belt-and-braces for whatever the copy becomes
     next, and degrades to a normal wrap where it is not supported. */
  text-wrap: pretty;
  line-height: var(--line-height-ui);
  color: var(--color-text-muted);
}
/* Thin italic, matching the roman it sits in. /fonts now carries the italic
   at 100, 300 and 500, so the weight resolves exactly instead of falling up
   to the nearest available face — which is what made this read heavy twice
   before. */
.hero__currently em {
  font-weight: var(--font-weight-serif-thin);
  font-style: italic;
}
.hero__actions { margin: 0; }
/* The subtext and the button follow the tagline. Both stay visible without
   JS — the hidden state lives only inside the keyframes, and the fill holds
   it during the delay rather than before the class lands. */
.hero__currently { --line-delay-base: var(--motion-hero-sub-delay); }
.hero__actions.is-revealing {
  animation: line-in var(--motion-line-duration) cubic-bezier(.22,.61,.24,1) both;
  animation-delay: var(--motion-hero-btn-delay);
}

.scroll-cue {
  position: absolute; left: 50%; bottom: var(--space-5xl);
  transform: translateX(-50%);
  display: grid; place-items: center;
  width: 95px; height: 60px;
  border-radius: var(--radius-pill);
  background: rgb(255 255 255 / .55);
  backdrop-filter: blur(12px);
  color: var(--color-text-on-light);
  transition: background-color .2s ease, color .2s ease;
}
@media (hover: hover) {
  /* the same fill the nav's ghost buttons take, rather than its own ink one */
  .scroll-cue:hover {
    background: var(--color-button-ghost-bg-hover-on-light);
  }
}
.scroll-cue:active { scale: .96; }
.scroll-cue svg { width: 13px; height: 26px; }

/* ───────── site nav — pill on desktop, bottom bar on mobile ───────── */
.site-nav {
  position: fixed; z-index: 40;
  top: var(--space-lg); left: 50%;
  transform: translateX(-50%) translateY(-12px);
  display: flex; align-items: center; gap: var(--space-sm);
  width: max-content; padding: var(--space-sm) var(--space-sm);
  border-radius: var(--radius-infinite);
  background: rgb(255 255 255 / .80);
  backdrop-filter: blur(20px);
  box-shadow: 0 8px 32px rgb(0 0 0 / .10);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .25s ease, transform .25s ease, visibility .25s;
}
.site-nav.is-visible {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}
.site-nav__links { display: contents; }
.site-nav__disclosure { display: none; }

.nav-popover {
  margin: 0; padding: var(--space-sm); border: 0;
  border-radius: var(--radius-card);
  background: var(--color-bg-page);
  /* no scrim: this is a menu anchored to its button, not a modal sheet, so
     the shadow alone lifts it. A tight contact layer under a soft ambient one
     keeps the edge legible against whatever it happens to sit over. */
  box-shadow: 0 1px 2px rgb(0 0 0 / .10),
              0 12px 40px rgb(0 0 0 / .22);
  font-family: var(--font-family-sans);
  font-size: var(--font-size-label);
  /* a popover toggles `display`, so the discrete properties have to be allowed
     to animate or it simply appears. @starting-style supplies the from-state
     the open rule would otherwise never have. */
  transform-origin: bottom left;
  opacity: 0;
  transform: translateY(6px) scale(.98);
  transition: opacity .18s ease,
              transform .18s cubic-bezier(.2, .7, .3, 1),
              display .18s allow-discrete,
              overlay .18s allow-discrete;
}
.nav-popover:popover-open { opacity: 1; transform: none; }
@starting-style {
  .nav-popover:popover-open { opacity: 0; transform: translateY(6px) scale(.98); }
}
.nav-popover a {
  display: block; padding: var(--space-md) var(--space-lg);
  border-radius: var(--radius-pill); text-decoration: none;
}
@media (hover: hover) {
  .nav-popover a:hover { background: var(--color-button-ghost-bg-hover-on-light); }
}
/* chevron flips while the sheet is open, so the control reads as a disclosure */
.site-nav__disclosure[aria-expanded="true"] svg { transform: rotate(180deg); }
.site-nav__disclosure svg { transition: transform .2s ease; }

/* ───────── glass sheets ───────── */
.sheet {
  /* rides up over whatever is behind it; rounded top + translucency do the overlay */
  position: relative;
  border-radius: var(--sheet-radius) var(--sheet-radius) 0 0;
  padding: var(--space-surface-pad) var(--layout-page-margin);
}
.skills {
  z-index: 2;
  /* opaque, not glass: with nothing behind it to show through there is
     nothing for the blur to do, and the translucency was half the card-stack
     effect */
  background: var(--color-surface-section);
}
.about .carousel {
  /* the section carries data-theme="dark", which flips --color-bg-page to
     black — but every other rule here (on-light ink, ink-filled next arrow)
     assumes a light card, so pin the surface rather than inherit it */
  background: var(--primitive-white);
  --sheet-ink: var(--color-text-on-light);
  --sheet-rule: rgb(0 0 0 / .16);
  --sheet-dot: rgb(0 0 0 / .22);
  --sheet-hover: rgb(0 0 0 / .06);
  color: var(--color-text-on-light);
}
.about .carousel__arrow--next {
  background: var(--color-card-surface-ink);
  color: var(--color-text-on-dark);
}
.about .carousel__dot[aria-selected="true"] { background: var(--color-card-surface-ink); }
.about {
  --sheet-ink: var(--color-text-on-dark);
  --sheet-rule: rgb(255 255 255 / .35);
  --sheet-dot: rgb(255 255 255 / .32);
  --sheet-hover: rgb(255 255 255 / .12);
  z-index: 3;
  background: var(--sheet-dark);
  color: var(--color-text-on-dark);
}

/* light-surface About — same structure, inverted ink */
.about--light {
  --sheet-ink: var(--color-text-on-light);
  --sheet-rule: rgb(0 0 0 / .16);
  --sheet-dot: rgb(0 0 0 / .22);
  --sheet-hover: rgb(0 0 0 / .06);
  background: var(--color-surface-section);
  color: var(--color-text-on-light);
}
.about--light .carousel { background: var(--color-bg-page); }
.about--light .carousel__arrow--next {
  background: var(--color-card-surface-ink);
  color: var(--color-text-on-dark);
}
@media (hover: hover) {
  .about--light .carousel__arrow--next:hover { background: var(--primitive-grey-950); }
}
.about--light .carousel__dot[aria-selected="true"] { background: var(--color-card-surface-ink); }

/* ───────── section furniture ───────── */
/* Chivo Mono, and the mono already has the spacing built in — the .08em
   tracking that was here on top of Inter is what made these read wrong. */
.eyebrow-chip {
  /* plain centred text, not a tag — 377:24941 draws it as an outline button
     with no fill and no stroke, which is just the label */
  width: max-content; margin: 0 auto var(--space-lg);
  font-family: var(--font-family-mono);
  font-size: var(--font-size-eyebrow-section);
  font-weight: var(--font-weight-mono-medium);
  letter-spacing: 0; text-transform: uppercase;
  color: var(--color-text-primary);
}
/* the About eyebrow sits on a sheet, so it takes the sheet's ink and keeps the
   underline it has in Figma; the Skills one has neither */
.eyebrow-chip--bare {
  color: var(--sheet-ink, var(--color-text-on-dark));
}
.section-heading {
  max-width: var(--layout-col-main);
  margin: 0 auto var(--space-5xl);
  font-size: var(--font-size-hero);
  font-weight: var(--font-weight-serif-thin);   /* serif-thin, as the hero */
  line-height: var(--line-height-heading);
  text-align: center;
}

/* ───────── skill cards ───────── */
.card-grid {
  list-style: none; margin: 0 auto; padding: 0;
  max-width: var(--layout-content-width);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--layout-card-gap-y) var(--layout-card-gap-x);
}
.card {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: var(--space-lg);
  min-height: var(--layout-card-h);
  /* the media is a rounded block inset 16px from the card's bottom and sides,
     so the bottom padding is that inset and the media breaks out of the 40px
     side padding with negative margins */
  padding: var(--space-card-pad-large) var(--space-card-pad-x) var(--space-card-media-inset);
  border-radius: var(--radius-card-large);
  /* the hairline Figma draws on the Skill Card, and the same one the carousel
     slides carry. Inside the box, so the media stays symmetric: every edge
     picks up the border's 1px together rather than only the sides. */
  border: var(--size-border-width) solid var(--color-card-border);
  background: var(--color-bg-page);
}
.card:nth-child(2) { background: var(--color-card-surface-paper); }

/* Flipping reveals the card's own accent — the same colour as its media, which
   is fading out underneath. All five clear AA against the #2E2E2E copy
   (periwinkle is the tightest at 4.91), so the ink does not need to change. */
.card--ui             { --card-accent: var(--primitive-gold-400); }
.card--prototyping    { --card-accent: var(--primitive-coral-400); }
.card--content        { --card-accent: var(--primitive-chartreuse-300); }
.card--design-systems { --card-accent: var(--primitive-periwinkle-400); }
.card--user-research  { --card-accent: var(--primitive-pink-300); }
.card.is-flipped      { background: var(--card-accent, var(--color-bg-page)); }
/* the fifth card is centred, not bigger — pin it to a column width so it keeps
   matching the other four as the grid narrows */
.card--wide {
  grid-column: 1 / -1; justify-self: center;
  width: calc((100% - var(--layout-card-gap-x)) / 2);
}
.card__eyebrow {
  margin: 0;
  font-family: var(--font-family-mono);
  font-size: var(--font-size-eyebrow);
  font-weight: var(--font-weight-mono-light);
  letter-spacing: 0; text-transform: uppercase;
  color: var(--color-text-primary);
}
.card__face--front {
  margin: 0;
  font-size: var(--font-size-h1);
  font-weight: var(--font-weight-serif-thin);
  line-height: var(--line-height-heading);
}
.card__frame {
  display: block;
  margin-top: auto;                                /* pin to the card's bottom */
  margin-inline: calc(var(--space-card-media-inset) - var(--space-card-pad-x));
  width: calc(100% + 2 * (var(--space-card-pad-x) - var(--space-card-media-inset)));
  aspect-ratio: 548 / 308;                         /* the exported band */
  border-radius: var(--radius-card);
  overflow: hidden;                                /* so hover scales the art, not the block */
  transition: opacity .28s ease;
}
.card__media {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
}

/* ── flip faces ── */
/* deliberately not a positioning context: the stack is only as tall as the
   front copy, so the back face resolves against the card and can centre
   against the whole face instead of against one line of heading */
.card__stack { position: static; }
.card__face { margin: 0; transition: opacity .28s ease, visibility .28s; }
.card__face--back {
  position: absolute;
  inset: var(--space-card-pad-large) var(--space-card-pad-x)
         var(--space-card-media-inset);
  /* grid rather than block so the copy centres against the full card face */
  display: grid; align-content: center;
  opacity: 0; pointer-events: none;
  font-family: var(--font-family-grotesk);
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-grotesk-light);
  line-height: var(--line-height-body);
}
.card.is-flipped .card__face--front { opacity: 0; visibility: hidden; }
.card.is-flipped .card__face--back  { opacity: 1; pointer-events: auto; }
.card.is-flipped .card__frame       { opacity: 0; }

.card { cursor: pointer; transition: box-shadow .25s ease, background-color .3s ease; }
.card:hover { box-shadow: 0 10px 36px rgb(0 0 0 / .09); }
.card:hover .card__flip { transform: scale(1.14); }
.card.is-flipped:hover .card__flip { transform: rotate(45deg) scale(1.14); }
.card:hover .card__flip svg { transform: scale(1.06); }

.card__flip {
  position: absolute;
  right: var(--space-card-action-inset); bottom: var(--space-card-action-inset);
  width: var(--size-card-action); height: var(--size-card-action);
  display: grid; place-items: center; cursor: pointer;
  border: 0; border-radius: 50%;
  background: var(--color-card-surface-ink);
  color: var(--color-text-on-dark);
  transition: transform .28s cubic-bezier(.34,1.3,.5,1);
}
.card__flip svg {
  width: var(--size-card-glyph); height: var(--size-card-glyph);
  transition: transform .25s cubic-bezier(.34,1.3,.5,1);
}
.card.is-flipped .card__flip { transform: rotate(45deg); }

.u-visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ───────── about carousel ───────── */
/* Geometry from Figma 226:18248 — text column dominant, media full-height,
   controls pinned bottom-left so their baseline matches the media's bottom edge. */
.carousel {
  position: relative;
  container-type: inline-size;        /* mobile arrows measure off the card width */
  max-width: var(--layout-content-width);
  margin: 0 auto;
  padding: var(--space-card-pad-large);
  border: var(--size-border-width) solid var(--color-card-border);
  border-radius: var(--radius-card);
  background: var(--color-bg-page);          /* white, same as the flip cards */
}

.carousel__viewport {
  /* A real scroll-snap track. The slides used to share one grid cell and
     cross-fade, so a swipe had nothing to move — the gesture, the momentum and
     the snap are now the browser's, which is both better than the hand-rolled
     pointer tracking it replaces and the only way to show the neighbouring
     cards at the same time. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.carousel__viewport::-webkit-scrollbar { display: none; }
.slide {
  scroll-snap-align: center;
  display: grid;
  /* Equal columns. A fixed 572px media starved the copy as the window narrowed
     — at a 900px viewport the text column computed to -4px. Both sides now
     shrink together, and the media's aspect ratio sets the row height. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--layout-gutter);
  align-items: center;
}

/* each slide carries its own field colour; only the mobile layout paints it */
.slide--pink { --slide-accent: var(--primitive-pink-300); }
.slide--gold { --slide-accent: var(--primitive-gold-400); }
.slide--lime { --slide-accent: var(--primitive-chartreuse-300); }


.slide__copy {
  margin: 0;
  /* keep the copy clear of the controls pinned beneath it */
  padding-bottom: calc(var(--size-card-action) + var(--space-lg));
  font-family: var(--font-family-grotesk);
  font-size: var(--font-size-slide);
  font-weight: var(--font-weight-grotesk-light);
  line-height: var(--line-height-body);
  /* starts at the top of the slide, as in Figma — the leftover space sits
     under the copy rather than being split around it */
  align-self: start;
}
.slide__media {
  width: 100%;
  aspect-ratio: 572 / 474;                   /* the landscape graphics */
  object-fit: cover;
  border-radius: var(--radius-card);
  /* The graphics are transparent PNGs, so the slide's accent carries straight
     through as the field — the same colour the card gets below 700, without a
     second set of exports. The portrait has no accent and stays on the card. */
  background: var(--slide-accent, transparent);
}
/* The intro slide reads better with a wide measure, so it doesn't split 50/50.
   Its media column is sized to land the portrait on exactly the row height the
   landscape slides set — (avail / 2) / 1.2068 * 0.6076 — which works out to
   144/572 of the available width and leaves ~72% for the copy, matching the
   original Figma proportion (820 / 1144). */
.slide--portrait {
  grid-template-columns: minmax(0, 1fr) calc((100% - var(--layout-gutter)) * 0.2518);
}
.slide--portrait .slide__media {
  width: 100%;
  aspect-ratio: 288 / 474;
}
@keyframes slide-panel-in {
  from { transform: translateY(18px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

.carousel__controls {
  position: absolute;
  left: var(--space-card-pad-large);
  bottom: var(--space-card-pad-large);
  display: flex; align-items: center; gap: var(--space-md);
}
.carousel__arrow {
  width: var(--size-card-action); height: var(--size-card-action);
  display: grid; place-items: center;
  border-radius: 50%; cursor: pointer;
  background: none; color: var(--sheet-ink);
  border: var(--size-border-width) solid var(--sheet-rule);
  transition: background-color .2s ease, color .2s ease, border-color .2s ease,
              scale .12s ease;
}
@media (hover: hover) {
  .carousel__arrow:hover { background: var(--sheet-hover); }
}
/* Composed, not replaced. The press used to be a bare scale(), which wiped
   out the translateY(-50%) that centres the arrow below 700 — so a tap
   dropped it half its own height, and :active sticks on touch, so it stayed
   there. Both parts now live in the same transform. */
.carousel__arrow { transform: translateY(var(--arrow-y, 0)); }
/* the independent scale property, not a transform: it composes with the
   translateY above instead of replacing it */
.carousel__arrow:active { scale: .94; }
.carousel__arrow--next {
  background: var(--color-text-on-dark);
  color: var(--color-text-on-light);
  border-color: transparent;
}
@media (hover: hover) {
  .carousel__arrow--next:hover { background: var(--primitive-grey-100); }
}
.carousel__arrow svg { width: 26px; height: 13px; }
.carousel__dots { display: flex; gap: 10px; margin-left: var(--space-sm); }
.carousel__dot {
  width: 8px; height: 8px; padding: 0; border: 0; border-radius: 50%;
  background: var(--sheet-dot); cursor: pointer;
  transition: background-color .25s ease, transform .25s ease;
}
.carousel__dot[aria-selected="true"] {
  background: var(--color-text-on-dark);
  transform: scale(1.25);
}

/* Glass pill around the controls, from 485:30070. Only above the compact
   breakpoint — below it the controls are display:contents and the arrows
   straddle the card edge, so there is no pill to fill. The negative offsets
   keep the arrows where they already sat; the pill grows around them. Figma's
   blur there is progressive, which CSS has no equivalent for, so this takes
   the 12px start radius and applies it evenly. */
@media (min-width: 901px) {
  .carousel__controls {
    left: calc(var(--space-card-pad-large) - var(--space-md));
    bottom: calc(var(--space-card-pad-large) - var(--space-md));
    padding: var(--space-md);
    border-radius: var(--radius-infinite);
    background: linear-gradient(to bottom,
                rgb(255 255 255 / 0) 0%, rgb(255 255 255 / .8) 50%);
    backdrop-filter: blur(12px);
  }
  /* fixed white, not --color-bg-page: that token flips to black inside the
     dark .about sheet, which left the prev arrow black with a dark glyph */
  .carousel__controls .carousel__arrow {
    background: var(--primitive-white);
    border-color: var(--color-card-border);
    color: var(--color-text-on-light);
  }
  .carousel__controls .carousel__arrow--next {
    background: var(--color-card-surface-ink);
    color: var(--color-text-on-dark);
    border-color: transparent;
  }
  .carousel__controls .carousel__dot { background: rgb(46 46 46 / .25); }
  .carousel__controls .carousel__dot[aria-selected="true"] {
    background: var(--color-card-surface-ink);
  }
}

/* 900-700: hold two columns and shrink instead of stacking. Columns land at
   ~300px, the first slide's media goes square so it fills its column instead
   of leaving a tall gap beside the copy, and the controls move to the compact
   treatment. */
@media (max-width: 900px) {
  .carousel { padding: var(--space-lg); }
  .slide--portrait { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  /* every slide's media is square here, not just the headshot — the landscape
     graphics left dead space above and below themselves otherwise. They are
     letterboxed rather than cropped, onto the same surface they are drawn on,
     so the extra height reads as part of the graphic. */
  .slide__media {
    width: 100%;
    aspect-ratio: 1;
    object-fit: contain;
    /* letterboxed onto the accent rather than the neutral surface, so the
       extra height still reads as part of the graphic */
    background: var(--slide-accent, var(--color-media-surface));
  }
  /* the base .slide--portrait rule is more specific than .slide__media, so the
     square has to be restated here or the headshot keeps its 288:474 */
  .slide--portrait .slide__media {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    background: none;
  }
  .slide__copy { padding-bottom: 0; align-self: start; }

  /* Indicators sit below the card body, not inside it. display:contents on the
     wrapper rather than positioning it: making the wrapper absolute would turn
     it into the containing block for the arrows, which have to keep resolving
     against the card. */
  .carousel { margin-bottom: calc(var(--space-lg) + 8px); }
  .carousel__controls { display: contents; }
  .carousel__dots {
    position: absolute;
    left: 0; right: 0;
    bottom: calc(-1 * (var(--space-lg) + 8px));
    margin-left: 0;
    justify-content: center;
  }

  /* Arrows straddle the card edge. The page margin is smaller than the control,
     so half of it overlaps the card — that still keeps it clear of the text.
     The slide row is exactly as tall as the square, which is half the content
     width less the gutter, so container query units put the arrow on the media
     midpoint rather than the card's (those differ by ~16px). */
  .carousel__arrow {
    position: absolute;
    z-index: 2;
    top: calc(var(--space-lg) + (100cqw - var(--layout-gutter)) / 4
              - var(--size-carousel-arrow-mobile) / 2);
    width: var(--size-carousel-arrow-mobile);
    height: var(--size-carousel-arrow-mobile);
    box-shadow: 0 2px 10px rgb(0 0 0 / .16);
  }
  .carousel__arrow[data-prev] {
    left: calc(var(--size-carousel-arrow-mobile) / -2);
    background: var(--color-bg-page);
  }
  .carousel__arrow--next { right: calc(var(--size-carousel-arrow-mobile) / -2); }
  .carousel__arrow svg { width: 22px; height: 11px; }
}

/* Below 700 even ~300px columns are too tight, so the slide stacks. */
@media (max-width: 700px) {
  /* align-content, not align-items: the slides share one grid cell, so without
     it the auto rows stretch to the tallest slide and tear copy from media */
  /* Figma 438:11100 — the card's interior is one coloured field running the
     full height, with the artwork floating in the upper square and the copy in
     a rounded panel at the bottom. The field is square-cornered; something
     above it has to do the rounding. */
  /* The track runs full-bleed so the neighbouring cards can sit in the page
     margin and peek. The carousel is no longer the card — each slide is — so
     its border, fill and radius come off here. */
  .carousel {
    /* the inner width the card's own border leaves behind, which is what the
       artwork square has to match */
    --slide-card-inner: calc(var(--slide-card-w) - 2 * var(--size-border-width));
    /* One source of truth for the card width. An element cannot query itself,
       so every cqw below resolves against this full-bleed carousel — which is
       exactly why the width has to be named once and reused, rather than each
       rule deriving it again. */
    --slide-card-w: min(var(--layout-slide-card-max),
                        calc(100cqw - 2 * (var(--layout-slide-peek)
                                           + var(--layout-slide-peek-gap))));
    padding: 0;
    max-width: none;
    margin-inline: calc(var(--layout-page-margin) * -1);
    border: 0;
    border-radius: 0;
    background: none;
  }
  .about .carousel,
  .about--light .carousel { background: none; }

  /* The scroller's inline padding is peek + gap, and the card is simply 100%
     of what is left. That is what pins the peek at exactly 16px: when the card
     reaches its 420 cap the scroller caps at the same width plus the two
     paddings, so the sum never drifts. */
  .carousel__viewport {
    max-width: calc(var(--layout-slide-card-max)
                    + 2 * (var(--layout-slide-peek) + var(--layout-slide-peek-gap)));
    margin-inline: auto;
    padding-inline: calc(var(--layout-slide-peek) + var(--layout-slide-peek-gap));
    gap: var(--layout-slide-peek-gap);
    grid-auto-columns: var(--slide-card-w);
    border-radius: 0;
  }
  /* each slide is the card now, so the clip and the radius live here */
  .slide,
  .slide--portrait {
    border-radius: var(--radius-card);
    overflow: hidden;
    /* the hairline used to be on .carousel, which drew it around the card.
       The slide is the card now, so it carries its own. */
    border: var(--size-border-width) solid var(--color-card-border);
    /* The carousel no longer paints the card, so each slide carries its own
       field. The fallback has to be a fixed white, not --color-bg-page: that
       token flips to black inside the dark .about sheet, which turned slide 1
       into a black card — visible the moment the panel animates down. */
    background: var(--slide-accent, var(--primitive-white));
    grid-template-columns: minmax(0, 1fr);
    /* the image region is square: the card's inner width, less its 1px border */
    /* 1fr, not auto: row 2 then absorbs the slack so every slide's panel is the
       same height as the tallest without measuring any of them */
    grid-template-rows: var(--slide-card-inner) 1fr;
    row-gap: 0;
    align-self: stretch;
  }

  /* the markup is copy-then-media, but this layout puts the image first */
  .slide__media { grid-row: 1; }
  .slide__copy  { grid-row: 2; }

  /* the artwork floats inset on the field; a photo fills it instead */
  .slide__media,
  .slide--portrait .slide__media {
    width: 90.4%;                       /* 300 of 332 */
    max-width: none;
    height: auto;
    aspect-ratio: auto;
    object-fit: contain;
    /* sits above centre, as drawn: 22 of a 332 field on 438:11100, kept as a
       ratio so it holds when the card grows to its 420 cap */
    align-self: start;
    margin-top: calc(var(--slide-card-inner) * 0.0663);
    margin-inline: auto;
    border-radius: var(--radius-slide-art);
    background: none;
  }
  /* Slide 1 is served a pre-cropped square on mobile — 432:87 frames a 664x664
     region of the portrait, which object-fit cannot reach from the full file at
     this box size. Cropping the source means the box and the image share an
     aspect and nothing is re-cropped here. It runs 24px under the panel so the
     panel's rounded top corners land on photo. */
  .slide--portrait { position: relative; }
  .slide--portrait .slide__media {
    position: absolute;
    top: 0; left: 0;
    margin-top: 0;          /* the shared rule's top bias must not apply here */
    width: 100%;
    height: calc(var(--slide-card-inner) + var(--radius-card));
    object-fit: cover;
    border-radius: 0;
  }
  .slide--portrait .slide__copy { position: relative; z-index: 1; }

  /* the copy is its own panel; align-content:stretch makes row 2 absorb the
     slack, so every slide's panel is the same height without measuring */
  .slide__copy {
    align-self: stretch;
    /* flush to the card's inner edge: a margin here let the field colour show
       as a hairline all the way round the panel */
    margin: 0;
    padding: 28px 32px;
    /* only the top corners: the viewport's clip owns the bottom two, and
       rounding them here at a slightly larger radius let the field colour
       show through as a ring */
    border-radius: var(--radius-card) var(--radius-card) 0 0;
    background: var(--color-slide-panel);
  }

  /* Arrows straddle the card edge, as they did before the track. The carousel
     is full-bleed now, so they can no longer be offset from its own edge —
     they hang off the card, which is centred inside it. z-index keeps them
     over the edge treatment below, so they never pick up its blur. */
  /* Centred on the artwork square, not on the card. 50% of the card depended
     on the copy panel's height, which depends on where the text wraps, which
     depends on when the webfont lands — so the arrows moved after load. The
     square is a pure function of the viewport width and cannot shift. */
  .carousel__arrow {
    top: calc(var(--slide-card-w) / 2);
    bottom: auto;
    --arrow-y: -50%;
    z-index: 2;
  }
  .carousel__arrow[data-prev] {
    left: calc((100% - var(--slide-card-w)) / 2
               - var(--size-carousel-arrow-mobile) / 2);
  }
  .carousel__arrow--next {
    right: calc((100% - var(--slide-card-w)) / 2
                - var(--size-carousel-arrow-mobile) / 2);
  }

  /* The panel moves in from below the card, and the type follows it — the
     slide's own 18px of field shows underneath for the length of the travel.
     Driven by a class rather than :is-current so it can be replayed, and only
     applied when a control was used: after a free swipe the panel is already
     on screen, and re-animating it there reads as a glitch. */
  .slide:not(.is-current) {
    filter: blur(3px);
    transform: scale(.94);
    opacity: .85;
  }
  .slide {
    transition: filter .3s ease,
                transform .3s cubic-bezier(.22, .61, .24, 1),
                opacity .3s ease;
  }

  .slide.is-entering .slide__copy {
    animation: slide-panel-in var(--motion-slide-panel)
               cubic-bezier(.22, .61, .24, 1) both;
  }

  /* Soften the peek, two ways. The scroller is masked so a neighbour fades out
     at the edge instead of ending on a cut, and the slides that are not
     centred are blurred and held back a little, so they read as waiting rather
     than as cropped. The class tracks the scroll live rather than waiting for
     it to settle, or a card would stay blurred under your finger. */
  .carousel__viewport {
    --edge: calc(var(--layout-slide-peek) + var(--layout-slide-peek-gap));
    -webkit-mask-image: linear-gradient(to right,
        transparent 0, #000 var(--edge),
        #000 calc(100% - var(--edge)), transparent 100%);
    mask-image: linear-gradient(to right,
        transparent 0, #000 var(--edge),
        #000 calc(100% - var(--edge)), transparent 100%);
  }

}


/* ───────── footer ───────── */
/* 513:30134 — the meta row on its own. The page already ends on the section
   surface, so the footer continues it and a hairline across the content
   column does the separating; the black band is gone. The space above the
   rule is the About section's own bottom padding. */
.site-footer {
  position: relative; z-index: 4;
  background: var(--color-surface-section);
  color: var(--color-text-primary);
  padding: 0 var(--layout-page-margin) var(--space-4xl);
}
.footer-meta {
  max-width: var(--layout-content-width);
  margin: 0 auto;
  padding-top: var(--space-xl);
  border-top: var(--size-border-width) solid var(--color-card-border);
  /* three equal columns on the 1224 column, which is 376 + 48 gaps as drawn */
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3xl);
  font-family: var(--font-family-sans);
  font-size: var(--font-size-micro);
}
/* label above value, not label-left value-right */
.footer-meta > div { display: grid; gap: var(--space-xs); }
.footer-meta dt { color: var(--color-text-muted); }
.footer-meta dd { margin: 0; color: var(--color-text-primary); }
.footer-meta a { color: inherit; text-decoration: none; }
@media (hover: hover) {
  .footer-meta a:hover { text-decoration: underline; text-underline-offset: 3px; }
}

/* ───────── mobile ───────── */
@media (max-width: 750px) {
  /* The hero stops being a full-bleed background here. Copy sits on white and
     the video becomes a square block below it, matching the square media the
     cards and carousel already use. Nothing is pinned, so no sticky and no
     text-shadow to hold legibility over video. */
  /* no fade here: the media is an inline card on this layout, not a full-bleed
     background, so a bottom gradient just hazes the photo */
  .hero::after { content: none; }
  .hero {
    position: static;
    min-height: 0;
    overflow: visible;
    flex-direction: column;
    align-items: stretch;
    padding: var(--space-5xl) var(--layout-page-margin) var(--space-4xl);
  }
  .hero__intro { order: 1; width: auto; max-width: none; }
  .hero__media,
  .hero__still {
    order: 2;
    position: static;
    inset: auto;
    z-index: auto;
    width: 100%;
    height: auto;
    aspect-ratio: 1;
    object-fit: cover;
    /* The square block uses the landscape encode, not the 9:16 one: at 1:1 the
       portrait crop is a tight head shot, while the mock's framing comes from
       the wide source anchored right. Matched against 418:26951. */
    object-position: 95% 50%;
    border-radius: var(--radius-card);
    margin-top: var(--space-xl);
  }
  .hero__heading { margin-bottom: var(--space-lg); text-shadow: none; }
  .hero__currently {
    margin-bottom: var(--space-xl);
    font-size: var(--font-size-h4);
    color: var(--color-text-on-light);
  }
  /* the bottom bar owns this space, and the video already implies "scroll" */
  .scroll-cue { display: none; }

  /* nav becomes a full-width bar pinned to the bottom */
  .site-nav {
    top: auto; bottom: 0; left: 0;
    /* Move in, rather than dissolve. A 12px nudge under a fade read as a
       cross-dissolve; the bar now travels its whole height up from under the
       edge and stays opaque the whole way, so it arrives rather than appears.
       Reduced motion is already covered by the blanket transition-duration
       override further down, which snaps it into place instead. */
    transform: translateY(100%);
    opacity: 1;
    transition: transform .32s cubic-bezier(.22, .61, .24, 1), visibility .32s;
    width: 100%; border-radius: 0;
    justify-content: space-between;
    padding: var(--space-md) var(--layout-page-margin)
             calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
    box-shadow: 0 -1px 0 var(--color-border-subtle), 0 -8px 32px rgb(0 0 0 / .08);
  }
  .site-nav.is-visible { transform: translateY(0); }
  .site-nav__links { display: none; }
  .site-nav__disclosure {
    display: inline-flex; align-items: center; gap: var(--space-sm);
    padding: var(--space-md) 0; border: 0; background: none; cursor: pointer;
    font-family: var(--font-family-sans);
    font-size: var(--font-size-label);
    font-weight: var(--font-weight-sans-medium);
    color: var(--color-text-primary);
  }
  .site-nav__disclosure svg { width: 12px; height: 8px; }
  /* sits above the bar it opened from, not over it — --nav-h is measured in JS
     because the bar's height depends on the wrapped label */
  /* a menu above the control that opened it, not a full-width sheet */
  .nav-popover:popover-open {
    position: fixed;
    inset: auto auto
           calc(var(--nav-h, 88px) + var(--space-sm) + env(safe-area-inset-bottom, 0px))
           var(--layout-page-margin);
    width: max-content;
    min-width: 180px;
    max-width: calc(100% - 2 * var(--layout-page-margin));
    border-radius: var(--radius-card);
    padding: var(--space-sm);
  }

  .card-grid { grid-template-columns: minmax(0, 1fr); }

  /* The back copy starts below the eyebrow instead of centring over the whole
     card. Centred, the longest copy (User Research) reached 5px above the
     eyebrow's baseline at 390 — the card is sized by its front face, which is
     much shorter. The eyebrow's own line box is the only measured value here;
     the rest is the card's existing padding and row gap. */
  .card__face--back {
    inset-block-start: calc(var(--space-card-pad-large)
                            + var(--size-card-eyebrow) + var(--space-lg));
    align-content: start;
  }
  .card--wide { width: 100%; }
  .section-heading { font-size: var(--font-size-h1); }
  .sheet { padding-inline: var(--layout-page-margin); }
  .footer-meta { grid-template-columns: minmax(0, 1fr); gap: var(--space-lg); }
  /* the nav is a fixed bottom bar here, so the last row has to clear it.
     --nav-h is measured in JS, since the bar's height depends on its label. */
  .site-footer {
    padding-bottom: calc(var(--space-2xl) + var(--nav-h, 88px)
                         + env(safe-area-inset-bottom, 0px));
  }
}

/* ───────── reduced motion ───────── */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .hero__media { display: none; }
  .hero__still { display: block; }
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ───────── line-by-line reveal ───────── */
/* Text is visible by default. The hidden state lives only inside the keyframes,
   so a failed script or a browser that skips the animation still shows the copy. */
.line { display: block; overflow: hidden; }
.line > span { display: block; }

.is-revealing .line > span {
  animation: line-in var(--motion-line-duration) cubic-bezier(.22,.61,.24,1) both;
  /* the base lets a second block start after the one above it has led */
  animation-delay: calc(var(--line-delay-base, 0ms)
                        + var(--n, 0) * var(--motion-line-stagger));
  will-change: opacity, transform;
}
@keyframes line-in {
  from { opacity: 0; transform: translateY(0.52em); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .is-revealing .line > span { animation: none; }
}

/* ───────── animated gradient ring ─────────
   Stops lifted from the Figma "Button / Gradient border" component. The ring is a
   masked pseudo-element so it never touches the element's own background. */
@property --grad-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

.has-ring { position: relative; }
.has-ring::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  padding: 2px;
  background: conic-gradient(from var(--grad-angle),
    #FFCE70 0%, #F0B4E4 30%, #C6B6F0 58%, #A8E8DC 82%, #FFCE70 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  opacity: 0;
  transition: opacity .3s ease;
  pointer-events: none;
}
.has-ring:focus-visible::after { opacity: 1; animation: ring-spin 3.2s linear infinite; }
@media (hover: hover) {
  .has-ring:hover::after { opacity: 1; animation: ring-spin 3.2s linear infinite; }
}

@keyframes ring-spin { to { --grad-angle: 360deg; } }

/* no @property support: the ring still shows, it just doesn't rotate */
@supports not (background: conic-gradient(from var(--grad-angle), red, blue)) {
  .has-ring::after { background: conic-gradient(#FFCE70, #F0B4E4 30%, #C6B6F0 58%, #A8E8DC 82%, #FFCE70); }
}
@media (prefers-reduced-motion: reduce) {
  .has-ring:hover::after { animation: none; }
}

/* ───────── envelope reveal on the contact buttons ───────── */
.btn__icon {
  display: inline-flex; align-items: center;
  width: 0; margin-left: 0; opacity: 0; overflow: hidden;
  transition: width .3s cubic-bezier(.2,.7,.3,1),
              margin-left .3s cubic-bezier(.2,.7,.3,1),
              opacity .22s ease;
}
.btn__icon svg { width: 18px; height: 18px; flex: none; }
.btn:focus-visible .btn__icon { width: 18px; margin-left: var(--space-sm); opacity: 1; }
@media (hover: hover) {
  .btn:hover .btn__icon { width: 18px; margin-left: var(--space-sm); opacity: 1; }
}

/* the nav button already carries the avatar, so swap it rather than add to it */
.btn__dec { position: relative; }
.btn__dec svg {
  position: absolute; inset: 0; margin: auto;
  width: 16px; height: 16px;
  opacity: 0; transition: opacity .25s ease;
  color: var(--color-text-on-dark);   /* white glyph over the ink veil */
}
@media (hover: hover) {
  .btn--avatar:hover .btn__dec svg { opacity: 1; }
}
/* The veil used to be white, which lit the avatar up against a black button.
   Ink reads as the photo receding rather than flashing. */
.btn--avatar .btn__dec::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: var(--color-card-surface-ink);
  opacity: 0; transition: opacity .25s ease;
}
@media (hover: hover) {
  .btn--avatar:hover .btn__dec::after { opacity: .88; }
}
.btn--avatar .btn__dec svg { z-index: 1; }

/* ───────── flip card hover ───────── */
.card.has-ring::after { inset: 0; }
.card__media { transition: transform .45s cubic-bezier(.2,.7,.3,1); }
.card:hover .card__media { transform: scale(1.055); }
@media (prefers-reduced-motion: reduce) {
  .card:hover .card__media { transform: none; }
}

/* ───────── copy-to-clipboard toast ───────── */
.toast {
  position: fixed;
  left: 50%;
  bottom: var(--space-2xl);
  transform: translateX(-50%) translateY(12px);
  z-index: 60;
  display: flex; align-items: center; gap: var(--space-sm);
  width: min(360px, calc(100vw - var(--space-2xl)));
  padding: 16px 12px;
  border-radius: 16px;
  background: rgb(0 0 0 / .80);
  backdrop-filter: blur(32px);
  color: var(--color-text-on-dark);
  box-shadow: 0 12px 40px rgb(0 0 0 / .28);
  opacity: 0;
  transition: opacity .26s ease, transform .26s cubic-bezier(.2,.7,.3,1);
}
.toast[hidden] { display: none; }
.toast.is-open { opacity: 1; transform: translateX(-50%) translateY(0); }

.toast__check { width: 20px; height: 20px; flex: none; color: #6CD8A0; }
.toast__text {
  flex: 1; margin: 0;
  font-family: var(--font-family-sans);
  font-size: 15px;
  font-weight: var(--font-weight-sans-regular);
  line-height: var(--line-height-body);
}
.toast__close {
  flex: none; width: 24px; height: 24px; padding: 4px;
  display: grid; place-items: center; cursor: pointer;
  border: 0; border-radius: 8px;
  background: rgb(238 238 227 / .15);
  color: var(--color-text-on-dark);
  transition: background-color .2s ease;
}
.toast__close:hover { background: rgb(238 238 227 / .28); }
.toast__close svg { width: 10px; height: 10px; }

/* the bottom bar owns the bottom edge on mobile */
@media (max-width: 750px) {
  .toast { bottom: calc(var(--space-2xl) + 72px); }
}
@media (prefers-reduced-motion: reduce) {
  .toast { transition: opacity .01ms; transform: translateX(-50%); }
  .toast.is-open { transform: translateX(-50%); }
}
