/* ============================================================
   The shell — a mini browser, full-bleed.

   A chrome strip of tabs above an L of furniture: the rail down the
   left, the content card filling the rest. The shell IS the viewport,
   so the chrome and the rail render at true 1:1 and only the card's
   1280×832 stage scales (js/stage-fit.js).

   Two surfaces, not three. The furniture is one continuous colour and
   the card is the page colour; the active tab is an OPENING onto the
   card, so it is the page colour by being the page. That is what lets
   every seam go — a border between the tab and the page it opens is the
   thing this file exists to not have.
   ============================================================ */

:root {
  --shell-chrome-h: 40px;
  /* The rail asks for only what its widest row needs. It lost the twisty
     gutter and the progress thread, so 260 was carrying air. */
  --shell-rail:     208px;       /* the visitor owns this — drag the seam */
  --shell-rail-min: 180px;
  --shell-rail-max: 420px;
  --card-w:         1280px;
  --card-h:         832px;
  --card-pad:       0px;         /* breathing room inside the card */
  --card-scale-max: 1.25;        /* the only aesthetic knob in the fit —
                                    without it a 2560px monitor leaves a
                                    1280px island floating in the middle */

  /* The furniture: chrome + rail, one continuous L. It has to differ
     from --bg by enough that the join needs no line — bone against
     parchment is exactly the one-considered-step the Cursor surfaces
     ladder is built on — --g-100 against --bg, the exact pair
     cursor.com/docs wears (sidebar bg-card vs content bg-background). */
  --shell-surface: var(--g-100);
  /* the sitewide rgba(var(--ink-rgb), .06) grey hover chip, flattened over
     the furniture. Computed, not eyeballed. */
  --wash-solid:    #e4e3de;
  --shell-line:    var(--g-250);               /* stone — hairline borders */
  --shell-dim:     var(--g-400);               /* mist — group labels, meta */
}

/* ---- the window ----
   Full-bleed: the shell IS the viewport. No transform, no radius, no
   border, no shadow — so the chrome and the rail render at true 1:1 and
   their text is never resampled. `.shell-card > main` is the only scaled
   element on the page (js/stage-fit.js). ---- */
.shell {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--shell-surface);
}

/* The DOCUMENT scrolls now — one continuous vertical scroll through every
   page, on every width. The chrome strip and the rail stay put by being
   sticky rather than fixed, which matters: sticky keeps them in normal
   flow, so the rail can still close by pulling itself out with a negative
   margin and have the card reflow around it. */
.shell,
.shell-body,
.shell-card {
  scroll-behavior: auto;
}

/* No elastic overscroll. The chrome is sticky at top 0, so a rubber-band
   past the top drags the whole strip down with it and opens a white gap
   above the site — there is nothing up there to reveal, so do not go. */
html { overscroll-behavior-y: none; }

/* ONE scroll-corrector, not two. Every card rescale (`zoom` on main) reflows
   the whole document, and stage-fit.js immediately puts the reading line
   back where it was. Chrome's native scroll anchoring reacts to the SAME
   reflow by nudging scrollY to hold its own anchor node — a different point,
   picked by different rules — so during the rail's 0.3s slide the two
   corrections tugged the scroller against each other on every frame, and the
   page visibly juddered. stage-fit's hold is the deliberate one; turn the
   browser's off. */
html { overflow-anchor: none; }

/* The card's own border-radius scrolls away with the card — its top-left
   corner is a thousand pixels up the document once you have moved. This
   wedge redraws that corner where the strip meets the rail and never
   leaves: surface colour outside a 10px arc, transparent inside it, so the
   card simply shows through the round. It tracks --shell-rail, which is
   what the seam-drag writes, and sits at 0 when the rail is closed. */
.shell-chrome::after {
  content: '';
  position: fixed;
  top: var(--shell-chrome-h);
  left: var(--shell-rail);
  width: 10px;
  height: 10px;
  /* a 0.75px soft band on the stop — a 0-width stop aliases into stairs */
  background: radial-gradient(circle 10px at 100% 100%,
              transparent 0 9.25px, var(--shell-surface) 10px);
  pointer-events: none;
  z-index: 21;
}
html.rail-closed .shell-chrome::after { left: 0; }
@media (max-width: 700px) { .shell-chrome::after { display: none; } }

/* ---- the seam, drawn ----
   The furniture and the card differ by one considered step of tone, and for a
   while that step WAS the whole edge. This adds the hairline back on top of
   it: in from the right along the strip's floor, up over the active tab, and
   round the card's corner (js/shell.js drawSeam, measured off the same rects
   as the clip above). It stops at the foot of that corner arc, where the
   grip's own line takes over down the rail's edge.

   A fixed overlay rather than a border, for two reasons: a border cannot
   follow a hole, and this way the line costs the card no layout — the 1280
   stage's fit arithmetic never sees it. Above the chrome (20) so the tab's
   outline reads, above the case overlay (15) and the theater (16) so those
   are framed by the same edge, below the grip (30) and the rail's flyouts. */
.shell-seam {
  position: fixed;
  inset: 0;
  z-index: 21;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-micro) ease;
}
/* it arrives with the furniture it belongs to */
.shell-chrome.is-lit ~ .shell-seam { opacity: 1; }
.shell-seam path {
  fill: none;
  stroke: var(--shell-line);
  stroke-width: 1;
}
/* the rail's edge is the one stretch of the line that MOVES, so it is the one
   stretch that answers the pointer — the handle over it (.side-grip) is
   invisible, and this is how it says what you are about to drag. :has, because
   the grip is a later sibling of the layer this lives in. */
.seam-edge { transition: stroke var(--t-micro) ease; }
.shell:has(.side-grip:hover) .seam-edge,
html.rail-dragging .seam-edge { stroke: var(--orange); }

/* ---- the reach ----
   The rest of that column: from the top of the screen down to the foot of the
   corner arc, where .seam-edge picks up. Drawn on every measure and painted
   only for the drag. At rest the frame is exactly what it was — the corner is
   round, the strip is unbroken, and nothing runs up past the tabs. Under the
   pointer it fills in, and the accent states the whole width you are about to
   move rather than the part of it that happens to sit below the tab strip.

   `transparent`, not opacity or display: it is the same property the edge
   animates, on the same curve, so the two halves arrive as one line rather
   than as a line plus a thing appearing above it. */
/* `.shell-seam .seam-reach`, not `.seam-reach` — the stroke this has to beat
   is set on `.shell-seam path`, which a lone class ties with and loses to. */
.shell-seam .seam-reach { stroke: transparent; transition: stroke var(--t-micro) ease; }
.shell:has(.side-grip:hover) .seam-reach,
html.rail-dragging .seam-reach { stroke: var(--orange); }
html.is-embed .shell-seam { display: none; }
@media (max-width: 700px) { .shell-seam { display: none; } }

/* ============================================================
   Chrome strip
   ============================================================ */
.shell-chrome {
  position: sticky;
  top: 0;
  z-index: 20;
  /* The cream is painted by ::before, not here, so it can be CUT to the
     tab's own silhouette — rounded top corners, standing on the strip's
     floor — instead of a rectangle running to the top of the screen. */
  background: none;
  flex: none;
  height: var(--shell-chrome-h);
  display: flex;
  align-items: flex-end;        /* the tabs stand on the strip's floor */
  opacity: 0;
  transition: opacity var(--t-micro) ease;
}
.shell-chrome.is-lit { opacity: 1; }

/* the strip's cream, with the active tab cut out of it. js/shell.js measures
   the tab and writes the silhouette into --strip-clip; until it does (or
   where path() is unsupported) this is simply a full cream strip. The layer
   sits behind the chrome's own content but still inside the chrome's
   stacking context, so the cut reveals the card underneath and nothing
   else. */
