/**
 * Side navigation with a wipe reveal.
 *
 * Modelled on Osmo Supply's Side Navigation with Wipe Effect. The data-attribute
 * contract, the panel-stagger idea and the motion design are theirs; the
 * implementation is not.
 *
 * WHY IT IS NOT GSAP. The original drives everything from a GSAP timeline with
 * CustomEase. Our standards forbid it: §7 is vanilla JS with no frameworks and
 * no build step, and §10 bans render-blocking third-party scripts. Two CDN files
 * for one nav animation would also hand a third party every visitor's IP, which
 * M14 would then have to declare.
 *
 * The motion is identical in design: panels wipe in staggered, links rise and
 * unrotate, small items fade up last, the button label slides and the icon
 * turns. All of it is a transform and an opacity on a timeline, which is what
 * CSS transitions are for. Stagger is a per-item --i multiplied into
 * transition-delay.
 *
 * WHAT CHANGED FOR THIS PROJECT
 *  - The 01/02/03 eyebrows are gone. Numbered nav items are a catalogued
 *    machine-generated tell (docs/anti-slop.md) and they carry no information:
 *    a five-item list does not need ordinals.
 *  - "Socials" became the secondary pages. The site has no social accounts, and
 *    press, privacy and terms were previously reachable only from the page foot.
 *  - Dark-only, so the resting panel steps UP the surface ladder rather than
 *    going light.
 */

/* ------------------------------------------------------------------ button */

/**
 * THE BUTTON HAS ITS OWN FIXED HEADER, and it has to.
 *
 * It started inside the site header, which is sticky and therefore creates a
 * stacking context. A child cannot paint above its parent's stacking context,
 * so the panel — a higher layer — covered the button completely and there was
 * no visible way to close the menu. The upstream resource has the button in its
 * own fixed header for exactly this reason.
 *
 * So the wordmark stays in the site header and the toggle lives here, pinned
 * above everything. It is outside the panel rather than inside it because it
 * must still be visible and clickable while the panel is closed.
 */
.sidenav__header {
  /*
   * MOBILE ONLY. Above 48em the site header carries visible inline links and a
   * second way in is clutter — which is what shipped: the desktop header showed
   * the nav AND the "menu +" toggle at the same time.
   *
   * The display rule lives HERE, in the element's own block, rather than in a
   * separate `display: none` above it. It was written that way and the
   * declaration below simply overrode it — same specificity, later in the file.
   * One rule per property per element, or the cascade decides for you.
   */
  display: none;
  position: fixed;
  inset-block-start: 0;
  inset-inline-end: 0;
  z-index: var(--z-toast);
  align-items: center;
  min-block-size: calc(var(--control-unit) + var(--space-xs));
  padding-inline-end: var(--gutter);
  pointer-events: none;
}

.sidenav__header > * { pointer-events: auto; }

.sidenav__button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  min-block-size: var(--target-min);
  padding: 0 var(--space-2xs);
  background: none;
  border: none;
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
  border-radius: var(--radius-field);
}

/* The label pair slides as one column, so "menu" leaves as "close" arrives. */
.sidenav__button-text {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  block-size: 1.4em;
  overflow: hidden;
}

.sidenav__button-label {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  /* Controls are lowercase, always (§6.4). */
  text-transform: lowercase;
  line-height: 1.4;
  transition: translate var(--duration-base) var(--ease-in-out);
}

.sidenav__button-icon {
  display: grid;
  place-items: center;
  inline-size: 1em;
  block-size: 1em;
  transition: rotate var(--duration-slow) var(--ease-in-out);
}

.sidenav__button-icon svg { inline-size: 100%; }

/* Desktop: the toggle is bare text beside the wordmark, so the plate is not
 * drawn at all. It only exists for the mobile FAB. */
.sidenav__button-plate { display: none; }

/* Open state, driven entirely by the button's own ARIA state so the visual and
 * the announced state cannot disagree. */
.sidenav__button[aria-expanded="true"] .sidenav__button-label { translate: 0 -100%; }
.sidenav__button[aria-expanded="true"] .sidenav__button-icon { rotate: 315deg; }

/* ------------------------------------------------------------------- shell */

