/* Written by scripts/20_landing.py. Shared by every page under docs/topic/ and docs/target/.

   The two blocks below are `19_pages.py`'s, character for character, and that is an invariant rather
   than a coincidence: a reader who follows a link from the index to one of these pages must not see the
   palette change under them. `tests/theme_test.py` asserts the three copies are byte-identical, which
   is the only reason it is safe to keep them as copies instead of a shared file.

   The reasoning behind the values -- which accents are lifted and why --onbar inverts between the two
   modes -- is written out once, beside the blocks in `19_pages.py`. It is deliberately not repeated
   here; three copies of a rationale is three things to go stale, and the previous palette's did. */
:root{
  --surface:#090A0D; --plane:#101217; --band:#20242D; --ink:#F7F8FA; --ink2:#D0D5DD;
  --muted:#9BA5B3; --grid:#3A414D; --link:#78B7F4; --bar:#D6A034;
  --good:#5BD5AA; --warn:#EF7D86; --off:#9BA5B3; --onbar:#090A0D;
  --accent-sky:#78B7F4; --accent-mint:#5BD5AA; --accent-gold:#E7B64D;
  --accent-coral:#EF7D86; --accent-violet:#A99AF7;
  /* THE STRUCTURAL HALF OF A THEME. Not colours, so not contrast-bearing, and not declared per mode
     either -- a typeface and a blur radius do not change when the lights go out. Graphite's values are
     the page as it shipped, with one change since: the face is Geist, embedded from `assets/fonts/`
     (see scripts/sitewide.py), so every reader gets the same type rather than whichever sans their
     platform has. No wash, an opaque panel and no backdrop filter. --mono is Geist Mono, for code
     and the build tags. A reader who never opens Settings cannot otherwise tell this block exists.
     --bdf is `none` rather than `blur(0px)` on purpose: a non-none backdrop-filter makes the element a
     containing block and a stacking context, which would quietly re-parent the mascot's speech bubble
     and re-rank the pinned bar for every reader, including the ones who chose nothing. */
  --ui:"Geist","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;
  --mono:"Geist Mono",ui-monospace,"Cascadia Mono",Consolas,"Liberation Mono",monospace;
  --wash:none; --panel:var(--plane); --bdf:none;
}
html[data-theme=light]{
  --surface:#F2F4F7; --plane:#FAFBFC; --band:#E7EAF0; --ink:#14171C; --ink2:#353C47;
  --muted:#596574; --grid:#CAD1DB; --link:#1D5E9E; --bar:#6557C8;
  --good:#187557; --warn:#875A19; --off:#596574; --onbar:#FFFFFF;
  --accent-sky:#1D5E9E; --accent-mint:#187557; --accent-gold:#9A6718;
  --accent-coral:#B6465E; --accent-violet:#6557C8;
}
/* THE THREE OPT-IN THEMES, six blocks, and the pattern is the same for each: `html[data-skin=X]` carries
   the dark values and the structural tokens, `html[data-skin=X][data-theme=light]` carries the light
   ones. Both selectors are one attribute stronger than nothing and the pair is stronger than the light
   block above, so the cascade resolves skin-then-mode without a single `!important`.

   The light block restates all thirteen colours and nothing else. It does not restate --ui or --bdf
   because those are the same in both modes and the skin block above already applies at both, and it
   does restate --panel and --wash because the alpha that reads as frosted over near-black is a smear
   over near-white.

   --panel IS translucent, and it is the one token that can be without giving up the measurement. Every
   panel on this page sits on --surface and is filled from --plane, and the rgba below is --plane's own
   channels: so whatever the compositor produces lies between two backdrops that ARE in the checked set,
   and every text token clears 4.5:1 on both ends of that range. `theme_test.py` asserts the channels
   match --plane rather than trusting this paragraph, because a hand-typed rgba is exactly the kind of
   copy that drifts one digit and stops being a blend of anything. */
html[data-skin=glass]{
  --surface:#070912; --plane:#12141D; --band:#1C1E26; --ink:#F4F7FF; --ink2:#CBD5E8;
  --muted:#95A2BE; --grid:#3D4152; --link:#7CC4FF; --bar:#6FE3C4;
  --good:#86E0A0; --warn:#FF9BA8; --off:#95A2BE; --onbar:#04121A;
  --accent-sky:#7CC4FF; --accent-mint:#6FE3C4; --accent-gold:#F3C46A;
  --accent-coral:#FF9BA8; --accent-violet:#B3A6FF;
  --ui:"Geist","Inter","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;
  --wash:radial-gradient(1100px 520px at 12% -12%,rgba(80,140,255,.22),transparent 70%),
         radial-gradient(900px 480px at 88% 0%,rgba(110,227,196,.14),transparent 72%);
  --panel:rgba(18,20,29,.72); --bdf:blur(14px) saturate(1.25);
}
html[data-skin=glass][data-theme=light]{
  --surface:#EEF2FA; --plane:#F8FAFF; --band:#E2E8F5; --ink:#111726; --ink2:#323B4F;
  --muted:#56627A; --grid:#C3CCDE; --link:#0F5FA6; --bar:#0B6E7A;
  --good:#136B4E; --warn:#8A4A2B; --off:#56627A; --onbar:#FFFFFF;
  --accent-sky:#0F5FA6; --accent-mint:#0B6E7A; --accent-gold:#8A5A00;
  --accent-coral:#A63F52; --accent-violet:#4B44B8;
  --wash:radial-gradient(1100px 520px at 12% -12%,rgba(60,120,255,.14),transparent 70%),
         radial-gradient(900px 480px at 88% 0%,rgba(0,150,130,.10),transparent 72%);
  --panel:rgba(248,250,255,.74);
}
/* Terminal is the one theme that changes the typeface, and that is the whole of it: a monospace stack,
   phosphor greens for the foreground and amber for the action. Amber rather than a brighter green
   because --bar is drawn as text as well as as a fill, and a green action beside the green --good is two
   meanings in one hue -- the mark of a verdict and the colour of a button should not be the same thing. */
