/*
  Left sidebar navigation.

  Styled after lotusdocs.dev's sidebar, measured live rather than eyeballed:
  36px rounded icon chips on a lighter fill, borderless 44px rows with their
  own 6px radius, 16px/600 root labels, 14px/400 nested rows prefixed with a
  dash and indented under the chip, and the current row carrying accent-
  coloured text plus a bar on its trailing edge. Their accent is blue; ours
  is the portfolio's gold, so only the hue differs.

  The one structural departure: lotusdocs' sidebar is the SAME colour as its
  page, because that page is flat dark. Ours sits against the wave artwork,
  so it keeps a navy surface of its own to separate from it.

  FROSTED, LIKE THE CARDS

  That surface used to be opaque, and it read as a different material from
  everything else on the page -- the cards are translucent and blurred, and
  the rail was a solid slab beside them. It now uses the cards' recipe: a
  translucent navy gradient with a 14px backdrop blur (desaturated, so the
  rail keeps its blue -- see the #sidebar rule), over a wave canvas
  that runs on underneath it (see css/waves.css). The waves show through as
  a soft wash; no curve is ever legible through it.

  ONE SURFACE, TOP TO BOTTOM

  The brand block and the footer used to carry a second, darker fill. The
  reference has no such thing -- its whole sidebar column is one flat value
  -- and two fills read as a seam across a column meant to be a single
  frame. So there is now exactly one surface token, and the brand and footer
  are transparent over it. The hairlines above and below the menu stay: they
  mark the scroll boundary, since .nav-content scrolls beneath both pinned
  blocks and needs somewhere to visibly end.

  WHY THE SIDEBAR CARRIES ITS OWN COLOUR TOKENS

  css/waves.css sets `body[data-wave-contrast="dark"] { --text-primary: ... }`.
  That override lives on <body>, and js/waves.js flips it every time the
  current wave field changes value -- so anything inheriting --text-primary
  changes colour as the visitor scrolls. That is right for section copy
  sitting on the artwork and wrong for the nav, which sits on its own dark
  surface at all times. The `--nav-*` tokens below are declared ON #sidebar,
  downstream of body, so the wave contrast flip cannot reach them.

  The sidebar stays dark in BOTH themes. That is the user's own mockup, not
  an omission: it is the constant frame the light and dark content sit
  inside, and it keeps one contrast matrix to verify instead of two.

  Contrast is no longer a fixed table, because the surface is translucent
  and what it composites to depends on the wave behind it. The fills live in
  css/variables.css (--nav-fill-*, --nav-row-fill) so that
  `node tools/check-contrast.js` can read them, and it checks every nav text
  colour against the rail and the current-row fill over every wave colour.
  Worst cases, over the palest wave colours, at the rail's bottom end:
    gray-300 on the current-row fill ....  5.06:1
    gold-300 on the current-row fill ....  4.75:1
    gold-400 bar on the current fill ....  3.89:1  (UI, needs 3:1)
  (The desaturate is not modelled. saturate() keeps luminance, so it
  cannot move these numbers much either way.)
*/

#sidebar {
  /* Sidebar-local palette, pinned so it stops tracking the wave contrast
     attribute. The three translucent fills are white over the navy surface
     rather than fixed hexes, so they stay correct if the surface moves. */
  --nav-surface: linear-gradient(var(--nav-fill-top), var(--nav-fill-bottom));
  --nav-chip-fill: var(--nav-row-fill);
  --nav-row-hover: rgba(255, 255, 255, 0.05);
  /* Deliberately the SAME value as the chip fill: on the current row the
     chip stops reading as a separate tile and dissolves into the row, which
     is how the reference marks its selection. */
  --nav-row-current: var(--nav-chip-fill);
  --nav-text: var(--cream-100);
  --nav-text-muted: var(--gray-300);
  /* Two-tone, as the reference is: the lighter tint carries the text and
     the glyph, the deeper one is the trailing bar. Theirs is a light blue
     over a strong blue; ours is that same relationship in gold. */
  --nav-accent: var(--gold-300);
  --nav-accent-strong: var(--gold-400);
  --nav-divider: rgba(250, 247, 242, 0.1);
  --nav-brand-ring: var(--gold-400);
  --nav-brand-ring-inner: var(--gold-200);
  /* The rail's sizes are in rem, not px, so it grows with the rest of the
     layout on a large screen (the `html` rule in css/base.css). At the
     default 16px root every figure is the px value it replaced. Hairlines
     and the 3px current-row bar stay in px. */
  --nav-row-radius: 0.375rem;

  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  width: var(--nav-width-base);
  height: 100vh;
  background: var(--nav-surface);
  /* The cards' own 14px blur, so the two surfaces are visibly the same
     material -- plus a desaturate the cards do not have. At the rail's
     current 88-92% fill it is a minor term (the fill's density is what
     keeps the rail blue; see --nav-fill-* in css/variables.css), but it
     would start pulling its weight if the fill were made more transparent. */
  backdrop-filter: blur(14px) saturate(0.5);
  color: var(--nav-text);
  border-right: 1px solid var(--nav-divider);
}

