/* ==========================================================================
   KiroVault design system — v2, violet

   Colour discipline: the page is near-black and silver. Violet behaves like
   LIGHT — glow, rim, focus, state — never large flat fill. That restraint is
   the whole difference between this and a generic gamer/crypto page.
   Gold is retired to one job: premium and limited releases.
   ========================================================================== */
:root{
  /* The darks carry a violet cast rather than being neutral grey. On a page
     this dark the difference is barely nameable and completely visible: the
     black stops reading as "unstyled" and starts reading as night. */
  --void:#050409; --base:#08070E; --panel:#0D0B16; --raise:#151223; --edge:#241D36;
  /* Violet is light, not paint (see CLAUDE.md). Pushed a little brighter and
     a little bluer so it holds up as a glow against the warmer darks. */
  --iris:#AE62FA; --iris-lt:#D4B4FF; --iris-dp:#6D28D9;
  --iris-glow:rgba(174,98,250,.45); --iris-dim:rgba(174,98,250,.13);
  /* Text is never pure grey either — it is very slightly violet, so type and
     background belong to the same world. */
  --white:#FFFFFF; --silver:#EFEBF7; --steel:#AFA8C4; --slate:#77718E; --faint:#453E5C;
  --gold:#E3BC63;
  /* Two supporting accents. Used for state and edges only, never as fills —
     they exist so the site is not monochrome violet in every single moment. */
  --cyan:#5EE9F0; --rose:#FF7AA8;
  --line:rgba(174,98,250,.16); --line-soft:rgba(239,235,247,.075);
  --font:"Archivo",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
  --pad:clamp(20px,5vw,80px); --maxw:1280px; --ease:cubic-bezier(.16,1,.3,1);
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{margin:0;background:var(--void);color:var(--silver);font-family:var(--font);
  font-size:16.5px;line-height:1.65;font-variation-settings:"wdth" 100,"wght" 400;
  -webkit-font-smoothing:antialiased;overflow-x:hidden;display:flex;flex-direction:column;min-height:100vh}
main{flex:1}
p{margin:0} a{color:inherit} ul,ol{margin:0;padding:0;list-style:none}
img,svg,video{display:block;max-width:100%}
button{font:inherit} [hidden]{display:none!important}
:focus-visible{outline:2px solid var(--iris);outline-offset:3px;border-radius:2px}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);width:100%}
.narrow{max-width:820px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
/* The shared gradient <defs> and the grain layer must not take up space.
   Without this the defs SVG renders at its default 300x150 and pushes the
   entire page down. */
.defs{position:absolute;width:0;height:0;overflow:hidden;pointer-events:none}
.grain{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;
  opacity:.035;mix-blend-mode:overlay}
/* Violet light that lags behind the cursor. JS moves it; desktop only. */
.pointer-aura{position:fixed;left:0;top:0;width:600px;height:600px;pointer-events:none;
  z-index:0;border-radius:50%;opacity:.55;filter:blur(90px);will-change:transform;
  background:radial-gradient(circle,rgba(168,85,247,.2),transparent 62%)}
/* Ambient motes. z-index 2 puts them over section backgrounds (which are
   opaque and would otherwise swallow them in bands) but under .grain and the
   nav. screen blend on a near-black page means they read as light rather than
   as dots printed on the text — which is the only way this stays on-brand. */
.ambient{position:fixed;inset:0;z-index:2;pointer-events:none;
  mix-blend-mode:screen;opacity:.5}
@media(prefers-reduced-motion:reduce){.ambient{display:none}}

/* typography — one superfamily, using Archivo's width axis for hierarchy */
h1,h2,h3,h4{margin:0;line-height:1.02;letter-spacing:-.02em;font-variation-settings:"wdth" 112,"wght" 800}
.d1{font-size:clamp(44px,8.4vw,116px);font-variation-settings:"wdth" 118,"wght" 900;
  letter-spacing:-.035em;line-height:.92;text-transform:uppercase}
.d2{font-size:clamp(30px,5vw,64px);font-variation-settings:"wdth" 114,"wght" 850;
  letter-spacing:-.028em;line-height:1;text-transform:uppercase}
.d3{font-size:clamp(22px,2.7vw,32px);font-variation-settings:"wdth" 108,"wght" 750;letter-spacing:-.018em}
.label{font-size:11px;letter-spacing:.34em;text-transform:uppercase;
  font-variation-settings:"wdth" 100,"wght" 500;color:var(--iris)}
.label-mute{color:var(--slate)}
.lede{color:var(--steel);font-size:clamp(15.5px,1.7vw,18px);max-width:58ch;line-height:1.7}
.jp{font-size:12.5px;letter-spacing:.2em;color:var(--iris);opacity:.8}
.tint{color:var(--iris)} .dim{color:var(--slate)} .wht{color:var(--white)}

/* nav */
.nav{position:sticky;top:0;z-index:60;transition:background .35s,border-color .35s}
.nav.stuck{background:rgba(5,5,8,.86);backdrop-filter:blur(20px) saturate(1.3);border-bottom:1px solid var(--line-soft)}
.nav-in{display:flex;align-items:center;justify-content:space-between;height:74px;gap:20px}
.nav-logo{display:flex;align-items:center;gap:12px;text-decoration:none;flex-shrink:0}
.nav-logo .mk{width:30px;height:30px;overflow:visible}
/* The mark is alive: the ring counter-turns with scroll (JS sets --spin), the
   star breathes on hover. Both are transforms on a 5-node SVG, so it costs
   nothing to leave on every page. */
.mk-ring{transform-box:fill-box;transform-origin:center;
  transform:rotate(calc(var(--spin,0) * 1deg));transition:transform .5s var(--ease)}
.mk-star{transform-box:fill-box;transform-origin:center;
  transition:transform .55s var(--ease),opacity .55s}
.mk-crown{transition:opacity .45s}
.nav-logo:hover .mk-star,.door-dial:hover .mk-star{transform:scale(1.3)}
.nav-logo:hover .mk-ring{transform:rotate(calc(var(--spin,0) * 1deg + 180deg))}
@media(prefers-reduced-motion:reduce){
  .mk-ring,.mk-star{transition:none;transform:none}
}
.nav-logo .wm{font-size:16px;letter-spacing:.2em;font-variation-settings:"wdth" 105,"wght" 700;
  color:var(--white);line-height:1;white-space:nowrap}
.nav-logo .wm i{font-style:normal;color:var(--iris)}
.nav-menu{display:flex;align-items:center;gap:30px}
.nav-menu a{font-size:12.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--steel);
  text-decoration:none;position:relative;padding:6px 0;transition:color .22s;
  font-variation-settings:"wdth" 100,"wght" 500;white-space:nowrap}
.nav-menu a::after{content:"";position:absolute;left:0;bottom:0;height:1px;width:0;
  background:var(--iris);box-shadow:0 0 8px var(--iris-glow);transition:width .3s var(--ease)}
.nav-menu a:hover,.nav-menu a[aria-current="page"]{color:var(--white)}
.nav-menu a:hover::after,.nav-menu a[aria-current="page"]::after{width:100%}
.nav-cta{border:1px solid var(--iris);padding:10px 18px;color:var(--iris)!important;
  transition:background .25s,color .25s,box-shadow .25s}
.nav-cta::after{display:none!important}
.nav-cta:hover{background:var(--iris);color:var(--void)!important;box-shadow:0 0 24px var(--iris-glow)}
.burger{display:none;background:none;border:0;padding:10px;cursor:pointer}
.burger span{display:block;width:22px;height:1.5px;background:var(--iris);margin:5px 0;transition:transform .3s,opacity .3s}
.nav.open .burger span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
.nav.open .burger span:nth-child(2){opacity:0}
.nav.open .burger span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}
@media(max-width:1040px){
  .burger{display:block}
  .nav{background:rgba(5,5,8,.92);backdrop-filter:blur(20px);border-bottom:1px solid var(--line-soft)}
  .nav-menu{position:absolute;top:74px;left:0;right:0;flex-direction:column;align-items:stretch;gap:0;
    background:var(--base);border-bottom:1px solid var(--line);padding:8px var(--pad) 24px;display:none}
  .nav.open .nav-menu{display:flex}
  .nav-menu a{padding:15px 0;border-bottom:1px solid var(--line-soft);font-size:14px}
  .nav-cta{margin-top:16px;text-align:center}
}

/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;font-size:12.5px;
  letter-spacing:.16em;text-transform:uppercase;font-variation-settings:"wdth" 100,"wght" 600;
  padding:16px 32px;text-decoration:none;cursor:pointer;border:1px solid var(--iris);
  background:transparent;color:var(--iris);position:relative;isolation:isolate;overflow:hidden;
  transition:color .3s,box-shadow .3s,transform .3s var(--ease)}
.btn::before{content:"";position:absolute;inset:0;background:var(--iris);
  transform:translateY(101%);transition:transform .38s var(--ease);z-index:-1}
.btn:hover{color:var(--void);box-shadow:0 0 32px var(--iris-glow);transform:translateY(-2px)}
.btn:hover::before{transform:translateY(0)}
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
.btn-solid{background:var(--iris);color:var(--void);box-shadow:0 0 26px var(--iris-glow)}
.btn-solid::before{background:var(--iris-lt)}
.btn-solid:hover{color:var(--void)}
.btn-ghost{border-color:var(--edge);color:var(--silver)}
.btn-ghost::before{background:var(--raise)}
.btn-ghost:hover{color:var(--white);border-color:var(--iris);box-shadow:none}
.btn-sm{padding:11px 22px;font-size:11px}
.btn-block{display:flex;width:100%}
.btn-gold{border-color:var(--gold);color:var(--gold)}
.btn-gold::before{background:var(--gold)}
.btn-gold:hover{color:var(--void);box-shadow:0 0 26px rgba(227,188,99,.3)}

/* sections */
.sec{padding:clamp(70px,8.5vw,128px) 0;position:relative;overflow:hidden}
.sec-sm{padding:clamp(46px,5.5vw,80px) 0;position:relative;overflow:hidden}
.sec-line{border-top:1px solid var(--line-soft)}
.sec-panel{background:var(--base)}
.head{margin-bottom:clamp(34px,4.2vw,56px)}
.head .label{margin-bottom:15px;display:block}
.head .lede{margin-top:18px}
.aura{position:absolute;pointer-events:none;z-index:0;border-radius:50%;
  background:radial-gradient(circle,var(--iris-glow),transparent 66%);filter:blur(50px);opacity:.45}
.sec>.wrap{position:relative;z-index:1}

/* cards */
.grid{display:grid;gap:1px;background:var(--line-soft);border:1px solid var(--line-soft)}
.grid-3{grid-template-columns:repeat(3,1fr)}
.grid-2{grid-template-columns:repeat(2,1fr)}
.cell{background:var(--panel);padding:clamp(24px,3vw,38px);transition:background .35s;position:relative}
.cell:hover{background:var(--raise)}
.cell .ico{width:25px;height:25px;fill:none;stroke:var(--iris);stroke-width:1.4;
  stroke-linecap:round;stroke-linejoin:round;margin-bottom:20px}
.cell h3{font-size:18px;color:var(--white);margin-bottom:9px;
  font-variation-settings:"wdth" 106,"wght" 700;letter-spacing:-.01em}
.cell p{color:var(--slate);font-size:14px;line-height:1.62}
.cell-link{text-decoration:none;display:block}
.cell-link:hover h3{color:var(--iris-lt)}
.cell-more{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--iris);margin-top:16px;display:block}

/* ------------------------------------------------------- category plates
   Generated art (build.py -> art_plate). Every stroke inside is currentColor,
   so a plate takes the accent of whatever section it sits in — which is how
   the three tools keep their separate identities from one drawing routine.
   Layers marked data-depth are moved by the pointer in kv.js. */