.shell-chrome::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--shell-surface);
}
@supports (clip-path: path('M0 0')) {
  .shell-chrome::before { clip-path: path(evenodd, var(--strip-clip, 'M0 0')); }
  /* no path yet = no clip; the var default above would clip everything away */
  .shell-chrome:not([style*='--strip-clip'])::before { clip-path: none; }
}
/* Held from before first paint (the inline script in <head>) so the
   furniture is already invisible when it is built — no flash to hide.

   The rail is also held OUT of the layout, in exactly the pose it takes
   when closed. That is what lets the name type dead-centre of the whole
   screen rather than centred in a card that has been pushed 260px right
   by a panel nobody has seen yet. When the class comes off at the end of
   the typing, the layout LANDS — in one frame, rail and strip and zoom
   together — and js/typewriter.js glides the pieces in over the top of it
   from where they were. Nothing in here animates; see intro-glide below. */
html.intro-pending :is(.shell-chrome, .shell-side, .shell-card) { transition: none; }

/* During the intro there is no furniture at all — the card owns the whole
   viewport and the name types on plain white. The chrome is pulled UP out
   of the layout by exactly its own height and the rail is pushed LEFT by
   exactly its own width, both in the pose they take when closed, so
   letting the class go is not an entrance that had to be written: it is
   the same two closings, run backwards.

   They arrive together and perpendicular — one down, one right — which
   reads as the frame assembling around the name rather than as two
   separate things animating. */
html.intro-pending .shell-chrome {
  margin-top: calc(-1 * var(--shell-chrome-h));
  opacity: 0;
}
html.intro-pending .shell-side {
  margin-left: calc(-1 * var(--shell-rail));
  opacity: 0;
  pointer-events: none;
}
html.intro-pending .side-grip { left: 0; opacity: 0; pointer-events: none; }
html.intro-pending .chrome-left { width: auto; }
/* the corner is a detail of the frame, so it arrives with the frame */
html.intro-pending .shell-card { border-radius: 0; }

/* The left cluster spans the rail's column and holds ONE button — the rail
   toggle, sitting over the panel it opens. The traffic lights are gone: a
   full-bleed app has no window to close, and three dots that do nothing are
   a lie about what this is. Where-you-are moved to .chrome-right, the far
   end of the strip, so the left edge carries only what the rail is for. */
.chrome-left {
  width: calc(var(--shell-rail) + 1px);   /* +1 for the grip's hairline */
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
  height: 100%;
  padding-left: 10px;
  padding-right: 8px;
  transition: width var(--t-glide) var(--ease-glide);
}
/* Closed: the cluster keeps only the room the toggle needs — and it gets
   there on the same curve the panel leaves on, which `width: auto` could
   never do. auto is not an animatable length, so the strip used to SNAP to
   its closed width while the rail was still gliding out from under it,
   taking the back/forward pair and the whole tab strip with it in one jump.
   10 padding + 26 button + 8 padding = 44, stated so it can be tweened. */
html.rail-closed .chrome-left { width: 44px; }

/* ---- the jump menu: the pages, hung off the + while the rail is shut
   (js/shell.js). It exists only in that state, which is the only state
   with no nav on screen. ---- */
.chrome-jump {
  position: absolute;
  top: calc(var(--shell-chrome-h) + 6px);
  z-index: 30;
  min-width: 176px;
  padding: 6px;
  background: var(--bg);
  border: 1px solid var(--shell-line);
  border-radius: 11px;
  opacity: 0;
  translate: 0 -6px;
  transition: opacity var(--t-glide) ease, translate var(--t-glide) var(--ease-glide);
}
.chrome-jump.is-lit { opacity: 1; translate: 0 0; }
.chrome-jump[hidden] { display: none; }
.jump-row {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 32px;
  padding: 0 10px;
  border-radius: 7px;
  font-size: 13.5px;
  color: var(--ink);
  text-decoration: none;
  transition: background-color var(--t-micro) ease, color var(--t-micro) ease;
}
.jump-row:hover { background: rgba(var(--ink-rgb), 0.06); color: var(--ink); }
.jump-ico { flex: none; width: 17px; height: 17px; color: var(--g-550); }
.jump-ico svg { width: 100%; height: 100%; }
.jump-row:hover .jump-ico { color: var(--ink); }

/* ---- chrome buttons (rail toggle · back · forward · new tab) ---- */
.chrome-btn {
  flex: none;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 7px;
  color: var(--g-550);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--t-micro) ease, background-color var(--t-micro) ease;
}
.chrome-btn:hover,
.chrome-btn:focus-visible {
  color: var(--ink);
  background: rgba(var(--ink-rgb), 0.06);
}
.chrome-btn[disabled] {
  color: var(--g-250);
  cursor: default;
  background: none;
}
.chrome-btn svg { display: block; width: 20px; height: 20px; }
/* back/forward live in the TAB strip, against the first tab, as in the
   reference — not off in the rail's column. They sit on the strip's
   floor with the tabs, so they need lifting to optical centre. */
.chrome-btn[data-act='back'],
.chrome-btn[data-act='fwd'] { align-self: center; margin-bottom: 4px; }
.chrome-btn[data-act='back'] { margin-left: 4px; }
.chrome-btn[data-act='fwd']  { margin-left: 2px; margin-right: 6px; }

/* the strip's far end — the place chip rides here, opposite the rail toggle */
.chrome-right {
  flex: none;
  display: flex;
  align-items: center;
  height: 100%;
  padding-right: 10px;
  padding-left: 8px;
}

/* ---- where you are, in the top bar. Opens the globe (js/globe.js). ---- */
.chrome-place {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 26px;
  padding: 0 10px;
  border: 0;
  border-radius: 7px;
  background: none;
  font: inherit;
  font-size: 12px;
  /* not 1: at line-height 1 the content box is exactly the em, so the
     descender on a g or a y falls outside it and the overflow clip below
     shears it off — "Oregon" lost the tail of its g */
  line-height: 1.5;
  color: var(--g-700);
  white-space: nowrap;
  overflow: hidden;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--t-micro) ease, background-color var(--t-micro) ease;
}
.chrome-place:hover,
.chrome-place:focus-visible {
  color: var(--ink);
  background: rgba(var(--ink-rgb), 0.06);
}
.chrome-place-city { overflow: hidden; text-overflow: ellipsis; }
.chrome-place-time {
  color: var(--shell-dim);
  font-variant-numeric: tabular-nums;   /* the seconds tick without reflowing */
}
/* the chip is the first thing to give ground when the rail is narrow */
@media (max-width: 1100px) { .chrome-place-time { display: none; } }

/* ---- the tab strip ----
   The active tab is the PAGE colour and merges into the card below it.
   No border anywhere, on any tab, ever: a line between the tab and the
   page it opens is the seam this whole section exists to not have. ---- */
.chrome-tabs {
  flex: 1;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
  padding-right: 8px;
}

/* the list fills the strip; the tabs take their 200 from the left and the
   + rides along as the last flex item, so it is always immediately after
   the final tab rather than flung to the far edge */
.tab-list {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
}

.chrome-tab {
  position: relative;
  /* each tab wants 200 and gives ground evenly as they multiply, so
     there is no width arithmetic in JS at all */
  flex: 0 1 200px;
  min-width: 0;
  height: 32px;
  display: flex;
  align-items: center;
  /* the 7px gap and the 11px/10px insets moved into .tab-id, which is the
     box that has to hug its contents now */
  padding: 0 1px 0 2px;
  border: 0;
  border-radius: 9px 9px 0 0;
  background: none;
  font: inherit;
  color: var(--shell-dim);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* transform is added below, for the drag */
}
.chrome-tab:hover { background: rgba(var(--ink-rgb), 0.05); color: var(--g-700); }

