/* ============================================================
   Piyush Adhikari — portfolio

   One section, one page. Scrolled by the viewer.

   It once captured the wheel and drove the viewer to authored stops.
   That mechanism was fought at every turn and removed — see the header
   of main.js. Page-to-page movement now falls out of the layout
   instead: every section fills the viewport, so one flick moves one
   section without anything intercepting the flick.

   With JS off, reduced-motion set, a screen reader, or in print, the
   layout you get is *this exact layout*. There is no second mode.

   Everything you'd want to restyle is a variable in :root.
   ============================================================ */

:root {
  /* ── DARK (the default) ──────────────────────────────────────────
     Every colour on the site resolves through these. Nothing below
     this block hard-codes one, which is what makes the light theme a
     token swap rather than a second stylesheet. */
  --bg:        #08090A;
  --bg-lift:   #0D0F11;
  --fg:        #EDEDE8;
  --fg-dim:    #9A9FA5;
  /* 4.73:1 on --bg. Was #565C63 at 2.95:1, which fails WCAG AA (4.5:1)
     for every use — dates, tag chips, the quote attribution, the post
     list, the home cue. It read as "quiet"; it was unreadable. AC-17
     had been passing because it only tested --fg and --fg-dim. */
  --fg-faint:  #767C84;
  --accent:    #FF6B35;
  --accent-ring: 255, 107, 53;   /* rgb triplet, for the soft status ring */
  --rule:      #1A1D20;

  /* The starfield. Read by main.js, so the canvas follows the theme. */
  --star:      226, 232, 240;

  /* Fixed chrome sits on a scrim of the ground colour. */
  --scrim:     8, 9, 10;
  --scrim-a:   0.72;
  --overlay-a: 0.86;

  /* A cast shadow, not a scrim. `--scrim` is the ground colour, which
     is near-white under the light theme — a shadow drawn from it would
     be a glow. A shadow is always darker than what it falls on, so it
     gets its own token and both themes state it outright. */
  --shadow:    0 6px 20px rgba(0, 0, 0, 0.55);

  --on-accent:   #0A0A0A;

  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
          Inter, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
          "Liberation Mono", monospace;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --gutter: clamp(1.5rem, 6vw, 8rem);
  /* Distance from the right edge to the centre of the scroll rail. The
     slider track and the rocket that rides it both sit on this axis, so
     they cannot drift apart. */
  --rail: clamp(0.85rem, 1.8vw, 1.4rem);
  /* Portrait size and the gap beside it, named so the block under the
     card can indent by exactly the same amount and stay aligned with
     the name. */
  /* 200px against a ~140px head. At 248 the row was 108px taller than
     the text it centres on, and all of that slack fell between the title
     and the lines under it — the 86px hole that made this read clunky. */
  --portrait: clamp(104px, 14vw, 200px);
  --idgap: clamp(1.5rem, 4vw, 3.5rem);
  --measure: 34rem;
}

/* ── LIGHT ────────────────────────────────────────────────────────
   Greenish-yellow, at the author's request, and not white: #E8EDDC is a
   pale olive that keeps the night sky's inversion without becoming
   paper.

   The accent goes with the ground rather than staying orange, which
   fought it. #55701A is an olive in the same family as the field and
   measures 4.73:1, which it has to: the accent is used on TEXT, so 4.5
   is the floor, not 3.

   DEFINED ONCE NOW, ON THE ATTRIBUTE ONLY. It used to be defined twice —
   once under `prefers-color-scheme: light` so the OS preference chose
   it, once under `[data-theme="light"]` so the toggle could win. The
   author asked the site to open dark, so the media query is gone: dark
   is what everyone sees first, whatever their system is set to, and
   light is reachable in one click and remembered afterwards.

   Worth stating plainly, because it is a real cost: a reader whose
   system asks for light no longer gets light on arrival. The toggle is
   the whole remedy, which is why it stays in the corner of every page
   and why the choice persists. */
:root[data-theme="light"] {
  --bg:    #E8EDDC;
  --bg-lift:   #DDE4CB;
  --fg:    #171A12;
  --fg-dim:  #474D3B;
  --fg-faint:  #5C6349;
  --accent:  #55701A;
  --accent-ring: 85, 112, 26;
  --rule:    #CBD3B8;
  --star:    168, 180, 145;
  --scrim:   232, 237, 220;
  --scrim-a:   0.8;
  --overlay-a: 0.92;
  --on-accent:   #F4F7E8;
  --shadow:  0 6px 18px rgba(23, 26, 18, 0.20);
}