html[data-skin=terminal]{
  --surface:#050B0D; --plane:#0A1315; --band:#132220; --ink:#D8F5E6; --ink2:#A8D8C4;
  --muted:#84AC99; --grid:#2D4741; --link:#6FD0FF; --bar:#FFB627;
  --good:#3FE08C; --warn:#FF8B7A; --off:#84AC99; --onbar:#05100B;
  --accent-sky:#6FD0FF; --accent-mint:#3FE08C; --accent-gold:#FFB627;
  --accent-coral:#FF8B7A; --accent-violet:#C2A6FF;
  --ui:"Geist Mono","SF Mono",ui-monospace,"Cascadia Mono",Consolas,"Liberation Mono",monospace;
  --wash:radial-gradient(1200px 620px at 50% -20%,rgba(63,224,140,.12),transparent 70%);
  --panel:rgba(10,19,21,.86); --bdf:blur(3px);
}
html[data-skin=terminal][data-theme=light]{
  --surface:#EEF4EF; --plane:#F9FCF9; --band:#E0EBE3; --ink:#0D1A14; --ink2:#2B3E34;
  --muted:#4F6659; --grid:#BFCFC4; --link:#0C5F8F; --bar:#7A5300;
  --good:#166B45; --warn:#9A3D22; --off:#4F6659; --onbar:#FFFFFF;
  --accent-sky:#0C5F8F; --accent-mint:#166B45; --accent-gold:#7A5300;
  --accent-coral:#9A3D22; --accent-violet:#5A3FA8;
  --wash:radial-gradient(1200px 620px at 50% -20%,rgba(20,140,90,.10),transparent 70%);
  --panel:rgba(249,252,249,.86);
}
/* Prism is the maximal one: five washes at five positions so the page is never one colour, magenta for
   the action, and the deepest blur of the four. It is also where the constraint above shows its teeth --
   the hues are all in the accents and the wash, and the thirteen tokens underneath them are as plain and
   as measured as graphite's. Colour on this page is allowed to be loud in the background and never in
   the contract between ink and its backdrop. */
html[data-skin=prism]{
  --surface:#0B0718; --plane:#17112C; --band:#241A3F; --ink:#FBF7FF; --ink2:#DCD2F0;
  --muted:#ADA0C8; --grid:#4A3A70; --link:#8FD0FF; --bar:#FF9BD2;
  --good:#63E6B8; --warn:#FFB36B; --off:#ADA0C8; --onbar:#14061B;
  --accent-sky:#8FD0FF; --accent-mint:#63E6B8; --accent-gold:#FFD166;
  --accent-coral:#FF9BD2; --accent-violet:#C0A8FF;
  --ui:"Geist","Inter","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;
  --wash:radial-gradient(760px 430px at 6% -8%,rgba(255,155,210,.26),transparent 68%),
         radial-gradient(680px 400px at 96% 2%,rgba(143,208,255,.22),transparent 68%),
         radial-gradient(900px 520px at 50% 108%,rgba(99,230,184,.18),transparent 70%),
         radial-gradient(520px 320px at 78% 54%,rgba(255,209,102,.15),transparent 70%),
         radial-gradient(560px 340px at 18% 62%,rgba(192,168,255,.20),transparent 70%);
  --panel:rgba(23,17,44,.70); --bdf:blur(18px) saturate(1.4);
}
html[data-skin=prism][data-theme=light]{
  --surface:#F3EFFC; --plane:#FDFBFF; --band:#E8E0F8; --ink:#170B26; --ink2:#3A2B52;
  --muted:#5E4F7A; --grid:#C7B9E4; --link:#1A4FB0; --bar:#9A2C7E;
  --good:#0F6B52; --warn:#8A4410; --off:#5E4F7A; --onbar:#FFFFFF;
  --accent-sky:#1A4FB0; --accent-mint:#0F6B52; --accent-gold:#8A4410;
  --accent-coral:#9A2C7E; --accent-violet:#6A3FB8;
  --wash:radial-gradient(760px 430px at 6% -8%,rgba(214,60,150,.16),transparent 68%),
         radial-gradient(680px 400px at 96% 2%,rgba(40,120,220,.14),transparent 68%),
         radial-gradient(900px 520px at 50% 108%,rgba(0,170,120,.12),transparent 70%),
         radial-gradient(520px 320px at 78% 54%,rgba(230,160,20,.10),transparent 70%),
         radial-gradient(560px 340px at 18% 62%,rgba(130,90,230,.13),transparent 70%);
  --panel:rgba(253,251,255,.72);
}
/* THE FOUR LIGHTER-HEARTED ONES, eight more blocks in exactly the pattern above, and held to exactly the
   same arithmetic -- `theme_test.py` runs `audit()` over all sixteen sets. The fun is spent where it is
   free: the wash, the typeface and the accents. None of them moves. Every --wash here is a static
   gradient, so a reader who asked the OS for reduced motion has nothing to ask these to stop doing, and
   none of them sets an --fx-* token on the homepage, which is the only place a theme can animate.

   Sherbet is an ice-cream parlour: blackcurrant and cream, strawberry for the action, and a rounded face
   where the platform has one. `ui-rounded` resolves on Apple platforms only; everywhere else the stack
   falls through to Nunito if installed and then to the page's own Segoe UI, so the shape of the page is
   never at the mercy of a font nobody has. */