/**
 * THREE STATES, NOT TWO, and the third one is the fix for most of what was
 * wrong here.
 *
 *   closed   at rest, hidden, and TRANSITIONS ARE OFF ENTIRELY
 *   open     the wipe-in sequence
 *   closing  the reverse, held long enough to actually play
 *
 * The first version had only open and closed, and read the closing animation as
 * "the open rule stops matching". That is not a reverse animation, it is an
 * absence: every property snapped back to its rest value at once while the
 * panel was still on screen, which is exactly the abruptness that was reported.
 *
 * Turning transitions off in the closed state also kills a first-open jump. An
 * element going from visibility:hidden to visible has no reliably painted
 * previous value for the transition to start from, so the first item would
 * sometimes leap instead of easing. At rest it now simply IS in its start
 * position, with nothing to interpolate from.
 */
.sidenav__nav {
  position: fixed;
  inset: 0;
  z-index: var(--z-dialog);
  visibility: hidden;
  pointer-events: none;
}

.sidenav__nav[data-nav-state="open"],
.sidenav__nav[data-nav-state="opening"],
.sidenav__nav[data-nav-state="closing"] {
  visibility: visible;
}

.sidenav__nav[data-nav-state="open"] { pointer-events: auto; }

/**
 * TRANSITIONS ARE NEVER TOGGLED BY THE NAV STATE, and that was the bug.
 *
 * The previous version killed transitions in the closed state. Going
 * closed -> open then changed two things in one style recalculation: the
 * transforms AND whether transforms were allowed to transition. The browser is
 * free to resolve that either way, so the first link — the one with no stagger
 * delay to hide the race — sometimes snapped instead of easing. That is the
 * flicker.
 *
 * Transitions are now always on inside the panel. Load-time animation is
 * prevented by a one-off flag on <html> instead, which is set after the first
 * paint and never changes again, so it can never collide with a state change.
 */
html:not(.too-nav-ready) .sidenav__nav * {
  transition: none !important;
}

.sidenav__overlay {
  position: absolute;
  inset: 0;
  z-index: var(--z-base);
  cursor: pointer;
  background-color: rgb(10 10 10 / 0.72);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}

.sidenav__nav[data-nav-state="open"] .sidenav__overlay { opacity: 1; }

/**
 * The panel leaves as one object.
 *
 * On the way in the three layers wipe separately, which is the effect. On the
 * way out they do not: the whole menu slides off to the right carrying its
 * panels and its links with it. Reversing a staggered wipe looks like a
 * malfunction, and a single confident exit is both quicker to sit through and
 * easier to reason about.
 */
.sidenav__menu {
  position: relative;
  margin-inline-start: auto;
  inline-size: min(34rem, 100%);
  block-size: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-lg);
  padding-block: calc(var(--control-unit) + var(--space-md)) var(--space-md);
  overflow: auto;
  overscroll-behavior: contain;
  transition: translate var(--duration-slow) var(--ease-in-out);
}

.sidenav__nav[data-nav-state="closing"] .sidenav__menu { translate: 100% 0; }

/* ------------------------------------------------------------------ panels */

.sidenav__menu-bg {
  position: absolute;
  inset: 0;
  z-index: var(--z-base);
}

/**
 * Three layers wiping in behind each other. The first two are accents and are
 * only ever seen in motion; the third is where the menu comes to rest, and it
 * steps UP the dark ladder rather than going light — the system has one mode.
 */
.sidenav__menu-bg-panel {
  position: absolute;
  inset: 0;
  border-start-start-radius: var(--radius-card);
  border-end-start-radius: var(--radius-card);
  background-color: var(--color-surface-card);
  translate: 101% 0;
  transition: translate 0.42s var(--ease-in-out);
}

.sidenav__menu-bg-panel.is--first  { background-color: var(--brand-coral); }
.sidenav__menu-bg-panel.is--second { background-color: var(--brand-amber); }

/* Held in place through the close, so the menu can carry them out as one
 * object instead of them wiping back independently underneath it. */
.sidenav__nav[data-nav-state="open"] .sidenav__menu-bg-panel,
.sidenav__nav[data-nav-state="closing"] .sidenav__menu-bg-panel {
  translate: 0 0;
}