*, *::before, *::after { box-sizing: border-box; }
/* ============================================================
   PAGE-TO-PAGE SNAP, FREE SCROLLING INSIDE A PAGE

   Each section is a page and snaps to the top of the viewport.
   `proximity`, never `mandatory`: mandatory fights you the moment a
   section is taller than the screen — it drags you back to the edge
   while you are still reading the middle. Proximity snaps when you
   let go near a boundary and otherwise leaves you alone, which is
   exactly "page to page between sections, smooth within one".

   All of this is the browser's own scroller. No JavaScript touches
   the wheel; momentum, trackpad feel and keyboard shortcuts are the
   ones the viewer already has.
   ============================================================ */
html {
  scroll-behavior: smooth;
  scroll-padding-top: 0;
}

/* Snap only where scrolling is CONTINUOUS — touch and trackpad-style
   input, which arrives as a stream and lets snap settle at the end of a
   gesture. Measured on a discrete mouse wheel it was hostile: five
   ordinary clicks, 500px of intent, moved the page 41px, because each
   click landed inside the snap radius and was pulled straight back.
   That is the "scrolling is extremely difficult" feeling, rebuilt in
   CSS. A coarse pointer never has that problem.

   To snap on desktop too, move this declaration up into `html` — and
   try it with a real mouse wheel first. */
@media (hover: none), (pointer: coarse) {
  html { scroll-snap-type: y proximity; }
}
html, body { margin: 0; padding: 0; }

/* The card takes the first screen; the writing is what you scroll to.
   A short viewport just scrolls the card too — clipping it would be
   worse than a scroll nobody minds. */
body { min-height: 100svh; }

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.1rem);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

::selection { background: var(--accent); color: var(--on-accent); }

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

/* ------------------------------------------------------------
   Bands
   ------------------------------------------------------------ */

/* Padding was compressed hard before sections became full-height (the
   page ran 4,987px); min-height supplies the space now, and the padding
   only keeps content off the edges. */
/* Sections size to their content, with padding for rhythm.

   They used to be a viewport tall each, centred. Measured, that meant
   travelling 900px per section over 327-611px of content: skills was 64%
   empty, achievements 59%, the whole page 3812px. You scrolled through
   voids and met each section's animation already finished, because a
   section that fills the viewport starts revealing the moment its first
   pixel appears at the bottom edge.

   "One section, one page" bought clean PageDown landings. The void it
   cost was worse than the landings were worth — which is what "the
   scrolling is bad" was about. Snap alignment stays: a section that fits
   on screen is a better snap target than one that overflows it. */
.band {
  position: relative;
  min-height: 0;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(2.5rem, 6vh, 4.25rem);
  padding-inline: var(--gutter);
}


/* The writing is a full page of its own — asked for so that scrolling
   arrives somewhere rather than at a strip. Its content reveals as it
   comes, which is the animation: the section is what moves, not a
   transition painted over it.

   Worth being honest about the cost. AC-03 exists because sections that
   fill the viewport with little in them are what "the scrolling is bad"
   meant. With no posts written this page is about 15% content and 85%
   air. That resolves itself the moment there is a post; until then it is
   a deliberate trade, not an oversight, and AC-03 exempts it by class so
   the exemption is visible rather than silent. */
.band--page {
  min-height: 100svh;
  /* Top-aligned, not centred. Centred, the heading floated in the middle
     of an otherwise empty screen and the emptiness read as the subject.
     Starting at the top makes it a page that begins here, with room under
     it for posts to fill — which is the direction it will actually grow. */
  justify-content: flex-start;
}

/* A masthead is not a page. Full height is right for a section you
   scroll to and wrong for a title block the content has to clear: on the
   home page it would push the first post below the fold, which makes a
   blog read as a title card. Snapping is off for the same reason — there
   is no boundary here worth catching. Top padding clears the fixed
   chrome in the corners. */
.band--mast {
  /* The opening card is a screen, deliberately: it is the whole message,
     and what is under it should have to be scrolled to. */
  min-height: 100svh;
  scroll-snap-align: none;
  padding-block: clamp(4.5rem, 9vh, 6rem) clamp(2rem, 4.5vh, 3rem);
}

/* The signature name is sized for a full-height opening. In a masthead
   with the posts underneath it, 6.25rem is a title card. */
/* Also off the card: 88px on a 1200px canvas. The masthead cap was
   3.75rem, which read small beside a portrait this size. */
.band--mast .wordmark { font-size: clamp(2.4rem, 6vw, 5rem); }

/* width:100% matters more than it looks. .band is a flex column, and
   margin-inline:auto on a flex item cancels the default stretch — so
   without it the inner shrink-to-fits its content and the auto margins
   centre it. That is why the hero name used to sit 287px right of every
   section label while the wider sections happened to look correct: the
   grid sections filled the 78rem cap, the hero did not. */
.band__inner { width: 100%; max-width: 78rem; margin-inline: auto; }

