@import url("fonts.css");
@import url("dither.css");

/* ============================================================================
   keehan.co
   Dark gothic, ordered dither, brand carbon and brand purple. Nothing else.

   THE BRAND IS FOUR COLOURS
     #111111  carbon    the ground
     #A800FF  amethyst  structure
     #EF00FE  fuchsia   interaction
     cream              everything you read

   Everything on this site is one of those four or a measured step between two
   of them. There is no fifth hue. The previous build carried a lavender
   (#B583FC) doing all the link and accent work, which was a colour the brand
   never actually owned; it is gone, and fuchsia does that job now.

   THREE RULES HOLD THE WHOLE THING TOGETHER

     1. Nothing fades with opacity. Every transition, fill and ground loses
        dots through an ordered screen, not alpha. See dither.css.

     2. Every corner is square. No radius anywhere, on anything.

     3. Amethyst never sets small text. Measured on carbon it is 3.69:1, which
        clears AA for large text and UI boundaries and fails it for body. So
        amethyst is structure: rules, screens, fills, borders, the field.
        Fuchsia is 5.49:1 and clears AA on all three grounds, so fuchsia is
        the interactive colour. Cream is the reading colour. A visitor can
        infer the whole system from that: purple is the building, pink is the
        thing you can touch, cream is the thing you can read.

   WHY THE GROUNDS ARE NEUTRAL NOW
   The old grounds were purple-blacks (#0B0814, #161022, #22192F). Sitting
   amethyst on a purple-black leaves the accent sunk into the ground, and it
   is also the single most generated-looking combination on the web. Carbon is
   neutral, so the purple is the only chroma in the frame and reads as a
   decision instead of a default. The three tiers are #111111, #191919 and
   #212121 - the last is Carbon 2, already written down on the palette page.

   That leaves the tiers only 1.07 to 1.17 apart in luminance, which is too
   close to separate sections on brightness alone. That is deliberate: the
   dither screen separates them instead. Each ground tier carries a different
   Bayer level, so you read the change of section as a change of texture. It
   is the one job flat colour was doing badly and the screen does well.
   ============================================================================ */

:root{
  /* grounds - brand carbon, neutral, no violet in the ground itself */
  --carbon:#111111;
  --crypt:#191919;
  --stone:#212121;
  --pitch:#0B0B0B;   /* below carbon: insets, the debossed wordmark */

  /* rules - amethyst walked down into carbon, so even a hairline is brand */
  --edge:#4D0A70;
  --edge-soft:#3B0C54;
  --edge-faint:#2C0E3C;

  /* ink - one family, cream stepped toward carbon, so every tier is the same
     warm hue at a different distance. Measured on carbon (#111111) and on the
     worst ground (#212121):
       cream  15.67/13.36   vellum 12.21/10.41   ash 9.07/7.73
       dust    7.01/5.98    dimmest 5.36/4.57
     All five clear AA for small text, which the original ink ramp did not:
     its two quiet tiers ran 3.2 and 1.9 and failed every label on the site.

     WHY VELLUM EXISTS
     The four-tier ramp cleared AA everywhere and still read as low contrast,
     because clearing AA is not the same as ranking correctly. Measured across
     the home page, 58 elements sat between 4.5 and 7.7, 23 sat between 13 and
     15.7, and nothing at all sat between 9 and 13. Body prose was set in ash,
     a deliberately quiet tier, so the thing a reader came for was the second
     dimmest thing on the page while the ornament ran at full chroma.

     Vellum fills that hole at 12.21 and takes body prose. The five steps now
     run 15.67, 12.21, 9.07, 7.01, 5.36, each roughly 1.3x the next, so there
     is a real ladder instead of a bright cluster and a dim cluster. Cream is
     left to do what a top tier should: headings, emphasis, and links. */
  --cream:#F2E9D8;
  --vellum:#D7CFC0;
  --ash:#BAB3A6;
  --dust:#A39D92;
  /* Reserved, and currently unused. Every label that used to be set in it
     was 13px small text sitting at 4.57 on stone, which is AA by 0.07. Those
     all moved up to dust. Do not set text in dimmest again without checking
     the ground it lands on. */
  --dimmest:#8D887E;

  /* chroma - two, both raw brand, neither tinted or softened */
  --amethyst:#A800FF;
  --fuchsia:#EF00FE;

  /* faces
     --display is a pixel blackletter and only earns its keep at size, so it
     is reserved for the monumental moments: the poster title, the wordmark,
     the footer. Section headings are --prose at 600, which keeps the family
     count at three and keeps every heading legible. */
  --display:'Jacquard 12','Grenze',Georgia,serif;
  --prose:'Grenze',Georgia,'Times New Roman',serif;
  --mono:'Fira Mono',ui-monospace,Menlo,Consolas,monospace;

  /* type scale, 1.25 minor third, fluid between 380px and 1280px */
  --t--1:clamp(.78rem,.75rem + .12vw,.84rem);
  --t-0:clamp(1rem,.96rem + .2vw,1.12rem);
  --t-1:clamp(1.2rem,1.1rem + .45vw,1.4rem);
  --t-2:clamp(1.45rem,1.25rem + .9vw,1.95rem);
  --t-3:clamp(1.8rem,1.4rem + 1.7vw,2.9rem);
  --t-4:clamp(2.3rem,1.5rem + 3.4vw,4.6rem);
  --t-5:clamp(3rem,1.4rem + 6.8vw,8rem);
  /* One step past the top of the scale, and used in exactly one place:
     the front page title, which lost the paragraph under it and has to
     carry that row alone now. Adding a step rather than bumping --t-5
     because --t-5 is also the 404 and every other poster, and those all
     still have a lede doing half the work. */
  --t-6:clamp(3.6rem,1.2rem + 9.4vw,11rem);

  /* rhythm */
  /* The masthead is sticky, so anything trying to fill "the rest of the
     screen" has to subtract it. 66px of min-height on .masthead__in plus its
     1px bottom border; the .6rem padding is inside the min-height because
     everything here is border-box. Kept as a token rather than typed into the
     one rule that needs it, because the next thing that needs it will
     otherwise type 67px again and the two will drift. */
  --masthead:67px;
  --gutter:clamp(1.25rem,4vw,3.5rem);
  --band:clamp(3.5rem,8vw,7rem);
  --measure:64ch;

  /* Slime assets, painted through masks so colour stays tokenised.

     These paths are root-absolute, and they have to be. A url() inside a
     custom property does NOT resolve against the stylesheet that declares it;
     it resolves against the DOCUMENT that ends up using it. So the previous
     values, ../img/slime/*.svg, were resolved from /index.php as
     /img/slime/*.svg, which does not exist. Every one of them 404'd, and a
     mask whose image fails to load masks the element out entirely, so the
     slime dividers, the drip bullets, the corner creep, the blobmark and the
     fade-out were all rendering as nothing on every page.

     Verified: the identical url() written literally into a rule renders the
     drips correctly, and the same url() read back out of a custom property
     does not. Root-absolute is correct at any document depth. */
  --m-edge:url(/assets/img/slime/slime-edge-dither.svg);
  --m-blob:url(/assets/img/slime/slime-blob-dither.svg);
  --m-corner:url(/assets/img/slime/slime-corner-dither.svg);
  --m-bullet:url(/assets/img/slime/slime-bullet.svg);
  --m-ramp:url(/assets/img/slime/dither-ramp.svg);

  /* Back-compat aliases. admin.css, hermes.css and hermes-gate.css are their
     own stylesheets on the same tokens; these keep them rendering in brand
     while they carry the old names. Do not use them in new rules. */
  --void:var(--carbon);
  --bone:var(--cream);
  --lavender:var(--fuchsia);
}

*,*::before,*::after{box-sizing:border-box}

/* THE hidden ATTRIBUTE HAS TO WIN, AND WITHOUT THIS IT DOES NOT.
   [hidden]{display:none} comes from the user agent stylesheet, and ANY author
   declaration beats a UA one no matter how specific: .btn sets
   display:inline-block, which is (0,1,0) against the UA rule's (0,1,0), and
   author origin decides the tie. So <a class="btn" hidden> renders.

   That is not hypothetical here. The front page ships its Twitch button with
   hidden set and main.js toggles it, and without this rule the button is
   visible on an offline channel, which is the exact thing it exists to avoid.
   !important because the whole point is to outrank whatever display a
   component sets on itself; an element asked to be hidden is hidden. */
[hidden]{display:none!important}

html{
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;

  /* Tells the browser this document is dark, so native form controls, the
     default scrollbar and the canvas behind the page all render dark instead
     of flashing white. */
  color-scheme:dark;

  /* The masthead is sticky, so an anchor jump would otherwise land the target
     underneath it. */
  scroll-padding-top:5rem;
}

/* Same reason, for anything an in-page link can target. */
:target{scroll-margin-top:5rem}

/* --------------------------------------------------------------- lenis --
   Lenis 1.0.33 ships no stylesheet. It toggles lenis, lenis-smooth,
   lenis-scrolling, lenis-stopped and lenis-locked onto <html> and leaves it
   to the page to say what those mean. That was never done here, and the
   missing rule is the whole reason one notch of the wheel read as delayed.

   scroll-behavior:smooth above is CSS smooth scrolling and it applies to
   every programmatic scroll, which includes the ones Lenis makes. Lenis
   eases the wheel toward a target and writes scrollTop once per frame; the
   browser then ran its own easing toward each of those writes, and restarted
   it on the next frame before it had arrived. Two easings in series, the
   outer one never finishing, so the page trails the wheel by far more than
   the lerp in motion.js says it should.

   That is why halving the lerp in f460082 helped and did not fix it. The
   lerp was never the problem, so the fix is not another lerp change: 0.25
   stays, and it now actually behaves like the 154ms the table in motion.js
   derives.

   .lenis.lenis-smooth is (0,2,0) and html is (0,0,1), so the class wins on
   its own and no !important is needed. When Lenis is not running (reduced
   motion, or the three vendor scripts failing to load) the class is never
   added and the page keeps native CSS smooth scrolling, which is the right
   fallback rather than an accident.

   The other three are Lenis's own documented requirements: height:auto so a
   full-height <html> cannot cap the scroll range, overscroll containment on
   any subtree that opts out, and overflow:hidden while the scroll is
   stopped. */
html.lenis{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto}
.lenis.lenis-smooth [data-lenis-prevent]{overscroll-behavior:contain}
.lenis.lenis-stopped{overflow:hidden}

body{
  margin:0;
  background:var(--carbon);
  color:var(--vellum);
  font-family:var(--prose);
  font-size:var(--t-0);
  line-height:1.62;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;
}

/* the dithered blob field, painted by main.js. Fixed, inert, behind all. */
#field{
  position:fixed;
  inset:0;
  width:100%;
  height:100%;
  z-index:0;
  pointer-events:none;
  image-rendering:pixelated;
}
/* Lift page content above the fixed field canvas.

   This used to read `body > *:not(#field)`, and the :not(#field) was the
   problem. An id inside :not() contributes its own specificity, so that
   selector scored (1,0,1) and beat BOTH of the masthead's own declarations:
   .masthead is only (0,1,0), so its position:sticky was overridden to
   relative and its z-index:40 was overridden to 1.

   Two things broke as a result. The masthead stopped sticking on scroll. And
   because <main> is also z-index:1 and comes later in the document, main
   painted on top of the header, which buried the "Everything else" dropdown
   underneath the page: the panel was drawn but elementFromPoint over it
   returned section.shell.poster, so every link in it was unclickable.

   The :not() is not needed. #field's own rule is (1,0,0) and already wins on
   both position and z-index, so the plain selector below leaves the canvas
   alone while letting .masthead keep its own values. */
body > *{position:relative;z-index:1}

/* =============================================================== TILES ===
   The photo ground for small windows. includes/tiles.php emits the picture
   list and the two aspect-ratio queries into the head; everything here is
   the part that does not depend on which pictures are in it.

   WHY IT NEEDS ITS OWN POSITION AND Z-INDEX
   `body > *` directly above would otherwise make the mosaic a relative,
   z-index:1 sibling of main, which is a wall of photographs dropped on top
   of the page. A class is (0,1,0) and that selector is (0,0,1), so this rule
   wins without reaching for an id or an !important. z-index:-1 then puts it
   under the blob field at 0 and under content at 1, above nothing but the
   body's own carbon, which is the single layer it is there to replace.

   display:none is the resting state and the generated queries are the only
   thing that turns it into a grid. So above the trigger width there is no
   mosaic, no layout work, and no image request at all: a background-image
   inside an unmatched media query is never fetched, which is why this costs
   a desktop visitor nothing.

   WHY THE CELLS ARE HELD DOWN THIS HARD
   grayscale removes the one thing that would fight the brand, which is every
   hue in a photograph that is not amethyst or fuchsia. The opacity is
   --tile-ink from the admin and its default of .16 is not shyness: at full
   strength a face behind a paragraph takes cream-on-carbon from 15.67 down
   to roughly 2, and it does it in exactly the window sizes with the least
   room to recover. Raise it and read the page, not the thumbnail.

   The scrim is a second carbon pass over the mosaic, so the pictures sit in
   the ground rather than in front of it and the grid keeps one flat
   luminance for text however light or dark the individual photographs are. */
.tiles{
  position:fixed;
  inset:0;
  z-index:-1;
  display:none;
  grid-template-columns:repeat(auto-fill,minmax(var(--tile-cell,150px),1fr));
  grid-auto-rows:var(--tile-cell,150px);
  overflow:hidden;
  pointer-events:none;
  contain:layout paint;
}
/* THE BURN IS WHAT MAKES THIS SAFE, AND IT IS NOT THE OPACITY.

   The first version faded the pictures with opacity over a carbon scrim, and
   that is the obvious move and the wrong one. Fading moves the WHOLE range of
   the photograph toward the ground, so the only way to pull its brightest
   pixel down to something the palette can sit on is to fade the picture until
   it is not a picture. Measured on the front page at 700x620: ink .38 under a
   .32 scrim put the brightest possible ground at rgb(64,64,64), where dust
   fell to 3.86, fuchsia to 3.02 and amethyst to 2.04, all failing. Dropping
   the ink to clear those put it back at invisible.

   So the range is compressed first instead. contrast() expands the midtones
   while the picture is still full range, which is where the texture is, and
   brightness() then scales the result into the band this palette actually
   owns. scripts/contrast.mjs reports the blob field's worst ground as
   #300f32, luma about 25, so the mosaic is aimed at the same place: a
   brightest pixel near 32, which leaves fuchsia at 4.7, amethyst at 3.2 and
   dust at 6.0. The picture keeps all of its local contrast and loses only the
   absolute level, which is the half nobody reads the page through.

   --tile-ink is therefore a strength dial over an already-safe image, not the
   thing holding the contrast up. 100 is the ceiling the burn was chosen for. */
.tiles i{
  background-size:cover;
  /* A face sits high in a portrait frame, so a centred cover crop into a
     square cell lands on the chin and the collar. 38% keeps the eyes. */
  background-position:50% 38%;
  filter:grayscale(1) contrast(1.2) brightness(.15);
  opacity:var(--tile-ink,.85);
}
/* A ground made of photographs is a contrast reduction by definition, so
   anyone who asked the OS for more of it does not get one.

   !important, and it is load-bearing. The generated block is inline in the
   head and therefore later in the cascade than this file, and its
   `.tiles{display:grid}` scores the same (0,1,0) as the selector here, so
   source order would otherwise hand the win to the mosaic. This is the one
   rule in the file that has to beat markup it cannot be ordered against. */