html[data-skin=sherbet]{
  --surface:#1E1830; --plane:#272040; --band:#32294F; --ink:#FFF1E6; --ink2:#EEDCE6;
  --muted:#C4B3D0; --grid:#554872; --link:#A8B4FF; --bar:#FF7AA2;
  --good:#5FE0B4; --warn:#FFB36B; --off:#C4B3D0; --onbar:#1A1026;
  --accent-sky:#A8B4FF; --accent-mint:#5FE0B4; --accent-gold:#FFD166;
  --accent-coral:#FF9A8B; --accent-violet:#D0B3FF;
  --ui:ui-rounded,"SF Pro Rounded","Nunito","Geist","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;
  --wash:radial-gradient(900px 480px at 8% -10%,rgba(255,122,162,.16),transparent 70%),
         radial-gradient(820px 460px at 94% 104%,rgba(95,224,180,.12),transparent 70%),
         radial-gradient(520px 300px at 70% 8%,rgba(255,209,102,.08),transparent 70%);
  --panel:rgba(39,32,64,.80); --bdf:blur(12px) saturate(1.2);
}
html[data-skin=sherbet][data-theme=light]{
  --surface:#FFF7EE; --plane:#FFFCF8; --band:#FBEADF; --ink:#2B2140; --ink2:#4A3D5E;
  --muted:#6A5A78; --grid:#E8CFC3; --link:#3A48C9; --bar:#C22D62;
  --good:#11704F; --warn:#8E4E00; --off:#6A5A78; --onbar:#FFFFFF;
  --accent-sky:#3A48C9; --accent-mint:#11704F; --accent-gold:#8A5A00;
  --accent-coral:#B23A3A; --accent-violet:#6B42C2;
  --wash:radial-gradient(900px 480px at 8% -10%,rgba(232,70,124,.10),transparent 70%),
         radial-gradient(820px 460px at 94% 104%,rgba(60,207,160,.10),transparent 70%),
         radial-gradient(520px 300px at 70% 8%,rgba(255,209,102,.14),transparent 70%);
  --panel:rgba(255,252,248,.82);
}
/* Riso is a risograph zine: two inks on off-white paper, and the second one printed a hair off register.
   That is the wash -- a pink and a blue blob that overlap without lining up. Fluoro yellow is the third
   drum and appears only as --accent-gold in dark, where it is ink on black; on paper it is a mustard,
   because yellow on cream is 1.1:1 and a zine you cannot read is not a zine. Paper is nearly opaque, so
   the panel is too, with just enough blur that a line of text behind the open Settings menu reads as a
   smudge rather than as a second sentence. */
html[data-skin=riso]{
  --surface:#16161F; --plane:#1E1E29; --band:#282835; --ink:#F5F0E6; --ink2:#DCD6CA;
  --muted:#AAA59C; --grid:#46465A; --link:#6FB4EA; --bar:#FF5CB8;
  --good:#3FD08A; --warn:#FFB84D; --off:#AAA59C; --onbar:#16161F;
  --accent-sky:#6FB4EA; --accent-mint:#3FD08A; --accent-gold:#FFE800;
  --accent-coral:#FF7F7F; --accent-violet:#B9A2FF;
  --ui:"Space Grotesk","Geist","Inter","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;
  --wash:radial-gradient(700px 420px at 14% -6%,rgba(255,72,176,.16),transparent 70%),
         radial-gradient(700px 420px at 17% -3%,rgba(0,120,191,.14),transparent 70%),
         radial-gradient(640px 380px at 92% 100%,rgba(255,232,0,.07),transparent 70%);
  --panel:rgba(30,30,41,.96); --bdf:blur(6px);
}
html[data-skin=riso][data-theme=light]{
  --surface:#F5F0E6; --plane:#FBF8F1; --band:#ECE5D6; --ink:#1A1A2E; --ink2:#34344A;
  --muted:#5A5864; --grid:#CFC6B3; --link:#B0126E; --bar:#0066A6;
  --good:#00703F; --warn:#A33A1F; --off:#5A5864; --onbar:#FFFFFF;
  --accent-sky:#0066A6; --accent-mint:#00703F; --accent-gold:#7A6100;
  --accent-coral:#B0126E; --accent-violet:#5B3FB0;
  --wash:radial-gradient(700px 420px at 14% -6%,rgba(255,72,176,.14),transparent 70%),
         radial-gradient(700px 420px at 17% -3%,rgba(0,120,191,.12),transparent 70%),
         radial-gradient(640px 380px at 92% 100%,rgba(255,232,0,.16),transparent 70%);
  --panel:rgba(251,248,241,.96);
}
/* Blueprint is a drafting sheet, and dark is the one it was drawn for: chalk on cyanotype blue with a
   24px ruled grid and safety orange for the action. The blue is far lighter than any other dark surface
   here, which is what the theme is and also what it costs -- every foreground had to be lifted to clear
   the band, and the plane and band sit closer to the surface than the other themes' do so the orange can
   still reach 4.5:1 on the lightest of the three. The grid is two `repeating-linear-gradient`s rather
   than an image, so it needs no request and no `background-size`. */
html[data-skin=blueprint]{
  --surface:#0B3D91; --plane:#0D4399; --band:#104BA6; --ink:#F4F8FF; --ink2:#DCE8FF;
  --muted:#C0D4F7; --grid:#4677D2; --link:#B5ECFF; --bar:#FFB27A;
  --good:#9CF2C4; --warn:#FFB3C1; --off:#C0D4F7; --onbar:#0A2352;
  --accent-sky:#B5ECFF; --accent-mint:#9CF2C4; --accent-gold:#FFE066;
  --accent-coral:#FFB8B0; --accent-violet:#D8CCFF;
  --ui:"IBM Plex Sans","Geist","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;
  --wash:repeating-linear-gradient(0deg,rgba(234,242,255,.07) 0 1px,transparent 1px 24px),
         repeating-linear-gradient(90deg,rgba(234,242,255,.07) 0 1px,transparent 1px 24px);
  --panel:rgba(13,67,153,.96); --bdf:blur(6px);
}
html[data-skin=blueprint][data-theme=light]{
  --surface:#F3F7FF; --plane:#FBFDFF; --band:#E4ECFA; --ink:#0B2A5C; --ink2:#233F72;
  --muted:#4A6290; --grid:#BFCDE8; --link:#1650B8; --bar:#B5470F;
  --good:#0F6B4A; --warn:#A3245A; --off:#4A6290; --onbar:#FFFFFF;
  --accent-sky:#1650B8; --accent-mint:#0F6B4A; --accent-gold:#7D5A00;
  --accent-coral:#B5470F; --accent-violet:#5A3FB8;
  --wash:repeating-linear-gradient(0deg,rgba(31,95,209,.07) 0 1px,transparent 1px 24px),
         repeating-linear-gradient(90deg,rgba(31,95,209,.07) 0 1px,transparent 1px 24px);
  --panel:rgba(251,253,255,.96);
}
/* Aurora is the northern lights over a midnight sky: one teal-to-violet curtain across the top of the
   window and nothing else. Teal is the action, so the verdict green is pushed toward lime to keep "this
   is a button" and "this runs here" in two different hues, the same separation terminal makes with
   amber. The curtain is `fixed` like every wash, so it hangs in the window rather than scrolling away. */