.band__body { max-width: var(--measure); }
/* Sections built from rows need more room than a column of prose. */
.band__body--wide { max-width: none; }

/* Achievements and Writing were once paired side by side, to buy back
   ~600px on a page whose problem was length. Length stopped being the
   problem when every section became its own page, and the pairing was
   the last thing on the site putting a heading anywhere but the common
   left edge. They are two sections now, like everything else. */

/* ------------------------------------------------------------
   Type
   ------------------------------------------------------------ */

/* --- The name ------------------------------------------------
   Set tighter and heavier than the rest of the page, with a short
   accent rule that draws underneath once the letters have landed.
   Split into characters by main.js so it rises letter by letter
   rather than arriving as one block. */

.wordmark {
  font-size: clamp(2.75rem, 9vw, 6.25rem);
  line-height: 0.92;
  letter-spacing: -0.055em;
  font-weight: 680;
  margin: 0 0 clamp(1.1rem, 2.2vw, 1.75rem);
  /* The rule below needs somewhere to sit. */
  padding-bottom: clamp(0.9rem, 1.8vw, 1.4rem);
}
.wordmark span { color: var(--fg-faint); }

.wordmark .ch {
  display: inline-block;
  font-style: normal;
  clip-path: inset(0 0 115% 0);
  transform: translate3d(0, 0.42em, 0);
  transition:
    clip-path 0.72s var(--ease),
    transform 0.72s var(--ease);
  transition-delay: calc(var(--c, 0) * 26ms);
}
.ch--space { white-space: pre; }
.in .wordmark .ch { clip-path: inset(0 0 -25% 0); transform: none; }

/* Draws after the last letter has settled. */
.wordmark::after {
  content: "";
  display: block;
  height: 2px;
  width: 0;
  margin-top: clamp(0.9rem, 1.8vw, 1.4rem);
  background: var(--accent);
  border-radius: 1px;
  transition: width 0.8s var(--ease);
  transition-delay: 0.62s;
}
.in .wordmark::after { width: clamp(3rem, 6vw, 5rem); }

/* One line under the name saying what he is. It replaced three lines of
   prose; at that length it can sit up closer to the wordmark and carry
   more weight.

   The measure is 60ch and not the 46ch it was, because at 46ch the line
   ran out mid-title and broke as "Information Security Consultant and /
   Researcher | Living in Montreal, Canada" — the role split across two
   lines while the separator sat in the middle of the second. Measured,
   the whole line is 885px against 992px of available width at 1440 and
   666 against 717 at 1024: it was the cap doing the wrapping, not the
   room. `text-wrap: balance` came out with it; balance evens the two
   lines, which is the opposite of what a line with a natural break
   point in it wants. */
.standfirst {
  margin: clamp(0.35rem, 1.5vh, 0.9rem) 0 0;
  font-size: clamp(1.15rem, 2vw, 1.7rem);
  line-height: 1.3;
  letter-spacing: -0.02em;
  color: var(--fg-dim);
  max-width: 60ch;
}

/* Below 1024 there is genuinely not enough room, so it has to wrap
   somewhere. Making each clause an inline-block gives the browser the
   gap before the separator as its first break opportunity, and it only
   breaks inside a clause once a clause alone won't fit. Held above 40rem
   because under that the role clause doesn't fit on one line either, and
   forcing the split there costs a third line for nothing. */
@media (min-width: 40rem) {
  .standfirst__part { display: inline-block; }
}

/* The separator is punctuation, not text — it should not carry the same
   weight as the words on either side of it. This rule spent its life
   welded onto the end of `.wordmark +` by a comment that opened in the
   middle of a selector, so it matched nothing and the bar rendered at
   full text weight. */
.standfirst__sep {
  color: var(--fg-faint);
  margin: 0 0.15rem;
  font-weight: 300;
}

.h {
  font-size: clamp(1.6rem, 3.4vw, 2.7rem);
  letter-spacing: -0.03em;
  line-height: 1.1;
  font-weight: 600;
  margin: 0 0 1.25rem;
}

.p {
  color: var(--fg-dim);
  margin: 0 0 1.5rem;
  text-wrap: pretty;
  /* `.band__body--wide` lifts the body's measure so grids can span the
     page, which left running prose inside one setting ~110ch lines. */
  max-width: 68ch;
}
.p:last-child { margin-bottom: 0; }
.p em { font-style: italic; color: var(--fg-dim); }

.avail {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--fg-dim);
  margin: 1.75rem 0 0;
}
/* A quiet live dot. Availability is a status, so it reads as one. */
/* The accent lives here now, not on the whole line: a status dot is what
   the colour was for. */
.avail::before {
  content: "";
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px rgba(var(--accent-ring), 0.16);
}