@media (prefers-contrast:more){
  .tiles{display:none!important}
}

img{max-width:100%;height:auto;display:block}

/* ----------------------------------------------------------------- links --
   Fuchsia marks the link, but it marks it with the underline rather than the
   fill. That inverts what this rule used to do, for a measurable reason.

   Fuchsia on stone is 4.68. Body prose is 12.21. So a fuchsia link set inside
   a paragraph was 2.6x dimmer than the sentence containing it, and the eye
   read it as struck through rather than offered. A link that recedes from its
   own paragraph is the opposite of what "fuchsia is the interactive colour"
   was meant to buy, and it fails the general rule that an interactive state
   should carry more contrast than the surface it sits on, not less.

   So the text takes cream at 15.67, which puts a link one full step above the
   prose around it, and the underline carries the fuchsia. As a graphical
   object the underline only needs 3:1 and measures 5.49 on carbon and 4.68 on
   stone, so the brand colour still lands on every link and now clears its
   requirement with room rather than by 0.18.

   Hover still changes weight, not hue: the underline doubles. Contrast only
   ever goes up on interaction. */
a{
  color:var(--cream);
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-underline-offset:.22em;
  text-decoration-color:var(--fuchsia);
  transition:text-decoration-color .16s ease,text-decoration-thickness .16s ease;
}
a:hover{
  text-decoration-color:var(--fuchsia);
  text-decoration-thickness:2px;
}

/* Drops the 300ms tap delay and the default grey flash on touch. The flash is
   replaced by the real :active states further down, which are visible on
   touch as well as pointer. */
a,button,label,summary,[role="button"]{
  touch-action:manipulation;
  -webkit-tap-highlight-color:transparent;
}

:focus-visible{
  outline:2px solid var(--fuchsia);
  outline-offset:3px;
}

::selection{background:var(--amethyst);color:var(--cream)}

/* Scrollbar. The old thumb was an amethyst-to-fuchsia linear gradient, which
   is the one place on the site a gradient survived; it is now a flat amethyst
   bar with a fuchsia thumb on hover, so the scrollbar follows the same
   structure-versus-interaction rule as everything else. */
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:var(--pitch)}
::-webkit-scrollbar-thumb{background:var(--amethyst);border:2px solid var(--pitch)}
::-webkit-scrollbar-thumb:hover{background:var(--fuchsia)}
html{scrollbar-color:var(--amethyst) var(--pitch)}

.skip{
  position:absolute;left:-9999px;top:0;z-index:100;
  background:var(--stone);color:var(--cream);padding:.7rem 1rem;
  border:1px solid var(--fuchsia);
  font-family:var(--mono);font-size:var(--t--1);
}
.skip:focus{left:var(--gutter)}

/* =============================================================== TYPE ==== */

h1,h2,h3{
  color:var(--cream);
  margin:0;
  text-wrap:balance;
}

/* h1 is the one place the blackletter runs. It is a pixel face, so it wants
   loose tracking to keep the strokes from knitting together, and a line
   height near 1 because the glyphs already carry a lot of vertical air. */
h1{
  font-family:var(--display);
  font-weight:400;
  font-size:var(--t-4);
  line-height:1.02;
  letter-spacing:.02em;
}

/* Section headings are the reading face pushed to 600. Weight and size carry
   the hierarchy instead of a second display family. */
h2,h3{
  font-family:var(--prose);
  font-weight:600;
  line-height:1.16;
  letter-spacing:-.005em;
}
h2{font-size:var(--t-3)}
h3{font-size:var(--t-2)}

/* Section headings get a screened rule under them rather than a hairline.
   It is the same Bayer matrix as the grounds and the slime, so a heading
   reads as sitting on the page's texture rather than on a separate ruling
   system. Fine cell, because at 3px a 6px-tall rule would only be two dots. */
.band h2::after{
  content:"";
  display:block;
  height:6px;
  margin-top:.75rem;
  max-width:7.5rem;
  background-color:var(--amethyst);
  -webkit-mask:var(--dzf-8) repeat;
          mask:var(--dzf-8) repeat;
}

/* The small mono label. It is a look, not a heading level, so it is a class
   rather than an element: a subsection that genuinely sits under an h2 uses
   <h3 class="label">, and something that only labels a list uses
   <p class="label">. Tying the look to h4 forced an h2 to h4 jump on every
   page. */
h4,
.label{
  font-family:var(--mono);
  font-weight:500;
  font-size:var(--t--1);
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--dust);
  margin:0;
  line-height:1.4;
}
/* The label is already a heading-shaped thing; it does not also take the
   screened rule. */
.band h2.label::after{display:none}

p{margin:0 0 1em}
p:last-child{margin-bottom:0}

strong{color:var(--cream);font-weight:500}
em{font-style:italic}

code,kbd,samp{
  font-family:var(--mono);
  font-size:.86em;
  background:var(--pitch);
  color:var(--cream);
  padding:.1em .38em;
  border:1px solid var(--edge-faint);
}

/* A rule is a screened band, not a hairline. Same reason as the heading rule:
   one ruling system for the whole site, and it is the dither. */
hr{
  border:0;
  height:6px;
  margin:var(--band) 0;
  background-color:var(--amethyst);
  -webkit-mask:var(--dzf-6) repeat;
          mask:var(--dzf-6) repeat;
}

/* An eyebrow is rationed: at most one per three sections on any page.
   Most sections do not need one. The heading is enough. */
.eyebrow{
  font-family:var(--mono);
  font-size:var(--t--1);
  font-weight:500;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--dust);
  margin:0 0 .8rem;
}

/* A lede introduces the section under it, so it cannot be quieter than the
   body it introduces. It used to be ash, which put it below the prose it was
   meant to lead. Cream, and the size does the rest of the separation. */
.lede{
  font-size:var(--t-1);
  line-height:1.5;
  color:var(--cream);
  max-width:46ch;
}

/* ============================================================= LAYOUT ==== */

.shell{
  width:100%;
  max-width:1240px;
  margin-inline:auto;
  padding-inline:var(--gutter);
}

/* Vertical padding is deliberately not symmetric. Equal top and bottom reads
   as slightly top-heavy, because the heading at the top of a band already
   carries its own optical space above it. */
/* position:relative for the divider that hangs off the top of a band, and a
   z-index on everything else so the drips pass behind the type rather than
   over it. :not(.slime) rather than a class on the content, because a band's
   direct child is .shell on every page and adding a marker class to thirty of
   them to express "not the divider" is worse than saying it once here. */
.band{padding-block:var(--band) calc(var(--band) * 1.12);position:relative}
.band > *:not(.slime){position:relative;z-index:1}
.band--tight{padding-block:calc(var(--band) * .55) calc(var(--band) * .62)}

/* Section grounds.
   The three carbon tiers sit only 1.07 to 1.17 apart in luminance, which is
   not enough to separate sections on brightness. So each tier also carries
   its own Bayer level, and the change you actually perceive between two
   sections is a change of screen density. Flat colour was doing this job
   badly on a palette this dark; the screen does it well.

   The screen ink is amethyst over the ground, which at these levels lands
   between #1A1020 and #240F2F. Cream measures 15.30 and 14.71 against those,
   so screening a ground costs the text on it almost nothing. */
.band--stone{background:var(--stone)}
.band--crypt{background:var(--crypt)}

.band--stone,
.band--crypt,
.band--screened{position:relative;isolation:isolate}

.band--stone::before,
.band--crypt::before,
.band--screened::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--pitch);
  pointer-events:none;
  -webkit-mask-repeat:repeat;
          mask-repeat:repeat;
}
/* Raised sections carry more screen than recessed ones, so the texture
   gradient runs the same direction as the (very slight) luminance one.

   Two things here were wrong on the first pass and are worth writing down,
   because both are easy to get wrong again.

   The ink was raw amethyst. Averaged over the tile that measures fine, but a
   screen is not an average: at 2/16 one pixel in eight is FULL #A800FF, and a
   fully saturated dot under a glyph is a local contrast event no averaged
   number describes. Ground screens use --edge instead, which is amethyst
   already most of the way down to carbon, so a dot is a dark violet grain
   rather than a lit pixel.

   The cell was 3px. At that size a ground-wide screen stops reading as grain
   and starts reading as a checkerboard, and it fought the type instead of
   sitting under it. Grounds use the 2px family. The 3px family is still right
   for small deliberate surfaces, where there is room for exactly one idea.

   THIRD THING, AND IT UNDID THE HEADER OF THIS FILE
   The ink then became --edge-faint, which is dark enough to stop the local
   contrast problem above and still purple. A screen is not an average for
   hue either. Painting a violet ink across 37% of a ground does not leave a
   neutral ground with violet grain in it, it leaves a violet ground:

     stone  + edge-faint at 6/16  ->  #251A2B, chroma 17
     crypt  + edge-faint at 3/16  ->  #1D1720, chroma 9

   The tokens said chroma 0 and nothing on screen was. Worse, compare those
   to the purple-blacks this file's header says were deleted for being the
   most generated-looking combination on the web: #161022 chroma 18, #22192F
   chroma 22. The screen had quietly rebuilt the exact grounds the rebuild
   existed to remove, which is why the page read as plum.

   The ink is --pitch now: the tier below carbon, chroma 0. Sections separate
   by texture and by going DARKER, never by picking up a hue.

     stone  + pitch at 6/16  ->  #191919, chroma 0   (exactly the crypt tier)
     crypt  + pitch at 3/16  ->  #161616, chroma 0

   Be careful about what those blends do and do not mean. A screen is a mask,
   so the ground is not really #191919: it is 6 pixels in 16 of pitch and 10
   in 16 of bare stone. #191919 is what the eye integrates, which is why the
   plum was visible, and it is the right number for judging hue. It is the
   WRONG number for judging contrast, because a glyph can land on a bare stone
   pixel. Worst-case local ground is still #212121, exactly as before, so
   scripts/contrast.mjs is still measuring the right thing and no token needed
   re-toning. Going darker means nothing can have got worse.

   So: perceived hue neutral, worst-case contrast unchanged. With the grounds
   finally reading neutral, amethyst and fuchsia are the only chroma in the
   frame, which is what the header claimed all along. */
/* FOURTH THING: THE GROUNDS WERE ONE FREQUENCY
   Everything above is about hue and about the worst-case pixel, and both were
   settled correctly. What was left was that a flat tile only has one spatial
   frequency in it. Every lit cell was the same 2px square, so across a whole
   section the screen stopped reading as grain and started resolving as a
   lattice: crosshatch, a flat weave, the exact texture a duotone filter puts
   on a photo.

   The dzg family fixes it at the source. Same matrix, same levels, but the dot
   in each cell is graded from a full 5px cell down to a single pixel, so one
   20px tile contains five different dot sizes instead of one. See the note
   above GRAIN in scripts/gen-dither.mjs.

   LEVELS ARE NOT THE SAME NUMBER ANY MORE, BECAUSE THE UNIT CHANGED
   A dzf level is coverage: dzf-6 covers 6 cells in 16, which is 0.375 of the
   tile. A dzg level is only how many cells are LIT, and most of those dots are
   smaller than their cell, so the levels below were picked by matching AREA to
   what each ground already had rather than by keeping the number:

     crypt   dzf-3  0.1875  ->  dzg-6   0.1944
     stone   dzf-6  0.3750  ->  dzg-12  0.3750   exact
     footer  dzf-8  0.5000  ->  dzg-16  0.5000   exact

   Two of the three land exactly and crypt is 0.007 heavier, which is under
   half a dot per tile. The screen is --pitch painted over the ground and the
   worst case a glyph can land on is a BARE ground pixel, so the number that
   decides contrast is unchanged either way. Nothing in scripts/contrast.mjs
   needed re-toning, and its run is unchanged after this.

   dzg-16 is not a typo and not a fill. 16 is excluded from every other family
   because a flat tile with all sixteen cells lit is solid colour; here all
   sixteen carry a graded dot, half of them below full size, and the tile is
   half covered. */
.band--crypt::before{-webkit-mask-image:var(--dzg-6);mask-image:var(--dzg-6)}
.band--stone::before{-webkit-mask-image:var(--dzg-12);mask-image:var(--dzg-12)}
.band--screened::before{-webkit-mask-image:var(--dzg-12);mask-image:var(--dzg-12)}

/* No mask support: the bands fall back to their flat carbon tiers, which is
   a duller page but a correct one. */
