/* ============================================================
   Type. Two variable faces, self hosted, latin only, 72KB total.

   Why self hosted rather than a font CDN: a third party font request is a third party
   request, it blocks first paint on a DNS lookup we do not control, and this site sells
   speed. Why variable: one file per face covers every weight the site uses, which is
   cheaper than shipping four static cuts.

   The pairing was Space Grotesk plus Inter until 2026-08-18. Both are competent and both
   are everywhere, which is the problem: a studio selling design should not be wearing the
   two faces that ship as the default in every AI generated landing page. They also read
   austere, and the brief for That Crypto Bro is fun as well as premium.

   Bricolage Grotesque carries every heading, price and stat. It is a deliberately uneven
   grotesque, tight apertures and a slightly wonky lowercase, so it has personality at
   64px without turning into a novelty face at 18px. Its digits are the reason it survived
   the audition: this whole site argues in numbers, and they are heavy, even width and
   legible small. The variable file is pinned to optical size 48 and width 100, which is
   what keeps it at 41KB instead of the 77KB the full optical range costs.
   Instrument Sans carries body copy, where the only job is being read at 17px on a dark
   ground. It is narrower than Inter, so the same measure fits more words per line.

   Both are SIL Open Font License 1.1. The licences ship next to the files, in
   site/fonts/OFL-BricolageGrotesque.txt and site/fonts/OFL-InstrumentSans.txt, because
   self hosting an OFL face without its licence is a licence violation.
   ============================================================ */