/* --- Avatar --------------------------------------------------
   Sits beside the page title on About me. Square source, shown round;
   the ring keeps it from floating on either ground. */
/* Portrait then name, side by side, portrait centred against the block.

   The portrait leads, which insets the <h1> from the edge every section
   label sits on. That is a deliberate, constant offset rather than the
   drift AC-26 was written against — the block itself still starts on the
   common edge, and AC-26 measures the block on a masthead for exactly
   this reason.

   It wraps on a narrow screen, where a row cannot hold both without
   squeezing the name; the portrait lands above the text there. */
/* Two rows: the portrait beside the name and title, the supporting lines
   under them in the second column. align-items:center then centres the
   portrait against ROW ONE only — the composition on the social card —
   while the second row inherits the name's left edge for free.

   The earlier version indented `.support` with padding and dropped that
   padding at a breakpoint. Between that breakpoint and the width where
   the card actually wrapped, the name was still indented and the lines
   under it were not. A grid cannot desync like that. */
.idcard {
  display: grid;
  grid-template-columns: var(--portrait) minmax(0, 1fr);
  grid-template-areas:
    "portrait head"
    ".        support";
  column-gap: var(--idgap);
  row-gap: 1.75rem;
  align-items: center;
}
.avatar { grid-area: portrait; }
.idcard__head { grid-area: head; min-width: 0; }
/* One measure for the stack. The lines were ending at 596, 736, 798 and
   947 — four different right edges inside one block. */
.support { grid-area: support; min-width: 0; max-width: 52rem; }

/* One column once a row cannot hold both without squeezing the name. */
@media (max-width: 52rem) {
  .idcard {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "portrait" "head" "support";
    justify-items: start;
  }
}

/* Sized off the social card, which the author preferred: the portrait
   there is about 22% of the canvas width, against 12% here. It is the
   first thing you see, so it should be. */
.avatar {
  width: var(--portrait);
  height: var(--portrait);
  flex: none;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bg-lift);
  box-shadow: 0 0 0 1px var(--rule);
}


/* ------------------------------------------------------------
   Skip link

   First tab stop on every page. Three stops of fixed chrome came before
   the content, which a keyboard or screen-reader user had to walk past
   on both pages, every visit.
   ------------------------------------------------------------ */

.skip {
  position: fixed;
  top: 0.6rem;
  left: 50%;
  transform: translate(-50%, -160%);
  z-index: 60;
  background: var(--bg-lift);
  border: 1px solid var(--fg-faint);
  border-radius: 999px;
  padding: 0.7rem 1.2rem;
  color: var(--fg);
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  transition: transform 0.25s var(--ease);
}
.skip:focus-visible { transform: translate(-50%, 0); }

/* Rides inside the page pill rather than adding a third thing to a
   corner. One button, two states, no label — the icon is the label. */
.themetoggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  min-height: 44px;
  margin-left: 0.15rem;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--rule);
  background: none;
  color: var(--fg-faint);
  cursor: pointer;
  transition: color 0.35s var(--ease);
}
.themetoggle:hover, .themetoggle:focus-visible { color: var(--fg); }
.themetoggle svg { width: 15px; height: 15px; display: block; }
/* One icon per theme; CSS picks, so there is no flash of the wrong one
   before JavaScript runs and none at all with JavaScript off. */
.themetoggle .i-sun { display: none; }
:root[data-theme="light"] .themetoggle .i-sun { display: block; }
:root[data-theme="light"] .themetoggle .i-moon { display: none; }
:root[data-theme="dark"] .themetoggle .i-sun { display: none; }
:root[data-theme="dark"] .themetoggle .i-moon { display: block; }

/* ------------------------------------------------------------
   Reveal

   A page arrives as ONE choreographed moment, not a screenful of
   identical fades. The section gets `.in`; its children play in the
   order JavaScript wrote into `--i`.

   Each kind of element moves the way its shape suggests:
     · headings rise out from behind their own edge (clip, not fade)
     · rules and bars DRAW, along their length
     · rows arrive from the side they are read from
     · chips pop, quickly, in a cascade
   The shared 18px fade that used to do all of this is what "animation
   is not better" was about.
   ------------------------------------------------------------ */

/* Tightened from 55ms with the section-height change. The reveal now
   plays where you are looking rather than near the bottom edge, so it no
   longer has to be slow enough to survive being half-missed. */
:root { --step: 45ms; }

/* Base: everything not given its own behaviour below. */
.r {
  opacity: 0;
  transform: translate3d(0, 16px, 0);
  transition:
    opacity   0.45s var(--ease),
    transform 0.65s var(--ease);
  transition-delay: calc(var(--i, 0) * var(--step));
}
.in .r { opacity: 1; transform: none; }