@supports not ((mask-image:url(#x)) or (-webkit-mask-image:url(#x))){
  .band--stone::before,.band--crypt::before,.band--screened::before{display:none}
}

.measure{max-width:var(--measure)}

/* main gets no padding of its own; each band owns its rhythm */
main{display:block}

/* =============================================================== NAV ===== */

.masthead{
  position:sticky;
  top:0;
  z-index:40;
  background:color-mix(in srgb,var(--carbon) 90%,transparent);
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
  border-bottom:1px solid var(--edge-soft);
}

/* The masthead's bottom edge is a screened band rather than only a hairline,
   so the one piece of chrome that sits over every page is made of the same
   material as the page. */
.masthead::after{
  content:"";
  position:absolute;
  left:0;right:0;bottom:-5px;
  height:5px;
  background-color:var(--amethyst);
  -webkit-mask:var(--dzf-4) repeat;
          mask:var(--dzf-4) repeat;
  pointer-events:none;
}

.masthead__in{
  display:flex;
  align-items:center;
  gap:1.5rem;
  min-height:66px;
  padding-block:.6rem;
  /* the positioning context for .nav__panel, see the note on .nav__list > li */
  position:relative;
}

.brand{
  font-family:var(--display);
  font-weight:400;
  font-size:var(--t-2);
  color:var(--cream);
  text-decoration:none;
  letter-spacing:.02em;
  line-height:1;
  flex:none;
}
/* The wordmark keeps cream on hover and takes a fuchsia underline instead.
   Swapping the fill dropped it from 15.67 to 5.49, which made the one piece
   of permanent navigation on the site harder to see the moment you aimed at
   it. */
.brand:hover{
  color:var(--cream);
  text-decoration:underline;
  text-decoration-color:var(--fuchsia);
  text-decoration-thickness:2px;
  text-underline-offset:.18em;
}

.nav{margin-left:auto}
.nav__list{
  display:flex;
  /* baseline, not center. The four links are <a> and the fifth is a
     <button>, and centring aligns their BOXES, which are different heights:
     measured 33px for a link and 28px for the button. Text in boxes of
     different heights, centred, does not share a line. Baseline alignment
     puts every label on the same line whatever the box around it does. */
  align-items:baseline;
  gap:1.4rem;
  list-style:none;
  margin:0;
  padding:0;
}
/* NOT position:relative any more, and that was the bug.
   The panel is absolutely positioned with top:100%, so whatever is relative
   nearest to it decides where 100% starts. This li is only as tall as its
   link, roughly 48px, while the masthead is 67px, so the panel opened 10px
   INSIDE the header: measured, panel top 57px against masthead bottom 67px.
   The top of the dropdown was behind the bar it hangs off.

   .masthead__in is the anchor now. It is the full height of the bar, so
   top:100% is genuinely the bottom of the bar. */
.nav__list > li{position:static}

.nav__link{
  font-family:var(--mono);
  font-size:var(--t--1);
  /* inherit, and it is the fix. "Everything else" is a <button>, and buttons
     do NOT inherit line-height: the UA stylesheet gives every form control
     line-height:normal. The links around it inherit 1.62 from body. So the one
     trigger that opens a dropdown was built on a different line box from the
     four links beside it and sat 1.4px low, measured. */
  line-height:inherit;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--dust);
  text-decoration:none;
  background:none;
  border:0;
  padding:.35rem 0;
  cursor:pointer;
  display:inline-block;
  white-space:nowrap;
  -webkit-appearance:none;
          appearance:none;
}

/* THE CARET
   Nothing said "Everything else" opened anything. It was styled exactly like
   the four plain links beside it, so the only way to find out was to hover it,
   and on a touch screen hovering is not a thing.

   A chevron, drawn as two borders of a rotated square, in currentColor so it
   brightens with the label on hover and when open. It turns over when the
   panel is open, driven off aria-expanded, which main.js already maintains for
   screen readers: one attribute, two jobs, no extra state to keep in sync. */
.nav__item > .nav__link{display:inline-flex;align-items:baseline;gap:.55em}
.nav__item > .nav__link::after{
  content:"";
  display:inline-block;
  width:.42em;
  height:.42em;
  border-right:1.5px solid currentColor;
  border-bottom:1.5px solid currentColor;
  transform:translateY(-.22em) rotate(45deg);
  transition:transform .16s ease;
}
.nav__item > .nav__link[aria-expanded="true"]::after{
  transform:translateY(.08em) rotate(225deg);
}
@media (prefers-reduced-motion:reduce){
  .nav__item > .nav__link::after{transition:none}
}
.nav__link:hover,.nav__link[aria-expanded="true"]{color:var(--cream)}
.nav__link[aria-current="page"]{color:var(--cream);box-shadow:0 1px 0 var(--fuchsia)}

/* ------------------------------------------------------- the dropdown --
   Nineteen links in four groups. The old panel put them in a two-column grid
   with the group headings spanning both columns, which meant you read ACROSS
   a group ("About, Contact / Artwork, Pallette") instead of down it, and the
   headings were the only thing telling you a new group had started. It was
   also 480x658: mostly empty, and nearly as tall as the viewport.

   One column per group instead. Each group is a heading plus its own short
   list, so reading order is vertical and obvious, the panel is wide and shallow
   rather than narrow and deep, and the columns reflow on their own when there
   is less room.

   It also now carries a screen, like every other surface here. Flat crypt was
   the one component on the site that did not belong to the dither system. */
.nav__panel{
  position:absolute;
  /* 100% is the bottom of .masthead__in. The masthead's 1px border sits below
     that, and .masthead::after hangs a 5px screened band below THAT, so the
     panel clears both and leaves 2px of air. Written as three terms rather
     than one number so it is obvious what each one is paying for. */
  top:calc(100% + 1px + 5px + 2px);
  /* .masthead__in is .shell, so its box includes the gutter padding. right:0
     would put the panel against the shell's outer edge, a gutter to the right
     of the trigger it belongs to. */
  right:var(--gutter);
  /* Above .masthead::after. That band is a pseudo-element of .masthead and is
     therefore generated after .masthead__in, so at z-index:auto it painted
     over the panel's top border even once the geometry was right. */
  z-index:1;
  isolation:isolate;
  width:min(42rem,calc(100vw - var(--gutter) * 2));
  max-height:calc(100svh - var(--masthead) - 2rem);
  overflow-y:auto;
  overscroll-behavior:contain;
  background:var(--crypt);
  border:1px solid var(--edge);
  padding:1.5rem 1.7rem 1.6rem;
  display:grid;
  /* Three groups, three columns, stated rather than negotiated. auto-fit sized
     itself against a max-content width, came up one column short, and dropped
     the last group into the bottom of column one where a zero row gap jammed
     it against the list above. The count is known, so it is declared. It was
     four until the Turtel SMP group left the menu, and the width came down
     from 56rem with it. */
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:1.6rem 2.2rem;
  align-items:start;
  list-style:none;
  margin:0;
  opacity:0;
  visibility:hidden;
  transform:translateY(-4px);
  transition:opacity .15s ease,transform .15s ease,visibility 0s linear .15s;
}
/* The gap above is a dead zone the pointer must cross. This bridges it so
   :hover never breaks mid-travel. It has to be at least as tall as the gap in
   the top offset above, or the pointer drops the panel on the way down. */
.nav__panel::before{content:"";position:absolute;inset:-10px 0 auto 0;height:10px}

/* The ground screen. Same Bayer family and the same quiet ink as the bands,
   so the panel reads as a piece of the page rather than a floating widget. */
.nav__panel::after{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--edge-faint);
  -webkit-mask:var(--dzf-4) repeat;
          mask:var(--dzf-4) repeat;
  pointer-events:none;
}

.nav__item.is-open .nav__panel,
.nav__item:focus-within .nav__panel{
  opacity:1;visibility:visible;transform:none;
  transition:opacity .15s ease,transform .15s ease,visibility 0s;
}

.nav__col{min-width:0}

/* A group heading takes the same screened rule the section headings use, so
   the ruling system is the dither here too and not a stray hairline. */
.nav__group{
  position:relative;
  font-family:var(--mono);
  font-size:var(--t--1);
  font-weight:500;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--dust);
  margin:0 0 .6rem;
  padding-bottom:.55rem;
  line-height:1.4;
}
.nav__group::after{
  content:"";
  position:absolute;
  left:0;right:0;bottom:0;
  height:4px;
  background-color:var(--amethyst);
  -webkit-mask:var(--dzf-8) repeat;
          mask:var(--dzf-8) repeat;
}
.nav__set{list-style:none;margin:0 0 1.5rem;padding:0}
.nav__set:last-child{margin-bottom:0}

.nav__panel a{
  position:relative;
  display:block;
  padding:.4rem .6rem;
  margin-left:-.6rem;
  font-family:var(--prose);
  font-size:var(--t-0);
  line-height:1.35;
  color:var(--ash);
  text-decoration:none;
  transition:color .14s ease;
}
/* Hover is a screened bar that grows, not a block that fades in. Width is the
   thing that changes, so rule 1 holds: nothing here loses alpha. */
.nav__panel a::before{
  content:"";
  position:absolute;
  left:0;top:.1rem;bottom:.1rem;
  width:0;
  background-color:var(--fuchsia);
  -webkit-mask:var(--dzf-8) repeat;
          mask:var(--dzf-8) repeat;
  transition:width .14s ease;
}
.nav__panel a:hover,
.nav__panel a:focus-visible{color:var(--cream)}
.nav__panel a:hover::before,
.nav__panel a:focus-visible::before{width:3px}

