/* Structural layout: containers, grids, page regions.
   Mobile-first — base rules target small screens, min-width media
   queries layer on enhancements for tablet (600px) / desktop (900px) /
   large desktop (1200px). */

.container {
  width: 100%;
  max-width: var(--container-width);
  margin-inline: auto;
  padding-inline: var(--container-padding);
}

/* Tablet range: the side rail (from 700px, see .site-nav below) already
   eats into the available width, so the mobile container padding reads
   as cramped against both the rail and the outer edge. Give it more
   room until the desktop breakpoint, where the wider viewport already
   provides enough natural whitespace. */
@media (min-width: 700px) and (max-width: 899px) {
  .container {
    padding-inline: var(--space-lg);
    margin-top: var(--space-lg);
  }

}

@media (max-width: 700px) {
  .container {
    padding-inline: var(--space-sm);
  }

}

/* Mobile-first: less vertical padding by default since a phone screen
   pays for every pixel of it in extra scrolling, then step back up to
   the roomier desktop value once there's enough viewport height (and
   width — 900px matches the .grid--3up desktop breakpoint above) to
   afford it. */
.section {
  padding-block: var(--space-sm);
}

.section--tight {
  padding-block: var(--space-2xs);
}

@media (min-width: 900px) {
  .section {
    padding-block: var(--space-2xl);
  }

  .section--tight {
    padding-block: var(--space-lg);
  }
}

/* Simple flex stack with consistent vertical rhythm */
.stack > * + * {
  margin-top: var(--gap, var(--space-sm));
}

/* Horizontal cluster of items that wraps on small screens */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gap, var(--space-xs));
}

/* Auto-fit responsive grid: 1 column on mobile, more as space allows */
.grid {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: 1fr;
}

