  /* Every rule in this block is byte-identical in every template that carries it.
     build.mjs copies the file in verbatim, so --check is an exact string compare
     and cannot disagree with itself. Edit assets/shared.inc.css and re-run
     `node build.mjs --write`; never edit a template's copy.

     What is deliberately NOT here: a rule whose declarations differ between
     templates is not shared. The only one left is .sec - 38px between sections on
     the homepage, 34px on the category archive - which DESIGN.md states as a
     deliberate rhythm rather than drift. It lives in each template's own block. */

  /* Archivo, self-hosted. Was two preconnects and a stylesheet from
     fonts.googleapis.com, which makes every visitor's first paint depend on a third
     party a school network or an ad blocker may refuse - and a blocked CDN does not
     degrade loudly, it silently collapses all four weights to Arial. The variable
     file covers 400-700 in one request, so this is fewer round trips than the CDN
     was, not more. Paths are relative to the document; at conversion they become
     get_theme_file_uri(). Subsets are Google's own latin and latin-ext; vietnamese
     was not carried. */
  /* latin */
  @font-face{
    font-family:'Archivo';
    font-style:normal;
    font-weight:400 700;
    font-stretch:100%;
    font-display:swap;
    src:url(fonts/archivo-latin.woff2) format('woff2');
    unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  /* latin-ext */
  @font-face{
    font-family:'Archivo';
    font-style:normal;
    font-weight:400 700;
    font-stretch:100%;
    font-display:swap;
    src:url(fonts/archivo-latin-ext.woff2) format('woff2');
    unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }

  /* Light is the default scheme. Both the bare root and an explicit light stamp
     carry it, so the page reads light whether or not anything has stamped <html>. */
  :root,
  :root[data-theme="light"]{
    --bg:#f5f5f6;
    --surface:#ffffff;
    --surface-2:#ebebed;
    --line:#e0e1e4;
    --line-2:#cfd1d5;
    --ink:#15171b;
    --ink-2:#494e56;
    --ink-3:#676d75;
    --accent:#05533E;
    --accent-ink:#05533E;
    /* Separate token for surfaces carrying white text. In dark the display accent is
       lightened for legibility as ink, which drops it to 3.36:1 as a fill — so fills
       get their own value rather than reusing it. */
    --accent-fill:#05533E;
    --when:#8a5a12;
    --urgent:#bd2f1d;
    --poster:#e7e8ea;
    --harker:#05533E;
    --shadow-1:0 1px 2px rgba(16,18,22,.09);
    --shadow-2:0 6px 18px rgba(16,18,22,.16);
    /* Dialog ground and elevation. Named so a modal stops being three undocumented
       literals - the detector caught only the scrim and passed the two beside it. */
    --scrim:rgba(12,14,17,.55);
    --lift:0 24px 60px rgba(12,14,17,.4);
    --stage-shadow:0 2px 12px rgba(12,14,17,.14);
  }
  :root[data-theme="dark"]{
    --bg:#141619;
    --surface:#1d2025;
    --surface-2:#262a30;
    --line:#2b2f36;
    --line-2:#3a3f47;
    --ink:#f1f2f4;
    --ink-2:#b2b8bf;
    --ink-3:#8b9199;
    --accent:#2f9e73;
    --accent-ink:#43b98a;
    --accent-fill:#1b6b4c;
    --when:#e0a458;
    --urgent:#f0685a;
    --poster:#22262b;
    --harker:#12855f;
    --shadow-1:0 1px 2px rgba(0,0,0,.4);
    --shadow-2:0 8px 22px rgba(0,0,0,.5);
    --scrim:rgba(4,5,7,.66);
    --lift:0 24px 60px rgba(0,0,0,.62);
    --stage-shadow:0 2px 12px rgba(0,0,0,.45);
  }

  /* TYPE RAMP — shared, and identical in every template.
     Named because the sizes were being picked by eye, and picked by eye they drift:
     the category template landed four values at 10-10.5px on its first pass, under
     the floor this project had already had fixed once. --t-label IS the floor.
     11.5px is deliberately absent. It was the most-used size in this build, appeared
     in no design document, and every one of its five sites was a count, a meta line
     or footer prose — so the counts and meta joined the label step and the footer
     moved up to --t-note, which is prose meant to be read rather than a label.
     body stays 14px as the document's inherited default; it is a base, not a step. */
  :root{
    --t-label:11px;   /* uppercase micro-labels, counts, card and row meta. THE FLOOR. */
    --t-ui:12px;      /* buttons, chips, tabs */
    --t-note:12.5px;  /* secondary prose, meta values, the footer */
    --t-row:13px;     /* card and row titles */
    --t-input:13.5px; /* the search field, at its documented 40px height */
    --t-sub:14.5px;   /* school-year headings */
    --t-sec:17px;     /* section headings, dialog title */
    --t-page:26px;    /* the page's own title */
  }

  *{box-sizing:border-box}
  html,html[data-theme="light"]{color-scheme:light}
  html[data-theme="dark"]{color-scheme:dark}
  body{
    margin:0;background:var(--bg);color:var(--ink);
    font-family:"Archivo","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;
    font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;
  }
  svg{display:block}
  a{color:inherit;text-decoration:none;text-underline-offset:2px}
  ::selection{background:var(--accent-fill);color:#fff}
  /* the last unthemed browser surfaces: two scroll regions live inside the modal */
  html{scrollbar-color:var(--line-2) transparent;scrollbar-width:thin}
  .mstage,.mmeta{scrollbar-color:var(--line-2) transparent;scrollbar-width:thin}
  .mstage::-webkit-scrollbar,.mmeta::-webkit-scrollbar{width:10px;height:10px}
  .mstage::-webkit-scrollbar-thumb,.mmeta::-webkit-scrollbar-thumb{
    background:var(--line-2);border-radius:5px}
  .mstage::-webkit-scrollbar-track,.mmeta::-webkit-scrollbar-track{background:transparent}
  input,select{caret-color:var(--accent-ink);accent-color:var(--accent-fill)}

  .wrap{max-width:1640px;margin:0 auto;padding:0 24px 90px}

  /* 27 chips and a toolbar sit before the first tile; keyboard users get a way past. */
  /* fixed, not absolute: focused while scrolled, an absolute one lands at document
     top and is never seen */
  .skip{
    position:fixed;left:-9999px;top:0;z-index:60;
    background:var(--accent-fill);color:#fff;border-radius:0 0 5px 0;
    padding:11px 18px;font-size:var(--t-row);font-weight:600;
  }
  .skip:focus{left:0;outline:2px solid var(--accent);outline-offset:-2px}

  .modes{display:flex;border:1px solid var(--line-2);border-radius:5px;overflow:hidden;background:var(--surface)}
  .modes button{
    background:none;border:0;cursor:pointer;padding:0 16px;min-height:44px;font:inherit;
    font-size:var(--t-ui);font-weight:600;line-height:1;color:var(--ink-3);
    transition:background .14s ease,color .14s ease;
  }
  .modes button:hover{color:var(--ink)}
  .modes button[aria-pressed="true"]{background:var(--accent-fill);color:#fff}
  /* offset +2, not -2. Drawn inside, the ring sits on the pressed button's own fill,
     and in light theme --accent and --accent-fill are the same #05533E — a focus
     indicator at 1.00:1, which is a WCAG 1.4.11 failure on the one control with a
     persistent active state. Outside, it lands on page background at 8.33:1, which
     is what the other eleven rings in this file already do. */
  .modes button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

  /* ---------- search ---------- */
  .toolbar{
    display:flex;align-items:center;gap:10px;flex-wrap:wrap;
    padding:16px 0 14px;margin:16px 0 0;border-bottom:1px solid var(--line);
  }
  /* align-items:center, not the default stretch: the submit is 44px and the input is
     an explicit 40px, so stretch top-aligns the field and leaves 4px of dead space,
     and .mag (absolute, top:50%) then centres on the 44px line rather than the field. */
  .searchbox{position:relative;display:flex;align-items:center;gap:8px;flex:1 1 340px;max-width:520px;margin:0}
  .searchbox svg.mag{
    position:absolute;left:12px;top:50%;transform:translateY(-50%);
    width:15px;height:15px;color:var(--ink-3);pointer-events:none;
  }
  .searchbox input{
    flex:1;min-width:0;height:40px;border:1px solid var(--line-2);border-radius:5px;
    background:var(--surface);color:var(--ink);font:inherit;font-size:var(--t-input);padding:0 12px 0 35px;
  }
  .searchbox input::placeholder{color:var(--ink-3)}
  .searchbox input:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:var(--accent)}
  .searchbox button{
    border:1px solid var(--accent-fill);background:var(--accent-fill);color:#fff;border-radius:5px;
    font:inherit;font-size:var(--t-row);font-weight:600;padding:0 18px;min-height:44px;cursor:pointer;
  }
  .searchbox button:hover{filter:brightness(1.12)}
  .searchbox button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .bandhead{display:flex;align-items:center;gap:11px;margin:0 0 12px}
  .bandhead .rule{flex:1;height:1px;background:var(--line)}
  .bandhead .n{font-size:var(--t-label);color:var(--ink-3);font-variant-numeric:tabular-nums}

  /* ---------- poster grid ---------- */
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(158px,1fr));gap:22px 16px}

  .card{position:relative;display:flex;flex-direction:column;min-width:0}
  .cardlink{display:flex;flex-direction:column;gap:9px;min-width:0;cursor:pointer;color:inherit}

  .poster{
    position:relative;aspect-ratio:2/3;border-radius:5px;overflow:hidden;
    background:var(--poster);box-shadow:var(--shadow-1);
    transition:transform .22s cubic-bezier(.16,1,.3,1),box-shadow .22s cubic-bezier(.16,1,.3,1);
  }
  .card:hover .poster{transform:translateY(-3px) scale(1.012);box-shadow:var(--shadow-2)}
  .cardlink:focus-visible{outline:none}
  .cardlink:focus-visible .poster{outline:2px solid var(--accent);outline-offset:2px}

  .poster iframe{
    position:absolute;top:0;left:0;width:640px;height:960px;border:0;
    transform-origin:top left;background:#fff;opacity:0;transition:opacity .35s ease;
  }
  .poster iframe.ready{opacity:1}

  .ph{position:absolute;inset:0;display:flex;flex-direction:column;background:var(--surface)}
  .ph .bar{height:13%;background:var(--harker);flex:none}
  .ph .t{
    padding:11px 10px;font-size:var(--t-label);line-height:1.4;font-weight:600;color:var(--ink-2);
    display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden;
  }
  .phfail{
    margin-top:auto;padding:9px 11px;font-size:var(--t-label);font-weight:600;
    color:var(--urgent);border-top:1px solid var(--line);
  }

  /* .postwrap is a plain wrapper now. It once gave the grouped-tile stack a
     positioning context the height of the poster; with the stack gone it carries no
     position and no z-index, and neither do .poster or .meta. */
  .postwrap{min-width:0}
  /* NO stack layers. Four attempts lived here - 5px/10px with a shadow, then
     3px/6px flat - and Nick reported a gray bar beside an unrelated card every
     time, at a different grid position each time. The shadow was never the whole
     cause: the layers ARE two light-gray rectangles, and any width that reads as a
     page edge also reads as a stray bar in a 16px gutter. There is nowhere else to
     put them either - inside the poster they cover the artwork, below it they hit
     the metadata, and insetting the poster makes grouped tiles a different size
     from single ones.
     So the group is expressed in words instead: .meta prints "three versions" in
     Planning Ochre, which a screen reader gets too. is-group stays on the element
     as a hook for anything that needs to know, and styles nothing. */

  .meta{min-width:0}
  .when{
    font-size:var(--t-label);font-weight:700;letter-spacing:.13em;text-transform:uppercase;
    color:var(--when);margin:0 0 4px;font-variant-numeric:tabular-nums;
  }
  .when.soon{color:var(--urgent)}
  .meta .cardttl{
    margin:0 0 3px;font-size:var(--t-row);font-weight:600;line-height:1.34;color:var(--ink);
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
  }
  .card:hover .meta .cardttl{color:var(--accent-ink)}
  .meta .cat{font-size:var(--t-label);color:var(--ink-3)}
  .meta .cat b{font-weight:600;color:var(--ink-2)}
  .meta .vers{color:var(--when);font-weight:600}

  .foot{
    margin:56px 0 0;padding:20px 0 0;border-top:1px solid var(--line);
    font-size:var(--t-note);color:var(--ink-3);line-height:1.7;
  }
  .footnotes{display:flex;gap:30px;flex-wrap:wrap}
  /* The toggle lives here, not in the header. It served 1,461 permanently-light HTML
     emails from the best real estate on the page - directly under the h1, above search,
     and the second tab stop. In the footer it keeps the feature and costs the top of
     the page nothing. Its own row, so it does not read as a fourth footer note. */
  /* The toggle lives here, not in the header, where it served 1,461 permanently-light
     emails from the best real estate on the page: under the h1, above search, second
     tab stop. It sits BELOW the notes because it is a control, not a fourth note, and
     `width:max-content` keeps the bordered shell tight to its two buttons - left to
     stretch, the pill spans the column with empty box beside them.

     It is a sibling of .footnotes rather than a flex item among the notes. Making the
     notes and the toggle share one flex container and forcing a row break with
     `flex-basis:100%` worked on a phone and silently did not on a desktop, where the
     `max-content` clamp let it fit on the notes' line as a fourth column. */
  .foot .modes{width:max-content;margin:18px 0 0}
  .foot p{margin:0;max-width:56ch}
  .foot b{color:var(--ink-2);font-weight:600}

  /* ---------- preview modal ---------- */
  .scrim{position:fixed;inset:0;z-index:40;background:var(--scrim);opacity:0;transition:opacity .2s ease}
  .scrim.open{opacity:1}
  .scrim[hidden]{display:none}

  .modal{
    position:fixed;z-index:41;top:50%;left:50%;transform:translate(-50%,-48%);
    width:min(1120px,94vw);height:min(880px,92vh);
    /* Elevation, not both. A hairline edge under a 60px shadow is the generated-UI
       tell; over a scrim the shadow alone defines the dialog. */
    background:var(--bg);border:0;border-radius:8px;
    box-shadow:var(--lift);
    display:flex;flex-direction:column;overflow:hidden;
    opacity:0;transition:opacity .2s ease,transform .2s cubic-bezier(.16,1,.3,1);
  }
  .modal.open{opacity:1;transform:translate(-50%,-50%)}
  .modal[hidden]{display:none}

  .mhead{display:flex;align-items:flex-start;gap:14px;padding:16px 18px 13px;border-bottom:1px solid var(--line);background:var(--surface)}
  /* on the documented step, same as a section heading */
  .mhead h2{margin:0 0 3px;font-size:var(--t-sec);font-weight:700;line-height:1.3;letter-spacing:-.015em}
  .msub{font-size:var(--t-ui);color:var(--ink-3)}
  .mclose{
    margin-left:auto;flex:none;width:44px;height:44px;display:grid;place-items:center;
    background:none;border:1px solid var(--line-2);border-radius:5px;color:var(--ink-2);cursor:pointer;
    transition:background .14s ease,color .14s ease;
  }
  .mclose:hover{background:var(--surface-2);color:var(--ink)}
  .mclose:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .mclose svg{width:14px;height:14px}

  .mtabs{display:flex;align-items:center;gap:6px;padding:10px 18px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}
  .mtabs[hidden]{display:none}
  .mtablist{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
  .mtabs .lbl{font-size:var(--t-label);font-weight:700;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3);margin-right:4px}
  .mtab{
    border:1px solid var(--line-2);border-radius:5px;background:var(--bg);color:var(--ink-2);
    font:inherit;font-size:var(--t-ui);font-weight:600;padding:0 12px;min-height:44px;cursor:pointer;
    transition:background .14s ease,color .14s ease,border-color .14s ease;
  }
  .mtab:hover{border-color:var(--ink-3);color:var(--ink)}
  .mtab[aria-selected="true"]{background:var(--accent-fill);border-color:var(--accent-fill);color:#fff}
  .mtab:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .mstep{width:44px;height:44px;display:grid;place-items:center;border-radius:5px;background:var(--bg);border:1px solid var(--line-2);color:var(--ink-2);cursor:pointer}
  .mstep:hover:not(:disabled){background:var(--accent-fill);border-color:var(--accent-fill);color:#fff}
  .mstep:disabled{opacity:.3;cursor:default}
  .mstep svg{width:12px;height:12px}
  .mstep:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

  .mbody{flex:1;display:flex;min-height:0;overflow:hidden}
  .mstage{flex:1;min-width:0;background:var(--surface-2);display:flex;justify-content:center;overflow:auto;padding:16px}
  /* #mPanel carries tabindex="0" so a keyboard user can scroll the preview, which
     makes it a control, which means it takes the documented ring like every other. */
  .mstage:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
  .mstage iframe{width:640px;max-width:100%;height:1400px;border:0;background:#fff;box-shadow:var(--stage-shadow);flex:none}
  .mmeta{width:270px;flex:none;margin:0;padding:16px 18px;overflow:auto;border-left:1px solid var(--line);background:var(--surface)}
  .mrow{padding:9px 0;border-bottom:1px solid var(--line)}
  .mrow:last-child{border-bottom:0}
  .mrow dt{margin:0 0 3px;font-size:var(--t-label);font-weight:700;letter-spacing:.11em;text-transform:uppercase;color:var(--ink-3)}
  .mrow dd{margin:0;font-size:var(--t-note);color:var(--ink);word-break:break-word}
  .mrow dd a{color:var(--accent-ink);font-weight:600}
  .mrow dd a:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:5px}
  .mrow dd a:hover{text-decoration:underline}

  .mfoot{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:12px 18px;border-top:1px solid var(--line);background:var(--surface)}
  .btn{
    border-radius:5px;padding:0 15px;min-height:44px;font:inherit;font-size:var(--t-note);font-weight:600;
    cursor:pointer;border:1px solid;display:inline-flex;align-items:center;gap:7px;
    transition:background .14s ease,color .14s ease;
  }
  .btn svg{width:14px;height:14px}
  .btn.primary{background:var(--accent-fill);border-color:var(--accent-fill);color:#fff}
  .btn.primary:hover{filter:brightness(1.12)}
  .btn.ghost{background:var(--bg);border-color:var(--line-2);color:var(--ink)}
  .btn.ghost:hover{background:var(--surface-2)}
  .btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .btn:disabled{opacity:.5;cursor:default}
  .mstatus{font-size:var(--t-ui);color:var(--ink-3)}
  .mstatus.ok{color:var(--accent-ink);font-weight:600}
  .mstatus.bad{color:var(--urgent);font-weight:600}

  @media (prefers-reduced-motion:reduce){
    *,*::before,*::after{
      transition-duration:.01ms !important;animation-duration:.01ms !important;
      animation-iteration-count:1 !important;scroll-behavior:auto !important;
    }
    .card:hover .poster{transform:none}
    .poster iframe{transition:none;opacity:1}
  }

  @media (max-width:860px){
    .mbody{flex-direction:column}
    .mmeta{width:auto;border-left:0;border-top:1px solid var(--line);max-height:38%}
    .mstage{padding:10px}
  }
  @media (max-width:760px){
    .wrap{padding:0 14px 64px}
    .grid{grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:18px 12px}
    .searchbox{flex-basis:100%;max-width:none}
  }
  @media (max-width:460px){
    .grid{grid-template-columns:repeat(2,1fr)}
  }
