/* ==========================================================================
   jlot.me
   Hand-written, dependency-free. Dark by default, light on request.
   Section map:
     1.  Tokens
     1b. Light theme
     2.  Derived layout values
     3.  Reset and base
     4.  Focus and skip link
     5.  Rail (desktop) / top bar (mobile)
     6.  Rail nav and the sliding dot
     7.  Page shell
     8.  Sticky section-heading band
     9.  I. About
     10. II. Projects
     11. III. CV
     12. IV. Contact
     13. Mobile overlay
     14. Mobile layout
     15. Motion
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  color-scheme: dark;

  /* ---- Color. Ratios measured against --bg, WCAG 2.1.
         The light counterpart of every value below is in section 1b, which
         re-declares this list and nothing else. ---- */
  /* --bg:          #060D0D;  /*   n/a                            Sketch "BG"      */
  --bg: #0b0e11;
  --bg-chip:     #0A0F19;  /*  1.02:1  decorative   skip-link plate            */
  --text:        #FFFFFF;  /* 19.61:1  AA  PASS                                 */
  --text-muted:  #9BA0A0;  /*  7.40:1  AA  PASS     CV prose, project blurbs    */
  --text-dim:    #7E8383;  /*  5.10:1  AA  PASS     est. YYYY, dates, locations */
  --rule:        #1F2525;  /*  1.26:1  decorative   white @10% flattened        */
  --rule-strong: #444A4A;  /*  2.17:1  decorative   white @25% flattened        */

  /* The accent knob. One value drives links, the nav dot and focus rings.
     Swap this hex and re-check it stays >= 4.5:1 against --bg. */
  --accent:      #5FD4B4;  /* 10.80:1  AA  PASS                                 */
  --focus:       var(--accent);

  /* ---- Type. No webfonts (D9). Avenir Next ships on macOS/iOS and is what
         the mockup was drawn in; Segoe UI Variable Display is the Windows
         counterpart. Neither is a fallback-to-default. ---- */
  --font-display: "Avenir Next", "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
  --font-sans:    "Avenir Next", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
  --font-mono:    ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --size-h2:      1rem;                                 /* display, 1.4, chip */
  --size-body:    1.125rem;                             /* sans,    1.6  */
  --size-small:   0.875rem;                             /* sans,    1.5  */
  --size-mono:    0.8125rem;                            /* mono,    1.4, +0.02em */

  /* ---- Spacing, 4px base ---- */
  --space-1: .25rem;  --space-2: .5rem;   --space-3: .75rem;
  --space-4: 1rem;    --space-6: 1.5rem;  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;   --space-16: 4rem;   --space-24: 6rem;

  /* ---- Layout ---- */
  --rail-width:  204px;   /* Sketch: nav rule is 204px wide          */
  --gutter:      96px;    /* rail to content column                  */
  --content-max: 743px;   /* Sketch: the section rules are 743px     */
  --page-max:    1120px;  /* rail + gutter + content, then centered  */
  --header-h:    56px;    /* mobile bar; 44px target + 6px each side */
}


/* --------------------------------------------------------------------------
   1b. Light theme

   Dark is the drawn design; light is its mirror, not a second design. Every
   token below was picked to land on the SAME contrast ratio as the dark value
   it replaces, so nothing in sections 3-15 has to know which theme is on.
   The one value that cannot mirror is --accent: mint at 10.66:1 on near-black
   has no equal-ratio twin on paper that still reads as a colour rather than
   as black, so it steps down to a deep teal of the same hue at 5.41:1 - still
   comfortably AA, and still the only knob worth turning.

   Selected by attribute, never by media query - not even as a default. Dark
   is the design, so a visitor on a light desktop still lands on the page as
   it was drawn; light is opted into here, with the button, and remembered.
   That is also why this block exists exactly once: with no
   prefers-color-scheme branch to mirror, there is nothing to duplicate.
   -------------------------------------------------------------------------- */