@media (min-width: 600px) {
  .grid--2up,
  .grid--3up {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 900px) {
  .grid--3up {
    grid-template-columns: repeat(3, 1fr);
  }
  .grid--2up {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Site navigation
   Mobile: fixed bottom tab bar.
   Tablet/desktop (>=700px): fixed left side rail.
   `body` gets matching padding (below) so page content never sits
   underneath the fixed nav. */
.site-nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  display: flex;
  align-items: stretch;
  background: var(--color-bg);
  height: var(--tabbar-height);
  padding-inline: var(--space-3xs);
}

/* Floating glass pill — mobile bottom tab bar only. The desktop side
   rail is a flush, opaque edge of the page and wants none of this.

   The bar detaches from all three edges so page content scrolls
   through the gap underneath it, which is what makes the blur read as
   glass rather than as a flat translucent strip. It sits above the
   home-indicator inset rather than padding itself out to cover it, so
   the pill keeps its full stadium shape on notched devices.

   backdrop-filter is the whole effect; where it isn't supported the
   @supports below swaps in an opaque background so the bar stays
   legible instead of showing content straight through it. */
@media (max-width: 699px) {
  .site-nav {
    left: var(--space-sm);
    right: var(--space-sm);
    bottom: calc(var(--space-sm) + env(safe-area-inset-bottom, 0px));
    padding-inline: var(--space-2xs);
    background: var(--glass-bg);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-full);
    box-shadow: var(--glass-shadow);
  }

  @supports not (backdrop-filter: blur(1px)) {
    .site-nav {
      background: var(--color-bg);
    }
  }
}

/* Utility controls (Connect + theme toggle) vs. page navigation
   (Projects/About): the tabs stay the full pill shape, but tone down
   to a slightly less rounded corner so they read as "tabs" rather
   than matching the outlined, ghost-styled utility controls beyond
   the divider — see .site-nav__contact-trigger and the theme toggle's
   mobile rules (components.css) for the other half of that split. */
@media (max-width: 699px) {
  .site-nav__list .site-nav__link {
    border-radius: var(--radius-lg);
  }

  .site-nav__contact {
    flex: none;
    align-items: center;
    border-left: 1px solid var(--color-border);
    margin-left: var(--space-2xs);
    padding-left: var(--space-2xs);
  }

  /* Same 40px outlined circle as the theme toggle's mobile button
     (components.css) — icon only, no label, so the two utility
     controls read as a matched pair. */
  .site-nav__contact-trigger {
    flex: none;
    width: 40px;
    height: 40px;
    margin: 0;
    padding: 0;
    border: 1px solid var(--color-border);
  }

  .site-nav__contact-trigger .site-nav__link-label {
    display: none;
  }

  .site-nav__contact-trigger[aria-expanded="true"] {
    background: transparent;
    border-color: var(--color-accent);
  }
}

/* Collapse toggle is part of the side rail only — hidden in the mobile
   bottom tab bar. */
.site-nav__toggle {
  display: none;
}

/* Mobile: brand shows as an icon-only mark at the left of the tab bar. */
.site-nav__brand {
  display: flex;
  align-items: center;
  padding-inline: var(--space-sm);
}

.site-nav__brand-name {
  display: none;
}

/* flex: 2 rather than 1 is deliberate: .site-nav__contact (the Connect
   tab, below) is a flex: 1 sibling of this list, and the two li's in
   here split whatever width this list is given evenly between them.
   Giving the list twice the contact tab's share means each li ends up
   exactly the same width as the contact tab — three equal tabs — while
   still living in its own <ul> rather than flattening the list with
   display: contents, which would drop its list semantics. */
.site-nav__list {
  display: flex;
  flex: 2;
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-nav__list li {
  flex: 1;
  display: flex;
}

/* Bottom section holds just the theme switcher on mobile, shrunk to an
   icon-sized button at the right of the bar rather than a full tab. */
.site-nav__bottom {
  display: flex;
  flex: none;
  align-items: center;
  padding-inline: var(--space-sm);
}

.site-nav__link {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  margin: var(--space-3xs);
  /* Stadium, to nest cleanly inside the fully-rounded glass pill. */
  border-radius: var(--radius-full);
  color: var(--color-text-muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  text-decoration: none;
  transition: background var(--transition-fast), color var(--transition-fast),
    transform var(--transition-fast);
}

.site-nav__link:active {
  transform: scale(0.94);
}

.site-nav__link-icon {
  display: inline-flex;
  transition: transform var(--transition-base);
}

/* All Projects/All About Me carry a shorter mobile-only wording —
   the bottom tab bar gives each tab only ~50px, nowhere near enough
   for the full phrase on one line, so it was breaking mid-phrase
   ("All" / "Projects"). The desktop rail has room to spare, so it
   swaps back to the full label at the same 700px breakpoint the rest
   of the nav uses (see .site-nav__link-label-full below). */
.site-nav__link-label-full {
  display: none;
}

.site-nav__link[aria-current="page"] {
  color: var(--color-accent);
  background: var(--color-accent-soft);
}

.site-nav__link[aria-current="page"] .site-nav__link-icon {
  transform: translateY(-1px) scale(1.08);
}

/* Email/LinkedIn/GitHub: the desktop rail's own list of these lives
   below, styled for icon+label. On the mobile tab bar there isn't room
   for three more tabs, so it's hidden here in favor of the single
   .site-nav__contact trigger just below. */
.site-nav__social {
  display: none;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Mobile-only "Connect" tab: same width and visual weight as the two
   .site-nav__link tabs beside it (flex: 1, see the note on
   .site-nav__list above for how the three end up equal), but opens a
   popover instead of navigating. The trigger reuses .site-nav__link
   itself — same icon+label layout, same hover/active/tap treatment —
   so it reads as a third tab rather than a bolted-on control; only
   this wrapper's own rules are new. */
.site-nav__contact {
  position: relative;
  display: flex;
}

/* Mirrors .site-nav__link[aria-current="page"] visually (accent fill
   while "on"), but keyed off aria-expanded rather than aria-current —
   this tab never represents the current page, only whether its own
   popover is open. */
.site-nav__contact-trigger[aria-expanded="true"] {
  color: var(--color-accent);
  background: var(--color-accent-soft);
}

/* Opens upward from a bar pinned to the bottom of the screen, right-
   aligned so it doesn't overhang the viewport edge on narrow phones. */
/* [hidden] here beats the plain class selector below on specificity
   (0,2,0 vs 0,1,0) — needed because this element also sets its own
   `display: flex`, which at equal specificity would otherwise win over
   the browser's built-in `[hidden] { display: none }` and leave the
   menu visibly rendered (with the hidden attribute a no-op) whenever
   it isn't explicitly toggled closed via JS. */
.site-nav__contact-menu[hidden] {
  display: none;
}

.site-nav__contact-menu {
  position: absolute;
  right: 0;
  bottom: calc(100% + var(--space-sm));
  z-index: 90;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  width: max-content;
  min-width: 180px;
  padding: var(--space-2xs);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  transform-origin: bottom right;
  animation: contact-menu-in var(--transition-fast) ease backwards;
}

.site-nav__contact-item {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-2xs) var(--space-sm);
  border: none;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  text-align: left;
  cursor: pointer;
  transition: background var(--transition-fast);
}

.site-nav__contact-item:hover {
  background: var(--color-surface);
}

.site-nav__contact-item svg {
  flex: none;
  color: var(--color-text-muted);
}

/* The small outlink (LinkedIn/GitHub) or copy (Email) glyph at the end
   of a row — margin-left: auto pushes it to the row's far edge
   regardless of how short the label is. One rule shared by both
   places these rows appear: this popover's .site-nav__contact-item
   and the desktop rail's .site-nav__social-link below. */
.site-nav__social-trailing {
  margin-left: auto;
  flex: none;
  display: inline-flex;
  color: var(--color-text-muted);
}

@keyframes contact-menu-in {
  from {
    opacity: 0;
    transform: scale(0.94) translateY(4px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

@media (min-width: 700px) {
  .site-nav__contact {
    display: none;
  }
}

@media (min-width: 700px) {
  .site-nav {
    top: 0;
    right: auto;
    bottom: 0;
    width: var(--nav-width);
    height: 100vh;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-md);
    padding: var(--space-lg) var(--space-md);
    transition: width var(--transition-nav);
  }

  /* The brand plate: a fixed slab carrying the mark, the name and the
     collapse toggle, sitting above both the rail and the content card.

     It is the same size and position in both nav states — only its
     clip-path changes (see below). That is deliberate: the name and
     toggle never move when the rail expands or collapses, so the rail
     can slide underneath them without anything reflowing.

     Its silhouette is a single clip-path rather than a border-radius
     plus extra elements patching each corner, so the outline is one
     continuous thing by construction instead of separate curves that
     have to be kept in sync (and collide when they aren't).

     Numbers below are in the element's own px coordinates. The key
     ones: 12 = --space-xs, #main's top margin; 256 = the plate's
     visual right edge (--nav-plate-edge); 272 = 256 + the 16px corner
     radius, the extra sliver the top-right arc needs to curve into;
     96 = --nav-plate-height. */
  .site-nav__brand {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    gap: var(--space-2xs);
    width: var(--nav-plate-width);
    height: var(--nav-plate-height);
    padding-inline: var(--space-md);
    /* Centers the row within the top 72px — the part of the plate above
       the bottom sweep — rather than the full clipped height. */
    padding-bottom: 24px;
    white-space: nowrap;
    background: var(--color-bg);
    font-weight: var(--fw-semibold);
    font-size: var(--fs-md);
    color: var(--color-text);
    transition: clip-path var(--transition-nav);
    /* Expanded: the rail is 220 wide and the plate 256, so the drop to
       the rail is only 36px — just enough for a 20px convex arc and a
       16px concave one to meet nose-to-tail. The `H 236` between them
       is zero-length (the previous arc already ends there); see the
       note on the collapsed path below for why it's spelled out. */
    clip-path: path(
      "M 0 0 H 272 V 12 A 16 16 0 0 0 256 28 V 52 A 20 20 0 0 1 236 72 H 236 A 16 16 0 0 0 220 88 V 96 H 0 Z"
    );
  }

  /* Collapsed: same plate, but now the rail is only 76 wide, so the
     same journey has 180px to cover. The arcs grow (32 convex, 24
     concave) and the run between them opens up into a real length at
     y=72 — the long shelf the name appears to rest on. Both paths land
     their final arc exactly on the rail's own width, so the curve's
     foot meets the rail edge dead-on in either state.

     Both paths are written with an identical run of commands —
     M H V A V A H A V H Z — because clip-path only animates between
     two path() values when their structure matches exactly;
     mismatched commands make the browser jump straight to the end
     value instead of interpolating. Each path therefore carries one
     segment it doesn't strictly need (the `H 236` above, the `V 96`
     here), collapsed to zero length so it costs nothing visually but
     keeps the two in step. Editing either path means adding or
     removing the matching segment in the other. */
  .nav-collapsed .site-nav__brand {
    clip-path: path(
      "M 0 0 H 272 V 12 A 16 16 0 0 0 256 28 V 40 A 32 32 0 0 1 224 72 H 100 A 24 24 0 0 0 76 96 V 96 H 0 Z"
    );
  }

  /* The 12px of page background above the content card. Fixed rather
     than relying on #main's own margin, so the card scrolls underneath
     it and the plate's top-right corner always has that strip to curve
     against — without it the shape would run straight into the cream
     the moment the page scrolled. Lives on the nav because the nav's
     stacking context already sits above #main and below the plate. */
  .site-nav::after {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--space-xs);
    background: var(--color-bg);
  }

  .site-nav__brand-icon {
    display: inline-flex;
    color: var(--color-accent);
    flex: none;
  }

  .site-nav__brand-name {
    display: inline;
    white-space: nowrap;
  }

  /* Email/LinkedIn/GitHub: hidden on mobile (see .site-nav__contact's
     trigger+menu instead), shown here as a third icon+label list —
     same visual language as .site-nav__link above it, but its own
     class since these aren't page links (no aria-current highlight,
     and the icon stays visible when collapsed instead of disappearing
     the way .site-nav__link-icon does).

     margin-top: auto is what pins this list — and .site-nav__bottom
     right after it — to the bottom of the rail. Moving that margin
     here instead of leaving it on .site-nav__bottom means this list
     lands above the divider line and the theme toggle stays below it. */
  .site-nav__social {
    display: flex;
    flex-direction: column;
    gap: var(--space-3xs);
    margin-top: auto;
  }

  /* width: 100% (rather than leaving it to shrink-to-fit) is what
     gives the trailing outlink/copy icon somewhere to be pushed to —
     margin-left: auto on that icon has no effect without spare row
     width to push into. */
  .site-nav__social-link {
    position: relative;
    display: flex;
    align-items: center;
    width: 100%;
    gap: var(--space-2xs);
    padding: var(--space-2xs) var(--space-sm);
    border: none;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text-muted);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    text-align: left;
    cursor: pointer;
    transition: background var(--transition-fast), color var(--transition-fast);
  }

  .site-nav__social-link:hover {
    background: var(--color-surface);
    color: var(--color-text);
  }

  .site-nav__social-icon {
    flex: none;
    display: inline-flex;
  }

  .site-nav__social-label {
    overflow: hidden;
    min-width: 0;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  /* Rail rows sit in a vertical stack, so their tooltip (see the
     generic [data-tooltip] rule near .site-footer below) opens to the
     side instead of the default above-the-trigger placement — opening
     upward here would just cover the row above it. */
  .site-nav__social-link[data-tooltip]::after {
    top: 50%;
    left: calc(100% + var(--space-xs));
    bottom: auto;
    transform: translateY(-50%) translateX(-4px);
  }

  .site-nav__social-link[data-tooltip]:hover::after,
  .site-nav__social-link[data-tooltip]:focus-visible::after {
    transform: translateY(-50%) translateX(0);
  }

  /* Bottom section of the side rail: just the theme toggle, sitting
     below the divider that separates it from .site-nav__social above. */
  .site-nav__bottom {
    display: flex;
    flex: none;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2xs);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border);
  }

  /* Collapse toggle: rides inside the brand plate, just right of the
     name, in both nav states. Fixed and positioned off the same
     coordinates as the plate's clip-path (rather than sitting in the
     rail's flow) so it stays put while the rail resizes underneath it,
     and lands inside the bump the path leaves for it: 204 = the
     plate's 256 edge less its 24px padding and the button's own 28px;
     22 centers it in the plate's 72px-tall visible band. */
  .site-nav__toggle {
    z-index: 70;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    top: 22px;
    left: 204px;
    width: 28px;
    height: 28px;
    padding: 0;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    color: var(--color-text-muted);
    cursor: pointer;
    box-shadow: var(--shadow-sm);
    transition: color var(--transition-fast), border-color var(--transition-fast);
  }

  .site-nav__toggle:hover {
    color: var(--color-accent);
    border-color: var(--color-accent);
  }

  .site-nav__toggle-icon {
    transition: transform var(--transition-base);
  }

  /* The brand is fixed (out of flow) in both states now, so the column
     no longer reserves room for it — push the links down past the
     plate instead. --space-lg is the rail's own top padding. */
  .site-nav__list {
    flex: none;
    flex-direction: column;
    gap: var(--space-2xs);
    margin-top: calc(var(--nav-plate-height) - var(--space-lg));
  }

  .site-nav__list li {
    flex: none;
  }

  /* The plate covers this corner in both nav states and draws the
     curve there itself; #main's own radius underneath would read as a
     second, competing curve. */
  #main {
    border-top-left-radius: 0;
  }

  .site-nav__link {
    flex-direction: row;
    justify-content: flex-start;
    margin: 0;
    padding: var(--space-2xs) var(--space-sm);
    border-radius: var(--radius-md);
    font-size: var(--fs-base);
  }

  /* Same treatment as .site-nav__social-link below it, so hovering
     either group of items in the rail reads as one consistent
     surface — not defined on the shared (mobile-included) base rule
     above, since a touch device has no real hover to match. */
  .site-nav__link:hover {
    background: var(--color-surface);
    color: var(--color-text);
  }

  .site-nav__link-icon {
    display: none;
  }

  .site-nav__link[aria-current="page"] {
    background: var(--color-accent-soft);
  }

  .site-nav__link[aria-current="page"]:hover {
    background: var(--color-accent-soft);
  }

  .site-nav__link-label {
    display: inline-block;
    white-space: nowrap;
    transition: opacity 140ms ease;
  }

  .site-nav__link-label-short {
    display: none;
  }

  .site-nav__link-label-full {
    display: inline;
  }

  /* The label's rotation and its layout box (width, display) change
     in the very same instant the rail collapses/expands — animating
     that resize directly just looks like the text snapping mid-turn.
     js/main.js briefly adds this class to fade the labels out, swaps
     the collapsed state while they're invisible, then fades them back
     in, so the turn happens off-screen and reads as a soft crossfade
     instead of a jarring flip. */
  .site-nav__list.is-toggling-nav .site-nav__link-label {
    opacity: 0;
  }

  /* ---- Collapsed side rail ---- */
  .nav-collapsed .site-nav {
    width: var(--nav-width-collapsed);
    align-items: center;
  }

  .nav-collapsed .site-nav__toggle-icon {
    transform: rotate(180deg);
  }

  /* padding-inline is zeroed here: the desktop padding above is sized
     for the expanded icon+text row and is far too wide for this
     narrow collapsed column, which was forcing the link box (and its
     background) wider than the rail and out of step with the
     centered label. Dropping it lets the link's box match the rail's
     content width exactly. */
  .nav-collapsed .site-nav__link {
    position: relative;
    justify-content: center;
    padding-block: var(--space-md);
    padding-inline: 0;
  }

  /* Turn the labels on their side for the collapsed rail.
     writing-mode does this rather than transform: rotate(-90deg),
     because a transform is painted after layout — the box keeps its
     original horizontal dimensions, so the browser has no idea the
     text now runs vertically. That mismatch caused a chain of
     problems: boxes sized by (horizontal) text length made longer
     labels wider than shorter ones, and once the width was pinned to
     work around that, long labels overflowed their too-short box and
     nearly collided with the next item.
     writing-mode is a real layout mode, so the box genuinely becomes
     tall-and-narrow: its width is one line-height (identical for
     every item, whatever the wording) and its height tracks the
     actual text length. Padding, the list's gap, and the active
     highlight then all size themselves correctly with no hand-tuned
     values to drift out of sync when labels are renamed.
     vertical-rl alone would read top-to-bottom; the 180° turn flips
     it to bottom-to-top, matching the original rotate(-90deg). */
  .nav-collapsed .site-nav__link-label {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
  }

  /* Active highlight for the collapsed rail is a separate layer
     rather than the link's own background, so it can spill past the
     rail's right edge onto the page content (the effect this had
     before the link box above was tightened to match the rail width)
     without dragging the centered label along with it. */
  .nav-collapsed .site-nav__link[aria-current="page"] {
    background: none;
  }

  .nav-collapsed .site-nav__link[aria-current="page"]::before {
    content: "";
    position: absolute;
    inset: 0;
    right: calc(-1 * var(--space-md) - 10px);
    background: var(--color-accent-soft);
    border-radius: var(--radius-md);
  }

  /* Collapsed rail: icon only, centered, no rotated label — unlike
     .site-nav__link above, these don't get the writing-mode treatment.
     A universally recognized mark (envelope, "in", octocat) reads fine
     on its own; sideways text next to it would only add noise. */
  .nav-collapsed .site-nav__social {
    align-items: center;
  }

  .nav-collapsed .site-nav__social-link {
    justify-content: center;
    padding: var(--space-2xs);
  }

  .nav-collapsed .site-nav__social-label,
  .nav-collapsed .site-nav__social-trailing {
    display: none;
  }

  /* Collapsed theme toggle: only the active mode's icon shows, centered
     under the collapsed rail. Clicking it flips to the other mode
     (handled in js/main.js). */
  .nav-collapsed .site-nav__bottom {
    align-items: center;
  }

  .nav-collapsed .theme-toggle {
    padding: 0;
    background: transparent;
    border-color: transparent;
  }

  .nav-collapsed .theme-toggle__option[aria-pressed="false"] {
    display: none;
  }

  /* Morphs from a segmented pill into a single circular icon button as
     the rail collapses; width/height/padding pick up the transition
     already declared on .theme-toggle__option in components.css. */
  .nav-collapsed .theme-toggle__option[aria-pressed="true"] {
    width: 32px;
    height: 32px;
    padding: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    box-shadow: none;
    animation: theme-toggle-pop-in var(--transition-base);
  }

  .nav-collapsed .theme-toggle__label {
    display: none;
  }
}

/* Plays whenever the collapsed toggle's single visible icon appears —
   either from expanding/collapsing the rail or switching modes, since
   the element goes from display: none to visible each time. */
@keyframes theme-toggle-pop-in {
  from {
    opacity: 0;
    transform: scale(0.5);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* Offset page content so it clears the fixed nav. The floating bar
   needs its detach gap counted twice — once above the pill, once below
   it — plus the home-indicator inset it sits on top of. */
body {
  padding-bottom: calc(
    var(--tabbar-height) + 2 * var(--space-sm) + env(safe-area-inset-bottom, 0px)
  );
}

@media (min-width: 700px) {
  body {
    padding-bottom: 0;
    padding-left: var(--nav-width);
    transition: padding-left var(--transition-nav);
  }

  .nav-collapsed body {
    padding-left: var(--nav-width-collapsed);
  }
}

/* Main content frame. #main carries its own surface color and sits
   inset from the page background — the inset reads as a border, and
   since the nav rail shares the page background color, the nav
   visually reads as part of that border rather than a separate panel. */
#main {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  margin: var(--space-xs);
}

@media (min-width: 700px) {
  #main {
    margin-left: 0;
  }
}

/* Hero
   Two tiers rather than a single centered stack: an oversized statement
   that gets the full measure to itself, then a supporting row beneath it
   carrying the intro and the call to action. The headline is the only
   thing at --fs-display anywhere on the site, so it reads as the entry
   point without needing a graphic to prop it up. */
/* Mobile-first, same reasoning as .section above: less padding by
   default, stepping back up to the full statement-piece spacing once
   the viewport can afford it. */
.hero {
  padding-block: var(--space-sm) var(--space-md);
}

@media (min-width: 700px) {
  .hero {
    padding-block: var(--space-3xl) var(--space-2xl);
  }
}

.hero__eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin-bottom: var(--space-md);
  color: var(--color-accent);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--fs-sm);
}

