/* ==========================================================================
   Shared across every public page: light theme, theme toggle, site footer.
   Loaded AFTER each page's own <style> block so these rules win the cascade
   for any matching selectors (e.g. nav background).
   ========================================================================== */

/* ---- A listing's controls: the blog's toolbar, and a Listing bar on any page ----
   The search box, the Sort and View menus, the RSS link, the category pills
   and the count. Kept as blog.html had them, and first in this file, so they
   sit where they did in the cascade: after the page's own styles, before
   everything below (the pills' button colours, the light theme). */
.filters{
  display:flex; gap:10px; flex-wrap:wrap; justify-content:center;
  padding:28px 32px 10px; max-width:1180px; margin:0 auto;
}
/* The search field sits with the category pills and is built out of the same
   parts: mono type, a one-pixel border, the site's orange on focus. */
.blog-search{
  position:relative; display:flex; align-items:center; flex:1 1 300px; max-width:420px; min-width:0;
}
.blog-search .bs-icon{
  position:absolute; left:12px; width:15px; height:15px; color:var(--text-dim);
  pointer-events:none; transition:color .2s;
}
.blog-search input{
  width:100%; height:34px; font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px; color:var(--text);
  background:var(--surface); border:1px solid var(--line); border-radius:20px;
  padding:0 30px 0 33px; transition:.2s; -webkit-appearance:none; appearance:none;
}
.blog-search input::placeholder{color:var(--text-dim);}
.blog-search input:hover{border-color:var(--houdini);}
.blog-search input:focus{outline:none; border-color:var(--houdini); box-shadow:0 0 0 3px var(--houdini-glow);}
.blog-search input:focus + .bs-clear{color:var(--text-muted);}
.blog-search:focus-within .bs-icon{color:var(--houdini);}
/* The browser's own clear cross would be a second one next to ours. */
.blog-search input::-webkit-search-cancel-button{display:none;}
.bs-clear{
  position:absolute; right:8px; width:20px; height:20px; padding:0; line-height:1;
  display:flex; align-items:center; justify-content:center;
  background:transparent; border:0; border-radius:50%; cursor:pointer;
  color:var(--text-dim); font-size:17px; transition:.2s;
}
.bs-clear:hover{color:var(--houdini);}
.bs-clear[hidden]{display:none;}

/* Sized and shaped like the pickers beside it, so it reads as one more
   control rather than a badge stuck on the end. */
/* Reachable, and quieter than the two controls beside it: the border is what
   is missing, not the contrast. --text-dim measures 3.12:1 here, which is the
   mistake the note above .layout-picker > span records fixing once already. */
.blog-feed{
  display:inline-flex; align-items:center; gap:7px; flex:none; height:34px;
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--text-muted);
  padding:0 12px; border:1px solid transparent; border-radius:20px; transition:.2s;
}
.blog-feed svg{width:12px; height:12px;}
.blog-feed:hover{color:var(--houdini); border-color:var(--houdini);}