.plate{position:relative;overflow:hidden;aspect-ratio:16/10;
  color:var(--tone,var(--iris));border:1px solid var(--line-soft);
  background:radial-gradient(120% 90% at 50% 112%,rgba(168,85,247,.10),transparent 60%),
    linear-gradient(158deg,#0B0B12,#07070B)}
.plate-svg,.plate-img{position:absolute;inset:0;width:100%;height:100%;display:block}
.plate-img{object-fit:cover;opacity:.85}
.plate-glow{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .45s;
  background:radial-gradient(240px circle at var(--px,50%) var(--py,50%),
    currentColor,transparent 62%);mix-blend-mode:screen;filter:opacity(.22)}
.plate::after{content:"";position:absolute;inset:0;pointer-events:none;
  box-shadow:inset 0 0 44px rgba(0,0,0,.5)}
.plate [data-depth]{transition:transform .6s var(--ease)}

/* ------------------------------------------------- every drawing breathes
   Only a handful of scenes carried an explicit an-* class, so the other two
   hundred cards sat there as flat clipart. This animates the layers of every
   plate — drawn or yet to be drawn — by drifting them against each other at
   different rates. Depth separation is what makes a flat drawing read as a
   place rather than a sticker.

   It animates `translate` and `rotate` (the standalone properties), NOT
   `transform`: the pointer-parallax in kv.js writes inline `transform` on
   these same layers, and a CSS animation on that property would win and kill
   the interaction. The standalone properties compose with it instead. */
.plate [data-depth]{animation:plFloat 9s ease-in-out infinite alternate}
.plate [data-depth]:nth-of-type(2){animation-duration:12.5s;animation-delay:-2.4s}
.plate [data-depth]:nth-of-type(3){animation-duration:15.5s;animation-delay:-5.1s}
.plate [data-depth]:nth-of-type(4){animation-duration:10.5s;animation-delay:-3.3s}
.plate [data-depth]:nth-of-type(5){animation-duration:18s;animation-delay:-7.6s}
.plate [data-depth]:nth-of-type(6){animation-duration:13.5s;animation-delay:-1.2s}
@keyframes plFloat{from{translate:0 0;rotate:0deg}to{translate:2.5px -3px;rotate:.35deg}}
/* the whole scene swells very slightly, so even a one-layer plate is alive */
.plate-svg{animation:plBreathe 14s ease-in-out infinite alternate;
  transform-origin:50% 60%}
@keyframes plBreathe{from{scale:1}to{scale:1.035}}
/* on hover the drift speeds up — the card wakes when you reach for it */
.cat:hover .plate [data-depth],.sub:hover .plate [data-depth],
.cell-art:hover .plate [data-depth]{animation-duration:3.4s}
@media(prefers-reduced-motion:reduce){
  .plate [data-depth],.plate-svg{animation:none!important}
}
.cell-art:hover .plate-glow,.room:hover .plate-glow,.plate:hover .plate-glow{opacity:1}

/* a plated cell loses its padding — the art runs to the edges */
.cell-art{padding:0;display:flex;flex-direction:column}
.cell-art .plate{border:0;border-bottom:1px solid var(--line-soft)}
.cell-txt{padding:clamp(20px,2.6vw,32px);display:flex;flex-direction:column;flex:1}
/* links line up across a row even when the copy runs to different lengths */
.cell-txt .cell-more{margin-top:auto;padding-top:16px}
/* the icon and the link follow the section's accent, so a cyan plate is not
   paired with a violet icon */
.cell-art .ico{stroke:var(--tone,var(--iris))}
.cell-art .cell-more{color:var(--tone-lt,var(--iris))}
@media(prefers-reduced-motion:reduce){.plate [data-depth]{transition:none}}
@media(max-width:900px){.grid-3{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.grid-3,.grid-2{grid-template-columns:1fr}}

/* hex traits, echoing the key visual */
.traits{display:flex;flex-direction:column}
.trait{display:grid;grid-template-columns:54px 1fr;gap:18px;align-items:start;
  padding:18px 0;border-bottom:1px solid var(--line-soft)}
.trait-ico{width:46px;height:46px;display:grid;place-items:center;position:relative}
.trait-ico svg{width:20px;height:20px;fill:none;stroke:var(--iris);stroke-width:1.5;position:relative;z-index:1}
.trait-ico::before{content:"";position:absolute;inset:0;border:1px solid var(--line);
  clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);background:var(--iris-dim)}
.trait h3{font-size:13px;letter-spacing:.2em;text-transform:uppercase;color:var(--white);
  margin-bottom:5px;font-variation-settings:"wdth" 100,"wght" 600}
.trait p{color:var(--slate);font-size:13.5px;line-height:1.55}

/* tiers */
.tiers{border-top:1px solid var(--line)}
.tier{display:grid;grid-template-columns:66px 1.5fr 1fr 190px;gap:clamp(14px,2.6vw,36px);
  align-items:start;padding:clamp(26px,3.4vw,42px) 0;border-bottom:1px solid var(--line-soft);
  position:relative;transition:background .35s}
.tier::before{content:"";position:absolute;left:calc(var(--pad)*-1);right:calc(var(--pad)*-1);top:0;bottom:0;
  background:linear-gradient(90deg,transparent,var(--iris-dim),transparent);opacity:0;
  transition:opacity .4s;pointer-events:none}
.tier:hover::before{opacity:1}
.tier>*{position:relative}
.tier-num{font-size:clamp(26px,3.4vw,40px);color:var(--faint);line-height:1;
  font-variation-settings:"wdth" 118,"wght" 900;transition:color .35s}
.tier:hover .tier-num{color:var(--iris)}
.tier-name{font-size:clamp(22px,2.8vw,32px);color:var(--white);text-decoration:none;
  font-variation-settings:"wdth" 112,"wght" 850;letter-spacing:-.02em;text-transform:uppercase}
.tier:hover .tier-name{color:var(--iris-lt)}
.tier-who{color:var(--slate);font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;margin-top:9px}
.tier-desc{color:var(--steel);font-size:14.5px;margin-top:13px;max-width:38ch}
.tick li{position:relative;padding-left:19px;font-size:13.5px;color:var(--steel);margin-bottom:9px;line-height:1.5}
.tick li::before{content:"";position:absolute;left:0;top:8px;width:5px;height:5px;
  background:var(--iris);transform:rotate(45deg);box-shadow:0 0 8px var(--iris-glow)}
.tier-buy{text-align:right}
.price{font-size:clamp(24px,2.8vw,34px);color:var(--white);display:block;line-height:1;
  font-variation-settings:"wdth" 112,"wght" 800}
.price small{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);
  display:block;margin-top:9px;font-variation-settings:"wdth" 100,"wght" 400}
.tier-buy .btn{margin-top:16px}
.badge{display:inline-block;font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--iris);border:1px solid var(--iris);padding:4px 9px;margin-bottom:11px;
  font-variation-settings:"wdth" 100,"wght" 600}
@media(max-width:1000px){
  .tier{grid-template-columns:44px 1fr;gap:12px 16px}
  .tier-mid,.tier-list,.tier-buy{grid-column:2}
  .tier-buy{text-align:left;margin-top:6px}
}

/* accordion */
details{border-bottom:1px solid var(--line-soft)}
summary{cursor:pointer;list-style:none;padding:22px 44px 22px 0;position:relative;
  font-size:clamp(16px,1.9vw,20px);color:var(--silver);transition:color .25s;
  font-variation-settings:"wdth" 106,"wght" 650}
summary::-webkit-details-marker{display:none}
summary::after{content:"";position:absolute;right:8px;top:50%;width:10px;height:10px;margin-top:-6px;
  border-right:1.5px solid var(--iris);border-bottom:1.5px solid var(--iris);
  transform:rotate(45deg) translateY(-3px);transition:transform .3s}
details[open] summary{color:var(--iris-lt)}
details[open] summary::after{transform:rotate(-135deg) translateY(-3px)}
summary:hover{color:var(--white)}
details .body{padding:0 44px 24px 0;color:var(--steel);font-size:15px;max-width:70ch}

/* forms */
.input{width:100%;padding:15px 17px;font-family:var(--font);font-size:14px;background:var(--panel);
  border:1px solid var(--edge);color:var(--white);transition:border-color .25s,box-shadow .25s}