/* ---- dragging one along the strip (js/tabs.js). The neighbours glide;
   the tab in hand does not, because it is already following the pointer
   and a transition on it would put it on elastic. ---- */
.chrome-tab { transition: background-color var(--t-micro) ease,
                          color var(--t-micro) ease,
                          transform var(--t-glide) var(--ease-glide); }
/* The tab in hand SLIDES; it does not lift off. It keeps its silhouette —
   rounded at the top, standing on the strip's floor — and keeps the mouth
   cut under it, which js/tabs.js re-measures every frame of the drag, so
   the flares at its feet and the seam across it travel WITH it and the tab
   never comes away from the page it opens.
   The one thing that changes is that it turns opaque: at rest an inactive
   tab has no fill at all and the active one is a hole in the cream, so
   without a surface of its own the tab you are carrying let every tab it
   crossed read straight through it. Active takes the page's own colour —
   which is exactly what the mouth reveals, so the tab looks no different
   standing still. */
html.tab-dragging .chrome-tab.is-dragging {
  transition: none;
  z-index: 3;
  cursor: grabbing;
  background: var(--bg);
}
/* a background tab stays a background tab while it travels: the strip's own
   cream, one shade down so you can see what you are carrying */
html.tab-dragging .chrome-tab.is-dragging:not(.is-active) {
  background-color: var(--shell-surface);
  background-image: linear-gradient(rgba(var(--ink-rgb), 0.05), rgba(var(--ink-rgb), 0.05));
}
html.tab-dragging { cursor: grabbing; user-select: none; }
html.tab-dragging .chrome-tab { cursor: grabbing; }

/* The active tab is a WINDOW cut in the cream strip, not a panel painted on
   it. The card reaches up behind the strip (see .shell-card below), so what
   shows through the tab IS the page — same colour, because it is the same
   surface — and a line of text scrolling out travels up through the tab and
   disappears at its top edge instead of being cut off at the card's.

   Nothing is drawn where the tab meets the page: no fill, no hairline. The
   tab's own icon and name ride over the opening on a pill of the page's
   colour, and that pill is the only opaque thing in here — see below. */
.chrome-tab.is-active {
  background: transparent;
  color: var(--g-700);
  /* the white top-edge highlight retired here: the seam now draws a real
     hairline around this tab, and two lines on one edge is one too many */
}

/* ---- the identity pill: the icon and the name ride together on a plate of
   the page's own surface. The tab is a window and the page travels through
   it, so without this the label and a passing line of type share the same
   few greys and simply collide — the plate is what lets the page keep
   moving behind while the tab keeps saying what it is.

   It is --bg exactly, so at rest, with nothing behind the tab but the
   page's surface, the pill is invisible; it only asserts itself as content
   arrives under it. No border and no shadow for the same reason: an outline
   would be visible always, to solve a problem that is only sometimes. ---- */
.tab-id {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  max-width: 100%;
  /* EVERY tab carries the pill's padding, active or not — only the fill is
     the active one's. Give the padding to the active tab alone and its icon
     and name sit 9px right of where an inactive tab's do, so the label jumps
     sideways the moment you switch tabs. The tab's own padding is wound back
     by the same amount, so the icon lands where it always did. */
  padding: 6px 12px;
  /* fully round, not a rounded rectangle. The pill has to read as an object
     the page passes BEHIND — a lozenge that ends in two arcs the width of
     its own height does that; an 8px corner reads as a panel welded to the
     tab, which is the thing the opening exists to not have. */
  border-radius: 999px;
  transition: max-width var(--t-glide) var(--ease-glide);
}
.chrome-tab.is-active .tab-id { background: var(--bg); }

/* ---- the seam across the active tab's mouth is GONE, and nothing replaced
   it. It was a hairline standing in for a join that no change of tone marks,
   since the tab's white and the page's white are the same surface — and that
   sameness is the point: the tab is an opening, so the edge where it meets
   the page should be nothing at all. The lozenge above is now the only
   opaque thing in the tab, and it earns that by being what keeps the name
   readable while the page travels underneath it.

   js/shell.js still measures --flare-l/r; they cut the hole's concave feet,
   which is the only thing they were ever really for. ---- */
/* the pill gives the close button its room rather than sliding under it */
.chrome-tab:hover .tab-id,
.chrome-tab:focus-within .tab-id { max-width: calc(100% - 22px); }
.chrome-tab.is-active:hover { background: transparent; }

/* The flare — the two concave fillets carrying the tab down into the card —
   is part of the HOLE now, not painted on top of it. js/shell.js widens the
   clip-path cut with a concave arc at each foot, so page content scrolling
   up behind the strip passes through the flares exactly as it passes
   through the tab. The old masked quarter-disc squares painted opaque --bg
   there, which is what used to snap to a hard corner the moment anything
   scrolled beneath. */

/* where clip-path: path() is unsupported there is no hole at all — give the
   active tab a real fill so it still reads (square feet, honest fallback) */
@supports not (clip-path: path('M0 0')) {
  .chrome-tab.is-active { background: var(--bg); }
}

.tab-ico {
  flex: none;
  width: 15px;
  height: 15px;
  display: grid;
  place-items: center;
}
.tab-ico > svg,
.tab-ico > img { display: block; width: 100%; height: 100%; }
.tab-ico > img { border-radius: 3px; object-fit: contain; }

.tab-label {
  /* shrink-to-fit, never grow: the pill has to hug the name, or it becomes
     a plate the width of the whole tab and the page stops showing through */
  flex: 0 1 auto;
  min-width: 0;
  font-size: 12.5px;
  line-height: 1;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* the section the page is currently on, trailing the page's own name */
.tab-label .tab-sec { color: var(--shell-dim); }

/* ---- The active tab's name and icon used to go ORANGE whenever a piece of
   the page passed behind them — the tab being a window onto the card, page
   type and tab type share the same few greys and simply collided. The pill
   solves that at the source: the type sits on its own surface, so it never
   has to fight what is travelling underneath. ---- */
.chrome-tab.is-active .tab-ico,
.chrome-tab.is-active .tab-label {
  transition: color var(--t-micro) ease;
}
/* (.is-over retired with the pill — the label carries its own surface now) */
/* (the label's fade-under-the-close-button retired with the pill: the pill
   now yields its own width on hover, and the name ellipsises inside it) */

/* the same box and the same glyph weight as the + it sits opposite */
.tab-close {
  position: absolute;
  right: 4px;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 5px;
  color: var(--g-550);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-micro) ease, background-color var(--t-micro) ease,
              color var(--t-micro) ease;
}
.tab-close svg { width: 20px; height: 20px; }
.chrome-tab:hover .tab-close,
.tab-close:focus-visible { opacity: 1; }
.tab-close:hover { background: rgba(var(--ink-rgb), 0.08); color: var(--ink); }
/* you cannot close the window from inside it */
.tab-list.is-single .tab-close { display: none; }
/* At the cap the labels stand down and the favicons speak for themselves —
   and the + goes with them. A button that is present but refuses is worse
   than no button: the strip is full, and the honest way to say so is to
   stop offering. It returns the moment a tab closes. */
.tab-list.is-icon-only .chrome-new { display: none; }
.tab-list.is-icon-only .tab-label { display: none; }
.tab-list.is-icon-only .chrome-tab { flex: 0 0 44px; justify-content: center; padding: 0; }