@supports not ((mask-image:url(#x)) or (-webkit-mask-image:url(#x))){
  .nav__panel::after{display:none}
  .nav__group::after{background-color:var(--edge)}
}

/* Between the mobile sheet and a comfortable desktop there is not room for
   three columns of link text without them going ragged. */
@media (max-width:1180px){
  .nav__panel{grid-template-columns:repeat(2,minmax(0,1fr));width:min(30rem,92vw)}
}

.nav__toggle{display:none}

@media (max-width:900px){
  .nav__list{display:none}
  /* .nav still carries margin-left:auto from the desktop rule. With the list
     hidden it is a zero-width box that absorbs all the free space, which
     leaves the toggle stranded in the middle of the bar. Zeroing the row gap
     as well, or the flex gap to that empty box holds the toggle 24px short
     of the gutter. */
  .nav{margin-left:0}
  .masthead__in{gap:0}
  .nav__toggle{
    display:inline-block;margin-left:auto;
    font-family:var(--mono);font-size:var(--t--1);letter-spacing:.16em;
    text-transform:uppercase;color:var(--cream);background:none;
    border:1px solid var(--edge);padding:.5rem .85rem;cursor:pointer;
  }
  .nav__toggle:hover{border-color:var(--fuchsia)}
  .masthead.is-open .nav__list{
    display:block;position:absolute;left:0;right:0;top:100%;
    background:var(--crypt);border-bottom:1px solid var(--edge);
    /* No horizontal padding on the sheet itself. Every row inside carries its
       own gutter, which is what lets a row's separator and its tap target run
       the full width of the screen instead of stopping short of it. */
    padding:0 0 .5rem;
    max-height:calc(100svh - var(--masthead));
    overflow-y:auto;
    overscroll-behavior:contain;
  }
  .masthead.is-open .nav__list > li{margin-bottom:0}

  /* ROWS, NOT A NAV BAR LAID SIDEWAYS.
     The desktop rules were being inherited whole into the sheet: 16px mono at
     0.16em tracking, a 37px tall link, and the panel's group headings still
     wearing their full-width screened rule. That is the desktop component
     stacked, not a menu. Three things change.

     TRACKING. 0.16em is right for four short words sitting in a row on a
     1240px bar. Down a phone it turns SCHEDULE into eight separate letters and
     the words stop being scannable. 0.1em keeps the mono character and gives
     the word back.

     TAP TARGETS. Measured before this: 37px for a top-level link, 35px for a
     sub-link. The floor for a touch target is 44px and neither cleared it.
     Padding does that job here rather than a height, so a wrapped label grows
     the row instead of overflowing it.

     HIERARCHY. On desktop the four groups are four columns and the layout
     carries the structure. Stacked, there is no layout left to carry it, so
     the sub-lists take a rule down their left edge and the top-level rows take
     a separator. */
  .masthead.is-open .nav__link{
    display:block;
    width:100%;
    text-align:left;
    padding:.85rem var(--gutter);
    font-size:var(--t-0);
    letter-spacing:.1em;
    border-bottom:1px solid var(--edge-faint);
  }
  .masthead.is-open .nav__list > li:last-child > .nav__link{border-bottom:0}

  /* In the sheet the panel is not a panel: it is the rest of the list, held
     under its trigger by a rule rather than by a box. */
  .nav__panel{
    position:static;opacity:1;visibility:visible;transform:none;
    max-height:none;border:0;background:none;z-index:auto;
    grid-template-columns:1fr;width:auto;max-width:none;gap:0;
    padding:.35rem 0 .75rem;
    margin-inline:var(--gutter);
    border-left:1px solid var(--edge);
  }
  .nav__panel::after{display:none}
  .nav__panel::before{display:none}
  /* The sub-list is always shown in the sheet, held by its left rule, so a
     caret here would promise a toggle that does nothing. */
  .masthead.is-open .nav__item > .nav__link::after{display:none}
  .masthead.is-open .nav__item > .nav__link{display:block}

  .nav__col{padding-left:1rem}
  .nav__set{margin-bottom:.35rem}
  .nav__panel a{padding-block:.75rem;font-size:var(--t-0)}   /* 45px measured, clears the 44px floor */

  /* The group heading loses its screened rule. At four columns that rule is a
     column header; at one column it is a full-width band every few rows, and
     the sheet reads as four stacked tables. The label alone is enough once
     the border-left is showing the nesting. */
  .nav__group{
    margin:1.1rem 0 .15rem;
    font-size:var(--t--1);
    letter-spacing:.14em;
    color:var(--dust);
  }
  .nav__group::after{display:none}
  .nav__col:first-child .nav__group{margin-top:.35rem}
}

/* ============================================================== POSTER ===
   Layout family 1. Asymmetric opener: display type against a short column.
   Used once per page, at the top, and nowhere else. */

.poster{
  display:grid;
  grid-template-columns:minmax(0,1.55fr) minmax(0,.85fr);
  gap:clamp(1.75rem,4vw,3.5rem);
  align-items:center;
  padding-block:clamp(2.5rem,6vw,4.5rem) clamp(2rem,5vw,3.5rem);
}
.poster--solo{grid-template-columns:minmax(0,1fr)}
.poster--solo .poster__lede{max-width:52ch}

/* The front page, and only the front page.

   THE SIZE OF IT
   This row is the whole first screen: the name, what is happening right now,
   and two ways in. Everywhere else a poster is an introduction to the section
   under it and is sized to get out of the way. Here there is nothing above it
   and nothing beside it, so it holds the viewport.

   svh, not vh. On a phone 100vh is the height with the browser chrome
   RETRACTED, so a vh-sized hero is taller than the screen on arrival and the
   buttons sit under the address bar until you scroll. svh is the small
   viewport: correct on landing, which is the one moment this has to be right.
   The vh line above it is the fallback and stays first so a browser without
   svh keeps something sensible rather than nothing.

   WHY IT IS NOT THE WHOLE SCREEN
   4.5rem short, on purpose. A hero that fills the viewport exactly gives a
   reader no reason to believe anything is under it, and the usual answer is a
   bouncing chevron. This site already has a better one: the next band carries
   its slime divider on ITS top edge, so leaving a strip uncovered puts the
   drips on screen at the fold. The cue is the page's own artwork rather than
   an arrow bolted onto it.

   No cap on tall displays. A 1440px-tall screen gets a 1300px hero, and the
   space is not empty: the dithered field is behind it and reading as
   atmosphere is exactly that field's job.

   align-content, not align-items. align-items is already centring each item
   inside its own track; this centres the single row track inside the taller
   box. Both are needed and they are not the same property.

   line-height stays at the .86 every other poster title uses. It was tightened
   to .82 while there was a sub-line to pull up against; with one line that only
   crops the box and pushes the buttons closer than the rhythm elsewhere. */
.poster--lead{
  min-height:calc(100vh - var(--masthead) - 4.5rem);
  min-height:calc(100svh - var(--masthead) - 4.5rem);
  align-content:center;
  padding-block:clamp(3rem,7vw,5.5rem);
}
.poster--lead .poster__title{font-size:var(--t-6)}

/* The rail is half of this row now rather than a footnote beside a paragraph,
   so it stops being small print. Only on the front page: the same component
   on schedule.php sits under a heading in a normal band and is correctly
   quiet there. */
.poster--lead .rail dt,
.poster--lead .rail dd{font-size:var(--t-0)}
.poster--lead .rail .rail__row{padding-block:.85rem}

.poster__title{
  font-size:var(--t-5);
  line-height:.86;
  margin:0 0 1rem;
  position:relative;
}
/* The second line of a poster title, when there is one. Amethyst is legal
   here because the poster title runs at --t-5, which is far into large-text
   territory where 3.69:1 clears AA. */
.poster__title .quiet{display:block;color:var(--amethyst)}

.poster__lede{margin:0 0 1.6rem}

.poster__art{position:relative;isolation:isolate}
/* The art treatment started as a 3rem amethyst drop-shadow, which is a soft
   glow: the one effect this palette should never use, because a glow is alpha
   falloff and the whole site is built on the opposite idea. That was replaced
   by a screened plate, which fixed the alpha problem and introduced a worse
   one. A single dither level repeated across a box is a CONSTANT density, and
   a constant density has a hard edge. So the "halo" was a flat purple
   rectangle of dots stopping dead on a straight line behind a round
   character. It read as a mis-sized overlay rather than as a halo.

   A halo needs falloff, and rule 1 says falloff cannot be alpha, so it has to
   be density. The blob mask already carries exactly that: it is the slime
   mass, screened through the same Bayer matrix, dense in the middle and
   dithering out through its own drips at the edge. So the plate becomes the
   blob, and the edge of the shape is the edge of the halo.

   Two other changes. It is painted in edge rather than raw amethyst, because
   this sits BEHIND the subject and ornament should not out-shout the thing it
   frames. And it is scaled past the image on every side, so the mascot sits
   in the mass instead of on top of a shape the same size as itself.

   FOURTH THING, AND IT HELD THE RULE OFF THE PAGE ENTIRELY
   The paragraph below used to sit outside this comment, because the line
   above closed it early. Seventeen lines of prose were parsed as CSS,
   and a stylesheet recovering from that error consumes tokens until the next
   {, so the junk and the selector after it became one invalid prelude and the
   whole rule was dropped. The two pseudo-elements still picked up their inset,
   ink and mask from the rules below, but nothing declared content, and a
   pseudo-element without content never generates. The halo this comment
   spends thirty lines specifying had never rendered on any page using it.

   The blob mask was tried here and is wrong for the job: it is solid through
   its mass and only dithers where the drips fall, so stretched to a wide box
   it becomes a large solid dome with a hard curved top. Trading a hard
   straight edge for a hard curved one is not progress.

   What actually makes a halo is two steps of DENSITY, not one of shape. A
   dense inner plate sits just off the art, and a sparse outer field runs well
   past it. The outer field is only one cell in sixteen, so its own boundary is
   too thin to read as an edge at all, which is the whole trick: the halo ends
   by running out of dots rather than by stopping at a line. Both steps are
   square, because rule 2 leaves no radius anywhere, and both lose coverage
   rather than alpha, because rule 1 leaves no fades.

   The inner step is edge, walked down so it stays behind the subject. The
   outer step is raw amethyst, which it can afford at six percent coverage:
   sparse bright pixels read as scatter.

   FIFTH THING, AND IT IS THE ONE THAT ACTUALLY WORKED
   Two steps of density was still two boxes, and the first time this rule ever
   reached a browser that is exactly what it looked like: a dense dotted
   rectangle inside a sparse dotted rectangle, both stopping dead on straight
   lines behind a round character. The reasoning above is right about WHERE the
   falloff has to come from and wrong about where it can live. A tile repeated
   across a box is a constant density, so its only edge is the box, and no
   number of nested boxes fixes that. It just adds edges.

   The falloff belongs in the mask. --dz-ramp is that mask: nine concentric
   rings, level 14 at the middle down to level 1 at the rim, generated by
   gen-dither.mjs from the same Bayer matrix as every other surface here. The
   rim is one cell in sixteen, too sparse for its own boundary to read as a
   line, so the halo ends by running out of dots. One layer, not two, because
   the ramp is already doing what the second box was there to fake.

   Still painted in edge and not amethyst. That part of the reasoning survived
   contact with the render: at amethyst the halo is brighter than the mascot
   it sits behind, and ornament does not get to out-shout its subject. */
.poster__art::before{
  content:"";
  position:absolute;
  pointer-events:none;
  inset:-34%;
  z-index:-1;
  background-color:var(--edge);
  -webkit-mask:var(--dz-ramp) no-repeat center/contain;
          mask:var(--dz-ramp) no-repeat center/contain;
}
.poster__art img{width:100%}

@media (max-width:820px){
  /* The halo reaches 34% past the art on every side, which is right when the
     art has a whole column to itself and wrong once the poster stacks: at
     16rem the halo is 430px, wider than a phone, and the top of it lands in
     the lede. Ornament over content is the one thing this file will not do,
     so the reach comes in and the row gap goes out far enough to hold it. */
  .poster{grid-template-columns:1fr;gap:3rem}
  .poster__art{max-width:16rem}
  .poster__art::before{inset:-18%}

  /* Stacked, the hero's own content is already taller than the strip the
     min-height was reserving, so the rule stops doing anything except on a
     phone held sideways, where it would reserve a screen and a half for two
     buttons. It comes off and the padding carries the presence instead. */
  .poster--lead{min-height:0;padding-block:clamp(2.5rem,9vw,4rem)}
}

/* ============================================================== PORTAL ====
   Layout family 8. The front page hero's right hand column, standing in for
   the countdown rail. Same slot, same parallax number, opposite job: the rail
   was information, and information is not allowed to move much while you read
   it. This is atmosphere, so it can.

   THE PICTURE DOES NOT MOVE. Only the edge does. That is the difference
   between a wobbling photograph and a hole in the page: a hole is a fixed view
   with a restless boundary, so the boundary is the only thing given an
   animation and the image underneath is nailed down.

   Two parts:

     .portal::before   the halo. The same --dz-ramp mask .poster__art uses, for
                       the same reason: falloff on this site is dot density and
                       never alpha, so the glow is nine concentric Bayer rings
                       running out of dots at the rim.
     .portal__frame    the aperture, clipped to #kh-portal-path.

   WHY A PATH AND NOT border-radius. border-radius has eight control values and
   they are all corner radii, so whatever you feed it the result is a rounded
   box. Pushed hard it reads as a circle with a dent, and rendered at this size
   it just read as a circle. The clip path carries nine independent lobes with
   real curvature between them, which is the difference between wobbly and
   round. The path is generated, not hand-typed: see the note on the keyframes. */
.portal__def{position:absolute;width:0;height:0}
.portal{
  position:relative;
  isolation:isolate;
  width:min(100%,22rem);
  margin-inline:auto;
  aspect-ratio:1;
}
/* Reaches past the aperture the way .poster__art's halo does, and painted in
   edge rather than amethyst on the same grounds: at amethyst the halo is
   brighter than what it sits behind, and ornament does not out-shout its
   subject. */
.portal::before{
  content:"";
  position:absolute;
  pointer-events:none;
  inset:-30%;
  z-index:-1;
  background-color:var(--edge);
  -webkit-mask:var(--dz-ramp) no-repeat center/contain;
          mask:var(--dz-ramp) no-repeat center/contain;
  animation:kh-portal-breathe 11s ease-in-out infinite;
}
.portal__frame{
  position:absolute;
  inset:0;
  background-color:var(--edge-faint);
  -webkit-clip-path:url(#kh-portal);
          clip-path:url(#kh-portal);
}
.portal__img{
  position:absolute;
  inset:0;
  width:100%;height:100%;
  object-fit:cover;
}
/* The four frames are generated, nine points on a jittered circle run through
   a closed Catmull-Rom and fitted back inside the 0-1 box. They share a command
   count and command order on purpose: CSS can only interpolate d between paths
   that match segment for segment, and a mismatch does not warn, it just snaps.

   Regenerating them by hand is not worth it. If these ever need redoing, the
   shape is nine anchors at 40 degree spacing, radius 0.44 with 42% jitter, and
   the fit step rescales the result to sit inside 0.02 to 0.98 so no lobe is
   clipped by the element box. */
#kh-portal-path{
  animation:kh-portal-wobble 16s ease-in-out infinite;
}
@keyframes kh-portal-wobble{
  0%,100%{d:path("M0.923 0.476C0.935 0.590,0.980 0.742,0.923 0.814C0.865 0.887,0.695 0.898,0.578 0.912C0.461 0.925,0.311 0.931,0.221 0.896C0.130 0.860,0.045 0.786,0.032 0.697C0.020 0.607,0.090 0.452,0.146 0.359C0.202 0.265,0.300 0.177,0.371 0.137C0.442 0.096,0.490 0.117,0.570 0.115C0.649 0.114,0.792 0.069,0.851 0.129C0.910 0.189,0.911 0.362,0.923 0.476Z")}
  25%    {d:path("M0.955 0.485C0.980 0.570,0.919 0.739,0.853 0.808C0.788 0.876,0.657 0.890,0.561 0.896C0.466 0.902,0.358 0.879,0.280 0.844C0.202 0.809,0.131 0.769,0.093 0.684C0.055 0.599,0.020 0.408,0.052 0.333C0.085 0.259,0.197 0.275,0.288 0.238C0.378 0.200,0.529 0.098,0.597 0.108C0.666 0.118,0.641 0.236,0.701 0.299C0.760 0.362,0.929 0.400,0.955 0.485Z")}
  50%    {d:path("M0.963 0.481C0.980 0.570,0.962 0.736,0.897 0.798C0.833 0.860,0.666 0.833,0.577 0.853C0.489 0.873,0.453 0.954,0.365 0.920C0.278 0.886,0.086 0.747,0.053 0.650C0.020 0.553,0.132 0.428,0.168 0.336C0.203 0.245,0.182 0.144,0.266 0.100C0.350 0.056,0.583 0.046,0.671 0.073C0.759 0.099,0.745 0.193,0.794 0.261C0.842 0.329,0.946 0.391,0.963 0.481Z")}
  75%    {d:path("M0.976 0.483C0.972 0.573,0.874 0.704,0.818 0.769C0.762 0.834,0.722 0.843,0.643 0.870C0.563 0.897,0.431 0.957,0.341 0.930C0.251 0.903,0.150 0.806,0.103 0.708C0.056 0.610,0.020 0.430,0.060 0.342C0.100 0.255,0.254 0.231,0.345 0.183C0.435 0.134,0.521 0.043,0.604 0.050C0.687 0.057,0.780 0.153,0.842 0.225C0.904 0.297,0.980 0.392,0.976 0.483Z")}
}
/* A scale, not an opacity pulse. The halo's density is already carried by the
   ramp mask, so breathing it means moving the rings, not fading them, and rule
   1 stays intact. Its period does not divide into the wobble's, so the two
   never settle into a visible beat. */
@keyframes kh-portal-breathe{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(1.06)}
}

/* The shape is the point and the churn is decoration on top of it, so reduced
   motion keeps a still squiggle with its halo and loses the movement. The path
   element carries frame zero as a d ATTRIBUTE as well, so the resting shape is
   there whether or not the animation ever runs. That attribute is also the
   fallback for anything that cannot animate the CSS d property. */
@media (prefers-reduced-motion:reduce){
  .portal::before,
  #kh-portal-path{animation:none}
}

/* Matches the breakpoint the poster stacks at, and pulls the halo in for the
   same reason .poster__art's comes in there: at full reach on a stacked
   column the halo is wider than a phone and its top lands in the title. */
@media (max-width:820px){
  .portal{width:min(100%,15rem)}
  .portal::before{inset:-18%}
}

/* =============================================================== CTA ===== */

.cta{display:flex;gap:.7rem;flex-wrap:wrap;align-items:center}

/* The button fill is a screen, not a tint. color-mix at 14% produced a flat
   translucent wash, which is the "loses alpha" failure rule 1 exists to
   prevent. A quarter-density amethyst screen reads as a heavier, more
   physical surface at the same apparent lightness, and it gets denser on
   hover instead of brighter. */
.btn{
  position:relative;
  isolation:isolate;
  display:inline-block;
  font-family:var(--mono);
  font-size:var(--t--1);
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--cream);
  text-decoration:none;
  background:none;
  border:1px solid var(--amethyst);
  padding:.8rem 1.25rem;
  cursor:pointer;
  white-space:nowrap;
  transition:border-color .16s ease,transform .1s ease;
}
.btn::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--amethyst);
  -webkit-mask:var(--dzf-4) repeat;
          mask:var(--dzf-4) repeat;
}
.btn:hover{color:var(--cream);border-color:var(--fuchsia)}
.btn:hover::before{-webkit-mask-image:var(--dzf-10);mask-image:var(--dzf-10)}
.btn:active{transform:translateY(1px)}
.btn:active::before{-webkit-mask-image:var(--dzf-14);mask-image:var(--dzf-14)}

/* The secondary CTA. It used to set its label in fuchsia on an edge border,
   and both halves of that were wrong for a control.

   The label measured 5.49 on carbon against the primary button's 15.67, so
   two buttons sitting side by side on /shop and /socials had the secondary at
   a third of the primary's contrast and it read as disabled rather than as
   secondary. That is the same failure the link rule and the footer rule above
   were both rewritten to fix: an interactive thing may not be quieter than
   the surface around it, and the tier below a primary is still a tier of
   control, not a tier of decoration.

   The border was edge, which measures 1.43 and is a decorative token. WCAG
   wants 3:1 on a UI boundary, and more to the point a boundary you cannot see
   is not doing the one job a ghost button's border exists to do.

   So the difference between the two tiers is carried where this site carries
   every other difference: in screen density. Both buttons take a cream label
   and an amethyst border at 3.69; the primary fills at 4/16 and the secondary
   at 1/16. Same control family, one of them weighs more. Nothing loses alpha
   and nothing loses legibility to say so. */
.btn--ghost{border-color:var(--amethyst)}
.btn--ghost::before{-webkit-mask-image:var(--dzf-1);mask-image:var(--dzf-1)}
.btn--ghost:hover{border-color:var(--fuchsia)}
.btn--ghost:hover::before{-webkit-mask-image:var(--dzf-4);mask-image:var(--dzf-4)}