/* The stagger. Each panel starts 0.08s after the one before, which is what
 * makes three flat colours read as depth. Last one lands at 0.58s. */
.sidenav__nav[data-nav-state="open"] .sidenav__menu-bg-panel.is--first  { transition-delay: 0s; }
.sidenav__nav[data-nav-state="open"] .sidenav__menu-bg-panel.is--second { transition-delay: 0.08s; }
.sidenav__nav[data-nav-state="open"] .sidenav__menu-bg-panel:not(.is--first):not(.is--second) { transition-delay: 0.16s; }

.sidenav__nav[data-nav-state="closing"] .sidenav__menu-bg-panel { transition-delay: 0s; }

/* -------------------------------------------------------------------- list */

.sidenav__menu-inner {
  position: relative;
  z-index: var(--z-raised);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-lg);
  block-size: 100%;
}

/* Shown only inside the mobile panel — see the media query at the end. */
.sidenav__brand { display: none; }

.sidenav__menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  inline-size: 100%;
}

/* Each item clips its own link, so the link can start below the line and rise
 * into it. */
.sidenav__menu-list-item {
  position: relative;
  overflow: hidden;
}

/**
 * ONE `transform`, NOT SEPARATE `translate` AND `rotate`.
 *
 * The individual properties interpolate independently, so a link travelling up
 * while un-rotating traced a visible arc and read as a wobble. Composed into a
 * single transform they interpolate as one motion.
 *
 * The origin is the left edge, so the rotation reads as the line hinging up off
 * its own start rather than pivoting about its middle, and the angle is down
 * from 10deg to 6 — at the panel's width, 10deg swung the right end of a long
 * item well outside its clip and it showed.
 */
.sidenav__menu-link {
  display: block;
  padding: var(--space-2xs) var(--space-md);
  color: var(--color-text);
  text-decoration: none;
  transform: translateY(130%) rotate(6deg);
  transform-origin: 0% 50%;
  transition: transform var(--duration-slow) var(--ease-in-out),
              color var(--duration-fast) var(--ease-out);
}

/*
 * HELD UNTIL THE PANELS HAVE LANDED. The links used to start rising at 0.35s
 * while the last panel was still arriving at 0.82s, so for half a second they
 * were legible against the dimmed page with no panel behind them. That is the
 * "links partly visible as the panels expand" bug: not a z-index problem, a
 * timing one. The last panel now lands at 0.58s and the first link starts at
 * 0.56s, overlapping only the panel it will actually sit on.
 */
.sidenav__nav[data-nav-state="open"] .sidenav__menu-link {
  transform: translateY(0) rotate(0deg);
  transition-delay: calc(0.56s + var(--i, 0) * 0.05s);
}

/**
 * Promote the moving parts for the duration of the move, and only for that.
 *
 * A layer created on the first animated frame produces a hitch on that frame,
 * which on the first link reads as a stutter. Declaring it while the panel is
 * open or opening gets the promotion done before anything moves; leaving it off
 * at rest means seven idle layers are not sitting on the compositor.
 */
.sidenav__nav[data-nav-state="opening"] :is(.sidenav__menu-link, .sidenav__menu-bg-panel, [data-sidenav-fade]),
.sidenav__nav[data-nav-state="open"] :is(.sidenav__menu-link, .sidenav__menu-bg-panel, [data-sidenav-fade]) {
  will-change: transform, opacity;
}

.sidenav__menu-link-heading {
  margin: 0;
  max-inline-size: none;
  font-family: var(--font-display);
  font-weight: 900;
  font-stretch: 125%;
  /*
   * Capped at 3.4rem, not 4.2. The panel is 34rem wide and Archivo Expanded 900
   * runs about 0.86em per capital, so a nine-character item like "QUESTIONS"
   * needs roughly 420px — at the old cap it needed 520 and broke mid-word.
   */
  font-size: clamp(1.9rem, 5.2vw, 3.4rem);
  line-height: 0.85;
  letter-spacing: var(--tracking-display-face);
  text-transform: uppercase;
}

.sidenav__menu-link:hover { color: var(--brand-coral); }