html[data-skin=aurora]{
  --surface:#0A1128; --plane:#101936; --band:#172244; --ink:#F1F5FF; --ink2:#CFD7F0;
  --muted:#9DA8CC; --grid:#35416B; --link:#8CC8FF; --bar:#2EC4B6;
  --good:#A8EF7A; --warn:#FFB5E8; --off:#9DA8CC; --onbar:#06121F;
  --accent-sky:#8CC8FF; --accent-mint:#A8EF7A; --accent-gold:#FFD98A;
  --accent-coral:#FFB5E8; --accent-violet:#C39BF0;
  --ui:"Geist",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif;
  --wash:linear-gradient(172deg,rgba(46,196,182,.20),rgba(114,242,165,.10) 14%,rgba(157,78,221,.16) 26%,
         transparent 40%),radial-gradient(900px 420px at 80% -10%,rgba(255,181,232,.12),transparent 70%);
  --panel:rgba(16,25,54,.78); --bdf:blur(16px) saturate(1.3);
}
html[data-skin=aurora][data-theme=light]{
  --surface:#F4F6FF; --plane:#FBFCFF; --band:#E6EAF8; --ink:#1B1F3B; --ink2:#373C5E;
  --muted:#595F82; --grid:#C6CCE4; --link:#2447B0; --bar:#0E6E68;
  --good:#3D6A0F; --warn:#A3316E; --off:#595F82; --onbar:#FFFFFF;
  --accent-sky:#2447B0; --accent-mint:#3D6A0F; --accent-gold:#805A00;
  --accent-coral:#A3316E; --accent-violet:#6A2CB0;
  --wash:linear-gradient(172deg,rgba(18,122,115,.14),rgba(60,160,90,.07) 14%,rgba(123,44,191,.11) 26%,
         transparent 40%),radial-gradient(900px 420px at 80% -10%,rgba(220,90,170,.08),transparent 70%);
  --panel:rgba(251,252,255,.80);
}
/* Holo is the one theme the Settings menu does not offer until it is unlocked from the admin panel (see
   `docs/assets/atlas-admin.js`), and the head script will not wear it before then either. Clean lines,
   texture and light, in three layers of --wash, top first: a film-grain tile -- an SVG fractal-noise
   filter in a data URI, so it costs no request -- then a 32px hairline grid, then an iridescent violet,
   cyan and rose light falling from the top of the window. Violet is the action and cyan the link, so a
   button and a link are never one hue, and the thirteen tokens underneath are as plain and as measured as
   graphite's: all the colour is in the wash and the accents, where it costs no contrast. */
html[data-skin=holo]{
  --surface:#07090F; --plane:#0D1018; --band:#161A25; --ink:#F2F5FB; --ink2:#C8D0DF;
  --muted:#939EB5; --grid:#353D52; --link:#7AD7FF; --bar:#A99BFF;
  --good:#5FE3B1; --warn:#FF93A6; --off:#939EB5; --onbar:#0B0820;
  --accent-sky:#7AD7FF; --accent-mint:#5FE3B1; --accent-gold:#FFD38A;
  --accent-coral:#FF93A6; --accent-violet:#C6B8FF;
  --ui:"Geist",system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif;
  --wash:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"),
         repeating-linear-gradient(0deg,rgba(122,215,255,.05) 0 1px,transparent 1px 32px),
         repeating-linear-gradient(90deg,rgba(122,215,255,.05) 0 1px,transparent 1px 32px),
         radial-gradient(900px 460px at 88% -10%,rgba(169,155,255,.24),transparent 70%),
         radial-gradient(760px 420px at 4% -8%,rgba(122,215,255,.18),transparent 70%),
         radial-gradient(640px 360px at 50% -14%,rgba(255,147,166,.10),transparent 70%);
  --panel:rgba(13,16,24,.74); --bdf:blur(16px) saturate(1.5);
}
html[data-skin=holo][data-theme=light]{
  --surface:#F3F5FA; --plane:#FCFDFF; --band:#E7EBF3; --ink:#0D1220; --ink2:#2C3446;
  --muted:#525D73; --grid:#C0C7D6; --link:#0B57C2; --bar:#5A3FD1;
  --good:#0E6B4C; --warn:#A32F4F; --off:#525D73; --onbar:#FFFFFF;
  --accent-sky:#0B57C2; --accent-mint:#0E6B4C; --accent-gold:#7A5600;
  --accent-coral:#A32F4F; --accent-violet:#5A3FD1;
  --wash:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .055 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"),
         repeating-linear-gradient(0deg,rgba(90,63,209,.055) 0 1px,transparent 1px 32px),
         repeating-linear-gradient(90deg,rgba(90,63,209,.055) 0 1px,transparent 1px 32px),
         radial-gradient(900px 460px at 88% -10%,rgba(90,63,209,.13),transparent 70%),
         radial-gradient(760px 420px at 4% -8%,rgba(11,87,194,.10),transparent 70%),
         radial-gradient(640px 360px at 50% -14%,rgba(163,47,79,.07),transparent 70%);
  --panel:rgba(252,253,255,.78);
}
*{box-sizing:border-box}
/* --ui and --wash are the structural tokens declared in the block above; graphite resolves them to this
   file's own font stack and to `none`, so a reader who has chosen no theme gets what was written here
   before. See the note beside them in `19_pages.py` for why they are tokens at all. */