:root[data-theme="light"] {
  color-scheme: light;

  /* ---- Ratios measured against --bg. Dark's ratio in the last column. ---- */
  --bg:          #F5F7F6;  /*   n/a                            was  n/a    */
  --bg-chip:     #FFFFFF;  /*  1.08:1  decorative  skip-link   was  1.02:1 */
  --text:        #0A0E0D;  /* 18.05:1  AAA PASS                was 19.35:1 */
  --text-muted:  #4C5453;  /*  7.23:1  AAA PASS                was  7.31:1 */
  --text-dim:    #646D6C;  /*  4.95:1  AA  PASS                was  5.03:1 */
  --rule:        #DDE3E1;  /*  1.21:1  decorative              was  1.24:1 */
  --rule-strong: #A3ACAA;  /*  2.16:1  decorative              was  2.14:1 */
  --accent:      #0D7357;  /*  5.41:1  AA  PASS   hue 164, as mint's       */
}

/* Three of the four marks are white glyphs on transparency and vanish on
   paper. They are re-inked into their own files by ./make-light-logos - a
   CSS filter strong enough to turn the white lettering black also turns
   LearnGit's purple olive and Jack Lot Digital's blue orange.

   Swapped with `content` rather than a second <img> per logo, so index.html
   keeps one tag per mark and the alt text has no duplicate to drift from -
   the h1's accessible name still comes from the signature's alt, which a
   second hidden copy would put at risk. Each variant is the same pixel size
   as its source, so the width/height attributes stay correct and nothing
   reflows. Verified in Chromium; `content` on a replaced element is old in
   WebKit and Blink but arrived late in Firefox, so a long-outdated Firefox
   on the light theme would see the white marks. Nothing else degrades, and
   the alternative - a second <img> per mark, or swapping src from JS - costs
   either duplicated alt text or a logo flash on every load.

   modern-coder.png is absent on purpose: white type on its own orange plate,
   it carries its background with it and reads on either theme. */
:root[data-theme="light"] img[src$="signature.png"] {
  content: url("logos/signature-light.png");
}
:root[data-theme="light"] img[src$="learngit.png"] {
  content: url("logos/learngit-light.png");
}
:root[data-theme="light"] img[src$="jacklotdigital.png"] {
  content: url("logos/jacklotdigital-light.png");
}


/* --------------------------------------------------------------------------
   2. Derived layout values
   Kept apart from the token block above so that block stays a plain list of
   design values, with everything computed from them living here.
   -------------------------------------------------------------------------- */

:root {
  --page-pad:    var(--space-8);   /* viewport inset for the whole assembly    */
  --band-bleed:  var(--space-4);   /* how far the sticky band paints past the
                                      content column so nothing peeks through */
  --sticky-top:  0px;              /* where the heading band parks            */
  --band-lead:   var(--space-12);  /* Sketch: chip top y=48                   */
  --band-tail:   5px;              /* Sketch: rule y=95, title bottom y=90    */
  --band-inner:  42px;             /* height of the band's type row. The rail
                                      bar uses it too, so both hairlines land
                                      on the same y: lead + inner + tail = 95 */
  --body-lead:   38px;             /* Sketch: first body line y=133           */
  --dot-size:    6px;
  --cv-inset:    25px;             /* CV text is 25px inboard of its rule     */
  --ring:        2px;
}


/* --------------------------------------------------------------------------
   3. Reset and base
   -------------------------------------------------------------------------- */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-padding-top: var(--sticky-top);
}

body {
  min-height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 400 var(--size-body) / 1.6 var(--font-sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Set by site.js while the mobile overlay is open. showModal() alone does
   not stop iOS Safari from scrolling the page behind the dialog. */
body.is-locked { overflow: hidden; }

img {
  display: block;
  max-width: 100%;
  height: auto;
}

ul, ol { list-style: none; padding: 0; }

a { color: var(--accent); }

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

/* Mono carries every numeral, date range and est. YYYY. */
.cv-meta, .project__est { letter-spacing: .02em; }


/* --------------------------------------------------------------------------
   4. Focus and skip link
   -------------------------------------------------------------------------- */

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

/* main takes focus from the skip link but should not draw a ring for it. */
.main:focus { outline: none; }

.skip-link {
  position: fixed;
  z-index: 100;
  top: var(--space-4);
  left: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-chip);
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  color: var(--text);
  font-family: var(--font-display);
  font-size: var(--size-small);
  font-weight: 500;
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-8)));
}

.skip-link:focus { transform: none; }