.input::placeholder{color:var(--faint)}
.input:focus{outline:none;border-color:var(--iris);box-shadow:0 0 0 3px var(--iris-dim)}
.input.err{border-color:#C4506B}
.form-inline{display:flex;gap:10px;max-width:540px;flex-wrap:wrap}
.form-inline .input{flex:1;min-width:220px}
.msg{margin-top:14px;font-size:13.5px}
.msg-ok{color:var(--iris-lt)} .msg-err{color:#D97A90}
.hint{font-size:12px;color:var(--slate);margin-top:13px}
.field{display:block;margin-bottom:18px}
.field span,.field>label{display:block;font-size:10.5px;letter-spacing:.24em;text-transform:uppercase;
  color:var(--iris);margin-bottom:9px}
.opts{display:flex;flex-wrap:wrap;gap:8px}
.opt{flex:1 1 auto;min-width:125px;padding:13px 15px;border:1px solid var(--edge);background:var(--panel);
  color:var(--steel);font-size:13px;cursor:pointer;text-align:center;transition:all .25s;font-family:var(--font)}
.opt:hover{border-color:var(--slate);color:var(--white)}
.opt[aria-pressed="true"]{border-color:var(--iris);color:var(--iris-lt);background:var(--iris-dim);
  box-shadow:0 0 20px var(--iris-dim)}
.panel-box{background:var(--panel);border:1px solid var(--line-soft);padding:clamp(24px,3.4vw,40px)}

/* footer */
.foot{border-top:1px solid var(--line-soft);padding:clamp(46px,5.5vw,70px) 0 38px;background:var(--base);margin-top:auto}
.foot-top{display:grid;grid-template-columns:1.3fr 2.2fr;gap:clamp(28px,4.6vw,60px);
  padding-bottom:38px;border-bottom:1px solid var(--line-soft)}
.foot-brand p{color:var(--slate);font-size:13px;max-width:32ch;margin-top:13px}
.foot-social{display:flex;gap:11px;margin-top:18px}
.foot-social a{width:34px;height:34px;border:1px solid var(--edge);display:grid;place-items:center;
  transition:border-color .25s,background .25s}
.foot-social a:hover{border-color:var(--iris);background:var(--iris-dim)}
.foot-social svg{width:15px;height:15px;stroke:var(--iris);fill:none;stroke-width:1.5}
.foot-nav{display:grid;grid-template-columns:repeat(4,1fr);gap:20px}
.foot-nav h4{font-size:10px;letter-spacing:.26em;text-transform:uppercase;color:var(--iris);
  margin:0 0 15px;font-variation-settings:"wdth" 100,"wght" 600}
.foot-nav a{display:block;color:var(--slate);text-decoration:none;font-size:13px;padding:5px 0;transition:color .22s}
.foot-nav a:hover{color:var(--white)}
.disc{margin-top:28px;color:#4A4A5E;font-size:11px;line-height:1.75;max-width:96ch}
.disc strong{color:var(--slate)}
.copy{margin-top:22px;color:#3A3A4A;font-size:11px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap}
@media(max-width:880px){.foot-top{grid-template-columns:1fr}.foot-nav{grid-template-columns:repeat(2,1fr)}}

/* prose */
.prose h2{font-size:clamp(21px,2.5vw,29px);margin:38px 0 13px;color:var(--white);
  font-variation-settings:"wdth" 108,"wght" 750}
.prose h3{font-size:16px;margin:26px 0 9px;color:var(--iris-lt);font-variation-settings:"wdth" 102,"wght" 650}
.prose p{color:var(--steel);margin-bottom:14px;font-size:15px}
.prose ul{margin:0 0 17px}
.prose li{color:var(--steel);position:relative;padding-left:19px;margin-bottom:8px;font-size:15px}
.prose li::before{content:"";position:absolute;left:0;top:10px;width:4px;height:4px;background:var(--iris);transform:rotate(45deg)}
.prose strong{color:var(--white)} .prose a{color:var(--iris-lt)}
.updated{font-size:12px;color:var(--slate);border-left:2px solid var(--iris);padding-left:13px}
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em;color:var(--iris-lt);
  background:var(--iris-dim);padding:2px 6px}
.crumb{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--slate);margin-bottom:20px}
.crumb a{color:var(--iris);text-decoration:none}
.crumb span{margin:0 8px;color:var(--faint)}

/* motion — reveal on entry; nothing loops */
.rv{opacity:0;transform:translateY(20px);transition:opacity .85s var(--ease),transform .85s var(--ease)}
.rv.in{opacity:1;transform:none}
.rv-d1{transition-delay:.08s}.rv-d2{transition-delay:.16s}.rv-d3{transition-delay:.24s}

@media(prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important}
  html{scroll-behavior:auto}
  .rv{opacity:1;transform:none}
  .parallax,.hero-art{transform:none!important}
}

/* ==========================================================================
   Motion layer
   Scroll- and pointer-driven only. Scene elements expose --p (0 to 1) as
   their scroll progress, which CSS below maps onto transforms.
   ========================================================================== */

/* progress bar */
.scroll-bar{position:fixed;top:0;left:0;right:0;height:2px;z-index:80;transform-origin:0 50%;
  transform:scaleX(0);background:linear-gradient(90deg,var(--iris-dp),var(--iris),var(--iris-lt));
  box-shadow:0 0 12px var(--iris-glow);will-change:transform}

/* word-by-word rise */
.split-w{display:inline-block;overflow:hidden;vertical-align:bottom;
  padding:0 .04em .12em;margin:0 -.04em -.12em}
.split-w>span{display:inline-block;transform:translateY(105%);opacity:0;
  transition:transform .85s var(--ease),opacity .85s var(--ease)}
.split-host.in .split-w>span{transform:translateY(0);opacity:1}

/* staggered children */
[data-stagger]>*{opacity:0;transform:translateY(26px);
  transition:opacity .75s var(--ease),transform .75s var(--ease)}
[data-stagger].in>*{opacity:1;transform:none}
[data-stagger].in>*:nth-child(1){transition-delay:.05s}
[data-stagger].in>*:nth-child(2){transition-delay:.13s}
[data-stagger].in>*:nth-child(3){transition-delay:.21s}
[data-stagger].in>*:nth-child(4){transition-delay:.29s}
[data-stagger].in>*:nth-child(5){transition-delay:.37s}
[data-stagger].in>*:nth-child(6){transition-delay:.45s}

/* grow-in */
[data-grow]{opacity:0;transform:scale(.94);transition:opacity 1s var(--ease),transform 1s var(--ease)}
[data-grow].in{opacity:1;transform:none}

/* ---------------------------------------------------------- vault door
   Two panels that part as you scroll, revealing what's behind them. The
   one showpiece moment on the page. */
.door{position:relative;min-height:230vh}
.door-stage{position:sticky;top:0;height:100vh;display:grid;place-items:center;
  overflow:hidden;padding:90px var(--pad) 60px}
.door-panel{position:absolute;top:0;bottom:0;width:52%;background:
  linear-gradient(160deg,#12121C,#08080C 60%);border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);z-index:2;will-change:transform;
  display:grid;place-items:center}
.door-l{left:0;border-right:1px solid var(--iris);
  transform:translateX(calc(min(var(--p,0) * 1.45, 1) * -100%))}
.door-r{right:0;border-left:1px solid var(--iris);
  transform:translateX(calc(min(var(--p,0) * 1.45, 1) * 100%))}
.door-seam{position:absolute;top:0;bottom:0;left:50%;width:1px;z-index:3;pointer-events:none;
  background:linear-gradient(180deg,transparent,var(--iris),transparent);
  box-shadow:0 0 26px var(--iris-glow);opacity:calc(1 - var(--p,0) * 1.6)}
.door-dial{width:min(180px,26vw);opacity:.5}
.door-in{position:relative;z-index:1;text-align:center;max-width:600px;
  opacity:calc((var(--p,0) - .42) * 3.6);transform:scale(calc(.92 + var(--p,0) * .08))}
.door-hint{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);z-index:4;
  font-size:10.5px;letter-spacing:.28em;text-transform:uppercase;color:var(--slate);
  opacity:calc(1 - var(--p,0) * 3)}
@media(max-width:700px){.door-panel{width:53%}.door-dial{width:34vw}}

/* ------------------------------------------------------------- marquee */
.marq{overflow:hidden;border-top:1px solid var(--line-soft);border-bottom:1px solid var(--line-soft);
  background:var(--base);padding:16px 0;position:relative}
.marq-track{display:flex;gap:44px;width:max-content;animation:marq 34s linear infinite}
.marq span{font-size:12px;letter-spacing:.26em;text-transform:uppercase;color:var(--slate);
  white-space:nowrap;display:flex;align-items:center;gap:44px}
.marq span::after{content:"";width:5px;height:5px;background:var(--iris);transform:rotate(45deg)}
@keyframes marq{to{transform:translateX(-50%)}}
.marq:hover .marq-track{animation-play-state:paused}
@media(prefers-reduced-motion:reduce){.marq-track{animation:none}}

/* The WebGL canvas stands in for the <img> and inherits its classes, so it
   picks up the same size, mask and animations. It must not be display:block
   only — it has to fill exactly what the image filled or the mask slips. */
/* The canvas fills its rig absolutely and is sized entirely from inline
   styles set in kv.js. Deliberately NO class-based sizing here: the canvas
   used to wear the image's classes and inherit `height:auto` /
   `aspect-ratio`, which fought the absolute fill and drew Kiro perfectly into
   a box positioned where nobody could see him. */
canvas.is-3d{display:block}

/* ==========================================================================
   The watcher — Kiro, present on every page
   Bottom-left, small, half-lit. He fades in once you have scrolled past the
   first screen so he never fights the hero, and he is decorative: aria-hidden,
   pointer-events:none, and removed outright on touch or reduced motion.
   ========================================================================== */
.watcher{position:fixed;left:18px;bottom:18px;z-index:40;width:132px;
  pointer-events:none;opacity:0;transform:translateY(22px) scale(.96);
  transition:opacity .9s var(--ease),transform .9s var(--ease)}
.watcher.on{opacity:1;transform:none}
/* Lean layer. JS writes --wx/--wy/--wr/--ws/--wt on .watcher and they inherit
   down. He turns AND tips AND swells slightly — a flat picture can carry
   whole-body movement convincingly, which is where his life comes from now
   that the eyelid overlay is gone. */
.watcher-fig{position:relative;display:block;transform-origin:50% 85%;
  transform:translate3d(var(--wx,0px),var(--wy,0px),0)
            rotate(calc(var(--wr,0deg) + var(--wt,0deg)))
            scale(var(--ws,1));
  transition:transform .55s var(--ease)}
/* he snaps his attention rather than easing when he looks at something */
.watcher.look .watcher-fig{transition:transform .28s cubic-bezier(.2,1.6,.4,1)}
.watcher.look{--ws:1.07}
/* a short dip — agreement, or acknowledging you took something */
.watcher.nod .kiro-idle{animation:kiroNod .75s var(--ease)}
@keyframes kiroNod{
  0%{transform:none}
  35%{transform:translateY(7px) rotate(2.5deg) scale(.97)}
  100%{transform:none}}
/* his glow takes the colour of the world you are standing in */
.watcher-glow{background:radial-gradient(circle at 52% 42%,
  color-mix(in srgb,var(--kiro-tone,var(--iris)) 46%,transparent),transparent 62%)}
.watcher.look .watcher-glow{opacity:.85}
.watcher-fig .kiro-idle{position:relative;display:block}
.watcher img{width:100%;height:auto;display:block;border-radius:50%;
  filter:grayscale(.15) contrast(1.08);
  -webkit-mask-image:radial-gradient(circle at 50% 44%,#000 54%,transparent 74%);
  mask-image:radial-gradient(circle at 50% 44%,#000 54%,transparent 74%)}
.watcher-glow{position:absolute;inset:-18% -18% -6%;border-radius:50%;
  background:radial-gradient(circle at 52% 42%,var(--iris-glow),transparent 62%);
  filter:blur(22px);opacity:.55;
  animation:watchPulse 6.5s var(--ease) infinite alternate}
@keyframes watchPulse{from{opacity:.32;transform:scale(.94)}to{opacity:.62;transform:scale(1.04)}}
/* He speaks in his own time now, so the bubble is driven by .on on ITSELF
   rather than by the watcher being visible — otherwise every line he says
   would be stuck on screen permanently. */
.watcher-say{position:absolute;left:calc(100% - 6px);bottom:24px;width:200px;
  font-size:11.5px;line-height:1.55;color:var(--silver);
  border-left:1px solid var(--kiro-tone,var(--iris));padding:2px 0 2px 12px;
  opacity:0;transform:translateX(-10px);
  transition:opacity .45s var(--ease),transform .55s var(--ease)}
.watcher-say.on{opacity:1;transform:none}
@media(max-width:1280px){.watcher-say{display:none}}
@media(max-width:900px){.watcher{display:none}}
@media(prefers-reduced-motion:reduce){.watcher{display:none}}

/* ==========================================================================
   Topic scenes
   A moving atmosphere per collection: One Piece gets a sea, Fire Force gets
   embers, Tokyo Nights gets rain. Defined once here and referenced by name
   from build.py's topic_theme(), so two thousand collections add no CSS.

   Every scene paints in --tone and --tone2 (the topic's colours) over the
   Vault's violet, which is what keeps them all recognisably the same site.
   All of them are transform/opacity keyframes on three spans — no canvas, no
   JS, nothing per-page.
   ========================================================================== */
.scene{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:0}
.scene>span{position:absolute;inset:-20% -10%;display:block}
.scene::after{content:"";position:absolute;inset:0;
  background:radial-gradient(ellipse 80% 60% at 50% 0%,
    color-mix(in srgb,var(--tone) 20%,transparent),transparent 70%)}

/* --- waves: slow horizontal swells ------------------------------------- */
.scene-waves>span{background:
  radial-gradient(120% 42% at 50% 100%,color-mix(in srgb,var(--tone) 26%,transparent),transparent 70%);
  animation:scWave 13s ease-in-out infinite}
.scene-waves>span:nth-child(2){inset:-10% -10% -30%;opacity:.6;animation-duration:19s;
  animation-direction:reverse;
  background:radial-gradient(110% 38% at 40% 100%,color-mix(in srgb,var(--tone2) 30%,transparent),transparent 70%)}
.scene-waves>span:nth-child(3){opacity:.4;animation-duration:26s}
@keyframes scWave{0%{transform:translate3d(-4%,4%,0) scaleY(1)}
  50%{transform:translate3d(4%,-3%,0) scaleY(1.12)}
  100%{transform:translate3d(-4%,4%,0) scaleY(1)}}

/* --- embers: sparks rising -------------------------------------------- */
.scene-embers>span{background-image:
  radial-gradient(2px 2px at 20% 90%,var(--tone),transparent),
  radial-gradient(1.6px 1.6px at 45% 95%,var(--tone2),transparent),
  radial-gradient(2.4px 2.4px at 70% 88%,var(--tone),transparent),
  radial-gradient(1.4px 1.4px at 88% 96%,var(--tone2),transparent);
  background-size:340px 340px;opacity:.75;animation:scRise 17s linear infinite}
.scene-embers>span:nth-child(2){background-size:230px 230px;opacity:.5;
  animation-duration:24s;animation-delay:-6s}
.scene-embers>span:nth-child(3){background-size:460px 460px;opacity:.35;
  animation-duration:31s;animation-delay:-12s}
@keyframes scRise{from{transform:translate3d(0,12%,0)}to{transform:translate3d(-6%,-70%,0)}}

/* --- snow: drifting down ---------------------------------------------- */
.scene-snow>span{background-image:
  radial-gradient(2.2px 2.2px at 15% 10%,var(--tone),transparent),
  radial-gradient(1.6px 1.6px at 55% 25%,var(--tone2),transparent),
  radial-gradient(2px 2px at 80% 5%,var(--tone),transparent);
  background-size:300px 300px;opacity:.6;animation:scFall 22s linear infinite}
.scene-snow>span:nth-child(2){background-size:200px 200px;opacity:.4;animation-duration:31s}
.scene-snow>span:nth-child(3){background-size:420px 420px;opacity:.28;animation-duration:40s}
@keyframes scFall{from{transform:translate3d(-3%,-40%,0)}to{transform:translate3d(3%,70%,0)}}

/* --- rain: streaks ----------------------------------------------------- */
.scene-rain>span{background-image:repeating-linear-gradient(104deg,
  transparent 0 26px,color-mix(in srgb,var(--tone) 34%,transparent) 26px 27.2px);
  opacity:.30;animation:scRain 1.5s linear infinite}
.scene-rain>span:nth-child(2){opacity:.18;animation-duration:2.3s;
  background-image:repeating-linear-gradient(101deg,
    transparent 0 40px,color-mix(in srgb,var(--tone2) 40%,transparent) 40px 41.4px)}
.scene-rain>span:nth-child(3){opacity:.1;animation-duration:3.4s}
@keyframes scRain{from{transform:translate3d(0,-14%,0)}to{transform:translate3d(-5%,14%,0)}}

/* --- stars: a slow turning field --------------------------------------- */
.scene-stars>span{background-image:
  radial-gradient(1.6px 1.6px at 12% 22%,var(--tone),transparent),
  radial-gradient(1.2px 1.2px at 38% 64%,var(--tone2),transparent),
  radial-gradient(2px 2px at 72% 31%,var(--tone),transparent),
  radial-gradient(1.4px 1.4px at 88% 78%,var(--tone2),transparent);
  background-size:280px 280px;opacity:.7;animation:scTwinkle 6s ease-in-out infinite}
.scene-stars>span:nth-child(2){background-size:400px 400px;opacity:.45;
  animation-duration:9s;animation-delay:-3s}
.scene-stars>span:nth-child(3){background-size:560px 560px;opacity:.3;
  animation:scTurn 120s linear infinite}
@keyframes scTwinkle{0%,100%{opacity:.28}50%{opacity:.8}}
@keyframes scTurn{to{transform:rotate(360deg)}}

/* --- speed: light streaking past --------------------------------------- */
.scene-speed>span{background-image:linear-gradient(90deg,
  transparent 0%,color-mix(in srgb,var(--tone) 45%,transparent) 48%,transparent 100%);
  height:2px;top:32%;inset-inline:-40%;bottom:auto;opacity:.6;
  animation:scDash 2.6s var(--ease) infinite}
.scene-speed>span:nth-child(2){top:58%;height:1px;opacity:.4;animation-duration:3.6s;
  animation-delay:-.9s;background-image:linear-gradient(90deg,
    transparent 0%,color-mix(in srgb,var(--tone2) 55%,transparent) 48%,transparent 100%)}
.scene-speed>span:nth-child(3){top:76%;height:1.4px;opacity:.3;animation-duration:4.6s;
  animation-delay:-2s}
@keyframes scDash{from{transform:translateX(-60%)}to{transform:translateX(60%)}}

/* --- bloom: soft breathing colour -------------------------------------- */
.scene-bloom>span{background:radial-gradient(closest-side,
  color-mix(in srgb,var(--tone) 34%,transparent),transparent 70%);
  width:60%;height:60%;inset:auto auto 10% 8%;filter:blur(28px);
  animation:scBloom 15s ease-in-out infinite}
.scene-bloom>span:nth-child(2){inset:8% 6% auto auto;width:52%;height:52%;
  animation-duration:21s;animation-delay:-5s;
  background:radial-gradient(closest-side,
    color-mix(in srgb,var(--tone2) 36%,transparent),transparent 70%)}
.scene-bloom>span:nth-child(3){inset:30% auto auto 40%;width:40%;height:40%;
  animation-duration:27s;animation-delay:-11s;opacity:.7}
@keyframes scBloom{0%,100%{transform:scale(.9) translate(0,0)}
  50%{transform:scale(1.2) translate(6%,-5%)}}

/* --- glyph: expanding rings, for the word collections ------------------ */
.scene-glyph>span{border:1px solid color-mix(in srgb,var(--tone) 40%,transparent);
  border-radius:50%;inset:50% auto auto 50%;width:32vmax;height:32vmax;
  translate:-50% -50%;animation:scPulse 9s var(--ease) infinite}
.scene-glyph>span:nth-child(2){animation-delay:-3s;
  border-color:color-mix(in srgb,var(--tone2) 40%,transparent)}
.scene-glyph>span:nth-child(3){animation-delay:-6s}
@keyframes scPulse{from{transform:scale(.25);opacity:0}
  25%{opacity:.55}to{transform:scale(1.5);opacity:0}}

/* --- drift: the default, soft floating light --------------------------- */
.scene-drift>span{background:radial-gradient(closest-side,
  color-mix(in srgb,var(--tone) 26%,transparent),transparent 70%);
  width:56%;height:56%;inset:auto auto 4% 4%;filter:blur(34px);
  animation:scDrift 24s ease-in-out infinite}
.scene-drift>span:nth-child(2){inset:2% 2% auto auto;width:48%;height:48%;
  animation-duration:33s;animation-direction:reverse;
  background:radial-gradient(closest-side,
    color-mix(in srgb,var(--tone2) 28%,transparent),transparent 70%)}
.scene-drift>span:nth-child(3){inset:34% auto auto 30%;width:34%;height:34%;
  animation-duration:41s;opacity:.6}
@keyframes scDrift{0%,100%{transform:translate(0,0) scale(1)}
  33%{transform:translate(9%,-7%) scale(1.14)}
  66%{transform:translate(-7%,5%) scale(.92)}}

@media(prefers-reduced-motion:reduce){
  .scene>span{animation:none!important}
}

/* --------------------------------------------------------- topic theming
   .topic wraps a whole collection page and carries --tone/--tone2, so the
   theme reaches the grid, the chips and the buttons rather than stopping at
   the header. Violet is still the site's own colour everywhere else — the
   nav, the mark, the cursor — which is what keeps a One Piece page and a
   Fire Force page recognisably the same building. */
.topic .sub-hero>.wrap{position:relative;z-index:1}
/* a stronger wash than the category default: the topic has to be readable in
   the first second, before anyone scrolls to the art */
.topic .sub-hero{background:
  linear-gradient(180deg,color-mix(in srgb,var(--tone) 15%,transparent),transparent 78%),
  radial-gradient(ellipse 60% 70% at 88% 0%,
    color-mix(in srgb,var(--tone2) 20%,transparent),transparent 72%)}
.topic .label{color:var(--tone)}
.topic .crumb a{color:var(--tone)}
.topic .chip:hover{border-color:var(--tone);color:var(--tone)}
.topic .wp-nav a:hover{color:var(--tone)}
.topic .btn-solid{background:var(--tone);border-color:var(--tone);
  box-shadow:0 0 26px color-mix(in srgb,var(--tone) 42%,transparent)}
.topic .btn-solid::before{background:var(--tone2)}
.topic .btn-ghost:hover{border-color:var(--tone);color:var(--tone)}
.topic .void-mark span{background:var(--tone)}
.topic .d1{text-shadow:0 0 70px color-mix(in srgb,var(--tone) 30%,transparent)}

/* ==========================================================================
   Kiro, alive
   He is one flat picture, so the blink is faked: a skin-toned lid sits over
   each eye and scales down over it. The coordinates below are measured off
   the actual artwork (irises located by cropping and enlarging the files),
   which is the only reason it lands — a lid a few percent out reads as a
   smudge on his face rather than an eyelid.

   Two different pictures, two different sets of eyes:
     .eyes-portrait  kiro-portrait.webp   both eyes, head tilted ~27deg
     .eyes-hero      kiro-hero.webp       one eye visible, tilted ~12deg
   ========================================================================== */
.eyes{position:absolute;inset:0;pointer-events:none;z-index:2}
.lid{position:absolute;display:block;
  transform-origin:50% 0;
  transform:rotate(var(--r,0deg)) scaleY(0);
  /* sampled from the art: shadowed upper lid into under-eye skin */
  background:linear-gradient(180deg,#241d20 0%,#453739 72%,#4b3d3f 100%);
  border-radius:50% 50% 46% 46%/72% 72% 28% 28%;
  filter:blur(.7px);
  transition:transform .085s cubic-bezier(.45,0,.55,1)}
.blink .lid{transform:rotate(var(--r,0deg)) scaleY(1)}

.eyes-portrait .lid-a{left:43.1%;top:52.4%;width:13%;height:7%;--r:-27deg}
.eyes-portrait .lid-b{left:15.4%;top:66.1%;width:15%;height:6.4%;--r:-27deg}
.eyes-hero .lid-a{left:57.1%;top:34.0%;width:9%;height:5%;--r:-12deg}

/* ---------------------------------------------------------------- shine
   The lid is the mechanical half of being alive; this is the other half. A
   violet catchlight sits in each iris, brightens as the pointer comes near
   him, and goes out for exactly as long as the lid is down. It is screen-
   blended, so on a dark iris it reads as light in the eye rather than as a
   dot stuck on his face — which also means it survives being a percent or
   two out, where the lid would not. */
   REMOVED, deliberately. It was drawn at ~9% of the image width at 95%
   opacity, screen-blended. A real catchlight is a pinprick — a couple of
   pixels. At that size it stopped being light in the eye and became a glowing
   orb pasted on his face, visible from across the room, and on the portrait
   the second one landed on his cheek. It made him look broken, not alive.

   The rule it taught: an overlay on flat art must be small enough that a
   two-percent placement error is invisible. The lid passes that test — it is
   skin-coloured and eye-shaped. A bright glowing dot does not. Do not
   reintroduce a shine without layered artwork to anchor it to. */
.shine{display:none!important}

/* He breathes. Defined here rather than on the home page because more than
   one page stands him up now, and his lungs should not be page-scoped. */
@keyframes kiroBreathe{
  0%{transform:translate3d(0,0,0) scale(1)}
  50%{transform:translate3d(0,-14px,0) scale(1.012)}
  100%{transform:translate3d(0,0,0) scale(1)}}

/* He notices you. --near is written by kv.js from the pointer's distance to
   him: further away he is a picture, close up he lifts and lights. */
[data-rig]{transition:transform 1.1s var(--ease),filter .8s var(--ease)}
[data-rig].near{filter:brightness(calc(1 + var(--near,0) * .1))
  saturate(calc(1 + var(--near,0) * .18))}
/* a single involuntary movement — he looks over, then back */
[data-rig].glance{animation:kiroGlance 1.5s var(--ease)}
@keyframes kiroGlance{
  0%{transform:none}
  22%{transform:translate3d(-6px,-2px,0) rotate(-.7deg)}
  70%{transform:translate3d(-4px,-1px,0) rotate(-.5deg)}
  100%{transform:none}}
/* and one when you actually touch him */
[data-rig].jolt{animation:kiroJolt .7s var(--ease)}
@keyframes kiroJolt{
  0%{transform:none}
  30%{transform:translate3d(5px,-8px,0) scale(1.012)}
  100%{transform:none}}
/* what he says when you prod him. Positioned in the viewport by kv.js, next
   to whichever copy of him you touched. */
.rig-say{position:fixed;z-index:120;pointer-events:none;max-width:min(280px,60vw);
  padding:11px 15px;background:rgba(8,8,12,.9);backdrop-filter:blur(10px);
  border:1px solid var(--line);color:var(--silver);font-size:13.5px;line-height:1.5;
  opacity:0;transform:translateY(8px) scale(.97);
  transition:opacity .4s var(--ease),transform .5s var(--ease);
  box-shadow:0 20px 60px rgba(0,0,0,.6),0 0 40px var(--iris-dim)}
.rig-say::before{content:"";position:absolute;left:-1px;top:12px;bottom:12px;width:2px;
  background:var(--iris);box-shadow:0 0 12px var(--iris-glow)}
.rig-say.on{opacity:1;transform:none}

@media(prefers-reduced-motion:reduce){
  .shine{transition:none}
  [data-rig].glance,[data-rig].jolt{animation:none}
  .rig-say{transition:none}}

/* the whole figure drifts: a slow idle sway on top of everything else, so he
   is never perfectly still even with the pointer off the page */
.kiro-idle{animation:kiroIdle 17s ease-in-out infinite}
@keyframes kiroIdle{
  0%{transform:translate3d(0,0,0) rotate(0deg)}
  28%{transform:translate3d(-5px,-7px,0) rotate(-.5deg)}
  55%{transform:translate3d(4px,-3px,0) rotate(.35deg)}
  80%{transform:translate3d(-2px,5px,0) rotate(-.2deg)}
  100%{transform:translate3d(0,0,0) rotate(0deg)}}
@media(prefers-reduced-motion:reduce){
  .eyes{display:none}
  .kiro-idle{animation:none}
}

/* ==========================================================================
   Click feedback
   Every click leaves a mark: a ring that expands from the exact point and a
   press state on whatever was hit. Both are transform/opacity only.
   ========================================================================== */
.tap{position:fixed;left:0;top:0;width:14px;height:14px;margin:-7px 0 0 -7px;
  border:1px solid var(--iris);border-radius:50%;pointer-events:none;z-index:9997;
  animation:tapRing .62s var(--ease) forwards}
@keyframes tapRing{
  0%{transform:scale(.4);opacity:.9;border-width:2px}
  100%{transform:scale(7);opacity:0;border-width:.5px}}
.tap-2{animation-duration:.9s;animation-delay:.06s;border-color:var(--iris-lt)}

/* anything clickable dips when pressed and springs back */
a,button,.opt,.wp,.cat,.sub,.fg-p,.fg-d,.wp-f,.ad-item,.ad-mini{
  -webkit-tap-highlight-color:transparent}
.is-press{transform:scale(.972)!important;transition:transform .09s ease-out!important}
@media(prefers-reduced-motion:reduce){
  .tap{display:none}
  .is-press{transform:none!important}
}

/* ==========================================================================
   Wallpaper cards
   Global, not page-scoped: the home page, the gallery and the detail pages
   all show these, and a card should not depend on where it lands.
   ========================================================================== */
.wp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(12px,1.6vw,22px)}
@media(max-width:1000px){.wp-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.wp-grid{grid-template-columns:1fr}}
.wp{position:relative;display:block;text-decoration:none;--tone:var(--iris)}
.wp-frame{position:relative;display:block;overflow:hidden;aspect-ratio:16/10;
  background:var(--panel);border:1px solid var(--line-soft)}
.wp-img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform 1.1s var(--ease),filter .6s;will-change:transform}
.wp:hover .wp-img{transform:scale(1.07)}
/* ---------------------------------------------------------- 3D cards ----
   Every card on the site leans into the screen under the pointer and the
   picture inside it travels the other way. Opposing directions is the whole
   trick — the same one .hero-echo uses — because matching them just reads as
   the card sliding about.

   The lean is on the CARD and the parallax is on the IMAGE, and they are
   deliberately different properties. .wp-img's transform is already spoken
   for by the hover scale, and the reveal engine writes transform:none onto
   images inline, which would win over anything set here. object-position
   moves the picture within its own frame instead, composes with the scale
   rather than fighting it, and the 1.07 hover scale is what supplies the
   overflow for it to move into.

   Driven from CSS variables so one delegated pointermove in kv.js can serve
   a page of forty cards, or a thousand, without a listener on each. */
.wp,.sub,.cat{transition:transform .4s var(--ease)}
.wp.is-lean,.sub.is-lean,.cat.is-lean{
  transition:transform .09s linear;
  transform:perspective(900px)
            rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg))
            translateZ(0)}
.wp.is-lean .wp-img{
  object-position:calc(50% + var(--px,0px)) calc(50% + var(--py,0px))}
@media (prefers-reduced-motion:reduce){
  .wp.is-lean,.sub.is-lean,.cat.is-lean{transform:none}
  .wp.is-lean .wp-img{object-position:50% 50%}}

.wp-sheen{position:absolute;inset:0;pointer-events:none;opacity:0;
  background:linear-gradient(115deg,transparent 32%,
    color-mix(in srgb,var(--tone) 40%,transparent) 50%,transparent 68%);
  transform:translateX(-60%);transition:opacity .3s}
.wp:hover .wp-sheen{opacity:.5;animation:sheen 1.1s var(--ease)}
@keyframes sheen{to{transform:translateX(60%)}}
.wp-frame::after{content:"";position:absolute;inset:0;pointer-events:none;
  box-shadow:inset 0 0 0 1px transparent;transition:box-shadow .35s}
.wp:hover .wp-frame::after{box-shadow:inset 0 0 0 1px var(--tone),
  inset 0 -70px 60px -50px color-mix(in srgb,var(--tone) 55%,transparent)}
.wp-meta{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  padding:14px 2px 0}
.wp-meta b{color:var(--white);font-size:15.5px;font-variation-settings:"wdth" 106,"wght" 700;
  transition:color .3s}
.wp:hover .wp-meta b{color:var(--tone)}
.wp-meta em{font-style:normal;font-size:10px;letter-spacing:.22em;text-transform:uppercase;
  color:var(--slate)}
.wp-get{position:absolute;left:14px;bottom:64px;padding:8px 16px;font-size:10.5px;
  letter-spacing:.2em;text-transform:uppercase;color:var(--void);background:var(--tone);
  opacity:0;transform:translateY(10px);transition:opacity .4s var(--ease),transform .4s var(--ease)}
.wp:hover .wp-get{opacity:1;transform:none}

/* ==========================================================================
   Motion vocabulary
   Fades read as cheap. These are the two moves the whole site now uses: text
   rises out from behind a clipped edge, and images are wiped in under a mask
   while the picture inside them settles from a slight scale. Both are one
   transform plus one clip, so they stay on the compositor.
   ========================================================================== */
.mask-head,.mask-line{clip-path:inset(0 0 110% 0);transform:translateY(.5em);
  opacity:0;transition:clip-path 1.05s var(--ease),transform 1.05s var(--ease),opacity .5s;
  transition-delay:var(--d,0s)}
.mask-head.in,.mask-line.in{clip-path:inset(-.25em 0 -.25em 0);transform:none;opacity:1}

[data-img-reveal]{position:relative;overflow:hidden;
  clip-path:inset(0 0 100% 0);transition:clip-path 1.25s var(--ease);
  transition-delay:calc(var(--i,0) * .07s)}
[data-img-reveal].in{clip-path:inset(0 0 0 0)}
[data-img-reveal] img{transform:scale(1.14);transition:transform 1.5s var(--ease);
  transition-delay:calc(var(--i,0) * .07s)}
[data-img-reveal].in img{transform:scale(1)}
/* hover scale is owned by .wp-img, so let it win once the reveal has run */
.wp:hover [data-img-reveal].in img{transform:scale(1.07)}

/* the grid leans with scroll velocity — cheap, and the single strongest
   "this site is alive" cue there is */
[data-skew]{will-change:transform}

@media(prefers-reduced-motion:reduce){
  .mask-head,.mask-line{clip-path:none;transform:none;opacity:1;transition:none}
  [data-img-reveal]{clip-path:none;transition:none}
  [data-img-reveal] img{transform:none;transition:none}
  [data-skew]{transform:none!important}
}

/* ------------------------------------------------------- page curtain
   A short violet wipe over the first paint, with the mark held in it. It is
   removed from the DOM once it has run, and never shown to a crawler or to
   anyone with reduced motion. */
.curtain{position:fixed;inset:0;z-index:400;background:var(--void);
  display:grid;place-items:center;pointer-events:none;
  transition:clip-path 1s var(--ease),opacity .5s .55s;
  clip-path:inset(0 0 0 0)}
.curtain.up{clip-path:inset(0 0 100% 0);opacity:0}
.curtain .mk{width:54px;height:54px;overflow:visible;
  animation:curtainMk 1.4s var(--ease) infinite alternate}
@keyframes curtainMk{from{opacity:.35;transform:scale(.94)}to{opacity:1;transform:scale(1)}}

/* ------------------------------------------------------- entry gate */
.gate{border:1px solid var(--line);background:linear-gradient(160deg,var(--panel),var(--base));
  padding:clamp(24px,3.4vw,40px);position:relative;overflow:hidden}
.gate::before{content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(600px circle at var(--mx,50%) var(--my,50%),var(--iris-dim),transparent 62%)}
.gate>*{position:relative}
.gate-q{font-size:clamp(18px,2.2vw,25px);color:var(--white);margin-bottom:20px;
  font-variation-settings:"wdth" 108,"wght" 700}
.gate-opts{display:grid;grid-template-columns:repeat(2,1fr);gap:9px}
@media(max-width:560px){.gate-opts{grid-template-columns:1fr}}
.gate-out{margin-top:24px;padding-top:22px;border-top:1px solid var(--line);
  opacity:0;transform:translateY(14px);transition:opacity .6s var(--ease),transform .6s var(--ease)}
.gate-out.in{opacity:1;transform:none}
.gate-out h3{color:var(--iris-lt);margin-bottom:10px}
.gate-out .btn{margin-top:18px}

/* --------------------------------------------------------- entry portal
   The door you knock on before the site opens. Asked once, remembered in
   localStorage, and skippable — it is a greeting, not a paywall. Built by JS
   so crawlers and no-JS visitors get the page itself with nothing in front. */
.portal{position:fixed;inset:0;z-index:200;display:grid;place-items:center;
  padding:clamp(18px,4vw,40px);opacity:0;transition:opacity .5s var(--ease)}
.portal.in{opacity:1}
.portal.out{opacity:0;pointer-events:none}
.portal-bg{position:absolute;inset:0;background:rgba(4,4,7,.93);
  backdrop-filter:blur(26px) saturate(1.2)}
/* A door that parts rather than a box that fades in. The leaves start closed
   over the whole viewport and split down the seam; everything inside then
   rises on its own --pd delay. */
.portal-door{position:absolute;inset:0;pointer-events:none;overflow:hidden}
.portal-leaf{position:absolute;top:0;bottom:0;width:50.5%;background:
  linear-gradient(180deg,#0A0A12,#050508);
  transition:transform 1.15s var(--ease)}
.portal-leaf-l{left:0;transform:translateX(0)}
.portal-leaf-r{right:0;transform:translateX(0)}
.portal.in .portal-leaf-l{transform:translateX(-101%)}
.portal.in .portal-leaf-r{transform:translateX(101%)}
.portal-seam{position:absolute;left:50%;top:0;bottom:0;width:1px;
  background:linear-gradient(180deg,transparent,var(--iris),transparent);
  opacity:.9;transition:opacity .5s .5s}
.portal.in .portal-seam{opacity:0}
/* everything inside rises out from behind a clipped edge, staggered */
.portal-r{opacity:0;transform:translateY(16px);
  transition:opacity .7s var(--ease),transform .7s var(--ease);
  transition-delay:var(--pd,0s)}
.portal.in .portal-r{opacity:1;transform:none}
.portal-in{position:relative;width:100%;max-width:720px;text-align:center;
  border:1px solid var(--line);background:linear-gradient(160deg,var(--panel),var(--base));
  padding:clamp(28px,4.4vw,54px);
  box-shadow:0 40px 120px rgba(0,0,0,.8),0 0 70px rgba(168,85,247,.1);
  transform:translateY(18px) scale(.985);transition:transform .6s var(--ease)}
.portal.in .portal-in{transform:none}
.portal-mk{width:52px;height:52px;margin:0 auto 20px;overflow:visible}
.portal-q{font-size:clamp(24px,3.6vw,42px);color:var(--white);margin:12px 0 0;
  font-variation-settings:"wdth" 114,"wght" 850;letter-spacing:-.025em;line-height:1.02;
  text-transform:uppercase}
.portal-sub{margin-top:14px;color:var(--steel);font-size:15px;max-width:46ch;
  margin-left:auto;margin-right:auto}
.portal-opts{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;
  margin-top:clamp(24px,3vw,34px);text-align:left}
.portal-opts button{padding:16px 15px;border:1px solid var(--edge);background:var(--panel);
  color:var(--silver);text-align:left;cursor:pointer;
  transition:border-color .28s,background .28s,color .28s,transform .4s var(--ease)}
.portal-opts button b{display:block;font-size:15px;color:var(--white);
  font-variation-settings:"wdth" 108,"wght" 750;letter-spacing:.01em}
.portal-opts button span{display:block;margin-top:5px;font-size:12px;color:var(--slate);line-height:1.45}
.portal-opts button:hover{border-color:var(--iris);background:var(--iris-dim);transform:translateY(-3px)}
.portal-opts button:hover b{color:var(--iris-lt)}
@media(max-width:720px){.portal-opts{grid-template-columns:repeat(2,1fr)}}
@media(max-width:420px){.portal-opts{grid-template-columns:1fr}}
.portal-skip{margin-top:22px;background:none;border:0;color:var(--slate);cursor:pointer;
  font-size:11.5px;letter-spacing:.18em;text-transform:uppercase;padding:8px;
  transition:color .25s}
.portal-skip:hover{color:var(--iris)}
.portal-ans h3{color:var(--iris-lt);font-size:clamp(20px,2.6vw,30px);margin-bottom:12px}
.portal-ans .lede{margin:0 auto}
.portal-ans .portal-go{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:26px}
@media(prefers-reduced-motion:reduce){
  .portal,.portal-in,.portal-opts button{transition:none}
  .portal-in{transform:none}
}

/* ------------------------------------------------------- tilt cards */
[data-tilt]{transition:transform .5s var(--ease),background .35s;transform-style:preserve-3d;
  position:relative;overflow:hidden}
[data-tilt]::after{content:"";position:absolute;inset:0;pointer-events:none;opacity:0;
  transition:opacity .35s;background:radial-gradient(400px circle at var(--mx,50%) var(--my,50%),
  rgba(168,85,247,.13),transparent 60%)}
[data-tilt]:hover::after{opacity:1}
[data-magnet]{transition:transform .4s var(--ease),color .3s,box-shadow .3s}

/* scroll-linked scale for section headings */
[data-scene].lift .lift-in{transform:translateY(calc((1 - var(--p,0)) * 40px));
  opacity:calc(.3 + var(--p,0) * .7)}

@media(prefers-reduced-motion:reduce){
  .split-w>span{transform:none;opacity:1}
  [data-stagger]>*,[data-grow]{opacity:1;transform:none}
  .door{min-height:auto}
  .door-stage{position:static;height:auto;padding:80px 0}
  .door-panel,.door-seam,.door-hint{display:none}
  .door-in{opacity:1;transform:none}
  .scroll-bar{display:none}
}

/* -------------------------------------------------------- dagger cursor */
.has-dagger,.has-dagger *{cursor:none}
/* keep the native caret where people actually need it */
.has-dagger.no-dagger,.has-dagger input,.has-dagger textarea,
.has-dagger select,.has-dagger [contenteditable]{cursor:auto}
/* 28x52 at 1:1 with the SVG viewBox, so transform-origin can name the blade
   tip in user units and the point stays welded to the real pointer position. */
.dagger{position:fixed;left:0;top:0;width:26px;height:54px;z-index:9999;pointer-events:none;
  will-change:transform;transition:opacity .18s}
.dagger svg{width:100%;height:100%;filter:drop-shadow(0 0 7px rgba(168,85,247,.7))
  drop-shadow(0 2px 4px rgba(0,0,0,.6));transform-origin:3px 2px;
  transition:transform .12s var(--ease)}
.dagger.strike svg{transform:scale(.86) rotate(-13deg)}
.dagger.off{opacity:0}
.dagger-trail{position:fixed;inset:0;z-index:9998;pointer-events:none}
/* interactive elements get a subtle lift instead of a cursor change */
.has-dagger a:hover,.has-dagger button:hover,.has-dagger .opt:hover{filter:brightness(1.08)}
@media(hover:none),(pointer:coarse){.dagger,.dagger-trail{display:none}}
@media(prefers-reduced-motion:reduce){
  .has-dagger,.has-dagger *{cursor:auto}
  .dagger,.dagger-trail{display:none}
}

/* ------------------------------------------------------- vault dropdown */
.dd{position:relative}
.dd-top{display:inline-flex;align-items:center;gap:7px}
.dd-caret{width:5px;height:5px;border-right:1.4px solid currentColor;border-bottom:1.4px solid currentColor;
  transform:rotate(45deg) translateY(-2px);transition:transform .28s}
.dd:hover .dd-caret,.dd.open .dd-caret{transform:rotate(225deg) translateY(-1px)}
.dd-panel{position:absolute;top:calc(100% + 14px);left:50%;transform:translate(-50%,10px);
  min-width:520px;background:rgba(8,8,12,.97);backdrop-filter:blur(22px);
  border:1px solid var(--line);padding:22px;opacity:0;visibility:hidden;
  transition:opacity .28s var(--ease),transform .28s var(--ease),visibility .28s;
  box-shadow:0 26px 70px rgba(0,0,0,.7),0 0 40px rgba(168,85,247,.09)}
.dd:hover .dd-panel,.dd:focus-within .dd-panel,.dd.open .dd-panel{
  opacity:1;visibility:visible;transform:translate(-50%,0)}
.dd-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px}
.dd-col h4{font-size:9.5px;letter-spacing:.28em;text-transform:uppercase;color:var(--iris);
  margin:0 0 12px;padding-bottom:9px;border-bottom:1px solid var(--line-soft);
  font-variation-settings:"wdth" 100,"wght" 600}
.dd-item{display:block;padding:9px 10px;margin:0 -10px;text-decoration:none;
  border-left:1px solid transparent;transition:background .22s,border-color .22s}
.dd-item::after{display:none!important}
.dd-item b{display:block;font-size:13.5px;color:var(--silver);letter-spacing:0;
  text-transform:none;font-variation-settings:"wdth" 102,"wght" 600}
.dd-item span{display:block;font-size:11.5px;color:var(--slate);letter-spacing:0;
  text-transform:none;margin-top:2px}
.dd-item:hover{background:var(--iris-dim);border-left-color:var(--iris)}
.dd-item:hover b{color:var(--iris-lt)}
.dd-all{display:block;margin-top:18px;padding-top:14px;border-top:1px solid var(--line-soft);
  font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--iris);text-decoration:none}