/* Short rule leading into the eyebrow — anchors the text block to the
   left edge and gives the top of the page a horizon line. */
.hero__eyebrow::before {
  content: "";
  width: var(--space-lg);
  height: 2px;
  background: currentColor;
  flex: none;
}

.hero__title {
  max-width: 18ch;
  margin-bottom: var(--space-xl);
  font-size: var(--fs-display);
  font-weight: var(--fw-semibold);
  /* Display sizes need a tighter leading than the --lh-tight used for
     ordinary headings, or the lines drift apart. */
  line-height: 0.98;
  letter-spacing: -0.03em;
}

/* The one word carrying the argument. Fraunces' italic is a genuinely
   different cut rather than a slant, so this reads as emphasis in the
   typeface's own voice instead of an effect applied on top of it. */
.hero__title em {
  font-style: italic;
  color: var(--color-accent);
}

.hero__meta {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--color-border);
}

.hero__lead {
  max-width: 52ch;
  margin: 0;
  font-size: var(--fs-md);
  line-height: var(--lh-loose);
  color: var(--color-text-muted);
}

.hero__aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  flex: none;
}

.hero__note {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

@media (min-width: 900px) {
  .hero__meta {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-2xl);
  }

  .hero__aside {
    align-items: flex-end;
    text-align: right;
  }
}