@font-face{
  font-family:"Bricolage Grotesque"; font-style:normal; font-weight:400 800;
  font-stretch:100%; font-display:swap;
  src:url(/fonts/bricolage-latin.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
                U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,
                U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:"Instrument Sans"; font-style:normal; font-weight:400 700;
  font-stretch:100%; font-display:swap;
  src:url(/fonts/instrument-latin.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
                U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,
                U+2212,U+2215,U+FEFF,U+FFFD;
}

/* ============================================================
   Design system.
   Principle: the site is monochrome. The 76 screenshots supply
   every bit of colour. One saturated accent, used only for the
   action we want clicked.
   ============================================================ */

:root{
  --ink:#0B0B0C;
  --ink-2:#141417;
  --ink-3:#1E1E22;
  --line:#2A2A30;
  --paper:#F6F4F0;
  --mute:#9A9AA4;
  /* Was #7D7D87, which measured 4.08:1 against --ink-3 and so failed the 4.5:1 AA
     floor everywhere it sat on a raised card: the strikethrough anchor price, the FAQ
     meta, the crumb, the gallery captions. #8A8A95 clears 4.5 on all three surfaces
     (4.87 on --ink-3, the worst case) and is still visibly quieter than --mute. */
  --mute-2:#8A8A95;
  --body:#DAD7D1;        /* body copy that is not full paper white */
  --body-2:#C9C7C2;      /* secondary body: nav links, FAQ answers, pills */
  --foot:#B4B2AE;        /* footer links */
  --line-2:#4A4A54;      /* the lifted border a card takes on hover */
  --accent:#FF5A1F;
  --accent-ink:#0B0B0C;
  --ok:#4ADE80;

  /* The second accent. Deliberately never used on a surface larger than a dot, a
     rule, or a caret: the brightest and largest saturated element on any screen has
     to stay the button we want clicked, and two loud colours competing for that job
     is how a premium page turns into a memecoin page. Lime earns its place in the
     small stuff, where it does the entire job of making the page feel alive. */
  --accent-2:#C8FF4D;
  --accent-2-dim:#8FBF2E;

  --w:1180px;
  --gap:clamp(48px,4.6vw,76px);
  --r:14px;

  --f:  "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;
  --fd: "Bricolage Grotesque", "Instrument Sans", -apple-system, BlinkMacSystemFont,
        "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --fm: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* ============================================================
   Depth.

   Flat #0B0B0C reads cheap at 1440px: a large unbroken black field looks like an
   unstyled background rather than a decision. Two things fix it without adding a
   single request. A fixed film of monochrome noise at 3.5 percent, generated by
   feTurbulence inside a data URI, gives the black a surface. A soft warm bloom
   behind the hero gives the fold a light source, so the headline sits on something
   instead of floating on nothing.

   Both are pointer-events:none and sit under everything, and both are dropped for
   anyone who asked for reduced motion or high contrast.
   ============================================================ */
body::before{
  content:""; position:fixed; inset:0; z-index:0; pointer-events:none; opacity:.035;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}
@media (prefers-contrast:more){ body::before{display:none} }
.site-header,main,footer{position:relative; z-index:1}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%; scroll-behavior:smooth}
body{
  margin:0; background:var(--ink); color:var(--paper);
  font-family:var(--f); font-size:17px; line-height:1.6;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  overflow-x:hidden;
  /* Project summaries carry real contract addresses, and a 44 character base58 string
     is one unbreakable word. Six project pages were 66 to 85px wider than a 390px
     viewport because of it. break-word only splits a word that cannot fit a line on
     its own, so ordinary copy is untouched. */
  overflow-wrap:break-word;
}
img{max-width:100%; height:auto; display:block}
a{color:inherit}

/* ---------- type scale ---------- */
h1,h2,h3,h4{
  margin:0; font-family:var(--fd); font-weight:640;
  letter-spacing:-0.032em; line-height:1.04;
}
h1{font-size:clamp(2.6rem,6.4vw,4.7rem)}
h2{font-size:clamp(1.9rem,4vw,3rem); letter-spacing:-0.03em}
h3{font-size:clamp(1.15rem,2vw,1.5rem); letter-spacing:-0.02em; line-height:1.2}
p{margin:0 0 1.1em}
.lede{font-size:clamp(1.1rem,1.9vw,1.32rem); line-height:1.5; color:var(--body); max-width:60ch}
.eyebrow{
  font-family:var(--fm); font-size:.75rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--mute); margin:0 0 20px
}
/* A lime pip in front of every section eyebrow. One shape, six pixels, repeated on
   every page: it is the cheapest possible way to make the second accent feel like a
   system rather than a decoration, and it turns a line of grey monospace into
   something that reads as a label on a designed object. */
/* The four process steps sit in an auto-fit grid, and "4. You approve, then pay" is
   long enough to wrap where the other three do not, so its body copy started a line
   lower than its neighbours and the row lost its baseline. Two lines of headline
   height is reserved on all four rather than shortening the one label that carries
   the actual promise. */
.steps h3{min-height:2.4em; margin-bottom:2px}

.eyebrow::before{
  content:""; display:inline-block; width:6px; height:6px; border-radius:2px;
  background:var(--accent-2); margin-right:10px; vertical-align:.14em;
}

/* ---------- the sticker system ----------
   The 2026-08-18 audit landed one criticism that was not a bug: the site was competent
   and serious, and every scrap of fun on it was borrowed from inside a client screenshot.
   The brand itself owned a dark ground, one orange, a lime pip and a pair of sunglasses.
   That is restraint, but the brief was "super fun and premium", and restraint alone is
   only the second half of it.

   So the brand gets ONE device, repeated everywhere, and it is the obvious one: this shop
   sells stickers, so everything the brand says about ITSELF is a sticker. Every section
   label, every price, every headline stat is peeled off a sheet and pressed onto the page,
   slightly off-square, with a hard shadow instead of a soft one.

   It is CSS only. No new artwork, nothing generated, nothing that can render as a broken
   asset. The tilt is deliberately tiny, a degree or so: enough that the eye reads "placed
   by hand" and not enough to look like a bug or to cost any legibility. Premium is the
   restraint; the peel is the fun. */
.eyebrow{
  display:inline-block; padding:7px 12px 6px; border-radius:8px;
  background:var(--ink-3); border:1px solid var(--line-2);
  color:var(--text); box-shadow:3px 3px 0 rgba(0,0,0,.45);
  transform:rotate(-1.1deg); transform-origin:left center;
  /* A sticker with one word on its second line reads as a printing mistake. At 390px
     the hero label broke as a 285px line and a 45px line containing only "NAME.".
     balance splits it evenly instead; unsupported browsers ignore it and get today's
     wrap, so this costs nothing where it does not work. */
  text-wrap:balance;
}
.eyebrow::before{width:7px; height:7px; box-shadow:0 0 0 3px rgba(200,255,77,.16)}
/* Alternate the lean so a page of labels reads as a handful of stickers rather than a
   repeated template. Odd sections lean the other way. */
section:nth-of-type(even) .eyebrow{transform:rotate(1deg)}

/* Headline numbers get the same treatment: pressed on, not typeset in. */
.stat{
  background:var(--ink-2); border:1px solid var(--line);
  border-radius:14px; padding:22px 20px 20px;
  box-shadow:4px 4px 0 rgba(0,0,0,.5); transform:rotate(-.7deg);
  transition:transform .18s ease, box-shadow .18s ease;
}
.stat:nth-child(even){transform:rotate(.8deg)}
.stat:hover{transform:rotate(0deg) translateY(-3px); box-shadow:5px 6px 0 rgba(255,90,31,.5)}
.stat .n{color:var(--accent)}

/* The price on a tier is the single thing a buyer is looking for, so it is the one that
   peels furthest off the card. */
.tier .price{
  display:inline-block; padding:2px 12px 4px; border-radius:12px;
  background:var(--ink-3); box-shadow:4px 4px 0 rgba(0,0,0,.45);
  transform:rotate(-1.4deg); transform-origin:left center;
  transition:transform .16s ease, box-shadow .16s ease;
}
.tier:hover .price{transform:rotate(0deg); box-shadow:4px 4px 0 rgba(255,90,31,.55)}
.tier.feat .price{background:var(--accent); color:#0B0B0C}

@media (prefers-reduced-motion:reduce){
  .eyebrow,.stat,.tier .price,section:nth-of-type(even) .eyebrow{transform:none}
  .stat:hover,.tier:hover .price{transform:none}
}
.muted{color:var(--mute)}
.small{font-size:.9rem}
.mono{font-family:var(--fm)}

/* ---------- layout ---------- */
.wrap{max-width:var(--w); margin:0 auto; padding:0 24px}
/* A narrow measure that does NOT recentre.
   .wrap centres itself in the viewport, so `.wrap.narrow` used to pull its whole
   block to the middle of the page. Every service page then alternated between a
   full width section starting at the grid's left edge and a narrow one starting
   130px further in, so the left edge jumped four times on the way down. Reading
   measure and column position are separate decisions: keep the 74ch measure, keep
   the left edge. */
.narrow{max-width:var(--w)}
.narrow > *{max-width:74ch}
.narrow > .cta-row{max-width:none}
/* Two columns only when there is something to put in the second one. The selector used
   to fire on any answer block, so an answer with no sources note beside it opened a
   220px+ empty track and the block sat against a hole. */
@media(min-width:1100px){
  .narrow:has(> .answer):has(> .src){
    display:grid; grid-template-columns:minmax(0,74ch) minmax(220px,1fr);
    column-gap:clamp(28px,3vw,56px); align-items:start;
  }
  .narrow:has(> .answer):has(> .src) > *{grid-column:1; max-width:none}
  .narrow:has(> .answer):has(> .src) > .src{
    grid-column:2; grid-row:1 / span 99; align-self:start;
    margin:32px 0 0; padding-left:20px; border-left:1px solid var(--line);
  }
}
section{padding:var(--gap) 0}
.rule{height:1px; background:var(--line); border:0; margin:0}

/* ---------- nav ---------- */
.nav{
  position:sticky; top:0; z-index:50;
  background:rgba(11,11,12,.86); backdrop-filter:saturate(160%) blur(14px);
  border-bottom:1px solid var(--line);
}
.nav-in{display:flex; align-items:center; gap:26px; height:64px}
.brand{
  font-family:var(--fd); font-weight:680; letter-spacing:-.03em; font-size:1.06rem;
  text-decoration:none; white-space:nowrap;
  display:inline-flex; align-items:center; gap:9px; padding:6px 0;
}
/* height:auto, because the file is 128x154 and 28x34 is not that ratio. */
.brand .mark{width:28px; height:auto; flex:none}
/* The shades tip on hover. Nothing depends on it and nobody will mention it, which is
   the correct weight for a joke in a header you see on all 103 pages. */
.brand .mark{transition:transform .18s cubic-bezier(.2,.8,.3,1.2)}
.brand:hover .mark{transform:rotate(-8deg) scale(1.06)}
@media (prefers-reduced-motion:reduce){ .brand:hover .mark{transform:none} }
.brand b{color:var(--accent)}
.nav-links{display:flex; gap:22px; margin-left:6px; flex:1; min-width:0}
/* padding 9px, not 6px. At 6px these were 28px tall, under the 32px a thumb needs, and
   the header is the one component every page shares. Not pushed to 44px because the
   mobile nav row is already the tallest fixed thing on a phone and the collapse below
   depends on knowing its height. */
.nav-links a{
  text-decoration:none; color:var(--body-2); font-size:.94rem; white-space:nowrap;
  padding:9px 0; border-bottom:1px solid transparent;
}
.nav-links a:hover{color:var(--paper); border-bottom-color:var(--accent)}
.nav-links a[aria-current]{color:var(--paper); border-bottom-color:var(--accent)}
/* No gap override here, deliberately, and the two lost rounds are worth the comment.
   .btn is a flex container with a 9px gap for icon buttons. The header label was a span
   plus a bare text node, so those became two flex items and the button read
   "Message on  Telegram". Zeroing the gap closed it too far, to "Message onTelegram",
   because a flex item's trailing white space is stripped. The fix is in the markup, in
   build/common.py: one wrapper span, one flex item, and the word space is just text. */
.btn.nav-cta{margin-left:auto}

/* ---------- buttons ---------- */
.btn{
  font-family:var(--fd);
  display:inline-flex; align-items:center; gap:9px;
  background:var(--accent); color:var(--accent-ink);
  font-weight:660; font-size:1rem; letter-spacing:-.01em;
  padding:14px 24px; border-radius:11px; text-decoration:none;
  border:1px solid var(--accent); transition:transform .12s ease, filter .12s ease;
}
.btn:hover{filter:brightness(1.08); transform:translateY(-1px)}
.btn.sm{padding:9px 16px; font-size:.9rem; border-radius:9px}
.btn.ghost{
  background:transparent; color:var(--paper); border-color:var(--line);
}
.btn.ghost:hover{border-color:var(--paper); filter:none}
.cta-row{display:flex; flex-wrap:wrap; gap:12px; align-items:center}
.cta-note{font-size:.88rem; color:var(--mute); margin-top:12px}
.cta-note b{color:var(--paper); font-weight:600}

/* ---------- hero ---------- */
.hero{padding:clamp(52px,7vw,92px) 0 clamp(40px,5vw,64px); position:relative}
.hero::before{
  content:""; position:absolute; z-index:-1; pointer-events:none;
  left:-14%; top:-32%; width:78%; height:150%;
  background:radial-gradient(46% 42% at 34% 44%, rgba(255,90,31,.13), transparent 68%);
}
.hero-grid{
  display:grid; grid-template-columns:1.12fr .88fr;
  gap:clamp(28px,4.5vw,60px); align-items:center;
}
.hero-vis{
  display:grid; grid-template-columns:1fr 1fr; gap:14px; height:520px;
  overflow:hidden; position:relative;
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 20%,#000 80%,transparent);
          mask-image:linear-gradient(180deg,transparent,#000 20%,#000 80%,transparent);
}
.hero-col{display:flex; flex-direction:column; gap:14px}
.hero-col.up{animation:scrollup 64s linear infinite}
.hero-col.down{animation:scrollup 64s linear infinite reverse}
.hero-vis img{
  width:100%; border-radius:11px; border:1px solid var(--line);
  /* 16/10 EXACTLY, because that is the ratio the card images are written at. It was 16/11,
     which is taller than the source, so object-fit:cover cropped them HORIZONTALLY and the
     first thing on the page was "APES' SWANKY YACHT CLUB" rendered as "S' SWANKY YACHT
     CLUB". Same fault the service cards had, in the more expensive place. Matching the
     ratio means cover has nothing to crop. */
  aspect-ratio:16/10; object-fit:cover; object-position:top center; background:var(--ink-2);
}
@keyframes scrollup{from{transform:translateY(0)} to{transform:translateY(-50%)}}
@media(max-width:980px){
  .hero-grid{grid-template-columns:1fr}
  /* 290px cut a 16:11 card almost exactly in half with only ~35px of fade over it, so
     the mobile hero opened on a card sliced by a straight horizontal line. 360px clears
     a whole card inside the frame. */
  .hero-vis{height:360px; margin-top:34px}
  .hero-vis{-webkit-mask-image:linear-gradient(180deg,transparent,#000 22%,#000 78%,transparent);
                    mask-image:linear-gradient(180deg,transparent,#000 22%,#000 78%,transparent)}
}
@media (prefers-reduced-motion:reduce){
  .hero-col.up,.hero-col.down{animation:none}
}
.hero h1{max-width:17ch}
.hero .lede{margin-top:22px}
.hero .cta-row{margin-top:30px}

/* Provenance lines. Set quiet on purpose: a citation is there to be checked, not to
   be read, and a source line competing with body copy would be its own kind of
   dishonesty. The left rule is the only thing marking it as apparatus rather than
   argument. */
.src{
  margin-top:22px; padding-left:15px; border-left:2px solid var(--line);
  font-size:.83rem; line-height:1.55; max-width:74ch;
}
.src a{color:var(--body-2); text-decoration:underline; text-underline-offset:2px;
  text-decoration-color:var(--line-2)}
.src a:hover{color:var(--paper); text-decoration-color:var(--accent)}

/* The project tagline. A figure, not a bare blockquote, because the caption is the
   whole point: it says out loud that this is the token's own line and not a review. */
.tagline{
  margin:26px 0 0; border:1px solid var(--line); border-radius:var(--r);
  background:var(--ink-2); padding:16px 18px 15px;
}
/* Labelled first, then quoted. The old shape was an accent bar plus 1.15rem type, which
   is the universal pull-quote signature, so 61 project pages carried something that
   scanned as a client review before the caption underneath could say it was not one.
   A bordered card with a header reads as a spec field, which is what it is. */
.tagline .lbl{
  display:block; font-family:var(--fm); font-size:.64rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--mute-2); margin-bottom:9px;
}
.tagline blockquote{
  margin:0; padding:0; border:0; font-size:1rem; color:var(--body); font-style:italic;
}
.tagline blockquote::before{content:"\201C"}
.tagline blockquote::after{content:"\201D"}
.tagline figcaption{margin-top:9px; font-size:.8rem; color:var(--mute-2)}

/* Palette swatches with their hex printed. */
.swatches{display:flex; gap:14px; margin-top:13px; flex-wrap:wrap}
.swatches .sw{display:flex; flex-direction:column; gap:6px; align-items:flex-start}
.swatches .chip{
  width:38px; height:38px; border-radius:9px; border:1px solid var(--line); display:block;
}
.swatches code{font-family:var(--fm); font-size:.66rem; color:var(--mute-2)}

/* ---------- spec columns ----------
   Packed into balanced columns at build time (see pack_specs in build/pages_core.py),
   because CSS has no masonry: a row-locked grid left 225 to 316px of dead space per page
   and multi-column left a whole empty third column on short pages. Here each column is
   already balanced, so the grid only has to place three pre-built stacks side by side.
   align-items:start keeps a short column short instead of stretching it. */
.specs{display:grid; gap:26px; margin-top:26px; align-items:start;
       grid-template-columns:repeat(3,minmax(0,1fr))}
.specs.c2{grid-template-columns:repeat(2,minmax(0,1fr))}
.specs.c1{grid-template-columns:minmax(0,1fr)}
.speccol{display:flex; flex-direction:column; gap:26px; min-width:0}
.specs h3{margin-top:0}
@media(max-width:820px){ .specs,.specs.c2{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media(max-width:560px){ .specs,.specs.c2{grid-template-columns:minmax(0,1fr)} }

/* ---------- guide rail ----------
   Long form pages ran one 780px column down the middle of a 1440px viewport, which
   left about a third of every screen as empty black for six thousand pixels. The rail
   puts the reader's position in the document and the way to hire someone in that
   space. It collapses below 1080px, where there is no spare column to fill and a
   sticky sidebar would just be a second thing to scroll past.
   ------------------------------------------------------------------ */
.guide-grid{display:grid; grid-template-columns:minmax(0,1fr) 264px; gap:clamp(32px,4vw,64px); align-items:start}
.rail{position:sticky; top:96px; display:flex; flex-direction:column; gap:22px; font-size:.88rem}
.rail-h{
  font-family:var(--fd); font-weight:660; font-size:.82rem; letter-spacing:.02em;
  color:var(--paper); margin:0 0 12px;
}
.rail-toc ol{list-style:none; margin:0; padding:0; border-left:1px solid var(--line)}
.rail-toc li{margin:0}
.rail-toc a{
  display:block; padding:6px 0 6px 15px; margin-left:-1px;
  border-left:1px solid transparent; color:var(--mute); text-decoration:none; line-height:1.4;
}
.rail-toc a:hover{color:var(--paper); border-left-color:var(--accent)}
.rail-cta{
  background:var(--ink-2); border:1px solid var(--line); border-radius:var(--r); padding:18px;
}
.rail-cta p:not(.rail-h){color:var(--body-2); margin:0 0 15px; font-size:.86rem; line-height:1.55}
.rail-list{margin:0 0 14px; padding-left:17px; color:var(--body-2); font-size:.86rem; line-height:1.5}
.rail-list li{margin-bottom:8px}
.rail-list a{color:var(--body-2); text-decoration:underline; text-underline-offset:2px}
.rail-list a:hover{color:var(--paper)}
.rail-cta .btn.small{padding:10px 16px; font-size:.9rem; width:100%; justify-content:center}
.rail-link{
  display:block; margin-top:11px; text-align:center; color:var(--mute);
  font-size:.84rem; text-decoration:underline; text-underline-offset:3px;
}
.rail-link:hover{color:var(--paper)}
/* A heading that is a jump target must not land under the sticky header. */
.prose h2[id]{scroll-margin-top:88px}
@media (max-width:1080px){
  .guide-grid{grid-template-columns:1fr}
  .rail{position:static; flex-direction:row; flex-wrap:wrap; gap:26px}
  .rail-toc{flex:1 1 260px} .rail-cta{flex:1 1 260px}
}
@media (max-width:640px){ .rail{flex-direction:column} }

/* ---------- logo marquee ---------- */
.marquee{
  overflow:hidden; border-block:1px solid var(--line);
  padding:26px 0; background:var(--ink-2);
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
}
.marquee-track{display:flex; gap:56px; width:max-content; animation:slide 78s linear infinite}
.marquee:hover .marquee-track{animation-play-state:paused}
.marquee img{
  height:52px; width:auto; background:var(--paper); border-radius:10px;
  padding:7px 11px; object-fit:contain; opacity:.9;
  transition:opacity .2s, transform .2s;
}
/* These are token logos, and the brand sells sticker packs, so on hover they behave
   like stickers: lift and tilt. It is the one place on the site where the motion is
   there purely because it is enjoyable. */
.marquee img:hover{opacity:1; transform:translateY(-3px) rotate(-3deg) scale(1.06)}
@keyframes slide{from{transform:translateX(0)} to{transform:translateX(-50%)}}
@media (prefers-reduced-motion:reduce){
  .marquee-track{animation:none; flex-wrap:wrap; width:auto; justify-content:center}
}

/* ---------- work grid ---------- */
/* min(310px,100%), not a bare 310px. auto-fill will not shrink a track below its stated
   minimum, so at a 320px viewport the 310px floor plus the wrap padding pushed 14px of
   the card grid off the right edge of /services/websites/. */
.grid{display:grid; gap:20px; grid-template-columns:repeat(auto-fill,minmax(min(310px,100%),1fr))}
/* The card is an <article>, not one big <a>, so a live project can carry a second link
   out to the client's own domain. .stretch covers the tile for the primary link; the
   outbound one sits above it. position:relative is what scopes the stretch. */
.card{
  position:relative; display:block; text-decoration:none; background:var(--ink-2);
  border:1px solid var(--line); border-radius:var(--r); overflow:hidden;
  transition:border-color .16s ease, transform .16s ease;
}
.card:hover{border-color:var(--line-2); transform:translateY(-3px)}
.card a{text-decoration:none; color:inherit}
.card .stretch::after{content:""; position:absolute; inset:0; z-index:1}
.card:focus-within{border-color:var(--line-2)}
a.tag.live-out{
  position:relative; z-index:2; font-family:var(--fm); font-size:.66rem;
  letter-spacing:.02em; text-transform:none; max-width:52%; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap;
  /* 8px vertical, not 3px. This is the only control on a card that is not the card
     itself, so it has to be hittable without hitting the card behind it. */
  padding:8px 10px;
}
a.tag.live-out:hover{background:rgba(74,222,128,.12); border-color:var(--ok)}
.card-img{aspect-ratio:16/10; overflow:hidden; background:var(--ink-3)}
.card-img img{
  width:100%; height:100%; object-fit:cover; object-position:top center;
  transition:transform .3s cubic-bezier(.2,.7,.3,1);
}
/* The screenshot leans in on hover. A 3 percent push inside a clipped frame is enough
   to say the tile is alive without moving the layout by a pixel. */
.card:hover .card-img img{transform:scale(1.03)}
.card-body{padding:15px 17px 17px; display:flex; align-items:baseline; gap:10px}
.card-body h2,.card-body h3{font-size:1.02rem; font-weight:620; flex:1; min-width:0}
.tag{
  font-family:var(--fm); font-size:.66rem; letter-spacing:.08em; text-transform:uppercase;
  color:var(--ok); border:1px solid rgba(74,222,128,.34); border-radius:99px;
  padding:3px 8px; white-space:nowrap;
}

/* ---------- stat band ---------- */
.stats{display:grid; gap:28px; grid-template-columns:repeat(auto-fit,minmax(min(180px,100%),1fr));
       align-items:stretch}
.stats > *{height:100%}
.stat .n{
  font-family:var(--fd); font-size:clamp(2.4rem,4.6vw,3.5rem); font-weight:680;
  letter-spacing:-.045em; line-height:1; display:block;
}
.stat .l{color:var(--mute); font-size:.94rem; margin-top:9px; display:block}

/* ---------- pricing ---------- */
/* align-items:stretch, not start. The four cards ran to 550, 780, 545 and 515px with
   three dangling bottoms and four buttons at four different heights, which reads as
   unfinished rather than airy. The feature list absorbs the difference (flex:1 below)
   so every button lands on the same line. */
.tiers{display:grid; gap:20px; grid-template-columns:repeat(auto-fit,minmax(min(268px,100%),1fr));
        align-items:stretch}
.tiers .tier .btn{margin-top:auto}
.tier{
  background:var(--ink-2); border:1px solid var(--line);
  border-radius:var(--r); padding:28px 24px; display:flex; flex-direction:column;
}
.tier.feat{border-color:var(--accent); background:var(--ink-3)}
.tier .name{font-family:var(--fd); font-weight:640; font-size:1.16rem; letter-spacing:-.02em}
/* width:max-content is load-bearing. As a block the pill stretched the full column,
   so at 390px the highlighter ran the width of the card with "$497" stranded at the
   left edge at 25 percent fill. It now hugs the number at every width. */
.tier .price{
  font-family:var(--fd); font-size:clamp(2.1rem,3.6vw,2.7rem); font-weight:690;
  letter-spacing:-.045em; margin:14px 0 4px; line-height:1;
  width:max-content; max-width:100%;
}
.tier .price span{font-size:1rem; font-weight:500; color:var(--mute); letter-spacing:0}
/* "from" sits beside the pill, not inside it. Inside, it wrapped the War Room number to
   two lines and made that one pill twice the height of the other three. Beside it rather
   than above it so all four price rows share one baseline across the card row. */
.price-row{display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin:14px 0 4px}
.price-row .price{margin:0}
.tier .from{
  font-family:var(--fm); font-size:.72rem; letter-spacing:.09em;
  text-transform:uppercase; color:var(--mute);
}
.tier .was{color:var(--mute); font-size:.9rem; margin-bottom:16px}
.tier .was s{color:var(--mute-2)}
.tier ul{list-style:none; margin:0 0 22px; padding:0; flex:1}
.tier li{padding:7px 0 7px 24px; position:relative; font-size:.95rem; color:var(--body)}
.tier li::before{
  content:""; position:absolute; left:4px; top:12px; width:5px; height:10px;
  border-right:2px solid var(--ok); border-bottom:2px solid var(--ok);
  transform:rotate(45deg);
}
.badge{
  display:inline-block; font-family:var(--fm); font-size:.68rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--accent); border:1px solid var(--accent);
  border-radius:99px; padding:3px 9px; margin-bottom:14px;
}

/* ---------- tables ---------- */
.tbl-scroll{overflow-x:auto; -webkit-overflow-scrolling:touch}
table{border-collapse:collapse; width:100%; font-size:.95rem; min-width:520px}

/* Below 640px a 520px table leaves the right column outside the scroll port, which
   on the pricing page meant every price sat off screen behind a sideways scroll
   nobody would think to try. Stack each row instead: item, description, then the
   price on its own line. The price is the thing being looked up, so it is never
   the part that gets hidden. */
@media(max-width:640px){
  .tbl-scroll{overflow-x:visible}
  .tbl-scroll table{min-width:0; width:100%; display:block; font-size:1rem}
  .tbl-scroll thead{position:absolute; width:1px; height:1px; overflow:hidden;
                    clip:rect(0 0 0 0); white-space:nowrap}
  .tbl-scroll tbody,
  .tbl-scroll tr,
  .tbl-scroll td{display:block; width:auto}
  .tbl-scroll tr{padding:15px 0; border-bottom:1px solid var(--line)}
  .tbl-scroll tr:last-child{border-bottom:0}
  .tbl-scroll td{border-bottom:0; padding:0}
  .tbl-scroll td + td{margin-top:9px}
  .tbl-scroll td.mono{font-size:1.15rem; color:var(--paper); font-weight:600}
}
th,td{text-align:left; padding:13px 16px; border-bottom:1px solid var(--line); vertical-align:top}
th{
  font-family:var(--fm); font-size:.74rem; letter-spacing:.09em; text-transform:uppercase;
  color:var(--mute); font-weight:500;
}
tbody tr:hover{background:var(--ink-2)}
td strong{font-weight:640}

/* ---------- answer block (AEO/GEO citability) ---------- */
.answer{
  background:var(--ink-2); border:1px solid var(--line);
  border-left:3px solid var(--accent); border-radius:0 var(--r) var(--r) 0;
  padding:24px 26px; margin:32px 0;
}
/* The question is an h2 so the outline never skips a level, but it is a question
   above its answer inside a card, not a section headline. Sized accordingly. */
.answer h2,.answer h3{font-size:clamp(1.2rem,1.9vw,1.55rem); line-height:1.22;
  letter-spacing:-0.02em; margin-bottom:14px}
.answer p:last-child{margin-bottom:0}

/* ---------- faq ---------- */
.faq{border-top:1px solid var(--line)}
.faq details{border-bottom:1px solid var(--line)}
.faq summary{
  cursor:pointer; padding:20px 0; font-weight:600; font-size:1.06rem;
  list-style:none; display:flex; justify-content:space-between; gap:20px; letter-spacing:-.015em;
}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"+"; color:var(--accent); font-weight:400; font-size:1.5rem; line-height:1}
.faq details[open] summary::after{content:"\2212"}
.faq .a{padding:0 0 22px; color:var(--body-2); max-width:74ch}
.faq .a p:last-child{margin-bottom:0}

/* ---------- prose (guide pages) ---------- */
.prose h2{margin:2em 0 .6em}
/* The 2em is for an h2 arriving mid-prose, where it needs air above it to break the
   column. The FIRST h2 in a prose block already has a section's worth of padding above
   it, so the two stacked: 66px of section padding plus 96px of margin put a 228px empty
   band between the About answer card and "Why this exists". */
.prose > h2:first-child,.prose > h3:first-child{margin-top:0}
.prose h3{margin:1.9em 0 .5em}
.prose ul,.prose ol{padding-left:22px; margin:0 0 1.2em; color:var(--body)}
.prose li{margin-bottom:.5em}
.prose a{color:var(--accent); text-decoration:underline; text-underline-offset:3px}
.prose code{
  font-family:var(--fm); font-size:.9em; background:var(--ink-3);
  padding:2px 6px; border-radius:5px;
}
.prose blockquote{
  margin:1.6em 0; padding:2px 0 2px 20px;
  border-left:3px solid var(--line); color:var(--mute);
}

/* ---------- closing band ---------- */
.close{background:var(--ink-2); border-top:1px solid var(--line); text-align:center}
.close h2{max-width:18ch; margin:0 auto}
.close .lede{margin:20px auto 0}
.close .cta-row{justify-content:center; margin-top:28px}

/* ---------- footer ---------- */
footer{background:var(--ink); border-top:1px solid var(--line); padding:56px 0 40px; font-size:.9rem}
.fgrid{display:grid; gap:36px; grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}
.fcol h2{
  font-family:var(--fm); font-size:.72rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--mute); font-weight:500; margin-bottom:14px;
}
.fcol a{display:block; color:var(--foot); text-decoration:none; padding:5px 0}
.fcol a:hover{color:var(--accent)}
.fbot{
  margin-top:44px; padding-top:22px; border-top:1px solid var(--line);
  display:flex; flex-wrap:wrap; gap:14px; justify-content:space-between; color:var(--mute-2);
}

/* ---------- FAQ, two columns ---------- */
.faq-wrap{display:grid; gap:56px; grid-template-columns:minmax(0,1fr) 320px; align-items:start}
.faq-ask{
  border:1px solid var(--line); border-radius:var(--r); background:var(--ink-2);
  padding:24px 22px; position:sticky; top:88px;
}
.faq-ask h3{font-size:1.12rem; margin:0 0 10px}
.faq-ask p{margin:0 0 18px}
.faq-ask .btn{width:100%; justify-content:center}
@media(max-width:900px){
  .faq-wrap{grid-template-columns:1fr; gap:30px}
  .faq-ask{position:static}
}

/* ---------- breadcrumb ---------- */
.crumb{font-size:.86rem; color:var(--mute-2); padding:14px 0 0}
/* inline-block with real padding so the crumb links are 32px tall. The wrapper loses
   the 8px the links gain, so the bar sits where it always did. */
.crumb a{color:var(--mute); text-decoration:none; display:inline-block; padding:8px 0}
.crumb a:hover{color:var(--accent)}

/* ---------- project detail: the two captures ----------
   Desktop and phone in one row. The phone shot is a real 390px render, so it is narrow
   and tall; giving it a fixed 300px track and letting the desktop shot take the rest
   keeps both at their own aspect ratio with nothing cropped or stretched. */
.shots{display:grid; gap:20px; grid-template-columns:minmax(0,1fr) 300px; align-items:start;
       margin-top:34px}
.shots.one{grid-template-columns:1fr}
.shots figure{margin:0}
.shots img{
  display:block; width:100%; height:auto; border-radius:var(--r);
  border:1px solid var(--line); background:var(--ink-2);
}
.shots figcaption{
  margin-top:10px; font-family:var(--fm); font-size:.68rem; letter-spacing:.06em;
  text-transform:uppercase; color:var(--mute-2);
}
@media(max-width:820px){
  .shots{grid-template-columns:1fr; gap:26px}
  /* The phone shot at full column width on a phone would be a 390px render blown up to
     390px of a device that is showing the real thing anyway. Half width, centred. */
  .shots figure:last-child img{max-width:62%; margin:0 auto}
  .shots.one figure:last-child img{max-width:100%}
}

/* ---------- project detail ---------- */
.shot{
  border:1px solid var(--line); border-radius:var(--r);
  overflow:hidden; background:var(--ink-2);
}
.meta{display:grid; gap:22px; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); margin:32px 0}
.meta .k{
  font-family:var(--fm); font-size:.72rem; letter-spacing:.09em;
  text-transform:uppercase; color:var(--mute); display:block; margin-bottom:6px;
}
.meta .v{font-weight:600}
.meta .v a{color:var(--accent)}

/* ---------- misc ---------- */
.split{display:grid; gap:var(--gap); grid-template-columns:1fr 1fr; align-items:start}
@media(max-width:820px){
  .split{grid-template-columns:1fr}
  section{padding:clamp(44px,9vw,64px) 0}
  /* Two rows: brand + CTA on top, links scroll horizontally underneath.
     Never hide navigation behind a burger: these links are the internal
     link graph and mobile is most of the traffic. */
  /* padding-top only. The shorthand used to be `11px 0 0`, which zeroed the
     horizontal padding .wrap sets, so the brand and the CTA sat flush against
     both screen edges on every page at mobile width. */
  .nav-in{flex-wrap:wrap; height:auto; padding-top:11px; gap:12px}
  .brand{order:1}
  .nav-cta{order:2; margin-left:auto}
  .nav-links{
    /* No top padding and 8px at the bottom, not 2 and 10. The row is capped at 48px
       by the collapse rule below, so every px of container padding comes straight off
       the links' hit area: at 2/10 they measured 36px tall no matter what padding the
       links themselves carried. At 0/8 they measure 40px and the header is the same
       height it was. */
    order:3; flex-basis:100%; margin:0 -24px; padding:0 24px 8px;
    overflow-x:auto; gap:18px; scrollbar-width:none;
  }
  .nav-links::-webkit-scrollbar{display:none}
  .nav-links a{font-size:.9rem; padding:11px 0}
}

/* The header CTA loses two words below 640px.
   "Message on Telegram" measures 176px. The brand lockup measures about 150px. At 390px
   the row has 342px of usable width, so the two of them plus the gap came to 338px and
   the button dropped onto a line of its own, turning a two row header into a three row
   header, 122px of chrome before any content. "Telegram" is 92px and the row fits with
   room to spare, down to 320px. The words are not lost, only postponed: the button
   still reads in full on every screen wide enough to hold it. */
.wide-only{display:inline}
@media(max-width:640px){ .wide-only{display:none} }
.pill-row{display:flex; flex-wrap:wrap; gap:9px; margin:22px 0}
.pill{
  border:1px solid var(--line); border-radius:99px; padding:7px 15px;
  font-size:.88rem; color:var(--body-2); text-decoration:none;
}
.pill:hover{border-color:var(--accent); color:var(--paper)}
.skip{position:absolute; left:-9999px}
.skip:focus{left:12px; top:12px; z-index:99; background:var(--accent); color:#000; padding:10px 16px; border-radius:8px}
:focus-visible{outline:2px solid var(--accent); outline-offset:3px}

/* ---------- service proof galleries ----------
   Each service page proves itself in its own medium: marks on the logo page,
   stickers on the sticker page, real 1500x500 headers on the banner page,
   whitepaper covers on the whitepaper page.

   Delivered assets arrive with mixed backgrounds. Some are transparent PNGs, some
   have their own art baked behind them, one wordmark is dark purple on nothing.
   So every tile gets the same plate and the mark is contained inside it rather
   than cropped to fill: containment is the only rule that survives all three cases.
*/
.proof{display:grid; gap:14px; margin-top:26px}
.proof figure{
  margin:0; background:var(--ink-2); border:1px solid var(--line);
  border-radius:12px; overflow:hidden; position:relative;
  transition:border-color .16s ease;
}
.proof figure:hover{border-color:var(--line-2)}
.proof img{width:100%; display:block}
.proof figcaption{
  font-family:var(--fm); font-size:.63rem; letter-spacing:.09em; text-transform:uppercase;
  color:var(--mute-2); padding:0 8px 11px; text-align:center; line-height:1.45;
  /* wraps rather than ellipsing: the page count is the load-bearing half of a
     whitepaper caption, and "ANGRY PEPE KIDZ . 11 ..." threw it away. */
  overflow-wrap:anywhere;
}
.proof-note{color:var(--mute); font-size:.95rem; margin:14px 0 0; max-width:64ch}

.p-mark{grid-template-columns:repeat(auto-fill,minmax(min(152px,100%),1fr))}
/* No CSS padding here on purpose. Every mark is composited onto its own square card at
   build time (build/assets_service.py, logo_card), so the optical margin is baked in and
   identical whether the source was a cut-out or a full artboard. CSS padding on top of
   that would double the margin on half the grid and not the other half. */
.p-mark img{aspect-ratio:1; object-fit:cover}
.p-wide{grid-template-columns:repeat(auto-fill,minmax(min(320px,100%),1fr))}
.p-wide img{aspect-ratio:3/1; object-fit:cover}
.p-doc{grid-template-columns:repeat(auto-fill,minmax(min(172px,100%),1fr))}
.p-doc img{aspect-ratio:3/4; object-fit:cover; object-position:top center}

/* the play affordance on an animation tile */
.proof figure.anim .play{
  position:absolute; right:8px; bottom:8px; width:30px; height:30px;
  display:grid; place-items:center; padding:0; cursor:pointer;
  background:rgba(11,11,12,.72); color:var(--paper);
  border:1px solid var(--line); border-radius:50%;
  font-size:.7rem; line-height:1; backdrop-filter:blur(6px);
  transition:background .14s ease, color .14s ease, border-color .14s ease;
}
.proof figure.anim:hover .play{background:var(--accent); color:var(--accent-ink);
                               border-color:var(--accent)}
.proof figure.anim.playing .play{display:none}

@media(max-width:560px){
  /* auto-fill collapses to a single column at 342px of usable width, which turned the
     whitepaper gallery into seven full-bleed covers and an 8,500px page. Marks and
     documents are legible small, so pin the column count instead of the tile width. */
  .p-mark{grid-template-columns:repeat(3,1fr); gap:10px}
  .p-doc{grid-template-columns:repeat(2,1fr); gap:10px}
}

/* The pinned message template on /services/telegram/. Monospace because that is how a
   Telegram pin is composed and read, and because it makes the placeholder braces obvious. */
.pinned{
  background:var(--ink-2); border:1px solid var(--line); border-left:3px solid var(--accent);
  border-radius:0 var(--r) var(--r) 0; padding:22px 24px; margin:24px 0;
  overflow-x:auto; -webkit-overflow-scrolling:touch;
}
.pinned code{
  font-family:var(--fm); font-size:.86rem; line-height:1.75; color:var(--body);
  white-space:pre; display:block;
}
@media(max-width:560px){ .pinned{padding:16px 14px} .pinned code{font-size:.76rem} }

/* ---------- spec lists on project pages ----------
   "Sections built", "Build details" and "Built with" were plain <ul>s inheriting the
   browser default: grey discs and a 40px indent that pushed the text right of its own
   heading. Every other list on the site is set. Default bullets on the page whose whole
   job is to show attention to detail is the wrong place to leave the UA sheet showing. */
.spec-list{list-style:none; margin:10px 0 0; padding:0}
.spec-list li{padding:4px 0 4px 15px; position:relative}
.spec-list li::before{
  content:""; position:absolute; left:0; top:.85em; width:7px; height:1px;
  background:var(--mute-2);
}

/* ---------- the three-up under the guarantee band ----------
   Each heading sat about 4px off its own paragraph while the paragraph's line gap was
   23px, so the heading bound to nothing and the block read as one clot of mixed weight
   text. Headings also started at three different heights because the first one wraps. */
.trio h3{margin-bottom:11px; min-height:2.4em; display:flex; align-items:flex-end}
.trio p{margin-top:0}
@media(max-width:820px){ .trio h3{min-height:0; display:block} }

/* ---------- sticky header on small screens ----------
   Two stacked rows measured 115px, 13.6 percent of an 844px viewport, pinned for the
   whole of a 27 screen work page. The link row scrolls away after the first screenful;
   the brand and the Telegram button, which are the useful half, stay. */
@media(max-width:640px){
  .nav-links{
    max-height:48px; opacity:1;
    transition:max-height .22s ease, opacity .16s ease, padding .22s ease;
  }
  body.scrolled .nav-links{max-height:0; opacity:0; padding-top:0; padding-bottom:0}
}
@media (prefers-reduced-motion:reduce){ .nav-links{transition:none} }

/* ---------- narrowest phones: wrap the nav instead of clipping it ----------
   At 320px the five links measure 372px, so the last one ("About") sat half off the
   screen edge inside an overflow-x scroller with no visual cue that it scrolled. A link
   you cannot see is not navigation. Below 361px the row wraps to two lines instead, and
   the scroll-away collapse gets the taller max-height it needs so the second line is
   not the thing that gets clipped in its place. */
@media(max-width:360px){
  .nav-links{
    flex-wrap:wrap; overflow-x:visible; gap:8px 15px;
    max-height:96px; padding-bottom:8px;
  }
  .nav-links a{font-size:.85rem}
}

/* ---------- service page hero ----------
   Text left, that service's own delivered work right. Before this the right third of a
   1440 viewport was empty on all seven pages, and the page selling website design showed
   no website until y=2100. */
.svc-hero{display:grid; grid-template-columns:1.04fr .96fr; gap:clamp(28px,4vw,54px);
          align-items:center}
.svc-hero-vis{margin:0}
.svc-hero-vis img{
  width:100%; border-radius:var(--r); border:1px solid var(--line); background:var(--ink-2);
}
.svc-hero-vis figcaption{
  font-family:var(--fm); font-size:.7rem; letter-spacing:.08em; text-transform:uppercase;
  color:var(--mute-2); margin-top:11px;
}
@media(max-width:900px){
  .svc-hero{grid-template-columns:1fr}
  .svc-hero-vis{margin-top:30px}
}

/* ---------- the identity card on /about/ ----------
   This page had a name, one paragraph, then 330px of empty black before the next
   heading, on a page whose entire job is answering "who am I sending money to".
   The right column now carries only checkable things: a handle that opens a real
   account, a link to every project, the parent agency at its own domain.
*/
/* The hero band sets its own rhythm. The site default is 112px top and bottom, which
   between a four line hero and the next H2 reads as 320px of nothing. */
.id-sec{padding:34px 0 clamp(40px,5vw,72px)}
.id-hero{display:grid; grid-template-columns:1fr 340px; gap:56px; align-items:start}
.id-card{
  background:var(--ink-2); border:1px solid var(--line); border-radius:var(--r);
  padding:24px; position:sticky; top:96px;
}
.id-card .id-face{
  width:100%; aspect-ratio:1; object-fit:cover; border-radius:10px;
  display:block; margin-bottom:18px; background:var(--ink-3);
}
.id-card .id-alias{
  font-family:var(--fm); font-size:.68rem; letter-spacing:.12em; text-transform:uppercase;
  color:var(--accent); margin:0;
}
.id-card .id-name{
  font-family:var(--fd); font-size:1.45rem; font-weight:660; letter-spacing:-.03em;
  margin:5px 0 18px; line-height:1.1;
}
.id-card dl{margin:0; border-top:1px solid var(--line)}
.id-card dl > div{
  display:flex; justify-content:space-between; align-items:baseline; gap:16px;
  padding:11px 0; border-bottom:1px solid var(--line);
}
.id-card dl > div:last-child{border-bottom:0; padding-bottom:0}
.id-card dt{
  font-family:var(--fm); font-size:.66rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--mute-2); flex:none;
}
.id-card dd{margin:0; font-size:.92rem; color:var(--body); text-align:right}
.id-card a{color:var(--paper); text-decoration:none; border-bottom:1px solid var(--line-2)}
.id-card a:hover{color:var(--accent); border-bottom-color:var(--accent)}

@media(max-width:900px){
  /* Stack, and drop the sticky: a sticky card in a single column just follows you
     down the page covering the copy it was meant to support. */
  .id-hero{grid-template-columns:1fr; gap:34px}
  .id-card{position:static; top:auto}
}

/* ---------- work page filter ----------
   73 screenshots is the argument this page makes, so none of them are paginated away.
   The filter exists because on a 390px viewport that argument was 23,000px long, and
   because "what have you shipped on Solana" is the question a founder actually has.
*/
.filter-bar{
  display:flex; flex-wrap:wrap; gap:8px; margin:34px 0 22px;
  padding-bottom:20px; border-bottom:1px solid var(--line);
}
.chip{
  font-family:var(--fm); font-size:.72rem; letter-spacing:.06em; text-transform:uppercase;
  background:transparent; color:var(--body-2); border:1px solid var(--line);
  border-radius:99px; padding:8px 15px; cursor:pointer;
  /* A 44px tall target, because these are thumbed on a phone. The label is small
     on purpose; the box it lives in is not. */
  display:inline-flex; align-items:center; min-height:44px;
  transition:border-color .14s ease, color .14s ease, background .14s ease;
}
.chip:hover{border-color:var(--line-2); color:var(--paper)}
.chip[aria-pressed="true"]{
  background:var(--paper); border-color:var(--paper); color:var(--ink);
}
.filter-empty{color:var(--mute); margin:30px 0}
.linkish{
  background:none; border:0; padding:0; font:inherit; color:var(--accent);
  cursor:pointer; text-decoration:underline;
}
.work-grid{margin-top:0}
/* .card sets its own display, and an author rule beats the user agent's [hidden] rule at
   equal specificity, so a filtered-out card kept its grid slot and the page never got
   shorter. Explicit, and scoped so it cannot surprise anything else. */
.work-grid .card[hidden]{display:none}

@media(max-width:640px){
  /* Two up. A 175px screenshot still reads as a website, which is all this grid is
     claiming, and it halves a page that was a visitor's entire thumb. */
  .work-grid{grid-template-columns:repeat(2,1fr); gap:12px}
  .work-grid .card-body{padding:11px 12px 13px}
  .work-grid .card-body h2,.work-grid .card-body h3{font-size:.95rem}
  /* Stacked, not side by side. In a 165px card the name and the domain chip fought over
     one line: "S3XYVERSE" broke to "S3XYVE / RSE" and the chip ellipsised to "s3xyve...",
     which is a link that names nothing. Own line each, and the chip stops truncating. */
  .work-grid .card-body{display:block}
  .work-grid .card-body h2,.work-grid .card-body h3{font-size:.95rem; margin-bottom:8px}
  .work-grid a.tag.live-out{display:inline-block; max-width:100%; font-size:.62rem;
    padding:7px 9px}
}

/* The outbound button on a still-live project. Ghost, not filled: the filled button on
   every page is the Telegram ask, and a second saturated button would compete with it. */
.btn.visit{gap:7px}
.btn.visit span{font-size:.85em; opacity:.75}
.cta-row .muted.small{align-self:center}

/* ---------- service cards ----------
   Desktop keeps the stacked card: a 720x450 tile of that service's own delivered work,
   then the name, the line and the price. On a phone the same seven cards ran to 2,775px,
   nearly a fifth of the homepage, so they turn on their side. The tile stays large enough
   to read as work, which is the only reason it is there.
*/
.svc-card-body{padding:16px 18px 18px}
/* Card titles are h2 so the outline never skips a level. They are still card
   titles, so they keep card-title size rather than section-heading size. */
.svc-card-body h2,.svc-card-body h3{font-size:1.14rem; letter-spacing:-0.02em; line-height:1.2}
.svc-card-body p{margin:9px 0 12px}
.svc-price{color:var(--accent)}

/* Seven services in a three column grid leaves the seventh alone on its own row with
   two empty tracks beside it, which reads as a card that failed to load rather than as
   the end of a list. When the last card is the only one on its row it takes the width
   it has and lays out sideways, the same shape it already uses on mobile, so the row
   is deliberate instead of short.

   :nth-child(3n+1) is the test for "first item of a row" in a three column grid, and
   combined with :last-child that is exactly the orphan case. If a service is ever added
   or removed the selector stops matching on its own and nothing needs maintaining. */
@media(min-width:1000px){
  .svc-grid > :last-child:nth-child(3n+1){
    grid-column:span 2; display:grid; grid-template-columns:1fr 1fr; align-items:stretch;
  }
  .svc-grid > :last-child:nth-child(3n+1) .card-img{aspect-ratio:auto; height:100%}
  .svc-grid > :last-child:nth-child(3n+1) .card-img img{height:100%; object-fit:cover}
  .svc-grid > :last-child:nth-child(3n+1) .svc-card-body{
    display:flex; flex-direction:column; justify-content:center; padding:22px 26px;
  }
}

@media(max-width:640px){
  .svc-grid{grid-template-columns:1fr; gap:14px}
  /* Stacked, not side by side. The side-by-side version cropped a 720x450 three panel
     collage of real client work into a 132px column, so every card sliced a headline
     through the middle of a word: the shop selling design displaying its own work
     chopped mid-glyph, on the viewport most people arrive on. object-position cannot
     fix that because there is no single safe focal point in a three panel collage.
     Showing the whole image at its own ratio is the only crop that is never wrong. */
  /* Back to a row, but the image is now a purpose-made SQUARE crop of the leftmost panel
     (build/service_cards.py writes <slug>-m.webp), swapped in by <picture>. A square in a
     square track cannot crop at all, so nothing is cut, and the row stays compact: the
     full-bleed collage version was correct about the words and added 1,900px to the page. */
  .svc-card{display:grid; grid-template-columns:124px 1fr; align-items:center; gap:0}
  .svc-card .card-img{aspect-ratio:1/1; width:124px; height:124px}
  .svc-card .card-img img{width:100%; height:100%; object-fit:contain; object-position:center}
  .svc-card-body{padding:14px 16px 14px 14px}
  .svc-card-body p{margin:6px 0 10px}
}

/* ---------- curated examples list (/meme-coin-website-examples/) ----------
   This page used to reprint the entire /work/ grid, which made two indexed URLs showing
   the same 71 images. It is now twelve examples with a note on each, so the row needs a
   shape the card grid does not have: shot on one side, commentary on the other. */
.ex-list{display:grid; gap:18px; margin-top:30px}
.ex{
  display:grid; grid-template-columns:300px 1fr; gap:24px; align-items:center;
  background:var(--ink-2); border:1px solid var(--line); border-radius:var(--r);
  padding:16px; transition:border-color .18s ease;
}
.ex:hover{border-color:var(--line-2)}
.ex-shot{display:block; border-radius:10px; overflow:hidden; background:var(--ink-3)}
.ex-shot img{width:100%; height:auto; display:block; aspect-ratio:16/10; object-fit:cover;
  object-position:top center}
.ex-body h3{font-size:1.12rem; letter-spacing:-.02em; display:flex; align-items:center;
  gap:10px; flex-wrap:wrap}
.ex-body h3 a{color:inherit; text-decoration:none}
.ex-body h3 a:hover{color:var(--accent)}
.ex-body p{margin-top:8px; max-width:66ch}
.tag.off{background:transparent; border:1px solid var(--line-2); color:var(--mute-2)}
@media(max-width:760px){
  .ex{grid-template-columns:1fr; gap:14px}
}

/* A card can carry a line explaining what its image is, for the handful of projects that
   have no page render to screenshot. Better than silently passing off an app splash as a
   site capture in a grid headed "screenshotted from the actual builds". */
.shot-note{margin:0 14px 14px; line-height:1.4}

/* Heading levels must not skip, but /work/ and /services/ lead straight from the h1 into a
   grid of h3 cards with no section headline in between: the page genuinely has one idea and
   a wall of it. The h2 exists for the outline, not the layout. */
.vh{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}

/* ============================================================
   MOTION
   ============================================================
   Added 2026-08-18. Before this the whole site owned two keyframes and
   thirteen transitions, which is why it read as competent and dead.

   Everything here is opt-in from markup (a data attribute or a class the
   motion layer adds), and every rule that moves something is inside the
   .js guard, so a reader without JavaScript never sees a half-played
   state and never sees an element stuck at opacity 0. That last part is
   the whole reason the initial state lives under .js rather than on the
   element itself: a reveal system that hides content by default is one
   script error away from a blank page.

   The reduced-motion contract is enforced twice, on purpose. The motion
   layer refuses to arm and stamps .still on the root, and the media query
   below neutralises anything that got through. Belt and braces, because
   this is an accessibility floor and not an effect.
   ============================================================ */

/* ---------- scroll progress ---------- */
/* A 2px bar under the sticky header. It is the cheapest possible "this page
   has more in it" signal, and on the long guide pages it is the difference
   between a reader scrolling and a reader guessing. */
.js .nav::after{
  content:""; position:absolute; left:0; bottom:-1px; height:2px;
  width:100%; transform:scaleX(var(--prog,0)); transform-origin:0 50%;
  background:linear-gradient(90deg,var(--accent),var(--accent-2));
  will-change:transform;
}

/* ---------- reveal ---------- */
.js [data-rv]{
  opacity:0; transform:translateY(18px);
  transition:opacity .62s cubic-bezier(.16,.84,.32,1),
             transform .62s cubic-bezier(.16,.84,.32,1);
  transition-delay:calc(var(--rv-i,0) * 55ms);
}
.js [data-rv].rv-in{opacity:1; transform:none}
/* Cards arrive with a little more travel and a hair of scale, because a grid of
   them reads as one object and a uniform fade looks like a page repaint. */
.js .card[data-rv]{transform:translateY(26px) scale(.985)}
.js .card[data-rv].rv-in{transform:none}

/* ---------- pointer: cards ---------- */
/* The spotlight is a radial wash pinned to the cursor, drawn behind the content
   and clipped by the card's own overflow. The tilt is small on purpose: past
   about 4 degrees a grid of tiles starts to read as a toy. */
.js .card{transform-style:preserve-3d}
.js .card::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  opacity:0; transition:opacity .25s ease;
  background:radial-gradient(220px circle at var(--mx,50%) var(--my,50%),
             rgba(255,90,31,.16), transparent 62%);
}
.js .card.hot::before{opacity:1}
.js .card.hot{
  transform:perspective(900px)
            rotateX(calc(var(--ty,0) * -3.2deg))
            rotateY(calc(var(--tx,0) * 3.2deg))
            translateY(-4px);
  border-color:var(--line-2);
}
.js .card-body,.js .svc-card-body,.js .card-img{position:relative; z-index:1}

/* ---------- pointer: buttons ---------- */
/* Magnetism, kept under 4px. The button should feel like it wants the click,
   not like it is running away from the cursor. */
.js .btn{transition:transform .16s cubic-bezier(.2,.8,.3,1.2), filter .12s ease}
.js .btn.hot{
  transform:translate(calc(var(--tx,0) * 7px), calc(var(--ty,0) * 4px))
            translateY(-2px);
}
/* The primary CTA gets a sheen that crosses once on hover. One element, one
   pass, no loop: a button that shimmers forever is a scam button. */
.js .btn:not(.ghost){position:relative; overflow:hidden}
.js .btn:not(.ghost)::after{
  content:""; position:absolute; top:0; left:-60%; width:40%; height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.42),transparent);
  transform:skewX(-18deg); pointer-events:none; opacity:0;
}
.js .btn:not(.ghost):hover::after{animation:sheen .62s ease-out}
@keyframes sheen{
  from{left:-60%; opacity:1} to{left:120%; opacity:0}
}