/* --------------------------------------------------------------------------
   5. Rail
   Desktop: fixed column pinned to the left edge of the centered page box.
   Mobile:  sticky --header-h bar. See section 14.
   -------------------------------------------------------------------------- */

.rail {
  position: fixed;
  z-index: 30;
  top: var(--band-lead);
  left: max(var(--page-pad), calc(50% - var(--page-max) / 2 + var(--page-pad)));
  width: var(--rail-width);
}

.rail__bar {
  /* Same construction as the section band: --band-inner of content, then
     --band-tail, then the rule. Both start at --band-lead, so both hairlines
     land on the same y and read as one continuous line across the page -
     which is how the Sketch file draws them. */
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* border-box, so min-height has to carry the tail as well or the rule sits
     --band-tail too high. */
  min-height: calc(var(--band-inner) + var(--band-tail));
  padding-bottom: var(--band-tail);
  border-bottom: 1px solid var(--rule-strong);
}

/* Right end of the bar. The bar is space-between with two children, so the
   toggles have to arrive as one cluster or the theme button lands in the
   middle of the bar instead of beside the hamburger. */
.rail__actions {
  display: flex;
  align-items: center;
  margin-right: -10px;   /* optical: the glyph, not the target, sits flush.
                            Lives on the cluster rather than on either button,
                            so whichever one is rightmost gets it - the theme
                            button on desktop, the hamburger on mobile. */
}

.nav-toggle { display: none; }

/* Hidden until site.js has resolved a theme onto <html>. With JS off there is
   nothing to toggle, so the control is simply not offered - the same bargain
   the hamburger makes in the <noscript> block. Only `display` is gated, so
   the states below stay at plain single-class specificity and :hover still
   beats the resting colour. */
.theme-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}

:root[data-theme] .theme-toggle { display: inline-flex; }

.theme-toggle:hover,
.theme-toggle:focus-visible { color: var(--text); }

/* Sun means "switch to light", moon means "switch to dark" - the icon names
   the destination, and so does the button's label. Exactly one is ever in the
   box, so the 44px target never changes size between themes.
   The swap is instant on purpose: a cross-fade here would have to be matched
   by a transition on every colour in the page, and a half-second of the page
   drifting between two themes reads as lag, not as polish. */
.theme-toggle svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.theme-toggle .icon-moon,
:root[data-theme="light"] .theme-toggle .icon-sun { display: none; }

:root[data-theme="light"] .theme-toggle .icon-moon { display: block; }


/* --------------------------------------------------------------------------
   6. Rail nav and the sliding dot
   -------------------------------------------------------------------------- */

.rail__nav {
  position: relative;    /* offset parent for the dot */
  margin-top: var(--space-6);
}

.nav-list { display: flex; flex-direction: column; }

.nav-link {
  display: flex;
  padding-block: var(--space-2);
  color: var(--text);
  font-size: 1rem;       /* 16px */
  font-weight: 400;
  line-height: 1.5;
  text-decoration: none;
}

.nav-num {
  flex: 0 0 28px;
  color: var(--text-dim);
}

.nav-link:hover .nav-num,
.nav-link[aria-current] .nav-num { color: var(--text-muted); }

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

.nav-link[aria-current] { font-weight: 500; }

.nav-dot {
  position: absolute;
  top: 0;
  left: calc(var(--space-4) * -1);
  width: var(--dot-size);
  height: var(--dot-size);
  border-radius: 50%;
  background: var(--accent);
  opacity: 0;                 /* revealed once site.js has measured a target */
  transform: translateY(var(--dot-y, 0px));
}

.rail__nav.is-ready .nav-dot { opacity: 1; }


/* --------------------------------------------------------------------------
   7. Page shell
   -------------------------------------------------------------------------- */

.page {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--page-pad);
}

.main {
  max-width: var(--content-max);
  margin-left: calc(var(--rail-width) + var(--gutter));
  padding-bottom: var(--space-24);
}

/* Owned by the OUTGOING section, not the incoming one. A margin sits outside
   both section boxes, so no heading is sticky across it and the band blinks
   out; as padding it belongs to a section whose heading is still stuck. Total
   rhythm is unchanged: this plus the next band's own lead = --space-24. */