/* the cap is legible: the + shakes rather than doing nothing */
@keyframes tab-cap-shake {
  0%, 100% { translate: 0; }
  25%      { translate: -3px; }
  75%      { translate: 3px; }
}
/* The + sits on the tabs' own centre line, not the strip's. Tabs are 32
   tall standing on a 40 strip, so their centre is 24 down; a 26px button
   centred in the strip lands at 18.5 and reads as floating above the row.
   flex-end plus 3 puts its centre at 24 — level with the close buttons
   inside the tabs. */
.tab-list .chrome-new { align-self: flex-end; margin: 0 0 4px 4px; }
.chrome-new.is-capped { animation: tab-cap-shake 0.18s var(--ease-glide) 2; }

/* ============================================================
   Sidebar
   ============================================================ */
.shell-body {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: flex-start;      /* so the sticky rail can be its own height */
}

.shell-side {
  /* sticky, not fixed: it stays in flow, so html.rail-closed's negative
     margin still pulls it out and the card still reflows to fill the gap */
  position: sticky;             /* the globe card flies out from here */
  top: var(--shell-chrome-h);
  height: calc(100vh - var(--shell-chrome-h));
  z-index: 10;
  background: var(--shell-surface);
  width: var(--shell-rail);
  flex: none;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 0 8px 8px;
  opacity: 0;
  /* NO geometry transition. The rail's width drives the card's width,
     which drives the stage's zoom — so easing it here meant a full
     re-layout and re-raster of the whole document on every frame of the
     ride, and four things (rail, card, zoom, overlay edge) that had to
     stay in step and never did. The toggle is one frame now: the rail is
     simply the other width. The toggle's glide is a transform written
     inline by js/shell.js; this transition covers only the rail's arrival
     fade on first paint (.is-lit). */
  transition: opacity var(--t-glide) ease;
}
.shell-side.is-lit { opacity: 1; }
/* while dragging, the width must track the pointer exactly — a transition
   here would make the handle feel like it is on elastic */
/* The grip belongs in here too. Without it the seam eases toward the new
   width over 0.3s while the panel snaps to the pointer instantly, so the
   orange line visibly lags the edge it is supposed to BE. */
html.rail-dragging .shell-side,
html.rail-dragging .chrome-left,
html.rail-dragging .side-grip { transition: none; }

/* Closed means CLOSED — the page gets the whole screen. It SLIDES out
   rather than vanishing: a negative margin equal to its own width, so the
   panel travels left under the card's edge while the card grows into the
   space in the same frame. display:none would have been instant, and an
   instant 260px jump reads as a glitch rather than as a panel closing.

   The card follows for free — stage-fit watches it with a ResizeObserver,
   so the stage rescales continuously through the whole 0.3s rather than
   snapping when it lands. */
html.rail-closed .shell-side {
  margin-left: calc(-1 * var(--shell-rail));
  /* NO opacity fade. The rail travels its own width to get off screen, so
     it is already gone when it lands — the fade added nothing but a
     non-opaque layer, and text on a non-opaque layer loses subpixel
     antialiasing in every browser: the whole rail went thin and grainy for
     the length of the slide. Opaque background + pure translate = crisp
     type the entire way. (js/shell.js toggleRail does the translate.) */
  pointer-events: none;
}
html.rail-closed .side-grip { left: 0; opacity: 0; pointer-events: none; }
/* the rail's contents hold their layout width while the panel travels, so
   nothing reflows on the way out */
.shell-side > * { flex-shrink: 0; }

/* ---- the resize handle, which is INVISIBLE at rest ----
   The line it sits on belongs to .shell-seam (the .seam-edge path), so this
   element is pure hit area: it draws nothing, and hovering it is what turns
   that path's rail-edge stretch to the accent. Keeping the drawing in one
   place is the whole reason the corner meets the edge exactly — see the note
   in js/shell.js drawSeam.

   FIXED, not absolute: the document is the scroller, so an absolute handle is
   as tall as the whole PAGE, which is both pointless (you can only grab what
   is on screen) and, back when it did the drawing, wrong — its offsets were
   measured from the document's top, so a scrolled page ran the hairline up
   through the tab strip. The drag is unaffected either way: it reads
   e.clientX against the shell's own rect, both viewport-relative already. */
.side-grip {
  position: fixed;
  left: var(--shell-rail);
  top: 0;
  bottom: 0;
  z-index: 30;
  width: 1px;
  background: transparent;
  cursor: col-resize;
  transition: background-color var(--t-micro) ease,
              opacity var(--t-glide) ease;
}
.side-grip::before {
  content: '';
  position: absolute;
  inset: 0 -4px;
}

/* ---- identity ----
   The letterhead: name and role as plain type on the bone, the top half
   of .ask-chip (which is a typographic block now, not a surface — see the
   bottom of this file).

   THE RAIL'S WHOLE TYPE SCALE lives in three voices, and this block obeys
   it like everything else:
     primary      13.5px / --g-900   rows, the status leads, and the name
                                     (whose ONLY distinction is weight 500)
     provisional  13px   / --g-550   the blank's rotating question
     meta         11px   / --g-400   labels, the role, subs, ⌘K
   The name used to sit at full --ink over a foot whose leads were 12.5px
   mid-grey, and the column read as one shout over a whisper. One ink for
   every primary line, one grey for every meta line, and the hierarchy is
   carried by weight and position instead of by volume. */
.side-id-text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: hidden;
}
.side-name,
.side-role {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.side-name {
  font-size: 13.5px;
  font-weight: 500;
  line-height: 17px;
  color: var(--g-900);
}
.side-role { font-size: 11px; line-height: 13px; color: var(--shell-dim); }

/* ---- the scrolling middle (groups land here, built by shell.js) ---- */
.side-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--g-200) transparent;
}
.side-scroll::-webkit-scrollbar { width: 6px; }
.side-scroll::-webkit-scrollbar-thumb {
  background: var(--g-200);
  border-radius: 3px;
}
.side-scroll::-webkit-scrollbar-track { background: none; }

/* ---- groups: a quiet lowercase label over a run of rows.
   No transition by DEFAULT, deliberately: on every navigation the rail
   must appear to have never moved — that is the whole illusion a
   persistent sidebar buys, and a fade on each page load would give it
   away. Only the home page's first visit animates, sequenced behind the
   typewriter, reusing the entrance idiom the chapter rails used. ---- */
.side-group { display: block; opacity: 0; position: relative; }

/* The nav thread — an orange line that tracked reading progress down the
   gutter beside the page rows — is gone. It needed a gutter to live in,
   and the rows gave that column up so every group could start on the same
   left edge as its label. js/nav-progress.js is no longer loaded. */
.shell-side.is-lit .side-group { opacity: 1; }

/* On the home page's first visit the panel itself is the movement — it
   slides in from behind the card's edge with its contents already in
   place. Staggering the groups on top of that would be two motions
   describing one arrival, and they fight: the rows drift right while the
   panel they sit in is still travelling right. So they simply come with
   it, and the chrome fades up over the same beat. */
html.intro-run .side-group { transition: none; }

/* ---- the assembly, while it is running (js/typewriter.js puts this class
   on for exactly the length of the ride).

   Nothing in the arrival is a CSS transition on these two any more. The
   version this replaces tweened `margin-top` on the strip — a layout
   property, so every frame relaid the document and re-fit the 1280 stage —
   while the rail's `margin-left` had no transition at all and simply
   snapped, taking the card's width, the stage's zoom and the half-typed
   name with it. One piece oozed, one piece jumped. They are a FLIP now:
   the layout lands first and a compositor transform carries each piece in
   from where it was, which is the same lesson the rail toggle learned.

   So the only job left here is to keep CSS out of the way. `transition:
   none` means the opacity these get from .is-lit lands in the same frame
   as the layout — which is not a flash, because both pieces are entirely
   off-screen at that moment — instead of fading up over 0.3s. That matters
   beyond tidiness: a layer mid-opacity is non-opaque, and both Chrome and
   Firefox drop subpixel text antialiasing on a non-opaque layer, so every
   label in the rail used to go thin for the length of the slide and then
   snap crisp. Opaque from frame one, translating only. ---- */