@supports not ((mask-image:url(#x)) or (-webkit-mask-image:url(#x))){
  .btn::before{display:none}
}

/* ================================================================ RAIL ===
   Layout family 2. Label and value rows. Live status, specs, facts.
   Digits are tabular so columns hold. */

.rail{position:relative;padding-left:1.5rem}
/* The rail's left rule is the one piece of structure a reader tracks down the
   whole block, so it is screened at full column height rather than drawn as
   a 1px line. */
.rail::before{
  content:"";
  position:absolute;
  left:0;top:0;bottom:0;
  width:5px;
  background-color:var(--amethyst);
  -webkit-mask:var(--dzf-6) repeat;
          mask:var(--dzf-6) repeat;
}
.rail > .label{margin-bottom:.9rem;color:var(--dust)}

.rail__row{
  display:flex;
  justify-content:space-between;
  align-items:baseline;
  gap:1rem;
  padding:.55rem 0;
  border-bottom:1px solid var(--edge-faint);
  font-family:var(--mono);
  font-size:var(--t--1);
}
.rail__row:last-child{border-bottom:0}
.rail__row dt,.rail__row .k{
  letter-spacing:.13em;text-transform:uppercase;color:var(--dust);margin:0;
}
.rail__row dd,.rail__row .v{
  margin:0;color:var(--cream);font-variant-numeric:tabular-nums;text-align:right;
}
.rail__row .v--hot{color:var(--fuchsia)}

@media (max-width:820px){
  .rail{padding-left:0;padding-top:1.25rem}
  .rail::before{left:0;right:0;top:0;bottom:auto;width:auto;height:5px}
}

/* =============================================================== INDEX ===
   Layout family 3. Long link sets: socials, shop, downloads, turtel.
   Grouped into clusters with one hairline per row and a heading per cluster,
   rather than one flat list with a rule under every item. */

.index{display:grid;gap:clamp(1.75rem,4vw,3rem);grid-template-columns:repeat(auto-fit,minmax(17rem,1fr))}

/* Quantity query, for the one count auto-fit handles badly.
   A 17rem minimum lands three tracks at desktop widths. Three groups fill
   that, and so do five or six. Four does not: it fills a row, then leaves the
   fourth alone on a second row with two thirds of the width empty beside it.
   That gap is most of why the socials and shop pages read as unfinished.
   Four is 2x2. Only above 60rem, where two tracks are certain to fit, so the
   single-column phone layout is untouched. */
@media (min-width:60rem){
  .index:has(> .index__group:nth-child(4):last-child){grid-template-columns:repeat(2,1fr)}
  .plate--wide:has(> figure:nth-child(4):last-child){grid-template-columns:repeat(2,1fr)}
}

.index__group > .label{
  padding-bottom:.55rem;
  margin-bottom:.3rem;
  border-bottom:1px solid var(--edge);
  color:var(--dust);
}

.index__list{list-style:none;margin:0;padding:0}

.index__row{
  display:grid;
  grid-template-columns:1fr auto;
  gap:.4rem 1rem;
  align-items:baseline;
  padding:.55rem 0;
  border-bottom:1px solid var(--edge-faint);
}
.index__row:last-child{border-bottom:0}

/* The sprite itself never renders. It is a bag of <symbol>, and a symbol is
   not drawn until something references it, but the <svg> carrying them is
   still a replaced element in the flow and would otherwise reserve a line. */
.icon-sprite{display:none}

/* The mark rides INSIDE the link, so a row stays one click target and the icon
   cannot be hit separately from the word it labels. inline-flex rather than a
   floated or absolutely placed icon: the row is a baseline-aligned grid, and an
   inline-flex box reports its first item's baseline, so the text still sits on
   the same line as the note in the next column. */
.index__icon{
  width:1em;height:1em;
  flex:none;
  fill:currentColor;
  color:var(--amethyst);
  /* Optical, not geometric. Centring an icon on the text box sits it a hair
     high against lowercase, because the box includes descender room the word
     beside it is not using. */
  translate:0 .06em;
}
/* The icon is the one part of the row allowed to be amethyst: it is structure,
   it is aria-hidden, and the link text carries the whole meaning. On hover it
   joins the text rather than staying behind, so the row brightens as one thing
   instead of two. */
.index__row a:hover .index__icon{color:inherit}

.index__row a{
  display:inline-flex;
  align-items:center;
  gap:.55rem;
  font-family:var(--prose);
  font-size:var(--t-0);
  color:var(--cream);
  text-decoration:none;
  text-decoration-color:transparent;
}
/* Cream holds through the hover and the fuchsia arrives as the underline, so
   the row gets brighter on interaction rather than dimmer. Swapping the fill
   to fuchsia took this from 15.67 to 5.49 on the way in. */
.index__row a:hover{
  color:var(--cream);
  text-decoration:underline;
  text-decoration-color:var(--fuchsia);
  text-decoration-thickness:2px;
  text-underline-offset:.22em;
}

.index__row .note{
  font-family:var(--mono);
  font-size:var(--t--1);
  color:var(--dust);
  text-align:right;
  font-variant-numeric:tabular-nums;
}
.index__row .note--muted{color:var(--dust)}

/* =============================================================== PLATE ===
   Layout family 4. Image-led. Artwork, merch shots, QR codes.
   No card chrome: the image is the object, the caption sits under it. */

.plate{display:grid;gap:clamp(1.25rem,3vw,2.25rem);grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))}
.plate--wide{grid-template-columns:repeat(auto-fit,minmax(20rem,1fr))}

/* The small plate, for the pieces that ARE small: sixteen pixel icons, emotes,
   doodles. At the default 15rem a pack icon is stretched to twenty times its
   size and eighteen of them run to five rows, which gives a 431 byte file the
   same weight on the page as a commission.

   auto-fill, not auto-fit: a short last row keeps its tile size instead of
   stretching three tiles across the whole line. */
.plate--small{gap:clamp(.9rem,2vw,1.4rem);grid-template-columns:repeat(auto-fill,minmax(9.5rem,1fr))}
.plate--small .plate__img{padding:.6rem}

.plate figure{margin:0;display:flex;flex-direction:column;gap:.75rem}
.plate__img{
  position:relative;
  isolation:isolate;
  background:var(--pitch);
  border:1px solid var(--edge-faint);
  aspect-ratio:1;
  display:grid;
  /* One track, the size of the tile and not of what is in it. Left to auto,
     the row grew to fit a portrait image and overflow:hidden cut the bottom
     off it: object-fit never ran, because the box it would have fitted the
     picture into was already as tall as the picture. */
  grid-template:minmax(0,1fr)/minmax(0,1fr);
  place-items:center;
  overflow:hidden;
  padding:1rem;
  transition:border-color .14s ease;
}
/* A cut-out product shot on a flat plate reads as a sticker on a wall. The
   screen gives it a ground to sit in. */
.plate__img::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--edge-soft);
  -webkit-mask:var(--dzf-4) repeat;
          mask:var(--dzf-4) repeat;
}
/* <picture> is taken out of the layout so the img is the grid item whether or
   not kh_img() wrapped it, and 100% means the tile both times.

   Its <source> children have to go with it. They are empty inline boxes, not
   display:none, so with the wrapper gone each one becomes a grid item, takes
   the tile's one cell, and pushes the img into a second row at the bottom.
   Hiding them is layout only: which source the browser picks is unchanged. */
.plate__img picture{display:contents}
.plate__img source{display:none}
.plate__img img{width:100%;height:100%;min-width:0;min-height:0;object-fit:contain}
.plate figure:hover .plate__img{border-color:var(--fuchsia)}

/* Pixel art, flagged in data/image-dims.php and marked by kh_img(). The file
   is sent at its real size, sixteen pixels in some cases, and the browser does
   the scaling here with hard edges. Smoothed, a skin face is purple fog. */
img[data-px]{image-rendering:pixelated}

/* Paper, flagged the same way. Black line work on transparency disappears
   into a ground this dark, so the page gives it something to be drawn on.
   The file is not touched: the cream is here, behind the <img>, and "Show
   original" opens the drawing with nothing behind it.

   On the img and not the tile, so the tile's border and screen still frame
   it and it reads as a sheet of paper laid on the plate. */
img[data-paper]{background:var(--cream)}

/* A whole tile as one link, for the shop. The figure is the hit area, not just
   the product name: a grid of pictures where only the caption is clickable is
   a grid that fights the reader.

   No colour of its own and no underline. The tile already signals itself on
   hover through the plate border, and a fuchsia underline under every caption
   in a grid of twenty would be the loudest thing on the page. */
.plate__link{display:block;color:inherit;text-decoration:none}
.plate__link:hover .plate__img,
.plate__link:focus-visible .plate__img{border-color:var(--fuchsia)}
.plate__link:hover figcaption b{color:var(--fuchsia)}
.plate__link:focus-visible{outline:2px solid var(--fuchsia);outline-offset:3px}
.plate figcaption,
.stack__label{
  font-family:var(--mono);
  font-size:var(--t--1);
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--dust);
}
.plate figcaption b,
.stack__label b{display:block;color:var(--cream);font-weight:500;letter-spacing:.06em}

/* "Show original", under a picture that is being served smaller than it is
   (kh_img_original_link in includes/config.php). It sits in the caption and
   takes the caption's type, so it reads as part of the label rather than as a
   button competing with the picture.

   The pixel size is the same colour as the words on purpose. Dimming it to
   set it apart was the first version, and at this size that took it to 4:1
   against the ground, under the 4.5 the a11y check holds body text to. */
.orig{display:block;width:fit-content;margin-top:.4rem;color:var(--dust);text-underline-offset:.2em}
.orig:hover,.orig:focus-visible{color:var(--fuchsia)}

/* ------------------------------------------------------------ the stack --
   A run of near-identical pictures as one pile (kh_gallery_runs in
   includes/gallery.php). Shut, it takes one tile: the top picture, with two
   more sheets squared up badly behind it. Open, it takes the whole row and
   lays every picture out as an ordinary figure.

   It is a <details>, so there is no script behind it and nothing to fail.
   The <summary> is the pile while it is shut and the label on the row once
   it is open.

   The sheets belong to the wrapper and not to .plate__img, which has already
   spent its ::before on the screen. They turn and slide, they do not fade or
   grow: rule 1, and a pile that fans out when you reach for it has told you
   what a click does.

   The wrapper is padded on two sides and the sheets sit in that padding, so
   the pile is exactly the size of a tile and the caption under it lines up
   with its neighbours. The first version only turned the sheets behind a
   full-size tile, and at rest all that showed was four corners a few pixels
   wide: on the small plate it could not be told from a plain picture. */
.stack{min-width:0}
.stack__top{display:flex;flex-direction:column;gap:.75rem;cursor:pointer;list-style:none}
.stack__top::-webkit-details-marker{display:none}

.stack__pile{position:relative;isolation:isolate;display:block;padding:.7rem .7rem 0 0}
.stack__pile::before,
.stack__pile::after{
  content:"";
  position:absolute;
  z-index:-1;
  background:var(--pitch);
  border:1px solid var(--edge);
  transition:rotate .18s ease,translate .18s ease;
}
.stack__pile::before{inset:0 0 .7rem .7rem;rotate:2deg}
.stack__pile::after{inset:.35rem;rotate:-1.5deg}
.stack__top:hover .stack__pile::before,
.stack__top:focus-visible .stack__pile::before{rotate:5deg;translate:.3rem -.3rem}
.stack__top:hover .stack__pile::after,
.stack__top:focus-visible .stack__pile::after{rotate:-4deg;translate:-.15rem -.1rem}
.stack__top:hover .plate__img,
.stack__top:focus-visible .plate__img{border-color:var(--fuchsia)}

/* The third line of the label, where a picture has "Show original". Same
   place and same weight, so a pile has the anatomy of the tiles round it.
   It is not a link: the whole summary is the control and this is the part of
   its name that changes with the state. Only one of the two is ever rendered,
   so only one is ever read out. */
.stack__label{display:block}
.stack__go{
  display:block;
  width:fit-content;
  margin-top:.4rem;
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-decoration-color:var(--fuchsia);
  text-underline-offset:.2em;
}
.stack__top:hover .stack__go,
.stack__top:focus-visible .stack__go{color:var(--fuchsia)}
.stack[open] .stack__go--shut,
.stack:not([open]) .stack__go--open{display:none}

/* Open. A rule above and below, so the row reads as one thing that was
   opened and not as the grid carrying on. The label goes onto one line with
   the way back at the far end of it. */
.plate > .stack[open]{grid-column:1/-1}
.stack[open]{border-block:1px solid var(--edge);padding-block:clamp(1rem,2.5vw,1.5rem)}
.stack[open] .stack__pile{display:none}
.stack[open] .stack__label{display:flex;flex-wrap:wrap;align-items:baseline;gap:.3rem 1.25rem}
.stack[open] .stack__label b{display:inline}
.stack[open] .stack__go{margin:0 0 0 auto}

/* auto-fill, not the auto-fit of the grid this sits in. The row is as wide
   as that grid, so the same minimum gives the same tracks, and four pictures
   stay the size of a tile instead of stretching across a row that holds five. */
.stack__spread{
  display:grid;
  gap:clamp(1.25rem,3vw,2.25rem);
  grid-template-columns:repeat(auto-fill,minmax(15rem,1fr));
  margin-top:1.1rem;
}
.plate--wide .stack__spread{grid-template-columns:repeat(auto-fill,minmax(20rem,1fr))}
.plate--small .stack__spread{gap:clamp(.9rem,2vw,1.4rem);grid-template-columns:repeat(auto-fill,minmax(9.5rem,1fr))}

/* Dealt out, one after the other, each sliding in from the side the pile was
   on and squaring up. Transform only, so nothing here fades. */
@keyframes kh-stack-deal{from{translate:-1.5rem 0;rotate:-3deg}}
.stack[open] .stack__spread > figure{animation:kh-stack-deal .3s cubic-bezier(.2,.8,.2,1) backwards}
.stack[open] .stack__spread > figure:nth-child(2){animation-delay:.04s}
.stack[open] .stack__spread > figure:nth-child(3){animation-delay:.08s}
.stack[open] .stack__spread > figure:nth-child(4){animation-delay:.12s}
.stack[open] .stack__spread > figure:nth-child(n+5){animation-delay:.16s}
@media (prefers-reduced-motion:reduce){
  .stack__pile::before,.stack__pile::after{transition:none}
  .stack[open] .stack__spread > figure{animation:none}
}

/* =============================================================== PANEL ===
   Layout family 5. A single bordered block with slime creeping in from the
   corner. Forms, single CTAs, notices. Used once per page at most. */

.panel{
  position:relative;
  isolation:isolate;
  background:var(--crypt);
  border:1px solid var(--edge);
  padding:clamp(1.5rem,4vw,2.5rem);
  overflow:hidden;
}
/* Two layers: the corner slime, and the ground screen under it. The corner
   used to run at opacity .2, which is rule 1 broken in the one place the
   slime actually appears inside a block. It is screened instead now. */
/* This was painted in raw amethyst at up to 144px square, which made a corner
   ornament the single brightest object on the page: brighter than the heading
   it sits behind, brighter than any link, brighter than the text it overlaps.
   Ornament outranking content is the whole reason the site read as low
   contrast while measuring clean, so the corner comes down into the rule
   colours like every other piece of structure, and gets smaller so it stops
   colliding with the first line of the heading. */
.panel::before{
  content:"";
  position:absolute;
  top:0;left:0;
  width:min(6.5rem,24%);
  aspect-ratio:1;
  z-index:-1;
  background-color:var(--edge);
  -webkit-mask:var(--m-corner) no-repeat top left/contain;
          mask:var(--m-corner) no-repeat top left/contain;
  pointer-events:none;
}
/* Same correction as the band screens. edge-soft at 4/16 over crypt blended
   to #221628, chroma 18: another violet ground wearing a neutral token name.
   pitch takes it to #151515, chroma 0, and every ratio inside the panel goes
   up. The purple in a panel is the border and the corner, which are edges and
   are supposed to be purple. The ground under the text is not. */
.panel::after{
  content:"";
  position:absolute;
  inset:0;
  z-index:-2;
  background-color:var(--pitch);
  -webkit-mask:var(--dzf-4) repeat;
          mask:var(--dzf-4) repeat;
  pointer-events:none;
}
.panel > *{position:relative}

.panel--notice{background:var(--stone);border-color:var(--amethyst)}

/* ============================================================== COLUMN ===
   Layout family 6. Long-form prose. About, legal, notes.

   THE MEASURE BELONGS TO THE CONTENTS, NOT TO THE SHELL.

   This used to be .column{max-width:var(--measure);margin-inline:0} set on the
   same element as .shell, and the note here defended the reset on the grounds
   that centring a narrow column would "put it out of alignment with every other
   section on the page". The reasoning was right and the fix was backwards:
   margin-inline:0 cancels .shell's own margin-inline:auto, so the block was not
   centred OR aligned, it went flush against the viewport's left edge with only
   the gutter holding it off the glass. On a 1440 that put the heading at 56px
   while every plain .shell heading on the same page sat at 150px.

   Every page using .column mixes it with plain .shell sections (about,
   giveaways, pallette, pavlok, projects, schedule and the front page), so it
   was visible on all seven, not only the one it got reported on.

   Constraining the children instead leaves the shell centred and exactly as
   wide as every other band, and still stops prose running to 1240px. What lives
   in these blocks is paragraphs, headings and lists, so nothing here wants the
   full width anyway. */
.column > *{max-width:var(--measure)}
.column p{margin-bottom:1.1em}
.column h3{margin:2rem 0 .7rem}
.column h3:first-child{margin-top:0}