.section { padding-bottom: var(--band-lead); }


/* --------------------------------------------------------------------------
   8. Sticky section-heading band
   The heading itself is the sticky element and paints an opaque --bg across
   the full content column (plus a bleed) so scrolling content disappears
   cleanly beneath it. The chip and the hairline ride along inside the band.
   A sticky element cannot escape its section, so each band pushes out the one
   above it with no JS.
   -------------------------------------------------------------------------- */

.section-heading {
  position: sticky;
  z-index: 2;
  top: var(--sticky-top);
  margin-inline: calc(var(--band-bleed) * -1);
  padding: var(--band-lead) var(--band-bleed) var(--band-tail);
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

/* The hairline is drawn inset to the content column, not to the bleed. */
.section-heading::after {
  content: "";
  position: absolute;
  right: var(--band-bleed);
  bottom: 0;
  left: var(--band-bleed);
  height: 1px;
  background: var(--rule-strong);
}

.section-title {
  /* No plate behind the type. The band itself is already opaque --bg, which
     is what stops content showing through; the chip fill was decoration on
     top of that, not the mechanism. */
  display: flex;
  align-items: center;
  min-height: var(--band-inner);
  color: var(--text);
  font-family: var(--font-display);
  font-size: var(--size-h2);
  font-weight: 500;
  line-height: 1.4;
}

.numeral { margin-right: .3em; }

.section-body { padding-top: var(--body-lead); }


/* --------------------------------------------------------------------------
   9. I. About
   -------------------------------------------------------------------------- */

/* <picture> is inline by default; blocking it keeps the inline box's
   half-leading from adding phantom space under the image. */
.portrait { display: block; }

/* The one filled shape on the page. Drawn as an offset shadow rather than
   baked into the asset so it follows --accent if that knob is ever turned.
   0 blur, 0 spread - it is a displaced copy of the box, not a glow. */
.portrait img {
  box-shadow: 6px 6px 0 var(--accent);
}

.about {
  margin-top: var(--space-6);
  max-width: 62ch;
  color: var(--text);
  text-wrap: pretty;
}

.skills {
  margin-top: var(--space-12);
  border-top: 1px solid var(--rule);
}

.skills__row, .writings__row {
  display: grid;
  grid-template-columns: 128px minmax(0, 1fr);
  gap: var(--space-4);
  padding-block: var(--space-3);
  border-bottom: 1px solid var(--rule);
}

.skills dt, .writings dt {
  color: var(--text-dim);
  font-family: var(--font-display);
  font-size: var(--size-small);
  font-weight: 500;
  line-height: 1.5;
  padding-top: 2px;   /* optical alignment against the 14px/1.5 dd */
}

.skills dd, .writings dd {
  color: var(--text-muted);
  font-size: var(--size-small);
  line-height: 1.5;
}

.sep { color: var(--text-dim); padding-inline: .15em; }


/* --------------------------------------------------------------------------
   10. II. Projects
   Linked cards carry an accent arrow so they read as links without hover.
   The unlinked card has no anchor, no pointer and no hover (D34).
   -------------------------------------------------------------------------- */

.projects {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-8);
}

/* Cards stretch to the tallest in the row, and est. is pushed to the bottom of
   its card. Without this a blurb that wraps to three lines drops its own est.
   line below the other two and the row stops reading as a row. */
.project {
  display: flex;
  flex-direction: column;
}

.project__link {
  display: block;
  color: inherit;
  text-decoration: none;
}

.project__logo {
  display: flex;
  align-items: flex-end;
  min-height: 44px;
  margin-bottom: var(--space-6);
}

.project__blurb {
  display: block;
  color: var(--text-muted);
  font-size: var(--size-small);
  line-height: 1.5;
}

.project__alt-title {
  display: block;
  color: var(--text);
  font-size: var(--size-small);
  line-height: 1.5;
  font-weight: 500;
  margin-bottom: var(--space-1);
}

.justforfun-projects {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--rule);
  margin-top: var(--space-8);
  width: 100%;
  max-width: 50ch;
}

/* Both link arrows are drawn from the SAME font at the SAME size. They used
   to inherit their context - the project arrow from the sans stack at 14px,
   the resume arrow from mono at 13px - which rendered them at visibly
   different scales. */