/* ---------- hero parallax ---------- */
/* The wall of real work drifts against the cursor. Small numbers: this sits
   directly behind an h1 and a price, and anything larger turns the first thing
   a buyer reads into something that will not hold still. */
.js .hero-vis{
  transition:transform .5s cubic-bezier(.2,.7,.3,1);
  transform:translate3d(calc(var(--px,0) * -13px), calc(var(--py,0) * -9px), 0);
  will-change:transform;
}

/* ---------- marquee lean ---------- */
.js .marquee img{
  transform:rotate(calc(var(--vel,0) * -5deg))
            translateY(calc(var(--vel,0) * -4px));
}
/* The hover rule already existed and has to keep winning over the lean. */
.js .marquee img:hover{transform:translateY(-3px) rotate(-3deg) scale(1.06)}

/* ---------- the mark ---------- */
.brand .mark.roll{animation:roll .62s cubic-bezier(.3,.9,.3,1)}
@keyframes roll{
  from{transform:rotate(0) scale(1)}
  55%{transform:rotate(220deg) scale(1.16)}
  to{transform:rotate(360deg) scale(1)}
}

/* ---------- stickers ---------- */
/* The die cut icon set from the brand pack. They carry meaning (a stopwatch on
   the turnaround claim, a badge on the guarantee) so they are never decoration
   with an empty alt: each one is either labelled or explicitly hidden by the
   template that places it. */