/* Entrance. Content resolves where it already sits — fading up from a
   slight blur, as if pulling into focus — rather than sliding in from
   an offset. Nothing changes position, so moving between pages reads
   as the page sharpening rather than the layout rearranging itself.

   Scoped to the top-level blocks of the first section in <main>, which
   is the part actually on screen at load, and staggered just enough to
   feel sequenced. The whole sequence is under half a second. Suppressed
   wholesale by the prefers-reduced-motion block in base.css. */
@keyframes settle-in {
  from {
    opacity: 0;
    filter: blur(4px);
  }
}

main > section:first-of-type > * {
  animation: settle-in var(--transition-enter) backwards;
}

main > section:first-of-type > :nth-child(2) {
  animation-delay: 60ms;
}

/* Past the second block the stagger stops: on most pages that's the
   body content, usually below the fold, and holding it back any longer
   only makes the page feel slower to arrive. */
main > section:first-of-type > :nth-child(n + 3) {
  animation-delay: 120ms;
}

/* Generic tooltip: a small bubble carrying the fuller copy for an
   item whose visible label is shorter or truncated (nav rail rows,
   the footer email link below). Opens above the trigger by default;
   .site-nav__social-link overrides the direction to the side since
   its rows are stacked vertically. (hover: hover) keeps it from
   popping up on tap with no pointer around to move away and dismiss
   it. */