/* Art on the left, prose on the right, inside a normal centred .shell.

   The prose half keeps .column, so the measure still governs the text; this
   rule only decides how the two halves split the row. The art side is the
   narrower of the two on purpose: the picture is a placeholder and the words
   are the content, and a 50/50 split would say the opposite.

   The art is .poster__art, not .portal. Same picture, no clip path and nothing
   animating: the wobbling aperture upstairs is the page's one restless edge and
   repeating it here would spend the trick twice. */
.about{
  display:grid;
  grid-template-columns:minmax(0,.72fr) minmax(0,1fr);
  gap:clamp(2rem,5vw,4rem);
  align-items:center;
}
.about__art{max-width:22rem}
/* .poster__art's halo reaches 34% past the art, which is right when it has a
   column to itself and wrong with a paragraph beside it: at this size that is
   240px of dots landing under the text. Pulled in to clear the gap. */
.about__art::before{inset:-20%}

@media (max-width:820px){
  /* Same breakpoint .poster stacks at. Art first, so reading order matches
     what is on screen. */
  .about{grid-template-columns:1fr;gap:2.5rem}
  .about__art{max-width:16rem;margin-inline:auto}
}

/* ============================================================== SLIME ====
   The divider. Solid where it leaves the block above, dithering away through
   the drips, so two sections interlock rather than butt. Tiles seamlessly at
   any width because it repeats rather than stretching. */

/* The tile is 1200x258 with base=104, so the top 40% of it is solid mass and
   only the bottom 60% is drips. Drawn whole at 762x164 and tiled, that cost
   656px on the home page, 18.1% of the document, and at 762px on a 1427px
   viewport it repeated 1.87 times: the worst possible frequency, because you
   see the same silhouette twice, side by side, close enough to compare. That
   is what made it read as a printed border rather than as slime.

   Two changes fix both without touching the artwork.

   CROP INSTEAD OF SHRINK. The mask is anchored to the bottom and the element
   is shorter than the mask, so what gets cut is the solid slab at the top,
   which was duplicating the ground colour above it anyway. Every drip and the
   whole dither fade survive intact.

   ONE TILE, NEVER REPEATED. mask-size is 100% auto, so the tile is scaled to
   the element width and its height follows the intrinsic ratio. There is no
   second copy at any viewport, so there is no seam to notice. Because both
   axes scale by the same factor the Bayer cells stay square, which is the
   constraint the old fixed 762x164 was there to protect. */
/* WHERE THE DIVIDER LIVES, AND WHY IT MOVED
   It used to be a block-level sibling sitting BETWEEN two sections. That is
   the wrong place for a shape whose whole idea is that it hangs down. Being in
   the flow, it reserved its own full height, so the drips fell through a gap
   the divider had itself created and landed on nothing. Two sections with a
   band of empty page between them, and some slime painted in the gap.

   The markup now puts each divider inside the section BELOW it, as that
   section's first child, and this rule lifts it out of the flow and pins it to
   that section's top edge. Three things follow:

     the sections butt directly, so there is no gap to explain
     the drips hang INTO the next section, over its own ground
     they pass BEHIND its content, because the content is z-index 1 and the
       divider is 0

   The mask is bottom-anchored (50% 100%), which is what makes this work
   without touching the artwork: the skin stays welded to the seam at the top
   and every runner points down into the block. Sizing is 100% auto, so one
   tile spans the width and its height follows the intrinsic ratio: no repeat,
   no seam, square Bayer cells at every viewport.

   Host blocks need position:relative and their contents need to sit above.
   Both are set further down, on .band and .footer. */
.slime{
  position:absolute;
  inset-block-start:0;
  inset-inline:0;
  /* THE WIDTH HAS TO BE DEFINITE, AND THIS LINE IS NOT DECORATION.
     aspect-ratio resolves whichever axis is not already known. With width left
     to fall out of the insets, a max-height that actually bites clamps the
     height and the ratio then runs BACKWARDS into the width: the box shrinks
     horizontally to stay 2800/300, and because it is left-aligned in its
     insets the whole shortfall lands on the right.

     .slime--c caps at 140px, which is the only cap low enough to bite. At a
     1440 viewport the natural height is 154px, so it clamped, and the footer
     divider measured 1307x140 against 1440x154 for the two above it. 133px of
     bare footer on the right hand end, on any viewport wider than about 1307.
     Below that the cap never engages and it looked perfect, which is why it
     reads as a desktop-only bug.

     width:100% makes the inline axis definite first, so aspect-ratio only ever
     computes the height and max-height only ever crops it. */
  width:100%;
  z-index:0;
  pointer-events:none;
  /* Matches the tile exactly, so the mask is never stretched on either axis
     and the Bayer cells stay square at every viewport. 98px at 1440, 27px on
     a phone: the divider scales with the page instead of stepping at a
     breakpoint, so the old max-width:700px override is gone. */
  aspect-ratio:2800/300;
  /* The box is taller than the old one and weighs less, because the tile is
     a solid skin with a dithered ramp of runners hanging off it: most of
     that height is transparent. Judge a divider by how much ink it puts on
     the page, not by its bounding box.

     The cap only bites on very wide screens, and it has to crop from the
     BOTTOM. The tile is a 44-unit solid bar across the top of its 300 with
     the ramp fading downward beneath it, so the top is the one part that
     cannot be spared: it is the edge the runners hang from. The mask used
     to be anchored 50% 100%, which crops the opposite end, and past about
     1307px the cap ate the whole bar: the footer drips lost their heads
     and read as blobs floating under a flat cut. (The old note here said
     the skin was 18 of 300 and safe to crop, which was true of the tile
     before it was redrawn.) Anchored to the top, a crop takes the sparse
     tail of the ramp instead, which is what .slime--c wants anyway. */
  max-height:196px;
  background-color:var(--edge-soft);
  -webkit-mask:var(--m-edge) no-repeat 50% 0/100% auto;
          mask:var(--m-edge) no-repeat 50% 0/100% auto;
}
/* The drips used to be painted in the ground tiers (stone, crypt). That worked
   when the grounds were purple-blacks and carried their own tint. On neutral
   carbon it does not: stone against carbon measures 1.17, so the divider went
   invisible and every section looked like it was floating in an unexplained
   black gap.

   They are painted in the rule colours instead, which are amethyst walked down
   into carbon. A drip is now legibly violet against the page, it is still made
   of brand, and it reads as the section above bleeding down rather than as a
   grey shape nobody can quite see. */
/* Variants exist so two dividers on one page do not read as the same object
   twice. The old ones only shifted mask-position and swapped colour, which
   does not work: with the tile repeating, an offset lands you back inside the
   same silhouette, and the eye tracks silhouette before it tracks hue. These
   mirror it and rescale it instead, so the outline genuinely differs. */
.slime--b{
  background-color:var(--edge-faint);
  transform:scaleX(-1);
}
/* The footer drip, and the only place .slime--c is used.

   PAINTED IN EDGE, ONE STEP ABOVE THE DIVIDERS ON THE PAGE. It was lilac
   (#6E5A93), a greyed lavender and the one colour on the site that was not
   one of the four or a step between them. Next to the footer's violet screen
   it read as washed out, a fifth hue wearing a decorative job.

   Edge is 1.49:1 against the pitch wordmark, which on luminance alone would be
   the same trap pitch fell into (1.00:1, the drips and the wordmark became one
   shape). It does not fall in, because the separation is hue: a saturated
   violet passing behind a neutral black glyph reads as two things at any
   contrast. Pitch against pitch had nothing to separate them.

   The old note said edge fought the section above as a bright purple band.
   That was the heavy tile, with a solid shoulder across the top. The current
   tile is a thin skin with runners hanging off it, and in edge it reads as the
   footer's own ground rather than as a stripe.

   Still cropped tighter than the base, so it loses its longest runners and
   reads as the shallower of the two. */
.slime--c{
  background-color:var(--edge);
  max-height:140px;
}
.slime--flip{transform:scaleY(-1)}
/* The one divider that is brand purple rather than a ground tier. Rationed
   to once per page, where a section genuinely changes subject. */
.slime--hot{background-color:var(--amethyst)}

/* No mobile override any more: the clamp on height and the 100%-wide mask
   already scale together, so the divider shrinks with the viewport instead of
   stepping at a breakpoint. */

/* No mask support: a hairline beats a full-bleed block. */
@supports not ((mask:url(#x)) or (-webkit-mask:url(#x))){
  .slime{height:1px;background-color:var(--edge)}
}

/* the bare fade, for blocks that should end without a rule */
.fade-out{
  height:56px;
  margin-top:-56px;
  background-color:var(--carbon);
  -webkit-mask:var(--m-ramp) repeat-x 0 0/48px 56px;
          mask:var(--m-ramp) repeat-x 0 0/48px 56px;
  transform:scaleY(-1);
  pointer-events:none;
  position:relative;
}

/* list marker and section mark */
.drips{list-style:none;margin:0;padding:0}
.drips li{
  display:flex;align-items:flex-start;gap:.7rem;padding:.3rem 0;
}
.drips li::before{
  content:"";
  flex:none;
  width:.8rem;height:1.25rem;
  margin-top:.28em;
  background-color:var(--amethyst);
  -webkit-mask:var(--m-bullet) no-repeat center/contain;
          mask:var(--m-bullet) no-repeat center/contain;
}

/* The blob mark used to run at opacity .5. It is screened instead, so it is
   made of the same dots as everything else. */
.blobmark{
  position:relative;
  isolation:isolate;
  width:2.6rem;height:3.25rem;
  background-color:var(--amethyst);
  -webkit-mask:var(--m-blob) no-repeat center/contain;
          mask:var(--m-blob) no-repeat center/contain;
}

/* ========================================================== CREATOR WALL ==
   Layout family 7. Thousands of small entries at once, so this is the one
   place on the site that gets genuinely dense. No borders per item: at this
   count, chrome on every cell turns the page into static. */

.tc{
  list-style:none;margin:0;padding:0;
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));
  gap:.4rem;
}
.tc__item{display:block}
.tc__link{
  display:flex;flex-direction:column;align-items:center;gap:.5rem;
  height:100%;
  padding:.85rem .4rem;
  color:var(--ash);text-decoration:none;
  background:transparent;
  transition:background .14s ease,color .14s ease;
}
a.tc__link:hover{background:var(--crypt);color:var(--cream)}
a.tc__link:hover .tc__av{border-color:var(--fuchsia)}

.tc__av{
  width:72px;height:72px;object-fit:cover;
  border:1px solid var(--edge);
  background:var(--pitch);
  transition:border-color .14s ease;
}
/* The missing-avatar placeholder was a 45deg repeating-linear-gradient, which
   was the last stripe on the site drawn with a gradient. It is a screen now,
   so an absent avatar looks like a hole punched in the page rather than like
   hatching. */
.tc__av--none{
  display:block;
  background-color:var(--edge-soft);
  -webkit-mask:var(--dzf-8) repeat;
          mask:var(--dzf-8) repeat;
  border-color:var(--edge-faint);
}
.tc__name{
  font-family:var(--mono);font-size:var(--t--1);
  text-align:center;line-height:1.3;
  overflow-wrap:anywhere;
}

/* ========================================================== STANDOUTS ===
   The glow from the original TwitchCon plugin, brought back. One per creator,
   decided in twitchcon.php, first match wins.

   THE HUES ARE THE ORIGINAL SIX, ON PURPOSE
   Everywhere else this site holds to two chroma. This page is the exception
   and it is a justified one under the role rule (skill section 4.2): six
   categories have to be told apart at a glance across two thousand avatars,
   and each colour has exactly one written job, printed in the legend above the
   grid. That is a data key, not decoration.

   Two of the six were already this site's own colours in all but name: the
   original "keehan" violet #9400D3 and "friend" magenta #FF00FF sit within a
   few degrees of amethyst and fuchsia, so they use the tokens. The rest keep
   their original hue. One changed lightness: the original speaker-only red was
   #8B0000, drawn on a white card. On carbon that measures 1.89:1, so it
   is the same red lifted to 4.82:1.

   WHY ONLY STANDOUTS GET A BORDER
   The note on .tc says no chrome per item, because at this count borders turn
   the wall into static. That still holds for everyone else. A border is what
   MAKES a standout stand out, so it is reserved for exactly them: a few dozen
   lit cells in a field of plain ones. */