/* --- Headings: a rise from behind an edge --------------------
   Clip rather than fade. Type that fades in looks like it is being
   switched on; type that rises out from under a line looks placed. */
.h {
  clip-path: inset(0 0 110% 0);
  transform: translate3d(0, 0.35em, 0);
  transition:
    clip-path 0.7s var(--ease),
    transform 0.7s var(--ease),
    opacity   0.01s linear;
  transition-delay: calc(var(--i, 0) * var(--step));
}
.in .h {
  clip-path: inset(0 0 -20% 0);
  transform: none;
  opacity: 1;
}

/* ------------------------------------------------------------
   Reduced motion / no JS

   Nothing to undo. The layout above IS the fallback: everything is
   visible, the document scrolls natively, no reveal is required.
   ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  html { scroll-snap-type: none; scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  /* Every reveal variant, not just the base one — a missed selector
     here leaves content invisible for the viewer least able to wait. */
  .r, .wordmark, .wordmark .ch, .h {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
  .wordmark::after { width: clamp(3rem, 6vw, 5rem) !important; }
}

html:not(.js) .r {
  opacity: 1;
  transform: none;
  clip-path: none;
}
html:not(.js) .wordmark .ch { clip-path: none; transform: none; }
html:not(.js) .wordmark::after { width: clamp(3rem, 6vw, 5rem); }

@media print {
  .r {
    opacity: 1; transform: none; clip-path: none;
  }
  .band { padding-block: 2rem; min-height: 0; }
}

/* ============================================================
   SPACE LAYER

   The author asked for space and a rocket over the black. The rule
   applied: it has to be tied to the travel, not sprinkled on top.
   Stars drift, and streak in proportion to how fast the guided scroll
   is moving — so the motion says "you are moving through this."

   Text always wins. The canvas sits behind everything, is never
   hit-testable, and the stars stay dim enough that body copy keeps
   its contrast (AC-15, AC-17).
   ============================================================ */

.sky {
  position: fixed;
  inset: 0;
  /* A canvas is a replaced element: `inset: 0` will NOT stretch it, it
     just keeps its intrinsic 300x150. The explicit size is required. */
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
  pointer-events: none;
}

/* Content sits above the sky. The fixed chrome already declares its own
   stacking above this and must not be caught by a blanket z-index here —
   a later rule of equal specificity would silently demote it. */
main { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  /* The slider goes with them: it is a moving indicator, and the native
     scrollbar already reports position without animating. */
  .sky { display: none; }
}


/* The theme toggle is the only control left in the chrome. A control
   that cannot work must not be drawn. */
html:not(.js) .themetoggle { display: none; }


/* ------------------------------------------------------------
   One-page additions
   ------------------------------------------------------------ */

/* The theme toggle is the only control left, so it gets the corner the
   page pill used to hold. */
.chrome {
  position: fixed;
  top: clamp(0.9rem, 2.6vh, 1.6rem);
  right: clamp(0.9rem, 2.4vw, 2rem);
  z-index: 20;
  display: flex;
  align-items: center;
  background: rgba(var(--scrim), var(--scrim-a));
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0 0.15rem;
}
.chrome .themetoggle { border-left: 0; }

/* The four areas, as titles. No chips under them: the detail lives on
   LinkedIn now, and the line below says so. */
/* The supporting stack is one type scale in the body face — the label was
   mono, uppercase and 0.66rem, which made it a different voice from
   everything under it. It leads at a step up; the routes sit a step down.
   Sizes, not typefaces, carry the hierarchy. */

/* The three routes out: LinkedIn, GitHub, and down the page. Small, one
   line each, one gap between them. */

/* The author's own line, closing the card. */

.p--note {
  font-size: 0.98rem;
  color: var(--fg-dim);
  max-width: 44ch;
  margin: clamp(1.1rem, 2.6vh, 1.6rem) 0 0;
  text-wrap: pretty;
}
.p--note a {
  color: var(--fg);
  text-underline-offset: 4px;
  text-decoration-color: var(--fg-faint);
  transition: color 0.3s var(--ease);
}
.p--note a:hover, .p--note a:focus-visible { color: var(--accent); }


/* ------------------------------------------------------------
   Writing — restored under the card
   ------------------------------------------------------------ */


/* A labelled band: the section name over a hairline, the content
   directly beneath it. Label and content share one left edge, and that
   edge is the same in every section — see .band__inner below. */
.band--split > .band__inner {
  display: grid;
  gap: clamp(0.6rem, 1.2vw, 1rem);
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}


/*Sized to clear both at either end of the range: 27.2px against 24px at
   1440, 21.6px against 18.4px at 390. Tracking comes down as the size
   goes up, because 0.2em that reads as deliberate at 11px reads as
   spaced-out at 27px. The colour lifts with it — a heading that is bigger
   but dimmer than its own content is a mixed signal, and --fg-faint on
   --bg is only about 3:1, which large text just scrapes and nothing
   smaller would. */