.dd-all::after{display:none!important}
@media(max-width:1040px){
  .dd-panel{position:static;transform:none;min-width:0;opacity:1;visibility:visible;
    border:0;padding:0 0 10px;box-shadow:none;background:none;backdrop-filter:none;
    display:none}
  .dd.open .dd-panel{display:block}
  .dd-grid{grid-template-columns:1fr;gap:14px}
  .dd-item{margin:0;padding:11px 0;border-left:0;border-bottom:1px solid var(--line-soft)}
  .dd-top{width:100%;justify-content:space-between}
}

/* ==========================================================================
   Tool identities
   All three shared one template and felt interchangeable. Each now gets its
   own accent, output shape and layout so they read as separate instruments.
   ========================================================================== */

/* -- 01 Rate calculator: cold steel, a single verdict number -------------- */
.t-rate{--tone:#8B5CF6;--tone-lt:#C4B5FD}
.t-rate .out{background:linear-gradient(165deg,#0E0C18,#08080C);
  border-color:rgba(139,92,246,.3)}
.t-rate .out-big{color:#fff;text-shadow:0 0 48px rgba(139,92,246,.55)}
.t-rate .out-sub{color:var(--tone-lt)}
.t-rate .tool-mark{position:absolute;top:20px;right:20px;width:34px;height:34px;
  border:1px solid rgba(139,92,246,.4);display:grid;place-items:center;color:var(--tone-lt);
  font-size:11px;font-variation-settings:"wdth" 112,"wght" 800}

/* -- 02 Income goal: a funnel that narrows, stage by stage ---------------- */
.t-goal{--tone:#06B6D4;--tone-lt:#67E8F9}
.t-goal .label,.t-goal .crumb a,.t-goal .field span{color:var(--tone-lt)}
.t-goal .input:focus{border-color:var(--tone);box-shadow:0 0 0 3px rgba(6,182,212,.16)}
.t-goal .out{background:linear-gradient(165deg,#04141A,#08080C);border-color:rgba(6,182,212,.3)}
.t-goal .out-big{color:#fff;text-shadow:0 0 48px rgba(6,182,212,.5)}
.t-goal .out-sub{color:var(--tone-lt)}
.funnel{margin-top:24px;display:flex;flex-direction:column;gap:8px}
.fn-row{position:relative;border:1px solid rgba(6,182,212,.24);background:rgba(6,182,212,.06);
  padding:13px 16px;display:flex;justify-content:space-between;align-items:center;
  transition:width .5s var(--ease),background .3s}
.fn-row:nth-child(1){width:100%}
.fn-row:nth-child(2){width:82%}
.fn-row:nth-child(3){width:64%}
.fn-row span{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--tone-lt)}
.fn-row b{font-size:17px;color:#fff;font-variation-settings:"wdth" 110,"wght" 800}
.fn-row::after{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;background:var(--tone);
  box-shadow:0 0 12px rgba(6,182,212,.7)}
@media(max-width:920px){.fn-row:nth-child(2){width:88%}.fn-row:nth-child(3){width:76%}}

/* -- 03 Runway: a depleting bar, warm because it's about time running out - */
.t-run{--tone:#F59E0B;--tone-lt:#FCD34D}
.t-run .label,.t-run .crumb a,.t-run .field span{color:var(--tone-lt)}
.t-run .input:focus{border-color:var(--tone);box-shadow:0 0 0 3px rgba(245,158,11,.16)}
.t-run .out{background:linear-gradient(165deg,#191006,#08080C);border-color:rgba(245,158,11,.3)}
.t-run .out-big{color:#fff;text-shadow:0 0 48px rgba(245,158,11,.45)}
.t-run .out-sub{color:var(--tone-lt)}
.months{display:grid;grid-template-columns:repeat(12,1fr);gap:4px;margin-top:24px}
.mo{aspect-ratio:1/2.4;border:1px solid rgba(245,158,11,.2);background:rgba(245,158,11,.05);
  transition:background .4s var(--ease),border-color .4s,box-shadow .4s}
.mo.on{background:linear-gradient(180deg,var(--tone),#B45309);border-color:var(--tone);
  box-shadow:0 0 12px rgba(245,158,11,.4)}
.mo.part{background:linear-gradient(180deg,rgba(245,158,11,.5),transparent);border-color:rgba(245,158,11,.5)}
.months-key{display:flex;justify-content:space-between;margin-top:10px;font-size:10.5px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--slate)}
.t-rate .btn,.t-goal .btn,.t-run .btn{border-color:var(--tone);color:var(--tone-lt)}
.t-rate .btn::before,.t-goal .btn::before,.t-run .btn::before{background:var(--tone)}
.t-rate .btn:hover,.t-goal .btn:hover,.t-run .btn:hover{color:#08080C}
.t-goal .tick li::before,.t-run .tick li::before{background:var(--tone)}
.tool-nav a[aria-current]{color:var(--tone,var(--iris));border-color:var(--tone,var(--iris))}


/* ==========================================================================
   The gateway
   The click animation between wallpaper pages. A link marked [data-portal]
   does not navigate straight away: a full-screen layer opens from the exact
   point you clicked, in the tone of the thing you clicked, plays one of five
   effects chosen by the category, and only then loads the next page. The page
   you land on plays the second half of the same move, so a hard navigation
   reads as one continuous door opening.

   Every element here is built by kv.js and removed afterwards. Nothing loops,
   and under prefers-reduced-motion the whole thing is skipped and the browser
   simply follows the link.
   ========================================================================== */
.gw{position:fixed;inset:0;z-index:9000;pointer-events:none;overflow:hidden;
  --tone:#A855F7;--x:50%;--y:50%}
.gw-ink{position:absolute;left:var(--x);top:var(--y);width:10px;height:10px;
  margin:-5px 0 0 -5px;border-radius:50%;transform:scale(0);
  background:radial-gradient(circle,color-mix(in srgb,var(--tone) 34%,#050508) 0%,
    #050508 62%);
  transition:transform .46s cubic-bezier(.7,0,.3,1)}
.gw.on .gw-ink{transform:scale(var(--reach,60))}
.gw-ring{position:absolute;left:var(--x);top:var(--y);width:80px;height:80px;
  margin:-40px 0 0 -40px;border-radius:50%;opacity:0;
  border:1px solid color-mix(in srgb,var(--tone) 70%,transparent);
  box-shadow:0 0 40px color-mix(in srgb,var(--tone) 45%,transparent)}
.gw.on .gw-ring{animation:gwRing .62s var(--ease) forwards}
@keyframes gwRing{0%{opacity:.9;transform:scale(.2)}100%{opacity:0;transform:scale(14)}}
.gw-name{position:absolute;left:0;right:0;top:50%;text-align:center;
  transform:translateY(-50%);color:var(--tone);font-size:clamp(22px,4.4vw,52px);
  font-variation-settings:"wdth" 116,"wght" 900;letter-spacing:-.02em;
  opacity:0;clip-path:inset(0 0 110% 0)}
.gw.on .gw-name{animation:gwName .6s .12s var(--ease) forwards}
@keyframes gwName{
  0%{opacity:0;clip-path:inset(0 0 110% 0);transform:translateY(-40%)}
  55%{opacity:1;clip-path:inset(-.3em 0 -.3em 0);transform:translateY(-50%)}
  100%{opacity:1;clip-path:inset(-.3em 0 -.3em 0);transform:translateY(-54%)}}

/* the five effects. Each is a set of plain divs kv.js positions; the movement
   is one transform each, so a burst of forty of them still composites. */
.gw-fx{position:absolute;left:var(--x);top:var(--y);width:0;height:0}
.gw-fx i{position:absolute;display:block;background:var(--tone);opacity:0;
  transform-origin:0 50%}
.gw.on .gw-fx i{animation:var(--anim) var(--dur,.6s) var(--ease) var(--d,0s) forwards}
@keyframes gwShard{0%{opacity:0;transform:rotate(var(--a)) scaleX(0)}
  30%{opacity:.9}100%{opacity:0;transform:rotate(var(--a)) scaleX(1)}}
@keyframes gwWarp{0%{opacity:0;transform:rotate(var(--a)) translateX(4vmax) scaleX(.1)}
  25%{opacity:1}100%{opacity:0;transform:rotate(var(--a)) translateX(72vmax) scaleX(1)}}
@keyframes gwBloom{0%{opacity:0;transform:rotate(var(--a)) translateX(0) scale(0)}
  40%{opacity:.85}100%{opacity:0;transform:rotate(var(--a)) translateX(46vmax) scale(1)}}
@keyframes gwScan{0%{opacity:0;transform:translate(var(--sx),var(--sy)) scaleX(0)}
  35%{opacity:.8}100%{opacity:0;transform:translate(calc(var(--sx) * -1),var(--sy)) scaleX(1)}}
@keyframes gwSweep{0%{opacity:0;transform:rotate(var(--a)) scaleX(0)}
  40%{opacity:.55}100%{opacity:0;transform:rotate(var(--a)) scaleX(1)}}

/* arrival: the same layer, already covering, dissolving outward. The tone and
   the name come across in sessionStorage, which is why the two halves match. */
.gw-in .gw-ink{transform:scale(var(--reach,60));
  transition:transform .8s var(--ease),opacity .55s .12s}
.gw-in.off .gw-ink{transform:scale(0);opacity:0}
.gw-in .gw-name{opacity:1;clip-path:inset(-.3em 0 -.3em 0);
  transition:opacity .4s,transform .7s var(--ease)}
.gw-in.off .gw-name{opacity:0;transform:translateY(-58%)}

@media(prefers-reduced-motion:reduce){.gw{display:none}}

/* ==========================================================================
   Lightbox
   Subcategory pages hold real image files rather than one page per file, so
   the viewer is here. It opens from the card it was launched from, keeps the
   download row of a detail page, and answers to the arrow keys.
   ========================================================================== */
.lb{position:fixed;inset:0;z-index:8000;display:grid;grid-template-rows:auto 1fr auto;
  opacity:0;transition:opacity .35s var(--ease);--tone:var(--iris)}
.lb.on{opacity:1}
/* `.lb > .lb-bg` and not `.lb-bg`, and this is not fussiness. `.lb-bg` and
   `.lb > *` have identical specificity (0,1,0), so the later rule won and the
   backdrop was being set back to position:relative. That turned it from a
   full-screen cover into a fourth grid item: the page showed straight through
   the viewer — the nav printed over the wallpaper's title — and the stray row
   pushed the image out of the 1fr track, leaving a large empty violet block
   where the picture should have been. One selector, three symptoms. */
.lb > .lb-bg{position:absolute;inset:0;background:rgba(4,4,7,.94);
  backdrop-filter:blur(22px) saturate(1.15)}
.lb > *{position:relative}
.lb-top{display:flex;justify-content:space-between;align-items:center;gap:16px;
  padding:clamp(14px,2vw,24px) clamp(16px,3vw,36px)}
.lb-title{display:flex;flex-direction:column;gap:4px}
.lb-title b{color:var(--white);font-size:17px;font-variation-settings:"wdth" 108,"wght" 750}
.lb-title span{font-size:10.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--slate)}
.lb-x{width:44px;height:44px;flex:none;border:1px solid var(--edge);background:transparent;
  color:var(--steel);cursor:pointer;font-size:19px;line-height:1;
  transition:color .25s,border-color .25s,transform .3s var(--ease)}
.lb-x:hover{color:var(--white);border-color:var(--iris);transform:rotate(90deg)}
.lb-stage{display:grid;place-items:center;padding:0 clamp(10px,3vw,40px);min-height:0}
/* min-height:0 is load-bearing. .lb-stage is a grid, and a grid item's
   default min-height:auto is an automatic MINIMUM size that refuses to shrink
   below the content — so a 3840x3840 master resolved to 1360x1360 inside a
   473px row and max-height:100% was silently ignored. The picture then ran
   under the download chooser, which looked like the bar was wrongly
   positioned rather than like the image was the wrong size. Measured in the
   browser: 1360px before, 473px after. */
.lb-img{max-width:100%;max-height:100%;min-height:0;object-fit:contain;display:block;
  border:1px solid var(--line);box-shadow:0 50px 140px rgba(0,0,0,.75);
  transform:scale(.94);opacity:0;transition:transform .5s var(--ease),opacity .4s}
/* The open wallpaper turns with the pointer. This is the moment the site is
   actually for — someone looking at one picture, full screen — so it is the
   one place worth spending a real perspective transform on.

   The variables default to zero, which composes to exactly the identity this
   rule had before, so an unset lightbox behaves as it always did and no-JS
   and reduced-motion visitors see the picture flat and still. */
.lb.on .lb-img{
  transform:perspective(1800px)
            rotateX(var(--lrx,0deg)) rotateY(var(--lry,0deg))
            scale(var(--lsc,1));
  opacity:1}
/* Pointer tracking has to be immediate; the .5s ease above is for the open. */
.lb.is-lean .lb-img{transition:transform .1s linear,opacity .4s}
@media (prefers-reduced-motion:reduce){
  .lb.on .lb-img{transform:none}}
.lb-bottom{display:flex;justify-content:space-between;align-items:center;gap:14px;
  flex-wrap:wrap;padding:clamp(14px,2vw,26px) clamp(16px,3vw,36px)}
.lb-dl{display:flex;gap:8px;flex-wrap:wrap}
.lb-dl a{padding:11px 18px;border:1px solid var(--edge);color:var(--silver);font-size:11.5px;
  letter-spacing:.16em;text-transform:uppercase;text-decoration:none;
  transition:color .25s,border-color .25s,background .25s}
.lb-dl a:hover{color:var(--tone);border-color:var(--tone);
  background:color-mix(in srgb,var(--tone) 10%,transparent)}
.lb-arrows{display:flex;gap:8px}
.lb-arrows button{width:44px;height:44px;border:1px solid var(--edge);background:transparent;
  color:var(--steel);cursor:pointer;transition:color .25s,border-color .25s}
.lb-arrows button:hover{color:var(--white);border-color:var(--iris)}
.lb-arrows button:disabled{opacity:.3;cursor:not-allowed}
body.lb-open{overflow:hidden}
@media(prefers-reduced-motion:reduce){
  .lb,.lb-img{transition:none}.lb-img{transform:none;opacity:1}}


/* ==========================================================================
   Living plates
   The card art is drawn (art_scenes.py) and then moved here. Rain falls, fire
   flickers, water rolls, stars breathe. Nothing runs until kv.js marks the
   card .on — with 190 of them on one page, animating the ones nobody is
   looking at is how you turn a wallpaper site into a space heater.

   The site's own rule is that motion is scroll- or pointer-driven and nothing
   loops. These loop, deliberately: a picture of rain that does not rain is
   just a drawing of lines. They are slow, low-amplitude and off entirely
   under prefers-reduced-motion.
   ========================================================================== */
.plate-live{--lit:0}
.plate-live .plate-svg [class^="an-"]{will-change:transform,opacity}

/* every layer breathes a little, at a rate set by its depth — this is what
   stops a still scene reading as clip art */
.plate-live.on [data-depth]{animation:anBreathe 11s ease-in-out infinite alternate}
.plate-live.on [data-depth="1.35"],
.plate-live.on [data-depth="1.4"],
.plate-live.on [data-depth="1.5"]{animation-duration:7s}
.plate-live.on [data-depth=".2"],
.plate-live.on [data-depth=".12"]{animation-duration:16s}
@keyframes anBreathe{from{transform:translate3d(-1.2px,.6px,0)}
  to{transform:translate3d(1.2px,-.6px,0)}}

/* the named behaviours. Each overrides the breathe above. */
.plate-live.on .an-drift{animation:anDrift 14s ease-in-out infinite alternate}
@keyframes anDrift{from{transform:translate3d(-7px,0,0)}to{transform:translate3d(7px,-3px,0)}}

.plate-live.on .an-rain{animation:anRain 1.1s linear infinite}
@keyframes anRain{from{transform:translate3d(2px,-16px,0);opacity:0}
  18%{opacity:1}82%{opacity:1}to{transform:translate3d(-2px,16px,0);opacity:0}}

.plate-live.on .an-fall{animation:anFall 6.5s linear infinite}
@keyframes anFall{from{transform:translate3d(-4px,-14px,0);opacity:0}
  22%{opacity:1}78%{opacity:1}to{transform:translate3d(4px,16px,0);opacity:0}}

.plate-live.on .an-roll{animation:anRoll 6s ease-in-out infinite alternate}
@keyframes anRoll{from{transform:translate3d(-6px,1px,0)}to{transform:translate3d(6px,-1px,0)}}

.plate-live.on .an-flick{animation:anFlick 2.2s steps(6,end) infinite;transform-origin:50% 80%}
@keyframes anFlick{0%{transform:scale(1,1) translateY(0);opacity:.85}
  25%{transform:scale(1.05,.94) translateY(1px);opacity:1}
  50%{transform:scale(.95,1.06) translateY(-1px);opacity:.8}
  75%{transform:scale(1.03,.97);opacity:1}
  100%{transform:scale(1,1);opacity:.85}}

.plate-live.on .an-pulse{animation:anPulse 4.2s ease-in-out infinite alternate;
  transform-origin:50% 50%;transform-box:fill-box}
@keyframes anPulse{from{transform:scale(.86);opacity:.55}to{transform:scale(1.1);opacity:1}}

.plate-live.on .an-twinkle{animation:anTwinkle 3.4s ease-in-out infinite alternate}
@keyframes anTwinkle{from{opacity:.35}to{opacity:1}}

.plate-live.on .an-dash{animation:anDash 1.5s linear infinite}
@keyframes anDash{from{transform:translate3d(-26px,0,0);opacity:0}
  20%{opacity:1}80%{opacity:1}to{transform:translate3d(120px,0,0);opacity:0}}

.plate-live.on .an-wave{animation:anWave 9s ease-in-out infinite alternate}
@keyframes anWave{from{transform:translate3d(-8px,2px,0) scaleY(.94)}
  to{transform:translate3d(8px,-2px,0) scaleY(1.06)}}

.plate-live.on .an-spin{animation:anSpin 48s linear infinite;
  transform-origin:50% 50%;transform-box:fill-box}
@keyframes anSpin{to{transform:rotate(360deg)}}

/* hovering a card leans into it: the picture warms and speeds up slightly */
.cat:hover .plate-live.on [class^="an-"],
.sub:hover .plate-live.on [class^="an-"]{animation-duration:.85s}
.cat:hover .plate-live.on .an-drift,.sub:hover .plate-live.on .an-drift,
.cat:hover .plate-live.on .an-spin,.sub:hover .plate-live.on .an-spin{animation-duration:9s}
.plate-live .plate-svg{transition:transform 1.4s var(--ease),filter .6s}
.cat:hover .plate-live .plate-svg,.sub:hover .plate-live .plate-svg{
  transform:scale(1.06);filter:saturate(1.25) brightness(1.12)}

/* the second colour is what stops every card being violet. It is set per
   card by build.py from the topic palette. */
.plate-live{color:var(--tone,var(--iris))}
.plate-live .plate-glow{background:radial-gradient(220px circle at var(--px,50%) var(--py,50%),
  color-mix(in srgb,var(--tone2,var(--iris)) 26%,transparent),transparent 68%)}

@media(prefers-reduced-motion:reduce){
  .plate-live.on [data-depth],.plate-live.on [class^="an-"]{animation:none!important}
  .cat:hover .plate-live .plate-svg,.sub:hover .plate-live .plate-svg{transform:none}
}


/* ==========================================================================
   Type, sharpened
   The system was already one superfamily on a width axis. What it was missing
   was the finish: display type that catches light instead of sitting flat,
   headings that break where a person would break them, and small type with
   enough tracking to feel deliberate rather than default.

   No second typeface. The brand rule stands — hierarchy still comes out of
   Archivo's width axis. This is the same voice, better lit.
   ========================================================================== */

/* Display type is filled with a vertical gradient rather than a flat white:
   bright at the cap line, cooling into violet at the baseline, as though the
   light in the room is above it. -webkit-text-fill-color rather than color,
   so the many places that set `color:var(--white)` on a .d1 do not silently
   flatten it back out. */
.d1,.d2,.kh-title,.hero-copy .d1{
  background-image:linear-gradient(174deg,#FFFFFF 6%,#F3EDFF 38%,#C9A0FA 78%,#9B6BE0 100%);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;
  /* a gradient needs a painted box; keep it tight to the glyphs */
  padding-bottom:.04em}
/* one caveat: gradient text cannot also carry a text-shadow glow, so the glow
   goes behind it instead, on the block itself */
.d1{filter:drop-shadow(0 0 42px rgba(174,98,250,.22))}
.d2{filter:drop-shadow(0 0 30px rgba(174,98,250,.16))}
/* If background-clip:text is not supported, transparent fill means every
   display heading on the site renders as nothing. A page that animates badly
   is survivable; a page with no headings is not — same rule as the reveal
   engine. Put the colour back rather than trusting the browser. */
@supports not ((-webkit-background-clip:text) or (background-clip:text)){
  .d1,.d2,.kh-title,.hero-copy .d1{-webkit-text-fill-color:currentColor;
    background-image:none;filter:none}}

/* Break headings like a person, not like a container. One line of CSS and
   the single biggest legibility win available to a display face. */
h1,h2,h3,h4,.d1,.d2,.d3{text-wrap:balance}
p,.lede,.cat-peek,.sub-blurb{text-wrap:pretty}

/* Optical tracking: big type wants less, small type wants more. */
.d1{letter-spacing:-.042em}
.d2{letter-spacing:-.032em}
.label{letter-spacing:.36em;font-variation-settings:"wdth" 96,"wght" 600}
.lede{color:var(--steel);line-height:1.72;font-variation-settings:"wdth" 102,"wght" 380}
.hero-tag{font-variation-settings:"wdth" 98,"wght" 520}

/* Body copy at a comfortable measure and a slightly warmer weight. */
body{font-variation-settings:"wdth" 100,"wght" 380}
strong,b{font-variation-settings:"wdth" 104,"wght" 700}

/* A light travels across a display heading once, as it arrives. It is tied to
   the reveal class, so it happens on scroll like everything else and never
   loops. */
.mask-head.in{animation:typeSheen 1.5s var(--ease) .25s both}
@keyframes typeSheen{
  from{background-position:0 -1.6em}
  to{background-position:0 0}}

/* Links inside prose get a violet underline that draws itself. */
.lede a,.prose a,p a{color:var(--iris-lt);text-decoration:none;
  background-image:linear-gradient(var(--iris),var(--iris));
  background-repeat:no-repeat;background-position:0 100%;background-size:0 1px;
  transition:background-size .45s var(--ease),color .3s}
.lede a:hover,.prose a:hover,p a:hover{background-size:100% 1px;color:var(--white)}

/* The two supporting accents, used only where something is genuinely a
   different kind of thing: a live count, a warning, a second state. */
.accent-cy{color:var(--cyan)}
.accent-ro{color:var(--rose)}

@media(prefers-reduced-motion:reduce){
  .mask-head.in{animation:none}
  .lede a,.prose a,p a{transition:none}}


/* ==========================================================================
   Real media
   The moment a real picture or a video lands in assets/wallpapers/, the card
   stops being a drawing and becomes that. This is the treatment that makes an
   uploaded image arrive looking like it belongs to this site rather than like
   a photo dropped onto it: a slow drift, a violet grade, a vignette, and the
   same glow every other card has.

   None of it is destructive — it is all overlay and filter, so the file you
   uploaded is still the file people download.
   ========================================================================== */
.plate-real{overflow:hidden;background:#05040A}
.plate-real .plate-img{width:100%;height:100%;object-fit:cover;display:block;
  transform:scale(1.08);
  filter:saturate(1.06) contrast(1.05) brightness(.94)}

/* the drift: a very slow push in and across, so a still image is never still.
   Only while the card is on screen — kv.js owns the .on class. */
.plate-real.on .plate-img{animation:plateDrift 26s ease-in-out infinite alternate}
@keyframes plateDrift{
  from{transform:scale(1.08) translate3d(-1.4%,0.9%,0)}
  to{transform:scale(1.17) translate3d(1.4%,-0.9%,0)}}

/* the grade: the house violet laid over the top, plus a vignette so the card
   has a centre. This is what stops twelve different anime screenshots from
   looking like twelve different websites. */
.plate-grade{position:absolute;inset:0;pointer-events:none;
  background:
    linear-gradient(180deg,rgba(5,4,9,.12) 0%,rgba(5,4,9,.62) 88%),
    radial-gradient(120% 90% at 50% 40%,transparent 42%,rgba(5,4,9,.72) 100%),
    linear-gradient(160deg,color-mix(in srgb,var(--tone,var(--iris)) 22%,transparent),
      transparent 46%,color-mix(in srgb,var(--tone2,var(--iris-dp)) 26%,transparent));
  mix-blend-mode:normal;opacity:.92;transition:opacity .6s var(--ease)}
.cat:hover .plate-grade,.sub:hover .plate-grade,.cell-art:hover .plate-grade{opacity:.7}
.cat:hover .plate-real .plate-img,.sub:hover .plate-real .plate-img,
.cell-art:hover .plate-real .plate-img{filter:saturate(1.2) contrast(1.08) brightness(1.06)}

/* video covers behave exactly like image ones */
.plate-vid .plate-img{object-fit:cover}
.plate-vid.on .plate-img{animation:none}

@media(prefers-reduced-motion:reduce){
  .plate-real.on .plate-img{animation:none;transform:scale(1.04)}}


/* ==========================================================================
   Kiro, alive — behaviour only
   Nothing here touches his face. The rule (CLAUDE.md) is that an overlay on
   flat art must survive a two-percent placement error, and no eyelid, pupil
   or catchlight does. So none of this paints on him: it moves him, lights
   him, and gives him weight.

   Six things, all driven by where you are and what you are doing:
     breath    a slow rise and fall, always running
     turn      the whole figure rotates toward the pointer
     light     a violet rim that sits where your cursor is
     sheen     a highlight that travels his edge as he turns
     attend    he leans toward whatever you are about to click
     weight    he lags behind a fast scroll, then settles
   ========================================================================== */

/* --lit is how close you are, --turn is which way. Both written by kv.js. */
[data-rig]{--lit:0;--turn:0}

/* The light on him. A large soft violet source positioned at the pointer,
   screen-blended so it reads as light falling on him rather than a shape
   drawn over him — which is exactly the test the face overlays failed. */
.rig-light{position:absolute;inset:-10%;pointer-events:none;mix-blend-mode:screen;
  opacity:calc(.18 + var(--lit,0) * .55);
  background:radial-gradient(38% 34% at var(--lx,60%) var(--ly,34%),
    rgba(212,180,255,.5),rgba(174,98,250,.28) 42%,transparent 72%);
  transition:opacity .7s var(--ease)}

/* The sheen: a hard, narrow band of light that slides along him as he turns.
   It is masked to his silhouette by sitting inside the rig, and it only shows
   while he is actually moving toward you. */
.rig-sheen{position:absolute;inset:0;pointer-events:none;mix-blend-mode:screen;
  opacity:calc(var(--lit,0) * .5);
  background:linear-gradient(calc(102deg + var(--turn,0) * 26deg),
    transparent 40%,rgba(233,213,255,.28) 50%,transparent 60%);
  transform:translate3d(calc(var(--turn,0) * 5%),0,0);
  transition:opacity .6s var(--ease),transform 1s var(--ease)}

/* He turns. Small numbers on purpose — past about four degrees a flat image
   stops reading as a person turning and starts reading as a picture skewing. */
[data-rig].alive{
  transform:rotate(calc(var(--turn,0) * 1.1deg))
            translate3d(calc(var(--turn,0) * -9px),calc(var(--lit,0) * -6px),0)
            scale(calc(1 + var(--lit,0) * .012));
  transition:transform 1.3s var(--ease)}

/* …and he has weight: a fast scroll leaves him behind for a moment. */
[data-rig].alive{--drag:0}
[data-rig].alive .rig-body{transform:translate3d(0,calc(var(--drag,0) * 1px),0)
  rotate(calc(var(--drag,0) * .04deg));transition:transform .9s var(--ease)}

/* He attends to what you are about to press. kv.js adds .attend while the
   pointer is over a button or a card, and he leans that way. */
[data-rig].attend{filter:brightness(1.08) saturate(1.12)}

@media(prefers-reduced-motion:reduce){
  [data-rig].alive,[data-rig].alive .rig-body{transform:none;transition:none}
  .rig-light,.rig-sheen{display:none}}


/* ==========================================================================
   Motion everywhere
   The rule the site started with — scroll- or pointer-driven, nothing loops —
   was there to stop the page becoming a screensaver. It stays as the default.
   What was missing was the other half: every element that can be touched
   should answer when you touch it. None of the below runs on a timer; all of
   it is a response to something you did.
   ========================================================================== */

/* ------------------------------------------------------- buttons answer */
/* a hard light crosses the button as the pointer enters it */
.btn{--shx:50%}
.btn::after{content:"";position:absolute;inset:0;pointer-events:none;z-index:1;
  background:linear-gradient(104deg,transparent 38%,rgba(255,255,255,.22) 50%,transparent 62%);
  transform:translateX(-120%);transition:transform .7s var(--ease);mix-blend-mode:screen}
.btn:hover::after{transform:translateX(120%)}
.btn:active{transform:translateY(1px) scale(.985);transition-duration:.09s}
.btn-solid:hover{box-shadow:0 0 44px var(--iris-glow),0 10px 40px rgba(0,0,0,.5)}

/* ---------------------------------------------------------- nav answers */
.nav-menu a{position:relative;transition:color .3s var(--ease)}
.nav-menu a:hover{color:var(--white);text-shadow:0 0 18px var(--iris-glow)}
.nav-logo .mk{transition:transform .8s var(--ease),filter .5s}
.nav-logo:hover .mk{transform:rotate(-8deg) scale(1.08);
  filter:drop-shadow(0 0 14px var(--iris-glow))}
.nav-logo .wm{transition:letter-spacing .6s var(--ease)}
.nav-logo:hover .wm{letter-spacing:.02em}

/* ------------------------------------------------------- cards answer */
/* the lift is already there; this is the light that comes with it */
.cat,.sub,.wp,.cell{position:relative}
.cat::after,.sub::after{content:"";position:absolute;inset:0;pointer-events:none;
  opacity:0;transition:opacity .5s var(--ease);
  background:radial-gradient(300px circle at var(--mx,50%) var(--my,50%),
    color-mix(in srgb,var(--tone,var(--iris)) 14%,transparent),transparent 66%)}
.cat:hover::after,.sub:hover::after{opacity:1}
/* the name slides a hair when you hover — small enough to feel, not to see */
.cat-top b,.sub-top b{transition:color .3s,transform .5s var(--ease)}
.cat:hover .cat-top b,.sub:hover .sub-top b{transform:translateX(3px)}

/* --------------------------------------------------- inputs answer */
.input:hover,.in:hover{border-color:color-mix(in srgb,var(--iris) 40%,var(--edge))}
.input,.in{transition:border-color .3s,box-shadow .3s,background .3s}
.input:focus{background:var(--raise)}

/* -------------------------------------------------- section arrivals */
/* a hairline that draws itself across the top of a section as it arrives */
.sec-line{position:relative}
.sec-line::before{content:"";position:absolute;left:0;top:-1px;height:1px;width:0;
  background:linear-gradient(90deg,var(--iris),transparent);
  transition:width 1.6s var(--ease)}
.sec-line.lit::before{width:min(420px,60%)}

/* the label above a heading gets a leading rule that grows */
.head .label,.label.mask-line{position:relative;padding-left:0}
.head .label::before{content:"";position:absolute;left:-14px;top:50%;width:0;height:1px;
  background:var(--iris);transition:width .9s var(--ease) .2s}
.head .label.in::before{width:10px}

/* -------------------------------------------------------- the marquee */
.marq-track{will-change:transform}

/* ------------------------------------------------------ crumbs, chips */
.chip,.wp-f{position:relative;overflow:hidden}
.chip::before,.wp-f::before{content:"";position:absolute;inset:0;background:var(--iris-dim);
  transform:scaleX(0);transform-origin:0 50%;transition:transform .45s var(--ease);z-index:-1}
.chip:hover::before,.wp-f:hover::before{transform:scaleX(1)}

/* ------------------------------------------------------- the counters */
.wp-stats b,.ab-num b,.stat b{transition:text-shadow .5s}
.wp-stats:hover b,.ab-num:hover b{text-shadow:0 0 30px var(--iris-glow)}

@media(prefers-reduced-motion:reduce){
  .btn::after,.cat::after,.sub::after{display:none}
  .sec-line::before,.head .label::before{transition:none}
  .nav-logo:hover .mk{transform:none}}


/* The WebGL surface sits exactly where the picture was. The <img> keeps its
   layout (hidden, not removed) so nothing reflows and the fallback is one
   line of JS away if the context is ever lost. */
.kiro-gl{position:absolute;left:0;top:0;width:100%;height:100%;display:block;
  pointer-events:none;z-index:1}
.rig{position:relative}


/* A nav link's transition is the same door moving faster. The ink has to be
   fully closed before the page leaves, so this timing and the setTimeout in
   kv.js are a pair — change one and you get a flash of the old page. */
.gw-quick .gw-ink{transition-duration:.28s}
.gw-quick .gw-ring{animation-duration:.4s}
.gw-quick .gw-name{display:none}

/* ==========================================================================
   The living ground
   Every page now sits on a slow violet weather system instead of flat black:
   three large soft masses drifting at different rates, far enough apart in
   speed that they never repeat visibly. It is one composited layer behind
   everything, costs nothing to animate, and is the difference between a dark
   page and a room with air in it.

   This is the one place the "nothing loops" rule is set aside on every page,
   deliberately and after being asked for it twice. It is behind z-index 0,
   under the grain, and off entirely under reduced motion.
   ========================================================================== */
.weather{position:fixed;inset:-20%;z-index:0;pointer-events:none;opacity:.55;
  filter:blur(60px) saturate(1.2)}
.weather i{position:absolute;display:block;border-radius:50%;mix-blend-mode:screen}
.weather i:nth-child(1){left:2%;top:6%;width:46vw;height:46vw;
  background:radial-gradient(circle,rgba(174,98,250,.30),transparent 66%);
  animation:wx1 48s ease-in-out infinite alternate}
.weather i:nth-child(2){right:0%;top:30%;width:52vw;height:52vw;
  background:radial-gradient(circle,rgba(109,40,217,.28),transparent 66%);
  animation:wx2 63s ease-in-out infinite alternate}
.weather i:nth-child(3){left:28%;bottom:-6%;width:40vw;height:40vw;
  background:radial-gradient(circle,rgba(94,233,240,.12),transparent 66%);
  animation:wx3 77s ease-in-out infinite alternate}
@keyframes wx1{to{transform:translate3d(14vw,8vh,0) scale(1.18)}}
@keyframes wx2{to{transform:translate3d(-16vw,-10vh,0) scale(.86)}}
@keyframes wx3{to{transform:translate3d(10vw,-12vh,0) scale(1.22)}}
@media(prefers-reduced-motion:reduce){.weather{display:none}}

/* format picker and live download inside the lightbox */
.lb-fmt{display:inline-flex;gap:1px;margin-right:10px;background:var(--edge);
  border:1px solid var(--edge)}
.lb-fmt button{padding:9px 14px;border:0;background:var(--panel);color:var(--slate);
  font-size:10.5px;letter-spacing:.18em;cursor:pointer;transition:color .25s,background .25s}
.lb-fmt button:hover{color:var(--white)}
.lb-fmt button.on{background:var(--tone,var(--iris));color:var(--void);
  font-variation-settings:"wdth" 104,"wght" 700}
.lb-live{position:relative;overflow:hidden}
.lb-live::before{content:"";position:absolute;left:0;top:0;bottom:0;width:2px;
  background:var(--tone,var(--iris));box-shadow:0 0 10px var(--tone,var(--iris))}


/* ==========================================================================
   The download chooser
   Twenty-two sizes in one flat row wrapped into three lines of identical grey
   buttons and buried the one anybody actually wanted. It is now two questions
   — what file, then what screen — and only then the sizes, which arrive
   staggered so the change of family is visible rather than a silent swap.
   ========================================================================== */
.lb-dl{display:grid;gap:16px;width:100%}
.lb-step{display:grid;grid-template-columns:auto 1fr;gap:8px 12px;align-items:center}
.lb-step-n{width:22px;height:22px;display:grid;place-items:center;font-size:10.5px;
  color:var(--tone,var(--iris));border:1px solid color-mix(in srgb,var(--tone,var(--iris)) 45%,transparent);
  border-radius:50%}
.lb-step-t{font-size:10px;letter-spacing:.26em;text-transform:uppercase;color:var(--slate)}
.lb-step>div{grid-column:1/-1}

/* step one — a real segmented control, not two more grey buttons */
.lb-seg{display:inline-flex;gap:1px;background:var(--edge);border:1px solid var(--edge)}
.lb-seg button{padding:10px 20px;border:0;background:var(--panel);color:var(--slate);
  cursor:pointer;font-size:12px;letter-spacing:.14em;text-transform:uppercase;
  display:flex;flex-direction:column;gap:3px;align-items:flex-start;
  transition:background .3s var(--ease),color .3s}
.lb-seg button em{font-style:normal;font-size:9.5px;letter-spacing:.08em;
  text-transform:none;color:var(--faint);transition:color .3s}
.lb-seg button:hover{color:var(--white);background:var(--raise)}
.lb-seg button.on{background:var(--tone,var(--iris));color:var(--void)}
.lb-seg button.on em{color:rgba(5,4,9,.6)}

/* step two — the families */
.lb-fam{display:flex;gap:8px;flex-wrap:wrap}
.lb-fam button{padding:9px 18px;border:1px solid var(--edge);background:transparent;
  color:var(--steel);cursor:pointer;font-size:11.5px;letter-spacing:.14em;
  text-transform:uppercase;position:relative;overflow:hidden;
  transition:color .3s,border-color .3s}
.lb-fam button::before{content:"";position:absolute;inset:0;z-index:-1;
  background:color-mix(in srgb,var(--tone,var(--iris)) 16%,transparent);
  transform:scaleX(0);transform-origin:0 50%;transition:transform .4s var(--ease)}
.lb-fam button:hover{color:var(--white);border-color:var(--slate)}
.lb-fam button:hover::before{transform:scaleX(1)}
.lb-fam button.on{color:var(--tone,var(--iris));
  border-color:color-mix(in srgb,var(--tone,var(--iris)) 60%,transparent)}
.lb-fam button.on::before{transform:scaleX(1)}

/* step three — the sizes, arriving one after another */
.lb-sizes{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:8px}
.lb-sizes a{display:flex;align-items:baseline;gap:10px;padding:13px 16px;
  border:1px solid var(--edge);text-decoration:none;position:relative;
  transition:border-color .3s,background .3s,transform .35s var(--ease);
  animation:sizeIn .42s var(--ease) both;animation-delay:calc(var(--i,0) * 38ms)}
@keyframes sizeIn{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}
.lb-sizes a b{color:var(--white);font-size:13px;
  font-variation-settings:"wdth" 104,"wght" 650}
.lb-sizes a span{color:var(--slate);font-size:11px;letter-spacing:.06em;margin-left:auto}
.lb-sizes a i{font-style:normal;color:var(--tone,var(--iris));opacity:0;
  transform:translateY(-3px);transition:opacity .3s,transform .3s var(--ease)}
.lb-sizes a:hover{border-color:var(--tone,var(--iris));transform:translateY(-2px);
  background:color-mix(in srgb,var(--tone,var(--iris)) 9%,transparent)}
.lb-sizes a:hover i{opacity:1;transform:none}
.lb-sizes a:hover b{color:var(--tone,var(--iris))}

.lb-live{display:inline-flex;align-items:center;gap:8px;padding:12px 18px;
  border:1px solid color-mix(in srgb,var(--cyan) 45%,transparent);color:var(--cyan);
  text-decoration:none;font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;
  justify-self:start;transition:background .3s,transform .3s var(--ease)}
.lb-live:hover{background:color-mix(in srgb,var(--cyan) 12%,transparent);
  transform:translateY(-2px)}

/* the bottom bar now holds a stack, so it lays out as one */
.lb-bottom{align-items:flex-end}
@media(max-width:640px){
  .lb-sizes{grid-template-columns:1fr}
  .lb-bottom{flex-direction:column;align-items:stretch}}
@media(prefers-reduced-motion:reduce){
  .lb-sizes a{animation:none}
  .lb-sizes a:hover,.lb-live:hover{transform:none}}


/* ==========================================================================
   Peek
   A tile showing one picture tells you almost nothing about a collection of
   forty. Hovering one cross-fades through the next few, so you can see what
   is actually inside without opening it.

   The extra images have no src until the pointer arrives — kv.js fills it in
   from data-shot on first hover. A category page carries forty tiles, and
   eagerly loading five images each would undo the whole thumbnail fix.
   ========================================================================== */
.plate-shot{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  opacity:0;transition:opacity .55s var(--ease)}
.plate-shot.on{opacity:1}
/* the base picture fades out only while a peek is showing, so the tile is
   never blank between two shots */
.plate-real[data-shots].peeking>.plate-img:first-of-type{opacity:0;
  transition:opacity .55s var(--ease)}

/* a quiet mark that there is more behind this one, so the cross-fade is
   discoverable instead of a surprise */
.plate-real[data-shots]::after{content:"";position:absolute;right:10px;bottom:10px;
  width:6px;height:6px;border-radius:50%;background:var(--tone,var(--iris));
  box-shadow:0 0 0 3px rgba(5,4,9,.5),10px 0 0 0 rgba(239,235,247,.35),
    20px 0 0 0 rgba(239,235,247,.18);
  opacity:.85;transition:opacity .4s;z-index:2}
.cat:hover .plate-real[data-shots]::after,
.sub:hover .plate-real[data-shots]::after{opacity:0}

@media(prefers-reduced-motion:reduce){
  .plate-shot{transition:none}
  .plate-real[data-shots]::after{display:none}}

/* ------------------------------------------------------ lightbox slider ----
   One control, always visible, no overlay over the picture.

   This replaced a radial dial. The dial hid every option until it was opened,
   covered the wallpaper while open, and took two interactions to reach a
   download. A slider shows the whole range at once and costs one drag. */
.lb-bar{display:flex;align-items:flex-end;justify-content:space-between;
  gap:clamp(14px,3vw,34px);padding:14px clamp(14px,3vw,32px) 18px}
.lb-bar-left{display:flex;align-items:flex-end;gap:clamp(12px,2vw,22px);
  flex:1;min-width:0;flex-wrap:wrap}

.lb-pick{flex:1;min-width:min(420px,100%)}
.lb-pick-head{display:flex;align-items:baseline;gap:10px;margin-bottom:7px}
.lb-pick-head b{font-size:15px;color:var(--white);
  font-variation-settings:"wdth" 110,"wght" 750}
.lb-pick-head span{font-size:12px;color:var(--slate);letter-spacing:.05em}

/* The track. accent-color styles the native thumb and fill in one line and
   keeps the control keyboard- and screen-reader-native, which a div-built
   slider would have thrown away. */
.lb-range{-webkit-appearance:none;appearance:none;width:100%;height:4px;
  background:var(--line);outline:none;cursor:pointer;accent-color:var(--tone)}
.lb-range::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;
  border-radius:50%;background:var(--tone);cursor:grab;
  box-shadow:0 0 0 4px color-mix(in srgb,var(--tone) 26%,transparent);
  transition:transform .2s var(--ease)}
.lb-range::-webkit-slider-thumb:active{cursor:grabbing;transform:scale(1.15)}
.lb-range::-moz-range-thumb{width:18px;height:18px;border:0;border-radius:50%;
  background:var(--tone);cursor:grab}

.lb-ticks{display:flex;justify-content:space-between;margin-top:8px;gap:6px}
.lb-ticks button{background:none;border:0;padding:0;cursor:pointer;
  font-size:10px;letter-spacing:.09em;color:var(--slate);
  text-transform:uppercase;transition:color .2s;white-space:nowrap}
.lb-ticks button:hover{color:var(--steel)}
.lb-ticks button.on{color:var(--tone)}

.lb-seg{display:flex;border:1px solid var(--line);align-self:flex-end}
.lb-seg button{padding:9px 13px;background:none;border:0;cursor:pointer;
  color:var(--slate);font-size:11px;letter-spacing:.1em}
.lb-seg button.on{background:var(--tone);color:var(--ink)}

.lb-get{display:inline-flex;align-items:center;gap:9px;padding:13px 26px;
  border:1px solid var(--tone);text-decoration:none;
  background:color-mix(in srgb,var(--tone) 16%,transparent);color:var(--white);
  font-variation-settings:"wdth" 110,"wght" 750;letter-spacing:.06em;
  align-self:flex-end;white-space:nowrap;
  transition:background .3s,transform .3s var(--ease)}
.lb-get:hover{background:color-mix(in srgb,var(--tone) 30%,transparent);
  transform:translateY(-2px)}

@media(max-width:760px){
  .lb-bar{flex-direction:column;align-items:stretch;gap:12px}
  .lb-ticks button{font-size:9px;letter-spacing:.04em}
  .lb-get{justify-content:center}
}