.tc__item--keehan   {--glow:var(--amethyst)}
.tc__item--friend   {--glow:var(--fuchsia)}
.tc__item--featured {--glow:#00CED1}
.tc__item--official {--glow:#FFA500}
.tc__item--also     {--glow:#ADFF2F}
.tc__item--speaker  {--glow:#E5484D}

.tc__item[class*="tc__item--"] .tc__link{
  border:1px solid var(--glow);
  box-shadow:inset 0 0 8px 1px color-mix(in srgb,var(--glow) 45%,transparent);
  color:var(--vellum);
}
.tc__item[class*="tc__item--"] .tc__av{border-color:var(--glow)}
.tc__item[class*="tc__item--"] a.tc__link:hover{
  box-shadow:inset 0 0 14px 3px color-mix(in srgb,var(--glow) 70%,transparent);
  color:var(--cream);
}
.tc__item[class*="tc__item--"] a.tc__link:hover .tc__av{border-color:var(--glow)}

.tc__meta{font-family:var(--mono);font-size:.7rem;letter-spacing:.06em;color:var(--dust)}

/* The key. A swatch per glow, drawn as the same inset border the cards use, so
   the legend is a small copy of the thing it explains rather than a coloured
   dot that has to be matched to it by eye. */
.tc-key{
  list-style:none;margin:0 0 1.5rem;padding:0;
  display:flex;flex-wrap:wrap;gap:.6rem 1.4rem;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.06em;color:var(--ash);
}
.tc-key__item{display:inline-flex;align-items:center;gap:.55rem}
.tc-key__item::before{
  content:"";
  width:.95rem;height:.95rem;
  border:1px solid var(--glow);
  box-shadow:inset 0 0 5px 1px color-mix(in srgb,var(--glow) 55%,transparent);
}
.tc-key__item--keehan   {--glow:var(--amethyst)}
.tc-key__item--friend   {--glow:var(--fuchsia)}
.tc-key__item--featured {--glow:#00CED1}
.tc-key__item--official {--glow:#FFA500}
.tc-key__item--also     {--glow:#ADFF2F}
.tc-key__item--speaker  {--glow:#E5484D}

/* ========================================================== WALL SEARCH ===
   After the search bar on Open Sauce's featured-creators page: one field the
   full width of the wall, a magnifier at the left, a small uppercase
   placeholder, results filtering as you type. Theirs is a rounded pill on
   cream. This is the site's own inset, --pitch sunk below the stone band
   with an --edge hairline and square corners like every other field here,
   so it reads as a slot cut into the page rather than a widget laid on it.
   The count sits inside the right end, where Open Sauce has nothing: at two
   thousand cards, "12 of 2,673" is how you know the filter did anything. */
.tc-find{position:relative;margin:0 0 1.5rem;color:var(--dust)}
.tc-find__icon{
  position:absolute;left:1rem;top:50%;transform:translateY(-50%);
  pointer-events:none;
  transition:color .14s ease;
}
.tc-find__input{
  display:block;width:100%;
  font-family:var(--mono);font-size:var(--t-0);
  color:var(--cream);
  background:var(--pitch);
  border:1px solid var(--edge);border-radius:0;
  padding:1rem 7.5rem 1rem 3rem;
  -webkit-appearance:none;appearance:none;
}
.tc-find__input::placeholder{
  color:var(--dust);
  font-size:var(--t--1);letter-spacing:.14em;text-transform:uppercase;
}
.tc-find__input:focus{outline:2px solid var(--fuchsia);outline-offset:1px;border-color:var(--fuchsia)}
.tc-find:focus-within .tc-find__icon{color:var(--fuchsia)}
.tc-find__count{
  position:absolute;right:1rem;top:50%;transform:translateY(-50%);
  pointer-events:none;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.06em;
  font-variant-numeric:tabular-nums;
}
.tc-none{
  margin:0 0 1.5rem;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.06em;color:var(--ash);
}

/* ---- add yourself to the community list ----
   One line: a field and a button, the same inset and hairline as the search
   above it, so the two read as a pair rather than as two different widgets.
   It wraps to two rows on a phone, where a 25-character name and a button
   do not fit side by side. */
.tc-add{margin:0 0 1.25rem}
.tc-add__row{display:flex;flex-wrap:wrap;gap:.6rem}
.tc-add__input{
  flex:1 1 14rem;min-width:0;
  font-family:var(--mono);font-size:var(--t-0);
  color:var(--cream);
  background:var(--pitch);
  border:1px solid var(--edge);border-radius:0;
  padding:.8rem 1rem;
  -webkit-appearance:none;appearance:none;
}
.tc-add__input::placeholder{color:var(--dust);font-size:var(--t--1);letter-spacing:.1em;text-transform:uppercase}
.tc-add__input:focus{outline:2px solid var(--fuchsia);outline-offset:1px;border-color:var(--fuchsia)}
.tc-add__msg{
  margin:.6rem 0 0;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.06em;
}
.tc-add__msg--bad{color:var(--fuchsia)}
.tc-add__msg--ok{color:var(--vellum)}
.tc-add__note{
  margin:.6rem 0 0;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.06em;color:var(--dust);
}
/* The unofficial / removal notice under each poster. Same voice as the note
   under the add form, with room to clear the buttons above it. */
.tc-note{
  margin:1.25rem 0 0;max-width:52ch;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.06em;color:var(--dust);
}

/* =============================================================== FORM ==== */

.field{display:grid;gap:.45rem;margin-bottom:1.15rem}
.field > label{
  font-family:var(--mono);
  font-size:var(--t--1);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--ash);
}
.field input,.field textarea,.field select{
  width:100%;
  font-family:var(--prose);
  font-size:var(--t-0);
  color:var(--cream);
  background:var(--pitch);
  border:1px solid var(--edge);
  padding:.7rem .85rem;
}
.field textarea{resize:vertical;min-height:9rem}
/* Placeholder is --dust, which measures 5.98 on the worst ground. A
   placeholder is not a label, but it still has to be readable. */
.field input::placeholder,.field textarea::placeholder{color:var(--dust)}
.field input:focus,.field textarea:focus,.field select:focus{
  outline:2px solid var(--fuchsia);outline-offset:1px;border-color:var(--fuchsia);
}
.field .help{font-family:var(--mono);font-size:var(--t--1);color:var(--dust)}
.field .err{font-family:var(--mono);font-size:var(--t--1);color:var(--fuchsia)}

.honeypot{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

.notice{
  position:relative;
  isolation:isolate;
  font-family:var(--mono);
  font-size:var(--t-0);
  padding:1rem 1.15rem;
  border:1px solid var(--amethyst);
  background:var(--stone);
  color:var(--cream);
  margin-bottom:1.5rem;
}
.notice::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--amethyst);
  -webkit-mask:var(--dzf-2) repeat;
          mask:var(--dzf-2) repeat;
}
.notice--bad{border-color:var(--fuchsia)}
.notice--bad::before{background-color:var(--fuchsia)}

/* ============================================================== EMBED ==== */

.embed{
  border:1px solid var(--edge);
  background:var(--crypt);
  position:relative;
  aspect-ratio:16/9;
}
.embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block}
.embed--form{aspect-ratio:auto;height:min(4200px,180vh)}
.embed--form iframe{position:static;height:100%}
@media (max-width:640px){.embed--form{height:min(4600px,220vh)}}

/* ============================================================= FOOTER ==== */

.footer{
  position:relative;
  isolation:isolate;
  background:var(--stone);
  /* The bottom number used to be 2rem, which left 32px of bare stone under the
     copyright line with nothing in it. It is the last thing in the document, so
     that space read as the page failing to end rather than as breathing room.
     .75rem keeps the type off the glass without the gap. */
  padding-block:clamp(2.5rem,5vw,3.5rem) .75rem;
  margin-top:0;
}
/* Same arrangement as .band: the footer hosts the last divider on every page,
   so its own contents have to sit above the drips. position:relative and
   isolation were already here for the ground screen, so only the stacking of
   the children is new. */
.footer > *:not(.slime){position:relative;z-index:1}
/* The footer is the heaviest screen on the page. It is the bottom of the
   document, so it is the one place the texture is allowed to close in. */
.footer::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--edge-faint);
  /* dzg rather than dzf, same reason as the band grounds above: the footer is
     the largest single screened surface on the page, so it is the one where a
     single-frequency tile is most obviously a lattice. */
  -webkit-mask:var(--dzg-16) repeat;
          mask:var(--dzg-16) repeat;
}

/* One horizontal row of legal links, sitting opposite the copyright inside
   .footer__end.

   This replaces .footer__grid, which was four link columns and then, briefly,
   one column with three collapsed tracks beside it. Three of the four groups
   duplicated the nav directly above them, and the survivor did not justify a
   grid, a group label and 2.5rem of margin to hold four words in a stack. The
   footer is now the wordmark and a single rule-topped row. */
.footer__legal{
  display:flex;
  flex-wrap:wrap;
  gap:.35rem 1.25rem;
  list-style:none;margin:0;padding:0;
}
/* The footer is a link list, so the links are the content and cannot sit in a
   quiet tier. They used to rest in ash at 7.73 and go to fuchsia at 4.68 on
   hover, which moved contrast down on interaction. Rest in vellum, one step
   below a body link so the footer stays subordinate, and hover to cream with
   the fuchsia underline. Interaction now adds contrast in both directions. */
.footer__legal a{
  font-family:var(--prose);font-size:var(--t-0);color:var(--vellum);
  text-decoration:none;
}
.footer__legal a:hover{
  color:var(--cream);
  text-decoration:underline;
  text-decoration-color:var(--fuchsia);
  text-decoration-thickness:2px;
  text-underline-offset:.22em;
}

/* The ghosted wordmark. Debossed into the footer rather than printed on it,
   so it sits a shade darker than its own ground. The word lives in CSS
   content and not in the markup because it is pure decoration: as real text
   it is a contrast failure, and WCAG exempts incidental decoration only when
   it genuinely is not content. */
.footer__mark{
  margin:0 0 1rem;
  -webkit-user-select:none;user-select:none;
  pointer-events:none;
}
/* The fill stays pitch, because the reasoning above is right: this is not
   content and it should not compete with the footer links. What was missing
   was the other half of a deboss. Something carved into a surface reads as
   carved because light catches the lower lip of the cut; with only the dark
   fill and no lip, this was not reading as debossed, it was reading as a
   smudge on the ground.

   The highlight is one solid pixel of edge-faint rather than a translucent
   white, so rule 1 holds: no alpha anywhere, the depth comes from a palette
   token. It is dark enough that the wordmark still measures below content
   contrast and stays decoration. */
.footer__mark::before{
  content:"Keehan";
  display:block;
  font-family:var(--display);
  font-weight:400;
  font-size:clamp(2.5rem,13vw,9rem);
  line-height:.8;
  color:var(--pitch);
  text-shadow:0 1px 0 var(--edge-faint);
  letter-spacing:.02em;
}

.footer__end{
  display:flex;
  justify-content:space-between;
  align-items:center;
  gap:1rem;
  flex-wrap:wrap;
  padding-top:1.25rem;
  border-top:1px solid var(--edge);
  font-family:var(--mono);
  font-size:var(--t--1);
  color:var(--dust);
  letter-spacing:.08em;
}

/* ============================================================= REVEAL ====
   Sections rise as they arrive. Gated on html.js so the page renders at full
   opacity with scripts off, rather than staying invisible forever. Anything
   already on screen at load is marked visible immediately, so there is no
   flash of movement on every navigation.

   This is the one opacity transition on the site. Rule 1 governs surfaces and
   states, not entrances: an element arriving is not a surface losing weight,
   and the alternative (dissolving a whole section through Bayer levels on
   scroll) costs a repaint per step for no legibility gain. */

html.js .reveal{
  opacity:0;
  transform:translate3d(0,18px,0);
  transition:opacity .5s cubic-bezier(.22,.61,.36,1),transform .6s cubic-bezier(.22,.61,.36,1);
}
html.js .reveal.is-in{opacity:1;transform:none}

/* ================================================== VIEW TRANSITIONS ====
   Cross-document, so this is the whole implementation: no router, no JS, no
   framework. The browser holds the outgoing page, fetches the next one, and
   cross-fades between them.

   This is worth having HERE specifically. The standing preference is that
   multiple pages beat one infinite scroll, and the usual cost of that
   preference is the white blink between navigations that makes a multi-page
   site feel older than a single-page one. This removes the cost without
   giving up the pages: /shop stays a real URL with a real response.

   Deliberately not naming the masthead as a shared element. Giving it a
   view-transition-name makes it persist and morph across the navigation,
   which sounds better than it looks here: the nav is a plain bar with a
   current-page marker that moves, so it just re-renders identically while
   drawing attention to itself. The default whole-page cross-fade is the
   right call for a site this size.

   Progressive enhancement in the honest sense: a browser without support
   navigates exactly as it does today, and nothing needs a fallback. */
@view-transition{ navigation:auto }

::view-transition-old(root),
::view-transition-new(root){
  animation-duration:.22s;
  animation-timing-function:cubic-bezier(.22,.61,.36,1);
}

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

  /* A cross-fade is motion. Reduced motion means the navigation happens with
     no transition at all, rather than a faster one. */
  @view-transition{ navigation:none }
}

/* ============================================================ FEATURE =====
   One image given the room to be read rather than glanced at. Used on
   /artwork.php for the reference sheet, which is a document: it is the only
   image on that page somebody might want to actually study, and it was
   sitting at a quarter width in a grid of four.

   It exists so that page does not run three .plate grids back to back. A
   layout family gets used once per page; when the third section wants a grid
   again, the answer is that one of the three was not really a grid.

   Same border and screened ground as .plate__img, deliberately: this is a
   bigger version of that idea, not a second visual language for pictures.
   The one difference is aspect-ratio, which .plate__img fixes to a square and
   this leaves to the artwork, because a reference sheet cropped to a square
   is a reference sheet with the reference cropped off. */
.feature{margin:0}
.feature__img{
  position:relative;
  isolation:isolate;
  border:1px solid var(--edge);
  background:var(--pitch);
  padding:clamp(.75rem,2vw,1.25rem);
}
.feature__img::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--edge-faint);
  -webkit-mask:var(--dzg-6) repeat;
          mask:var(--dzg-6) repeat;
}
.feature__img img{width:100%}
.feature figcaption{
  margin-top:.75rem;
  font-family:var(--mono);
  font-size:var(--t--1);
  letter-spacing:.06em;
  color:var(--dust);
}
.feature figcaption b{display:block;color:var(--cream);font-weight:500;letter-spacing:.06em}

/* =============================================================== GAME =====
   The 404 board. Shipped on one page; the markup is in 404.php and the loop is
   in assets/js/game404.js.

   The board borrows .plate__img's material exactly: same edge, same pitch
   ground, same grain. It is a picture frame with something moving in it, and
   giving it a treatment of its own would have made the one page nobody meant
   to visit the one page with a second visual language on it.

   touch-action:none is the whole mobile story. Without it a drag across the
   canvas scrolls the page and the pad never moves, which is the difference
   between a game that works on a phone and one that looks like it is broken.
   It is scoped to the canvas, so the page still scrolls normally everywhere
   else including the rest of this block. */
.game{display:grid;gap:1rem}
.game:focus-visible{outline:2px solid var(--fuchsia);outline-offset:6px}

.game__board{
  position:relative;
  isolation:isolate;
  border:1px solid var(--edge);
  background:var(--pitch);
  /* min-height so the frame is the right shape before the script sizes the
     canvas, and the section does not jump when it does */
  min-height:180px;
}
.game__board::before{
  content:"";
  position:absolute;
  inset:0;
  z-index:-1;
  background-color:var(--edge-faint);
  -webkit-mask:var(--dzg-6) repeat;
          mask:var(--dzg-6) repeat;
  pointer-events:none;
}
.game__board canvas{
  display:block;
  width:100%;
  touch-action:none;
  cursor:crosshair;
}

.game__bar{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  gap:1rem 1.5rem;
}
.game__hud{
  display:flex;
  flex-wrap:wrap;
  gap:.6rem 1.4rem;
  margin:0;
  font-family:var(--mono);
  font-size:var(--t--1);
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--dust);
}
/* tabular-nums so a score going from 9 to 10 does not shove the labels beside
   it sideways on every catch. */
.game__hud b{color:var(--cream);font-weight:500;font-variant-numeric:tabular-nums}

/* Always in the DOM and always taking its line, empty or not. It is a live
   region: if it collapsed when empty, every game-over would shift the two
   paragraphs under it down a line. */
.game__say{min-height:1.5em;color:var(--fuchsia)}

@media (max-width:600px){
  /* The button goes full width and the HUD takes its own row. At 390px the two
     of them side by side leave the score wrapping mid-label. */
  .game__bar{gap:.75rem}
  .game__bar .btn{width:100%;text-align:center}
}

/* =============================================================== LIVE =====
   The Twitch button on the front page, which is only in the DOM when the
   channel is actually live. main.js toggles [hidden] on it after checking
   /api/twitch-live.php.

   The dot pulses, and that is the one piece of decoration on this site that
   exists to be noticed rather than to sit under something. It is also the one
   thing that has to survive prefers-reduced-motion as MEANING rather than as
   movement, so under reduced motion it stops animating and stays at full
   opacity: still clearly a live indicator, just not a blinking one. */
.btn--live{border-color:var(--fuchsia)}
.btn__dot{
  display:inline-block;
  width:.5em;
  height:.5em;
  margin-right:.55em;
  vertical-align:.02em;
  background:var(--fuchsia);
  animation:kh-live 1.8s steps(2,end) infinite;
}
/* steps(), not a smooth fade. Everything else on this site is an ordered
   dither: a dot that cross-fades would be the only continuous-tone thing on
   the page. Two steps reads as a blink, which is what a tally light does. */
@keyframes kh-live{0%,60%{opacity:1}61%,100%{opacity:.35}}

@media (prefers-reduced-motion:reduce){
  .btn__dot{animation:none;opacity:1}
}

/* ============================================================== GLEAM =====
   The one third-party widget on the site. It renders itself white, in its own
   fonts, and offers no theming hook of any kind.

   So it is not fought. The panel gives it a hard edge and a little breathing
   room and the widget is allowed to look like what it is: somebody else's
   form, embedded. Trying to tint it would land halfway and read as broken.

   The only thing done to it is a width cap and centring, because Gleam sizes
   itself to its container and a 64ch measure of prose next to a full-bleed
   white block is not a layout. */