.project__arrow,
.band-action__arrow {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: var(--size-small);
  line-height: 1;
  color: var(--accent);
}

.project__arrow { padding-left: .35em; }
.band-action__arrow { padding-left: .3em; }

.project__link:hover .project__arrow,
.project__link:focus-visible .project__arrow { transform: translate(3px, -3px); }

.project__link:hover .project__blurb,
.project__link:focus-visible .project__blurb,
#writing .project__link:hover,
#writing .project__link:focus-visible { color: var(--text); }

.project__link:focus-visible { border-radius: 4px; }

.project__est {
  margin-top: auto;
  padding-top: var(--space-3);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--size-mono);
  line-height: 1.4;
}

/* --------------------------------------------------------------------------
   11. III. Writing
   -------------------------------------------------------------------------- */

   .writings {
    margin-top: var(--space-3);
    border-top: 1px solid var(--rule);
    border-top: none;
  }

  #writing .section-body {
    padding-top: 0;
  }

  .writings__row {
    padding-block: 0;
  }

  .writings__row a, .writings__row dt {
    padding-block: var(--space-3);
  }

  .writings__row dt {
    /* Was --text-white, which is not a token - the declaration was invalid at
       computed-value time, so these dt's silently inherited instead. Under one
       theme that is invisible; under two it would have inherited the wrong
       ink. */
    color: var(--text);
  }

/* --------------------------------------------------------------------------
   11. III. CV
   -------------------------------------------------------------------------- */

.cv { display: flex; flex-direction: column; gap: var(--space-10); }

.cv-entry {
  /* Sketch: rule x=492, text x=517, content column x=480. */
  /* margin-left: var(--space-3);
  padding-left: var(--cv-inset);
  border-left: 1px solid var(--rule); */
}

.cv-head {
  font-family: var(--font-display);
  font-size: 1.125rem;   /* 18px */
  font-weight: 600;
  line-height: 1.4;
}

.cv-company { color: var(--text); }


.cv-sep {
  color: var(--text-dim);
  font-weight: 400;
  padding-inline: .35em;
}

.cv-location {
  font-weight: 400;
  color: var(--text-muted);
}

.cv-meta {
  margin-top: var(--space-1);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--size-mono);
  line-height: 1.4;
}

.cv-bullet { padding-inline: .2em; }

.cv-summary {
  margin-top: var(--space-4);
  color: var(--text-muted);
  font-size: 1rem;   /* 16px */
  line-height: 1.6;
  max-width: 62ch;
}

.cv-highlights {
  margin-top: var(--space-2);
  padding-left: var(--space-6);
  color: var(--text-muted);
  font-size: 1rem;   /* 16px */
  line-height: 1.6;
  max-width: 62ch;
}

.cv-highlights li { padding-left: var(--space-4); }

.cv-highlights li::before {
  content: "\2022";
  display: inline-block;
  width: var(--space-4);
  margin-left: calc(var(--space-4) * -1);
  color: var(--text-dim);
}


/* --------------------------------------------------------------------------
   12. IV. Contact
   Same left-rule treatment as the CV entries.
   -------------------------------------------------------------------------- */

.contact__email {
  font-family: var(--font-display);
  font-size: var(--size-h2);
  font-weight: 500;
  line-height: 1.4;
}


.contact__meta {
  margin-top: var(--space-2);
  color: var(--text-dim);
  font-size: var(--size-small);
  line-height: 1.5;
}

.contact__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
}

.contact__links a {
  display: inline-flex;
  align-items: center;
  font-size: 1rem;
}


/* The address, drawn as outlines rather than set as text - see the comment on
   it in index.html, and ./make-email-svg, which produces both the path and the
   three numbers below.

   All three come out of the font, so this sets exactly as the sibling links do
   rather than approximately: the box is 6.092em because that is what Avenir's
   advance widths and kerning add up to at 1rem, side bearings included, so the
   flex gap beside it measures what it would have measured around text. The
   negative vertical-align is the depth of the j, the y and the tail of the @
   below the baseline, which pulls the drawn baseline down onto the real one.

   It is not a link and not accent-coloured, because it cannot be clicked - a
   mail link is the one thing a harvester never misses. --text says "this is
   the address, written out" rather than promising a click that is not there. */