html.intro-glide :is(.shell-chrome, .shell-side, .side-grip) { transition: none; }

/* The hairline is measured off the FINAL frame — the card's corner, the
   tab's mouth, the strip's floor — so it cannot ride in with the pieces it
   outlines: it would draw the strip's floor as a line hanging in space
   while the strip itself is still above the window. It waits out the ride
   and settles onto the assembled frame, which is also the first moment the
   corner it turns is round. (Written through `.is-lit ~` rather than as a
   plain descendant because that is the rule it has to beat.) */
html.intro-glide .shell-chrome.is-lit ~ .shell-seam { opacity: 0; transition: none; }

/* the corner is a detail of the frame, so it arrives on the frame's clock */
html.intro-run .shell-card {
  transition: border-radius var(--t-intro) var(--ease-intro);
}
.side-label {
  margin: 14px 0 4px;
  padding: 0 10px;
  font-size: 11px;
  font-weight: 400;
  line-height: 14px;
  letter-spacing: 0.05em;
  color: var(--shell-dim);
}
.side-group:first-child .side-label { margin-top: 6px; }

/* The rail is as tall as the window and the tree is short, so on anything
   above a laptop the groups clumped at the top and left a long fall of
   nothing above the chip. What slack there is goes into the JOINT between
   groups, where it reads as rhythm rather than as a hole.

   The subtraction is what keeps it honest at both ends: a plain vh would
   still be spending 24px on the joint at 660px tall, where there is no
   slack to spend and the tree is already scrolling. This gives back nearly
   nothing under ~700px, opens up through the laptop range, and stops at
   40px — past that it is not spacing any more, it is a second hole. */
.side-group + .side-group { margin-top: clamp(0px, calc(6vh - 30px), var(--sp-40)); }

/* ============================================================
   The globe card — where you are, where he is, and how far apart.
   Flies out to the RIGHT of the rail, over the card.
   ============================================================ */
.globe-card {
  position: fixed;              /* js/globe.js pins it under its trigger */
  z-index: 70;
  width: 208px;
  padding: 14px 16px 14px;
  background: var(--bg);
  border: 1px solid var(--shell-line);
  border-radius: 14px;
  opacity: 0;
  translate: -6px 0;
  transition: opacity var(--t-stage) ease, translate var(--t-rise) var(--ease-rise);
}
.globe-card[hidden] { display: none; }
.globe-card.is-lit { opacity: 1; translate: 0 0; }

.globe {
  display: block;
  width: 168px;
  height: 168px;
  margin: 0 auto 4px;
  cursor: grab;
  touch-action: none;           /* a drag turns the globe, never scrolls */
}
.globe-card.is-turning .globe { cursor: grabbing; }
/* while a hand is on it the pin and arc stop easing — they should track
   the geometry exactly, not lag a frame behind it */
.globe-card.is-turning :is(.globe-arc, .globe-home circle, .pin-drop) {
  transition: none;
}
/* the site's construction line, and its slow crawl (ai.css's spark-march) */
.globe .guide {
  fill: none;
  stroke: var(--g-250);
  stroke-width: 1;
  stroke-dasharray: 2 5;
  stroke-linecap: round;
}
.globe-orbit { animation: spark-march 24s linear infinite; }
/* the coastlines are the ink the eye actually reads; the grid stays a
   dotted construction line behind them */
.globe-land {
  fill: none;
  stroke: var(--g-700);
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.72;
}

/* the limb and the equator are INK, so there is one unambiguous reference
   line to read the dotted grid against */
.globe-limb,
.globe-eq {
  fill: none;
  stroke: var(--g-700);
  stroke-width: 1.2;
  stroke-linecap: round;
}
.globe-limb { opacity: 0.5; }