.band__label {
  font-family: var(--mono);
  font-size: clamp(1.35rem, 2.1vw, 1.7rem);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fg-dim);
  padding-top: 0.9rem;
  position: relative;
  /* Was 1em top and bottom, inherited from the <p> it used to be. At a
     27px font that is 27px of air on each side of every section title,
     which is most of "there are bigger gaps". */
  margin-block: 0.45em 0.5em;
  /* The rule is a pseudo-element, not border-top, so it can draw. See
     the reveal section. */
}

/* The hairline over the title, drawn left to right. */
.band__label::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: var(--rule);
}


/*The cap goes on the label and the body, never on .band__inner. The
   inner is centred by margin-inline:auto, so narrowing it would move
   the left edge inward and break the one thing AC-26 exists to hold.
   Capping the grid children instead constrains them from the right, because a stretched grid item with a max-width resolves to start. */
.band--list .band__label, .band--list .band__body { max-width: 56rem; }

.band__label {
  transform-origin: left center;
}


/*The clip lives on an inner span, not on .band__label: a clip-path on
   the label would clip its own ::before rule too, and the rule has to be
   visible while the words are still hidden behind it. */
.band__label.r { opacity: 1; transform: none; }

.band__label.r::before {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.7s var(--ease);
  transition-delay: calc(var(--i, 0) * var(--step));
}

.in .band__label.r::before { transform: scaleX(1); }

.band__label-t {
  display: inline-block;
  clip-path: inset(0 0 110% 0);
  transform: translate3d(0, 0.3em, 0);
  transition:
    clip-path 0.7s var(--ease),
    transform 0.7s var(--ease);
  /* Starts after the rule has begun drawing, so the two read as one
     movement instead of firing together. 120ms, not 200: at 200 the
     title visibly lagged its own rule. */
  transition-delay: calc(var(--i, 0) * var(--step) + 120ms);
}

.in .band__label-t { clip-path: inset(0 0 -30% 0); transform: none; }

.posts__item {
  opacity: 0;
  transform: translate3d(-10px, 0, 0);
  transition:
    opacity   0.5s var(--ease),
    transform 0.65s var(--ease);
  transition-delay: calc(var(--i, 0) * 70ms);
}

.in .posts__item { opacity: 1; transform: none; }

html:not(.js) .r, html:not(.js) .posts__item {
  opacity: 1;
  transform: none;
  clip-path: none;
}

html:not(.js) .band__label-t { clip-path: none; transform: none; }

html:not(.js) .band__label::before { transform: none; }

/* The closing rule lives on the list, not on the last item. A row may
   be `<li class="posts__item">` (the empty placeholder) or
   `<li><a class="posts__item">` (what the studio generates), and in the
   second shape every anchor is the last child of its own <li> — so
   `.posts__item:last-child` drew a border under every row. */
.posts { list-style: none; margin: 0; padding: 0; display: grid; gap: 0;
         border-bottom: 1px solid var(--rule); }

.posts__item {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  gap: 0.25rem 1.5rem;
  padding: 0.9rem 0;
  border-top: 1px solid var(--rule);
  align-items: baseline;
}

.posts__date {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  color: var(--fg-faint);
}

.posts__title { font-size: 1.1rem; color: var(--fg); }

/* An unwritten post should look unwritten, not like a broken link. */
.posts__item--empty .posts__title { color: var(--fg-faint); font-style: italic; }

/* On the home page the list IS the page, so its rows carry more weight
   than they would as one section among five. */
.posts--feed .posts__item { padding: 1.15rem 0; }

/* A writing title reads at body size. It had its own clamp, one step
   above the prose around it, on the reasoning that the list IS the page
   — but a size nothing else on the site uses is a third scale, not a
   hierarchy. The rule below it (`.band__label`) already marks the
   section; the row does not need to shout as well. */
.posts--feed .posts__title { font-size: inherit; }

a.posts__item { text-decoration: none; }

a.posts__item:hover .posts__title, a.posts__item:focus-visible .posts__title { color: var(--accent); }


/* ------------------------------------------------------------
   The card's two marks and three routes
   ------------------------------------------------------------ */

/* A rule stands beside each of the two things worth taking away: what he
   works on, and the line he works by. Everything between them is a way
   out of the page, so nothing else is marked — a rule on every block
   would mark nothing. */
.mark {
  border-left: 2px solid var(--accent);
  padding-left: clamp(0.85rem, 1.7vw, 1.15rem);
  margin: 0;
}
/* One step between the three marked blocks. */
.mark + .mark { margin-top: 1.6rem; }
/* The routes' rule runs the full height of the group, so the links sit
   flush to it rather than inside their own padding. */