/* The rail grows less than the page under browser zoom: js/zoom.js sets
   --nav-scale to 1/sqrt(zoom), so the rail ends up sqrt(zoom) bigger rather
   than zoom bigger. Rail mode only -- the drawer covers the page when open,
   so its size costs the content nothing. Pinned by its insets instead of
   100vh, which a zoomed element scales along with itself (see the note on
   the `html` rule in css/base.css). */
@media (min-width: 1024px) {
  #sidebar {
    zoom: var(--nav-scale);
    bottom: 0;
    height: auto;
  }
}

/* ---- Brand ---- */
.nav-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
  border-bottom: 1px solid var(--nav-divider);
}

.nav-brand__link {
  display: block;
  text-decoration: none;
}

.nav-brand__mark {
  width: 7rem;
  height: 7rem;
}

/* ---- Menu ----
   .nav-content is the only scrolling region: brand and footer stay pinned
   while a long tree scrolls between them, as in the reference. */
.nav-content {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0;

  /* The menu's own scrollbar, overriding the page's. css/scrollbar.css
     paints every thumb solid gold, and gold is precisely what marks the
     current row here -- once the list is long enough to scroll, the two run
     side by side down the sidebar's edge and merge into one smear, so the
     selection stops being findable. A neutral thumb leaves the only gold
     vertical in this column the one that carries meaning. Declared twice
     because Chrome honours the standard properties and Safari the -webkit-
     pseudo-elements. */
  scrollbar-width: thin;
  scrollbar-color: rgba(250, 247, 242, 0.22) transparent;
}

.nav-content::-webkit-scrollbar {
  width: 8px;
}

.nav-content::-webkit-scrollbar-thumb {
  background-color: rgba(250, 247, 242, 0.22);
}

.nav-menu,
.nav-sublist,
.nav-contacts {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Left inset only. Rows have to reach the sidebar's trailing edge for the
   current row's accent bar to sit ON that edge, which is where the
   reference puts it. */
.nav-menu {
  padding: var(--space-2) 0 var(--space-2) 0.625rem;
}

/* Root row. No dividers -- rows are separated by their own rhythm and the
   hover/current fill, which is what keeps the reference's tree from reading
   as a stack of table cells. */
.nav-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 2.75rem;
  margin: 0.125rem 0;
  padding: 0.3125rem 1rem 0.3125rem 0.625rem;
  color: var(--nav-text-muted);
  font-family: var(--font-body);
  font-size: 1rem;
  text-align: left;
  text-decoration: none;
  background: none;
  border: 0;
  border-radius: var(--nav-row-radius);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}

.nav-link:hover {
  background: var(--nav-row-hover);
  color: var(--nav-text);
}

/* Current destination. Four things at once, all of them measured off the
   reference's own selected row rather than invented:

     1. the row takes a fill, and that fill equals the chip fill, so the chip
        stops looking like a tile sitting on the row and merges into it;
     2. text and glyph go accent;
     3. the corners round on the leading edge only, so the row can run out
        flat to the sidebar's trailing edge;
     4. a 3px bar in the deeper accent sits on that trailing edge.

   Before this, a current root row got only accent text -- which is why
   "Who's Roman?" read as merely tinted next to a nested row that had the
   full fill-and-bar treatment. The two levels now differ in size and indent,
   as they should, and in nothing else.

   `.nav-link` is also the class on a group's toggle button, but js/nav.js
   only ever sets aria-current on a leaf, so a group whose child is current
   stays unmarked -- the child carries the highlight, as the reference does.
   Two rows lit at once would make the tree ambiguous about where you are. */