.globe-arc {
  fill: none;
  stroke: var(--orange);
  stroke-width: 1.4;
  stroke-dasharray: 3 4;
  stroke-linecap: round;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.globe-card.is-landed .globe-arc { opacity: 1; }

/* Tigo: a hollow dot, the site's .dot.hollow manner */
.globe-home circle {
  fill: var(--bg);
  stroke: var(--g-700);
  stroke-width: 1.4;
  opacity: 0;
  transition: opacity 0.3s ease 0.1s;
}
.globe-card.is-landed .globe-home circle { opacity: 1; }

/* the visitor: the case study's orange teardrop, dropped on arrival */
.globe-pin path { fill: var(--orange); stroke: var(--bg); stroke-width: 1.1; }
.globe-pin circle { fill: var(--bg); }
.pin-drop {
  opacity: 0;
  translate: 0 -14px;
  scale: 0.5;
  transform-box: fill-box;
  transform-origin: bottom center;
}
.globe-card.is-landed .pin-drop {
  opacity: 1;
  translate: 0 0;
  scale: 1;
  transition: opacity 0.2s ease, translate var(--t-pop) var(--ease-pop),
              scale var(--t-pop) var(--ease-pop);
}

.globe-where { font-size: 12.5px; color: rgba(var(--ink-rgb), 0.48); text-align: center; }
.globe-far {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--orange);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.globe-when {
  margin-top: 2px;
  font-size: 10.5px;
  color: var(--shell-dim);
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .globe-orbit { animation: none; }
  .globe-card { transition: opacity 0.25s ease; translate: none; }
  .globe-card.is-landed .pin-drop { transition: none; }
}
.now-dot {
  flex: none;
  width: 6px;
  height: 6px;
  margin-left: 4px;
  border-radius: 50%;
  background: var(--g-250);
}

@media (prefers-reduced-motion: reduce) {
  .side-row:hover .side-ico * { animation: none; }
  /* the pen holds still and the mark does not turn: the ink arrives
     without the draw, and the asterisk holds its pose (the six-fold
     symmetry means the banked notches land invisibly anyway) */
  .ask-blank::before { transition: none; }
  .ask-blank > svg { transition: none; }
  /* the rail appears; it does not perform */
  html.intro-run .side-group {
    translate: none;
    transition: opacity 0.25s ease;
  }
}

/* ---- a row. Identical at every depth, and now at the same INSET at every
   depth too: one left edge runs the whole rail, from Home down through the
   case studies to Goodreads. Depth used to be drawn twice over — a hairline
   with a margin under Work, plus the twisty holding a gutter open in front
   of it — which put three different left edges in a column eleven rows
   long. The twisty moved to the trailing edge (below) and the indent went;
   what is nested is now said by the disclosure alone. ---- */
.side-row {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 30px;
  /* the group label's own inset, so a row's glyph starts on exactly the
     letter its label starts on */
  padding-left: 10px;
  border-radius: 8px;
  /* A 2px channel above and below the FILL, so two lit rows next to each
     other (the page you are on, plus the one under the cursor) read as two
     chips rather than one tall block. Transparent borders + a padding-box
     clip rather than a margin, deliberately: with border-box sizing the
     row is still exactly 30px and the whole rail's rhythm is untouched —
     only the paint is inset. A margin would have pushed every row in the
     column down to solve a problem that only exists at two of them.
     Whatever paints a chip must use background-COLOR, not the `background`
     shorthand — the shorthand resets background-clip and closes the
     channel again. */
  border-block: 2px solid transparent;
  background-clip: padding-box;
  font-size: 13.5px;
  line-height: 1;
  /* Near-ink, not the old mid-grey: the rail is the site's table of
     contents, and its entries should read as firmly as the words in the
     card do. The step that used to be drawn between the label and the row
     is now drawn between the row and its GLYPH (.side-ico, its own grey) — one
     hierarchy instead of two. Kept a shade off --ink so hover still has
     somewhere to go. */
  color: var(--g-900);
  -webkit-tap-highlight-color: transparent;
  transition: color var(--t-micro) ease, background-color var(--t-micro) ease,
              box-shadow var(--t-micro) ease;
}
/* the link fills the rest of the row so the whole chip is clickable */
.side-link {
  flex: 1;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding-right: 10px;
  color: inherit;
  text-decoration: none;
  border-radius: inherit;
}
/* Hover fills. background-COLOR, never the `background` shorthand: the
   shorthand resets background-clip along with everything else, which put
   the fill back on the border box and quietly undid the channel the row
   sets up above. Every rule that paints a chip below obeys the same. */
.side-row:hover,
.side-row:has(.side-link:focus-visible) {
  color: var(--ink);
  background-color: rgba(var(--ink-rgb), 0.06);
}

/* You are here: the SAME chip the cursor draws, left standing. The accent
   used to say it instead — orange text, no fill — but a coloured word in a
   column of grey ones reads as a link that wants clicking, which is the
   opposite of "you are already on this". A resting chip is the plainer
   sentence, and it is the one every sidebar the visitor has used says it
   in. The ember is spent elsewhere in the rail; here it was noise. */
.side-row.is-current,
.side-row.is-here {
  color: var(--ink);
  background-color: rgba(var(--ink-rgb), 0.06);
}

/* The trail is the chain of parents you are inside — Work above PantryPal,
   Play above Pong. It steps back to the ROW colour: only the deepest thing
   you are actually looking at wears the chip, and a parent should sit
   level with its siblings, not a shade apart from them. */
.side-row.is-trail { color: var(--g-900); }
/* One chip at a time: the moment a section claims "here", the page row
   above it puts the fill down and goes back to being an ordinary row. */
.side-scroll.has-here .side-row.is-current {
  color: var(--g-900);
  background-color: transparent;
}
.side-scroll.has-here .side-row.is-current:hover {
  color: var(--ink);
  background-color: rgba(var(--ink-rgb), 0.06);
}

/* The glyph sits a step behind its own label: the name first, the shape
   second. A real grey, at full strength — NOT a faded currentColor, which
   is what an opacity here amounts to: it thins whatever colour the row is
   wearing rather than choosing one. So the glyph carries its own, and only
   the states that mean something take it back (below). */
.side-ico {
  flex: none;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  color: var(--g-500);
  transition: color var(--t-micro) ease;
}
/* under the cursor the whole row lifts together, the glyph by one step */
.side-row:hover .side-ico,
.side-row:has(.side-link:focus-visible) .side-ico { color: var(--g-700); }
/* the row you are on wears the chip at rest, so its glyph sits at the
   hover step too — the state is one thing, drawn the same way throughout */
.side-row.is-current .side-ico,
.side-row.is-here .side-ico { color: var(--g-700); }
/* ...and when a section further down claims "here", the glyph goes back
   with the rest of the row (this one outweighs both) */
.side-scroll.has-here .side-row.is-current .side-ico { color: var(--g-500); }
.side-scroll.has-here .side-row.is-current:hover .side-ico { color: var(--g-700); }
.side-ico > svg,
.side-ico > img { display: block; width: 100%; height: 100%; }
/* ---- on hover the glyph REDRAWS itself — the chapter rails' pen, which
   retired with the rails, now serving the tree that replaced them. The
   cascade is tighter than the rails' .1/.2/.3 because a 30px row wants a
   quicker beat than a 160px illustration did. ---- */
@keyframes side-redraw {
  from { stroke-dashoffset: 1; }
  to   { stroke-dashoffset: 0; }
}
.side-row:hover .side-ico svg :is(path, circle, rect, line, ellipse, polyline) {
  stroke-dasharray: 1;
  animation: side-redraw 0.62s var(--ease-draw) both;
}
.side-row:hover .side-ico svg > :nth-child(2) { animation-delay: 0.08s; }
.side-row:hover .side-ico svg > :nth-child(3) { animation-delay: 0.16s; }
.side-row:hover .side-ico svg > :nth-child(4) { animation-delay: 0.24s; }

.side-text {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.side-meta {
  flex: none;
  font-size: 11.5px;
  color: var(--shell-dim);
  font-variant-numeric: tabular-nums;
}

/* ---- the disclosure twisty, at the row's TRAILING edge.
   In front of the glyph it was an indent by another name: Work's briefcase
   sat 21px right of Home's house for no reason a reader could see, and the
   one row that could disclose anything was the one row out of line. Here it
   costs the left edge nothing, and it lands where a disclosure control is
   reached for anyway — the far side of the thing it opens. `order` and not
   DOM order, because the button must stay a SIBLING of the link (see
   rowHTML in js/shell.js: a button inside an anchor gets re-parented). ---- */
.side-tw {
  order: 2;
  margin-right: 8px;
  flex: none;
  width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 3px;
  color: var(--g-350);
  cursor: pointer;
  transition: color var(--t-micro) ease, rotate var(--t-micro) var(--ease-glide);
}
.side-tw[aria-expanded='true'] { rotate: 90deg; }
.side-tw:hover { color: var(--ink); }
/* a row with nothing to disclose holds no gutter open — every leaf row
   starts flush with its group's label */
.side-tw.is-empty { display: none; }
.side-tw svg { display: block; width: 13px; height: 13px; }

/* The children hang off nothing. They used to hang off a hairline with a
   13px margin — the .about-divider idiom carried into the rail — and that
   hairline was also the rail the reading thread rode. Both are gone: the
   thread only ever inked a .side-kids belonging to the CURRENT row, and the
   only row with children is Work, which is never current while you are
   reading a case study (the case row is, and Work is its trail). It drew
   itself perhaps never. The indent it justified was real every day.

   js/shell.js still writes --p here on shell:progress, harmlessly — left
   alone so the four pages that dispatch it need no edit, and so the thread
   has somewhere to come back to if nesting ever earns a gutter again. */
.side-kids[hidden] { display: none; }

/* ...except for a small step in. The disclosure alone said "nested" only
   while the twisty was on screen; with Work closed by default, a visitor
   who opens it sees three rows appear in a column that is otherwise flat
   and has to infer the relationship from the animation. 12px — half the
   glyph gutter, so the case studies read as belonging to Work without
   starting a second left edge you can see from across the column. */
.side-kids .side-row { padding-left: 22px; }

/* ---- the foot: status, and only status now (the ask chip moved up
   under the name) ---- */
.side-foot {
  flex: none;
  padding-top: 8px;
}

/* ---- NOW (see statusHTML in js/shell.js for why it lives down here).
   Two rows of plain text, no frame: the rail gave up boxes altogether —
   the letterhead at its head is type on the bone, and the foot matches.
   The label carries the same 11px grey the nav groups use, so the block
   reads as one more section of the rail rather than a widget bolted on.
   It is the last thing in the column now, so it carries no bottom margin —
   the rail's own 8px floor is the gap. */
.side-status { margin-bottom: 0; }
.side-status .side-label { margin-top: 0; }

.st-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 8px;
  text-decoration: none;
  transition: background-color var(--t-micro) ease;
}
.st-lines {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
/* Both entries take the SAME two-line shape — a lead and a quieter second
   line — so the block reads as one list rather than a row plus a footnote,
   and neither string has to wrap at the rail's narrow widths.
   The leads speak the rail's PRIMARY voice (13.5 / --g-900, the same as
   every nav row and the name): they used to sit at 12.5 mid-grey, and
   against the letterhead the whole foot read as the weak end of a column
   that was supposed to be one column. Hierarchy inside the foot is the
   subs' job. */
.st-lead,
.st-sub {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--t-micro) ease;
}
.st-lead { font-size: 13.5px; line-height: 17px; color: var(--g-900); }
.st-sub { font-size: 11px; line-height: 13px; color: var(--shell-dim); }

.st-row:hover { background: rgba(var(--ink-rgb), 0.05); }
.st-row:hover .st-lead { color: var(--ink); }

/* The markers carry the tense. FILLED is what is true right now; the same
   dot HOLLOW is the thing that has not happened yet. One shape, two states
   — cheaper than a second glyph, and it is legible at 6px in a way a second
   glyph would not be. Both hold still, in the same grey as the place dot in
   the chrome (.now-dot): a rail that is already carrying an accent per case
   study does not need a blinking orange light in its foot. */
.st-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--g-250);
}
.st-row.is-ahead .st-dot {
  background: none;
  box-shadow: inset 0 0 0 1.5px var(--g-250);
  transition: box-shadow var(--t-micro) ease;
}
.st-row.is-ahead:hover .st-dot { box-shadow: inset 0 0 0 1.5px var(--ink); }
/* ============================================================
   The letterhead — identity and the ask, one typographic object

   Four shapes got us here. A 42px outlined lozenge at the foot of the
   rail; a bare field under the name; then one filled chip holding both
   — paper on the rail's bone — and the chip is what taught the rule:
   it was the ONLY filled surface in a column that is otherwise pure
   type, and one box in a rail of typography does not read as "the
   important thing", it reads as a widget from another site. Sore
   thumb, verbatim.

   So no surface at all, and the rail speaks one material everywhere.
   Under the name sits the blank: ONE asterisk — the AI's mark, worn
   exactly once — then the rotating question on a dotted rule. The dots
   are the site's construction line (the globe's grid, the spark's
   orbit) doing what dots do on paper: marking the blank you are
   invited to write in. Proximity ties the halves; a twin asterisk on
   the name tried to say it too and read as a duplicate.

   The hover is the pencil being INKED, and the mark being STIRRED. A
   solid line draws itself left→right over the dots on the pen's own
   curve, the question steps up a grey, and the asterisk TURNS — its
   own verb, not the glyph redraw the rows do. It also advances one
   notch per question (js/shell.js banks --ask-spin), so the mark and
   the question move on the same beat, like the asterisk is stirring
   the next one up.

   (Still no colour to spend. --orange is black on this site and
   --ember is spoken for — the blank does its inviting in ink.)
   ============================================================ */