[data-tooltip] {
  position: relative;
}

@media (hover: hover) {
  [data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + var(--space-2xs));
    left: 50%;
    z-index: 80;
    transform: translateX(-50%) translateY(2px);
    padding: var(--space-3xs) var(--space-2xs);
    border-radius: var(--radius-sm);
    background: var(--color-text);
    color: var(--color-bg);
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    white-space: nowrap;
    box-shadow: var(--shadow-md);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--transition-fast), transform var(--transition-fast);
  }

  [data-tooltip]:hover::after,
  [data-tooltip]:focus-visible::after {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
}

/* Site footer */
.site-footer {
  border-top: 1px solid var(--color-border);
  padding-block: 33px;
  color: var(--color-text-muted);
}

.site-footer__inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

@media (min-width: 700px) {
  .site-footer__inner {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
  }
}

.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--space-md);
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-footer__links li {
  min-width: 0;
  max-width: 100%;
}

.site-footer__link {
  display: inline-flex;
  align-items: center;
  min-width: 0;
  max-width: 100%;
  gap: var(--space-3xs);
  color: var(--color-text-muted);
  transition: color var(--transition-fast);
}

.site-footer__link:hover {
  color: var(--color-text);
}

/* Only the email address is long enough to need this — LinkedIn/GitHub
   never approach the container width — but it's kept generic so any
   link that does run long truncates the same way instead of wrapping
   mid-word onto a second line. */