.contact__address {
  display: inline-block;
  font-size: 1rem;
  width: 6.0920em;
  height: 0.9960em;
  vertical-align: -0.2400em;
  /* Avenir's j leans a little left of its own origin. Text is allowed to
     overhang its box that way and an SVG viewport is not, so say so, or the
     address would sit half a pixel right of where it belongs. */
  overflow: visible;
  fill: currentColor;
  color: var(--text);
}


/* --------------------------------------------------------------------------
   13. Mobile overlay
   -------------------------------------------------------------------------- */

.nav-dialog {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--bg);
  color: var(--text);
}

.nav-dialog::backdrop { background: var(--bg); }

.nav-dialog__inner {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--page-pad);
}

.nav-dialog__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--header-h);
  border-bottom: 1px solid var(--rule);
}

.nav-list--overlay { padding-top: var(--space-6); }

.nav-list--overlay .nav-link {
  position: relative;
  align-items: center;
  min-height: 44px;
  font-size: 1.125rem;
}

/* The overlay has no sliding dot, so the current item gets a static one.
   Weight alone would be too quiet a signal. */
.nav-list--overlay .nav-link[aria-current]::before {
  content: "";
  position: absolute;
  top: calc(50% - var(--dot-size) / 2);
  left: calc(var(--space-4) * -1);
  width: var(--dot-size);
  height: var(--dot-size);
  border-radius: 50%;
  background: var(--accent);
}

.nav-list--overlay .nav-num { flex-basis: 40px; }

/* Hamburger and close mark: three rules and a cross, drawn in CSS so the
   page still makes zero extra requests. */
.nav-toggle, .nav-close {
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  cursor: pointer;
}

.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  display: block;
  width: 20px;
  height: 1px;
  background: currentColor;
}

.nav-toggle__bars { position: relative; }

.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: "";
  position: absolute;
  left: 0;
}

.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after  { top: 6px; }

/* optical: the glyph, not the target, sits flush. The rail's copy of this
   lives on .rail__actions, which holds two buttons; the overlay bar has only
   the one, so it carries the inset itself. */
.nav-close {
  display: inline-flex;
  margin-right: -10px;
}

.nav-close__mark {
  position: relative;
  display: block;
  width: 18px;
  height: 18px;
}

.nav-close__mark::before,
.nav-close__mark::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 18px;
  height: 1px;
  background: currentColor;
}

.nav-close__mark::before { transform: rotate(45deg); }
.nav-close__mark::after  { transform: rotate(-45deg); }


/* --------------------------------------------------------------------------
   14. Mobile layout (< 900px)
   The rail collapses into a sticky bar and the nav moves to the overlay.
   -------------------------------------------------------------------------- */

@media (max-width: 899px) {
  :root {
    --page-pad:   var(--space-6);
    --band-bleed: var(--space-6);   /* bleeds exactly to the viewport edges */
    --sticky-top: var(--header-h);
    --band-lead:  var(--space-8);
    --body-lead:  var(--space-8);
    /* 13px mono is below the 14px readability floor at arm's length. */
    --size-mono:  0.875rem;
  }

  .rail {
    position: sticky;
    top: 0;
    left: auto;
    width: 100%;
    background: var(--bg);
  }

  .rail__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    max-width: var(--page-max);
    height: var(--header-h);
    margin-inline: auto;
    padding-bottom: 0;
    padding-inline: var(--page-pad);
    border-bottom: 1px solid var(--rule);
  }

  .nav-toggle { display: inline-flex; }

  /* Hidden here because the nav lives in the overlay. The box is still set up
     so the <noscript> fallback can simply switch it back on. */
  .rail__nav {
    display: none;
    max-width: var(--page-max);
    margin-inline: auto;
    padding-inline: var(--page-pad);
  }

  .main {
    margin-left: 0;
    padding-bottom: var(--space-16);
  }

  .section { padding-bottom: var(--space-8); }   /* + --band-lead = --space-16 */

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

  .cv-entry, .contact {
    margin-left: 0;
    padding-left: var(--space-4);
  }

  .skills__row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-1);
  }

  .contact__links {
    flex-direction: column;
    gap: 0;
    align-items: flex-start;
  }

  /* The dot separates two things that sit side by side. Stacked, it separates
     nothing and reads as a stray bullet on a line of its own. */
  .contact__sep { display: none; }
}