.nav-link[aria-current] {
  background: var(--nav-row-current);
  color: var(--nav-accent);
  border-radius: var(--nav-row-radius) 0 0 var(--nav-row-radius);
}

/* A pseudo-element rather than a border-right: turning a border on and off
   would resize the row's content box and nudge the label a pixel sideways
   every time the selection moves. */
.nav-link[aria-current]::after,
.nav-sublink[aria-current]::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 3px;
  background: var(--nav-accent-strong);
}

.nav-item__text {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  line-height: 1.2;
}

.nav-item__label {
  font-weight: 600;
}

.nav-item__sub {
  margin-top: 1px;
  color: var(--nav-text-muted);
  font-size: 0.75rem;
  font-weight: 400;
}

/* The icon's rounded chip. Its fill is constant; only the glyph takes the
   accent, which is how the reference marks the current root. */
.nav-chip {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 0.25rem;
  background: var(--nav-chip-fill);
}

.nav-icon {
  width: 1.25rem;
  height: 1.25rem;
}

.nav-icon--sm {
  width: 0.9375rem;
  height: 0.9375rem;
}

/* Points right when closed, down when open -- the reference's affordance.
   The sprite glyph is a down chevron, so closed is the rotated state. */
.nav-chevron {
  flex: none;
  width: 1rem;
  height: 1rem;
  transform: rotate(-90deg);
  transition: transform var(--duration-fast) var(--ease-standard);
}

.nav-group.is-open > .nav-group__toggle .nav-chevron {
  transform: rotate(0deg);
}

/* ---- Submenu ----
   Collapsed/expanded via grid-template-rows 0fr -> 1fr, which animates to
   the panel's natural height with no JS measuring. (max-height would need a
   guessed ceiling: too small clips the list, too large makes closing read as
   a delay.) The inner wrapper is what clips, so the row track has something
   to collapse.

   The panel keeps the `inert` attribute while closed -- set in js/nav.js --
   so collapsed rows stay out of the tab order and the accessibility tree.
   display:none would do that too, but cannot be transitioned. */
.nav-submenu {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--duration-fast) var(--ease-standard);
}

.nav-group.is-open > .nav-submenu {
  grid-template-rows: 1fr;
}

.nav-sublist {
  overflow: hidden;
  /* Indents the dashes under the chip, so nested rows read as hanging off
     their parent rather than starting a second column. */
  padding-left: 1rem;
}

.nav-sublink {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0.125rem 0;
  padding: 0.375rem 0.625rem;
  color: var(--nav-text-muted);
  font-size: 0.875rem;
  text-decoration: none;
  border-radius: var(--nav-row-radius);
  transition: background-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}

/* The reference's leading dash. A pseudo-element, so the label stays a
   single clean string for search and screen readers. */
.nav-sublink::before {
  content: "\2013";
  flex: none;
  opacity: 0.75;
}

.nav-sublink:hover {
  background: var(--nav-row-hover);
  color: var(--nav-text);
}

.nav-sublink[aria-current] {
  background: var(--nav-row-current);
  color: var(--nav-accent);
  font-weight: 600;
  border-radius: var(--nav-row-radius) 0 0 var(--nav-row-radius);
}

/* ---- Footer ----
   Built from the rest of the site's parts rather than its own. It used to
   set the name in the serif from the user's old business cards, the only
   serif on the site, and 13px contact links on 2px of padding -- a third
   typeface on every screen and tap targets about 17px tall. Now:

     eyebrow   the cards' uppercase gold label (.eyebrow in cards.css)
     name      the section titles' display face
     contacts  the menu's own rows: 40px tall, a chip, hover fill, gold glyph

   The left inset puts the chips 20px from the sidebar's edge -- 16px here
   plus .nav-contact's 4px -- the same as the menu's chips above them
   (.nav-menu's 10px plus .nav-link's 10px). It used to be 10px, which left
   the contact chips 6px short of the menu's. */
.nav-footer {
  flex: none;
  padding: var(--space-3) var(--space-3) var(--space-3) 1rem;
  border-top: 1px solid var(--nav-divider);
}

.nav-footer__eyebrow,
.nav-footer__name {
  margin: 0;
  padding-left: 0.25rem;
}

.nav-footer__eyebrow {
  margin-bottom: var(--space-1);
  color: var(--nav-accent);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  line-height: 1.2;
  text-transform: uppercase;
}

