/* About: life as a linked list.

   Each year is a node, drawn the way a linked list is drawn on a whiteboard:
   a circle with a photograph in it, joined to the next by an arrow, ending in
   an empty "null". The year and role sit under the circle.

   Mobile first: the list stacks vertically with each description under its
   node, which is also the layout without JavaScript. With JavaScript and
   enough width the list turns into a row and the current node's text is
   mirrored into a reader panel below it. */

.ll {
  /* Stacked sizing. The row overrides these below, where the node has to fit
     a share of one line rather than a share of the page. */
  --node-w: 128px;
  --pic: 128px;     /* circle diameter */
  --ptr-w: 40px;    /* pointer arrow length, and the label's column width */
  --pic-mid: 64px;  /* the circle's midline: where an arrow meets it */
}

/* Where the body sits in the page.

   The list is one row of circles and a short panel under it: about 310px of
   content on a page that is otherwise empty. Pinned under the heading it left
   240-odd pixels of nothing beneath it and read as having stopped early, which
   is the one shape a single-screen page should not have.

   So the wrap fills the screen and the list takes the leftover space as
   automatic margins, top and bottom, which centers it between the heading and
   the foot of the page without anyone having to know how tall the header and
   the heading are. Flexbox does that arithmetic; a hand-written `margin-top`
   would be a number that is right at one viewport height and wrong at every
   other one.

   The heading does not move. It is above the list in the same flex column and
   keeps its own margins.

   Selected with `:has()` rather than a class on the wrap, so this needs no
   change to about.html. A stylesheet is cached separately from the document
   that uses it, and markup that depends on CSS arriving at the same moment is
   how the say hi page broke (ARCHITECTURE D-72). A browser too old for `:has()`
   simply gets the old, top-pinned layout, which is a fine thing to degrade to.

   Only in the row layout. Stacked, the list is taller than the screen, there is
   no free space to distribute, and the auto margins resolve to zero. */
@media (min-width: 760px) {
  :root.js .wrap:has(> .ll) {
    min-height: 100vh;
    min-height: 100svh;  /* the small viewport, so a collapsing toolbar adds no scroll */
    display: flex;
    flex-direction: column;
  }

  :root.js .ll { margin-block: auto; }

  /* The foot line does not compete for that free space. `.site-foot` takes
     `margin-top: auto` in base.css so it reaches the bottom of a short page,
     and on this page the list's own two auto margins already carry it there.
     Left in, it is a third claimant on one pool of space, and flexbox divides
     the pool between all of them: the list stopped being centered and sat about
     a third of the way down instead. The same correction is in notfound.css;
     a third page that self-centers is the point to give base.css a way to know
     rather than repeating this.

     `padding-top` stays. Zeroing it as well was tried and overshot: the 64px
     went back into the pool the auto margins divide, the list dropped another
     32px, and the gap went from 111 above and 127 below to 143 and 95. The
     padding is not double counting, it is the part of the lower gap that keeps
     the list off the foot line. */
  :root.js .wrap:has(> .ll) > .site-foot { margin-top: 0; }
}

/* The page is set in the serif. It is a page of writing and photographs, so
   the reading face carries all of it: the year, the role, the description and
   the reader panel. The sans is kept for exactly one job, below. */
.ll { font-family: var(--serif); }