.sticker{
  display:inline-block; flex:none; line-height:0;
  transition:transform .28s cubic-bezier(.2,.8,.3,1.3);
}
.sticker img{display:block; width:100%; height:auto}

/* A sticker in a fixed box, for rows. See common.sticker(): the art keeps its own
   proportions and sits on the floor of the box, so the headings under a row of
   different shaped stickers all start at the same y. */
.sticker.boxed{display:flex; align-items:flex-end; line-height:0}
.sticker.boxed img{width:100%; height:auto; max-height:100%}
.js .sticker.hot{transform:rotate(-7deg) scale(1.1)}
/* Arriving stickers overshoot slightly. This is the one place on the site where
   a bounce is right: they are stickers, and stickers get slapped on. */
.js .sticker[data-rv]{transform:scale(.5) rotate(-14deg)}
.js .sticker[data-rv].rv-in{
  animation:slap .5s cubic-bezier(.24,1.4,.4,1) backwards;
  animation-delay:calc(var(--rv-i,0) * 55ms);
  transform:none;
}
@keyframes slap{
  from{transform:scale(.5) rotate(-14deg); opacity:0}
  60%{transform:scale(1.09) rotate(3deg); opacity:1}
  to{transform:none; opacity:1}
}

/* ---------- copy-to-clipboard ---------- */
.copyable{
  font-family:var(--fm); cursor:pointer; background:none; color:inherit;
  border:1px dashed var(--line-2); border-radius:8px; padding:5px 10px;
  font-size:.86rem; transition:border-color .15s ease, color .15s ease;
}
.copyable:hover{border-color:var(--accent); color:var(--paper)}
.copyable.copied{border-color:var(--ok); border-style:solid; color:var(--ok)}