.ask-chip {
  flex: none;
  /* 10px here + the rail's own 8 lands the name on the exact left edge
     the group labels and row glyphs share — the letterhead joins the
     column's one text line instead of floating in its own box. 14 on
     top so the block clears the strip's floor rather than wedging
     under it; 12 below, then the first label's own 6. */
  margin: 14px 10px 12px;
}

.ask-line {
  position: relative;
  /* 22px of line and 8 of ground under it: the padding keeps the dotted
     rule inside the pointer's target, so the blank can be clicked ON the
     blank, not only on the words above it */
  height: 30px;
  padding-bottom: 8px;
  /* the step off the role line — far enough that the question is a second
     thought rather than a third line of the name, close enough that it is
     plainly the same object */
  margin-top: 10px;
  display: flex;
  align-items: center;
  gap: 8px;
  /* POINTER, not text. The field is a facade — a mouse click here never
     types, it opens the sheet — and an I-beam promises a caret that a
     click does not deliver. The arrow-to-hand is the honest story:
     this line is a door that looks like a blank, not a blank. */
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* ---- THE BLANK, and its measure.
   The ruled span is the asterisk plus the writable field, and NOTHING
   else — the ⌘K is a hint about the blank, so it stands outside the
   rule. That alone pulls the line in by the width of the shortcut.

   The cap is the important half. The rule used to run the full width of
   the column: 172px against a name that sets 126 and a role that sets
   128, so the AI block read a third longer than the letterhead it hangs
   from — and dragging the rail out to 420 stretched it into a bar three
   times the name. Type does not stretch, so the blank must not either.
   138 is the letterhead's own measure plus the few px the longest
   question (110 at 13px) needs past it — near enough to read as the
   same column edge, wide enough that no question is ever clipped at the
   default width. It is a MAX, so a rail dragged narrow still hands the
   question everything there is (and is-tight spends the ⌘K first). ---- */
.ask-blank {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  max-width: 138px;
  align-self: stretch;           /* so its foot IS the line's content edge */
  display: flex;
  align-items: center;
  gap: 8px;
}

/* dots at rest, the pencil's line. A gradient rather than an SVG so the
   dash pitch holds at any width the rail is dragged to; 2 on, 5 off is
   the .guide spec the site's construction lines keep. */
.ask-blank::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: repeating-linear-gradient(90deg,
              var(--g-250) 0 2px, transparent 2px 7px);
}
/* …and the ink that draws over it on approach. A dashed line cannot draw
   itself in, so the ink is a second, solid line scaling open from the
   left on the pen's curve — over the dots, exactly on top of them. */
.ask-blank::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  z-index: 1;
  background: var(--orange);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.5s var(--ease-draw);
}
.ask-line:hover .ask-blank::before,
.ask-line:focus-within .ask-blank::before { transform: scaleX(1); }

/* the question steps up a grey while the pen is on the line */
.ask-line:hover .ask-input::placeholder,
.ask-line:focus-within .ask-input::placeholder { color: var(--g-700); }

/* …and the mark TURNS — the AI's own verb. The rows' glyphs redraw
   themselves under the pen; the asterisk is not one of the rows, and
   giving it the same trick made it furniture. It spins instead: half a
   revolution on approach, sprung on the glide curve so it twirls a hair
   past and settles. Six-fold symmetry is what makes this free — every
   60° lands the shape on itself, so the spin reads as life, never as a
   new pose.

   The rotation STACKS on the notch the question-swap banks into
   --ask-spin (js/shell.js): a calc over the same var, so hover and the
   per-question advance can never fight, and letting go unwinds only the
   half-turn the hand added. */
.ask-blank > svg {
  flex: none;
  /* the rows' 18px glyph box, so the question starts on the exact text
     edge Home and Resume start on — one vertical through the column */
  width: 18px;
  height: 18px;
  color: var(--orange);
  rotate: var(--ask-spin, 0deg);
  transition: rotate 0.55s var(--ease-glide);
}
.ask-line:hover .ask-blank > svg,
.ask-line:focus-within .ask-blank > svg { rotate: calc(var(--ask-spin, 0deg) + 180deg); }

.ask-input {
  flex: 1;
  min-width: 0;
  height: 100%;
  padding: 0;
  font: inherit;
  /* the PROVISIONAL voice of the rail's scale: 13, between the rows'
     13.5 and the meta 11 — a sample of input, not a line of the column.
     (13.5 was tried and the longest ASKS string clips at the default
     rail width; the half-step is what keeps every question whole.) */
  font-size: 13px;
  line-height: 1;
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: 0;
  /* pointer with the rest of the line — a mouse press opens the sheet,
     it never types here (the caret below is the keyboard path's) */
  cursor: pointer;
  /* the questions are written to fit the narrowest rail (see ASKS in
     js/shell.js); this is the seatbelt for a question that one day is not,
     so it trails off instead of stopping mid-letter */
  text-overflow: ellipsis;
  /* the field is a facade (js/shell.js wireAI): a caret shows only on the
     keyboard path, in the pause between tabbing in and the first letter —
     the same --orange the sheet's real field writes with (ai.css) */
  caret-color: var(--orange);
}
/* the rail draws its own rule; the browser's focus ring would box a field
   that has deliberately given up its box */