.nav-footer__name {
  margin-bottom: var(--space-2);
  color: var(--nav-text);
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.05;
}

.nav-contact-item {
  display: flex;
  align-items: center;
}

.nav-contact {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  min-height: 2.5rem;
  padding: 0.125rem 0.5rem 0.125rem 0.25rem;
  color: var(--nav-text-muted);
  font-size: 0.875rem;
  text-decoration: none;
  border-radius: var(--nav-row-radius);
  transition: background-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}

.nav-contact:hover {
  background: var(--nav-row-hover);
  color: var(--nav-text);
}

/* The menu's 36px chip, a step smaller: these are secondary rows and sit
   under a 44px menu without competing with it. */
.nav-chip--sm {
  width: 2rem;
  height: 2rem;
}

.nav-contact__icon {
  width: 1.125rem;
  height: 1.125rem;
  transition: color var(--duration-fast) var(--ease-standard);
}

.nav-contact:hover .nav-contact__icon {
  color: var(--nav-accent);
}

/* The longest label, the LinkedIn URL, is the one that would wrap in the
   drawer's narrower width; one line with an ellipsis keeps the rows even. */
.nav-contact__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Trailing glyphs are visible at rest, not revealed on hover: a phone has no
   hover, and an action nobody can see is an action nobody takes. They are
   dimmed until the row is pointed at so three of them do not shout. */
.nav-contact__trail {
  flex: none;
  width: 0.9375rem;
  height: 0.9375rem;
  opacity: 0.55;
}

.nav-contact:hover .nav-contact__trail {
  color: var(--nav-accent);
  opacity: 1;
}

.nav-contact__copy {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  color: var(--nav-text-muted);
  background: none;
  border: 0;
  border-radius: var(--nav-row-radius);
  opacity: 0.55;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard),
              opacity var(--duration-fast) var(--ease-standard);
}

.nav-contact__copy:hover,
.nav-contact__copy:focus-visible,
.nav-contact-item:hover .nav-contact__copy {
  opacity: 1;
}

.nav-contact__copy:hover {
  background: var(--nav-row-hover);
  color: var(--nav-accent);
}

.nav-contact__copy .nav-icon {
  width: 1rem;
  height: 1rem;
}

.nav-contact__done-icon,
.nav-contact__copy.is-copied .nav-contact__copy-icon {
  display: none;
}

.nav-contact__copy.is-copied {
  color: var(--nav-accent);
  opacity: 1;
}

.nav-contact__copy.is-copied .nav-contact__done-icon {
  display: block;
}

/* Screen-reader-only: announces a copy the tick shows visually. */
.nav-footer__status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Honour a reduced-motion preference: the disclosure still works, it just
   arrives instead of travelling. Matches how js/waves.js snaps the morph. */
@media (prefers-reduced-motion: reduce) {
  .nav-submenu,
  .nav-chevron,
  .nav-link,
  .nav-sublink {
    transition: none;
  }
}


/* ============================================================
   BELOW A LANDSCAPE TABLET: top bar + off-canvas drawer
   ============================================================

   A 280px fixed column costs a third of a 900px window and the whole of a
   phone. Above 1024px it is a rail the content is inset beside; below it the
   same element becomes a drawer that slides in over the page, and a slim bar
   pinned to the top carries the brand and the button that opens it.

   The sidebar's markup does not change between the two -- it is the same
   tree, the same brand block, the same footer. Only its position and how it
   is reached do. That is why the drawer is the sidebar itself rather than a
   second copy of the nav: two trees would be two things to keep in step, and
   the current-row highlight would have to be written to both.

   1024px is the width js/scroll-engine.js pages on and css/layout.css insets
   #main on. All three have to agree; see the note in scroll-engine.js for
   what an 80px disagreement actually looked like. */

/* The bar exists only in drawer mode. */
.topbar {
  display: none;
}