body{margin:0;background:var(--surface);background-image:var(--wash);
  background-attachment:fixed;color:var(--ink);font:15px/1.5 var(--ui)}
a{color:var(--link);text-decoration:none}
a:hover{text-decoration:underline}
header{background:var(--panel);backdrop-filter:var(--bdf);border-bottom:1px solid var(--grid);
  padding:22px 20px 16px}
.wrap{max-width:1500px;margin:0 auto}
h1{margin:0 0 6px;font-size:26px;letter-spacing:-.02em}
h2{margin:34px 0 8px;font-size:16px;letter-spacing:-.01em}
.sub{color:var(--ink2);font-size:14px;margin:0}
.sub b{color:var(--ink)}
.top{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap}
.top nav{font-size:13px;color:var(--muted);text-align:right;line-height:1.9}
.blurb{color:var(--muted);font-size:13px;margin:6px 0 0;max-width:70ch}
button{font:inherit;cursor:pointer}
.chip{display:inline-block;background:var(--band);color:var(--ink2);border:1px solid var(--grid);
  border-radius:999px;padding:5px 12px;font-size:13px;white-space:nowrap;margin:0 5px 5px 0}
.chip:hover{border-color:var(--bar);color:var(--ink);text-decoration:none}
/* The row count inside a navigation chip. Dimmed rather than smaller, so the chips stay one height. */
.ct{opacity:.65}
/* The one link on the page that has to be found, because it is the thing the page is a preview of:
   this is a snapshot of one crossing, and that is the live version of it with both axes still live.
   Filled with the accent, which is why it needs --onbar for its ink. */
.cta{display:inline-block;background:var(--bar);color:var(--onbar);font-weight:600;
  border-radius:8px;padding:9px 16px;margin:14px 0 0}
.cta:hover{text-decoration:none;filter:brightness(1.08)}
main{padding:0 20px 64px}
table{width:100%;border-collapse:collapse;margin-top:14px}
th{text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);
  padding:8px 10px;border-bottom:1px solid var(--grid);background:var(--surface)}
th.n,td.n{text-align:right}
th.c,td.c{text-align:center}
td{padding:12px 10px;border-bottom:1px solid var(--grid);vertical-align:top}
/* Guarded, because a touch device reports a hover that then latches: tapping a row anywhere -- to
   follow its link, or just while scrolling -- left it tinted until something else was tapped. */
@media(hover:hover){tr:hover td{background:var(--band)}}
.rk{color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
.shot{width:200px}
.shot img{width:200px;aspect-ratio:2/1;object-fit:cover;border-radius:6px;
  background:var(--band);border:1px solid var(--grid);display:block}
/* These two set the table's width. Auto table layout takes the widest unbreakable run in the column
   across every row on the page, and neither a slash nor a comma is a break opportunity in Chrome or
   Safari, so a repo name and a blurb naming six agents between them demand more than the content box
   has -- which the document then has to scroll sideways to satisfy. */
.nm{font-weight:600;font-size:15px;overflow-wrap:anywhere}
.nwo{display:block;color:var(--muted);font-size:12px;margin-top:2px;word-break:break-all}
/* The `nwo` is a link out to GitHub and the name above it is a link into this site, so the two need to
   be told apart without colour doing it -- muted is the right weight for the secondary line and the
   link colour would make it shout. Hover promotes it to full ink, which with the underline `a:hover`
   already adds is enough to say "this is a link" at the moment somebody asks. */
a.nwo:hover{color:var(--ink)}
.st{font-size:16px;font-weight:700;font-variant-numeric:tabular-nums}
.st.none{font-size:13px;font-weight:400;color:var(--muted)}
.meta{color:var(--muted);font-size:12px;margin-top:5px}
.desc{color:var(--ink2);font-size:13.5px;max-width:44em;overflow-wrap:anywhere}
.cmd{display:block;margin-top:7px;font:12px/1.5 var(--mono);
  color:var(--ink2);background:var(--band);border:1px solid var(--grid);border-radius:5px;
  padding:5px 8px;max-width:44em;overflow-wrap:anywhere}
.tag{display:inline-block;background:var(--band);border:1px solid var(--grid);border-radius:5px;
  padding:2px 7px;font-size:11.5px;color:var(--ink2);margin:0 4px 4px 0;white-space:nowrap}
.tag.cat{border-color:var(--bar);color:var(--ink)}
.os{font-size:11.5px;letter-spacing:.02em;white-space:nowrap}
/* Prefixed because the verdict characters are the class names and one of them is "-", which is not a
   valid CSS identifier on its own -- ".-" invalidates the whole selector list it appears in, so the
   unprefixed version silently dropped the colour from every No, n/a and dash on the page. */
.vY{color:var(--good);font-weight:700}
.vL{color:var(--warn)}
.vN,.va,.v-{color:var(--off)}
/* The five platform marks -- scripts/osicons.py owns the geometry; these are the only rules that
   apply to all of them. `fill:currentColor` is the whole reason they can sit inside a coloured verdict:
   a green Y and a grey dash get a green and a grey mark without either being named here. */
.oi{width:1.15em;height:1.15em;fill:currentColor;vertical-align:-.19em;flex:none}
/* A mark and its word, when both are shown, should not be separable by a line break. */
.oiw{white-space:nowrap}
/* The legend -- see `legend()` for why a `title` is not enough. Sized and coloured like the caption it is,
   and it wraps, because five pairs do not fit on one line at 335px. */
.oikey{display:flex;flex-wrap:wrap;gap:4px 14px;margin:0 0 12px;font-size:12.5px;color:var(--muted)}
.mk{display:inline-grid;place-items:center;width:28px;height:28px;flex:none;
  vertical-align:-7px;margin-right:2px}
.brand{display:inline-flex;align-items:center;gap:7px;text-decoration:none;color:inherit;
  font-weight:600;letter-spacing:-.01em;white-space:nowrap}
.brand:hover span{text-decoration:underline}
/* The brand's own line, on the page families that have no breadcrumb to sit in. Named rather than
   reusing `.crumb`, which is `22_detail.py`'s class and already carries a rule there: a shared
   stylesheet that redefines a generator's own selector wins or loses on source order, which is not a
   thing to leave to chance across four files. */
.brandbar{margin:0 0 14px}
.brand .mk{vertical-align:baseline;margin-right:0}
.mk svg{width:30px;height:30px;display:block;overflow:visible;
  filter:drop-shadow(0 0 calc(5px * var(--fx-glow,1)) rgba(214,160,52,.45));
  animation:mkbreath calc(6s * var(--fx-pulse,1)) ease-in-out infinite}
@keyframes mkbreath{
  0%,100%{filter:drop-shadow(0 0 calc(3px * var(--fx-glow,1)) rgba(214,160,52,.32))}
  50%{filter:drop-shadow(0 0 calc(9px * var(--fx-glow,1)) rgba(214,160,52,.60))}}
@media(prefers-reduced-motion:reduce){.mk svg{animation:none}}

/* 14px rather than the shared 1.15em, which at this column's 11.5px would be 13.2px. A mark the same
   size as the text beside it does not read as a mark, it reads as a smudge in the line -- and unlike the
   detail page's table there is no word next to these to be measured against, so the mark has to carry
   the whole of "which platform" on its own. Fixed rather than relative for the same reason: the five sit
   in a row and their size is the row's rhythm, not the text's. `.os` is `white-space:nowrap`, so the
   five and the spaces between them cannot break across a line. */
.os .oi{width:14px;height:14px}
/* Off screen rather than display:none or visibility:hidden -- both of those take the text out of the
   accessibility tree as well as off the screen, which defeats the point. Verbatim from `19_pages.py`,
   which declares it for the index; `.sr` and not `.sr-only`, because that is the name the index uses and
   the one `25_collections.py` emits.
   Nothing this stage writes uses it -- the facet rows name their five marks once on the container instead,
   which is cheaper at 505 marks a page. It is here because the collections pages load this sheet and
   *do* emit it, and the failure of a missing rule is the worst kind: a platform word printed visibly
   beside every mark on the page that was meant to be the tidy one. */
.sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}
.rest{margin:22px 0 0;color:var(--ink2);font-size:13.5px}
.rel{margin-top:34px;border-top:1px solid var(--grid);padding-top:18px}
.rel h2{margin-top:0}
.rel p{margin:0 0 12px;color:var(--muted);font-size:13px}