.site-footer__link-text {
  overflow: hidden;
  min-width: 0;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.site-footer__link svg {
  flex: none;
}

/* Case-study hero
   Intro copy on the left, project image on the right running off the
   edge of the content card. The bleed is the point: an image framed
   inside a column reads as one more figure in the page, where an image
   escaping it reads as the header of the whole piece.

   padding-inline reproduces .container's own content edge rather than
   using .container itself, since a centered max-width box can't also
   bleed. .container is centered inside #main, so its left edge sits
   half the leftover width (zero until the viewport passes
   --container-width) plus --container-padding from #main's edge —
   which is what the max() below computes. That keeps the intro column
   in line with every .container section further down the page. */
.case-hero {
  display: grid;
  gap: var(--space-lg);
  padding-block: var(--space-xl);
  padding-inline: calc(
    max(0px, (100% - var(--container-width)) / 2) + var(--container-padding)
  );
}

/* Mobile-first, same reasoning as .section/.hero above: the base
   --space-xl padding-block set on .case-hero is sized for the
   tablet/desktop tiers below (700px+, which still need it — one of
   them only overrides padding-top for nav-plate clearance), so it has
   to be pulled back down specifically below 700px rather than changed
   at the source. */
@media (max-width: 699px) {
  .case-hero {
    padding-inline: var(--space-sm);
  }
}

/* Matches the tablet-range .container exception at the top of this
   file — same reasoning: the side rail already eats the width here. */
@media (min-width: 700px) and (max-width: 899px) {
  .case-hero {
    padding-inline: var(--space-lg);
  }
}

/* Side rail is present from 700px up to the 1200px breakpoint below,
   where the fixed brand plate (96px tall, see .site-nav__brand in this
   file) sits above #main's own 12px top margin. The hero's default
   --space-xl top padding isn't enough to clear it at this range, so
   the back-link ends up underneath the plate; 1200px+ already gets
   more breathing room from its own padding-block override below. */
@media (min-width: 700px) and (max-width: 1199px) {
  .case-hero {
    padding-top: calc(var(--nav-plate-height) - var(--space-xs) + var(--space-sm));
  }
}

/* Source order is intro → media → facts, which is the reading order on
   a phone: the image arrives right after the summary instead of behind
   four fact cards. On desktop the areas stack intro and facts into the
   left column and give the media the right one to itself, spanning
   both rows.

   This splits at --bp-lg rather than the --bp-md most of the site uses,
   because half of a --bp-md page is not enough width for what goes in
   the left column: the fact cards end up two narrow squares instead of
   the wide 2x2 they're meant to be, and the summary loses its measure.
   Below this the hero simply stacks, which that width handles well —
   full-bleed image, facts two-up across the whole page. */
@media (min-width: 1200px) {
  .case-hero {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      "intro media"
      "facts media";
    /* Rows size to the intro and facts; the media stretches across
       whatever total they come to, so the two columns read as one band
       rather than a small picture floating beside a column of text. */
    align-items: stretch;
    gap: var(--space-lg) var(--space-2xl);
    padding-block: var(--space-3xl);
    /* The right column runs to the card edge. */
    padding-right: 0;
  }

  .case-hero__intro {
    grid-area: intro;
  }

  .case-hero__media {
    grid-area: media;
  }

  .case-hero .fact-grid {
    grid-area: facts;
  }
}

.case-hero__title {
  margin-bottom: var(--space-2xs);
}

/* The one-line "what this project actually was" under the title —
   set in the body face, not Fraunces, so it reads as a caption on the
   heading rather than a second heading competing with it. */
.case-hero__tagline {
  margin: 0 0 var(--space-3xs);
  font-family: var(--font-body);
  font-weight: var(--fw-semibold);
}

.case-hero__client {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
}

/* The rule above the summary separates the title block from the body
   copy — the same job .hero__meta's border does on the home page. */
.case-hero__summary {
  margin: var(--space-md) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border);
  color: var(--color-text-muted);
  line-height: var(--lh-loose);
}

