  /* dark is the default here; light is the alternate */
  :root{
    --bg:#0b0c0e; --raised:#121418; --ink:#f2f3f5; --muted:#8a9099;
    --rule:#23262b; --accent:#e8564a; --dot:rgba(255,255,255,.07);
    --chip:#15181c; --rule-strong:#2e3239; --scrim-rgb:11,12,14;
    --key:#f09a91; --key-bg:#241512;
    /* the stack entries are the only raised surfaces on the site, so the
       shadow lives here rather than in a general scale */
    --emboss:inset 0 1px 0 rgba(255,255,255,.055), 0 1px 2px rgba(0,0,0,.45), 0 4px 10px rgba(0,0,0,.3);
    --emboss-hover:inset 0 1px 0 rgba(255,255,255,.085), 0 2px 4px rgba(0,0,0,.5), 0 10px 22px rgba(0,0,0,.42);
    /* one radius scale for the whole page */
    --r-sm:4px; --r-md:6px; --r-lg:9px;
  }
  @media (prefers-color-scheme:light){
    :root:not([data-theme="dark"]){
      --bg:#f7f7f5; --raised:#ffffff; --ink:#111316; --muted:#5f666e;
      --rule:#e2e2de; --accent:#c0392f; --dot:rgba(0,0,0,.07); --chip:#eeeeea;
      --rule-strong:#cbcbc5; --key:#a33227; --key-bg:#f7e7e4; --scrim-rgb:247,247,245;
      --emboss:inset 0 1px 0 rgba(255,255,255,.85), 0 1px 2px rgba(0,0,0,.07), 0 3px 8px rgba(0,0,0,.05);
      --emboss-hover:inset 0 1px 0 #fff, 0 2px 5px rgba(0,0,0,.1), 0 10px 20px rgba(0,0,0,.09);
    }
  }
  :root[data-theme="light"]{
    --bg:#f7f7f5; --raised:#ffffff; --ink:#111316; --muted:#5f666e;
    --rule:#e2e2de; --accent:#c0392f; --dot:rgba(0,0,0,.07); --chip:#eeeeea;
    --rule-strong:#cbcbc5; --key:#a33227; --key-bg:#f7e7e4; --scrim-rgb:247,247,245;
    --emboss:inset 0 1px 0 rgba(255,255,255,.85), 0 1px 2px rgba(0,0,0,.07), 0 3px 8px rgba(0,0,0,.05);
    --emboss-hover:inset 0 1px 0 #fff, 0 2px 5px rgba(0,0,0,.1), 0 10px 20px rgba(0,0,0,.09);
  }

  *{box-sizing:border-box}
  html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
  @media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
  body{
    margin:0;background:var(--bg);color:var(--ink);
    font:400 1rem/1.62 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  }
  .wrap{max-width:58rem;margin:0 auto;padding-left:1.25rem;padding-right:1.25rem}
  a{color:inherit}
  a:focus-visible,button:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
  .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Courier New",monospace}
  code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em}

  .sr-only{
    position:absolute;width:1px;height:1px;padding:0;margin:-1px;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;
  }
  /* Theme crossfade.
     The transition is applied only while .theme-anim is on <html>, added on
     toggle and removed once it finishes. Transitioning colours permanently
     would make the first paint fade in and slow ordinary hovers. */
  .theme-anim, .theme-anim *{
    transition:background-color .28s ease, color .28s ease,
               border-color .28s ease, fill .28s ease !important;
  }
  #theme-icon{display:inline-block;line-height:1}
  #theme-icon.swap{animation:icon-swap .34s ease}
  @keyframes icon-swap{
    from{transform:rotate(-140deg) scale(.6);opacity:0}
    to{transform:rotate(0) scale(1);opacity:1}
  }
  @media (prefers-reduced-motion:reduce){
    .theme-anim, .theme-anim *{transition:none !important}
    #theme-icon.swap{animation:none}
  }
  /* Theme reveal, for browsers with view transitions (site.js falls back to
     the crossfade above otherwise). The old page stays still underneath and
     the new one is uncovered by a circle from the toggle; site.js sets its
     centre and the radius that just reaches the farthest corner. The
     browser's default crossfade is switched off, or both pages would fade
     while the circle grew. .45s rather than the usual 1s+: it is a flourish
     on a toggle, not an event. */
  ::view-transition-old(root),
  ::view-transition-new(root){animation:none;mix-blend-mode:normal}
  ::view-transition-new(root){animation:theme-reveal .45s ease-in-out}
  @keyframes theme-reveal{
    from{clip-path:circle(0 at var(--vt-x) var(--vt-y))}
    to{clip-path:circle(var(--vt-r) at var(--vt-x) var(--vt-y))}
  }

  .skip{position:absolute;left:-9999px}
  .skip:focus{left:1rem;top:1rem;z-index:20;background:var(--raised);padding:.6rem 1rem;border:1px solid var(--rule)}

  /* ---------- header ---------- */
  header{position:sticky;top:0;z-index:10;background:var(--bg);border-bottom:1px solid var(--rule)}
  .bar{display:flex;align-items:center;gap:1rem;padding-top:.85rem;padding-bottom:.85rem;flex-wrap:wrap}
  .brand{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.9375rem;font-weight:700;letter-spacing:.02em;text-decoration:none;margin-right:auto;
    color:var(--muted);
  }
  /* the name behaves like the nav: --muted at rest, --ink on hover */
  .brand:hover{color:var(--ink)}
  nav ul{display:flex;gap:1.15rem;list-style:none;margin:0;padding:0;flex-wrap:wrap}
  nav a{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.75rem;letter-spacing:.1em;text-transform:lowercase;
    color:var(--muted);text-decoration:none;
  }
  nav a:hover{color:var(--ink)}
  #theme-toggle{
    border:1px solid var(--rule);background:transparent;color:var(--ink);
    width:1.75rem;height:1.75rem;cursor:pointer;font-size:.8rem;line-height:1;
    border-radius:var(--r-sm);
    display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;
  }
  @media (max-width:34rem){
    nav{order:3;width:100%;border-top:1px solid var(--rule);padding-top:.6rem}
    /* space-between spreads the five links across the full row; the gap is only
       the minimum, kept small enough that they still fit on one line at 320px,
       where 0.8rem was pushing contact onto a second row */
    nav ul{gap:.4rem;justify-content:space-between}
    nav a{font-size:.6875rem;letter-spacing:.06em}
  }
  /* six links do not fit one line on the narrowest phones: the labels alone are
     269px inside 280px, leaving 11px for five gaps. One step down of type and
     tracking buys the room back, below the width where 375px devices sit. */
  @media (max-width:21.5rem){
    nav a{font-size:.625rem;letter-spacing:.03em}
  }

  /* ---------- hero ---------- */
  /* #top is the hero itself and the header is sticky, so jumping to it parked
     the header over the top of the photograph and the frame began at the neck.
     The offset matches the header, which lands the page at its true top where
     the whole figure shows. Desktop has a shorter header, so it simply clamps. */
  #top{scroll-margin-top:7rem}
  /* Same reason for the section anchors: the header is sticky, so landing a
     section at the viewport top hid its heading behind it.

     The offset cannot be a fixed number. .bar wraps, so the nav drops onto a
     second line as soon as the labels stop fitting beside the brand, and the
     header goes from 56px to 96px to 109px. That point depends on the labels
     themselves, not on a breakpoint: adding stack moved it. site.js measures
     the header and publishes it here; the value below is the fallback for
     when that script has not run.

     A section also carries 3.25rem of its own top padding. Subtracting less
     than that tucks the remainder behind the header rather than leaving it
     below as dead space, so the clearance between header and heading is 24px
     on phones. Wide screens keep the full header height, where the padding
     is the gap and a taller viewport carries it comfortably. */
  :root{--header-h:56px}
  /* the nav is on its own line below 34rem, which is the one wrap point a
     media query can state; the script corrects the band in between */
  @media (max-width:34rem){ :root{--header-h:109px} }
  section[id]{scroll-margin-top:calc(var(--header-h) - 1.75rem)}
  @media (min-width:44rem){ section[id]{scroll-margin-top:var(--header-h)} }
  .hero{
    position:relative;padding-top:4rem;padding-bottom:3.5rem;
    background-image:radial-gradient(var(--dot) 1px, transparent 1px);
    background-size:14px 14px;
    border-bottom:1px solid var(--rule);
  }
  .avail{
    display:inline-flex;align-items:center;gap:.5rem;
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.6875rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
    border:1px solid var(--rule);background:var(--bg);padding:.35rem .7rem;margin:0 0 1.75rem;
    border-radius:var(--r-md);
  }
  .avail::before{content:"";width:.45rem;height:.45rem;border-radius:50%;background:var(--accent)}
  .herogrid{display:grid;gap:2rem;align-items:start}
  @media (min-width:44rem){
    .herogrid{grid-template-columns:1fr 15rem;gap:2.75rem}
    .portrait{order:2}
  }
  /* The portrait is a background image so each theme fetches only its own
     file. An <img> pair would download both, and <picture> cannot see the
     manual data-theme toggle - only the OS preference. */
  .portrait{
    margin:0;max-width:14rem;width:100%;aspect-ratio:560/697;
    border:1px solid var(--rule);border-radius:var(--r-lg);
    background:#edece8 center/cover no-repeat;
    background-image:url(portrait.jpg);
  }
  @media (prefers-color-scheme:light){
    :root:not([data-theme="dark"]) .portrait{background-image:url(portrait-light.jpg)}
  }
  :root[data-theme="light"] .portrait{background-image:url(portrait-light.jpg)}
  :root[data-theme="dark"] .portrait{background-image:url(portrait.jpg)}

  h1{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    margin:0 0 1.25rem;font-size:clamp(2rem,8vw,3.25rem);font-weight:700;
    line-height:1.05;letter-spacing:-.02em;
  }
  .hero p{margin:0 0 1.1rem;max-width:34rem;color:var(--muted)}
  .hero p strong{color:var(--ink);font-weight:600}
  .herolinks{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.8125rem;margin:1.75rem 0 0;display:flex;flex-wrap:wrap;gap:1.25rem;
  }
  .herolinks a{text-decoration:none;color:var(--muted)}
  .herolinks a:hover{color:var(--accent)}
  .herolinks a::after{content:" \2197";color:var(--accent)}

  /* The icon belongs to the hero links alone: the label carries wide screens,
     and every other .herolinks on the site - all repos on projects.html - stays
     a plain text link rather than being forced into an icon circle. */
  .herolinks .ico{display:none}

  /* Below 44rem the portrait fills the screen and the copy sits on it, centred.
     The photograph's backdrop peaks at 0.99 relative luminance, so light text
     over it unwashed measures about 1.05:1. A wash of 0.85 is what AA needs at
     that peak, and it fades in only under the copy, so the figure above stays
     as it was shot. */
  @media (max-width:43.98rem){
    .hero{
      position:relative;
      /* 7rem is the header above it, so the photograph fills what is left */
      min-height:calc(100vh - 7rem);min-height:calc(100svh - 7rem);
      display:flex;align-items:flex-end;text-align:center;
      padding-top:1.5rem;padding-bottom:2rem;
      background-image:url(portrait-full.jpg);
      background-size:cover;background-position:50% 50%;background-repeat:no-repeat;
    }
    .hero::before{
      content:"";position:absolute;inset:0;pointer-events:none;z-index:1;
      background:linear-gradient(to bottom,
        rgba(var(--scrim-rgb),.10) 0%,
        rgba(var(--scrim-rgb),0) 30%);
    }
    .hero > .wrap{position:relative;width:100%;z-index:2}
    /* sized to the copy, not to a share of the viewport: a percentage cannot
       know how tall the text is */
    .hero > .wrap::before{
      content:"";position:absolute;z-index:-1;pointer-events:none;
      left:0;right:0;top:-6rem;bottom:-2rem;
      background:linear-gradient(to bottom,
        rgba(var(--scrim-rgb),0) 0,
        rgba(var(--scrim-rgb),.86) 6rem,
        rgba(var(--scrim-rgb),.93) 100%);
    }
    .portrait{display:none}
    .herogrid{gap:0}
    .herogrid > div:last-child{display:flex;flex-direction:column;align-items:center}

    /* .hero p is (0,1,1) and outranks a bare .avail, so this has to match it */
    .hero .avail{
      order:1;margin:0 0 1.1rem;
      background:rgba(var(--scrim-rgb),.45);border-color:rgba(255,255,255,.22);
      -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
      color:var(--ink);
    }
    .hero .lede{
      order:2;margin:0 0 1.4rem;max-width:30rem;
      font-size:.9375rem;line-height:1.6;color:var(--ink);
    }
    .hero .lede strong{font-weight:600}
    /* the second paragraph is desktop only */
    .hero .sub{display:none}

    .hero .herolinks{order:3;margin:0 0 1.5rem;gap:.75rem;justify-content:center}
    .hero .herolinks .lbl{display:none}
    .hero .herolinks .ico{display:inline-flex;line-height:0}
    .hero .herolinks a::after{content:none}
    .hero .herolinks a{
      display:inline-flex;align-items:center;justify-content:center;
      width:2.75rem;height:2.75rem;border-radius:50%;color:var(--ink);
      background:rgba(var(--scrim-rgb),.45);border:1px solid rgba(255,255,255,.22);
      -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
    }

    /* family, weight and tracking stay as the desktop h1 sets them; only the
       position and the size change for the splash */
    .hero h1{
      order:4;margin:0;
      font-size:clamp(2rem,11vw,3.25rem);line-height:1.05;
    }
  }
  /* Each theme gets its own frame, as .portrait already does, so only the one
     in use is fetched. A light wash is pale, so its glass needs a dark edge. */
  @media (max-width:43.98rem){
    :root[data-theme="light"] .hero{background-image:url(portrait-full-light.jpg)}
    :root[data-theme="dark"] .hero{background-image:url(portrait-full.jpg)}
    :root[data-theme="light"] .hero .avail,
    :root[data-theme="light"] .hero .herolinks a{border-color:rgba(0,0,0,.18)}
  }
  @media (max-width:43.98rem) and (prefers-color-scheme:light){
    :root:not([data-theme="dark"]) .hero{background-image:url(portrait-full-light.jpg)}
    :root:not([data-theme="dark"]) .hero .avail,
    :root:not([data-theme="dark"]) .hero .herolinks a{border-color:rgba(0,0,0,.18)}
  }
  /* ---------- stat strip ---------- */
  .stats{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--rule)}
  .stat{padding:1.5rem 1rem;border-right:1px solid var(--rule)}
  .stat:last-child{border-right:0}
  .stat b{
    display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:clamp(1.25rem,5vw,1.75rem);font-weight:700;line-height:1;margin-bottom:.4rem;
  }
  .stat span{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.625rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);
  }

  /* ---------- sections ---------- */
  section{padding-top:3.25rem;padding-bottom:3.25rem;border-bottom:1px solid var(--rule)}
  .shead{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin:0 0 1.75rem;flex-wrap:wrap}
  .shead h2,.shead h3{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.8125rem;font-weight:500;letter-spacing:.12em;text-transform:lowercase;
    color:var(--muted);margin:0;
  }
  .shead h2 b,.shead h3 b{color:var(--ink);font-weight:700}
  .shead a{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.6875rem;letter-spacing:.12em;text-transform:uppercase;
    color:var(--muted);text-decoration:none;
  }
  .shead a:hover{color:var(--accent)}

  /* ---------- projects: the result set ---------- */
  .tags{display:flex;flex-wrap:wrap;gap:.4rem;margin:0 0 .9rem}
  .tag{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.5625rem;letter-spacing:.11em;text-transform:uppercase;
    border:1px solid var(--rule);color:var(--muted);padding:.22rem .45rem;
    border-radius:var(--r-sm);white-space:nowrap;
  }
  .tag-hot{border-color:var(--accent);color:var(--accent)}
  .sintro{margin:-.75rem 0 1.5rem;max-width:34rem;color:var(--muted);font-size:.9375rem}

  /* the query is a display of what the section is, not a live console:
     there is no server here, so there is no run control and no timing. */
  .qpane{border:1px solid var(--rule);background:var(--raised);border-radius:var(--r-md);overflow:hidden;margin:0 0 1.5rem}
  .qbar{
    display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;
    border-bottom:1px solid var(--rule);background:var(--chip);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.625rem;letter-spacing:.14em;
    text-transform:uppercase;color:var(--muted);
  }
  .qconn{margin-left:auto;display:inline-flex;align-items:center;gap:.4rem;letter-spacing:.08em;text-transform:none}
  .qconn::before{content:"";width:.4rem;height:.4rem;border-radius:50%;background:var(--accent)}
  .qsql{margin:0;padding:.9rem .75rem;overflow-x:auto}
  .qsql code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8125rem;line-height:1.75;display:block;white-space:pre}
  .kw{color:var(--accent)}
  .str{color:var(--ink)}
  .ln{color:var(--muted);opacity:.6;-webkit-user-select:none;user-select:none}

  .rowhead{display:none}
  @media (min-width:52rem){
    .rowhead{
      display:grid;grid-template-columns:2rem minmax(0,1fr) 13rem 7rem;gap:1rem;
      /* matches the card's 1.1rem padding plus its 1px border, so these
         labels sit over the columns they name */
      padding:.5rem calc(1.1rem + 1px);
      border-top:1px solid var(--rule-strong);border-bottom:1px solid var(--rule-strong);
      font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.5625rem;letter-spacing:.14em;
      text-transform:uppercase;color:var(--muted);
    }
  }
  /* Each project is a card rather than a table row. The result-set framing
     still comes from the query pane above, the column header strip and the
     "two rows returned" line; what changes is that a project is now a surface
     you can pick out at a glance, which is the one thing this section wants.

     Separation is the gap between cards, so the dividing rule goes. */
  .prow{
    position:relative;
    padding:1.25rem 1.1rem;margin:0 0 .9rem;
    border:1px solid var(--rule);border-radius:var(--r-md);
    background:var(--bg);
    box-shadow:var(--emboss);
    transition:transform .18s ease, box-shadow .18s ease;
  }
  .prow:last-of-type{margin-bottom:0}
  /* hover:hover keeps this off touch screens, where :hover sticks after a tap */
  @media (hover:hover){
    .prow:hover{
      /* 1.005, not the stack's 1.02. These cards are five times wider, and the
         same ratio would move them 18px. This matches the stack's absolute
         movement, about 4px, so the two read as one gesture rather than one
         number. */
      transform:scale(1.005);
      box-shadow:var(--emboss-hover);
      z-index:1;
    }
  }
  @media (prefers-reduced-motion:reduce){
    .prow{transition:none}
    .prow:hover{transform:none}
  }
  .ptop{display:grid;gap:.85rem}
  @media (min-width:52rem){
    .ptop{grid-template-columns:2rem minmax(0,1fr) 13rem 7rem;gap:1rem;align-items:start}
  }
  .pn{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.75rem;color:var(--muted);padding-top:.3rem}
  @media (max-width:52rem){ .pn{display:none} }
  .phead h3{margin:0 0 .5rem;font-size:1.25rem;font-weight:650;letter-spacing:-.01em}
  .phead h3 a{text-decoration:none}
  @media (hover:hover){ .phead h3 a:hover{color:var(--accent)} }
  .plede{margin:0;font-size:.9375rem;color:var(--muted)}
  .pstack{margin:0}
  .pctx{margin:0}

  .pcols{display:grid;gap:1.1rem;margin:1.25rem 0 0}
  @media (min-width:52rem){
    .pcols{grid-template-columns:repeat(3,minmax(0,1fr));gap:1.5rem;margin-left:3rem}
    .pcol{padding-left:1rem;border-left:1px solid var(--rule)}
  }
  .pcol-label{
    margin:0 0 .5rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.625rem;
    letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
  }
  .pcol-label--hot{color:var(--accent)}
  /* :not() keeps this off the label, which would otherwise win on specificity */
  .pcol p:not(.pcol-label){margin:0;font-size:.875rem;color:var(--muted);line-height:1.6}
  .pmore{margin:1.1rem 0 0}
  @media (min-width:52rem){ .pmore{margin-left:3rem} }
  .go{
    display:inline-flex;align-items:center;gap:.4rem;
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.75rem;letter-spacing:.1em;
    text-transform:uppercase;color:var(--accent);text-decoration:none;
    /* grows rightward from its own left edge rather than drifting sideways */
    transform-origin:left center;
    transition:transform .16s ease;
  }
  /* the arrow is generated rather than typed, as .herolinks already does, so it
     can move without dragging the label with it */
  .go::after{content:"\2192";transition:transform .16s ease}
  .go--back::after{content:none}
  .go--back::before{content:"\2190";transition:transform .16s ease}
  /* hover:hover keeps this off touch screens, where :hover sticks after a tap.
     1.05 rather than the .btn's 1.025: at 12px that would be half a pixel. */
  @media (hover:hover){
    .go:hover{transform:scale(1.05)}
    .go:hover::after{transform:translateX(.25rem)}
    .go--back:hover::before{transform:translateX(-.25rem)}
  }
  @media (prefers-reduced-motion:reduce){
    .go,.go::after,.go--back::before{transition:none}
    .go:hover,.go:hover::after,.go--back:hover::before{transform:none}
  }
  .pnote{
    margin:1.5rem 0 0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.75rem;color:var(--muted);
  }
  .statusline{
    display:flex;align-items:center;gap:.75rem;margin:1.25rem 0 0;padding-top:.9rem;
    border-top:1px solid var(--rule);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.6875rem;color:var(--muted);
  }
  .statusline::before{content:"";width:.4rem;height:.4rem;border-radius:50%;background:var(--accent);flex-shrink:0}

  /* ---------- entity cards: the schema figure and the stack ---------- */
  .fig{margin:1.5rem 0 1.4rem}
  .figcap{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.625rem;letter-spacing:.14em;text-transform:uppercase;
    color:var(--muted);margin:0 0 .85rem;
  }
  /* Cards in a grid, not a positioned diagram: the page is fluid to 58rem and
     has to survive a 375px phone, so there are no fixed coordinates to break.
     The snowflake is shown by nesting dim_circuit under dim_race, and the joins
     by naming the table each foreign key points at. */
  .erd{display:grid;gap:.7rem}
  .erdcol{display:grid;gap:.7rem;align-content:start}
  @media (min-width:46rem){
    .erd{grid-template-columns:1fr 1fr;gap:.7rem 1.75rem;align-items:start}
  }
  .ent{border:1px solid var(--rule);background:var(--bg);border-radius:var(--r-md)}
  .ent--fact{border-color:var(--accent)}
  .ent--sub{margin-left:1.1rem;border-left:2px solid var(--rule-strong)}
  .entname{
    display:flex;align-items:center;gap:.5rem;
    padding:.45rem .65rem;border-bottom:1px solid var(--rule);
    background:var(--chip);border-radius:var(--r-md) var(--r-md) 0 0;
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.75rem;font-weight:650;
  }
  .entname .kind{margin-left:auto;font-weight:400;font-size:.5625rem;letter-spacing:.14em;color:var(--muted)}
  .ent--fact .entname .kind{color:var(--accent)}
  .ent ul{margin:0;padding:0;list-style:none}
  .ent li{
    display:flex;align-items:center;gap:.5rem;
    padding:.35rem .65rem;border-top:1px solid var(--rule);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.75rem;color:var(--ink);
  }
  .ent li:first-child{border-top:0}
  .key{
    font-size:.5625rem;letter-spacing:.08em;padding:.1rem .3rem;
    border:1px solid var(--rule);color:var(--muted);border-radius:var(--r-sm);
    flex-shrink:0;width:1.7rem;text-align:center;
  }
  .key--pk{background:var(--key-bg);border-color:transparent;color:var(--key)}
  .key--none{border-color:transparent;background:transparent}
  .ent li .ref{margin-left:auto;color:var(--muted);padding-left:.5rem}
  .fignote{margin:.9rem 0 0;font-size:.875rem;color:var(--muted)}

  .dot{width:.35rem;height:.35rem;border-radius:50%;background:var(--muted);flex-shrink:0}
  /* Columns, not grid. Grid gives every card in a row the same row height, so a
     two-item card beside a five-item one left dead space beneath it. Columns let
     each card take only the height it needs and pack the next one under it.
     column-width behaves like the old minmax: as many columns as fit. */
  .stackgrid{columns:9.5rem;column-gap:.7rem}
  .stackgrid .ent{break-inside:avoid;-webkit-column-break-inside:avoid;margin-bottom:.7rem}
  /* The entries lift off the page: a hairline highlight along the top edge for
     the emboss, a shadow beneath for the lift, and a small grow on hover. The
     shadows are theme tokens because a value that reads on near-black is
     invisible on near-white, and the reverse.

     Scoped to .stackgrid like everything else here. .ent is also the schema
     figure on projects.html, which is a diagram and stays flat. */
  .stackgrid .ent{
    position:relative;
    box-shadow:var(--emboss);
    transition:transform .18s ease, box-shadow .18s ease;
  }
  .stackgrid .ent li{transition:background-color .16s ease}
  .stackgrid .dot{transition:background-color .16s ease}
  /* hover:hover keeps this off touch screens, where :hover sticks after a tap */
  @media (hover:hover){
    .stackgrid .ent:hover{
      /* 1.02, not the 1.025 the buttons use: a 169px card grows four times as
         many pixels as a 45px button at the same ratio */
      transform:scale(1.02);
      box-shadow:var(--emboss-hover);
      z-index:1;   /* stay above the neighbouring columns while grown */
    }
    .stackgrid .ent li:hover{background:var(--chip)}
    .stackgrid .ent li:hover .dot{background:var(--ink)}
  }
  @media (prefers-reduced-motion:reduce){
    .stackgrid .ent,.stackgrid .ent li,.stackgrid .dot{transition:none}
    .stackgrid .ent:hover{transform:none}
  }

  .test{border:1px solid var(--accent);background:var(--bg);border-radius:var(--r-md);overflow:hidden}
  .testbar{
    display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;
    padding:.45rem .65rem;border-bottom:1px solid var(--rule);background:var(--chip);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.6875rem;color:var(--muted);
  }
  .fail{
    background:var(--accent);color:var(--bg);font-weight:700;
    font-size:.5625rem;letter-spacing:.12em;padding:.12rem .35rem;border-radius:var(--r-sm);
  }
  table.res{border-collapse:collapse;width:100%;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
  table.res th,table.res td{padding:.45rem .65rem;text-align:left}
  table.res th{
    font-size:.5625rem;letter-spacing:.12em;text-transform:uppercase;
    color:var(--muted);font-weight:400;border-bottom:1px solid var(--rule-strong);
  }
  table.res td{font-size:.8125rem;color:var(--ink)}
  table.res .delta{color:var(--accent)}
  @media (max-width:26rem){
    table.res th,table.res td{padding:.4rem .45rem}
    table.res td{font-size:.75rem}
  }

  /* ---------- case study page ---------- */
  .crumb{
    margin:0 0 1.25rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.75rem;color:var(--muted);letter-spacing:.06em;
  }
  .crumb a{color:var(--muted)}
  /* .pagehead carried no bottom spacing of its own. That was invisible while
     the case studies were plain sections, because .casehead's own 3rem read as
     the gap. Now that each study is a card, the all-repos link sat flush
     against its top border.

     2.5rem: more than the 1.75rem between the cards, so the heading block
     separates more strongly than the cards do from each other, and less than
     the card's own 3rem of internal padding. */
  .pagehead{padding-bottom:2.5rem}
  .casehead{padding-top:3rem;padding-bottom:1rem}
  /* A case-study header can sit on a photo: add .photo to it and give its
     project a background-image below. The scrim is .62 of the page colour, so
     it darkens in dark mode and lightens in light mode. On the F1 photo the
     brightest pixel behind the text (the white kerb, luminance .73) held
     --muted to about 2.1:1 at that strength, so the deck and the labels
     switch to --ink. A scrim strong enough to save --muted (.82 dark, .90
     light) left the photo a ghost. --ink holds 5:1 over the scrim even
     where the photo is pure white (dark theme; light is higher), so it
     passes on any photo and wherever the crop lands.

     isolation:isolate makes the header its own stacking context, so the
     scrim's z-index:0 and the content's z-index:1 cannot interleave with
     anything outside it. The top corners follow the card's. */
  .casehead.photo{
    position:relative;isolation:isolate;
    background-size:cover;background-position:50% 50%;
    border-radius:var(--r-md) var(--r-md) 0 0;
  }
  .casehead.photo::before{
    content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
    border-radius:inherit;
    background:rgba(var(--scrim-rgb),.62);
  }
  .casehead.photo > *{position:relative;z-index:1}
  .casehead.photo .deck,
  .casehead.photo .meta dt{color:var(--ink)}
  /* 66% rather than 50%: the car's centre is 62% across the banner. On a
     phone the header is about 1:2, so cover shows only the middle quarter of
     the image's width and 50% put the car half out of frame. At desktop
     width the header is the banner's own 2.13:1 and the x value does nothing,
     so one value serves every width without a media query. */
  #f1 .casehead{background-image:url(f1-banner.jpg);background-position:66% 50%}
  /* the aisle's vanishing point is centred, so the default 50% keeps it in
     frame at every width */
  #warehouse .casehead{background-image:url(warehouse-banner.jpg)}
  /* Each case study is a card, so the page reads as two projects rather than
     one long scroll. Separation is the gap between them, so the rule that used
     to divide them goes.

     The article takes over the wrapping. Its inner .wrap elements stop
     constraining width and their 1.25rem padding becomes the card's internal
     padding instead. calc(100% - 2.5rem) keeps the site's gutter on either
     side at narrow widths, where the card would otherwise sit flush against
     the screen edge. */
  .caseblock{
    width:calc(100% - 2.5rem);max-width:58rem;margin:0 auto 1.75rem;
    border:1px solid var(--rule);border-radius:var(--r-md);
    background:var(--bg);box-shadow:var(--emboss);
  }
  .caseblock:last-of-type{margin-bottom:0}
  .caseblock .wrap{max-width:none;margin-left:0;margin-right:0}
  /* the closing section's rule would otherwise double up with the card's own
     bottom edge, one hairline above the other */
  .caseblock section:last-of-type{border-bottom:0}

  /* The gutter outside the card and the padding inside it both come out of the
     same narrow screen: at 320px they took 84px of it, dropping the text column
     from 280px to 238px. Tightening both recovers most of that, and only where
     the width is actually scarce. */
  @media (max-width:30rem){
    .caseblock{width:calc(100% - 1.5rem)}
    .caseblock .wrap{padding-left:.9rem;padding-right:.9rem}
  }

  /* No hover grow here, unlike the summary cards on the home page. The F1
     case study is about 2,600px tall: you cannot see both its edges at once,
     so a 4px shift reads as the page twitching rather than as a card lifting,
     and there is nothing to click on the block as a whole. */
  /* the header is sticky, so a project linked to by #id must clear it */
  /* The case studies are linked from the home page, so they land the same way.
     .caseblock has no padding of its own, but .casehead inside it has 3rem,
     which is where the clearance comes from: 20px on a phone. Wide screens
     keep the 4.5rem they already had, which reads correctly there. */
  .caseblock[id]{scroll-margin-top:calc(var(--header-h) - 1.75rem)}
  @media (min-width:44rem){ .caseblock[id]{scroll-margin-top:4.5rem} }
  .caseblock h2{
    margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:clamp(1.5rem,5vw,2.25rem);font-weight:700;line-height:1.1;letter-spacing:-.02em;
  }
  .deck{margin:1.25rem 0 0;max-width:40rem;font-size:1.0625rem;line-height:1.6;color:var(--muted)}
  .cta{
    display:inline-block;margin:1.5rem 0 0;padding:.65rem 1.1rem;
    background:var(--accent);color:var(--bg);text-decoration:none;font-weight:650;
    border-radius:var(--r-sm);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8125rem;
    /* stands alone on the left, so it grows rightward rather than outward */
    transform-origin:left center;
    transition:transform .18s ease;
  }
  /* hover:hover keeps this off touch screens, where :hover sticks after a tap.
     1.025 matches .btn: both are buttons of much the same height, unlike the
     12px .go links, which needed more to read as movement at all. */
  @media (hover:hover){ .cta:hover{transform:scale(1.025)} }
  @media (prefers-reduced-motion:reduce){ .cta{transition:none} .cta:hover{transform:none} }
  .meta{display:grid;gap:1rem;margin:2.25rem 0 0;padding:1.25rem 0;border-top:1px solid var(--rule-strong);border-bottom:1px solid var(--rule)}
  @media (min-width:46rem){ .meta{grid-template-columns:repeat(4,minmax(0,1fr));gap:1.5rem} }
  .meta dt{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.5625rem;letter-spacing:.14em;
    text-transform:uppercase;color:var(--muted);margin:0 0 .45rem;
  }
  .meta dd{margin:0;font-size:.9375rem}
  .caseintro{margin:1.25rem 0 0;max-width:44rem;color:var(--muted);font-size:.9375rem}

  .flow{display:grid;gap:.6rem;margin:1.5rem 0 0}
  /* flex, not grid: grid-auto-columns gave the arrows a full 1fr track each,
     squeezing the five nodes into a ninth of the row apiece. */
  @media (min-width:52rem){
    .flow{display:flex;align-items:stretch;gap:0}
    .node{flex:1 1 0;min-width:0}
  }
  .node{border:1px solid var(--rule);background:var(--raised);border-radius:var(--r-md);padding:.75rem .8rem}
  .node--model{border-color:var(--accent)}
  .node .nlabel{
    margin:0 0 .45rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.5625rem;letter-spacing:.14em;
    text-transform:uppercase;color:var(--muted);
  }
  .node--model .nlabel{color:var(--accent)}
  .node .nname{margin:0 0 .3rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8125rem;font-weight:650}
  .node .nsub{margin:0;font-size:.75rem;color:var(--muted);line-height:1.45}
  .arrow{display:none}
  /* the connector spans the whole gap, so it reads as a join rather than a stub */
  @media (min-width:52rem){
    /* The line and head are inset inside the track so neither touches a card.
       The head is a square rotated 45deg, and rotation throws its corners about
       1.7px outside its own box, so it needs clearance of its own. */
    .arrow{
      display:block;flex:0 0 3rem;align-self:center;position:relative;height:9px;
    }
    .arrow::before{
      content:"";position:absolute;left:5px;right:5px;top:4px;
      height:1px;background:var(--rule-strong);
    }
    .arrow::after{
      content:"";position:absolute;right:5px;top:0;width:8px;height:8px;
      border-top:1px solid var(--rule-strong);border-right:1px solid var(--rule-strong);
      transform:rotate(45deg);
    }
  }
  .orch{
    display:flex;align-items:baseline;gap:.75rem;flex-wrap:wrap;
    margin:.6rem 0 0;padding:.6rem .8rem;border:1px dashed var(--rule-strong);border-radius:var(--r-md);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.75rem;color:var(--muted);
  }
  .orch b{color:var(--muted);font-weight:400;letter-spacing:.14em;text-transform:uppercase;font-size:.5625rem}

  .testgrid{display:grid;gap:1.25rem;margin:1.5rem 0 0}
  @media (min-width:52rem){ .testgrid{grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start} }
  .code{border:1px solid var(--rule);background:var(--raised);border-radius:var(--r-md);overflow:hidden}
  .codebar{
    padding:.45rem .65rem;border-bottom:1px solid var(--rule);background:var(--chip);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.625rem;color:var(--muted);
  }
  .code pre{margin:0;padding:.85rem .65rem;overflow-x:auto}
  .code code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.75rem;line-height:1.7;display:block;white-space:pre}
  .cm{color:var(--muted);opacity:.7}
  .testnote{margin:1rem 0 0;font-size:.9375rem;color:var(--muted)}

  .metrics{display:grid;gap:1.25rem;margin:1.5rem 0 0;padding:1.5rem 0;border-top:1px solid var(--rule-strong);border-bottom:1px solid var(--rule-strong)}
  @media (min-width:46rem){ .metrics{grid-template-columns:repeat(4,minmax(0,1fr));gap:1.5rem} }
  .metric b{
    display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:clamp(1.25rem,4vw,1.75rem);
    font-weight:700;color:var(--accent);letter-spacing:-.02em;margin-bottom:.5rem;
  }
  .metric .mlabel{
    display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.5625rem;letter-spacing:.14em;
    text-transform:uppercase;color:var(--muted);margin-bottom:.5rem;
  }
  .metric span.mtext{display:block;font-size:.8125rem;color:var(--muted);line-height:1.5}
  .backrow{padding-top:2rem;padding-bottom:1rem}

  /* ---------- experience: migration history ---------- */
  /* negative margin + matching padding: the hover tint gets breathing room
     without the text shifting position when it appears */
  .xp{
    display:grid;grid-template-columns:1fr;gap:.2rem;
    padding:1rem .75rem;margin-left:-.75rem;margin-right:-.75rem;
    border-bottom:1px solid var(--rule);
    border-radius:var(--r-md);
    transition:background-color .16s ease;
  }
  @media (min-width:52rem){
    .xp{grid-template-columns:7rem minmax(0,14rem) minmax(0,1fr);gap:1.5rem;align-items:baseline}
  }
  @media (hover:hover){
    .xp:hover{background:var(--chip)}
    .xp:hover .yr{color:var(--ink)}
  }
  @media (prefers-reduced-motion:reduce){ .xp{transition:none} }
  .xp:last-of-type{border-bottom:0}
  .xp .yr{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
    font-size:.75rem;color:var(--muted);letter-spacing:.06em;
    transition:color .16s ease;
  }
  .xp .role{font-weight:650;font-size:.9375rem}
  .xp .org{font-size:.8125rem;color:var(--muted)}
  /* ---------- contact ---------- */
  .contactrow{display:grid;gap:1.5rem;margin:1.5rem 0 0}
  @media (min-width:46rem){
    .contactrow{grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:2rem}
  }
  .cstatement{margin:0;max-width:32rem;font-size:1.25rem;line-height:1.5;color:var(--ink)}
  .cactions{display:flex;flex-wrap:wrap;gap:.6rem;align-items:center}
  .btn{
    display:inline-flex;align-items:center;gap:.6rem;
    padding:.7rem 1.1rem;border:1px solid var(--rule);border-radius:var(--r-sm);
    background:transparent;color:var(--ink);text-decoration:none;
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8125rem;
    transition:border-color .18s ease, color .18s ease, transform .18s ease;
  }
  /* hover:hover keeps this off touch screens, where :hover sticks after a tap.
     transform does not reflow, so the neighbouring button never shifts */
  @media (hover:hover){ .btn:hover{border-color:var(--ink);transform:scale(1.025)} }
  @media (prefers-reduced-motion:reduce){ .btn{transition:none} .btn:hover{transform:none} }
  .btn-icon{display:inline-flex;line-height:0;color:var(--muted)}
  .btn:hover .btn-icon{color:var(--ink)}
  /* the label is fixed-width so swapping it to 'copied' does not shift the row */
  #email-action .btn-label{min-width:4.5rem}
  #email-action.done{border-color:var(--accent);color:var(--accent)}
  #email-action.done .btn-icon{color:var(--accent)}
  footer{
    padding-top:2rem;padding-bottom:3rem;color:var(--muted);
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.6875rem;
    letter-spacing:.1em;text-transform:uppercase;
    display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;
  }
  .social{display:flex;align-items:center;gap:.25rem}
  .social a{
    display:inline-flex;align-items:center;justify-content:center;
    width:2.75rem;height:2.75rem;color:var(--muted);border-radius:var(--r-sm);
    transition:color .16s ease, background-color .16s ease;
  }
  @media (hover:hover){ .social a:hover{color:var(--ink);background:var(--chip)} }
  @media (prefers-reduced-motion:reduce){ .social a{transition:none} }