.sidenav__menu-link[aria-current="page"] { color: var(--brand-coral); }

/* ---------------------------------------------------------------- details */

.sidenav__menu-details {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding-inline: var(--space-md);
}

.sidenav__menu-secondary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-sm);
}

/* The last things to arrive. Fading rather than sliding keeps them subordinate
 * to the links, which is the point of doing them separately at all. */
[data-sidenav-fade] {
  opacity: 0;
  transform: translateY(50%);
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-out);
}

.sidenav__nav[data-nav-state="open"] [data-sidenav-fade] {
  opacity: 1;
  transform: translateY(0);
  transition-delay: calc(0.78s + var(--i, 0) * 0.04s);
}

.sidenav__menu-details a {
  color: var(--color-text-muted);
  text-decoration: none;
  text-transform: lowercase;
  font-size: var(--text-sm);
  display: inline-flex;
  align-items: center;
  min-block-size: var(--target-min);
}

.sidenav__menu-details a:hover { color: var(--color-text); }

.sidenav__menu-details .sidenav__menu-label {
  margin: 0;
  font-size: var(--text-meta);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-chip);
  text-transform: uppercase;
  color: var(--color-text-subtle);
}

/* ------------------------------------------------------------ preferences */

/**
 * Reduced motion. This one genuinely does move on its own once triggered — a
 * full-height panel sliding across the viewport is exactly the kind of large
 * transform the preference is about. It becomes a cross-fade.
 */