@media (max-width: 1023.98px) {
  .topbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    /* Top of the stack: scrim 110, drawer 115, bar 120. The bar has to stay
       whole and on top while the drawer is open, because the button that
       closes the drawer is the same button that opened it -- with the drawer
       over the bar, it slid across the brand and left the bar looking cut in
       half. The drawer runs the full height underneath and starts its rows
       below the bar (see .nav-content's padding). */
    z-index: 120;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    height: var(--topbar-height);
    padding: 0 var(--space-3);
    /* The sidebar's own surface and tokens, because it is the same piece of
       chrome in a different shape. Declared here rather than inherited: the
       bar is a sibling of #sidebar, not a child, so the --nav-* block above
       does not reach it.

       The two ring tokens painted the old SVG monogram. BRAND_MARK in
       js/nav.js is a picture now and ignores them; they stay so a return to
       a drawn mark does not repeat the bug where, in a scope without them,
       it came out as the word "UX" in black with both gold rings missing. */
    --nav-brand-ring: var(--gold-400);
    --nav-brand-ring-inner: var(--gold-200);
    --nav-row-radius: 6px;
    background: var(--navy-rail);
    color: var(--cream-100);
    border-bottom: 1px solid rgba(250, 247, 242, 0.1);
  }

  .topbar__brand {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    color: inherit;
    text-decoration: none;
  }

  /* The sidebar's 112px mark, cut down to fit a 60px bar. Same picture. */
  .topbar__brand .nav-brand__mark {
    width: 38px;
    height: 38px;
    flex: none;
  }

  /* The display face, as the footer's name is -- the same name in two faces
     on one screen (bar above, drawer below) would read as two brands. */
  .topbar__name {
    font-family: var(--font-display);
    font-size: 1.125rem;
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* 44px square: the drawer's only opener, and the one control on this
     screen that has to be hit reliably with a thumb. */
  .topbar__toggle {
    display: flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    color: var(--cream-100);
    background: rgba(255, 255, 255, 0.09);
    border: 0;
    border-radius: var(--nav-row-radius);
    cursor: pointer;
  }

  .topbar__toggle:hover {
    background: rgba(255, 255, 255, 0.16);
  }

  .topbar__icon {
    width: 22px;
    height: 22px;
  }

  /* Hamburger when closed, X when open. Both glyphs are in the sprite and
     both are in the button; the class on <html> picks one. Swapping which
     is shown beats rewriting the button's innerHTML, which would throw away
     focus in the middle of the interaction that set it. */
  .topbar__icon--close,
  html.nav-open .topbar__icon--open {
    display: none;
  }

  html.nav-open .topbar__icon--close {
    display: block;
  }

  /* ---- The drawer ----
     Off-canvas by transform rather than by `left`, so it composites and
     does not relayout the page on every frame of the slide.

     `visibility` rides along with the transform for one specific reason:
     the sidebar is 280px wide and translated fully off the left edge, but
     its box-shadow is not, and a shadow hanging over the content's left
     edge with nothing casting it reads as a rendering fault. Delaying
     visibility to the end of the transition on the way out hides it. */
  #sidebar {
    width: min(320px, 86vw);
    z-index: 115;
    visibility: hidden;
    transform: translateX(-100%);
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
    transition: transform var(--duration-panel) var(--ease-standard),
                visibility 0s linear var(--duration-panel);
  }

  html.nav-open #sidebar {
    visibility: visible;
    transform: translateX(0);
    transition: transform var(--duration-panel) var(--ease-standard),
                visibility 0s;
  }

  /* The page behind the drawer must not scroll under it -- on a phone a
     swipe meant for the menu otherwise moves the document instead. Set on
     <html> by js/nav.js and removed when the drawer closes; it is also
     cleared if the window widens past this breakpoint with the drawer open,
     which would otherwise leave the desktop layout unscrollable. */
  html.nav-open,
  html.nav-open body {
    overflow: hidden;
  }

  .nav-scrim {
    position: fixed;
    inset: 0;
    z-index: 110;
    background: rgba(6, 8, 20, 0.55);
    opacity: 0;
    transition: opacity var(--duration-panel) var(--ease-standard);
  }

  html.nav-open .nav-scrim {
    opacity: 1;
  }

  /* The brand block is in the bar now, so a second copy at the top of the
     drawer is the logo twice on one screen. The drawer still runs the full
     height of the screen -- it is one continuous surface behind the bar, not
     a panel hanging off it -- so the menu is padded down instead, far enough
     that its first row starts below the bar rather than behind it. */
  .nav-brand {
    display: none;
  }

  .nav-content {
    padding-top: calc(var(--topbar-height) + var(--space-2));
  }
}

@media (prefers-reduced-motion: reduce) {
  #sidebar,
  html.nav-open #sidebar,
  .nav-scrim {
    transition: none;
  }
}