/* ---- the curated collections, docs/collections/, written by scripts/25_collections.py ----
   Here rather than in a second stylesheet because these pages share this shell exactly: the same
   masthead, nav, footer, chips, `.cmd`, `.tag` and the five `.v*` verdict colours. A separate file
   would duplicate all of that to add the ten rules below, and cost a second blocking request on the
   page most likely to be somebody's first. `22_detail.py` does ship its own -- a detail page is a
   different layout -- and that is the line: same shell, same sheet. */
.kick{margin:0 0 4px;font-size:11px;text-transform:uppercase;letter-spacing:.09em;color:var(--bar)}
.intro{color:var(--ink2);font-size:14px;margin:10px 0 0;max-width:74ch}
.take{max-width:52ch}
/* The numbers are ours, in `.slot`, so the marker has to go. A collection is read as slots, and an
   `<ol>` is still the honest element for it -- the order carries meaning and a screen reader should
   say "7 items" -- but the browser's marker sits outside the card border and cannot be styled to sit
   inside it. */
.picks{list-style:none;margin:20px 0 0;padding:0}
/* Prose left, evidence right, and the split is the point of the page: the `why` is a judgement and
   everything in the second column is what it was made from, so a reader can weigh one against the
   other without scrolling between them. 300px is what the five verdicts plus a licence name need on
   one line each; `minmax(0,1fr)` rather than `1fr` because a `code.cmd` with no break opportunity in
   it will otherwise push a grid track wider than its share and take the card with it. */
.pick{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:4px 26px;align-items:start;
  background:var(--plane);border:1px solid var(--grid);border-radius:10px;
  padding:16px 18px;margin:0 0 12px}