@media (prefers-reduced-motion: reduce) {
  .sidenav__menu-bg-panel,
  .sidenav__menu-link,
  [data-sidenav-fade],
  .sidenav__button-label,
  .sidenav__button-icon {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
  .sidenav__menu,
  .sidenav__menu-link,
  [data-sidenav-fade] { transform: none !important; translate: none !important; }
}

@media (width < 48em) {
  /**
   * On a phone the toggle becomes a bottom-centred FAB.
   *
   * Top-right is the worst corner on a large phone — it is the one place a
   * thumb cannot reach without shifting grip, and this is the only navigation
   * the site has. Bottom-centre is reachable from either hand.
   *
   * It sits above the panel, not inside it, so the same control opens and
   * closes: a separate close button somewhere else would make the toggle's
   * label animation meaningless.
   */
  /**
   * THE FILL LIVES ON THE HEADER, NOT ON THE BUTTON.
   *
   * It was a ::before on the button and the label would not paint. Measured
   * rather than assumed: the label's pixel region came back 100% fill colour
   * with no white in it, while `elementFromPoint` at the same coordinates
   * returned the label as the topmost element. So it was hit-testable and
   * invisible — the `filter` promotes the pseudo to its own compositing layer,
   * and inside that promotion the parent's own text does not reliably order
   * above it. Neither z-index -1 nor 0 with an explicit 1 on the content fixed
   * it.
   *
   * Putting the plate on the wrapper sidesteps the question entirely: the
   * filtered thing and the text are now in different elements, and a child
   * element painting above its parent's background is not in doubt. The header
   * shrinks to the button so the plate is exactly button-shaped.
   *
   * THE FILL AND THE CONTOUR PULL IN OPPOSITE DIRECTIONS, and they have to.
   * This control is fixed, so it floats over whatever is behind it: the
   * near-black shell on most of a page, and a near-white accent zone at the top
   * of every one. No single fill clears 3:1 against both — white measures 18.4
   * on the shell but 1.5 on amber and 1.2 on cyan; near-black measures 12.2 on
   * amber and 15.2 on cyan but 1.0 on the shell. The first version used the
   * page accent and landed coral-on-coral, invisible. So the near-black fill
   * separates it from every light accent and the cream contour separates it
   * from the dark shell, and at least one of the two is always doing the work.
   */
  .sidenav__header {
    display: flex;
    inset-block: auto 0;
    inset-inline: 0;
    inline-size: max-content;
    margin-inline: auto;
    justify-content: center;
    min-block-size: 0;
    padding: 0;
    /* Clear of the home indicator on a notched phone, with a floor for
     * everything else. */
    margin-block-end: max(var(--space-sm), env(safe-area-inset-bottom, 0px));
    border-radius: var(--radius-full);
  }

  /*
   * z-index on BOTH, explicitly. An absolutely positioned element paints in the
   * positioned phase, above static inline content, so the plate would cover the
   * label without it. Ordering both ends removes the assumption.
   *
   * The fill and the stroke pull in opposite directions on purpose — see the
   * note above the header rule.
   */
  .sidenav__button-plate {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 0;
    inline-size: 100%;
    block-size: 100%;
    overflow: visible;
  }

  .sidenav__button-plate path {
    fill: var(--color-surface);
    stroke: var(--brand-cream);
    stroke-width: var(--control-contour);
    stroke-linejoin: round;
  }

  .sidenav__button {
    position: relative;
    min-block-size: var(--control-chip);
    padding-inline: var(--space-sm);
    border-radius: var(--radius-full);
    color: var(--color-text);
  }

  .sidenav__button-text,
  .sidenav__button-icon {
    position: relative;
    z-index: 1;
  }

  .sidenav__menu {
    inline-size: 100%;
    /*
     * The 84px of top padding was reserved for a toggle sitting top-right. On a
     * phone the toggle is a bottom FAB, so that space is dead — and it was
     * pushing the link list far enough down that the panel could overflow on a
     * short viewport, which is what let a focus-scroll hide the first item.
     */
    padding-block-start: var(--space-md);
  }
  /**
   * IT OPENS UPWARD (owner, 2026-09-09).
   *
   * The wipe was written for a panel entering from the right, which is the
   * right gesture for a desktop drawer and the wrong one here: the control that
   * opens it is a bottom-centred FAB, so the menu was arriving from a direction
   * the reader's hand was nowhere near. Coming up out of the button is the
   * motion the button implies.
   *
   * The three-layer stagger is unchanged, only its axis. The links already rise
   * into place from below, so they now travel with the panel instead of across
   * it.
   */
  .sidenav__menu-bg-panel {
    translate: 0 101%;
    border-start-start-radius: var(--radius-card);
    border-start-end-radius: var(--radius-card);
    border-end-start-radius: 0;
  }

  /* And it leaves the way it came, as one object. */
  .sidenav__nav[data-nav-state="closing"] .sidenav__menu { translate: 0 100%; }

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

  /**
   * ONE CENTRED BLOCK (owner, 2026-09-13).
   *
   * Main links then secondary, centred horizontally, and the two together
   * centred vertically as a single object. The previous version pushed the
   * list to the bottom third and put the secondary links above it; centring
   * the pair reads as a deliberate menu rather than as content that has slid
   * down the panel.
   *
   * Natural DOM order now, so the `order` overrides are gone — primary first
   * for a screen reader, for the tab sequence, and visually.
   */
  .sidenav__menu-inner {
    justify-content: center;
    align-items: center;
    gap: var(--space-md);
    text-align: center;
    /* SYMMETRICAL, so "centred" is actually centred. The FAB sits over the
     * panel's bottom edge and the block has to clear it, but padding on the
     * bottom alone shifts the content box up by half of it — the menu then
     * reads as sitting high rather than as centred. Equal padding keeps the
     * box centred in the panel and still holds a tall, scrolled menu off the
     * button. */
    padding-block: calc(var(--control-chip) + var(--space-md));
  }

  /**
   * THE BRAND IS PINNED, NOT STACKED.
   *
   * As a flex child it stretched to the panel's full width, so the focus ring
   * it received on open drew as a full-width rounded border — which read as a
   * bug, not as focus. Taking it out of flow fixes the stretch and frees the
   * inner block to centre on its own content. site.js no longer starts focus
   * here either.
   */
  .sidenav__brand {
    display: inline-flex;
    align-items: center;
    position: absolute;
    inset-block-start: var(--space-md);
    inset-inline-start: var(--gutter);
    z-index: var(--z-raised);
    border-radius: var(--radius-field);
  }

  .sidenav__menu-list { inline-size: auto; }
  .sidenav__menu-link { padding-inline: var(--space-md); }

  .sidenav__menu-details {
    align-items: center;
    padding-inline: var(--gutter);
  }

  .sidenav__menu-secondary { justify-content: center; }
}