.gleam{padding:clamp(.75rem,2vw,1.25rem)}
.gleam .e-widget{display:block;max-width:38rem;margin-inline:auto}

/* ================================================================ ASK =====
   /ask, the how-to-ask reference page. Everything here is built from pieces
   the rest of the site already has: pitch insets with an edge-faint border
   for anything that is a transcript or a template, the mono label face for
   metadata, and the drip bullets for plain lists. No left stripes on the
   quotes and chat logs, which is the one shortcut this kind of page usually
   reaches for. */

.ask-toc{margin-top:.4rem}
.ask-toc ul{
  list-style:none;margin:0;padding:0;
  display:flex;flex-wrap:wrap;gap:.45rem 1.25rem;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.04em;
}

/* The twelve rules. Numbers are the display face at --t-3, which is large
   text, so amethyst is legal for them under rule 3 in the header. Positioned
   rather than gridded because each item mixes a bold lead, loose text and a
   source line, and a grid would turn the loose text into its own cell. */
.ask-rules{
  list-style:none;margin:0;padding:0;
  counter-reset:ask-rule;
  display:grid;gap:1.75rem 3rem;
  grid-template-columns:repeat(auto-fit,minmax(min(100%,22rem),1fr));
}
.ask-rules li{
  counter-increment:ask-rule;
  position:relative;
  padding-left:3.4rem;
  color:var(--vellum);
}
.ask-rules li::before{
  content:counter(ask-rule,decimal-leading-zero);
  position:absolute;left:0;top:-.15rem;
  font-family:var(--display);
  font-size:var(--t-3);
  line-height:1;
  color:var(--amethyst);
}
.ask-rules b{
  display:block;
  margin-bottom:.2rem;
  font-weight:600;
  font-size:var(--t-1);
  line-height:1.25;
  color:var(--cream);
}
.ask-src{
  display:block;
  margin-top:.45rem;
  font-family:var(--mono);
  font-size:var(--t--1);
  letter-spacing:.06em;
  color:var(--dust);
}

/* Numbered steps inside prose. Mono counters in cream, not amethyst: these
   are small, and rule 3 says amethyst never sets small text. */
.ask-steps{list-style:none;margin:0 0 1.1em;padding:0;counter-reset:ask-step}
.ask-steps li{counter-increment:ask-step;position:relative;padding:.3rem 0 .3rem 2.1rem}
.ask-steps li::before{
  content:counter(ask-step);
  position:absolute;left:0;top:.45rem;
  font-family:var(--mono);font-size:var(--t--1);
  color:var(--cream);
}

/* A list sitting in prose needs the same air under it a paragraph gets. */
.split .drips{margin-bottom:1.1em}

/* Transcripts, templates and quotes all share one inset. */
.chat,
.ask-pre,
.ask-quote{
  margin:0 0 1.1em;
  padding:1rem 1.15rem;
  background:var(--pitch);
  border:1px solid var(--edge-faint);
}

.chat{
  list-style:none;
  font-family:var(--mono);font-size:var(--t--1);line-height:1.55;
  color:var(--vellum);
}
.chat li + li{margin-top:.75rem}
.chat__who{display:block;color:var(--dust);letter-spacing:.06em}
.chat__who time{margin-left:.5rem}
.chat__gap{
  padding-block:.4rem;
  border-block:1px dashed var(--edge);
  color:var(--dust);
  text-transform:uppercase;letter-spacing:.12em;
}

/* Scrolls sideways on a phone rather than wrapping, because wrapped code is
   wrong code. min-width:0 on the column holding it, or the grid track grows
   to the width of the longest line and drags the whole page with it. */
.split > :has(.ask-pre){min-width:0}
.ask-pre{
  overflow-x:auto;
  font-family:var(--mono);font-size:var(--t--1);line-height:1.6;
  color:var(--vellum);
  tab-size:2;
}
.ask-pre code{background:none;border:0;padding:0;font-size:inherit;color:inherit}

.ask-quote{
  font-size:var(--t-1);
  line-height:1.5;
  color:var(--cream);
}

/* Bad and good, stupid and smart. The verdict is a mono label in its own
   column so a reader can scan the right-hand side as a clean list. */
.ask-pairs,
.ask-defs{
  display:grid;
  grid-template-columns:max-content minmax(0,1fr);
  gap:0 1.1rem;
  margin:0 0 1.1em;
}
.ask-pairs dt,
.ask-defs dt{
  padding-block:.55rem;
  border-bottom:1px solid var(--edge-faint);
  font-family:var(--mono);font-size:var(--t--1);line-height:1.9;
  letter-spacing:.1em;text-transform:uppercase;
  color:var(--dust);
}
.ask-pairs dd,
.ask-defs dd{
  margin:0;
  padding-block:.55rem;
  border-bottom:1px solid var(--edge-faint);
  color:var(--vellum);
}
.ask-defs dt{text-transform:none;letter-spacing:.04em;color:var(--cream)}

/* ---- the duck ----
   The page's easter egg. Four clicks, one per step of the method beside it,
   then the hidden link. The wobble is three hard steps rather than a smooth
   ease, so it moves like the pixel type around it, and it does not move at
   all for a reader who has asked for stillness. */
.duck{
  display:grid;
  justify-items:center;
  align-content:center;
  gap:1.1rem;
  text-align:center;
}
.duck__btn{
  appearance:none;
  font:inherit;
  background:var(--pitch);
  border:1px solid var(--edge);
  color:var(--cream);
  padding:1.5rem 2rem;
  cursor:pointer;
  transition:border-color .14s ease;
}
.duck__btn:hover{border-color:var(--fuchsia)}
.duck__art{
  display:block;
  white-space:pre;
  text-align:left;
  font-family:var(--mono);
  font-size:var(--t-2);
  line-height:1.1;
}
.duck__btn.is-quacking .duck__art{animation:duck-quack .36s steps(3,end) 1}
@keyframes duck-quack{
  0%  {transform:rotate(0)}
  33% {transform:rotate(-9deg)}
  66% {transform:rotate(7deg)}
  100%{transform:rotate(0)}
}
@media (prefers-reduced-motion:reduce){
  .duck__btn.is-quacking .duck__art{animation:none}
}
.duck__say{
  max-width:32ch;
  min-height:4.8em;
  margin:0;
  font-family:var(--mono);font-size:var(--t--1);
  color:var(--vellum);
}
.duck__prize{display:grid;justify-items:center;gap:.6rem}
.duck__prize .muted{margin:0;font-family:var(--mono);font-size:var(--t--1)}

/* ---- the try it and see video ----
   The frame is the same inset as the transcripts, so the video reads as one
   more exhibit on the page rather than an embed dropped on top of it. The
   width and height attributes on the element reserve its 16:9 box before a
   single byte loads. */
.ask-video{margin:0;max-width:56rem}
.ask-video video{
  display:block;
  width:100%;
  height:auto;
  background:var(--pitch);
  border:1px solid var(--edge-faint);
}
.ask-video figcaption{
  margin-top:.6rem;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.06em;
  color:var(--dust);
}

/* ---- tools: let me google that, the scrambler ---- */
.ask-tool{margin:1.25rem 0 1.1em}
.ask-tool .field{margin-bottom:.9rem}
.ask-tool__row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem 1rem}
.ask-tool__out{margin-top:1.25rem}
.ask-tool__status{font-family:var(--mono);font-size:var(--t--1);color:var(--cream)}
.ask-tool__check{
  display:flex;align-items:center;gap:.6rem;
  margin-bottom:1rem;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.04em;
  color:var(--vellum);
  cursor:pointer;
}
.ask-tool__check input{width:1.1rem;height:1.1rem;margin:0;accent-color:var(--amethyst)}

/* The scrambler writes plain text with newlines into the quote. Only once
   main.js owns it, so the static sample keeps its normal wrapping. */
.ask-quote.is-live{white-space:pre-line}

/* ---- dialogs ----
   Both are native modal dialogs: the browser handles focus, Escape and the
   inert page behind them. The backdrop is solid pitch rather than a
   translucent wash, because nothing on this site fades with opacity. */
.lmg-play::backdrop,
.conga::backdrop{background:var(--pitch)}

.lmg-play{
  width:min(38rem,calc(100% - 2 * var(--gutter)));
  padding:clamp(1.5rem,4vw,2.25rem);
  background:var(--crypt);
  border:1px solid var(--edge);
  color:var(--vellum);
}
.lmg-play__step{
  margin:.9rem 0 1.1rem;
  font-size:var(--t-1);
  color:var(--cream);
}
.lmg-play__bar{display:flex;gap:.6rem;margin-bottom:1.5rem}
.lmg-play__box{
  flex:1;
  min-width:0;
  min-height:2.9rem;
  padding:.7rem .85rem;
  background:var(--pitch);
  border:1px solid var(--edge);
  font-family:var(--mono);
  color:var(--cream);
  overflow-wrap:anywhere;
}
.lmg-play__caret{
  display:inline-block;
  width:.55em;height:1.1em;
  margin-left:.1em;
  vertical-align:-.18em;
  background:var(--fuchsia);
  animation:lmg-blink 1s steps(1,end) infinite;
}
@keyframes lmg-blink{50%{visibility:hidden}}
.lmg-play__go{
  display:grid;place-items:center;
  padding:0 1.1rem;
  border:1px solid var(--edge);
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.14em;text-transform:uppercase;
  color:var(--dust);
}
.lmg-play__go.is-pressed{border-color:var(--fuchsia);color:var(--cream);background:var(--stone)}

/* The conga takes the whole screen, the way the original page does. Its
   canvas is drawn at half resolution and scaled up pixelated, which is where
   the chunky Ditto comes from. */
.conga{
  position:fixed;
  inset:0;
  width:100%;height:100%;
  max-width:none;max-height:none;
  margin:0;padding:0;
  border:0;
  background:#000;
  color:var(--cream);
  overflow:hidden;
  /* On the parent, not the canvas: perspective only applies to children, so
     on the canvas itself the cue-in tilt would render flat. */
  perspective:1000px;
}
.conga__stage{
  display:block;
  image-rendering:pixelated;
}
.conga__stage.is-cueing{animation:conga-cue 2s ease-in-out}
@keyframes conga-cue{
  0%  {transform:scale(.1) rotateX(45deg)}
  100%{transform:scale(1) rotateX(0)}
}
.conga__wait{
  position:absolute;inset:0;
  display:grid;place-items:center;
  margin:0;
  font-family:var(--mono);font-size:var(--t-1);
  color:var(--ash);
}
/* The stop button sits on solid pitch, because the ground behind it is a
   different random colour every 0.4 seconds and no text colour survives
   that on its own. */
.conga__close{position:absolute;top:1rem;right:1rem;background:var(--pitch)}
@media (prefers-reduced-motion:reduce){
  .lmg-play__caret,
  .conga__stage.is-cueing{animation:none}
}

/* /nsfw, the standalone conga. The same .conga box, as the page itself
   instead of inside a dialog, so everything above applies unchanged. What is
   added is the page ground and the Trust me gate, which the dialog version
   does not need because the duck is its gate. The page cannot scroll: there
   is nothing on it but the one screen. */
html:has(.conga-page),
.conga-page{height:100%;overflow:hidden;background:#000}
.conga__gate{
  position:absolute;inset:0;
  display:grid;
  place-content:center;
  justify-items:center;
  gap:.9rem;
  padding-inline:var(--gutter);
  text-align:center;
}

/* =============================================================== MISC ==== */

.stack{display:grid;gap:1.5rem}
.stack--wide{gap:clamp(2rem,4vw,3rem)}
.split{display:grid;grid-template-columns:repeat(auto-fit,minmax(18rem,1fr));gap:clamp(1.75rem,4vw,3rem)}
.tabular{font-variant-numeric:tabular-nums}
.scroll-x{overflow-x:auto}

/* Rhythm inside a section. Three steps, matching the type scale, so a block
   that needs air above it says so with a class instead of carrying its own
   margin in a style attribute. */
.flow-top{margin-top:2rem}
.flow-top-sm{margin-top:1.5rem}
.flow-top-xs{margin-top:.9rem}
.push-under{margin-bottom:1.5rem}

/* A lede that opens a section rather than a page needs less room above it
   than below, because the heading is already sitting right on top of it. */
.lede--section{margin:.9rem 0 2rem}

/* A footnote hanging off a lede: its own line, in the data face, small and
   grey. Inside the paragraph so the pair keeps the lede's one bottom margin. */
.lede__fine{
  display:block;
  margin-top:.4rem;
  font-family:var(--mono);font-size:var(--t--1);letter-spacing:.04em;
  color:var(--dust);
}

.muted{color:var(--dust)}
.narrow{max-width:40rem}
.narrower{max-width:26rem}
.full{grid-column:1 / -1}
.is-relative{position:relative}

/* One-line readouts: a fact set in the data face inside a prose block. */
.stat-line{font-family:var(--mono);color:var(--cream)}

/* Range input. accent-color does the whole job in every current browser, and
   the track inherits the page ground rather than the platform default. */
.range{
  width:100%;
  accent-color:var(--amethyst);
  background:none;
  border:0;
  padding:0;
}

/* The Pavlok send result. aria-live, so it must hold its height or the layout
   jumps every time the message changes. Cream for the ordinary case, fuchsia
   for the failure, because those are the only two inks that clear AA at this
   size and the words carry the rest of the meaning. */
.pv-result{
  font-family:var(--mono);
  font-size:var(--t--1);
  margin-top:1rem;
  min-height:1.5em;
}
.pv-result[data-state="ok"]{color:var(--cream)}
.pv-result[data-state="bad"]{color:var(--fuchsia)}

/* A colour chip printed next to a hex value. The colour itself is data, so it
   stays inline; everything else about the chip lives here. */
.chip{
  display:inline-block;
  width:1rem;height:1rem;
  border:1px solid var(--edge);
  vertical-align:-2px;
  margin-right:.6rem;
}

/* Palette swatches. Only the fill and its readable ink are data; the box,
   the label and the caption are all styled from here. */
.swatch__fill{
  padding:1.15rem 1rem;
  min-height:6.5rem;
  display:flex;
  flex-direction:column;
  justify-content:flex-end;
  border:1px solid var(--edge-faint);
}
.swatch__hex{
  font-family:var(--mono);
  font-size:var(--t--1);
  font-weight:400;
  letter-spacing:.1em;
}
.swatch__meta{
  font-family:var(--mono);
  font-size:var(--t--1);
  color:var(--dust);
  padding-top:.55rem;
  letter-spacing:.08em;
  text-transform:uppercase;
}
.swatch__name{
  display:block;
  color:var(--cream);
  font-weight:500;
  letter-spacing:.06em;
}
/* The measured contrast printed under a swatch on the palette page. The
   palette page is the one place on the site where the numbers behind the
   colour decisions are the content, so they are shown rather than described. */
.swatch__ratio{
  display:block;
  color:var(--dust);
  font-variant-numeric:tabular-nums;
  letter-spacing:.06em;
  text-transform:none;
}

/* Flex and grid children refuse to shrink past their content by default,
   which is how a long unbroken string forces a horizontal scrollbar on the
   whole page. */
.masthead__in > *,
.poster > *,
.rail__row > *{min-width:0}
.visually-hidden{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