.case-hero__media {
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--color-bg);
}

.case-hero__media img {
  width: 100%;
  height: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

/* Media slot holding an interactive piece rather than a photo (see
   .court-demo in components.css). The demo draws its own plinth in
   --color-bg, so the panel behind it steps back to the card colour to
   give that slab something to sit against; and it needs room to
   breathe, where a photo wants to bleed to the edges. */
.case-hero__media--demo {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  background: var(--color-surface);
  /* The illustration's drop-shadow spreads outside the SVG's own box,
     and on desktop the court itself overhangs this slot entirely — the
     base rule's clip would cut off both. */
  overflow: visible;
}

/* Phone widths can't spare 24px a side — that's most of the difference
   between a court you can aim at and one you can't. */
@media (max-width: 699px) {
  .case-hero__media--demo {
    padding: var(--space-xs);
  }
}

@media (min-width: 1200px) {
  /* Only the left corners curve — the right edge is flush with the
     card it's bleeding off. */
  .case-hero__media {
    border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  }

  /* No padding: the court is meant to run right out to the card's edge
     here, and it gains the room it breaks leftward with in
     components.css.

     The court passes behind the fact cards, which are opaque, so it's
     pinned to the bottom of the slot rather than centred in it — that
     way it keeps hanging off the same edge as the left column grows,
     instead of drifting as the intro copy gets longer. */
  .case-hero__media--demo {
    padding: 0;
    align-items: flex-end;
    position: relative;
    z-index: 0;
  }

  /* Both sides of the overlap are positioned with an explicit z-index,
     rather than leaning on source order: the cards come after the court
     in the markup, but the court is positioned, and a positioned
     element paints over a static one no matter what order they're in.
     Saying it outright is what keeps the cards on top.

     Because they're opaque and on top, the cards don't need to reserve
     any gutter for the court — their text just sits above it. */
  .case-hero:has(.court-demo) .fact-grid {
    position: relative;
    z-index: 1;
  }

  /* aspect-ratio would fight the stretched grid row, so the image just
     fills the height the intro column sets — with a floor for the case
     where the intro is unusually short. */
  .case-hero__media img {
    aspect-ratio: auto;
    min-height: 24rem;
  }
}

/* Case-study body copy. The hero runs the full width of the card, so
   the prose under it needs its own measure or it inherits the
   container's 1120px and gets unreadably wide. */
.case-body {
}

/* Two-column layout: main content beside a narrower meta column
   (home page intro, about page). */
.detail-layout {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: 1fr;
}

@media (min-width: 900px) {
  .detail-layout {
    grid-template-columns: 2fr 1fr;
    align-items: start;
  }
}