.ask-input:focus { outline: none; }
.ask-input::placeholder {
  color: var(--g-550);
  opacity: 1;                         /* Firefox dims placeholders by default */
}
/* (the .is-swap crossfade retired with the fade itself — the ghost
   writer types and erases the question in place, js/shell.js) */

/* ---- the ghost writer's two instruments (js/shell.js wireAI) ----
   The mirror: an invisible twin of the placeholder-so-far, wearing the
   input's exact type so its width IS the text's width — measured once
   per letter to stand the caret after the last character. Absolute, so
   the flex line never lays it out; pre, so a trailing space measures. */
.ask-mirror {
  position: absolute;
  top: 0;
  left: 0;
  visibility: hidden;
  white-space: pre;
  font-size: 13px;                    /* the input's own voice, exactly */
  pointer-events: none;
}
/* The caret: EMBER, and the only ember the rail ever shows — the
   palette's first sanctioned ember object is the caret that types the
   hero's name (styles.css), and this is the same caret at its second
   desk. It exists only while the writer writes or erases; every hold
   is plain ink. No fade: a typewriter's caret is either at work or
   gone, and the mechanical cut is the honest one. */
.ask-caret {
  position: absolute;
  top: 4px;                           /* centres the 14px bar in the 22px line */
  width: 1.5px;
  height: 14px;
  border-radius: 1px;
  background: var(--ember);
  pointer-events: none;
}
.ask-caret[hidden] { display: none; }
/* the tail: the site's steps(1) blink manner (play.css types this way
   too) — a beat of presence at the end of the sentence, then gone */
@keyframes ask-caret-blink { 50% { opacity: 0; } }
.ask-caret.is-blink { animation: ask-caret-blink 1.06s steps(1) infinite; }

.ask-kbd {
  flex: none;
  font-size: 11px;
  color: var(--shell-dim);            /* the shortcut is furniture, not accent */
  letter-spacing: 0.02em;
}
/* a rail dragged towards its minimum spends the hint on the question —
   see the ResizeObserver in wireAI. ⌘K keeps working either way */
.ask-line.is-tight .ask-kbd { display: none; }

/* ============================================================
   Content card — the page's own 1280×832 stage, untouched
   ============================================================ */
/* ---- the sill is GONE. It was a band of the page's own surface pinned
   across the strip's height, under the chrome but over everything the card
   scrolls, and it stopped page content dead at the strip's floor — which is
   what made the seam under the tab an edge the page ended at, and what made
   the tab's opening show a blank white band instead of the page.

   Without it the card scrolls all the way up behind the strip again. The
   cream (.shell-chrome::before) hides all of that except the tab's own cut,
   so a line of type rises through the tab and leaves at its top edge, and
   the identity lozenge is what keeps the name readable while it passes. ---- */

.shell-card {
  position: relative;
  flex: 1;
  min-width: 0;
  background: var(--bg);
  /* the top-left corner is the only one that is not a viewport edge —
     it softens where the chrome strip meets the rail. NO ring: the drawn
     hairline that used to live here was the seam between the tab and the
     page it opens, and removing it is the point. */
  border-radius: 10px 0 0 0;
}

/* THE one transformed element on the page. Absolute + translate(-50%,-50%)
   rather than grid centring on purpose: a grid container with no explicit
   template sizes its implicit track to max-content (1280px) and overflows
   to the right. Absolute has no such trap, and it keeps main a positioned
   containing block — which nextlevel's .look and every page's absolutely
   placed chrome depend on. */
/* ZOOM, not transform. This is load-bearing: the document is the scroller
   now, and `position: sticky` does not survive a transformed ancestor
   between itself and the scrollport — the pin silently stops pinning and
   just scrolls away. `zoom` scales the same 1280 grid but participates in
   layout, so sticky works, the document reserves the right height on its
   own, and no JS has to reconcile the two. */
.shell-card > main {
  position: relative;
  width: var(--card-w);
  height: var(--card-h);        /* single-screen pages keep a definite stage */
  margin: 0 auto;
  zoom: var(--card-scale, 1);
}
/* pages whose chapters run down the document opt out of the fixed height —
   their stage is as tall as its content and the page scrolls through it */
.shell[data-page="about"] .shell-card > main,
.shell[data-page="pantrypal"] .shell-card > main,
.shell[data-page="nextlevel"] .shell-card > main,
.shell[data-page="work"] .shell-card > main,
.shell[data-page="play"] .shell-card > main,
.shell[data-page="vicino"] .shell-card > main,
.shell[data-page="home"] .shell-card > main { height: auto; }
.shell-card {
  min-height: calc(100vh - var(--shell-chrome-h));
  /* up behind the strip, then padded back down by the same amount: the page
     rests exactly where it always did, but its surface — and anything that
     scrolls up into that band — now exists under the chrome, ready to be
     seen through the tab. The strip's z-index paints over the rest of it. */
  margin-top: calc(-1 * var(--shell-chrome-h));
  padding-top: var(--shell-chrome-h);
}

/* ============================================================
   EMBED (?embed=1 → html.is-embed, set pre-paint) — this document is
   inside the case overlay's iframe on index.html (js/case-overlay.js).
   The page is content there, not a window: the chrome, the rail and the
   seam all stand down, and the card owns the whole frame. shell.js skips
   building them too — this CSS is the no-flash guarantee for the moment
   before scripts run.
   ============================================================ */
html.is-embed .shell-chrome,
html.is-embed .shell-side,
html.is-embed .side-grip { display: none; }
/* The frame reaches up behind the strip (css/case-overlay.css), so this
   document's top 40px lie under the chrome. No negative margin — there is
   no strip in THIS document's flow to pull back past — just the padding,
   which rests the story below the strip while leaving the card's surface
   (and everything that scrolls up into it) behind the tab's opening. */
html.is-embed .shell-card {
  margin-top: 0;
  padding-top: var(--shell-chrome-h);
  min-height: 100vh;
  border-radius: 0;
}
/* the frame's scroll must not chain into the page behind it */
html.is-embed { overscroll-behavior: contain; }

/* ============================================================
   MOBILE (≤700px) — the shell is a desktop conceit. A fake browser
   inside a real one is nonsense, so it dissolves entirely and the
   page goes back to being a plain document under the bottom tab bar.
   The `!important` is belt-and-braces: it guarantees no position:fixed
   descendant is ever containing-block'd to a transformed .shell.
   ============================================================ */
@media (max-width: 700px) {
  .shell {
    position: static;
    inset: auto;
    transform: none !important;
    width: auto;
    height: auto;
    overflow: visible;
    background: var(--bg);
  }
  .shell-chrome,
  .shell-side,
  /* there is no rail to resize down here, so there is nothing to grab */
  .side-grip { display: none; }
  .shell-body { display: block; overflow: visible; }
  .shell-card {
    border-radius: 0;
    overflow: visible;
  }
  /* main is the one scaled element on desktop — here it must be a plain
     block again, or the phone renders a 1280×832 box it cannot scroll */
  .shell-card > main {
    position: static;
    top: auto;
    left: auto;
    width: auto;
    height: auto;
    transform: none;
    overflow: visible;
  }
}