/* ---------- the reduced-motion floor ---------- */
/* Second line of defence. The motion layer already refuses to arm under this
   preference; this makes the stylesheet correct on its own terms too. */
@media (prefers-reduced-motion:reduce){
  .js [data-rv],
  .js .card[data-rv],
  .js .sticker[data-rv]{opacity:1; transform:none; animation:none; transition:none}
  .js .hero-vis,
  .js .marquee img,
  .js .card.hot,
  .js .btn.hot{transform:none}
  .js .card.hot::before{opacity:0}
  .js .btn:not(.ghost):hover::after{animation:none}
  .brand .mark.roll{animation:none}
  .hero-col.up,.hero-col.down,.marquee-track{animation:none}
}

/* ---------- pillars: fast, good, grown up ---------- */
/* The three word argument. Ishan's brief on 2026-08-18 was that the site talked about
   speed and nothing else, which made it read as the cheap fast option rather than the
   good fast one. Three equal columns, three equal weights, no ranking. */
.pillars{grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:34px}
.pillars h3{font-size:1.5rem; margin:14px 0 8px; letter-spacing:-.02em}
.pillars .sticker{margin-bottom:2px}

/* ---------- the receipts ladder ---------- */
/* Claimed, published, verifiable. Three rows, descending, and the numbers get smaller
   as the evidence gets harder, which is the entire point of the block: the shape of it
   argues before the copy does. */