.routes .route:first-child { padding-top: 0; }
.routes .route:last-child  { padding-bottom: 0; }
.mark__label {
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--fg);
  margin: 0 0 0.1rem;
}
.mark__body {
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--fg-dim);
  margin: 0;
  text-wrap: pretty;
}

/* Three ways out, stacked. Each is one link end to end — the icon is part
   of the target, not decoration beside it. */
/* One rule down all three, not one each. `.mark` supplies it; this only
   has to stack the links and stop them stretching to the block's width. */
.routes {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.route {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  /* Padding, not just line height: these are block links now rather than
     words inside a sentence, so WCAG 2.5.8's inline exemption does not
     cover them and they have to clear 24px on their own. */
  padding: 0.34rem 0;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--fg-dim);
  text-decoration: none;
  transition: color 0.3s var(--ease);
}
.route:hover, .route:focus-visible { color: var(--fg); }
/* Each icon sits in its own circle, so the three read as a set of
   buttons down the group's rule rather than three loose glyphs. The
   padding is inside the box, so the circle is 30px and the glyph 16px. */
.route svg {
  width: 30px;
  height: 30px;
  flex: none;
  padding: 7px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  fill: currentColor;
  opacity: 0.75;
  transition:
    opacity 0.3s var(--ease),
    transform 0.3s var(--ease),
    border-color 0.3s var(--ease);
}
/* The arrow is stroked, not filled. */
.route__down { fill: none; }
.route:hover svg, .route:focus-visible svg { opacity: 1; border-color: var(--fg-faint); }
/* The one that scrolls nudges the way it will send you. */
.route[href^="#"]:hover svg { transform: translateY(2px); }

@media (prefers-reduced-motion: reduce) {
  .route[href^="#"]:hover svg { transform: none; }
}


/* ------------------------------------------------------------
   Writing: the list, and the posts themselves
   ------------------------------------------------------------ */

/* A row is a date and a title. The `↗` that marked entries leaving the
   site, the outlet tag and the summary line were all removed at the
   author's request — see the note in tools/build.mjs for what that
   costs. `.posts__item--away` survives as a hook with no styling of its
   own, because AC-39 still uses it to tell the two kinds apart.

   An undated row drops the date column rather than reserving space for
   nothing. Every entry is undated today: Baeldung's author page carries
   no dates and none were invented (Constitution V). */
.posts__item--undated { grid-template-columns: minmax(0, 1fr); }

/* --- Hover: a shadow and an underline, and no motion ------------
   Asked for: "just add some shadows when highlighted, and underline.
   remove animations from that hover."

   What was here: an accent rule that DREW left to right under the title
   over 0.45s, plus transitions on the row's hairline and the date. All
   of it is gone — not sped up, removed — so the state change is
   instantaneous. There is nothing here for `prefers-reduced-motion` to
   suppress, because nothing moves.

   The highlight needs a surface for a shadow to land on: a shadow cast
   by a transparent row is a dark halo on a dark ground, which is to say
   invisible. So the row lifts onto `--bg-lift` and takes a soft shadow,
   and reads as a raised strip.

   There is no inline padding. The first version padded the row and
   cancelled it with an equal negative margin, which kept the text from
   moving on hover but started every row 14.4px LEFT of the section
   label, the band rule and the prose above it — measured x=81.6 against
   96. A highlight that needs the words moved off the page's left edge
   is the wrong highlight: the row is full-width already, so it has all
   the room it needs without borrowing any. */
.posts--feed .posts__title { justify-self: start; }

a.posts__item:hover,
a.posts__item:focus-visible {
  background: var(--bg-lift);
  box-shadow: var(--shadow);
  border-radius: 3px;
}

a.posts__item:hover .posts__title,
a.posts__item:focus-visible .posts__title {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}

/* --- A post ---------------------------------------------------
   Generated by tools/build.mjs. The page reuses the band system, so a
   post is a section like any other and inherits the reveal, the sky and
   the theme without knowing they exist. */

.post .band__inner { max-width: 46rem; }

.post__back {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin: 0 0 clamp(1.5rem, 4vh, 2.5rem);
}
.post__back a {
  display: inline-block;
  padding: 0.35rem 0;
  color: var(--fg-faint);
  text-decoration: none;
  transition: color 0.3s var(--ease);
}
.post__back a::before { content: "← "; }
.post__back a:hover, .post__back a:focus-visible { color: var(--fg); }
.post__back--end { margin: clamp(2.5rem, 6vh, 4rem) 0 0; }

.post__meta {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-faint);
  margin: 0 0 clamp(1.75rem, 4vh, 2.75rem);
}