.list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.node {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* The node is a button so hover, focus, and tap all select it. The circle
   carries the elevation; the button itself is only a hit area. */
.card {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: var(--node-w);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  /* The readable nodes are buttons and null is an anchor, so the rule has to
     neutralise both defaults. */
  text-decoration: none;
  cursor: pointer;
  position: relative;
  transform: scale(1);
  transition: transform var(--dur-base) var(--ease);
}

/* The ring is what makes a photograph read as a node in a drawn diagram,
   and it is the circle's only edge treatment: a hairline and a shadow on the
   same element are two answers to where the edge is. The current node darkens
   its ring from --line to --muted instead of gaining a shadow. */
.card img,
.card__null {
  display: block;
  width: var(--pic);
  height: var(--pic);
  border: 2px solid var(--line);
  border-radius: 50%;
  object-fit: cover;
  background: var(--card);
  transition: border-color var(--dur-base) var(--ease);
}

.card__foot {
  display: block;
  padding: var(--s3) 0 0;
}
.card__year {
  display: block;
  font-size: var(--text-sm);
  line-height: 1.3;
  color: var(--ink);
}
.card__role {
  display: block;
  font-size: var(--text-label);
  line-height: 1.4;
  color: var(--muted);
}

.card:hover,
.card:focus-visible,
.node.is-current .card {
  z-index: 2;
  transform: scale(1.045);
}
.card:hover img,
.card:focus-visible img,
.node.is-current .card img,
.card:hover .card__null,
.card:focus-visible .card__null,
.node.is-current .card__null { border-color: var(--muted); }
.card:focus-visible { outline: 2px solid var(--ink); outline-offset: 6px; }

/* The tail. Same circle, nothing in it: a dashed hairline where the
   photograph would be, with the word null in the middle. */
/* `null` is the list's own vocabulary rather than anything written, so it
   takes the sans, the same as the `next` labels. Those two words are the only
   sans on the page, which is what makes the switch mean something: this is the
   data structure talking, not the person. */
.card__null {
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border-style: dashed;
  font-family: var(--sans);
  font-size: var(--text-body);
  letter-spacing: .01em;
  color: var(--muted);
}

/* Pointers. In the stacked layout they run downward on the circle's axis,
   with the label beside the arrow. */
.ptr {
  display: flex;
  align-items: center;
  gap: var(--s2);
  height: 48px;
  margin-left: calc(var(--pic-mid) - 6px);
  color: var(--muted);
}
.ptr__label {
  order: 2; /* stacked: arrow on the circle's axis, label beside it */
  font-family: var(--sans);
  font-size: var(--text-label);
  line-height: 1;
  white-space: nowrap;
}
.ptr__arrow { display: block; }
.ptr__arrow--h { display: none; width: var(--ptr-w); height: 12px; }
.ptr__arrow--v { width: 12px; height: var(--ptr-w); }

.node__desc {
  margin: var(--s3) 0 0;
  max-width: 60ch;
  font-size: var(--text-sm);
  line-height: 1.6;
}
.node__desc a,
.reader__body a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* The reader panel only exists in the row layout. */
.reader { display: none; }

/* Row layout: JavaScript is running and the line is wide enough to hold the
   whole list.

   Gated on `:root.js`, set in the head before the first paint, and not on the
   class about.js adds from the foot of the body. D-103 moved the rules that
   *hide* something onto the head flag and left the rules that *lay out* on the
   late one, reasoning that a layout arriving late is invisible because nothing
   is painted differently until it lands. That was wrong, and it is the whole of
   D-131: the browser does not wait, it paints the fallback. So on a first visit,
   where neither stylesheet nor script is cached, a wide screen got the stacked
   phone layout for as long as about.js took to arrive, then snapped into a row.
   Everything in this block is one condition, "scripting is on", and it is read
   in one place. See ARCHITECTURE D-131.

   760px, not the 661px the stack used to hand over at: a wrapped
   linked list is a broken one, and below this the nodes get too small to
   carry a role under them. At 820px the old fixed sizing put null on a second
   line with a `next` arrow above it pointing at nothing.

   Inside the row the nodes and pointers divide the line rather than taking
   fixed figures, so the circle grows with the page up to 128px and the
   pointer takes what is left over. 34px is the floor because the `next` label
   is 28.5px and has to clear the circle beside it; the ceiling is whatever
   makes the row span the measure, so the list ends at the right margin instead
   of stopping short of it. The wrap tops out at 900px, so past that the
   figures stop growing. 100px rather than the wrap's own 96px of padding
   leaves a little back for a classic scrollbar, which `100vw` counts and the
   wrap does not.

   The two counts are the list's own: four nodes and the three pointers between
   them. Dropping or adding a node means changing both, and the 512px is four
   circles at their 128px ceiling. */
@media (min-width: 760px) {
  :root.js .ll {
    --ptr-w: clamp(34px, calc((min(100vw, 900px) - 100px - 512px) / 3), 96px);
    --pic: min(128px, calc((min(100vw, 900px) - 100px - 3 * var(--ptr-w)) / 4));
    --node-w: var(--pic);
    --pic-mid: calc(var(--pic) / 2);
  }

  :root.js .ll .list {
    flex-direction: row;
    flex-wrap: nowrap;
  }
  :root.js .ll .node {
    flex-direction: row;
  }
  :root.js .ll .node__desc { display: none; }

  /* The caption belongs to the node being read, not to all five at once. At
     rest the row is five photographs and two words of the list's own
     vocabulary; the year and the role appear under whichever node the reader
     is on and go again when they leave.

     Opacity, not `display` or `visibility`: the foot keeps its box, so the row
     does not change height as the caption comes and goes, and the text stays
     in the accessibility tree, so a screen reader still reads every year and
     role in order.

     Keyed on `:root.js`, set in the head, like every other rule in this block.
     It used to key on `.ll.is-live`, which about.js added from the foot of the
     body. The two say nearly the same thing and differ by one frame, which was
     the bug: until the script ran, this rule did not match and the foot kept
     its inherited `opacity: 1`. So the first paint carried every year and role
     on the page, and `is-live` landing a moment later transitioned them out
     one by one. The reader saw the caption appear and fade, which is the
     opposite of what the rest of this block is arranging. The hover and
     current rules below move with it, since a caption that is hidden before
     the script runs still has to be revealable by a pointer that arrives
     first. See ARCHITECTURE D-103 and D-131. */
  :root.js .ll .card__foot {
    opacity: 0;
    /* Centered on the circle it belongs to. Left-aligned, the caption hung off
       one side of a round photograph and read as attached to the pointer beside
       it rather than to the node above it. The stack keeps its captions
       left-aligned, where the circle is at the left margin and the paragraph
       under it runs the width of the page. */
    text-align: center;
    transition: opacity var(--dur-fast) var(--ease);
  }

  /* One line under the circle: the year. The role reads in the panel, where it
     has the room for "UC San Diego, B.S. in C.S." on one line instead of
     wrapping to three under a 128px photograph. */
  :root.js .ll .card__role { display: none; }

  /* null has no year, so its foot carries its role instead: the line that says
     where the link goes. It reveals on hover like every other caption, so the
     row rests as four circles and nothing else. */
  :root.js .ll .node--null .card__role { display: block; }
  /* Hover and focus only. `is-current` is deliberately not here: it is sticky,
     because the reader panel below keeps the last node's text rather than
     emptying on the way out, and a caption keyed on it inherited that. Once
     the pointer had crossed any node, that node's year stayed on screen for
     good, so the row rested with a date showing and the reader saw it without
     pointing at anything. The caption is a hover state and lasts exactly as
     long as the hover; the panel is the thing that persists, which is what it
     is for. */
  :root.js .ll .card:hover .card__foot,
  :root.js .ll .card:focus-visible .card__foot { opacity: 1; }

  :root.js .ll .ptr {
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
    gap: var(--s1);
    width: var(--ptr-w);
    height: auto;
    margin: 0;
    /* label, 4px gap, then the arrow centered on the circle's midline */
    padding-top: calc(var(--pic-mid) - 6px - var(--s1) - 14px);
  }
  :root.js .ll .ptr__label { order: 0; }
  :root.js .ll .ptr__arrow--h { display: block; }
  :root.js .ll .ptr__arrow--v { display: none; }

  /* Centered under the row, because the row is centered: the circles divide the
     full measure and the panel reading one of them was hanging off the left
     margin with the list spread out above it.

     The year and role line centers with it. The description keeps its own
     alignment inside a centered block, which is the same split the stacked
     layout takes below 760px: a run of prose set center is the one thing
     DESIGN.md takes off the table, so the column reads as centered and the
     sentences still start where the eye returns to. */
  :root.js .ll .reader {
    /* Flex, centered: the panel reserves the height of its longest entry so the
       row above it never moves, and a one-line description sits in the middle
       of that reserve instead of hanging from its top edge with the rest of the
       box empty under it. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin-top: var(--s6);
    margin-inline: auto;
    max-width: 60ch;
    /* The tallest entry: the title's line, the gap under it, and three lines of
       description. The old figure counted the three lines and then a flat --s6
       for the title, which came up six pixels short of the real one and let the
       page shift as the reader moved between nodes. */
    min-height: calc(1.25 * var(--text-h2) + var(--s2) + 3 * 1.5 * var(--text-body));
    text-align: center;
  }
  /* Flush left, and the block shrunk to the text so that flush left and
     centered are the same place. `text-align: left` inside a 60ch box that the
     sentence does not fill put the line hard against the left edge of that box
     while the title above it stayed centered, so the two lines disagreed by
     however much the description fell short: 62px on "Joined Uber as a software
     engineer, based in the Bay Area." That reads as a centering bug, and it was
     the reason the panel looked off even though every box in it was centered.

     `fit-content` fixes it without centering any prose, which DESIGN.md does
     not allow: a description that fits on one line makes a block exactly that
     wide, and centering the block centers the line. A longer one hits the
     60ch cap, fills the block, and wraps flush left with a ragged right the
     way it always did. */
  :root.js .ll .reader__body {
    text-align: left;
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
  }
  /* Year then role. This is where the role lives; under the circle there is
     only the year.

     Serif, like the description under it. It was sans on the argument that the
     line is the list talking rather than the writing itself, the same as `next`
     and `null`; but those two are one word each in the margin, and this is a
     heading over a paragraph, sitting directly on it. Two faces in two lines
     that belong together split the panel in half. `next` and `null` keep the
     sans, and they are the only things on this page that have it. */
  .reader__title {
    margin: 0 0 var(--s2);
    font-family: var(--serif);
    font-size: var(--text-h2);
    line-height: 1.25;
    color: var(--ink-display);
  }
  .reader__role { color: var(--muted); }
  .reader__body {
    margin: 0;
    font-size: var(--text-body);
    line-height: 1.5;
  }

}

/* A wide touch screen, a tablet held sideways, has the row layout and no
   pointer to open a caption with. Without this the years would be unreachable
   there: `is-current` no longer reveals them, and nothing else can. So they
   are simply left on, which is what the projects board does with its tile
   captions for the same reason. */
@media (min-width: 760px) and (hover: none) {
  :root.js .ll .card__foot { opacity: 1; }
}

/* Touch: nothing scales, the darkened ring alone marks the current node. */
@media (hover: none) {
  .card:hover,
  .node.is-current .card { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .card,
  .card img,
  :root.js .ll .card__foot { transition: none; }
  .card:hover,
  .card:focus-visible,
  .node.is-current .card { transform: none; }
}

/* ---------------------------------------------------------------------------
   The stack, centered.

   Below the row's 760px the list is a column of circles with their captions
   under them, and left-aligning that column left it hanging off the left edge
   of a phone with the descriptions running the whole width of the screen. A
   column of circles has an axis, so the list is built on that axis instead:
   the circles, every caption and the `next` arrows sit on it, and the column
   itself is capped and centered so the writing never spans the full width of a
   tablet held upright.

   The page title is not on that axis. It sits where `projects` and `say hi :)`
   sit, hard against the left margin, because it belongs to the page rather than
   to the list: three headings in the same place on three pages is one thing to
   learn, and a heading that moves because of what happens to be under it is
   three. It was centered here, which read as the about page being laid out by a
   different hand.

   The descriptions keep their own alignment. Centering a run of prose is the
   one thing DESIGN.md takes off the table, so the paragraph is centered as a
   block and set flush left inside it: the column reads as centered, the
   sentences still start where the eye returns to.
   --------------------------------------------------------------------------- */
@media (max-width: 759px) {
  .ll {
    max-width: 34rem;
    margin-inline: auto;
  }

  .list,
  .node { align-items: center; }

  .card { text-align: center; }
  .card__foot { text-align: center; }

  .node__desc { max-width: 100%; }

  /* The arrow goes back onto the circle's axis, which is now the middle of the
     column rather than a measured offset from its left edge. The label is
     taken out of the flow so that centering the row centers the arrow alone;
     left in it, the pair would center together and push the arrow off the
     axis by half the width of the word. */
  .ptr {
    margin-left: 0;
    justify-content: center;
    position: relative;
  }
  .ptr__label {
    position: absolute;
    left: 50%;
    /* Half the arrow, then the gap the flex row would have given it. */
    margin-left: calc(6px + var(--s2));
  }
}