.ladder{display:flex; flex-direction:column; gap:2px}
.ladder > div{
  display:flex; align-items:baseline; gap:22px; padding:22px 0;
  border-top:1px solid var(--line);
}
.ladder > div:last-child{border-bottom:1px solid var(--line)}
.ladder .n{
  font-family:var(--fd); font-weight:760; letter-spacing:-.045em;
  color:var(--accent); font-variant-numeric:tabular-nums;
  flex:none; min-width:3.4ch; text-align:right;
}
.ladder > div:nth-child(1) .n{font-size:clamp(2.7rem,5.4vw,4rem)}
.ladder > div:nth-child(2) .n{font-size:clamp(2.2rem,4.2vw,3.1rem); color:var(--paper)}
.ladder > div:nth-child(3) .n{font-size:clamp(1.8rem,3.2vw,2.4rem); color:var(--accent-2)}
.ladder .l{color:var(--body-2); font-size:1rem; line-height:1.5}
@media(max-width:520px){
  .ladder > div{gap:14px}
  .ladder .l{font-size:.94rem}
}

/* ---------- steps get their sticker ---------- */
.steps .sticker{margin-bottom:10px}

/* ---------- the closing band, with the bro in it ---------- */
.close-in{display:grid; grid-template-columns:minmax(0,1fr) auto; gap:40px; align-items:end}
.close-bro{
  width:clamp(120px,15vw,186px); height:auto; align-self:end;
  margin-bottom:-1px; pointer-events:none;
}
/* Under 860px the mascot would eat a third of the width next to the one CTA that
   matters. It goes rather than shrinks: a 70px sticker of a person is not a mascot,
   it is a smudge. */
@media(max-width:860px){
  .close-in{grid-template-columns:minmax(0,1fr)}
  .close-bro{display:none}
}

/* ---------- 404 ---------- */
/* The mascot shrugging next to a dead end. Same asset as the closing band, so this
   costs no extra bytes for anyone who has already been on another page. */
.notfound{display:grid; grid-template-columns:minmax(0,1fr) auto; gap:44px; align-items:center}
.nf-bro{width:clamp(130px,17vw,220px); height:auto}
@media(max-width:820px){
  .notfound{grid-template-columns:minmax(0,1fr)}
  .nf-bro{display:none}
}