/* 34rem of body text under a 46rem measure — prose wants a shorter line
   than a heading does. */
.post .band__body.prose { max-width: 38rem; }

.prose { color: var(--fg-dim); }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }

.prose p { margin: 0 0 1.35em; text-wrap: pretty; }

.prose h2, .prose h3, .prose h4 {
  color: var(--fg);
  letter-spacing: -0.02em;
  line-height: 1.25;
  margin: 2.4em 0 0.85em;
}
.prose h2 { font-size: clamp(1.3rem, 2.4vw, 1.6rem); font-weight: 620; }
.prose h3 { font-size: clamp(1.1rem, 1.9vw, 1.25rem); font-weight: 600; }
.prose h4 { font-size: 1rem; font-weight: 600; }

.prose a {
  color: var(--fg);
  text-underline-offset: 4px;
  text-decoration-color: var(--fg-faint);
  transition: color 0.3s var(--ease);
}
.prose a:hover, .prose a:focus-visible { color: var(--accent); }

.prose strong { color: var(--fg); font-weight: 620; }
.prose em { font-style: italic; }

.prose ul, .prose ol { margin: 0 0 1.35em; padding-left: 1.35em; }
.prose li { margin-bottom: 0.4em; }
.prose li::marker { color: var(--fg-faint); }

.prose blockquote {
  margin: 1.8em 0;
  padding-left: clamp(0.85rem, 1.7vw, 1.15rem);
  border-left: 2px solid var(--accent);
  color: var(--fg-dim);
}

.prose hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 2.6em 0;
}

/* Code keeps its own ground so a block reads as a block, and scrolls in
   its own box rather than widening the page (AC-12). */
.prose code {
  font-family: var(--mono);
  font-size: 0.86em;
  background: var(--bg-lift);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.1em 0.35em;
  color: var(--fg);
}
.prose pre {
  margin: 0 0 1.6em;
  padding: clamp(0.9rem, 2vw, 1.2rem);
  background: var(--bg-lift);
  border: 1px solid var(--rule);
  border-radius: 6px;
  overflow-x: auto;
}
.prose pre code {
  background: none;
  border: 0;
  padding: 0;
  font-size: 0.82rem;
  line-height: 1.6;
  color: var(--fg-dim);
  white-space: pre;
}

.prose figure { margin: 2em 0; }
.prose img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  box-shadow: 0 0 0 1px var(--rule);
  background: var(--bg-lift);
}
.prose figcaption {
  margin-top: 0.7rem;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--fg-faint);
  text-wrap: pretty;
}


/* ------------------------------------------------------------
   The writing page arrives like a swipe
   ------------------------------------------------------------ */

/* Asked for: "when you scroll hard, make the scroll slow, and show like
   writing page, like a swipe."

   WHAT THIS IS NOT. It is not a wheel handler. Slowing a hard scroll
   literally means intercepting the gesture and animating the page
   yourself, which is the mechanism Constitution I exists to forbid —
   built here twice, fought at every turn, and removed after the verdict
   "scrolling is extremely difficult". Before writing this, the CSS
   alternative was measured on today's layout: enabling scroll-snap for
   a mouse wheel, with both sections as snap targets, moved the page
   ZERO pixels for five wheel clicks — 500px of intent pulled straight
   back to the top. The old pathology, reproduced.

   WHAT THIS IS. The arrival is tied to scroll POSITION rather than to
   time, using the browser's own view() timeline. The writing page lags
   the scroll by ~100px and catches up exactly as it lands, so it reads
   as a page being swiped into place and gives the hand something to
   push against. Scroll slowly and it eases in slowly; flick and it
   arrives at once. Nothing is intercepted — stop scrolling and it stops
   where you left it, which a time-based animation cannot do.

   Progressive enhancement, twice over: browsers without scroll-driven
   animations get the layout unchanged, and anyone with reduced-motion
   set is excluded by the media query rather than by a duration of
   0.01ms, so the section never moves for them at all. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    #writing .band__inner {
      animation: page-swipe linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 88%;
    }

    /* The swipe displaces content DOWNWARD by up to 99px, and the
       browser scrolls a focused element into view by its LAYOUT box —
       so a keyboard user tabbing into the list could land on a link the
       transform had pushed below the fold. AC-14 caught it on the first
       run; it is a real defect, not a strict test.

       Focus inside the section ends the swipe. Removing a downward
       offset can only move content UP, into view, so the correction is
       always in the safe direction, and it is bounded by the 99px the
       animation was ever allowed to displace. A mouse or a scroll never
       triggers it. */
    #writing:focus-within .band__inner { animation: none; }
  }
}

@keyframes page-swipe {
  from { transform: translate3d(0, clamp(56px, 11vh, 110px), 0); }
  to   { transform: none; }
}