/* --------------------------------------------------------------------------
   15. Motion
   Every transition lives here, so reduced-motion users get none of it.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }

  .nav-dot {
    transition: transform .32s cubic-bezier(.2, .7, .3, 1), opacity .2s linear;
  }

  /* Only elements that actually change colour on hover AND do not carry
     .link-wipe. Listing a .link-wipe element here sets the `transition`
     SHORTHAND on it a second time, which replaces transition-property
     wholesale and kills the underline animation. */
  .nav-link,
  .project__blurb,
  .theme-toggle,
  .band-action { transition: color .15s ease; }

  /* Both arrows, one rule - so neither can end up animating while the other
     snaps, which is exactly what happened when they were specified apart. */
  .project__arrow,
  .band-action__arrow { transition: transform .18s ease, color .15s ease; }

  .link-wipe { transition: background-size .28s ease; }
}

/* --------------------------------------------------------------------------
   Animated link underline
   text-decoration cannot animate directionally, so the accent line is a
   background gradient sized from 0 to 100% across the inline box. The dim
   resting rule stays on text-decoration underneath it, so a link still reads
   as a link before you touch it - which is the whole affordance on a
   touchscreen, where hover never happens.
   -------------------------------------------------------------------------- */

.link-wipe {
  /* A wrapped link is two line boxes. Under the default "slice" the gradient
     is painted once across the combined box, so the first line gets no
     underline at all - "LearnGit.io (The Modern Coder Group LLC)" wraps at
     390px. "clone" gives every fragment its own background box. */
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  text-decoration: none;

  /* Two layers at the SAME position: the accent on top, the resting rule
     underneath. Only the accent's width animates, so the line appears to
     change colour left to right rather than a second line appearing below
     the first - which is what a text-decoration underline plus a gradient
     would give you, at two different heights. */
  background-image:
    linear-gradient(var(--accent), var(--accent)),
    linear-gradient(var(--rule-strong), var(--rule-strong));
  background-repeat: no-repeat;
  background-position: 0 100%, 0 100%;
  background-size: 0 1px, 100% 1px;
  padding-bottom: 2px;
}

.link-wipe:hover,
.link-wipe:focus-visible {
  background-size: 100% 1px, 100% 1px;
}

/* --------------------------------------------------------------------------
   Nav reveal and hover
   The five rail items rise into place once, on load. Everything else on the
   page is present immediately - a reviewer should never wait to read the
   sentence that says who Jack is.

   The animation is opt-IN via .is-ready, which site.js adds. With JS off, or
   with reduced motion, the items are simply there.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .rail__nav.is-ready .nav-link {
    animation: nav-rise .42s cubic-bezier(.22, .61, .36, 1) backwards;
  }
  .rail__nav.is-ready li:nth-child(1) .nav-link { animation-delay: .06s; }
  .rail__nav.is-ready li:nth-child(2) .nav-link { animation-delay: .11s; }
  .rail__nav.is-ready li:nth-child(3) .nav-link { animation-delay: .16s; }
  .rail__nav.is-ready li:nth-child(4) .nav-link { animation-delay: .21s; }
  .rail__nav.is-ready li:nth-child(5) .nav-link { animation-delay: .26s; }

  /* The label slides, the numeral stays put - so the numerals keep forming a
     straight column while the words lean toward the content they point at. */
  .nav-link .nav-label { transition: transform .18s ease; }
  .nav-link:hover .nav-label,
  .nav-link:focus-visible .nav-label { transform: translateX(3px); }
}

@keyframes nav-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   Band action - the resume link pinned to the right of the CV band
   -------------------------------------------------------------------------- */

.band-action {
  flex: none;
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: var(--size-mono);
  letter-spacing: .02em;
  text-decoration: none;
  white-space: nowrap;
}

.band-action:hover,
.band-action:focus-visible { color: var(--text); }

.band-action:hover .band-action__arrow,
.band-action:focus-visible .band-action__arrow { transform: translateY(3px); }


span.color-white { color: var(--text); }