.pick>*{grid-column:1;min-width:0}
.pick .ev,.pick .os,.pick .tp{grid-column:2}
.pick .ev{grid-row:2;margin-top:0}
.pick .os{grid-row:3}
.pick .tp{grid-row:4}
.pick h2{margin:2px 0 0;font-size:20px;letter-spacing:-.02em}
.role{margin:0;font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
/* Tabular, because seven of these stack in a column and a proportional "1" would step the labels
   beside them in and out. */
.slot{display:inline-block;min-width:20px;margin-right:6px;padding:1px 0;text-align:center;
  border-radius:5px;background:var(--band);color:var(--ink);font-variant-numeric:tabular-nums}
.why{color:var(--ink);font-size:14.5px;margin:9px 0 0;max-width:66ch}
.pick .desc{margin:7px 0 0;font-size:13px;color:var(--muted)}
.ev b{color:var(--ink)}
.allin{margin-top:34px;border-top:1px solid var(--grid);padding-top:18px}
.allin p{margin:0 0 12px;color:var(--ink2);font-size:13.5px;max-width:74ch}
/* `.cmd` is one line elsewhere; here it holds seven. `pre` brings its own margin and font, both of
   which have to go for it to match the single-line version above it. */
pre.cmd{margin:0;white-space:pre-wrap;max-width:none;padding:12px 14px;font-size:12.5px}
/* `.cmd` is `display:block` because everywhere else it is an install command on its own line. Inside a
   sentence -- "the atlas reads `#list=` in a URL" -- that block broke the sentence into three lines with
   a full-width bar through the middle of it. Named narrowly so the install commands, which are `.cmd`
   directly inside a `.pick`, keep the block treatment they want. */
p code.cmd{display:inline;margin:0;padding:2px 6px}
/* 340px is the width at which a kicker, a two-line title and the roles below it stop wrapping into
   something unreadable; auto-fit rather than a fixed count so five cards reflow to 3+2 and then 2+2+1
   without a query for each step. */
.colls{list-style:none;margin:20px 0 0;padding:0;display:grid;gap:14px;
  grid-template-columns:repeat(auto-fit,minmax(340px,1fr))}
.coll{background:var(--plane);border:1px solid var(--grid);border-radius:10px;padding:16px 18px}
.coll h2{margin:2px 0 0;font-size:18px;letter-spacing:-.02em}
.coll .intro{font-size:13.5px;margin-top:8px}

/* ---- Discover, docs/discover/, written by scripts/19d_discover.py ----
   Here for the reason the collections block above is here: that page is this shell exactly -- same
   masthead, nav, footer, chips, `.tag`, `.nwo`, `.desc` -- plus the rail below. A second stylesheet would
   copy all of that to add twenty rules, and would cost a blocking request on a page whose whole design is
   that the first paint is already the content.

   The rail is `scroll-snap` and `overflow-x`, not a transformed track with JavaScript computing offsets.
   That is what makes the carousel work before its script has run and keep working if the script never runs
   at all: a reader can drag it, a trackpad flicks it, the scrollbar is a real scrollbar, and Tab into a
   card scrolls it into view because that is what a browser does for a focused element inside a scroller.
   The buttons and the auto-advance are `scrollBy` calls on top of that, so they add to the behaviour
   instead of being the whole of it. */
.drail{display:flex;gap:14px;margin:14px 0 0;padding:4px 4px 16px;overflow-x:auto;
  scroll-snap-type:x mandatory;overscroll-behavior-x:contain;scrollbar-width:thin}
/* A ring on the scroller itself, because it is `tabindex="0"` and the arrow keys are bound to it: a reader
   has to be able to see when they are talking to the rail rather than to the page. */
.drail:focus-visible{outline:2px solid var(--bar);outline-offset:3px}
/* 320px is where a two-line project name, the `nwo` under it and six lines of blurb stop fighting each
   other. `flex:0 0` rather than a min-width, so a card never stretches to fill a short rail; `min()` is the
   phone, where a fixed 320 plus the padding leaves no sliver of the next card -- and that sliver is the
   only thing on screen saying the rail scrolls. */
.dcard{flex:0 0 min(320px,78vw);scroll-snap-align:start;display:flex;flex-direction:column;
  background:var(--plane);border:1px solid var(--grid);border-radius:10px;padding:14px 16px 12px;
  border-top:3px solid var(--card-accent)}
.dcard h2{margin:6px 0 0;font-size:17px;letter-spacing:-.02em}
.dcard .nwo{margin-top:3px}
/* Clamped for the reason the index's cards are: these blurbs are other people's table cells and run from
   six words to sixty, and in a rail the tall one does not stretch its neighbours, it just runs off the
   bottom of its own card. Both spellings, because Firefox implements only the prefixed one. */
.dcard .desc{margin:8px 0 0;font-size:13px;color:var(--ink2);max-width:none;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:6;line-clamp:6;overflow:hidden}
.dtag{margin:0;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.dmeta{margin:7px 0 0;color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums}
.dmeta b{color:var(--ink);font-size:14px;font-weight:700}
/* Pushed to the bottom by `margin-top:auto`, so "17 of 50" sits on one line across a row of cards whose
   blurbs are different lengths. It is the one thing on a card that is about the rail rather than about the
   project, and a reader looking for where they are should not have to hunt for it. */
.dn{margin:auto 0 0;padding-top:8px;color:var(--muted);font-size:11px;letter-spacing:.06em;
  text-transform:uppercase;font-variant-numeric:tabular-nums}
.dctl{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:18px}
.dpos{margin-left:6px;color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums}
.dnote{margin:12px 0 0;background:var(--band);border:1px solid var(--grid);
  border-left:3px solid var(--bar);border-radius:6px;padding:10px 13px;color:var(--ink2);font-size:13.5px}
/* The card a deep link from the homepage landed on. The same `--bar` the rest of the site uses for "this
   one", and it does not fade: a reader who followed a link to a particular project should still be able to
   tell which one it was after reading two cards either side of it. */
.dcard.dhit{border-color:var(--bar);box-shadow:0 0 0 2px var(--bar)}
.dcard.dhit:focus{outline:none}
/* New to the atlas, on a Discover card. The index's rule restated -- see the long note beside `tr.nw` in
   `19_pages.py` for why this is an outline that breathes and not a colour wash -- and capped at five swells
   for the same reason: it says "look at this one", and something that says that forever is not saying it.
   No `data-wave` equivalent, because a day here is fifty cards drawn from a rotation rather than a filtered
   set, so the case that rule exists for -- most of what is on screen being new at once -- cannot arise. */
.dcard.nw{border-color:var(--warn);
  box-shadow:0 0 0 2px var(--warn),0 0 12px color-mix(in srgb,var(--warn) 30%,transparent);
  animation:new-breathe 3s ease-in-out 5}
@keyframes new-breathe{
  0%,100%{box-shadow:0 0 0 2px var(--warn),0 0 10px color-mix(in srgb,var(--warn) 24%,transparent)}
  50%{box-shadow:0 0 0 3px var(--warn),0 0 26px color-mix(in srgb,var(--warn) 58%,transparent)}}
.dnew{background:var(--warn);color:var(--onbar);border-radius:999px;padding:1px 8px;
  font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
/* All fifty at once. The rail and the grid are the same cards in the same order with the same markup and
   only the container changes, which is why the toggle is a class on <html> and nothing is re-rendered.
   `auto-fill` on a 290px floor so it reflows from four across to one without a query per step, and the
   cards give up their fixed width because here they are not a rail. */
html[data-dmode=grid] .drail{display:grid;gap:14px;overflow:visible;padding-bottom:4px;
  grid-template-columns:repeat(auto-fill,minmax(290px,1fr))}
html[data-dmode=grid] .dcard{flex-basis:auto}
/* The two rail buttons have nothing to scroll in the grid, so they go rather than sit there inert. The view
   toggle and the count stay, because both still mean something. */
html[data-dmode=grid] #dprev,html[data-dmode=grid] #dnext{display:none}
/* Guarded like every other hover on this sheet: a touch device latches a hover it can never clear, so
   tapping a card to follow its link would leave it lit until something else was tapped. */
@media(hover:hover){
  .dcard:hover{border-color:var(--card-accent);
    box-shadow:0 0 0 2px var(--card-accent),0 0 18px color-mix(in srgb,var(--card-accent) 40%,transparent)}
}
.dcard:focus-within{border-color:var(--card-accent);
  box-shadow:0 0 0 2px var(--card-accent),0 0 18px color-mix(in srgb,var(--card-accent) 36%,transparent)}
/* The three kinds of motion this section introduces, all off for a reader who asked for none. Scoped to
   these selectors rather than written as the blanket `*{animation-duration:.01ms}` the index uses, because
   this sheet is also the 156 facet pages and the five collection pages and none of those asked for a rule.
   Nothing here is *communicated* by the motion: the pulse is a swell on a `--warn` ring that is already
   there in the base rule, and the rail scrolls either way.

   The third kind cannot be reached from CSS at all. `scroll-behavior` governs a scroll the document starts;
   the buttons and the auto-advance pass `behavior` to `scrollBy`, which overrides it, and `setInterval` is
   not an animation. So `19d_discover.py` checks the same media query in JavaScript -- and either half alone
   would leave a reader who asked for no motion with most of it. */
@media(prefers-reduced-motion:reduce){
  .dcard.nw{animation:none}
  .drail{scroll-behavior:auto}
}
footer{border-top:1px solid var(--grid);background:var(--plane);padding:22px 20px;
  color:var(--muted);font-size:13px}
footer .wrap{max-width:1500px}
/* Two queries, because the page fails in two places: at 900px the screenshot and the two detail
   columns stop paying for their width, and below 640px four columns stop fitting in what is left. */
@media(max-width:900px){
  .shot,.hide{display:none}
  td,th{padding:9px 6px}
  /* The second column is a fixed 300px against a `1fr`, so it stops being a third of the card and
     starts being most of it. Below this the evidence goes back under the prose it is evidence for,
     which is also the order it should be read in when there is only one column to read. */
  .pick{grid-template-columns:minmax(0,1fr)}
  .pick .ev,.pick .os,.pick .tp{grid-column:1;grid-row:auto}
  .pick .ev{margin-top:12px}
}
@media(max-width:640px){
  header{padding:16px 14px 12px}
  main{padding:0 14px 48px}
  footer{padding:18px 14px}
  h1{font-size:21px}
  .top nav{text-align:left;line-height:2.1}
  /* A row stops being a row. Four columns cannot share 335px -- the blurb on its own wants more than
     that -- so each row becomes a card, and the headings, no longer above anything, go away. */
  thead{display:none}
  table,tbody,tr,td{display:block}
  table{margin-top:12px}
  tr{display:grid;grid-template-columns:1fr auto;gap:1px 12px;align-items:start;
    border:1px solid var(--grid);border-radius:10px;padding:11px 13px;margin:0 0 9px}
  td{border-bottom:0;padding:0}
  td.rk{grid-column:1;grid-row:1;text-align:left}
  td.st-c{grid-column:2;grid-row:1}
  td.pj{grid-column:1/-1;grid-row:2}
  td.ds{grid-column:1/-1;grid-row:3;margin-top:6px}
  .st{font-size:15px}
  .desc{font-size:13px}
  .cta{display:block;text-align:center}
}
@font-face{font-family:"Geist";src:url(assets/fonts/geist.woff2) format("woff2");font-weight:100 900;font-style:normal;font-display:optional}
@font-face{font-family:"Geist Mono";src:url(assets/fonts/geist-mono.woff2) format("woff2");font-weight:400;font-style:normal;font-display:optional}
@font-face{font-family:"Geist Mono";src:url(assets/fonts/geist-mono-600.woff2) format("woff2");font-weight:500 800;font-style:normal;font-display:optional}
@font-face{font-family:"Atlas Icons";src:url(assets/fonts/geist-mono.woff2) format("woff2");unicode-range:U+F400-F5FF;font-display:swap}
/* The build tags in the footer. See `buildtags()` in scripts/sitewide.py. Two classes deep so that
   `footer a`, which every page family has, does not outrank them. */
.btags{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 14px;font:600 11px/1 var(--mono)}
.btags .btag{display:inline-flex;align-items:center;gap:6px;min-height:24px;padding:4px 9px;border-radius:7px;
  color:var(--ink);background:var(--band);border:1px solid var(--grid);text-decoration:none;
  letter-spacing:.01em}
.btags .btag:hover{border-color:var(--link);text-decoration:none}
.btags .btag b{font-weight:600}
.btags .btag .bv{color:var(--muted);font-weight:500}
.btags .btag .bico{color:var(--muted);font:400 12px/1 "Atlas Icons",var(--mono)}
.btags .btag .bico:empty{display:none}
.bdot{width:7px;height:7px;flex:none;border-radius:50%;background:var(--off)}
.btags .btag[data-state=ok] .bdot{background:var(--good);box-shadow:0 0 8px var(--good)}
.btags .btag[data-state=bad] .bdot{background:var(--warn);box-shadow:0 0 8px var(--warn)}
.btags .btag[data-state=run] .bdot{background:var(--link);box-shadow:0 0 8px var(--link)}
.btags .btag[data-state=ok] .bv{color:var(--good)}
.btags .btag[data-state=bad] .bv{color:var(--warn)}
.btags .btag[data-state=run] .bv{color:var(--link)}
@media (prefers-reduced-motion:no-preference){
  .btags .btag[data-state=run] .bdot{animation:bpulse 1.6s ease-in-out infinite}
  @keyframes bpulse{50%{opacity:.35}}
}
@media print{.btags{display:none}}