.filter-pill{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px;
  padding:8px 14px; border-radius:20px; border:1px solid var(--line);
  color:var(--text-muted); cursor:pointer; transition:.2s; background:transparent;
}
.filter-pill:hover{color:var(--text); border-color:var(--houdini);}
.filter-pill.active{background:var(--houdini); border-color:var(--houdini); color:var(--on-accent,#0A0C10);}
/* Held open while the posts load, so nothing moves when the numbers come. */
.blog-count{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px; line-height:1.4; min-height:1.4em;
  color:var(--text-muted);
}
/* The pills wrap downwards, so the label stays with the first row of them
   rather than floating in the middle of three. */
.bt-cats{align-items:flex-start; gap:12px;}
.bt-label{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--text-muted); flex:none; padding-top:9px;
}
.layout-picker{display:flex; align-items:center; gap:9px; flex:none;}
.layout-picker > span{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11px; letter-spacing:.1em;
  /* --text-dim was #5B6070 then, 3.12:1 against the dark background (raised to
     #818592, 5.3:1, in Nov 2026, audit A11Y-01). --text-muted measures 6.04:1
     and matches the filter pills beside it. */
  text-transform:uppercase; color:var(--text-muted);
}
.layout-picker select{
  height:34px;
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px;
  background:var(--surface); color:var(--text);
  border:1px solid var(--line); border-radius:6px;
  padding:7px 30px 7px 11px; cursor:pointer; transition:border-color .2s, color .2s;
  /* The native arrow is drawn in the OS accent colour and looks foreign next
     to everything else here, so it is replaced with one in the site's own
     palette. appearance:none is what removes it, and the SVG is inline so
     there is no extra request for a 9-pixel chevron. */
  appearance:none; -webkit-appearance:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238A8F9A' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 11px center; background-size:9px;
}
.layout-picker select:hover{border-color:var(--houdini); color:var(--houdini);}
.layout-picker select:focus-visible{outline:2px solid var(--houdini); outline-offset:2px;}
/* Tablet: the controls drop under the search rather than crushing it. */
@media(max-width:860px){
  .blog-search{flex:1 1 100%; max-width:none;}
}
/* Phone: the two pickers share a line only where each menu still shows its
   longest words ("Two-column showcase"); otherwise a line each. The label goes
   above the pills so neither has to fight for the width. */
@media(max-width:620px){
  .layout-picker{flex:1 1 240px; min-width:0;}
  .layout-picker select{flex:1; min-width:0; width:100%;}
  .blog-feed{margin-left:auto;}
  .bt-cats{flex-direction:column; align-items:stretch; gap:9px;}
  .bt-label{padding-top:0;}
}
/* A Listing bar: one row that wraps, as the blog's toolbar Section laid out its
   elements: the search box first, the menus and the RSS link pushed to the
   right, the Topics row and the count on lines of their own. The space between
   the search box and the menus under it is a pixel less than the space above
   the Topics, as the two rows always had it. */
.listbar{display:flex; align-items:center; flex-wrap:wrap; column-gap:12px; row-gap:15px;}
.listbar > .blog-search{margin-right:2px;}
.listbar > .blog-search + .layout-picker{margin-left:auto;}
.listbar > .bt-cats{flex:1 1 100%; display:flex; gap:12px;}
.listbar > .blog-count{flex:1 1 100%;}
.listbar .filters{padding:0; margin:0; flex:1 1 0; min-width:0; justify-content:flex-start;}
@media(max-width:860px){
  .listbar > .blog-search{margin-right:0;}
  .listbar > .blog-search ~ :is(.layout-picker, .blog-feed){margin-top:-1px;}
  .listbar > .blog-search + .layout-picker{margin-left:0;}
}
@media(max-width:620px){
  .listbar{column-gap:10px; row-gap:13px;}
  .listbar > .blog-search ~ :is(.layout-picker, .blog-feed){margin-top:-3px;}
  .listbar > .blog-feed{margin-left:auto;}
  .listbar > .bt-cats{gap:9px;}
}

/* ---- Light theme ---- */
:root[data-theme="light"]{
  --bg:#F7F7F4;
  --bg-2:#FFFFFF;
  --surface:#FFFFFF;
  --surface-2:#F0F0EC;
  --line:#E1E1DB;
  --line-soft:#EBEBE6;
  --text:#16171B;
  --text-muted:#53555D;
  --text-dim:#6A6B72;
  --houdini:#D9591C;
  --houdini-glow:#D9591C2E;
  --unreal:#2A64CC;
  --unreal-glow:#2A64CC2E;
  --nav-bg: rgba(247,247,244,.55);
}
nav{ background: var(--nav-bg, rgba(10,12,16,.5)); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
#navLinksSlot{ display: contents; }

/* ---- Theme toggle: both icons visible, sliding indicator shows the active one ---- */
.theme-toggle{
  position:relative;
  display:inline-flex; align-items:center; gap:2px;
  padding:3px; border-radius:999px;
  border:1px solid var(--tt-border, var(--line)); background:var(--tt-bg, transparent);
  flex:none;
}
.theme-toggle .tt-indicator{
  position:absolute; top:3px; left:3px; width:26px; height:26px;
  border-radius:50%; background:var(--tt-accent, var(--houdini));
  transform:translateX(28px); /* default (dark theme): sits under the moon button */
  transition:transform .35s cubic-bezier(.4,0,.2,1);
  z-index:0;
}
:root[data-theme="light"] .theme-toggle .tt-indicator{ transform:translateX(0); }

.theme-toggle .tt-btn{
  position:relative; z-index:1;
  width:26px; height:26px;
  display:flex; align-items:center; justify-content:center;
  border:none; background:transparent; border-radius:50%;
  color:var(--tt-ink, var(--text-dim)); font-size:13px; line-height:1;
  cursor:pointer; transition:color .3s;
}
.theme-toggle .tt-dark{ color:var(--on-accent,#0A0C10); } /* active by default (dark theme) */
:root[data-theme="light"] .theme-toggle .tt-dark{ color:var(--text-dim); }
:root[data-theme="light"] .theme-toggle .tt-light{ color:var(--on-accent,#0A0C10); }

/* ---- Nav link hover scramble: vertical letter-reel motion ---- */
.nav-scramble{ display:inline-flex; align-items:center; text-transform:uppercase; letter-spacing:.02em; }
.nav-scramble .scr-letter{
  position:relative; display:inline-block; overflow:hidden;
  height:1.2em; line-height:1.2em; text-align:center;
  /* width is set inline per-letter by JS, locked to that letter's own
     natural size so cycling through other glyphs never shifts spacing */
}
.nav-scramble .scr-reel{
  display:flex; flex-direction:column; align-items:center;
  will-change:transform;
}
.nav-scramble .scr-reel span{ display:block; height:1.2em; line-height:1.2em; }

/* ==========================================================================
   Site footer
   ========================================================================== */
.site-footer{
  position:relative; z-index:1;
  border-top:1px solid var(--line-soft);
  padding:56px 32px 34px;
  margin-top:40px;
}
.site-footer-inner{ max-width:1180px; margin:0 auto; }

/* The footer is rendered from one place (site.js, off /api/footer) and has to
   look the same on every page, so it carries its own link reset rather than
   leaning on each page's global a{} rule. The two legal pages never had that
   rule, which is why the "Become a Patron" button was showing up underlined
   there and nowhere else. */
.site-footer a{ text-decoration:none; }
/* What sits beside the nav's links (the light/dark switch), as elements:
   each sits in the row as the switch always did. */
.nav-tools{ display:contents; }

.sf-section{ margin-bottom:38px; }
/* The part before the copyright line, and the last part, sit on it and on the
   footer's edge with no gap of their own, as Useful links always did. */
.sf-part:has(+ .sf-part[data-fpart="copyright"]) > :is(.sf-section, .sf-patreon-row),
.site-footer > .sf-part:last-child > :is(.sf-section, .sf-patreon-row){ margin-bottom:0; }
.sf-label{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11.5px; letter-spacing:.1em;
  text-transform:uppercase; color:var(--text-dim); margin-bottom:16px;
  text-align:center;
}

/* Supporters */
/* The supporter list is the one part of the footer that wants the whole screen:
   it is a long list of short names, and boxing it into the content column
   stacks it into a narrow tower with most of the width unused. The six social
   icons and the two useful links are short enough that centring them in the
   column is right, so this is the only part that spreads.

   It gets that width by sitting OUTSIDE .site-footer-inner in the markup rather
   than by breaking out of it with 100vw. A 100vw breakout counts the scrollbar
   in a way the visible area does not, which pushed the whole row about 7px off
   centre and pushed the same amount off the right edge into the clip. Being a
   direct child of .site-footer means the width is simply the footer's own
   content box: exact, symmetrical, and with no viewport units involved. */
/* The tiers, one under another, 20px apart (the gap, the empty line and the
   gap the names were split by before each tier had a row of its own). */
.sf-supporters{
  display:flex; flex-direction:column; gap:20px;
}
/* The section rail (section-rail.js) is fixed to the right edge, and these
   names run to it: with the rail on the page they keep clear of it, by the
   same on both sides so they stay centred. --hd-rail-room is set with the
   rail's own sizes, further down. */
.hd-has-rail .site-footer .sf-supporters, .hd-has-rail .site-footer .sf-supporters-empty{
  padding-left:var(--hd-rail-room, 0px); padding-right:var(--hd-rail-room, 0px);
}
/* A tier of supporters: a row of its own that wraps, carrying what the panel
   sets for it: its name size (--sup-size), the space between its lines
   (--sup-rgap) and between the names on a line (--sup-cgap), each 10px as
   designed; 0 puts the capsules edge to edge. They are boxes in the row, so
   a bigger capsule moves the others along and the footer grows: they never
   overlap. */
.sf-tier{
  display:flex; flex-wrap:wrap; justify-content:center;
  row-gap:var(--sup-rgap, 10px); column-gap:var(--sup-cgap, 10px);
}

.sf-supporter-pill{
  font-family:var(--font-mono,'JetBrains Mono',monospace);
  /* Size is a setting. The padding follows it so a bigger name does not end up
     in a pill that still fits the old one. */
  font-size:var(--sup-size, 12.5px);
  /* The tier's capsule offsets are how far the capsule reaches past the name
     sideways (--sup-pad-x) and up and down (--sup-pad-y), and its corners
     are --sup-radius; at 0 the capsule's edge is the name's bounds. Its line
     is held in from the top of the capitals to the baseline (::before,
     ::after): a font's line has room above and below its letters that the
     sides do not, which would stay above and below at 0. Its outline is drawn
     inside it for the same reason. Unset they are the design: 2.62 times the
     name size and 2px tall, 1.12 times the size and 1px past it sideways. */
  line-height:1.3;
  padding:var(--sup-pad-y, calc(var(--sup-size, 12.5px) * .945 + 1px)) var(--sup-pad-x, calc(var(--sup-size, 12.5px) * 1.12 + 1px));
  border-radius:var(--sup-radius, 20px); background:var(--surface-2);
  box-shadow:inset 0 0 0 1px var(--line); color:var(--text-muted);
}
/* JetBrains Mono at line height 1.3: .28em above its capitals, .29em below
   its baseline. Pulled in by margins, not a smaller line height, so a long
   name that wraps keeps its lines apart. */
.sf-supporter-pill::before, .sf-supporter-pill::after,
.hd-pill[style*="--pill-x"]::before, .hd-pill[style*="--pill-x"]::after,
.hd-pill[style*="--pill-y"]::before, .hd-pill[style*="--pill-y"]::after,
.pw-note code[style*="--pill-x"]::before, .pw-note code[style*="--pill-x"]::after,
.pw-note code[style*="--pill-y"]::before, .pw-note code[style*="--pill-y"]::after{
  content:""; display:block; height:0;
}
.sf-supporter-pill::before,
.hd-pill[style*="--pill-x"]::before, .hd-pill[style*="--pill-y"]::before,
.pw-note code[style*="--pill-x"]::before, .pw-note code[style*="--pill-y"]::before{ margin-bottom:-.28em; }
.sf-supporter-pill::after,
.hd-pill[style*="--pill-x"]::after, .hd-pill[style*="--pill-y"]::after,
.pw-note code[style*="--pill-x"]::after, .pw-note code[style*="--pill-y"]::after{ margin-top:-.29em; }
/* Only the hue comes from the name; the saturation and lightness are fixed per
   theme, which is what keeps every name readable on both pages and lets a theme
   switch re-colour without re-rendering the footer.

   The lightness values are the ones that clear WCAG AA for body text at EVERY
   hue against the pill background, not a guess. Measured across all 36 names:
   the light theme was 3.43:1 at 34% lightness -- a yellow-green name on a near
   white pill is the worst case -- and 5.01:1 at 27%. Dark measures 5.76:1. */
body.sup-colour .sf-supporter-pill{ color:hsl(var(--sup-hue, 0) 62% 72%); }
:root[data-theme="light"] body.sup-colour .sf-supporter-pill{ color:hsl(var(--sup-hue, 0) 58% 27%); }
/* Forces the next pill onto a new line. flex-basis:100% makes it fill the rest
   of the current line, and height:0 keeps it from adding one of its own. The
   gap still applies, which is what gives the two lines their spacing. */
.sf-supporter-break{flex-basis:100%; height:0; margin:0; padding:0;}

/* How the names line up: the supporterAlign setting, which site.js writes on
   the body as data-sup-align. None is centred, as they always were. */
body[data-sup-align="left"] .sf-tier{ justify-content:flex-start; }
body[data-sup-align="right"] .sf-tier{ justify-content:flex-end; }
/* Justified. The names are boxes, not words, so text-align:justify does
   nothing to them; instead the room left on each full line is shared out
   between them. The filler that ends every group of names (site.js
   SUP_FILL) takes the room on the group's last line, which keeps that line
   to the left the way a justified paragraph's last line is, rather than two
   names being pushed to opposite edges. Its negative margin cancels the gap
   in front of it, so it never adds any width of its own. */
body[data-sup-align="justify"] .sf-tier{ justify-content:space-between; }
/* Every tier is justified, however many there are. A tier's own space between
   names (--sup-cgap) is the least there is between two names; justifying
   shares the rest of a full line on top of it. A rule here once turned
   justifying off for any tier given its own space, so only the first tier
   (which had none) was justified (Nov 2026). */
.sf-sup-fill{ display:none; }
body[data-sup-align="justify"] .sf-sup-fill{ display:block; flex:1 0 0px; height:0; margin:0 0 0 calc(-1 * var(--sup-cgap, 10px)); padding:0; }

.sf-supporters-empty{
  text-align:center; font-size:13px; color:var(--text-dim);
  font-family:var(--font-mono,'JetBrains Mono',monospace);
}

/* Patreon button */
.sf-patreon-row{ text-align:center; margin-bottom:38px; }
.sf-patreon-btn{
  display:inline-flex; align-items:center; gap:8px;
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:13px; font-weight:500;
  padding:12px 26px; border-radius:8px; background:var(--houdini); color:var(--on-accent,#0A0C10);
  transition:.2s;
}
.sf-patreon-btn:hover{ background:#FF9A5C; }

/* Socials — icon pill that expands into a gradient label on hover,
   adapted from the 21st.dev "Gradient Menu" component (React/Tailwind)
   into plain CSS to match this site's vanilla stack. */
.sf-socials-row{
  display:flex; justify-content:center; align-items:center;
  gap:18px; flex-wrap:wrap; margin-bottom:14px;
}
.social-pill{
  position:relative;
  /* Set rather than inherited: the two legal pages colour their prose links
     with a{color:var(--houdini)} so links in the legal text stand out, and
     without this that leaks into the footer and turns these icons orange on
     those two pages only. */
  color:var(--text);
  width:52px; height:52px;
  border-radius:999px;
  background:var(--surface-2);
  border:1px solid var(--line);
  display:flex; align-items:center; justify-content:center;
  overflow:hidden;
  cursor:pointer;
  transition:width .45s cubic-bezier(.4,0,.2,1), border-color .3s;
}
.social-pill:hover{
  width:152px;
  border-color:transparent;
}
.social-pill .pill-glow{
  position:absolute; inset:-8px; z-index:-1; border-radius:999px;
  background:linear-gradient(45deg, var(--g-from), var(--g-to));
  filter:blur(16px);
  opacity:0; transition:opacity .45s;
}
.social-pill:hover .pill-glow{ opacity:.55; }
.social-pill .pill-bg{
  position:absolute; inset:0; border-radius:999px;
  background:linear-gradient(45deg, var(--g-from), var(--g-to));
  opacity:0; transition:opacity .45s;
}
.social-pill:hover .pill-bg{ opacity:1; }
.social-pill .pill-icon{
  position:relative; z-index:1;
  display:flex; width:20px; height:20px; color:var(--text-muted);
  transition:transform .35s ease, opacity .3s ease;
}
.social-pill:hover .pill-icon{ transform:scale(0); opacity:0; }
.social-pill .pill-label{
  position:absolute; z-index:1;
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px; letter-spacing:.03em;
  color:#fff; white-space:nowrap;
  transform:scale(0); opacity:0;
  transition:transform .35s ease .08s, opacity .3s ease .08s;
}
.social-pill:hover .pill-label{ transform:scale(1); opacity:1; }

/* Useful links */
.sf-uselinks-row{
  display:flex; justify-content:center; align-items:center;
  gap:28px; flex-wrap:wrap;
  padding-bottom:30px; border-bottom:1px solid var(--line-soft); margin-bottom:26px;
}
.sf-uselinks-row a{
  font-size:13.5px; color:var(--text-muted); padding:5px 0; transition:.15s;
}
.sf-uselinks-row a:hover{ color:var(--houdini); }

.sf-bottom{
  display:flex; align-items:center; justify-content:center; gap:16px; flex-wrap:wrap;
}
.sf-copy{ font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11.5px; color:var(--text-dim); }

@media(max-width:640px){
  .sf-socials-row{ gap:12px; }
  .sf-uselinks-row{ gap:20px; }
}

/* Below this there is no room to expand a pill. Five collapsed ones plus one at
   152px needs 472px, and the footer's content box is narrower than that from
   about 536px of viewport down -- so the row wrapped to a second line the moment
   any pill was hovered: the footer jumped from 52px tall to 116px and the icons
   moved out from under the cursor. It read worst on the last pill, which landed
   on the second line by itself.

   The hover still answers -- the brand gradient and the glow both arrive -- but
   the pill keeps its size and the label stays out of it. Nothing is lost by
   that: every pill already carries the same name in its title attribute, and a
   narrow screen is usually a touch one, where there is no hover to begin with. */
@media(max-width:560px){
  .social-pill:hover{ width:52px; }
  .social-pill:hover .pill-icon{ transform:none; opacity:1; }
  .social-pill:hover .pill-label{ opacity:0; transform:scale(.8); }
}

/* ==========================================================================
   Mobile navigation
   Below 760px each page's own stylesheet sets .nav-links{display:none}, which
   hides the links and the theme toggle together. site.js injects .nav-burger
   into the bar; opening it reveals that same row as a drop-down panel.
   .nav-links.open outranks the pages' .nav-links rule on specificity, so this
   works without editing all nine HTML files.
   ========================================================================== */
.nav-burger{ display:none; }

@media(max-width:760px){
  /* The desktop bar stacks logo above links; on a phone it's one row. */
  .nav-inner{
    flex-direction:row; justify-content:space-between; align-items:center;
    padding:14px 20px;
  }

  .nav-burger{
    display:flex; flex-direction:column; justify-content:center; gap:5px;
    width:42px; height:38px; padding:0 9px;
    background:transparent; border:1px solid var(--line); border-radius:8px;
    cursor:pointer;
  }
  .nav-burger span{
    display:block; width:100%; height:2px; border-radius:2px;
    background:var(--text);
    transition:transform .2s ease, opacity .2s ease;
  }
  /* Bars fold into an X while the panel is open. */
  .nav-burger[aria-expanded="true"] span:nth-child(1){ transform:translateY(7px) rotate(45deg); }
  .nav-burger[aria-expanded="true"] span:nth-child(2){ opacity:0; }
  .nav-burger[aria-expanded="true"] span:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }

  .nav-links.open{
    display:flex; flex-direction:column; align-items:stretch; gap:0;
    position:absolute; top:100%; left:0; right:0;
    padding:8px 20px 16px;
    background:var(--bg);
    border-bottom:1px solid var(--line-soft);
    box-shadow:0 18px 30px rgba(0,0,0,.28);
  }

  /* Row rhythm comes from each link's own padding rather than a flex gap, so
     top-level links and dropdown children share one spacing scale instead of
     compounding (the dropdown's 9px pill padding sat on top of the gap and
     made the Assets group straggle). Full-width rows also give every link a
     proper tap target. */
  .nav-links.open .nav-scramble,
  .nav-links.open .nav-drop-menu-inner a{
    display:flex; width:100%; padding:11px 0; line-height:1.2;
  }

  /* Sub-links sit inline in the panel: the desktop dropdown opens on :hover,
     which never fires on a touch screen. */
  .nav-links.open .nav-drop{
    display:flex; flex-direction:column; align-items:stretch; gap:0; width:100%;
  }
  /* The desktop panel starts hidden and is revealed by :hover. Inline in the
     mobile sheet it is simply always open, so the hover-driven opacity,
     visibility and rise all have to be undone here -- otherwise the sub-links
     occupy their row while invisible. */
  .nav-links.open .nav-drop-menu{
    position:static; display:block; transform:none; padding-top:0;
    opacity:1; visibility:visible; transition:none;
  }
  .nav-links.open .nav-drop-menu-inner{
    background:transparent; border:none; box-shadow:none;
    padding:0 0 0 16px; gap:0; min-width:0;
    transform:none; transition:none;
  }
  /* align-self keeps the pill at its natural width: the panel stretches its
     children, which otherwise pulled the toggle across the full row and left
     its sliding indicator misaligned. */
  .nav-links.open .theme-toggle{ margin-top:14px; align-self:flex-start; }
}

/* ==========================================================================
   Page entrance
   Every body-level block fades up as the page opens, lightly staggered so the
   heading leads and the footer trails.

   Two exclusions. <nav> only fades: it is position:sticky, and animating a
   transform on it would make it a containing block for the duration. .hero is
   left alone because the canvas already runs its own grow-in reveal and the
   layer underneath carries the scroll transform — a second animation on the
   same property would fight it.

   .page-canvas is excluded for a sharper reason: it is a body child, so it was
   being caught by this rule, and an animation outranks every normal declaration
   in the cascade. hd-page-in drove its opacity and the theme rules below never
   got a say — which is why the field ignored the light theme's .55 entirely.
   The canvas has its own opacity transition and does not want a second one.
   ========================================================================== */
@keyframes hd-page-in{
  from{ opacity:0; transform:translateY(16px); }
  to{ opacity:1; transform:none; }
}
@keyframes hd-nav-in{
  from{ opacity:0; }
  to{ opacity:1; }
}

nav{ animation:hd-nav-in .45s ease both; }
/* Held, unseen, until its links are drawn (site.js, from /api/nav): faded in
   before them, the bar showed the logo and the light/dark switch alone, then
   the links landed mid-fade and pushed the switch and the logo along. The
   switch and the logo's own elements are drawn in the same moment. */
nav:has(#navLinksSlot:empty){ animation-play-state:paused; }

/* The delay rides a custom property rather than its own animation-delay
   rule: the selector below carries more specificity than a bare class, so a
   plain `.page-head{animation-delay:...}` would lose to this shorthand and
   reset itself to 0. Custom properties inherit instead of competing. */
/* .hd-rail (the section rail) is left out like the assistant: it is fixed to
   the screen and centred by its own transform, which this animation's
   transform would replace. */
body > *:not(nav):not(.hero):not(.page-canvas):not(script):not(style):not(link):not(.hd-bot):not(.hd-rail){
  animation:hd-page-in .6s cubic-bezier(.22,.61,.36,1) var(--hd-enter-delay, 0s) both;
}

.page-head{ --hd-enter-delay:.04s; }
main, .filters, .post-grid, .asset-grid{ --hd-enter-delay:.12s; }
.cta-strip{ --hd-enter-delay:.16s; }
.site-footer{ --hd-enter-delay:.2s; }

@media(prefers-reduced-motion:reduce){
  nav, body > *{ animation:none; }
  /* The dropdown still opens and closes -- only the fade and the rise go. These
     sit in each page's own stylesheet, which loads before this one, so equal
     specificity resolves here on source order. */
  .nav-drop-menu, .nav-drop:hover .nav-drop-menu,
  .nav-drop-menu-inner, .nav-drop:hover .nav-drop-menu-inner{ transition:none; }
  .nav-drop-menu-inner{ transform:none; }
}

/* ==========================================================================
   Reactive page background + staged entrance

   Shared by every page that opts in: contact, blog, assets, free assets,
   premium assets, and the two legal pages. The homepage is excluded (it has
   the hero grid instead) and so are individual blog posts.

   A page opts in with <body class="has-canvas"> plus the canvas element, and
   configures the motion through window.hojdeeEntrance before loading
   page-entrance.js. The CSS here holds the motion; the script only decides
   direction and timing, per element, through custom properties.
   ========================================================================== */

/* The canvas is masked so the field is strongest in the middle and dissolves
   into the page background at the edges — without the mask the network just
   stops at a hard rectangle. */
.page-canvas{
  position:fixed; inset:0; width:100vw; height:100vh;
  z-index:0; pointer-events:none;
  mask-image:radial-gradient(ellipse 68% 62% at 50% 45%, black 18%, transparent 82%);
  -webkit-mask-image:radial-gradient(ellipse 68% 62% at 50% 45%, black 18%, transparent 82%);
  opacity:0; transition:opacity 1.2s ease;
}
/* --field-opacity is this page's own setting, written onto <html> by site.js
   from fieldOpacity_<page>. It defaults to 1 so the field still appears at full
   strength if the fetch never lands. */
.page-canvas.is-ready{opacity:var(--field-opacity, 1);}
/* Light theme sits on near-white, where the same glow reads as haze: it is
   tuned lower in its own opacity (the light theme's setting is the opacity it
   shows; .55 is only where it starts if that setting never arrives). */
:root[data-theme="light"] .page-canvas{opacity:0;}
:root[data-theme="light"] .page-canvas.is-ready{opacity:var(--field-opacity, .55);}

/* The canvas is position:fixed, and a positioned element paints above
   non-positioned siblings whatever its z-index. Without this the glow would sit
   on top of the copy instead of behind it. nav is excluded because it is
   already position:sticky with its own z-index, and this would flatten it. */
/* .hero is excluded because it is position:sticky. Forcing position:relative on
   it would silently turn the homepage hero into a normal block — no pinning, no
   scroll choreography — the moment the homepage opted into a canvas. */
/* .g-lightbox is excluded for the same class of reason: gallery.js appends it
   to <body>, so it becomes a child of this selector and had its position:fixed
   replaced with relative and its z-index:200 with 1. The overlay then sat in
   the page flow instead of over it — the image rendered, at the right size,
   with the footer painted on top of it. Specificity is why it loses without
   this: (0,3,2) here against (0,1,0) for a lone class. */
/* .hd-bot is left out for the same reason as the lightbox: it is fixed to the
   SCREEN. Made relative here, the assistant sat at the bottom of the page, under
   the footer, instead of in the corner of the window. */
/* .hd-rail, the section rail, likewise: fixed to the screen. */
body.has-canvas > *:not(nav):not(.page-canvas):not(.hero):not(.g-lightbox):not(.hd-bot):not(.hd-rail){ position:relative; z-index:1; }

/* One heading size across all of these pages. */
.has-canvas .page-head h1{ font-size:clamp(46px, 9vw, 96px); line-height:1.05; }

/* Nothing animates in a tab the reader has not switched to yet.

   CSS animations are not requestAnimationFrame: their clock runs in a hidden
   tab, so opening a page in a background tab and getting to it ten seconds
   later meant the whole entrance had already been and gone -- everything was
   simply there. api.js sets this attribute from <head> when the page starts
   hidden and removes it on the first visibilitychange, so in the normal case
   the attribute never exists at all.

   Pausing holds the animation-delay as well as the animation, which is what
   makes this work without any page having to know what it animates.

   !important, and deliberately: the `animation` SHORTHAND resets
   animation-play-state to running, so every rule that sets `animation:` on a
   class -- .fly, .write-on .ltr, .hero-content > * -- would otherwise cancel
   this the moment its class was added. */
html[data-hold-fx] *,
html[data-hold-fx] *::before,
html[data-hold-fx] *::after{ animation-play-state:paused !important; }

/* The same hold while the page waits for its saved copy (page-entrance.js
   sets the attribute and takes it off once the copy is in, a moment at most):
   the page's rise, its eyebrow and its heading's letters start with the words,
   on the copy the reader will actually see, instead of on the copy written
   into the page. Held at their first frame, they are hidden. The nav is left
   to arrive as usual. */
html[data-entrance-wait] body > *:not(nav),
html[data-entrance-wait] body > *:not(nav) *,
html[data-entrance-wait] body > *:not(nav) *::before,
html[data-entrance-wait] body > *:not(nav) *::after{ animation-play-state:paused !important; }

/* ---- Entrance ----
   Every rule below only applies once the script has created .fly / .word /
   .write-on spans, so if the script never runs, nothing is ever hidden. */
.fly, .fly-word{
  opacity:0;
  transform:translate(var(--fx, 0px), var(--fy, 40px)) rotate(var(--rot, 0deg)) scale(var(--sc, 1));
  animation:flyIn var(--dur, .72s) var(--ease, cubic-bezier(.16,1,.3,1)) forwards;
  animation-delay:var(--d, 0s);
  will-change:transform, opacity;
}
/* Words are wrapped individually so each one can travel on its own. They must
   be inline-block to accept a transform; the spaces between them stay as real
   text nodes, so lines still wrap exactly where they did before.
   .word is the wrapper and starts hidden; .fly-word is added only when the
   piece is actually scheduled. Keeping them separate is what stops a word below
   the fold from animating on load with default values instead of waiting until
   it is scrolled to. */
.word{display:inline-block; white-space:pre; opacity:0;}
@keyframes flyIn{
  from{opacity:0; transform:translate(var(--fx, 0px), var(--fy, 40px)) rotate(var(--rot, 0deg)) scale(var(--sc, 1));}
  to{opacity:1; transform:translate(0,0) rotate(0deg) scale(1);}
}

/* The heading gets the same idea pushed much harder: each letter comes in from
   its own direction, further out, rotated, oversized and blurred, then
   overshoots slightly on the way to its place. The accent-to-text settle is the
   part that reads as "writing on". */
.write-on .ltr{
  display:inline-block;
  opacity:0;
  color:var(--houdini);
  transform:translate(var(--tx, 0px), var(--ty, 60px)) rotate(var(--rot, 0deg)) scale(1.9);
  filter:blur(14px);
  animation:letterIn .82s cubic-bezier(.18,1.15,.28,1) forwards,
            writeSettle .5s ease forwards;
  animation-delay:var(--d, 0s), calc(var(--d, 0s) + .5s);
  will-change:transform, opacity, filter;
}
/* Each word is a nowrap box so the line can only break at the real spaces
   between them, never between two of the inline-block letters inside one. */
.write-on .tw{display:inline-block; white-space:nowrap;}
@keyframes letterIn{
  from{
    opacity:0;
    transform:translate(var(--tx, 0px), var(--ty, 60px)) rotate(var(--rot, 0deg)) scale(1.9);
    filter:blur(14px);
  }
  60%{ opacity:1; filter:blur(0); }
  to{ opacity:1; transform:translate(0,0) rotate(0) scale(1); filter:blur(0); }
}
/* --ltr-ink is set by page-entrance.js on an element inside the heading that
   the panel gave a colour, so its letters settle to that colour, not the text's. */
@keyframes writeSettle{ to{color:var(--ltr-ink, var(--text));} }

.has-canvas .page-head .eyebrow{
  opacity:0;
  transform:translateY(-22px);
  animation:eyebrowIn .7s cubic-bezier(.16,1,.3,1) forwards;
  animation-delay:var(--d, 0s);
  letter-spacing:.12em;
}
@keyframes eyebrowIn{
  from{opacity:0; transform:translateY(-22px); letter-spacing:.4em;}
  to{opacity:1; transform:translateY(0); letter-spacing:.12em;}
}

/* ==========================================================================
   Page content blocks
   The same shapes the blog post editor produces. blog-post.html carries its own
   copy of these because it is not part of the CMS page system; these are for
   every page that has a #pageBlocks region.
   ========================================================================== */
.page-blocks{ max-width:760px; margin:0 auto; padding:0 32px; }
/* A homepage section of the owner's own spans what the built-in rows (Blog,
   Tools...) span, under its heading, rather than the narrow column of a page's text. */
.home-custom-section > .page-blocks{ max-width:none; padding:0; }
.home-custom-section[hidden]{ display:none !important; }
.page-blocks:empty{ display:none; }
.page-blocks .block{ margin-bottom:26px; }
.page-blocks .block-paragraph{ font-size:15.5px; color:var(--text-muted); line-height:1.75; }
/* Paragraphs of their own inside a Text element (aligned, justified): the
   space between them, and none before the first. */
.page-blocks .block-paragraph > p{ margin:0; }
.page-blocks .block-paragraph > p + p{ margin-top:.8em; }

/* ---- Pages built from elements (Free and Premium Assets) -------------------
   What those pages drew themselves before, for blocks: a Section in the page
   header's look, a note, a group's label heading, and a group of cards. The
   pages' own .page-head, .eyebrow and h1 rules still apply inside the header;
   these set what a block's own rules would otherwise change. */
/* --head-pad-top/-bottom: a page whose header sat otherwise says so (the
   Assets hub, the contact page). */
.page-blocks .block-section.page-head{ padding:var(--head-pad-top, 56px) 0 var(--head-pad-bottom, 10px); margin-bottom:0; text-align:center; }
/* A Section of centred prose (the contact page's About and Philosophy) and
   one drawn as a panel of its own (its contact block): what the page's
   .about-us, .philosophy and .contact-block drew, for elements. */
.page-blocks .block-section.look-prose{ padding-top:36px; margin-bottom:0; text-align:center; }
.page-blocks .look-prose .block{ margin-bottom:0; }
.page-blocks .look-prose .block-heading{ font-size:22px; margin:0 0 18px; line-height:inherit; }
.page-blocks .look-prose .block-paragraph{ font-size:15.5px; color:var(--text-muted); line-height:inherit; }
.page-blocks .look-prose p.block-paragraph, .page-blocks .look-prose .block-paragraph > p{ margin:0 0 16px; }
/* The last paragraph's space merged with the room after the section before;
   in a column it cannot, so it is not added. */
.page-blocks .look-prose .bs-col > p.block-paragraph:last-child,
.page-blocks .look-prose .bs-col > .block-paragraph:last-child > p:last-child{ margin-bottom:0; }
/* 640px wide, or on a phone the column and its 32px sides, edge to edge as it
   was. */
.page-blocks .block-section.look-panel{
  width:min(640px, calc(100% + 64px)); max-width:none; margin:56px 0 100px calc(50% - min(320px, 50% + 32px)); padding:36px 32px;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius, 10px); text-align:center;
}
.page-blocks .look-panel .block{ margin-bottom:0; }
.page-blocks .look-panel .block-heading, .page-blocks .look-panel .block-heading.level-3{ font-size:19px; margin:0 0 8px; line-height:1.5; }
.page-blocks .look-panel .block-paragraph{ font-size:14px; color:var(--text-muted); line-height:inherit; margin-bottom:24px; }
.page-blocks .look-panel .block-paragraph > p{ margin:0; }
.page-blocks .look-panel .block-paragraph:has(.contact-email){ margin-bottom:0; font-size:inherit; color:inherit; }
:where(.page-blocks) .contact-email{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:16px; color:var(--houdini);
  display:inline-block; margin-bottom:28px;
}
/* A legal document's text (the "doc" look): what .legal-article drew. A
   one-column one is laid out as plain text rather than a grid, so its parts
   follow on as the document's did: the space between two parts is the
   heading's, merged with the words' above it. */
.page-blocks .block-section.look-doc{ margin-bottom:0; font-size:15px; color:var(--text-muted); }
.page-blocks .look-doc .bs-cols:has(> .bs-col:only-child){ display:block; }
.page-blocks .look-doc .bs-col > .block-paragraph{ font-size:15px; line-height:inherit; color:inherit; margin-bottom:0; }
.page-blocks .look-doc .bs-col > p.block-paragraph, .page-blocks .look-doc .block-paragraph > p{ margin:0 0 14px; line-height:1.7; }
.page-blocks .look-doc .bs-col > .block-heading.level-3, .page-blocks .look-doc .block-paragraph > h3{
  color:var(--text); font-size:17px; margin:30px 0 10px; line-height:inherit;
}
.page-blocks .look-doc strong{ color:var(--text); }
.page-blocks .look-doc .block-paragraph :is(ul, ol){ margin:0 0 14px 22px; }
.page-blocks .look-doc .block-paragraph li{ margin-bottom:6px; line-height:1.6; }
.page-blocks .look-doc .block-paragraph table{ width:100%; border-collapse:collapse; margin:0 0 20px; }
.page-blocks .look-doc .block-paragraph :is(th, td){ text-align:left; padding:10px 12px; border:1px solid var(--line); font-size:13.5px; }
.page-blocks .look-doc .block-paragraph th{ background:var(--surface); color:var(--text); font-weight:500; }
/* A legal page's "Last updated" line. */
.page-blocks .block-updated{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px; color:var(--text-dim);
  text-align:center; margin-bottom:36px;
}
/* A note was the header's neighbour, below its padding: it keeps that place. */
.page-blocks .block-section.page-head:has(.is-note:last-child){ padding-bottom:0; }
.page-blocks .block-section.page-head .block{ margin-bottom:0; }
/* The eyebrow is a <p> now, and each page's own .page-head p rule (the
   intro's width and margin) is not for it. */
.page-blocks .page-head .block-paragraph.is-eyebrow{ max-width:none; margin:0; }
/* --head-title-mt: a page whose title sat right under its eyebrow says so
   (the contact page). */
.page-blocks .page-head .block-heading.level-title{ margin:var(--head-title-mt, 10px) 0 0; font-size:clamp(46px, 9vw, 96px); line-height:1.05; color:var(--text); }
/* --head-intro-w: a page whose intro was narrower says so (Free Assets). */
.page-blocks .page-head .block-paragraph:not(.is-eyebrow):not(.is-note){
  max-width:var(--head-intro-w, 600px); margin:14px auto 0; font-size:15px; line-height:1.5; color:var(--text-muted);
}
.page-blocks .page-head .block-paragraph p{ max-width:none; margin:0; font-size:inherit; color:inherit; }
.page-blocks .page-head .block-paragraph p + p{ margin-top:.8em; }
/* 640px wide, or on a phone the column and its 32px sides, edge to edge as it
   was. */
.page-blocks .block-paragraph.is-note{
  width:min(640px, calc(100% + 64px)); max-width:none; margin:32px 0 0 calc(50% - min(320px, 50% + 32px)); padding:14px 18px; text-align:center;
  background:var(--surface); border:1px solid var(--line); border-radius:8px;
  font-size:13.5px; line-height:1.5; color:var(--text-muted);
}
.page-blocks .block-paragraph.is-note code{
  font-family:var(--font-mono,'JetBrains Mono',monospace); background:var(--bg-2); border:1px solid var(--line);
  padding:2px 7px; border-radius:4px; color:var(--houdini);
}
.page-blocks .block-heading.level-label{
  display:flex; align-items:center; gap:14px; margin:52px 0 20px;
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:13px; font-weight:400; line-height:1.5;
  letter-spacing:.06em; text-transform:uppercase; color:var(--text-dim);
}
.page-blocks .block-heading.level-label::after{ content:""; flex:1; height:1px; background:var(--line); }
/* A group of cards: four across, three on a tablet, two on a phone, the
   page's card group settings over that. The cards' own look is the asset
   card's (below), so a group placed on any page looks as it does here. */
.page-blocks .block-assets{ margin-bottom:0; }
.page-blocks .bs-col > .block-assets:not(.is-split):not(.is-channel):first-child{ margin-top:40px; }
/* A Listing bar in an ordinary Section (the blog's sits in its toolbar). The
   header above has no room below it: a Section that starts with one brings
   its own, as the card grid does. Under it, the grid (or a group's heading)
   needs only the Section's own space. */
.page-blocks .block-section:not(.look-toolbar) .bs-col:has(> .block-listbar:first-child){ padding-top:40px; }
.page-blocks .block-section:has(.block-listbar) + .block-section .bs-col >
  :is(.block-assets:not(.is-split):not(.is-channel), .block-heading.level-label):first-child{ margin-top:0; }
/* In the blog's toolbar, the bar is the row. */
.page-blocks .look-toolbar .bs-col > .block-listbar{ flex:1 1 100%; min-width:0; }
.listbar-empty{
  text-align:center; padding:60px 20px; color:var(--text-dim);
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:13px;
}
/* A card, a group of them or a row of pills a Listing bar has left out. */
.page-blocks .is-filtered-out{ display:none !important; }
/* The big cards (the Assets hub's) and the QR cards (the contact page's),
   wherever one is placed. Weightless (:where), so the page that drew them
   before keeps its own look exactly. */
:where(.block-assets.is-split) .split{
  max-width:1000px; margin:50px auto 100px; padding:0 32px;
  display:grid; grid-template-columns:1fr 1fr; gap:24px;
}
@media(max-width:700px){ :where(.block-assets.is-split) .split{ grid-template-columns:1fr; } }
/* In a page's elements the column already has its sides: the cards keep the
   1000px row less its old 32px sides, and on a phone the column's width. */
.page-blocks .block-assets.is-split .split{ padding:0; max-width:936px; }
:where(.block-assets.is-split) .split-card{
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius, 10px);
  padding:36px 30px; display:flex; flex-direction:column; gap:14px; transition:.2s; color:inherit; text-decoration:none;
}
:where(.block-assets.is-split) .split-card:hover{ border-color:var(--houdini); transform:translateY(-3px); }
:where(.block-assets.is-split) .split-card .tag{
  align-self:flex-start; font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11px;
  padding:5px 12px; border-radius:20px; letter-spacing:.04em;
}
:where(.block-assets.is-split) .split-card.free .tag{ background:#12351f; color:#5FBF7A; border:1px solid #1e5334; }
:where(.block-assets.is-split) .split-card.premium .tag{ background:#3a2a12; color:var(--houdini); border:1px solid #5a3f1c; }
:where(.block-assets.is-split) .split-card > .asset-thumb:not(.has-img){ display:none; }
:where(.block-assets.is-split) .split-card > .asset-thumb{ aspect-ratio:16/9; }
:where(.block-assets.is-split) .split-card > .asset-thumb > img{ object-fit:contain; }
:where(.block-assets.is-split) .split-card h2{ font-size:22px; }
:where(.block-assets.is-split) .split-card p{ color:var(--text-muted); font-size:14px; flex:1; }
:where(.block-assets.is-split) .split-card .go{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:13px; color:var(--text);
  display:inline-flex; align-items:center; gap:6px;
}
:where(.block-assets.is-split) .split-card:hover .go{ color:var(--houdini); }
:where(.block-assets.is-channel) .contact-channels{ display:flex; flex-wrap:wrap; gap:16px; justify-content:center; }
:where(.block-assets.is-channel) .channel-card{
  display:flex; flex-direction:column; align-items:center; gap:10px;
  padding:20px 22px; background:var(--surface-2); border:1px solid var(--line); border-radius:8px;
  min-width:150px; transition:.2s; color:inherit; text-decoration:none;
}
:where(.block-assets.is-channel) .channel-card:hover{ border-color:var(--houdini); }
:where(.block-assets.is-channel) .channel-card .name{ font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:13px; }
:where(.block-assets.is-channel) .channel-card img{ width:96px; height:96px; border-radius:6px; background:#fff; padding:4px; }
.block-assets .asset-grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:20px; max-width:none; margin:0; padding:0; }
@media(max-width:900px){ .block-assets .asset-grid{ grid-template-columns:repeat(3,1fr); } }
@media(max-width:640px){ .block-assets .asset-grid{ grid-template-columns:repeat(2,1fr); } }
.page-blocks .block-section:not(.look-panel):has(> .bs-cols .block-assets){ margin-bottom:0; }
.block-assets .asset-card{
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius, 10px);
  overflow:hidden; display:flex; flex-direction:column; transition:.2s; color:inherit; text-decoration:none;
}
.block-assets .asset-card:hover{ border-color:var(--houdini); transform:translateY(-3px); }
.block-assets .asset-thumb{
  aspect-ratio:1/1; background:
    linear-gradient(135deg, var(--surface-2), var(--bg-2)),
    radial-gradient(circle at 30% 30%, var(--unreal-glow), transparent 60%);
  background-size:cover; background-position:center;
}
/* A card with no picture keeps its picture area, a square of gradient, unless
   its Asset cards element is set to compact in Edit pages (audit UX-02 made
   every one compact; the owner wanted the choice, Nov 2026). */
.block-assets.no-pic-compact .asset-thumb:not(.has-img){ display:none; }
.block-assets .asset-body{ padding:14px 14px 16px; display:flex; flex-direction:column; gap:10px; flex:1; }
.block-assets .asset-body h4{ font-size:14px; font-weight:500; line-height:1.35; flex:1; }
.block-assets .asset-cta{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11.5px; text-align:center;
  padding:8px 0; border:1px solid var(--line); border-radius:6px; color:var(--text-muted); transition:.2s;
}
.block-assets .asset-card:hover .asset-cta{ border-color:var(--houdini); color:var(--houdini); }
.page-blocks .block-heading{ font-size:22px; margin-top:14px; margin-bottom:4px; color:var(--text); }
.page-blocks .block-image img{ width:100%; border-radius:8px; border:1px solid var(--line); display:block; }
/* A heading block at the smaller of the two sizes the site actually styles:
   the same 18px the free HTML region gives an h3. */
.page-blocks .block-heading.level-3{ font-size:18px; }
/* Width sits on the wrapper rather than the picture so the caption tracks it,
   and alignment is the margin left over. A picture with neither keeps the whole
   column and carries no class at all. */
.page-blocks .block-image.w-75{ max-width:75%; }
.page-blocks .block-image.w-50{ max-width:50%; }
.page-blocks .block-image.w-33{ max-width:33%; }
/* Any other size, from the slider: its percentage in --img-w. */
.page-blocks .block-image.w-custom{ max-width:var(--img-w, 100%); }
.page-blocks .block-image.align-center{ margin-left:auto; margin-right:auto; }
.page-blocks .block-image.align-right{ margin-left:auto; margin-right:0; }
/* A third of a phone screen is a thumbnail nobody can see. */
@media(max-width:640px){
  .page-blocks .block-image.w-75,
  .page-blocks .block-image.w-50,
  .page-blocks .block-image.w-33,
  .page-blocks .block-image.w-custom{ max-width:none; }
}
.page-blocks .cap{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px; color:var(--text-dim);
  margin-top:8px; text-align:center;
}
.page-blocks .img-fallback, .page-blocks .video-fallback{
  width:100%; aspect-ratio:16/9; border-radius:8px; border:1px dashed var(--line);
  display:flex; align-items:center; justify-content:center;
  color:var(--text-dim); font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px;
}
.page-blocks .video-wrap{
  position:relative; width:100%; aspect-ratio:16/9; border-radius:8px;
  overflow:hidden; border:1px solid var(--line);
}
.page-blocks .video-wrap iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; }
/* A pasted embed has no idea how wide this column is, so anything oversized
   scrolls inside its own box rather than pushing the page sideways. */
/* A button, a card, a divider and a spacer: the blocks the element panel in
   Edit pages can drop onto a page. The button is drawn by the same rules as the
   site's other solid buttons, colours and animations included. */
.page-blocks .block-button.align-center{ text-align:center; }
.page-blocks .block-button.align-right{ text-align:right; }
.btn-block{
  display:inline-flex; align-items:center; gap:8px; position:relative; text-decoration:none;
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:13px; font-weight:500;
  padding:12px 26px; border-radius:8px; transition:.2s;
  background:var(--btn-bg, var(--houdini)); color:var(--btn-ink, var(--on-accent,#0A0C10));
  border:1px solid var(--btn-border, transparent);
  --btn-wash:var(--btn-fx, #0A0C10); --btn-wash-ink:var(--btn-fx-ink, var(--houdini));
  --btn-edge:var(--btn-fx, #0A0C10); --btn-glow:var(--btn-fx-glow, rgba(10,12,16,.30));
  --trace-inset:3px;
}
.btn-block:hover{ background:var(--btn-hover, #FF9A5C); }
.page-blocks .block-card{
  display:block; overflow:hidden; text-decoration:none; color:inherit;
  background:var(--card-bg, var(--surface)); border:1px solid var(--card-border, var(--line));
  border-radius:var(--radius, 10px); transition:border-color .25s, transform .25s;
}
.page-blocks a.block-card:hover{ border-color:var(--card-accent, var(--houdini)); transform:translateY(-3px); }
.page-blocks .block-card img{ display:block; width:100%; max-height:320px; object-fit:cover; }
.page-blocks .block-card-body{ padding:20px 22px 22px; }
.page-blocks .block-card h3{ font-size:18px; font-weight:600; margin:0 0 8px; color:var(--card-title, var(--text)); }
.page-blocks .block-card p{ font-size:14px; color:var(--card-text, var(--text-muted)); margin:0; line-height:1.6; }
/* A section: an optional heading over up to four columns of blocks, side by
   side on a wide screen and stacked on a narrow one. */
.page-blocks .block-section{ margin-bottom:34px; }
.page-blocks .block-section-head{ font-size:22px; margin:0 0 16px; color:var(--text); }
.page-blocks .bs-cols{ display:grid; grid-template-columns:repeat(var(--cols, 1), minmax(0, 1fr)); gap:26px; align-items:start; }
.page-blocks .bs-col > .block:last-child{ margin-bottom:0; }
/* minmax(0, 1fr), not 1fr: a plain 1fr column will not shrink below its
   widest piece, so a button or picture given a fixed width in Edit pages
   (470px on Contact) pushed the whole page past a phone's edge (audit RESP-01). */
@media (max-width:760px){ .page-blocks .bs-cols{ grid-template-columns:minmax(0, 1fr); } }
.page-blocks .bs-col{ min-width:0; }
.page-blocks .block-divider{ border:0; border-top:1px solid var(--line); margin:34px 0; }
.page-blocks .block-spacer{ margin:0; }
.page-blocks .block-html{ overflow-x:auto; }
.page-blocks .block-html > *{ max-width:100%; }
.page-blocks .block-html img, .page-blocks .block-html svg,
.page-blocks .block-html iframe, .page-blocks .block-html video,
.page-blocks .block-html canvas{ max-width:100%; height:auto; }

@media (prefers-reduced-motion: reduce){
  .fly, .fly-word, .write-on .ltr, .has-canvas .page-head .eyebrow{
    animation:none; opacity:1; transform:none; filter:none; color:inherit;
  }
}

/* ===========================================================================
   Card hover effects
   ---------------------------------------------------------------------------
   Six effects for the Tools and Free Assets cards, chosen in the admin panel
   under Site Settings. They COMPOSE: any subset can be on at once, which is why
   this is built the way it is rather than as six mutually exclusive themes.

   Two consequences of that requirement drive the whole design:

   1. Transforms compose through variables, not through separate rules. "Lift"
      and "Tilt" both want to transform the card, and an element has exactly one
      transform property - a second rule replaces the first, it does not add to
      it. So there is ONE transform here, assembled from --fx-lift/--fx-rx/--fx-ry,
      and each effect only sets its own variable.

   2. Overlays are real elements, not pseudo-elements. Spotlight, sheen and the
      border trace each need their own stacking layer, and a card has only
      ::before and ::after - with ::before already spent on .asset-card's
      gradient. site.js injects a .card-fx wrapper on first hover instead, so
      all three can be on together.

   Effects are switched by a class on <body> (hv-lift, hv-spotlight, hv-border,
   hv-tilt, hv-sheen, hv-rise) written by site.js from the cardHover setting.
   Nothing below applies unless its class is present, so an unselected effect
   costs nothing.
   =========================================================================== */

/* Every card that takes the hover effects carries .fx-card, stamped on by the
   sweep in site.js. One class rather than an enumeration of card types: the set
   grew from two to eight (tools, free assets, tutorials, the assets hub's split
   cards, the contact page's channel cards and three blog layouts) and every rule
   below would otherwise have to name all of them, twice, once for :hover. */
.fx-card{
  /* The overlay layers are absolutely positioned against the card and clip
     themselves to its rounded corners (.card-fx, .card-reveal-clip). The card
     does not clip: a button in it pulled toward the pointer (a CTA card's) went
     past its edge and was cut off. A card that clips a picture of its own
     still does, in its page's stylesheet. */
  position:relative;
  --fx-lift:0px; --fx-rx:0deg; --fx-ry:0deg;
  /* Defaults for any card type with no line of its own below. */
  --fx-accent:var(--card-accent, var(--houdini)); --on-fx:var(--on-accent,#0A0C10);
  --fx-glow:var(--card-glow, var(--houdini-glow));
  --fx-rise:-9px;
}
/* Per-card-type accent. Tools and tutorials read as Unreal blue, everything
   else as Houdini orange, matching the borders they already had. */
/* --fx-rise is how far content lifts for "rise & reveal", set per card type
   because the cards hold their content differently. A tool card spreads its
   content down the box, so a small nudge reads as a lift. An asset card
   bottom-aligns its title 16px above the edge and the bar that slides in is 36px
   tall, so at -9px the title ends up UNDER the bar. It has to clear the bar's
   height, not merely move. */
.tool-card{ --fx-accent:var(--card-accent, var(--unreal)); --on-fx:var(--on-accent2,#0A0C10); --fx-glow:var(--card-glow, var(--unreal-glow)); --fx-rise:-9px; }
.tut-card{ --fx-accent:var(--card-accent, var(--unreal)); --on-fx:var(--on-accent2,#0A0C10); --fx-glow:var(--card-glow, var(--unreal-glow)); --fx-rise:-9px; }
.asset-card{ --fx-accent:var(--card-accent, var(--houdini)); --fx-glow:var(--card-glow, var(--houdini-glow)); --fx-rise:-26px; }

/* The scope classes sit on the CARD GROUP's container, not on <body>, which is
   what lets one page run different effects on different rows. A card outside
   every registered container simply gets none — see site.js. A Card element
   carries its own on itself, hence the second half of each :is(). */

/* ---- the single composed transform ----
   Only applied when a transform-based effect is on, so cards keep their normal
   stacking and rendering when neither is selected. perspective() lives inside
   the transform rather than on the grid, because the grids differ per page and
   this needs no knowledge of any of them. */
:is(.hv-lift .fx-card, .fx-card.hv-lift),
:is(.hv-tilt .fx-card, .fx-card.hv-tilt){
  transform:translateY(var(--fx-lift)) perspective(900px)
            rotateX(var(--fx-rx)) rotateY(var(--fx-ry));
  transform-style:preserve-3d;
  transition:transform .25s cubic-bezier(.2,.7,.3,1), border-color .25s, box-shadow .25s;
}
/* Tilt has to keep up with the pointer, so it overrides the easing above with
   something short and linear. A 0.25s ease reads as the card lagging behind the
   cursor rather than following it. */
:is(.hv-tilt .fx-card, .fx-card.hv-tilt){
  transition:transform .12s linear, border-color .25s, box-shadow .25s;
}

/* ---- 1. Lift and glow ---- */
:is(.hv-lift .fx-card, .fx-card.hv-lift):hover{
  --fx-lift:-6px;
  border-color:var(--fx-accent);
  box-shadow:0 14px 34px -12px var(--fx-glow);
}

/* ---- 4. Tilt toward the cursor ----
   --fx-rx/--fx-ry are written by site.js from the pointer's position. */
:is(.hv-tilt .fx-card, .fx-card.hv-tilt):hover{
  border-color:var(--fx-accent);
  box-shadow:0 18px 40px -18px var(--fx-glow);
}

/* ---- the injected overlay stack ----
   Sits above .asset-card::before (the card gradient) but below the text, which
   site.js lifts to z-index 2. */
.card-fx{
  position:absolute; inset:0; pointer-events:none; z-index:1;
  border-radius:inherit; overflow:hidden;
}
.card-fx > span{ position:absolute; inset:0; display:block; }

/* ---- 2. Cursor spotlight ---- */
.card-fx-spot{ opacity:0; transition:opacity .3s; }
.hv-spotlight .card-fx-spot{
  background:radial-gradient(240px circle at var(--fx-mx,50%) var(--fx-my,50%),
             var(--fx-glow), transparent 70%);
}
:is(.hv-spotlight .fx-card, .fx-card.hv-spotlight):hover .card-fx-spot{ opacity:1; }
:is(.hv-spotlight .fx-card, .fx-card.hv-spotlight):hover{ border-color:var(--fx-accent); }

/* ---- 5. Sheen sweep ----
   A one-shot band that crosses the card on entry. Only the band moves, so it is
   a narrow inset rather than a full-size layer. */
.card-fx-sheen{
  display:none;
  left:-60%; right:auto; width:45%; transform:skewX(-18deg);
  background:linear-gradient(90deg, transparent, rgba(255,255,255,.10), transparent);
  transition:left .55s cubic-bezier(.3,.7,.3,1);
}
.hv-sheen .card-fx-sheen{ display:block; }
:root[data-theme="light"] .card-fx-sheen{
  background:linear-gradient(90deg, transparent, rgba(0,0,0,.055), transparent);
}
:is(.hv-sheen .fx-card, .fx-card.hv-sheen):hover .card-fx-sheen{ left:115%; }

/* ---- 3. Gradient border trace ----
   A gradient filled shape masked down to a 1px frame: paint the whole box, then
   punch out everything except the padding ring with mask-composite. */
.card-fx-edge{
  display:none; inset:-1px; border-radius:inherit;
  padding:1px; opacity:0; transition:opacity .35s;
  background:linear-gradient(120deg, var(--fx-accent), transparent 40%,
             transparent 60%, var(--fx-accent));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
}
.hv-border .card-fx-edge{ display:block; }
:is(.hv-border .fx-card, .fx-card.hv-border):hover .card-fx-edge{ opacity:1; }

/* ---- 6. Rise and reveal ----
   Content lifts and a call-to-action bar slides up from the bottom. The bar's
   text is taken from whatever CTA the card already carries, so it never
   contradicts the card it is on. */
.card-reveal-clip{
  position:absolute; left:0; right:0; bottom:0; z-index:3;
  overflow:hidden; pointer-events:none;
  border-bottom-left-radius:inherit; border-bottom-right-radius:inherit;
}
.card-reveal{
  position:relative;
  display:none; padding:11px 16px;
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11px; line-height:1.3;
  color:var(--on-fx,#0A0C10); background:var(--fx-accent);
  transform:translateY(100%);
  transition:transform .3s cubic-bezier(.2,.7,.3,1);
  pointer-events:none;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.hv-rise .card-reveal{ display:block; }
/* .free-pill is absolutely positioned in the card's top corner - lifting it
   moves it away from that corner, which reads as a mistake rather than an
   effect. Everything else in the card rises together. */
:is(.hv-rise .fx-card, .fx-card.hv-rise) > *:not(.card-reveal-clip):not(.card-fx):not(.free-pill){
  transition:transform .3s cubic-bezier(.2,.7,.3,1);
}
:is(.hv-rise .fx-card, .fx-card.hv-rise):hover > *:not(.card-reveal-clip):not(.card-fx):not(.free-pill){
  transform:translateY(var(--fx-rise));
}
:is(.hv-rise .fx-card, .fx-card.hv-rise):hover .card-reveal{ transform:translateY(0); }
:is(.hv-rise .fx-card, .fx-card.hv-rise):hover{ border-color:var(--fx-accent); }
/* On a card that is not a link (an update card) the bar is its link: it takes
   the click once it is up, rather than covering the link it repeats. */
a.card-reveal-link{ text-decoration:none; cursor:pointer; }
:is(.hv-rise .fx-card, .fx-card.hv-rise):hover a.card-reveal-link{ pointer-events:auto; }
a.card-reveal-link:hover{ text-decoration:underline; }

/* Same rule the rest of the site follows: no motion for anyone who asked not to
   have any. The colour changes stay - they carry the hover affordance. */
@media (prefers-reduced-motion: reduce){
  :is(.hv-lift .fx-card, .fx-card.hv-lift), :is(.hv-tilt .fx-card, .fx-card.hv-tilt){
    transform:none; transition:border-color .25s, box-shadow .25s;
  }
  .card-fx-sheen{ display:none !important; }
  .card-reveal, :is(.hv-rise .fx-card, .fx-card.hv-rise) > *{ transition:none; }
}

/* The reveal bar repeats the card's own call to action, so the original fades
   out as the bar rises. Without this a hovered card shows the same words twice,
   once in the lifted content and once in the bar under it. */
.hv-rise .card-cta-src{ transition:opacity .22s; }
:is(.hv-rise .fx-card, .fx-card.hv-rise):hover .card-cta-src{ opacity:0; }

/* ===========================================================================
   Button animations
   ---------------------------------------------------------------------------
   Five effects, chosen in the admin panel under Site Settings and stored in one
   `buttonFx` setting. They COMPOSE, the same way the card hover effects do, and
   are switched by classes on <body> (bf-sweep, bf-press, bf-trace, bf-roll,
   bf-ripple) written by site.js. Nothing below applies unless its class is
   present, so an unselected effect costs nothing.

   Default is roll + ripple: the roll answers the pointer arriving, the ripple
   answers the press. Before the ripple existed the site had no click feedback on
   any button at all.

   Applied to .stat-btn, .sf-patreon-btn, .view-all and .filter-pill. Everything
   is keyed on class and never on element, because only .filter-pill is a real
   <button> — the rest are <a>. .nav-cta is included because its styles exist on
   every page even though no markup currently renders one; if one is ever added
   it gets this for free.
   =========================================================================== */

.btn-fx, .btn-fx-own{
  /* The overlays are absolutely positioned inside the control and must clip to
     it, and the roll needs a positioning context. None of the real button styles
     declare either.
     isolation:isolate is what lets the sweep sit at z-index:-1 — behind the
     label but still in front of the button's own background. */
  position:relative;
  overflow:hidden;
  isolation:isolate;
}

/* ---- Per-button-type palette ----
   The two solid orange buttons and the outlined/text ones need opposite
   treatments, and this is the whole reason these variables exist rather than
   the effects just using --houdini directly.

   On a solid orange button an orange sweep is orange-on-orange and an orange
   traced border is invisible against the fill it sits on — the effect fires and
   nothing appears to happen. So the solid buttons wash and trace DARK, and flip
   their label to the accent; the outlined ones wash and trace with the accent
   and flip their label to the dark ink. */
.stat-btn, .sf-patreon-btn{
  --btn-wash:var(--btn-fx, #0A0C10);
  --btn-wash-ink:var(--btn-fx-ink, var(--houdini));
  --btn-edge:var(--btn-fx, #0A0C10);
  --btn-glow:var(--btn-fx-glow, rgba(10,12,16,.30));
}
.view-all, .filter-pill, .nav-cta{
  --btn-wash:var(--btn-fx, var(--houdini));
  --btn-wash-ink:var(--btn-fx-ink, #0A0C10);
  --btn-edge:var(--btn-fx, var(--houdini));
  --btn-glow:var(--btn-fx-glow, var(--houdini-glow, rgba(255,122,51,.33)));
}

/* Every effect below is written twice in one rule: "body.bf-X .btn-fx" for
   the page-wide setting, and ".btn-fx-own.bo-X" for a button that has its
   own (an article's button with data-own-fx; see decorateButton() in
   site.js). Such a button is .btn-fx-own and never .btn-fx, so the page
   setting does not reach it. Add a rule here, add both forms. */
/* ==================================================================
   01. Fill sweep — the wash wipes across from the left
   ================================================================== */
body.bf-sweep .btn-fx, .btn-fx-own.bo-sweep{ transition:color .28s ease, border-color .28s ease; }
body.bf-sweep .btn-fx::before, .btn-fx-own.bo-sweep::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background:var(--btn-wash);
  transform:scaleX(0);
  transform-origin:left center;
  transition:transform .34s cubic-bezier(.4,.8,.3,1);
}
body.bf-sweep .btn-fx:hover::before, .btn-fx-own.bo-sweep:hover::before{ transform:scaleX(1); }
body.bf-sweep .btn-fx:hover, .btn-fx-own.bo-sweep:hover{ color:var(--btn-wash-ink); border-color:var(--btn-wash); }
/* A section link has no box to fill, so its sweep grows the underline instead of
   painting a block behind the words. */
body.bf-sweep .view-all.btn-fx::before{
  inset:auto 0 0 0;
  height:1px;
  z-index:0;
}
body.bf-sweep .view-all.btn-fx:hover{ color:var(--houdini); }

/* ==================================================================
   02. Press & lift — rises on hover, depresses on the press
   ================================================================== */
body.bf-press .btn-fx, .btn-fx-own.bo-press{
  transition:transform .18s cubic-bezier(.2,.7,.3,1), box-shadow .18s,
             background .18s, color .18s, border-color .18s;
}
body.bf-press .btn-fx:hover, .btn-fx-own.bo-press:hover{ transform:translateY(-2px); }
body.bf-press .btn-fx:active, .btn-fx-own.bo-press:active{ transform:translateY(1px); transition-duration:.06s; }
body.bf-press .btn-fx:hover, .btn-fx-own.bo-press:hover{ box-shadow:0 8px 20px -10px var(--btn-glow); }

/* ==================================================================
   03. Border trace — a line runs around the edge and settles
   The outline is an SVG rect built by site.js, because a border cannot be
   animated around its own perimeter and the dash length has to equal the real
   rounded-rectangle perimeter, which is only knowable after layout.
   ================================================================== */
/* The ring is drawn OUTSIDE the control by default, which is why its colour has
   to contrast with the PAGE, not with the button. That distinction was got wrong
   first time: the solid buttons were given a dark stroke to contrast with their
   orange fill, but the ring never touches that fill — it sits just beyond it, on
   the dark page, where dark-on-dark made it invisible in the dark theme.

   So the solid buttons move their ring INSIDE instead, onto the fill it was
   always meant to contrast with. --trace-inset is read by site.js, which sizes
   the SVG from it; positive insets sit within the control, negative ones beyond
   it. */
.btn-trace{
  position:absolute;
  inset:var(--trace-inset, -1px);
  pointer-events:none;
  overflow:visible;
  display:none;
}
.stat-btn, .sf-patreon-btn{ --trace-inset:3px; }
body.bf-trace .btn-trace, .btn-fx-own.bo-trace > .btn-trace{ display:block; }
/* The traced outline sits just outside the fill, so the control must not clip
   it away. The ripple is the only thing that needed the clip, and it is round
   and well inside the box, so letting this overflow costs nothing. */
body.bf-trace .btn-fx, .btn-fx-own.bo-trace{ overflow:visible; }

/* ==================================================================
   06. Grow — the control swells a little under the pointer
   Written as `scale` rather than a transform, so it composes with the
   press effect's lift instead of overwriting it.
   ================================================================== */
body.bf-grow .btn-fx, .btn-fx-own.bo-grow{ transition:scale .18s cubic-bezier(.2,.7,.3,1); }
body.bf-grow .btn-fx:hover, .btn-fx-own.bo-grow:hover{ scale:1.06; }
body.bf-grow .btn-fx:active, .btn-fx-own.bo-grow:active{ scale:1.02; transition-duration:.06s; }

/* The words on every button sit in the middle of it, however wide it is. */
.stat-btn, .sf-patreon-btn, .view-all, .filter-pill, .nav-cta, .btn-block{ text-align:center; }
.stat-btn, .sf-patreon-btn, .nav-cta, .btn-block{ justify-content:center; }
.btn-roll, .btn-roll-a, .btn-roll::after{ text-align:center; }
.btn-trace rect{
  fill:none;
  stroke:var(--btn-edge);
  stroke-width:1.5;
  stroke-dasharray:var(--peri) var(--peri);
  stroke-dashoffset:var(--peri);
  transition:stroke-dashoffset .55s cubic-bezier(.4,.8,.3,1);
}
body.bf-trace .btn-fx:hover .btn-trace rect, .btn-fx-own.bo-trace:hover .btn-trace rect{ stroke-dashoffset:0; }

/* ==================================================================
   04. Label roll — the label rolls out, a copy rolls in
   ================================================================== */
.btn-roll{
  display:block;
  position:relative;
  overflow:hidden;
}
.btn-roll-a{
  display:block;
  transition:transform .32s cubic-bezier(.3,.8,.3,1);
}
/* The incoming copy is a pseudo-element, NOT a second span, and that is the
   whole point. Duplicating the label as real text made button.textContent
   return it twice — a pill read back as "CGICGI" — which silently breaks any
   code that reads a control's text, and doubles what a user copies. Generated
   content never enters textContent, so the label stays honest.
   It is out of flow as well, so the control keeps the height of a single label
   instead of doubling. */
.btn-roll::after{
  content:attr(data-label);
  position:absolute;
  inset:0;
  transform:translateY(105%);
  transition:transform .32s cubic-bezier(.3,.8,.3,1);
  /* Set by wrapLabel() from the label's own look, so a label sized or coloured
     in the panel rolls in the same way it rolls out. */
  font-size:var(--roll-size, inherit);
  font-weight:var(--roll-weight, inherit);
  font-style:var(--roll-style, inherit);
  letter-spacing:var(--roll-spacing, inherit);
  color:var(--roll-ink, inherit);
}
body.bf-roll .btn-fx:hover .btn-roll-a, .btn-fx-own.bo-roll:hover .btn-roll-a{ transform:translateY(-105%); }
body.bf-roll .btn-fx:hover .btn-roll::after, .btn-fx-own.bo-roll:hover .btn-roll::after{ transform:translateY(0); }

/* ==================================================================
   05. Ripple — a ring expands from where the pointer landed
   Positioned and sized in JS.
   ================================================================== */
.btn-ripple{
  position:absolute;
  border-radius:50%;
  pointer-events:none;
  transform:translate(-50%,-50%) scale(0);
  background:var(--btn-fx-glow, var(--houdini-glow, rgba(255,122,51,.33)));
  animation:btnRipple .55s ease-out forwards;
}
/* On the solid orange buttons an orange ring is invisible, so those darken. */
.stat-btn .btn-ripple,
.sf-patreon-btn .btn-ripple{ background:var(--btn-fx-glow, rgba(10,12,16,.22)); }
@keyframes btnRipple{
  to{ transform:translate(-50%,-50%) scale(1); opacity:0; }
}

/* The disabled stat button is an inert <span> with no destination. It must not
   look pressable, so it gets none of the five. */
.stat-btn.is-disabled{ pointer-events:none; }

@media (prefers-reduced-motion: reduce){
  /* The colour changes stay — they carry the affordance. Only the motion goes,
     and with it the ripple, whose cleanup no longer depends on an animation that
     never runs (see the timeout in site.js). */
  body.bf-sweep .btn-fx::before, .btn-fx-own.bo-sweep::before{ transition:none; transform:scaleX(1); opacity:0; }
  body.bf-sweep .btn-fx:hover::before, .btn-fx-own.bo-sweep:hover::before{ opacity:1; }
  body.bf-press .btn-fx:hover, body.bf-press .btn-fx:active, .btn-fx-own.bo-press:hover, .btn-fx-own.bo-press:active{ transform:none; }
  .btn-trace rect{ transition:none; }
  .btn-roll-a, .btn-roll::after{ transition:none; }
  body.bf-roll .btn-fx:hover .btn-roll-a, .btn-fx-own.bo-roll:hover .btn-roll-a{ transform:none; }
  body.bf-roll .btn-fx:hover .btn-roll::after, .btn-fx-own.bo-roll:hover .btn-roll::after{ transform:translateY(105%); }
  .btn-ripple{ display:none; }
}


/* A media-library video inside a content block. Unlike an embed, it is not
   forced to 16/9 -- a screen recording is often a different shape, and boxing
   it into 16/9 would letterbox it and waste half the width. */
.block-video.is-hosted > video{
  width: 100%;
  height: auto;
  display: block;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: #000;
}


/* ===========================================================================
   Colours chosen in the panel
   ---------------------------------------------------------------------------
   Edit pages stores a colour per token per scope; site.js turns those into a
   small stylesheet of custom properties (see public/color-scopes.js). Nothing
   here changes the design: every property falls back to exactly the colour it
   already had, so a token nobody has set is the original.

   This sits at the end of site.css deliberately. The card and button colours
   are declared in each page's own <style> block, and site.css is linked after
   those — so these win on order, without editing every page.
   =========================================================================== */
.tool-card, .asset-card, .tut-card, .split-card,
.l1-card, .l1-feature, .l3-card, .l5-card, .stat-card{
  background:var(--card-bg, var(--surface));
  border-color:var(--card-border, var(--line));
}
.channel-card{ background:var(--card-bg, var(--surface-2)); border-color:var(--card-border, var(--line)); }
/* An asset card paints a gradient OVER its background, so the card colour has
   to be the gradient's own base or setting it would do nothing at all. */
/* An asset card's own picture fills its square thumbnail. With one for each
   theme, only the theme's own shows (a see-through picture with light lines
   for the dark theme would vanish on the light card). */
.asset-thumb.has-img{ position:relative; overflow:hidden; }
.asset-thumb > img{ display:block; width:100%; height:100%; object-fit:cover; }
.asset-thumb > img.thumb-light{ display:none; }
:root[data-theme="light"] .asset-thumb > img.thumb-light{ display:block; }
:root[data-theme="light"] .asset-thumb > img.thumb-dark{ display:none; }
.asset-card::before{
  background:
    radial-gradient(120% 100% at 100% 0%, var(--card-glow, var(--houdini-glow)), transparent 60%),
    linear-gradient(160deg, var(--surface-2), var(--card-bg, var(--surface)));
}
.tool-card:hover{ border-color:var(--card-accent, var(--unreal)); }
.asset-card:hover, .tut-card:hover, .split-card:hover, .channel-card:hover,
.l1-card:hover, .l1-feature:hover, .l3-card:hover, .l5-card:hover, .stat-card:hover{
  border-color:var(--card-accent, var(--houdini));
}
/* Card titles inherit their colour today, so "inherit" is the no-change
   fallback; the CTA card's label is the one that names a colour of its own. */
.fx-card h2, .fx-card h3, .fx-card h4{ color:var(--card-title, inherit); }
.stat-card .stat-label{ color:var(--card-title, var(--text)); }
/* Card body text. Each fallback is what that card's text already was. */
.fx-card p{ color:var(--card-text, var(--text-muted)); }
.tut-card .tut-meta{ color:var(--card-text, var(--text-dim)); }
.stat-card .stat-unit{ color:var(--card-text, var(--text-dim)); }
.blog-row p{ color:var(--card-text, var(--text-muted)); }
.card-reveal{ color:var(--card-reveal-ink, var(--on-fx,#0A0C10)); }
/* The label a card carries at its foot — "View on Gumroad" — is its button. */
.tool-card .buy-links span{ color:var(--btn-ink, inherit); }
.asset-cta{ color:var(--btn-ink, var(--text-muted)); border-color:var(--btn-border, var(--line)); background:var(--btn-bg, transparent); }
.asset-card:hover .asset-cta{ color:var(--btn-hover, var(--houdini)); border-color:var(--btn-hover, var(--houdini)); }
/* The homepage blog strip is rows rather than cards. */
.blog-row{ border-bottom-color:var(--card-border, var(--line-soft)); }
.blog-row h3{ color:var(--card-title, inherit); }
.blog-row:hover h3{ color:var(--card-accent, var(--houdini)); }

.stat-btn{ background:var(--btn-bg, var(--houdini)); color:var(--btn-ink, var(--on-accent,#0A0C10)); }
.stat-btn:hover, a.stat-card:hover .stat-btn{ background:var(--btn-hover, #FF9A5C); }
.sf-patreon-btn{ background:var(--btn-bg, var(--houdini)); color:var(--btn-ink, var(--on-accent,#0A0C10)); }
.sf-patreon-btn:hover{ background:var(--btn-hover, #FF9A5C); }
.section-head .view-all, .tut-channel-head .view-all{
  color:var(--btn-ink, var(--text-muted));
  border-bottom-color:var(--btn-border, transparent);
}
.section-head .view-all:hover, .tut-channel-head .view-all:hover{
  color:var(--btn-hover, var(--houdini)); border-color:var(--btn-hover, var(--houdini));
}
.filter-pill{
  background:var(--btn-bg, transparent); color:var(--btn-ink, var(--text-muted));
  border-color:var(--btn-border, var(--line));
}
.filter-pill:hover{ color:var(--btn-hover, var(--text)); border-color:var(--btn-hover, var(--houdini)); }
.filter-pill.active{
  background:var(--btn-hover, var(--houdini)); border-color:var(--btn-hover, var(--houdini));
  color:var(--btn-ink, var(--on-accent,#0A0C10));
}
.nav-cta{
  background:var(--btn-bg, transparent); color:var(--btn-ink, var(--text));
  border-color:var(--btn-border, var(--line));
}
.g-info-link{ background:var(--btn-bg, var(--houdini)); color:var(--btn-ink, #0B0C0E); }
.g-info-link:hover{ background:var(--btn-hover, var(--unreal)); }


/* ===========================================================================
   Capsule and background panel — text formatting from Edit pages
   ---------------------------------------------------------------------------
   The two looks the Free Assets note already had, made available to any text:
   a word set in a capsule (the password "hojdee"), and a passage set on a
   panel (the note itself). Classes rather than inline styles, so the look is
   decided here once and follows the theme.
   =========================================================================== */
/* --pill is the capsule's own colour, set from the panel: what is behind the
   words and the line around them, see-through as it was chosen. The words
   keep their own colour (the design's orange, or the one given them), so a
   faint capsule never fades its words. Unset, the capsule is the design's:
   the page's darker ground in a line tinted orange. */
/* Its room round the words is in em, so it grows with them (2px 7px at the
   size it was drawn at), plus the offset set from the panel (--pill-pad, in
   px, less than 0 to pull it in). Its line height is the capsule's whole
   height (its words' 1.32em, its room, its border), so the line it is on
   makes room for all of it, centred: a capsule bigger than the text round it
   stuck up into what was above it (on a first line, the top of its panel),
   and two capsules on two lines overlapped. */
.hd-pill{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:.92em;
  background:var(--pill, var(--bg-2)); border:1px solid var(--line);
  border-color:var(--pill, color-mix(in srgb, var(--houdini) 35%, var(--line)));
  padding:calc(.15em + var(--pill-pad, 0px)) calc(.55em + var(--pill-pad, 0px));
  border-radius:var(--pill-r, 4px); color:var(--houdini);
  line-height:calc(1.62em + 2 * var(--pill-pad, 0px) + 2px);
  -webkit-box-decoration-break:clone; box-decoration-break:clone;
}
/* A capsule with its own offsets set from the panel, each in px and 0 at the
   words' own bounds (--pill-x sideways, --pill-y up and down; one unset is the
   design's reach). A box in the line, so the line makes room for all of it;
   held to the capitals and the baseline, and outlined inside itself, the way a
   supporter's capsule is. */
.hd-pill[style*="--pill-x"], .hd-pill[style*="--pill-y"],
.pw-note code[style*="--pill-x"], .pw-note code[style*="--pill-y"]{
  display:inline-block; line-height:1.3; border:0;
  padding:var(--pill-y, calc(max(0px, .15em + var(--pill-pad, 0px)) + .29em + 1px)) var(--pill-x, calc(max(0px, .55em + var(--pill-pad, 0px)) + 1px));
  box-shadow:inset 0 0 0 1px var(--pill, color-mix(in srgb, var(--houdini) 35%, var(--line)));
}
/* Words made bigger inside a capsule (the size on them rather than on it, as
   the editor once did): the capsule wraps them as a box, so it is round all
   of them. The editor now sizes a capsule itself when all its words are
   sized. */
.hd-pill:has(> [style*="font-size"]){ display:inline-block; line-height:1.2; }
.hd-pill [style*="font-size"]{ line-height:1.2; }
/* Words given a colour or a font around their capsule (the editor can wrap
   the capsule rather than the words in it) are shown in it: the capsule's
   orange and its mono are only what it has when nothing is given. */
:is([style^="color"], [style*=" color:"], [style*=";color:"]) > .hd-pill{ color:inherit; }
:is([style^="font-family"], [style*=" font-family:"], [style*=";font-family:"]) > .hd-pill{ font-family:inherit; }
/* Words' own colour in the light theme (Edit pages' second text colour). On
   the element that has it, over the colour of both themes. */
:root[data-theme="light"] [style*="--hd-light"]{ color:var(--hd-light) !important; }
.hd-panel{
  display:block; margin:14px 0; padding:14px 18px;
  background:var(--surface); border:1px solid var(--line); border-radius:8px;
}
.hd-panel:first-child{ margin-top:0; } .hd-panel:last-child{ margin-bottom:0; }
/* A panel can either run the width of the text (the default) or shrink to fit
   the words it holds. */
.hd-panel.hd-panel-fit{ display:inline-block; width:auto; max-width:100%; }
/* A border around the words, from the text toolbar. */
.hd-bordered{ display:block; border:1px solid var(--line); border-radius:6px; padding:10px 14px; }
span.hd-bordered{ display:inline-block; padding:1px 8px; }


/* ===========================================================================
   The site assistant (assistant.js)
   ---------------------------------------------------------------------------
   Below the gallery lightbox (z-index 200) so a picture opened full screen is
   never covered. Colours are overridable from Edit pages like everything else.
   =========================================================================== */
/* --hd-kb: how much of the window a phone's on-screen keyboard covers, while
   the chat is open (assistant.js fitToKeyboard); --hd-vh: what is left. */
.hd-bot{ position:fixed; right:22px; bottom:calc(22px + var(--hd-kb, 0px)); z-index:150; font-family:var(--font-body,'Inter',sans-serif); }
.hd-bot-toggle{
  width:54px; height:54px; border-radius:50%; border:0; cursor:pointer;
  display:grid; place-items:center; margin-left:auto;
  background:var(--bot-accent, var(--houdini)); color:var(--bot-ink, var(--on-accent,#0A0C10));
  box-shadow:0 10px 28px rgba(0,0,0,.35); transition:transform .2s, box-shadow .2s;
}
.hd-bot-toggle:hover{ transform:translateY(-2px); box-shadow:0 14px 32px rgba(0,0,0,.42); }
.hd-bot-toggle svg{ width:24px; height:24px; }
.hd-bot.is-open .hd-bot-toggle{ transform:scale(.92); }
.hd-bot-panel{
  position:absolute; right:0; bottom:68px; width:min(360px, calc(100vw - 32px));
  max-height:min(520px, calc(var(--hd-vh, 100vh) - 120px)); display:flex; flex-direction:column;
  background:var(--bot-panel, var(--surface)); color:var(--bot-text, var(--text));
  border:1px solid var(--line); border-radius:14px; overflow:hidden;
  box-shadow:0 22px 60px rgba(0,0,0,.45);
}
.hd-bot-panel[hidden]{ display:none; }
.hd-bot-head{ display:flex; align-items:center; justify-content:space-between; padding:12px 14px;
  border-bottom:1px solid var(--line-soft); font-weight:600; font-size:14px; }
.hd-bot-close{ background:none; border:0; color:var(--text-dim); font-size:22px; line-height:1; cursor:pointer; }
.hd-bot-close:hover{ color:var(--text); }
.hd-bot-log{ flex:1; overflow-y:auto; padding:14px; display:flex; flex-direction:column; gap:10px; }
.hd-bot-msg{ font-size:13.5px; line-height:1.5; padding:9px 12px; border-radius:10px; max-width:92%; }
.hd-bot-msg p{ margin:0 0 8px; }
/* A link arrives as its own message, so the bubble is only as big as the card. */
.hd-bot-msg.is-link{ padding:6px; max-width:96%; }
/* Every message arrives rather than appears, and a link card arrives with a
   little more travel, because it lands after the answer has been read. */
.hd-bot-msg{ animation:hd-bot-arrive .22s cubic-bezier(.2,.7,.3,1) both; }
/* A link grows in from the left as its space opens: it comes in narrow and
   sideways, settles into place, and the messages above it drift up while the
   height underneath them opens (makeRoom in assistant.js). */
.hd-bot-msg.is-link{
  animation:hd-bot-arrive-link .42s cubic-bezier(.16,.84,.28,1) both;
  transform-origin:left center;
}
@keyframes hd-bot-arrive{ from{ opacity:0; transform:translateY(6px); } to{ opacity:1; transform:none; } }
@keyframes hd-bot-arrive-link{
  from{ opacity:0; transform:translateX(-26px) scaleX(.55) scaleY(.82); }
  55%{ opacity:1; transform:translateX(2px) scaleX(1.02) scaleY(1); }
  to{ opacity:1; transform:none; }
}
/* The panel itself, opening and closing. */
.hd-bot-panel.is-in{ animation:hd-bot-open .24s cubic-bezier(.2,.8,.3,1) both; }
.hd-bot-panel.is-out{ animation:hd-bot-shut .2s cubic-bezier(.4,0,.7,.2) both; }
@keyframes hd-bot-open{
  from{ opacity:0; transform:translateY(18px) scale(.94); }
  to{ opacity:1; transform:none; }
}
@keyframes hd-bot-shut{
  from{ opacity:1; transform:none; }
  to{ opacity:0; transform:translateY(12px) scale(.96); }
}
/* The button turns a little as it becomes the way out. */
.hd-bot-toggle{ transition:transform .24s cubic-bezier(.2,.8,.3,1), background .2s, color .2s; }
.hd-bot.is-open .hd-bot-toggle{ transform:rotate(-8deg) scale(.92); }
.hd-bot-toggle:active{ transform:scale(.9); }
@media (prefers-reduced-motion: reduce){
  .hd-bot-msg, .hd-bot-msg.is-link, .hd-bot-panel.is-in, .hd-bot-panel.is-out{ animation:none; }
  .hd-bot-toggle{ transition:none; }
}
.hd-bot-msg.is-link .hd-bot-results{ margin:0; }
/* The caret that follows the words as the answer is written out. */
.hd-bot-typing{ white-space:pre-wrap; }
.hd-bot-typing::after{
  content:"▍"; margin-left:1px; color:var(--bot-accent, var(--houdini));
  animation:hd-bot-caret 1s steps(1) infinite;
}
@keyframes hd-bot-caret{ 50%{ opacity:0; } }
.hd-bot-msg p:last-child{ margin-bottom:0; }
.hd-bot-msg.is-bot{ background:var(--surface-2); align-self:flex-start; }
.hd-bot-msg.is-me{ background:var(--bot-accent, var(--houdini)); color:var(--bot-ink, var(--on-accent,#0A0C10)); align-self:flex-end; }
.hd-bot-results{ list-style:none; margin:8px 0 0; padding:0; display:flex; flex-direction:column; gap:6px; }
.hd-bot-results a{ display:flex; flex-direction:column; gap:2px; padding:8px 10px; border-radius:8px;
  background:var(--bot-panel, var(--surface)); border:1px solid var(--line); text-decoration:none; color:inherit; }
.hd-bot-results a:hover{ border-color:var(--bot-accent, var(--houdini)); }
.hd-bot-kind{ font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:10px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--bot-accent, var(--houdini)); }
/* Small text on a light panel: the orange alone is too faint to read (under
   4.5:1), so it is taken a quarter of the way toward the text colour. */
:root[data-theme="light"] .hd-bot-kind{ color:color-mix(in srgb, var(--bot-accent, var(--houdini)) 75%, var(--text)); }
.hd-bot-title{ font-weight:600; font-size:13.5px; }
.hd-bot-sum{ font-size:12px; color:var(--text-muted); }
.hd-bot-chips{ display:flex; flex-wrap:wrap; gap:6px; padding:0 14px 10px; }
.hd-bot-chip{ font-size:12px; padding:5px 10px; border-radius:999px; cursor:pointer;
  background:transparent; border:1px solid var(--line); color:var(--text-muted); }
.hd-bot-chip:hover{ border-color:var(--bot-accent, var(--houdini)); color:var(--text); }
.hd-bot-form{ display:flex; gap:6px; padding:10px; border-top:1px solid var(--line-soft); }
.hd-bot-input{ flex:1; min-width:0; background:var(--bg-2); border:1px solid var(--line); border-radius:8px;
  color:var(--text); font-size:13.5px; padding:9px 11px; }
.hd-bot-input:focus{ outline:none; border-color:var(--bot-accent, var(--houdini)); }
.hd-bot-send{ width:40px; border:0; border-radius:8px; cursor:pointer; font-size:16px;
  background:var(--bot-accent, var(--houdini)); color:var(--bot-ink, var(--on-accent,#0A0C10)); }
.hd-bot-dots{ display:inline-flex; gap:4px; }
.hd-bot-dots i{ width:6px; height:6px; border-radius:50%; background:currentColor; opacity:.35;
  animation:hdBotDot 1s infinite ease-in-out; }
.hd-bot-dots i:nth-child(2){ animation-delay:.15s; } .hd-bot-dots i:nth-child(3){ animation-delay:.3s; }
@keyframes hdBotDot{ 0%,100%{ opacity:.25; transform:translateY(0); } 50%{ opacity:.9; transform:translateY(-3px); } }
@media (prefers-reduced-motion: reduce){ .hd-bot-dots i{ animation:none; opacity:.6; } }
@media (max-width:560px){ .hd-bot{ right:14px; bottom:calc(14px + var(--hd-kb, 0px)); } .hd-bot-toggle{ width:48px; height:48px; } }
/* Where the keyboard is, it shows: every control of the chat gets a ring when
   reached with Tab (not on a click). */
.hd-bot-toggle:focus-visible, .hd-bot-close:focus-visible, .hd-bot-chip:focus-visible,
.hd-bot-send:focus-visible, .hd-bot-results a:focus-visible{
  outline:2px solid var(--bot-accent, var(--houdini)); outline-offset:2px;
}
/* Said to a screen reader, not shown. */
.hd-bot-sr{ position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; }

/* A text whose ELEMENT draws the panel — the Free Assets note — cannot have its
   panel removed by editing the text inside it. The panel button marks the text
   instead, and the element drops its panel when it sees the mark. */
.hd-unpanel{ display:block; }
.pw-note:has(> .hd-unpanel){ background:transparent; border-color:transparent; padding-left:0; padding-right:0; }
/* The Free Assets password is the original capsule, a <code>; it takes the same
   colour a panel-made capsule does. Its own rule is in the page's <style>, which
   loads before this file. */
.pw-note code{
  background:var(--pill, var(--bg-2));
  border-color:var(--pill, color-mix(in srgb, var(--houdini) 35%, var(--line)));
  padding:calc(.15em + var(--pill-pad, 0px)) calc(.55em + var(--pill-pad, 0px));
  border-radius:var(--pill-r, 4px);
  line-height:calc(1.62em + 2 * var(--pill-pad, 0px) + 2px);
}
.pw-note code:has(> [style*="font-size"]){ display:inline-block; line-height:1.2; }


/* ===========================================================================
   Section options, from Edit pages (applySectionStyles in site.js)
   ---------------------------------------------------------------------------
   data-hd-width: "wide" spans the window; "space" spans it less --hd-side on
   each side. Wide screens only; a phone keeps the stacking it was built with.
   The margins are measured from the column the section sits in, so the same
   rule serves a section inside the homepage's <main> and one directly in body.
   data-hd-panel: "on" draws the site's panel behind the section, "off" takes
   away one the design drew. Neither attribute exists until someone sets it.
   =========================================================================== */
@media (min-width:901px){
  [data-hd-width]{
    max-width:none !important; width:auto !important; box-sizing:border-box;
    margin-left:calc((100% - var(--hd-page-w, 100vw)) / 2 + var(--hd-side, 0px)) !important;
    margin-right:calc((100% - var(--hd-page-w, 100vw)) / 2 + var(--hd-side, 0px)) !important;
  }
  /* The page containers inside a section are what held it to the column. */
  [data-hd-width] > :is(.wrap, .asset-grid, .cat-block, .split, .hd-beside-grid){ max-width:none !important; }
  /* The site's content width: the 1180px column every page's .wrap is, less
     its 32px sides, centred in the window; never closer than 32px to an edge. */
  [data-hd-width="wrap"]{ --hd-side:max(32px, (var(--hd-page-w, 100vw) - 1116px) / 2); }
}
[data-hd-panel="on"]{
  background:var(--surface) !important; border:1px solid var(--line) !important;
  border-radius:12px !important; padding:var(--hd-pad, 32px) !important; box-sizing:border-box;
}
[data-hd-panel="on"] > :first-child{ margin-top:0; }
main > section[data-hd-panel="on"]{ margin-top:28px; margin-bottom:28px; }
[data-hd-panel="off"]{
  background:transparent !important; border-color:transparent !important; box-shadow:none !important;
  -webkit-backdrop-filter:none !important; backdrop-filter:none !important;
}
@media (max-width:600px){
  [data-hd-panel="on"]{ padding:min(var(--hd-pad, 32px), 20px) !important; }
}

/* ---- Blocks placed around a section (page-content.js renderSlots) ---------- */
.hd-slot{ position:relative; z-index:1; }
.hd-slot-flow{ margin-top:32px; margin-bottom:32px; }
/* Beside a section on a page: the section and its neighbours share a row. */
.hd-beside{
  display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:32px; align-items:center;
  max-width:1180px; margin:0 auto; padding:0 32px; box-sizing:border-box; position:relative; z-index:1;
}
.hd-beside > [data-beside-anchor]{ min-width:0; max-width:none; margin-left:0; margin-right:0; }
.hd-beside > .hd-slot, .hd-beside-grid > .hd-slot{ max-width:none; margin:0; padding:0; }
/* Beside a homepage section: inside it, so <main> keeps its list of sections. */
.hd-beside-grid{ display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:32px; align-items:center; }
.hd-beside-main{ min-width:0; }
.legal-article .hd-slot, .legal-article .hd-beside{ padding-left:0; padding-right:0; max-width:none; }
@media (max-width:760px){
  .hd-beside, .hd-beside-grid{ grid-template-columns:1fr !important; }
}

/* ===========================================================================
   Section rail (section-rail.js)
   ---------------------------------------------------------------------------
   One chip per section on the right edge; the one being read is lit, nudged
   out, and its line (--p, 0..1) fills as the reader moves through it. A div
   with role="navigation" rather than a <nav>: every page styles `nav` as its
   sticky bar. Hidden where there is no room beside the text. A long list
   scrolls inside itself.
   =========================================================================== */
.hd-rail{
  position:fixed; top:50%; right:14px; z-index:60; translate:0 -50%;
  display:grid; gap:7px; max-height:calc(100vh - 160px); overflow-y:auto;
  scrollbar-width:none; padding:2px 2px 2px 8px;
}
.hd-rail::-webkit-scrollbar{ display:none; }
.hd-rail-chip{
  display:grid; grid-template-columns:auto minmax(0,1fr); column-gap:6px; align-content:center;
  width:112px; min-height:44px; padding:8px 10px;
  border:1px solid var(--line); border-radius:8px;
  background:color-mix(in srgb, var(--surface) 76%, transparent);
  backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:10.5px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--text-muted); text-decoration:none;
  transition:transform .2s, border-color .2s, background .2s, color .2s;
}
.hd-rail-chip:hover, .hd-rail-chip:focus-visible, .hd-rail-chip.is-on{
  transform:translateX(-5px); color:var(--text);
  border-color:color-mix(in srgb, var(--houdini) 60%, transparent);
  background:color-mix(in srgb, var(--houdini) 12%, var(--surface));
}
.hd-rail-n{ display:inline-block; color:var(--houdini); }
.hd-rail-name{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.hd-rail-chip i{ grid-column:1 / -1; position:relative; display:block; height:1px; margin-top:6px; overflow:hidden; background:var(--line); }
.hd-rail-chip i::after{ content:""; position:absolute; inset:0; background:var(--houdini); transform:scaleX(0); transform-origin:left center; }
.hd-rail-chip.is-on i::after{ transform:scaleX(var(--p, 0)); }
.hd-rail-chip.is-on .hd-rail-n{ animation:hd-rail-flip .4s cubic-bezier(.22,1,.36,1); }
@keyframes hd-rail-flip{ from{ transform:translateY(-70%); opacity:0; } to{ transform:none; opacity:1; } }
@media (max-width:900px){ .hd-rail{ display:none; } }
/* What the rail takes from the right edge, past the footer's own 32px side,
   with a little room: the full chips (14 + 112 + its padding + the lit
   chip's nudge), and below 1450px the numbers alone. */
@media (min-width:1450px){ .hd-has-rail{ --hd-rail-room:112px; } }
@media (min-width:901px) and (max-width:1449px){ .hd-has-rail{ --hd-rail-room:28px; } }
/* Between that and a window wide enough to leave it room beside the content
   (about 1450px: a landscape tablet, a small laptop), the full chips sat over
   the right of the content, card text and links included. There each chip
   is its number alone, and opens out to its name on hover or focus. */
@media (min-width:901px) and (max-width:1449px){
  .hd-rail{ right:4px; justify-items:end; }
  .hd-rail-chip{ width:36px; grid-template-columns:auto; justify-items:center; padding:8px 4px; }
  .hd-rail-name{ display:none; }
  .hd-rail-chip:hover, .hd-rail-chip:focus-visible{ width:112px; grid-template-columns:auto minmax(0,1fr); justify-items:stretch; padding:8px 10px; }
  .hd-rail-chip:hover .hd-rail-name, .hd-rail-chip:focus-visible .hd-rail-name{ display:block; }
}
@media (prefers-reduced-motion: reduce){
  .hd-rail-chip{ transition:none; }
  .hd-rail-chip.is-on .hd-rail-n{ animation:none; }
}

/* ===========================================================================
   Interactive blocks (panel-fx.js)
   ---------------------------------------------------------------------------
   Options of the page builder's own blocks:
   - a text drawn as a small label (.is-eyebrow);
   - a heading at the very large size (.level-display), and one that glitches
     while the mouse moves over it ([data-glitch]: panel-fx.js splits it into
     letters only while it is hot, and puts it back after);
   - a picture pixelated except in a circle round the mouse ([data-pixel]: a
     canvas over the <img>, which stays for screen readers), and one whose left
     edge fades away (.soft-left);
   - a Section block drawn as an interactive panel ([data-ifx]): a ring round
     the mouse (--ifx-r, --ifx-ra), a colour at rest (--ifx-a) that changes to
     another while the mouse is in it (--ifx-b), a glow that follows the mouse
     (--ifx-x/-y), and faint grid lines. The layers sit behind the content.
   =========================================================================== */
.page-blocks .block-paragraph.is-eyebrow{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:12px; letter-spacing:.12em; text-transform:uppercase;
  color:var(--text-dim); line-height:1.5;
}
.page-blocks .block-heading.level-display{
  font-size:clamp(44px, 7.5vw, 112px); line-height:.95; letter-spacing:-.02em; font-weight:700; margin-top:6px;
}
[data-glitch] .gl-w{ white-space:nowrap; }
[data-glitch] .gl-ch{ position:relative; --h:0; }
[data-glitch] .gl-ch::after{
  content:attr(data-g); position:absolute; left:0; top:0; pointer-events:none;
  color:var(--houdini); opacity:0;
}
[data-glitch] .gl-ch.is-hot{
  text-shadow:calc(var(--h) * .06em) 0 var(--houdini-glow, rgba(255,122,51,.35)),
              calc(var(--h) * -.05em) 0 var(--unreal-glow, rgba(78,143,255,.3));
}
[data-glitch] .gl-ch.is-hot::after{ opacity:calc(var(--h) * .85); }

.page-blocks .block-image[data-pixel]{ position:relative; }
.page-blocks .block-image .ifx-pixel{ position:absolute; pointer-events:none; border-radius:8px; }
.page-blocks .block-image.soft-left img{ border-color:transparent; }
.page-blocks .block-image.soft-left img, .page-blocks .block-image.soft-left .ifx-pixel{
  -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 22%);
          mask-image:linear-gradient(90deg, transparent 0, #000 22%);
}

/* Not clipped: what is in it (a button pulled toward the pointer, a card
   tilting) may pass its edge and is seen whole. Its decoration clips itself:
   the glow and the grid take its rounded corners, and the ring sits in its own
   clipped layer (.ifx-layer, panel-fx.js). */
.block-section[data-ifx]{ position:relative; isolation:isolate; border-radius:12px; background-color:var(--ifx-a, transparent); }
[data-hd-panel="on"][data-ifx]{ background:var(--ifx-a, var(--surface)) !important; }
.block-section[data-ifx]::before,
.block-section[data-ifx]::after,
.ifx-layer{ border-radius:inherit; }
.ifx-layer{ position:absolute; inset:0; z-index:3; overflow:hidden; pointer-events:none; }
.block-section[data-ifx]::before{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none;
  opacity:0; transition:opacity .6s ease;
  background:
    radial-gradient(520px circle at var(--ifx-x, 50%) var(--ifx-y, 50%), color-mix(in srgb, var(--houdini) 16%, transparent), transparent 62%),
    var(--ifx-b, color-mix(in srgb, var(--houdini) 7%, var(--surface)));
}
.block-section[data-ifx][data-ifx-glow="off"]::before{ background:var(--ifx-b, color-mix(in srgb, var(--houdini) 7%, var(--surface))); }
.block-section[data-ifx].is-in::before{ opacity:1; }
.block-section[data-ifx]::after{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none; opacity:.55;
  background-image:linear-gradient(var(--line-soft) 1px, transparent 1px),
                   linear-gradient(90deg, var(--line-soft) 1px, transparent 1px);
  background-size:48px 48px;
}
.block-section[data-ifx][data-ifx-grid="off"]::after{ display:none; }
.ifx-ring{
  position:absolute; left:0; top:0; z-index:3; pointer-events:none;
  width:calc(var(--ifx-r, 90px) * 2); height:calc(var(--ifx-r, 90px) * 2);
  margin:calc(var(--ifx-r, 90px) * -1) 0 0 calc(var(--ifx-r, 90px) * -1);
  border:1px solid var(--houdini); border-radius:50%;
  opacity:0; transition:opacity .3s;
}
.ifx-ring::after{ content:""; position:absolute; left:50%; top:50%; width:4px; height:4px; margin:-2px; border-radius:50%; background:var(--houdini); }
.block-section[data-ifx].is-in .ifx-ring{ opacity:var(--ifx-ra, .7); }
.block-section[data-ifx-ring="off"] .ifx-ring{ display:none; }

/* ---- How an interactive panel arrives (panel-fx.js) -------------------------
   Waiting, it is faint, blurred and clipped, its blocks held to the left;
   scrolled to (.ifx-shown) it sharpens and opens, and its blocks slide in one
   after another (--ifx-d). panel-fx.js marks it .ifx-ready as soon as it has
   it; a panel the script never reaches shows anyway after three seconds. */
@media (prefers-reduced-motion: no-preference){
  .block-section[data-ifx]{
    clip-path:inset(0 round 12px);
    transition:opacity .9s ease, filter .9s ease, clip-path 1s cubic-bezier(.22,1,.36,1);
  }
  .block-section[data-ifx]:not(.ifx-shown){ opacity:.35; filter:blur(8px); clip-path:inset(10% 0 round 12px); }
  /* Open (panel-fx.js, once the opening has played): nothing clips it. */
  .block-section[data-ifx].ifx-open{ clip-path:none; }
  .block-section[data-ifx] .bs-col > .block{
    transition:opacity .6s ease var(--ifx-d, 0s), transform .8s cubic-bezier(.22,1,.36,1) var(--ifx-d, 0s);
  }
  .block-section[data-ifx]:not(.ifx-shown) .bs-col > .block{ opacity:0; transform:translateX(-28px); }
  .block-section[data-ifx]:not(.ifx-ready),
  .block-section[data-ifx]:not(.ifx-ready) .bs-col > .block{ animation:ifx-failsafe .01s 3s forwards; }

  /* On a page whose content flies in (panel-fx.js .ifx-late): its blocks fly
     with the rest, unclipped and unmoved by the panel, and the panel forms
     behind them once they have landed (.ifx-formed): its surface, border and
     grid fade in, and its edge flashes once. */
  /* From the first frame on such a page (page-entrance.js sets
     data-entrance-wait before anything is drawn), before panel-fx.js has got
     to it: not faint and blurred, simply not there yet. */
  html[data-entrance-wait] .block-section[data-ifx]:not(.ifx-late){ opacity:1; filter:none; clip-path:none; transition:none; }
  html[data-entrance-wait] .block-section[data-ifx]:not(.ifx-late),
  html[data-entrance-wait] [data-hd-panel="on"].block-section[data-ifx]:not(.ifx-late){ background-color:transparent !important; border-color:transparent !important; }
  html[data-entrance-wait] .block-section[data-ifx]:not(.ifx-late)::after{ opacity:0; }
  .block-section[data-ifx].ifx-late{ clip-path:none; transition:none; }
  .block-section[data-ifx].ifx-late.ifx-formed{ transition:background-color .7s ease, border-color .7s ease; }
  .block-section[data-ifx].ifx-late .bs-col > .block{ transition:none; }
  .block-section[data-ifx].ifx-late:not(.ifx-formed),
  [data-hd-panel="on"].block-section[data-ifx].ifx-late:not(.ifx-formed){ background-color:transparent !important; border-color:transparent !important; }
  .block-section[data-ifx].ifx-late.ifx-formed::after{ transition:opacity .9s ease; }
  .block-section[data-ifx].ifx-late:not(.ifx-formed)::after{ opacity:0; }
  .block-section[data-ifx].ifx-late.ifx-formed:not(.ifx-open){ animation:ifx-form 1.1s ease-out both; }
}
@keyframes ifx-form{
  0%{ box-shadow:0 0 0 0 color-mix(in srgb, var(--houdini) 55%, transparent); }
  35%{ box-shadow:0 0 0 3px color-mix(in srgb, var(--houdini) 45%, transparent), 0 0 28px color-mix(in srgb, var(--houdini) 22%, transparent); }
  100%{ box-shadow:0 0 0 0 transparent; }
}
@keyframes ifx-failsafe{ to{ opacity:1; filter:none; clip-path:none; transform:none; } }
/* Until its canvas is drawn, a pixelated picture is a grey blur rather than
   the sharp picture for a moment. Not on a screen with no hover, where it is
   never pixelated. */
@media (hover:hover){
  .page-blocks .block-image[data-pixel] img{ filter:grayscale(1) blur(10px); }
}
/* The rail comes in once the page has started arriving, not before it. */
@media (prefers-reduced-motion: no-preference){
  .hd-rail{ animation:hd-rail-in .5s cubic-bezier(.22,1,.36,1) .6s both; }
}
@keyframes hd-rail-in{ from{ opacity:0; transform:translateX(16px); } to{ opacity:1; transform:none; } }
/* An interactive panel's columns fill its height, and each column's last block
   sits at the bottom: the heading at the top and its line of text level with
   the button, as the panel was designed. One block alone stays at the top. */
.block-section[data-ifx] .bs-cols{ align-items:stretch; }
.block-section[data-ifx] .bs-col{ display:flex; flex-direction:column; }
.block-section[data-ifx] .bs-col > .block:last-child:not(:first-child){ margin-top:auto; }

/* ---- A button's or a card's own size and place (set in the panel) ----------
   A Button element's or an article button's own width and height (--btn-w,
   --btn-h): the box around the words changes, the words keep their size. A
   Card element's own width, height and place in its column (--card-w,
   --card-h, .align-*). Unset, each is exactly as it was. The site's own card
   groups and buttons get theirs from color-scopes.js (gridCss, boxCss). */
.btn-block{ width:var(--btn-w, auto); min-height:var(--btn-h, auto); max-width:100%; box-sizing:border-box; }
.page-blocks .block-card{ width:var(--card-w, auto); max-width:100%; min-height:var(--card-h, auto); box-sizing:border-box; }
.page-blocks .block-card.align-center{ margin-left:auto; margin-right:auto; }
.page-blocks .block-card.align-right{ margin-left:auto; }
/* An element's place up and down, where there is room for it: in a column
   beside a taller one, or among the elements placed beside a section. */
.page-blocks .bs-cols > .bs-col:has(> .block[data-valign]),
:is(.hd-beside, .hd-beside-grid) > .hd-slot:has(> .block[data-valign]){ display:flex; flex-direction:column; align-self:stretch; }
:is(.hd-beside, .hd-beside-grid) > .hd-slot:has(> .block[data-valign]) > .block:last-child:not([data-valign="middle"], [data-valign="top"]){ margin-bottom:0; }
:is(.page-blocks .bs-col, .hd-beside > .hd-slot, .hd-beside-grid > .hd-slot) > .block[data-valign="top"]{ margin-bottom:auto; }
:is(.page-blocks .bs-col, .hd-beside > .hd-slot, .hd-beside-grid > .hd-slot) > .block[data-valign="middle"]{ margin-top:auto; margin-bottom:auto; }
:is(.page-blocks .bs-col, .hd-beside > .hd-slot, .hd-beside-grid > .hd-slot) > .block[data-valign="bottom"]{ margin-top:auto; }
/* Where an element's words line up, left and right: in a page's elements and
   in the nav's and the footer's own (they are drawn outside .page-blocks, and
   the setting did nothing there). */
:is(.page-blocks, .site-footer, nav) .block[data-halign="left"]{ text-align:left; }
:is(.page-blocks, .site-footer, nav) .block[data-halign="center"]{ text-align:center; }
:is(.page-blocks, .site-footer, nav) .block[data-halign="right"]{ text-align:right; }
:is(.page-blocks, .site-footer, nav) .block[data-halign="justify"]{ text-align:justify; }

/* Justified text on a phone: a narrow column has nowhere to put the slack, so
   words sat far apart. Hyphenating long words there lets the lines fill
   (the owner keeps justified text, audit UX-01). */
@media (max-width:600px){
  :is(.page-blocks, .site-footer, nav) .block[data-halign="justify"], [style*="text-align: justify"], [style*="text-align:justify"]{
    -webkit-hyphens:auto; hyphens:auto;
  }
}

/* One long word or address (a file path, a node name, a pasted link) must not
   make an article or a page wider than a phone (audit RESP-02). Code and
   tables keep their own sideways scrolling, unbroken. */
.page-blocks, .post-body{ overflow-wrap:anywhere; }
:is(.page-blocks, .post-body) :is(pre, code, table){ overflow-wrap:normal; }

/* Words and cards fly in from beyond the screen's edge; while they travel they
   made the page wider than a phone, so it could be nudged sideways (audit
   RESP-03). clip cuts that off without making either element a scrolling box,
   so sticky parts keep working; a browser without clip keeps each page's own
   overflow-x:hidden on body. "html body" outranks those page rules. */
html, html body{ overflow-x:clip; }

/* ===========================================================================
   Phones and tablets
   ---------------------------------------------------------------------------
   Small links and buttons get a bigger place to tap on a touch screen,
   without looking any bigger or moving anything: an empty ::after, larger
   than the control, takes the tap for it (::before is the sweep animation's).
   The menu's own rows are tall already and touch each other, so only the
   nav row a tablet shows is widened.
   A section's "View all" link is an animated button, which clips to its own
   box (the ripple needs it), so it is padded instead, the padding taken back
   out of the layout by an equal negative margin, and its underline and traced
   outline kept round the words.
   =========================================================================== */
@media (pointer:coarse){
  .view-all{ padding-block:12px; margin-block:-12px; --trace-inset:11px -1px; }
  body.bf-sweep .view-all.btn-fx::before{ bottom:12px; }
  .back-link, .contact-email, .nav-links:not(.open) .nav-scramble, .theme-toggle .tt-btn, .hd-bot-close{ position:relative; }
  .back-link::after, .contact-email::after, .nav-links:not(.open) .nav-scramble::after,
  .theme-toggle .tt-btn::after, .hd-bot-close::after{ content:""; position:absolute; }
  .back-link::after, .contact-email::after{ inset:-12px -6px; }
  .nav-links:not(.open) .nav-scramble::after{ inset:-14px -10px; }
  /* The two theme buttons sit 2px apart: taller, not much wider. */
  .theme-toggle .tt-btn::after{ inset:-9px -1px; }
  .hd-bot-close::after{ inset:-11px -15px; }
}
/* A wide table in a post, or a piece of custom HTML on a page, scrolls inside
   its own box on a narrow screen rather than being cut off at the edge. */
.post-body table{ display:block; max-width:100%; overflow-x:auto; }
.page-blocks .block-html{ overflow-x:auto; }

/* ===========================================================================
   Depth in the light theme
   ---------------------------------------------------------------------------
   White cards on an off-white page, edged with the page's own pale line,
   read as flat, and the footer's round buttons (surface-2 on a page nearly
   that colour) all but disappear. In the light theme only, on every light
   theme (they all set data-theme="light"), cards and buttons get a soft
   shadow tinted with the theme's own ink and an edge a step firmer than the
   page's lines; the outlined buttons sit on the white surface. The dark
   theme has none of it.
   Written as fallbacks and at low weight (:where), so a colour set in the
   panel (--card-border, --btn-bg, --btn-border) still wins, and on hover
   every card and button effect keeps its own border and shadow.
   =========================================================================== */
:root[data-theme="light"]{
  --depth-edge:color-mix(in srgb, var(--text) 15%, var(--surface));
  --depth-card:0 1px 2px color-mix(in srgb, var(--text) 6%, transparent),
               0 8px 22px -12px color-mix(in srgb, var(--text) 22%, transparent);
  --depth-btn:0 1px 2px color-mix(in srgb, var(--text) 10%, transparent),
              0 3px 8px -4px color-mix(in srgb, var(--text) 16%, transparent);
}
/* No weight at all: any shadow a card or a hover effect declares wins. */
:where(:root[data-theme="light"]) :where(.fx-card, .tool-card, .asset-card, .tut-card, .split-card, .channel-card, .stat-card,
  .l1-card, .l1-feature, .l3-card, .l5-card, .rel-card, .post-card, .page-blocks .block-card){
  box-shadow:var(--depth-card);
}
:root[data-theme="light"] :where(.fx-card, .tool-card, .asset-card, .tut-card, .split-card, .channel-card, .stat-card,
  .l1-card, .l1-feature, .l3-card, .l5-card, .rel-card, .post-card, .page-blocks .block-card):not(:hover){
  border-color:var(--card-border, var(--depth-edge));
}
/* The filled buttons: lifted a little off the page. */
:where(:root[data-theme="light"]) :where(.btn-block, .stat-btn, .sf-patreon-btn, .post-button){ box-shadow:var(--depth-btn); }
/* The outlined ones: on the white surface, with a firmer edge, until they
   are hovered or chosen. */
:root[data-theme="light"] :where(.filter-pill, .social-pill, .nav-cta):not(:hover):not(.active){
  background:var(--btn-bg, var(--surface));
  border-color:var(--btn-border, var(--depth-edge));
  box-shadow:var(--depth-btn);
}
:root[data-theme="light"] :where(.btn-block, .stat-btn, .sf-patreon-btn, .post-button, .filter-pill, .social-pill, .nav-cta):is(:disabled, [aria-disabled="true"]){
  box-shadow:none;
}

/* An element made wider than designed (space taken away on its left or
   right, in Edit pages; site.js marks it data-hd-wider): its words may use
   the room. Text set to a reading width (the About copy's 64ch) otherwise
   stopped short of the box, so the box grew and nothing seemed to change. */
[data-hd-wider] :is(p, h1, h2, h3, h4, h5, h6, li, blockquote){ max-width:none; }

/* ---- Premium suggestions (public/suggest.js) ----
   A row of premium assets as the site's asset cards: a Suggestions element
   on any page, and the row under an article's related posts. The cards' own
   look is written here in full, scoped to the row, because a page that draws
   asset cards of its own keeps their look in its own stylesheet and an
   article has none. Held unseen until it is filled, so an empty row never
   shows. */
:is(.hd-suggest, .post-suggest)[hidden]{ display:none !important; }
:is(.hd-suggest-grid, .post-suggest-grid){
  display:grid; grid-template-columns:repeat(var(--suggest-n, 3), minmax(0, 1fr)); gap:20px;
}
:is(.hd-suggest-grid, .post-suggest-grid):not([data-filled]){ min-height:1px; }
@media (max-width:900px){ :is(.hd-suggest-grid, .post-suggest-grid){ grid-template-columns:repeat(min(var(--suggest-n, 3), 3), minmax(0, 1fr)); } }
@media (max-width:640px){ :is(.hd-suggest-grid, .post-suggest-grid){ grid-template-columns:repeat(2, minmax(0, 1fr)); } }
:is(.hd-suggest-grid, .post-suggest-grid) .asset-card{
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius, 10px);
  overflow:hidden; display:flex; flex-direction:column; transition:.2s; color:inherit; text-decoration:none;
}
:where(.hd-suggest-grid, .post-suggest-grid) .asset-card:hover{ border-color:var(--houdini); transform:translateY(-3px); }
:is(.hd-suggest-grid, .post-suggest-grid) .asset-thumb{
  aspect-ratio:1/1; background:
    linear-gradient(135deg, var(--surface-2), var(--bg-2)),
    radial-gradient(circle at 30% 30%, var(--houdini-glow), transparent 60%);
  background-size:cover; background-position:center;
}
:is(.hd-suggest-grid, .post-suggest-grid) .asset-body{ padding:14px 14px 16px; display:flex; flex-direction:column; gap:10px; flex:1; }
:is(.hd-suggest-grid, .post-suggest-grid) .asset-body h4{ font-size:14px; font-weight:500; line-height:1.35; flex:1; margin:0; }
:is(.hd-suggest-grid, .post-suggest-grid) .asset-cta{
  font-family:var(--font-mono,'JetBrains Mono',monospace); font-size:11.5px; text-align:center;
  padding:8px 0; border:1px solid var(--line); border-radius:6px; color:var(--text-muted); transition:.2s;
}
:is(.hd-suggest-grid, .post-suggest-grid) .asset-card:hover .asset-cta{ border-color:var(--houdini); color:var(--houdini); }
.block-suggest .hd-suggest-title{ margin:0 0 20px; }
.block-suggest .hd-suggest-title:empty{ display:none; }
.post-suggest{ margin:40px 0; }
.post-suggest-title{ margin:0 0 18px; }

/* ==== Elements an article and a page share (public/post-elements.js) ==== */
/* Each sits in an article's .post-el wrapper or a page's .block-widget. */
.block-widget{ min-width:0; }

/* ---- Before and after: the before picture over the after, cut at --ba ---- */
.post-ba{
  position:relative; overflow:hidden; border-radius:10px; border:1px solid var(--line); background:var(--surface);
  touch-action:pan-y; cursor:ew-resize; user-select:none; -webkit-user-select:none; line-height:0; isolation:isolate;
}
.post-ba:focus-visible{ outline:2px solid var(--houdini); outline-offset:3px; }
.post-ba .post-ba-img{
  display:block; width:100%; height:auto; margin:0; border:0; border-radius:0;
  pointer-events:none; -webkit-user-drag:none; user-select:none;
}
.post-ba .post-ba-before{
  position:absolute; inset:0; height:100%; object-fit:cover;
  clip-path:inset(0 calc(100% - var(--ba, 50%)) 0 0);
}
.post-ba .post-ba-empty{
  aspect-ratio:16 / 9; height:auto; display:flex; align-items:center; justify-content:center; padding:0 10%;
  font:500 13px/1.4 var(--font-mono, 'JetBrains Mono', monospace); letter-spacing:.04em; color:var(--text-dim);
  background:var(--surface-2, var(--surface)); box-sizing:border-box;
}
/* The before box says so on its own half, the after box on the other. */
.post-ba .post-ba-after.post-ba-empty{ justify-content:flex-end; }
.post-ba .post-ba-before.post-ba-empty{ justify-content:flex-start; background:var(--surface); }
.post-ba-handle{
  position:absolute; top:0; bottom:0; left:var(--ba, 50%); width:2px; margin-left:-1px; z-index:2;
  background:var(--text); box-shadow:0 0 0 1px rgba(0,0,0,.25); pointer-events:none;
}
.post-ba-handle::after{
  content:"\2194"; position:absolute; top:50%; left:50%; width:38px; height:38px; margin:-19px 0 0 -19px;
  display:flex; align-items:center; justify-content:center; border-radius:50%;
  background:var(--text); color:var(--bg); font:600 18px/1 system-ui, sans-serif;
  box-shadow:0 2px 10px rgba(0,0,0,.35); transition:transform .15s;
}
.post-ba:hover .post-ba-handle::after, .post-ba.is-dragging .post-ba-handle::after{ transform:scale(1.08); }
.post-ba-label{
  position:absolute; top:12px; z-index:1; padding:5px 10px; border-radius:999px; line-height:1.2;
  font:500 11px/1.2 var(--font-mono, 'JetBrains Mono', monospace); letter-spacing:.12em; text-transform:uppercase;
  background:rgba(10,12,16,.62); color:#fff; pointer-events:none; transition:opacity .2s;
}
.post-ba-label-before{ left:12px; }
.post-ba-label-after{ right:12px; }

/* Words only a screen reader hears. */
.post-sr{ position:absolute !important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }

/* ---- Testimonials: a card each, in --cols columns ---- */
.post-testimonials{ display:grid; grid-template-columns:repeat(var(--cols, 2), minmax(0, 1fr)); gap:18px; align-items:stretch; }
@media (max-width:860px){ .post-testimonials{ grid-template-columns:repeat(min(var(--cols, 2), 2), minmax(0, 1fr)); } }
@media (max-width:620px){ .post-testimonials{ grid-template-columns:minmax(0, 1fr); } }
.post-testimonials .post-quote{
  margin:0; padding:22px 22px 20px; display:flex; flex-direction:column; gap:14px; min-width:0;
  background:var(--surface); border:1px solid var(--line); border-radius:12px;
}
.post-quote-stars{ color:var(--houdini); font-size:15px; letter-spacing:2px; line-height:1; }
.post-quote-unstar{ color:var(--line); }
.post-testimonials .post-quote-text{
  margin:0; padding:0; border:0; background:none; font-style:normal; flex:1;
  font-size:15.5px; line-height:1.65; color:var(--text);
}
.post-testimonials .post-quote-text::before{ content:"\201C"; color:var(--houdini); font-weight:700; margin-right:2px; }
.post-testimonials .post-quote-text::after{ content:"\201D"; color:var(--houdini); font-weight:700; margin-left:1px; }
.post-testimonials .post-quote-by{
  display:flex; align-items:center; gap:12px; margin:0; padding:0; text-align:left;
  font-family:var(--font-body, "Inter", system-ui, sans-serif); font-size:14px; color:var(--text);
}
.post-testimonials .post-quote-avatar{
  flex:none; width:40px; height:40px; border-radius:50%; object-fit:cover; margin:0; border:1px solid var(--line);
}
.post-quote-initial{
  display:flex; align-items:center; justify-content:center; background:var(--surface-2, var(--bg)); color:var(--text-muted);
  font:600 15px/1 var(--font-display, 'Space Grotesk', system-ui, sans-serif);
}
.post-quote-who{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.post-quote-name{ font-weight:600; font-size:14px; color:var(--text); line-height:1.3; }
.post-quote-role{ font-size:12.5px; color:var(--text-muted); line-height:1.35; }

/* ---- FAQ: a question that opens to its answer ---- */
.post-faq{ display:flex; flex-direction:column; border-top:1px solid var(--line); }
.post-faq-item{ border-bottom:1px solid var(--line); }
.post-faq-q{
  list-style:none; cursor:pointer; position:relative; padding:18px 44px 18px 2px;
  font-weight:600; font-size:16px; line-height:1.45; color:var(--text); transition:color .2s;
}
.post-faq-q::-webkit-details-marker{ display:none; }
.post-faq-q::marker{ content:""; }
.post-faq-q:hover{ color:var(--houdini); }
.post-faq-q:focus-visible{ outline:2px solid var(--houdini); outline-offset:2px; border-radius:4px; }
/* A plus that turns into a minus as it opens. */
.post-faq-q::before, .post-faq-q::after{
  content:""; position:absolute; right:10px; top:50%; width:14px; height:2px; margin-top:-1px;
  background:currentColor; border-radius:2px; transition:transform .25s ease, opacity .25s;
}
.post-faq-q::after{ transform:rotate(90deg); }
.post-faq-item[open] > .post-faq-q::after{ transform:rotate(90deg) scaleX(0); opacity:0; }
.post-faq-a{ padding:0 44px 20px 2px; color:var(--text-muted); font-size:15px; line-height:1.7; }
.post-faq-item[open] > .post-faq-a{ animation:post-faq-open .25s ease; }
@keyframes post-faq-open{ from{ opacity:0; transform:translateY(-4px); } to{ opacity:1; transform:none; } }
@media (prefers-reduced-motion: reduce){ .post-faq-item[open] > .post-faq-a{ animation:none; } }

/* ---- Code: what it is, a Copy button, and the code as typed ---- */
.post-code{ border:1px solid var(--line); border-radius:10px; overflow:hidden; background:var(--surface); min-width:0; }
.post-code-head{
  display:flex; align-items:center; gap:10px; min-height:40px; padding:6px 8px 6px 14px; box-sizing:border-box;
  border-bottom:1px solid var(--line); background:var(--surface-2, var(--bg));
  font:500 11.5px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--text-muted);
}
.post-code-title{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text); }
.post-code-lang{ letter-spacing:.1em; text-transform:uppercase; color:var(--text-dim); }
.post-code-copy{
  flex:none; padding:5px 11px; border:1px solid var(--line); border-radius:6px; background:transparent; cursor:pointer;
  font:inherit; color:var(--text-muted); transition:color .2s, border-color .2s;
}
.post-code-copy:hover, .post-code-copy:focus-visible{ color:var(--houdini); border-color:var(--houdini); }
.post-code-copy:focus-visible{ outline:2px solid var(--houdini); outline-offset:2px; }
.post-code-copy.is-done{ color:var(--houdini); border-color:var(--houdini); }
.post-code .post-code-pre{
  margin:0; padding:16px 18px; border:0; border-radius:0; background:transparent; overflow:auto; max-height:560px;
  font:13px/1.65 var(--font-mono, 'JetBrains Mono', monospace); color:var(--text); tab-size:4; white-space:pre;
  scrollbar-width:thin; scrollbar-color:var(--line) transparent;
}
.post-code .post-code-pre code{ font:inherit; background:none; padding:0; border:0; color:inherit; white-space:inherit; }

/* ---- Latest videos: filled by elements-live.js ---- */
.post-videos{ display:grid; grid-template-columns:repeat(var(--cols, 3), minmax(0, 1fr)); gap:18px; }
.post-videos:not([data-filled]){ min-height:1px; }
@media (max-width:860px){ .post-videos{ grid-template-columns:repeat(min(var(--cols, 3), 2), minmax(0, 1fr)); } }
@media (max-width:560px){ .post-videos{ grid-template-columns:minmax(0, 1fr); } }
/* While an article is written it stays empty: it says what it will be. */
[contenteditable="true"] .post-videos:empty::before{
  content:"Your latest videos show here, on the site."; grid-column:1 / -1; padding:28px 16px; text-align:center;
  border:1px dashed var(--line); border-radius:10px; color:var(--text-dim);
  font:500 12px/1.4 var(--font-mono, 'JetBrains Mono', monospace);
}
.post-videos-none{ grid-column:1 / -1; margin:0; color:var(--text-dim); font-size:14px; }
.post-video-card{
  display:flex; flex-direction:column; gap:8px; min-width:0; text-decoration:none; color:inherit;
  padding:10px 10px 14px; border:1px solid var(--line); border-radius:12px; background:var(--surface);
  transition:border-color .2s, transform .2s;
}
.post-video-card:hover{ border-color:var(--houdini); transform:translateY(-3px); }
.post-video-card:focus-visible{ outline:2px solid var(--houdini); outline-offset:3px; }
.post-video-thumb{ position:relative; display:block; aspect-ratio:16 / 9; border-radius:8px; overflow:hidden; background:var(--surface-2, var(--bg)); }
.post-video-thumb img{ display:block; width:100%; height:100%; object-fit:cover; margin:0; border:0; border-radius:0; }
.post-video-play{
  position:absolute; left:50%; top:50%; width:44px; height:44px; margin:-22px 0 0 -22px; border-radius:50%;
  background:rgba(10,12,16,.62); transition:background .2s, transform .2s;
}
.post-video-play::after{
  content:""; position:absolute; left:17px; top:13px; border-style:solid; border-width:9px 0 9px 14px;
  border-color:transparent transparent transparent #fff;
}
.post-video-card:hover .post-video-play{ background:var(--houdini); transform:scale(1.06); }
.post-video-title{
  padding:0 4px; font-weight:600; font-size:14.5px; line-height:1.4; color:var(--text);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.post-video-meta{ padding:0 4px; font:500 11.5px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--text-dim); }

/* ---- Newsletter sign-up ---- */
.post-news{
  padding:26px 26px 24px; border:1px solid var(--line); border-radius:12px; background:var(--surface);
  display:flex; flex-direction:column; gap:12px;
}
.post-news.align-center{ text-align:center; align-items:center; }
.post-news .post-news-heading{ margin:0; font-size:20px; line-height:1.3; color:var(--text); }
.post-news .post-news-text{ margin:0; color:var(--text-muted); font-size:15px; line-height:1.6; max-width:60ch; }
.post-news-form{ width:100%; max-width:520px; margin-top:4px; }
.post-news-form, .post-news-live{ display:flex; gap:10px; flex-wrap:wrap; }
.post-news-live{ width:100%; position:relative; }
.post-news-input, .post-news-live input[type="email"]{
  flex:1 1 220px; min-width:0; box-sizing:border-box; height:44px; padding:0 14px; margin:0;
  border:1px solid var(--line); border-radius:8px; background:var(--bg); color:var(--text);
  font:400 15px/42px var(--font-body, 'Inter', system-ui, sans-serif); text-align:left;
}
.post-news-input{ color:var(--text-dim); }
.post-news-live input[type="email"]::placeholder{ color:var(--text-dim); }
.post-news-live input[type="email"]:focus{ outline:none; border-color:var(--houdini); box-shadow:0 0 0 3px var(--houdini-glow, rgba(255,122,51,.18)); }
.post-news-button, .post-news-live button{
  flex:none; height:44px; padding:0 20px; border:0; border-radius:8px; cursor:pointer;
  background:var(--houdini); color:var(--on-accent, #0A0C10); font:600 14px/44px var(--font-body, 'Inter', system-ui, sans-serif);
  transition:filter .2s, transform .2s;
}
.post-news-live button:hover{ filter:brightness(1.08); }
.post-news-live button:focus-visible{ outline:2px solid var(--houdini); outline-offset:3px; }
.post-news-live button:disabled{ opacity:.6; cursor:default; }
.post-news-trap{ position:absolute !important; left:-9999px; width:1px; height:1px; opacity:0; }
.post-news .post-news-status{ margin:0; font-size:13.5px; color:var(--houdini); min-height:0; }
.post-news .post-news-status:empty{ display:none; }
.post-news .post-news-done{ display:none; margin:4px 0 0; font-weight:600; color:var(--text); }
.post-news.is-done .post-news-done{ display:block; }
.post-news .post-news-done:focus{ outline:none; }
.post-news.is-done .post-news-form{ display:none; }
.post-news .post-news-note{ margin:0; font-size:12.5px; color:var(--text-dim); line-height:1.5; }
@media (max-width:480px){ .post-news{ padding:20px 18px; } .post-news-button, .post-news-live button{ flex:1 1 100%; } }

/* ---- Comparison table: scrolls sideways on its own when it is too wide ---- */
.post-compare{ overflow-x:auto; border:1px solid var(--line); border-radius:12px; background:var(--surface); min-width:0; }
.post-compare table{ width:100%; border-collapse:collapse; margin:0; border:0; font-size:14.5px; line-height:1.45; }
.post-compare th, .post-compare td{ padding:12px 16px; border:0; border-bottom:1px solid var(--line); text-align:center; vertical-align:middle; background:none; }
.post-compare tbody tr:last-child > *{ border-bottom:0; }
.post-compare thead th{ font-weight:600; color:var(--text); white-space:nowrap; }
.post-compare thead td{ border-bottom:1px solid var(--line); }
.post-compare tbody th{ text-align:left; font-weight:500; color:var(--text); min-width:140px; }
.post-compare tbody td{ color:var(--text-muted); }
.post-compare-yes{ color:var(--houdini); font-weight:700; font-size:16px; }
.post-compare-no{ color:var(--text-dim); font-size:14px; }
/* The column picked out: a band the height of the table. */
.post-compare-hl-first :is(th, td):nth-child(2), .post-compare-hl-second :is(th, td):nth-child(3),
.post-compare-hl-third :is(th, td):nth-child(4), .post-compare-hl-fourth :is(th, td):nth-child(5),
.post-compare-hl-fifth :is(th, td):nth-child(6){ background:color-mix(in srgb, var(--houdini) 9%, transparent); }
.post-compare-hl-first thead th:nth-child(2), .post-compare-hl-second thead th:nth-child(3),
.post-compare-hl-third thead th:nth-child(4), .post-compare-hl-fourth thead th:nth-child(5),
.post-compare-hl-fifth thead th:nth-child(6){ color:var(--houdini); }

/* ---- Version history: a timeline, newest at the top ---- */
.post-versions{ position:relative; display:flex; flex-direction:column; gap:22px; padding-left:26px; }
.post-versions::before{ content:""; position:absolute; left:6px; top:8px; bottom:8px; width:2px; border-radius:2px; background:var(--line); }
.post-version{ position:relative; min-width:0; }
.post-version::before{
  content:""; position:absolute; left:-26px; top:5px; width:14px; height:14px; border-radius:50%; box-sizing:border-box;
  background:var(--bg); border:2px solid var(--line);
}
.post-version:first-child::before{ border-color:var(--houdini); background:var(--houdini); }
.post-version-head{ display:flex; align-items:center; flex-wrap:wrap; gap:6px 12px; margin-bottom:8px; }
.post-version-num{ font:600 16px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--text); }
.post-version-date{ font:500 12px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--text-dim); }
.post-version-latest{
  padding:3px 9px; border-radius:999px; border:1px solid var(--houdini); color:var(--houdini);
  font:600 10.5px/1.2 var(--font-mono, 'JetBrains Mono', monospace); letter-spacing:.1em; text-transform:uppercase;
}
.post-version ul{ margin:0; padding-left:18px; color:var(--text-muted); font-size:15px; line-height:1.65; }
.post-version li{ margin:2px 0; }
.post-version li::marker{ color:var(--text-dim); }

/* ---- Tabs: a row of names, one panel showing ---- */
.post-tabs{ min-width:0; }
.post-tabs-bar{
  display:flex; gap:4px; overflow-x:auto; scrollbar-width:none; border-bottom:1px solid var(--line); margin-bottom:18px;
}
.post-tabs-bar::-webkit-scrollbar{ display:none; }
.post-tab{
  flex:none; padding:10px 16px; margin-bottom:-1px; cursor:pointer; user-select:none; white-space:nowrap;
  border-bottom:2px solid transparent; color:var(--text-muted); font-weight:500; font-size:14.5px; line-height:1.3;
  transition:color .2s, border-color .2s;
}
.post-tab:hover{ color:var(--text); }
.post-tab.post-tab-on{ color:var(--houdini); border-bottom-color:var(--houdini); }
.post-tab:focus-visible{ outline:2px solid var(--houdini); outline-offset:-2px; border-radius:4px; }
.post-tab-panel{ display:none; color:var(--text-muted); font-size:15.5px; line-height:1.7; }
.post-tab-panel.post-tab-on{ display:block; }
.post-tab-panel:focus-visible{ outline:2px solid var(--houdini); outline-offset:4px; border-radius:4px; }
.post-tab-panel p{ margin:0 0 12px; }
.post-tab-panel p:last-child{ margin-bottom:0; }
/* While an article is written every tab's words show, each under its name. */
[contenteditable="true"] .post-tab-panel{ display:block; padding:8px 0 14px; border-bottom:1px dashed var(--line); }

/* ---- Updates: the newest few as cards, or the whole feed ---- */
.post-updates{ display:grid; grid-template-columns:repeat(var(--cols, 3), minmax(0, 1fr)); gap:18px; }
.post-updates-feed{ display:block; max-width:680px; margin:0 auto; }
@media (max-width:860px){ .post-updates:not(.post-updates-feed){ grid-template-columns:repeat(min(var(--cols, 3), 2), minmax(0, 1fr)); } }
@media (max-width:600px){ .post-updates:not(.post-updates-feed){ grid-template-columns:minmax(0, 1fr); } }
[contenteditable="true"] .post-updates:empty::before{
  content:"Your updates show here, on the site."; grid-column:1 / -1; padding:28px 16px; text-align:center;
  border:1px dashed var(--line); border-radius:10px; color:var(--text-dim); font:500 12px/1.4 var(--font-mono, 'JetBrains Mono', monospace);
}
.up-list{ display:flex; flex-direction:column; gap:18px; }
.up-single{ margin-bottom:28px; display:flex; flex-direction:column; gap:12px; }
.up-all{ align-self:flex-start; font:500 12.5px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--houdini); text-decoration:none; }
.up-all:hover{ text-decoration:underline; }
.up-card{
  display:flex; flex-direction:column; gap:12px; min-width:0; padding:18px 20px;
  background:var(--card-bg, var(--surface)); border:1px solid var(--card-border, var(--line)); border-radius:12px; scroll-margin-top:110px;
}
.up-card.is-pinned, .up-card.is-chosen{ border-color:color-mix(in srgb, var(--houdini) 55%, var(--line)); }
.up-meta{ display:flex; align-items:center; gap:10px; font:500 11.5px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--text-dim); }
.up-date{ color:inherit; text-decoration:none; }
.up-date:hover{ color:var(--houdini); }
.up-pin{ padding:3px 8px; border-radius:999px; border:1px solid var(--houdini); color:var(--houdini); letter-spacing:.08em; text-transform:uppercase; font-size:10px; }
.up-share{ margin-left:auto; padding:4px 9px; border:1px solid var(--btn-border, var(--line)); border-radius:6px; background:var(--btn-bg, transparent); color:var(--btn-ink, var(--text-dim)); font:inherit; cursor:pointer; }
.up-share:hover, .up-share:focus-visible{ color:var(--houdini); border-color:var(--houdini); }
.up-text{ font-size:15.5px; line-height:1.65; color:var(--card-text, var(--text)); overflow-wrap:anywhere; }
.up-text a{ color:var(--houdini); }
.up-compact .up-text{ display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical; overflow:hidden; font-size:14.5px; }
.up-card .up-image{ display:block; width:100%; height:auto; max-height:520px; object-fit:cover; border-radius:8px; border:1px solid var(--line); margin:0; }
.up-compact .up-image{ max-height:180px; }
.up-link{
  display:flex; align-items:center; gap:12px; min-width:0; padding:8px; text-decoration:none; color:inherit;
  border:1px solid var(--line); border-radius:10px; background:var(--bg); transition:border-color .2s;
}
.up-link:hover{ border-color:var(--houdini); }
.up-link img{ flex:none; width:96px; height:56px; object-fit:cover; border-radius:6px; margin:0; border:0; }
.up-link-words{ display:flex; flex-direction:column; gap:3px; min-width:0; }
.up-link-title{ font-weight:600; font-size:14px; line-height:1.35; color:var(--card-title, var(--text)); overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
.up-link-host{ font:500 11px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--text-dim); }
.up-more{ margin-top:auto; font:500 12px/1.3 var(--font-mono, 'JetBrains Mono', monospace); color:var(--houdini); text-decoration:none; }
.up-more:hover{ text-decoration:underline; }
.up-none{ grid-column:1 / -1; margin:0; color:var(--text-dim); font-size:14px; }
.up-loading{ min-height:1px; padding:14px 0; text-align:center; color:var(--text-dim); font-size:13px; }

/* "Skip to content" (site.js): off screen until the keyboard reaches it. */
.skip-link{
  position:absolute; left:12px; top:-80px; z-index:1000; padding:10px 14px; border-radius:8px;
  background:var(--surface); color:var(--text); border:1px solid var(--houdini);
  font-size:14px; text-decoration:none;
}
.skip-link:focus{ top:10px; }

/* Light theme: four small texts drawn in an accent colour were under 4.5:1
   (audit R3-A11Y-06). The orange on a light ground is 3.6 to 3.9 and dark
   words on the blue are 3.5, so here the words use a darker shade of their
   own accent, and the label a card shows on hover is white on its accent
   darkened a fifth (over 5:1 for any accent). The dark theme is unchanged. */
:root[data-theme="light"] .l1-badge,
:root[data-theme="light"] .contact-email{ color:color-mix(in srgb, var(--houdini) 80%, #000); }
:root[data-theme="light"] .tut-tag{ color:color-mix(in srgb, var(--unreal) 80%, #000); }
:root[data-theme="light"] .card-reveal{ color:#fff; background:color-mix(in srgb, var(--fx-accent) 80%, #000); }
