/* =========================================================================
   BEACON — the marketplace surface
   =========================================================================
   THE NAME IS THE THESIS, AND IT WORKS TWICE.

   A beacon is a fixed point you navigate by: this marketplace's one claim is
   that a price can be checked against the chain rather than believed. And a
   beacon is a signal that brings help: somebody out there wrote the thing you
   need, and this is how it reaches you. The first meaning is why the design is
   built around a proof. The second is why it is warm rather than clinical.

   THE ROOM THIS IS SET IN
   White ground, engraved brass, hairline rules, tabular figures, and nothing
   decorative that is not also load-bearing. The ground is white because the
   things that matter here are made of light: a signal that checks out, and a
   field of beams firing between points. Instruments are not styled; they are
   made accurate and then made beautiful by being accurate.
   That is the whole brief: no rounded boxes stacked inside rounded boxes, no
   badges that say nothing, no gradient standing in for a hierarchy.

   THREE VOICES, AND THE DIFFERENCE IS LOAD-BEARING
   This product spends its whole time telling you whose words you are reading,
   because that is exactly what the verification is for. So the type says it:

     system voice   Archivo         what this site claims
     creator voice  Spectral        what a person wrote — names, descriptions
     machine voice  IBM Plex Mono   what the chain says — fingerprints, roots

   A skill's name is set in Spectral because a human chose it. A content
   fingerprint is set in Plex Mono because nobody chose it. That is not
   decoration; it is the same distinction the proof exists to enforce, made
   visible before anyone reads a word.

   All three are self-hosted in assets/fonts/. `font-src 'self'` is in the CSP
   on every page, so a CDN font would not merely be bad practice here — it
   would fail to load. See fonts/LICENSE.md.

   ARCHIVO HAS A WIDTH AXIS, AND IT IS USED FOR ONE THING
   Small engraved labels — eyebrows, table keys, the count line — are set at
   108% width with open tracking, which is how signage and instrument panels
   are lettered. Display type runs the other way, at 92%, so a headline gains
   authority by getting tighter rather than by getting bigger. Nothing else in
   the system touches the axis, which is what keeps it a signature instead of
   an effect.

   COLOUR MEANS STATE, AND ONLY STATE
     amber   the signal is present — proved against Base, live, on sale
     teal    we are behind, not broken — index catching up, sales paused
     red     this cannot be undone, or it can reach your wallet

   The teal is the important one. An index a few minutes behind the chain and a
   row that has been tampered with used to get the same alarming sentence, and
   they are opposite problems: one waits, one runs. They now look nothing alike.

   Every ink here clears 4.5:1 on this paper. The old palette did not: the
   amber measured 3.86:1 and the faint grey 2.83:1, which put the id, the kind
   and the version of every listing below legibility on the first screen.

   THE SIGNATURE: THE SWEEP
   A real beacon is known by its light passing over you at a fixed interval.
   The verification bar sweeps once, in amber, when a page's proof checks out —
   and does not sweep when it does not. The absence of the signal is the alarm,
   which is how it works at sea. Respects prefers-reduced-motion.
   ========================================================================= */

/* ---------- the three voices, self-hosted ------------------------------- */
@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 75% 125%;
  font-display: swap;
  src: url("fonts/archivo-var.woff2") format("woff2");
}
@font-face {
  font-family: "Spectral";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/spectral-400.woff2") format("woff2");
}
@font-face {
  font-family: "Spectral";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/spectral-600.woff2") format("woff2");
}
@font-face {
  font-family: "Spectral";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/spectral-400-italic.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/plex-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/plex-mono-500.woff2") format("woff2");
}

:root {
  /* ---- ground and ink ------------------------------------------------- */
  /*
     STOCK, NOT SCREEN. The ground carries a whisper of warmth (#FCFBF9 rather than pure
     white), and the relationship to panels is INVERTED from what it was: a panel is now
     #FFFFFF and sits ABOVE the ground, rather than a grey tint sitting below it.
     Everything the eye reads as expensive in a printed document comes from that order --
     paper has a colour, and the important card is the brighter thing laid on top of it.
     The old arrangement buried every panel in a slightly dirtier grey than the page.

     The warmth is a fraction: #FCFBF9 is 1.5% off white. Enough that the whites on it
     read as objects; not so much that it announces itself as cream, which is a different
     and much louder decision.
  */
  --bg:        #FCFBF9;   /* the stock. The ground the light travels across. */
  /* The same stock, under a name the black stage does not shadow. `.stage` sets its own
     `--bg` so the buttons on it invert correctly, which means its fade-out gradient can no
     longer name `--bg` as the colour to reach -- it would resolve to the stage's black and
     fade black into black. This is what it fades to instead. */
  --paper:     #FCFBF9;
  --surface:   #FFFFFF;   /* raised: a panel, a card, anything laid on the page */
  --surface-2: #F2F0EB;   /* sunk: code, excerpts, anything quoted rather than said */
  --ink:       #14161A;
  --muted:     #4C5560;
  --faint:     #565F6A;   /* 6.3:1 on the stock, and 4.6:1 under the densest part of the field */
  --rule:      #E7E4DD;   /* hairline: warm, so a rule reads as a fold rather than a wire */
  --rule-firm: #C9C5BB;   /* structural: a division the reader is meant to notice */
  /* The one metal. A hairline of it marks the single structural moment on a page and
     nothing else -- spend it twice and it stops being a material. */
  --gild:      color-mix(in srgb, var(--signal-fill) 42%, transparent);

  /* ---- state ---------------------------------------------------------- */
  --signal:      #9A5B00;  /* beacon amber as INK — 4.95:1 on paper */
  --signal-fill: #E4901C;  /* beacon amber as LIGHT — fills, dots, the sweep */
  --signal-bg:   #FDF3E0;
  --deep:        #14555F;  /* catching up, paused */
  --deep-bg:     #E8F2F4;
  --flag:        #A3342A;  /* irreversible, touches your wallet */
  --flag-bg:     #FCEBE8;
  /* The full-bleed alarm band. Separate from --flag because --flag inverts for the
     dark theme -- it becomes a light salmon, and white text on light salmon is the
     one place on this site where a warning would be harder to read than the prose
     around it. This pair stays dark-ground/light-ink in both. */
  --alarm:       #A3342A;
  --alarm-ink:   #FFEDEA;

  /* ---- type ----------------------------------------------------------- */
  --sans:  "Archivo", ui-sans-serif, -apple-system, "Segoe UI", sans-serif;
  --serif: "Spectral", ui-serif, Georgia, "Times New Roman", serif;
  --mono:  "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --t-xs:   0.8125rem;
  --t-sm:   0.9375rem;
  --t-base: 1rem;
  --t-lg:   1.125rem;
  --t-xl:   1.3125rem;
  --t-2xl:  1.625rem;
  --t-3xl:  2.125rem;
  --display: clamp(2.35rem, 4.6vw, 3.35rem);

  /* ---- metrics -------------------------------------------------------- */
  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --max:    76rem;
  --max-read: 38rem;
  /* Squared. A chart is ruled, not rounded. The one curve left in the system
     is on things a finger presses, where a hard corner reads as unfinished. */
  --radius:    0px;
  --radius-sm: 3px;
  --tap: 2.75rem;                       /* 44px — never smaller, anywhere */
  --hair: 1px solid var(--rule);
  /*
     TWO STAGES, and the near one is what makes an edge. A single soft shadow reads as a
     blur; a tight 1px contact shadow plus a wide ambient one reads as a sheet of card
     resting on paper. The third value is a top highlight, applied as an inset so a raised
     surface catches light on its upper edge the way a real one does.
  */
  --shadow:
    0 1px 0 rgb(20 22 26 / 0.04),
    0 1px 2px rgb(20 22 26 / 0.05),
    0 18px 40px -24px rgb(20 22 26 / 0.20);
  --edge: inset 0 1px 0 rgb(255 255 255 / 0.9);
}

/* WHITE IS THE DEFAULT, and it does not depend on the reader's machine.

   THERE IS ONE THEME NOW, AND IT IS THIS ONE.

   The site used to ship both and let the reader choose, with a control in the masthead and
   the choice remembered. Two complete palettes is two of everything to keep true, and every
   surface added afterwards had to be checked twice or it was only right by accident. Dark is
   what this product looks like; the light one is gone rather than hidden, so there is no
   second set of values to drift.

   The tokens above are still the light ones, and this block still overrides them, because
   collapsing the two into one list would touch every line of the palette for no gain. What
   changed is that nothing has to ask for it.

   DARK IS THE SAME MATERIAL SEEN AT NIGHT, not an inversion. The ground keeps a trace of
   warmth rather than going blue-black, panels are lifted by ONE step of lightness plus a
   faint top edge, and the shadow does the opposite job it does on paper: it deepens the
   ground rather than lifting the card, because light on a dark surface comes from the
   edge and not from underneath.
*/
:root {
  color-scheme: dark;
  --paper:#0C1013; --bg:#0C1013; --surface:#14191E; --surface-2:#1B2126; --ink:#E9EBE8; --muted:#9EA6AC;
  --faint:#98A0A7; --rule:#242B31; --rule-firm:#3A434B;
  --signal:#EFAE52; --signal-fill:#E8A33F; --signal-bg:#2A2013;
  --deep:#6FBFD0; --deep-bg:#10262C; --flag:#EE8E80; --flag-bg:#2C1613;
  --alarm:#7E2A22; --alarm-ink:#FFE4DE;
  --shadow:
    0 1px 0 rgb(0 0 0 / 0.5),
    0 2px 6px rgb(0 0 0 / 0.35),
    0 24px 48px -28px rgb(0 0 0 / 0.75);
  --edge: inset 0 1px 0 rgb(255 255 255 / 0.045);
}

/* ---------- base ------------------------------------------------------- */
* { box-sizing: border-box }
html { -webkit-text-size-adjust: 100% }
/* `hidden` is how this codebase shows and hides panels, and a class carrying its own
   `display` silently beats it. Anything marked hidden is hidden, whatever it is. */
[hidden] { display: none !important }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-base);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  font-synthesis-weight: none;
}
img { max-width: 100%; display: block }
a { color: inherit; text-decoration: none }
::selection { background: var(--signal-fill); color: #14181D }

:where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--signal-fill);
  outline-offset: 2px;
  border-radius: 2px;
}

h1, h2, h3, h4 { margin: 0; line-height: 1.15; letter-spacing: -0.018em; font-weight: 640 }
/* Headings in running prose need air beneath them; every other heading on the site is
   positioned by its own block and must not inherit a margin it did not ask for. */
.read h2 { margin-bottom: 0.55rem }
.read h3 { margin-bottom: 0.45rem }
p { margin: 0 0 0.85em }
p:last-child { margin-bottom: 0 }

/* Emphasis in running text is set in the book italic. The sans italic is a 99 KB
   file and this site sets two words in one; the serif was loading anyway, and a
   text italic is the better piece of typography. */
em, i { font-family: var(--serif); font-style: italic; font-size: 1.04em }
.card-desc em, .detail-desc em { font-size: 1em }

/* ---------- the three voices ------------------------------------------- */

/* The engraved label. Archivo's width axis, used here and nowhere else. */
.eyebrow, .foot h4, .fact-k, .count-note, .card-id, .picker-sub, .beam-detail, .lab.micro {
  font-stretch: 108%;
}

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--gutter) }
.read { max-width: var(--max-read); min-width: 0 }
.muted { color: var(--muted) }

/* ---------- masthead ---------------------------------------------------- */
.masthead {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 90%, transparent);
  /* PREFIXED, because Safari before 16 only knows the prefixed form and this is the one surface
     where losing the blur shows on every page. */
  backdrop-filter: saturate(140%) blur(10px);
  -webkit-backdrop-filter: saturate(140%) blur(10px);
  border-bottom: 1px solid var(--rule);
}
/* THE NOTCH AND THE HOME INDICATOR. Nothing was inset, so in landscape on a phone with rounded
   corners the bar's own contents ran under them, and the band at the foot ended under the home
   indicator. Zero on every desktop, so it costs nothing where it is not needed. */
@supports (padding: max(0px)) {
  .masthead-inner {
    padding-left: max(var(--gutter), env(safe-area-inset-left));
    padding-right: max(var(--gutter), env(safe-area-inset-right));
  }
  .wrap {
    padding-left: max(var(--gutter), env(safe-area-inset-left));
    padding-right: max(var(--gutter), env(safe-area-inset-right));
  }
  .foot-fine { padding-bottom: max(clamp(1.5rem, 3vh, 2.25rem), env(safe-area-inset-bottom)) }
}
.masthead-inner {
  max-width: var(--max); margin: 0 auto; padding: 0.4rem var(--gutter);
  display: flex; align-items: center; gap: 0.75rem;
}
/* ---------- the search box in the masthead ------------------------------
   Beside the wordmark, on every page, injected by assets/search-bar.js.

   The `margin-right: auto` that pushes the nav to the right normally sits on the
   wordmark. When a search box is present it has to move to the box instead, or the
   auto margin would shove the search across to the nav. `.has-search` is set by the
   injector, so a page where the script has not run still lays out correctly rather
   than collapsing the nav against the logo. */
.masthead-inner.has-search .wordmark { margin-right: 0 }
.masthead-search {
  display: flex; align-items: center; gap: 0.45rem;
  /* Centred in the space between the wordmark and the nav, not parked against the logo.
     `flex-grow: 0` is what makes that possible: a box that grows swallows the free
     space, leaving the auto margins nothing to balance, and it ends up hard against the
     wordmark with all the slack dumped on the right. Fixed width, two auto margins, and
     the gap either side comes out equal at every width. */
  flex: 0 1 32rem; min-width: 0; margin-inline: auto;
  padding: 0 0.7rem;
  height: 2.25rem;
  /* WHITE, ON A PAGE THAT IS OTHERWISE DARK. The one control a visitor is meant to type into
     is the one thing given paper to type on, so it reads as a field rather than as another
     panel. Its ink is set here rather than inherited: everything inside it now sits on white
     and would otherwise take the page's light ink and disappear. */
  color: #3A4149;
  background: #FFFFFF;
  border: 1px solid transparent; border-radius: var(--radius-sm);
}
/* A FINGER IS NOT A CURSOR. 36px is comfortable to click and small to tap; on a touch screen
   this is the one field on the page and it gets the full target height. Keyed to the input
   method rather than to the width, so a small window on a desktop keeps the tighter bar. */
@media (pointer: coarse) {
  .masthead-search { height: 2.75rem }
}
.masthead-search:focus-within {
  border-color: var(--signal-fill); background: #FFFFFF; color: #14161A;
}
.masthead-search svg { flex: none }
.ms-input {
  flex: 1 1 auto; min-width: 0;
  border: 0; background: none; outline: none; padding: 0;
  font: inherit; font-size: var(--t-sm); color: #14161A;
}
.ms-input::placeholder { color: #6B737B; opacity: 1 }
/* The browser's own clear affordance is inconsistent between engines and invisible in
   some; this one is always the same control. */
.ms-input::-webkit-search-cancel-button { display: none }
.ms-clear {
  flex: none; border: 0; background: none; cursor: pointer;
  color: #6B737B; font-size: 1.15rem; line-height: 1;
  padding: 0 0.15rem;
}
.ms-clear:hover { color: var(--ink) }

/* Narrow: the logo, four links and a theme toggle already fill the bar, so the search
   takes a row of its own rather than squeezing everything into illegibility. */
@media (max-width: 52rem) {
  .masthead-inner { flex-wrap: wrap }
  .masthead-search { order: 3; flex-basis: 100%; max-width: none; margin: 0 0 0.35rem }
  .masthead-inner.has-search .wordmark { margin-right: auto }
}

.wordmark {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-weight: 680; font-size: 1.125rem; letter-spacing: -0.025em; margin-right: auto;
  min-height: var(--tap);
}
/* The mark: a swirling ring of golden light, a beacon seen head-on. Sized to fill the
   wordmark's tap height exactly, so it reads large without making the masthead any taller. */
.wordmark-lamp { width: auto; height: 2.75rem; flex: none; margin: -0.05rem 0 }
.masthead-nav { display: flex; align-items: center; gap: 0.1rem }
.masthead-nav a {
  padding: 0.5rem 0.7rem;
  color: var(--muted); font-size: var(--t-sm); font-weight: 500;
  min-height: var(--tap); display: inline-flex; align-items: center;
  border-bottom: 2px solid transparent;
}
.masthead-nav a:hover { color: var(--ink) }
.masthead-nav a[aria-current="page"] { color: var(--ink); font-weight: 600; border-bottom-color: var(--signal-fill) }
@media (max-width: 34rem) {
  .masthead-nav a { padding: 0.5rem 0.4rem; font-size: var(--t-xs) }
  .wordmark-lamp { height: 2.3rem }
}

/* ---------- THE SIGNATURE: the verification beam ------------------------ */
/*
   Sits under the masthead on every page that makes a claim about a price.
   It sweeps once when the proof checks out, and stays dark when it does not.
   Pages that prove nothing do not get an inert copy of it: an unlit signal
   that never had anything to report is worse than no signal.
*/
.beam {
  position: relative; overflow: hidden;
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
}
.beam-inner {
  max-width: var(--max); margin: 0 auto; padding: 0.65rem var(--gutter);
  display: flex; align-items: center; gap: 0.7rem;
  font-size: var(--t-xs); color: var(--muted); min-height: 2.4rem;
}
.beam-dot {
  width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--faint); flex: none;
  transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
.beam-text strong { color: var(--ink); font-weight: 620 }
.beam-detail {
  margin-left: auto; font-family: var(--mono); font-size: 0.6875rem;
  color: var(--faint); letter-spacing: -0.01em; white-space: nowrap;
}
@media (max-width: 52rem) { .beam-detail { display: none } }

.beam.is-proved { border-bottom-color: color-mix(in srgb, var(--signal-fill) 55%, var(--rule)) }
/* A hairline of the same metal the panel uses, laid under the proved bar. It is drawn only
   when the proof actually succeeded, so the material means something rather than decorating
   a strip that is always there. */
.beam.is-proved::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gild) 18%, var(--gild) 82%, transparent);
  pointer-events: none;
}
.beam.is-proved .beam-dot { background: var(--signal-fill); box-shadow: 0 0 0 3px color-mix(in srgb, var(--signal-fill) 24%, transparent) }
.beam.is-catching { background: var(--deep-bg); border-bottom-color: color-mix(in srgb, var(--deep) 45%, var(--rule)) }
.beam.is-catching .beam-dot { background: var(--deep) }
.beam.is-catching .beam-text { color: var(--deep) }
.beam.is-catching .beam-text strong { color: var(--deep) }
.beam.is-failed { background: var(--flag-bg); border-bottom-color: color-mix(in srgb, var(--flag) 55%, var(--rule)) }
.beam.is-failed .beam-dot { background: var(--flag) }
.beam.is-failed .beam-text, .beam.is-failed .beam-text strong { color: var(--flag) }

.beam::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(100deg,
    transparent 0%,
    color-mix(in srgb, var(--signal-fill) 8%, transparent) 36%,
    color-mix(in srgb, var(--signal-fill) 38%, transparent) 50%,
    color-mix(in srgb, var(--signal-fill) 8%, transparent) 64%,
    transparent 100%);
  transform: translateX(-101%);
}
/* Slower and softer than it was. The lamp passes once, and a reader who happens to be
   looking sees it; a reader who is not is never chased by it. */
.beam.is-proved::after { animation: beam-sweep 2000ms cubic-bezier(0.16, 0.72, 0.24, 1) forwards }
@keyframes beam-sweep {
  0%   { transform: translateX(-101%); opacity: 0 }
  8%   { opacity: 1 }
  88%  { opacity: 1 }
  100% { transform: translateX(101%); opacity: 0 }
}
@media (prefers-reduced-motion: reduce) { .beam.is-proved::after { animation: none } }

/* ---------- THE ALARM: a link has repointed the money path --------------- */
/*
   This is the single loudest thing the site can say, and until now it said it
   in unstyled body text running edge to edge with a button touching x=0 — the
   old stylesheet defined `.caution` and the new one did not carry it over. It
   is the one place full-weight inversion is spent.
*/
.caution {
  background: var(--alarm);
  color: var(--alarm-ink);
  border-bottom: 3px solid color-mix(in srgb, var(--alarm) 68%, #000);
  padding: 1.15rem var(--gutter) 1.25rem;
  /* One centred column whose width matches `.wrap`'s content box exactly, so the
     alarm's left edge lands on the same vertical as every other page element. */
  display: grid; justify-content: center; justify-items: start;
  grid-template-columns: minmax(0, calc(var(--max) - var(--gutter) * 2));
}
.caution > * { justify-self: stretch }
.caution-head {
  font-size: var(--t-lg); font-weight: 660; letter-spacing: -0.015em;
  color: #FFF; margin: 0 0 0.35rem;
}
.caution .terms { color: var(--alarm-ink); margin: 0 0 0.9rem; max-width: 62ch }
.caution .terms strong { color: #FFF }
.caution .excerpt {
  background: color-mix(in srgb, #000 26%, var(--alarm)); color: #FFF;
  border-left-color: color-mix(in srgb, #FFF 55%, var(--alarm));
  max-height: 11rem; overflow: auto; margin: 0 0 0.95rem;
}
.caution .btn {
  background: #FFF; color: var(--alarm); border-color: #FFF; font-weight: 640;
  justify-self: start; width: auto;
}
.caution .btn:hover:not(:disabled) { background: #FFF; color: #000; border-color: #FFF }
.caution .caution-head { color: #FFF }

/* ---------- hero --------------------------------------------------------- */
/* `relative` so the "built on" badge can sit in its lower-right corner. */
.hero { position: relative; padding: clamp(2.5rem, 7vw, 4.75rem) 0 clamp(1.75rem, 4vw, 2.75rem) }
.hero h1 {
  font-size: var(--display); font-stretch: 92%; letter-spacing: -0.035em;
  font-weight: 700; max-width: 15ch; text-wrap: balance;
}
.hero-sub { margin-top: 1.1rem; max-width: 44ch; font-size: var(--t-lg); color: var(--muted); line-height: 1.5 }
.hero-acts { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: 1.75rem }

/*
   The hero used to be a headline in the left half of the screen and nothing at
   all in the right, which on a 1440 viewport is 700 empty pixels doing no work.
   What belongs there is the claim itself, made checkable: the actual readout
   this page produced while it loaded. It is the product, printed.
*/
/* A centred opening. The claim, the explanation, the two ways in, and then the
   field doing the thing the claim is about. */
/* ---------- built on ----------------------------------------------------
   Sits in the lower-right of the hero, over the firing field. Anchored to the
   hero rather than fixed to the viewport: `.firing` is `position: fixed`, so a
   badge pinned to it would ride along over every section as you scroll and stop
   being a statement about the product and start being a sticky widget.

   Quiet on purpose. It is a provenance line, not a call to action, so it takes
   the muted colour and the smallest type in the system, and earns attention only
   from the two marks. */
/* IN FLOW, not absolutely positioned, and that is the second attempt.
   Pinned to the hero's bottom-right it cleared the readout strip by two pixels --
   and only when the strip was there at all, since it stays hidden until the catalog
   verifies. So the badge's spacing depended on whether a network call had come back.
   In flow it is the last thing in the hero and cannot collide with anything. */
.built {
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
  margin-top: 2.6rem;
}
/* Centred over the marks rather than aligned to the row's edge: the label belongs to the pair
   beneath it, and reading it as a caption is what tells you the two shapes are a set. */
.built-k {
  font-size: 0.625rem; font-weight: 620; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.15em; text-align: center;
}
.built-marks { display: flex; align-items: center; gap: 1.35rem }
.built-item { display: inline-flex; align-items: center; color: var(--ink) }
/* Optically matched rather than equal: an outlined ring reads smaller than a solid
   square at the same box size, so the ring is given a little more room.

   Keyed to the MARK, not to its position. This was `:last-child`, which silently tied
   the sizes to the order -- reordering the two logos then shrank the ring and grew the
   square, which is exactly backwards, and nothing would have failed to say so. */
.built-logo { width: 1.9rem; height: 1.9rem; flex: none; display: block }
.built-logo.is-ring  { width: 1.9rem; height: 1.9rem }
.built-logo.is-solid { width: 1.68rem; height: 1.68rem }

/* On a narrow screen a right-aligned mark reads as stranded rather than placed, so it
   centres under the hero it belongs to. */
@media (max-width: 46rem) {
  .built { margin-top: 2.1rem }
  .built-marks { gap: 1rem }
}

.hero-centre { text-align: center; padding-bottom: clamp(2rem, 4vw, 3rem) }
.hero-centre h1 { max-width: 20ch; margin-inline: auto }
.hero-centre .hero-sub { max-width: 54ch; margin-inline: auto }
.hero-centre .hero-acts { justify-content: center }

/* ---------- the firing field -------------------------------------------- */
/*
   THE LAMP, AND THE BLACK IT STANDS IN.

   A sphere of beacon light, grooved, that the reader flies into as they scroll. It replaces
   the lattice of firing points that used to sit behind this page -- not because that was
   wrong, but because two ambient systems on one screen is one too many, and the sphere is
   the one that says what this place is: every grain of light in the catalogue, on one body,
   and you go inside it.

   THE GROUND IS BLACK, AND THAT IS STRUCTURAL. The grains are drawn additively, so they sum
   where they overlap and a dense ridge glows. Additive light over paper washes to white and
   the whole effect dies. So the stage is black wherever the lamp is lit, and the page
   returns to its warm stock the moment the lamp is gone. Nothing else on the site changes:
   this is one section of one page, not a second theme.

   THE STAGE IS TWO AND A HALF SCREENS TALL. The first holds the hero. The second is the
   flight, because a camera needs somewhere to travel and a stage that ended with the hero
   would make the sphere snap forward rather than approach. The last half screen is the room
   the arrival needs: at two screens the fade back to paper began while the reader was still
   reading, and washed the bottom quarter of it out.
*/
.stage {
  position: relative;
  /* Full-bleed out of `.wrap`, and its gutter put back, so the black runs edge to edge
     while the text inside stays on the same measure as every other section. */
  margin-inline: calc(50% - 50vw);
  padding-inline: max(var(--gutter), calc(50vw - var(--max) / 2));
  background: #000;
  min-height: 250vh;
  /* Light on black for everything inside, taken from the dark theme's own inks rather than
     invented here, so the hero reads as this site at night and not as a foreign panel. */
  --ink: #F2F4F1;
  --muted: #A8AFB4;
  --faint: #98A0A7;
  --rule: #23282C;
  --rule-firm: #3A434B;
  --surface: #0B0D0F;
  --signal: #EFAE52;
  --signal-fill: #E8A33F;
  /* The ground itself, so anything that inverts against it inverts the right way -- the
     filled button is `background: var(--ink); color: var(--bg)`, and without this it drew
     a light button with light text on it. */
  --bg: #000;
  color: var(--ink);
}
/* THE SCRIM. The sphere is now big enough that the words sit ON it rather than above it, and
   type over a field of moving lights is type nobody reads. A soft fall of black down from the
   top of the stage puts a ground back under them without drawing a panel: it is strongest
   where the first line sits and gone by the middle of the sphere. */
.stage::before {
  content: ""; position: fixed; inset: 0 0 auto 0; height: 29vh;
  /* ABOVE THE CANVAS, and that is not a detail: a pseudo-element is generated as the FIRST
     child of its box, so at the same z-index the canvas painted over it and the scrim did
     nothing at all. Canvas 0, scrim 1, mark 2, words 3. */
  z-index: 1;
  background: linear-gradient(to bottom,
    rgba(0, 0, 0, 0.96) 0%, rgba(0, 0, 0, 0.92) 40%, rgba(0, 0, 0, 0.55) 74%,
    rgba(0, 0, 0, 0) 100%);
  pointer-events: none;
}

/* The seam. Without it the black ends on a hard horizontal line halfway down a scroll, which
   reads as a rendering fault rather than as the end of a section. */
.stage::after {
  content: ""; position: absolute; inset-inline: 0; bottom: 0; height: 14vh;
  background: linear-gradient(to bottom, transparent, var(--paper));
  pointer-events: none;
}

/* The canvas and the mark are FIXED, not absolute: the flight is a camera move, and a
   surface that scrolled away under it would fight the camera rather than carry it. Both are
   faded out by the script before the stage ends. */
.lamp-canvas {
  position: fixed; inset: 0; z-index: 0;
  display: block; width: 100%; height: 100%;
  pointer-events: none;
  /* CANDLELIGHT, CHAMPAGNE, BRONZE -- not orange. The site's signal amber is a warning
     colour doing a warning colour's job elsewhere; sixteen thousand grains of it reads as
     gold dust. Pulled pale and desaturated, the same light reads as metal. */
  --lamp-hot:  255, 244, 220;
  --lamp-mid:  232, 195, 106;
  --lamp-deep: 138, 106, 42;
  --lamp-gain: 1;
}
/* THE ARRIVAL. Centred in the viewport rather than in the page, because by the time it is
   read the reader is inside the sphere and there is no page left to be centred in. It starts
   invisible and is raised by the script -- the one place on this site where that is right,
   because it is addressed to somebody who has already travelled to reach it. */
.stage-inside {
  position: fixed; left: 50%; top: 50%; z-index: 3;
  width: min(86rem, calc(100vw - 2 * var(--gutter)));
  transform: translate(-50%, -50%);
  /* THE DROP ON THE LEFT, THE LINE THAT ARGUES FOR IT ON THE RIGHT. A reader who has just been
     told to stop being paid in stars should not have to go looking for the alternative. */
  /* The zone takes the larger share: it is a target you aim a folder at, and the line beside
     it is one sentence. Equal halves gave the sentence more room than it needs and the target
     less than it deserves. */
  display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  align-items: center; gap: clamp(2rem, 4vw, 3.75rem);
  text-align: left; opacity: 0;
  /* NOT CLICKABLE UNTIL IT IS READABLE. The block is over the whole viewport at all times, so
     without this it would swallow clicks meant for the hero while invisible. `lamp.js` turns it
     on with the same number that fades it in. */
  pointer-events: none;
  will-change: opacity, transform;
}
.stage-inside h2 {
  font-size: clamp(1.45rem, 2.5vw, 2.15rem);
  font-weight: 400; font-stretch: 100%; letter-spacing: -0.01em; line-height: 1.2;
  color: #F6F3EC; margin: 0;
}
/* The line under the claim. Quieter and smaller, because it is the thought the claim rests on
   rather than a second claim -- a reader who agrees with the sentence above it has already
   agreed with this one. */
.inside-line {
  margin: 1rem 0 0; color: #A8AFB4;
  font-size: clamp(0.95rem, 1.15vw, 1.15rem); line-height: 1.5; letter-spacing: 0.004em;
}
/* The zone keeps the sell page's own behaviour; its ground changes because here it sits on
   black rather than on paper, and it is scaled up because here it is the SUBJECT. On the sell
   page it is one step in a form somebody arrived already meaning to fill in; here it is half
   the screen and has to carry the same weight as the line beside it. Scoped to this block, so
   the sell page's own zone is untouched. */
/* The instruction over the zone. Sized under the line beside it, because that line is the
   argument and this is the label on the thing that answers it. */
.inside-drop-h {
  margin: 0 0 0.9rem; color: #F6F3EC;
  font-size: clamp(1.05rem, 1.35vw, 1.3rem);
  font-weight: 620; letter-spacing: -0.01em;
}
.inside-drop .dropzone {
  /* A SURFACE, NOT A HOLE CUT IN THE PAGE, and not a solid panel either. At 3% white it was
     reading as the star field with a dashed line round it, so the target had no body -- and the
     one thing on this screen you are meant to aim at looked like the least substantial thing on
     it. Given a ground it is opaque, and the field stops travelling behind it.

     So: glass. Warm rather than grey, because the whole page is lit amber and a neutral panel on
     it reads as dead. Lit from the top, which is where the light comes from here. Still see-
     through, so the stars carry on underneath. The hairline inside the top edge does most of the
     work -- it is the difference between a tinted rectangle and something with a surface. */
  background:
    linear-gradient(180deg, rgba(240, 178, 84, 0.3), rgba(255, 246, 232, 0.11) 48%, rgba(255, 255, 255, 0.17));
  /* Glass, so the field still travels behind it. The blur is small on purpose: enough that the
     stars under the panel go soft and read as being BEHIND something, not enough to smear them
     into a grey wash and lose the depth that makes it worth doing. */
  backdrop-filter: blur(12px) saturate(130%);
  -webkit-backdrop-filter: blur(12px) saturate(130%);
  box-shadow: inset 0 1px 0 rgba(255, 240, 214, 0.28);
  border-color: rgba(246, 243, 236, 0.3);
  min-height: 15rem;
  padding: clamp(1.75rem, 5vw, 2.75rem) 1.75rem;
}
/* On hover it warms rather than changing colour: the same surface with the light turned up. */
.inside-drop .dropzone:hover, .inside-drop .dropzone:focus-visible, .inside-drop .dropzone.is-over {
  border-color: var(--signal-fill);
  background:
    linear-gradient(180deg, rgba(240, 178, 84, 0.42), rgba(255, 246, 232, 0.15) 48%, rgba(255, 255, 255, 0.2));
  box-shadow: inset 0 1px 0 rgba(255, 240, 214, 0.4);
}
.inside-drop .dropzone-icon { width: 3rem; height: 3rem; margin-bottom: 1.1rem }
.inside-drop .dropzone b { font-size: clamp(1.25rem, 1.6vw, 1.5rem); margin-bottom: 0.5rem }
.inside-drop .dropzone span { font-size: clamp(0.9rem, 0.95vw, 1rem) }
.inside-drop .help { color: #A8AFB4; text-align: left; font-size: 0.875rem }
@media (prefers-reduced-motion: reduce) { .stage-inside { transition: none } }

/* Everything the reader is meant to read sits above both. Targeted at the hero rather than
   at every child: a `.stage > *` rule also caught the canvas and the mark, put them back in
   flow, and pushed the whole hero off the bottom of the screen. */
.stage .hero { position: relative; z-index: 3 }

/*
   THE MASTHEAD, OVER THE BLACK. On this one page the bar sits directly on the stage with no
   ground of its own, the way it does on any page that opens on a full-bleed image -- a white
   strip above a black hero reads as two pages stacked, not one. It takes back its paper the
   moment the stage is behind you, which is what `is-past-stage` is for; the script sets it.
*/
.has-stage .masthead {
  /* BLACK, NOT TRANSPARENT, and the difference is not cosmetic: the stage begins BELOW the
     bar in the flow, so a transparent bar shows the paper behind it and reads as a white
     strip above a black hero. Painting it the stage's own black makes the seam disappear. */
  background: #000; border-bottom-color: transparent;
  --ink: #F2F4F1; --muted: #A8AFB4; --faint: #98A0A7;
  --signal: #EFAE52; --signal-fill: #E8A33F;
  --surface: rgba(255, 255, 255, 0.06); --rule: rgba(255, 255, 255, 0.14);
  color: var(--ink);
  transition: background-color 0.25s ease-out, border-color 0.25s ease-out;
}
/* `revert`, not `initial`: on a custom property `initial` is the guaranteed-invalid value,
   so every `var(--ink)` under this bar would have resolved to nothing rather than to the
   page's own ink. `revert` drops this rule's override and lets the value from `:root`
   through, which is what was meant. */
.has-stage .masthead.is-past-stage {
  background: var(--bg); border-bottom-color: var(--rule-firm);
  --ink: revert; --muted: revert; --faint: revert;
  --signal: revert; --signal-fill: revert; --surface: revert; --rule: revert;
}

/* THE HERO, ON THE STAGE.
   Text above, sphere below it, and the numbers along the foot -- the headline is read first,
   the lamp is what the eye falls to, and the proof is there for anyone who looks down.

   IT IS SET LIGHTER AND WIDER HERE THAN ANYWHERE ELSE ON THE SITE, and that is the whole
   difference in register. Every other page uses the display face at 700 and 92% width, which
   is how a headline gets authority on paper. On black, at this size, that same setting reads
   as loud. Weight comes off, the width axis goes back to normal, the tracking opens, and the
   line gets room -- which is what expensive looks like: fewer marks, further apart. */
.stage .hero {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  min-height: calc(100dvh - 3.6rem);
  padding-top: clamp(0.7rem, 1.7vh, 1.2rem); padding-bottom: clamp(1.25rem, 3vh, 2rem);
}
/* THE WORDS GIVE THE SPHERE THE SCREEN. They are set small here -- smaller than the same
   headline anywhere else on the site -- because the sphere is the subject and type at hero
   scale in front of it turns it back into a decoration behind a poster.

   IT IS BIGGER THAN THAT ARGUMENT ALLOWED. The line is short enough to hold one line at this
   size, so it costs the sphere nothing off the top of the screen and reads as the page's
   first sentence rather than as a caption over it. The ceiling is what keeps that true: past
   it the line wraps, and every line it wraps to is a line of sphere lost. */
.stage .hero h1 {
  max-width: 26ch; margin-inline: auto; text-wrap: balance;
  font-size: clamp(1.85rem, 3.05vw, 2.7rem);
  font-weight: 400; font-stretch: 100%; letter-spacing: -0.012em; line-height: 1.12;
}
/* Raised with the headline, and still the smaller of the two: it is the explanation, not the
   claim, and a sub-heading that matches its headline leaves neither of them leading. Held to
   two lines by its measure -- every line this runs to is a line the sphere loses off the top
   of the screen. */
.stage .hero-sub {
  margin-top: 1.05rem; margin-inline: auto; max-width: 58ch;
  font-size: clamp(0.95rem, 1.05vw, 1.125rem); line-height: 1.55; letter-spacing: 0.004em;
  /* Three short sentences on two lines rag badly left to themselves -- one line full and a
     stub under it. Balanced, they break at the sentence instead. */
  text-wrap: balance;
  color: #A0A6AB;
}
.stage .hero-acts { justify-content: center; margin-top: 1.6rem; gap: 0.75rem }
.stage .btn { font-size: 0.875rem; min-height: 0 }

/* The buttons soften to pills here, and only here. A hard-cornered rectangle is the right
   control on an instrument panel, which is what the rest of this site is; on the black it
   reads as a dialog button dropped onto a photograph. */
.stage .btn {
  border-radius: 999px; padding: 0.95rem 2.4rem; font-weight: 500; letter-spacing: 0.002em;
}
.stage .btn-ghost { border-color: rgba(232, 195, 106, 0.38); color: #EFE6D2 }
.stage .btn-ghost:hover { border-color: rgba(232, 195, 106, 0.75); background: rgba(232, 195, 106, 0.07) }

/* THE FOOT OF THE FIRST SCREEN. The one instruction on the left -- the reader is told to
   scroll, because a page whose first screen is mostly black gives no other clue that there is
   more -- and what it is built on at the far right, as two marks and nothing else.

   The counts that used to sit here are gone, and so is the instruction to scroll. The counts
   were a second printing of two numbers the readout above already states; the instruction was
   telling a reader to do the one thing they were already doing. */
.stage-foot {
  margin-top: auto; width: 100%;
  display: flex; align-items: flex-end; justify-content: flex-end; gap: 1.5rem;
  padding-top: clamp(1.5rem, 4vh, 2.75rem);
}
.stage .built { margin: 0 }

/* THE BLACK HAS TO END INSIDE ONE SCREEN ON A PHONE.
   `min-height: 250vh` is two and a half screens of it before anything for sale is reachable, and
   on a phone that is the whole first impression: scroll, scroll, scroll, still black. It buys
   room for the sphere to be looked at, which is a thing somebody does on a desk and not on a
   train. So under 700px the stage is one viewport, the figures and the panel of copy inside the
   sphere come off, and the skills start on the second screen instead of the fourth.
   `svh` is the SMALL viewport height -- what is visible with the mobile browser's toolbars still
   showing -- so the hero does not hide a strip of itself behind them at rest. `vh` first for
   anything that does not know the unit. Desktop is untouched. */
@media (max-width: 699px) {
  .stage { min-height: 100vh; min-height: 100svh }
  /* THE MARKS STAY ON A PHONE. This row was hidden here when it carried two figures and an
     instruction to scroll that a phone had no room for. Those are gone; what is left is what the
     marketplace is built on, which is the same claim to a reader on a phone as to one on a desk.
     It sits under the hero rather than across it, because at this width a row has nowhere to go. */
  .stage-foot { justify-content: flex-start; padding-top: 2rem }
  /* THE ARRIVAL STOPS BEING AN ARRIVAL ON A PHONE. There is no room to fly through a sphere and
     then read something inside it, and the drop zone now lives in here -- hiding the block
     outright would take the one control this page exists for off every phone. So it comes out
     of the overlay, stacks, and sits in the hero like an ordinary section. The script writes
     opacity and transform inline every frame, which is why these have to shout. */
  #lamp-inside {
    position: static; transform: none !important; opacity: 1 !important;
    pointer-events: auto !important;
    display: flex; flex-direction: column-reverse; gap: 1.5rem;
    width: 100%; margin-top: 2rem;
  }
  .stage { min-height: auto; padding-bottom: 3rem }
  .stage-inside h2 { font-size: clamp(1.35rem, 5.5vw, 1.75rem) }
}

/* ---------- the readout, as a strip ------------------------------------- */
.readout-strip {
  display: grid; gap: 0; grid-template-columns: repeat(2, minmax(0, 1fr));
  border-top: 1px solid var(--rule); max-width: 46rem; margin: clamp(2.25rem, 5vw, 3.5rem) auto 0;
}
@media (min-width: 44rem) { .readout-strip { grid-template-columns: repeat(4, minmax(0, 1fr)) } }
/* Inside the hero disclosure the strip no longer needs its own top gap -- the summary above it
   provides the separation, and keeping both pushed the skills a screen further down. The
   summary is centred here and only here, because in the hero it sits under centred text. */
#readout { max-width: 46rem; margin: clamp(2.25rem, 5vw, 3.5rem) auto 0; }
#readout > summary { justify-content: center; }
#readout .readout-strip { margin-top: 0; }
.rs-item {
  display: flex; flex-direction: column; gap: 0.3rem; align-items: center;
  padding: 0.9rem 0.75rem; border-bottom: 1px solid var(--rule);
}
@media (min-width: 44rem) { .rs-item { border-bottom: 0 } .rs-item + .rs-item { border-left: 1px solid var(--rule) } }
.rs-k {
  font-size: 0.625rem; font-weight: 620; font-stretch: 108%; text-transform: uppercase;
  letter-spacing: 0.14em; color: var(--muted);
}
.rs-v { font-family: var(--mono); font-size: 0.8125rem; font-weight: 500; letter-spacing: -0.01em; color: var(--ink) }
.rs-v.is-live { color: var(--signal) }

/* ---------- buttons ------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: var(--tap); padding: 0.65rem 1.3rem;
  border: 1px solid var(--ink); border-radius: var(--radius-sm);
  background: var(--ink); color: var(--bg);
  font-family: inherit; font-size: var(--t-sm); font-weight: 600; line-height: 1.2;
  letter-spacing: -0.005em; cursor: pointer; text-align: center;
  transition: transform 0.06s ease, background-color 0.14s ease, border-color 0.14s ease, color 0.14s ease;
}
.btn:hover:not(:disabled) { background: color-mix(in srgb, var(--ink) 86%, var(--bg)) }
.btn:active:not(:disabled) { transform: translateY(1px) }
.btn:disabled { opacity: 0.42; cursor: not-allowed }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--rule-firm) }
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); border-color: var(--ink) }
.btn-block { width: 100% }
.btn-lg { min-height: 3.25rem; font-size: var(--t-base); padding: 0.8rem 1.6rem }
.btn-sm { min-height: var(--tap); padding: 0.4rem 0.85rem; font-size: var(--t-xs) }
.linklike {
  background: none; border: 0; padding: 0; font: inherit; color: var(--signal);
  text-decoration: underline; text-decoration-thickness: 1px;
  text-underline-offset: 0.2em; cursor: pointer;
}
.linklike:hover { color: var(--ink) }
/*
   An inline link in running prose cannot be 44px tall without wrecking the line it
   sits in, which is exactly why WCAG exempts targets inside a sentence. So the hit
   area is grown rather than the box: an invisible overlay centred on the link, with
   no layout effect at all. A thumb finds it; the paragraph never knows.
*/
.linklike { position: relative }
.linklike::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: var(--tap); transform: translateY(-50%);
}
/* Where a link stands alone rather than inside a sentence, it gets the real height
   instead of a phantom one. */
.help .linklike, .hint .linklike, li > .linklike,
.section-head .linklike { padding: 0.78rem 0; display: inline-block }

/* A copy control. Defined here because `format.js` has always emitted `.copy-btn`
   and the stylesheet it was styled in is no longer loaded by any page. */
.copy-btn {
  background: transparent; color: var(--muted);
  border: 1px solid var(--rule-firm); border-radius: var(--radius-sm);
  padding: 0.45rem 0.75rem; min-height: var(--tap);
  font-family: var(--sans); font-size: var(--t-xs); font-weight: 560;
  cursor: pointer; margin-top: 0.5rem;
  /* These labels are contract addresses -- forty-two characters with no spaces in them,
     so nothing about the text offers a place to break. At 360px one measured 350px wide
     starting 20px in, which put it 10px past the viewport and 30px past its own container,
     and a button hanging off the edge of a phone is one nobody can press. `anywhere` rather
     than `break-word` because a hex string has no word boundaries to prefer. */
  max-width: 100%; overflow-wrap: anywhere; text-align: left;
}
.copy-btn:hover { color: var(--ink); border-color: var(--ink) }

/* ---------- state marks -------------------------------------------------- */
/*
   These were pills, and everything was one: what you get, whether anyone had
   reviewed it, how many copies exist, and whether it could reach your wallet
   all wore the same rounded outline. When every fact is a badge, the badge
   that matters is invisible. So the ordinary facts are now just a line of
   small caps, and only a real warning keeps a mark of its own.
*/
.chip {
  display: inline-flex; align-items: center; gap: 0.35rem;
  font-size: var(--t-xs); font-weight: 500; line-height: 1.5;
  color: var(--muted); white-space: nowrap; letter-spacing: 0.01em;
}
.chip + .chip { padding-left: 0.55rem; border-left: 1px solid var(--rule) }
.chip-proved   { color: var(--signal); font-weight: 600 }
.chip-danger {
  color: var(--flag); font-weight: 600; border-left: 0;
  padding: 0.15rem 0.5rem 0.15rem 0.45rem;
  background: var(--flag-bg); box-shadow: inset 2px 0 0 var(--flag);
}
.chip-danger + .chip, .chip + .chip-danger { border-left: 0; padding-left: 0.45rem }
.chip-dot { width: 0.4rem; height: 0.4rem; border-radius: 50%; background: currentColor; flex: none }

/* ---------- the index: one skill, one ruled row -------------------------- */
/*
   Twenty-five rounded white boxes in a three-up grid, most of them titled with
   a 64-character fingerprint and carrying a paragraph of empty space above the
   price. A card earns its box when it has an image to hold; with nothing but
   text inside, the box is a border drawn around a border.

   So the catalog is a ruled index — an auction list, a chart table — where the
   hairline does the separating and the paper does the rest. It reads faster, it
   handles a row with no published name without shouting, and it is a fifth of
   the height on a phone.
*/
.index { display: grid; gap: 0; border-top: 1px solid var(--rule-firm) }
.index > .card { border-bottom: var(--hair) }

.card {
  display: flex; flex-direction: column; gap: 0.2rem;
  background: none; border: 0; border-radius: 0;
  padding: 0.9rem 0.25rem; color: inherit;
  transition: background-color 0.16s ease, box-shadow 0.16s ease;
}
a.card { cursor: pointer }
a.card:hover { background: var(--surface); box-shadow: inset 2px 0 0 var(--signal-fill) }

/* The catalogue row. The id sits out in the left margin as a lot number, which is
   what gives a long ruled list its spine, and the price runs down its own column so
   the eye can compare without reading. */
.index > .card {
  display: grid; align-items: baseline;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  column-gap: 0.8rem; row-gap: 0.2rem;
  padding: 0.85rem 0.15rem 0.9rem;
}
.index > .card > .card-lot { grid-column: 1; grid-row: 1 }
.index > .card > .card-top,
.index > .card > .card-desc,
.index > .card > .card-meta,
.index > .card > .card-foot { grid-column: 2 }
.index > .card > .card-foot { justify-content: flex-end; margin-top: 0.35rem }
@media (min-width: 46rem) {
  .index > .card {
    grid-template-columns: 3.25rem minmax(0, 1fr) 9rem;
    column-gap: 1.75rem; padding: 0.95rem 0.5rem 1rem;
  }
  .index > .card > .card-foot { grid-column: 3; grid-row: 1 / span 3; margin-top: 0 }
}
.card-lot {
  font-family: var(--mono); font-size: 0.75rem; color: var(--faint);
  letter-spacing: -0.01em; font-variant-numeric: tabular-nums;
}
.card-top { display: flex; align-items: baseline; gap: 0.55rem; flex-wrap: wrap }
.card-name {
  font-family: var(--serif); font-size: var(--t-xl); font-weight: 600;
  letter-spacing: -0.005em; line-height: 1.25;
}
/* No name has been proved, so the fingerprint stands in — and changes voice, which
   is the tell that nobody chose those characters. */
.card-name.is-hash {
  font-family: var(--mono); font-size: var(--t-sm); font-weight: 400;
  color: var(--muted); word-break: break-all; letter-spacing: -0.02em;
}
/* `Skill #12` is a number the contract assigned, not a name a person chose, so it
   does not get the creator's voice. Setting a serial in a book serif is a small lie
   about who wrote it — and this system spends its whole time on exactly that
   distinction, so it is not a small lie here. */
.card-name.is-untitled, .picker-name.is-untitled {
  font-family: var(--sans); font-weight: 620; letter-spacing: -0.015em;
  font-size: var(--t-base); color: var(--muted);
}
.card-desc {
  font-family: var(--serif); font-size: var(--t-sm); color: var(--muted);
  line-height: 1.5; margin: 0.1rem 0 0; max-width: 64ch;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* One line above the list when some names could not be loaded. Interface voice, not the
   creator's serif, because it is the site talking about itself. Quiet on purpose: the
   prices are unaffected and nothing here is unbuyable, so this is a note and not an alarm. */
.preview-note {
  font-family: var(--sans); font-size: var(--t-xs); color: var(--muted);
  margin: 0 0 0.75rem; max-width: 70ch;
}
.preview-note[hidden] { display: none }
.card-meta { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-top: 0.35rem }
.card-id {
  font-family: var(--mono); font-size: 0.75rem; color: var(--faint);
  letter-spacing: -0.01em; white-space: nowrap;
}
.card-foot {
  margin-top: 0.4rem; padding-top: 0;
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
}
.card-price { font-variant-numeric: tabular-nums;
  font-size: var(--t-xl); font-weight: 660; letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.card-price small { font-size: 0.6875rem; font-weight: 500; color: var(--muted); margin-left: 0.3rem; letter-spacing: 0 }

/* A price that could not be proved is replaced, never decorated. */
.price-withheld {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: var(--t-sm); font-weight: 600; color: var(--deep); white-space: nowrap;
}

/* Where a card really is a card — the library and the console list — it keeps a
   surface, because there it is an object you act on rather than a row you scan.
   The 1px grid gap over a ruled ground draws the separators, so no child needs a
   border of its own and nothing ends up boxed inside a box. */
.grid { display: grid; gap: 1px; grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr)); background: var(--rule); border: 1px solid var(--rule) }
.grid > * { background: var(--bg) }
.grid > .card { padding: 1.1rem 1.15rem 1.15rem; height: 100% }
.grid > .card .card-foot { margin-top: auto; padding-top: 0.85rem; border-top: var(--hair) }
.grid > a.card:hover { background: var(--surface); box-shadow: inset 0 2px 0 var(--signal-fill) }

/* ---------- sections ------------------------------------------------------ */
.section { padding: clamp(2.25rem, 5vw, 3.5rem) 0 }
.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 1.15rem; flex-wrap: wrap }
.section-head h2 { font-size: var(--t-2xl); letter-spacing: -0.025em; font-stretch: 96% }
.eyebrow {
  display: block; font-size: 0.6875rem; font-weight: 620; color: var(--signal);
  text-transform: uppercase; letter-spacing: 0.15em; margin-bottom: 0.4rem;
}
.count-note { font-size: var(--t-xs); color: var(--muted); font-variant-numeric: tabular-nums }


/* ---------- the chain of custody ---------------------------------------- */
/*
   Three stages of one thing, not three features. They hang from a single continuous
   rule with an amber node at each stage, so the eye reads a progression rather than a
   row of boxes — and the last column is wider than the other two, because the last
   stage is the one nobody else offers and the composition should say so.
*/
/* ---------- ownership -----------------------------------------------------------
   THE SAME REGISTER AS THE HERO, AND FOR THE SAME REASON. It was four paragraphs in a list --
   everything on screen at once, nothing leading, and it read like documentation next to the
   thing above it. What makes the hero work is not the sphere; it is that one idea gets the
   whole screen. So: one claim to a beat, each beat about a screen tall, set large enough that
   it is the only thing being read. Fewer marks, further apart. */
.own-sec { padding-top: clamp(5rem, 12vh, 9rem) }
.own-head { min-height: 62vh; display: flex; flex-direction: column; justify-content: center }
.own-h {
  margin: 0.9rem 0 0; max-width: 20ch;
  font-size: clamp(2.35rem, 5.2vw, 4.5rem);
  font-weight: 400; font-stretch: 100%; letter-spacing: -0.028em; line-height: 1.04;
}
.own-lede {
  margin: 1.6rem 0 0; max-width: 40ch; color: var(--muted);
  font-size: clamp(1.05rem, 1.35vw, 1.35rem); line-height: 1.55;
}

.own { list-style: none; margin: 0; padding: 0 }
/* Without the script this is what it is: four claims down the page, each with room. */
.own-beat {
  min-height: 78vh; display: flex; flex-direction: column; justify-content: center;
  padding: clamp(2rem, 6vh, 4rem) 0; border-top: var(--hair);
}

/* ---------- the pinned sequence ------------------------------------------------
   `.own-stage` is scroll LENGTH and nothing else -- one screen per claim, plus one so the last
   one is read before the page moves on. The panel inside pins, and `ownership.js` decides which
   claim is showing from how far through that length you are. */
/* `svh`, NOT `vh`, AND THAT IS THE WHOLE OF MOBILE HERE.
   On a phone `100vh` is the height with the browser's toolbars HIDDEN, which is taller than what
   the reader can see while scrolling. Pinned at that height the panel is bigger than the screen
   it is pinned to: the claim sits low, its foot hides under the toolbar, and the scroll length
   stops matching the number of screens the sequence assumes. `svh` is the small viewport -- what
   is visible with the toolbars showing -- so the panel is never taller than its window. `vh`
   first, for anything that predates the unit. */
.own-ready .own-stage {
  height: calc(var(--own-beats, 4) * 100vh + 100vh);
  height: calc(var(--own-beats, 4) * 100svh + 100svh);
}
.own-ready .own-pin {
  position: sticky; top: 0;
  height: 100vh;
  height: 100svh;
  /* CENTRED IN THE SPACE A READER CAN ACTUALLY SEE, not in the viewport. The bar across the top
     covers the first 3.6rem of it, so centring on the whole height puts every claim that much
     high. Padding the top by the bar's own height leaves the remainder to centre in, which puts
     the middle of the claim in the middle of what is visible. */
  display: flex; align-items: center;
  padding-top: 3.6rem;
}
/* THE LIST IS THE STACK, not the panel around it.
   This was on `.own-pin`, whose only child is the list -- so the beats, being the list's
   children, never shared a cell at all. All four sat in normal flow inside a box one screen
   tall: the first ran up under the masthead, the last ran off the bottom, and nothing was
   centred. One element out. */
.own-ready .own { display: grid; width: 100% }
/* Every claim in the SAME cell, so one replaces another in place rather than the page moving
   under them. */
.own-ready .own-beat {
  grid-area: 1 / 1; min-height: 0; padding: 0; border: 0;
  opacity: 0; visibility: hidden;
  transform: translateY(30px);
  transition:
    opacity 1.05s cubic-bezier(0.16, 0.84, 0.3, 1),
    transform 1.05s cubic-bezier(0.16, 0.84, 0.3, 1),
    visibility 0s linear 1.05s;
}
.own-ready .own-beat.is-on {
  opacity: 1; visibility: visible; transform: none;
  transition:
    opacity 1.05s cubic-bezier(0.16, 0.84, 0.3, 1),
    transform 1.05s cubic-bezier(0.16, 0.84, 0.3, 1),
    visibility 0s;
}
/* THE PARTS ARRIVE IN THE ORDER THEY ARE READ. The numeral is the cue and leads; the claim
   follows it; the line under it last, by which time the eye is already on the claim. Each is
   given its own distance to travel, so they do not move as one block. */
.own-ready .own-beat .own-n,
.own-ready .own-beat .own-t,
.own-ready .own-beat .own-d {
  opacity: 0; transform: translateY(22px);
  transition: opacity 0.95s cubic-bezier(0.16, 0.84, 0.3, 1), transform 0.95s cubic-bezier(0.16, 0.84, 0.3, 1);
}
.own-ready .own-beat.is-on .own-n { opacity: 1; transform: none; transition-delay: 0.05s }
.own-ready .own-beat.is-on .own-t { opacity: 1; transform: none; transition-delay: 0.16s }
.own-ready .own-beat.is-on .own-d { opacity: 1; transform: none; transition-delay: 0.3s }

/* A hairline that grows under the numeral as the claim settles -- the one moving mark in the
   section, and the thing that makes it read as measured rather than merely faded in. */
.own-ready .own-beat .own-n::after {
  content: ""; display: block; height: 1px; width: 0; margin-top: 0.85rem;
  background: linear-gradient(90deg, var(--signal-fill), rgba(232, 163, 63, 0));
  transition: width 1.4s cubic-bezier(0.16, 0.84, 0.3, 1) 0.24s;
}
.own-ready .own-beat.is-on .own-n::after { width: min(22rem, 60%) }

@media (prefers-reduced-motion: reduce) {
  .own-ready .own-stage { height: auto }
  .own-ready .own-pin { position: static; height: auto; display: block; padding-top: 0 }
  .own-ready .own { display: block }
  .own-ready .own-beat {
    grid-area: auto; min-height: 62vh; padding: clamp(2rem, 6vh, 4rem) 0;
    border-top: var(--hair); opacity: 1; visibility: visible; transform: none; transition: none;
  }
  .own-ready .own-beat .own-n, .own-ready .own-beat .own-t, .own-ready .own-beat .own-d {
    opacity: 1; transform: none; transition: none;
  }
  .own-ready .own-beat .own-n::after { display: none }
}
.own-n {
  display: block; font-family: var(--mono); font-size: var(--t-sm);
  letter-spacing: 0.22em; color: var(--signal);
}
.own-t {
  margin: 1.5rem 0 0; max-width: 17ch;
  font-size: clamp(1.9rem, 4.2vw, 3.4rem);
  font-weight: 400; font-stretch: 100%; letter-spacing: -0.026em; line-height: 1.08;
}
.own-d {
  margin: 1.75rem 0 0; max-width: 44ch; color: var(--muted);
  font-size: clamp(1rem, 1.2vw, 1.2rem); line-height: 1.65;
}
.own-close {
  min-height: 52vh; display: flex; flex-direction: column; justify-content: center;
  margin: 0; max-width: 30ch; border-top: var(--hair);
  font-size: clamp(1.35rem, 2.4vw, 2.1rem); font-weight: 400;
  letter-spacing: -0.018em; line-height: 1.3; color: var(--ink);
}

/* THE ARRIVAL.
   Hidden only once `reveal.js` has said it is running -- see the note there about failing
   visible. Up rather than in from the side: a sideways entrance draws attention to the motion,
   and the motion is not the point. Slow to start and long to settle, which is the difference
   between something arriving and something being switched on.

   EVERY PART OF A BEAT COMES IN SEPARATELY -- the numeral, then the claim, then the line under
   it -- so the grouping assembles in the order it is read rather than appearing whole. */
.reveal-ready [data-reveal] {
  opacity: 0; transform: translateY(26px);
  transition: opacity 1s cubic-bezier(0.16, 0.84, 0.3, 1), transform 1s cubic-bezier(0.16, 0.84, 0.3, 1);
}
.reveal-ready [data-reveal].is-in { opacity: 1; transform: none }
/* The stagger inside a beat. The numeral is the cue, so it leads; the claim follows it; the
   explanation last, by which time the reader is already on the claim. */
.reveal-ready .own-t { transition-delay: 0.1s }
.reveal-ready .own-d { transition-delay: 0.2s }
.reveal-ready .own-h { transition-delay: 0.08s }
.reveal-ready .own-lede { transition-delay: 0.18s }
@media (prefers-reduced-motion: reduce) {
  .reveal-ready [data-reveal] { opacity: 1; transform: none; transition: none }
}
@media (max-width: 46rem) {
  .own-head { min-height: 46vh }
  .own-beat { min-height: 62vh }
  .own-close { min-height: 40vh }
}
}


/* What a creator needs before they start. A ruled list of three requirements, not
   three boxes side by side — the second three-up grid on the site would have made
   the shape look like a template rather than a decision. */
.needs { border-top: 2px solid var(--ink); margin-top: 2.25rem }
.need { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.2rem 2.5rem; padding: 1rem 0; border-bottom: var(--hair) }
@media (min-width: 48rem) { .need { grid-template-columns: 19rem minmax(0, 1fr); gap: 0 2.5rem; align-items: baseline } }
.need-t { font-size: var(--t-sm); font-weight: 620; letter-spacing: -0.01em }
.need-d { color: var(--muted); font-size: var(--t-sm); margin: 0; line-height: 1.6; max-width: 56ch }

/* ---------- the connect gate -------------------------------------------------
   Shown once a folder has been read, and never before: the whole point of the
   flow is that a visitor meets the work, not an errand. See `lib/connect.js`.

   Built from the same flat vocabulary as everything else -- a rule, weight and
   space, no fill and no radius -- because a rounded callout box here would read
   as borrowed from somewhere else, and this is the most important thing on the
   page at the moment it appears.

   Every colour is a token, so both themes come for free.
*/
.gate { border-top: 2px solid var(--ink); padding: 1.35rem 0 0.25rem; margin-top: 1.75rem }
.gate-t { font-size: var(--t-lg); font-weight: 620; letter-spacing: -0.015em; text-wrap: balance }
.gate-note { color: var(--muted); font-size: var(--t-sm); line-height: 1.6; margin: 0.5rem 0 0; max-width: 58ch }
.gate-act { margin-top: 1.15rem }
/* Reserves no space when empty, so the card does not jump as errors come and go. */
.gate-err:empty { display: none }
.gate-err { color: var(--flag); font-size: var(--t-sm); line-height: 1.6; margin: 0.85rem 0 0; max-width: 58ch }

/* Connected: the gate stops being a call to action and becomes a quiet receipt. */
.gate.is-ready { border-top-color: var(--rule); padding-bottom: 0 }
.gate-line { display: flex; align-items: baseline; gap: 0.55rem; flex-wrap: wrap }
.gate-addr { font-family: var(--mono); font-size: var(--t-sm); color: var(--ink) }

/* ---------- notes: the three states, as blocks --------------------------- */
/*
   A note used to be a bordered, rounded, filled box — which meant that on the
   skill page a note sat inside the buy panel and produced a card inside a card,
   twice. A note is now a rule with words beside it. Only the two that carry a
   real warning keep a wash of colour behind them.
*/
.note {
  border-left: 2px solid var(--rule-firm); border-radius: 0;
  background: none; padding: 0.15rem 0 0.15rem 0.95rem;
  font-size: var(--t-sm); line-height: 1.6;
}
.note h3, .note strong:first-child { font-size: var(--t-sm); font-weight: 640 }
.note + .note { margin-top: 1rem }

/* Who sold this, when they have a Base name.
   The name leads and the address stays beneath it in the same mono face it always used --
   the address is the fact the chain enforces, the name is a label pointing at it, and
   dropping the address would be trading something true for something merely friendlier. */
.seller-name { display: block; font-weight: 560 }
.seller-addr { display: block; font-size: 0.86em; color: var(--faint); margin-top: 0.1rem }

/* The install procedure, shown the moment a download lands.
   Numbered because these are ORDERED steps -- unzip, restart, ask -- and doing the second
   before the first leaves the buyer looking at an assistant that cannot see what they
   bought. The counter is set in the same mono face the readouts use, so it reads as a
   sequence rather than as decoration. */
.install-steps { margin: 0.5rem 0 0; padding: 0; list-style: none; counter-reset: step }
.install-steps li {
  counter-increment: step; position: relative; padding-left: 1.6rem; margin-top: 0.35rem;
}
.install-steps li::before {
  content: counter(step); position: absolute; left: 0; top: 0.05em;
  font-family: var(--mono); font-size: 0.7em; color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.install-steps code, .install-steps strong { font-weight: 540 }

/* Where to get a wallet. Rendered by lib/getstarted.js in front of every sale this
   marketplace makes, so it is styled like the install steps beside it rather than like a
   generic list: same left rail, same measure, same restraint. */
.wallet-list { margin: 0.55rem 0 0; padding: 0; list-style: none }
.wallet-list li {
  position: relative; padding-left: 1.6rem; margin-top: 0.4rem; line-height: 1.5;
}
.wallet-list li::before {
  content: "\2192"; position: absolute; left: 0; top: 0;
  color: var(--faint); font-family: var(--mono); font-size: 0.8em;
}
.wallet-list a { font-weight: 540 }
.note-info   { border-left-color: var(--deep) }
.note-info strong { color: var(--deep) }
.note-signal { border-left-color: var(--signal-fill) }
.note-signal strong { color: var(--signal) }
.note-danger { border-left-color: var(--flag); background: var(--flag-bg); padding: 0.85rem 1rem 0.9rem 0.95rem }
.note-danger strong { color: var(--flag) }
.note-quiet  { border-left-color: var(--rule); color: var(--muted) }
.note-quiet strong { color: var(--ink) }

/* ---------- forms --------------------------------------------------------- */
.form { display: grid; gap: 1.5rem; max-width: 34rem; min-width: 0 }
.form-row { display: grid; gap: 1.35rem; grid-template-columns: minmax(0, 1fr) }
@media (min-width: 38rem) { .form-row.two { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) } }
.label, .lab { display: block; font-size: var(--t-xs); font-weight: 620; margin-bottom: 0.4rem; color: var(--ink);
  font-stretch: 106%; letter-spacing: 0.02em }
.lab.micro { font-size: var(--t-xs); color: var(--muted); font-weight: 560; text-transform: uppercase; letter-spacing: 0.1em }
.field {
  width: 100%; min-height: var(--tap);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--rule-firm); border-radius: var(--radius-sm);
  padding: 0.6rem 0.75rem; font-family: inherit; font-size: var(--t-base);
}
.field::placeholder { color: var(--faint) }
.field:focus { outline: 2px solid var(--signal-fill); outline-offset: 1px; border-color: var(--signal-fill) }
.field.is-mono { font-family: var(--mono); font-size: var(--t-sm) }
.help, .hint { display: block; margin-top: 0.45rem; font-size: var(--t-xs); color: var(--muted); line-height: 1.55 }
.help strong, .hint strong, .hint b { color: var(--ink); font-weight: 600 }
.field-prefix { position: relative; display: flex; align-items: center }
.field-prefix .field { padding-left: 2rem }
.field-prefix-mark { position: absolute; left: 0.8rem; color: var(--muted); font-size: var(--t-base); pointer-events: none }
/* The first checkbox in this product: a tick the creator makes, beside the sentence it answers.
   Big enough to hit on a phone, and the whole label is the target rather than the box alone. */
.check { display: flex; align-items: center; gap: 0.55rem; min-height: var(--tap); cursor: pointer; font-size: var(--t-sm) }
.check input { width: 1.05rem; height: 1.05rem; accent-color: var(--signal-fill); flex: none; cursor: pointer }

/* ---------- stepper ------------------------------------------------------- */
/* Numbered because a launch IS a sequence: each step can fail differently and
   the order decides what is recoverable. */
.stepper { display: grid; gap: 0; margin: 1.25rem 0; border-top: 1px solid var(--rule-firm); border-bottom: 1px solid var(--rule-firm) }
.step { display: grid; grid-template-columns: 1.9rem minmax(0, 1fr); gap: 0.9rem; padding: 0.8rem 0.25rem; align-items: start }
.step + .step { border-top: var(--hair) }
.step-n {
  width: 1.5rem; height: 1.5rem; border-radius: 50%; display: grid; place-items: center;
  font-family: var(--mono); font-size: 0.6875rem; font-weight: 500; font-variant-numeric: tabular-nums;
  border: 1px solid var(--rule-firm); color: var(--muted); background: none; margin-top: 0.1rem;
}
.step-t { font-size: var(--t-sm); font-weight: 620 }
.step-d { font-size: var(--t-xs); color: var(--muted); margin-top: 0.15rem; line-height: 1.55 }
.step.is-active .step-n { border-color: var(--signal-fill); color: var(--signal); background: var(--signal-bg) }
.step.is-active .step-t { color: var(--signal) }
.step.is-done .step-n { background: var(--signal-fill); border-color: var(--signal-fill); color: #14181D }
.step.is-done .step-t { color: var(--muted) }
.step.is-failed .step-n { background: var(--flag); border-color: var(--flag); color: #FFF }
.step.is-failed .step-t { color: var(--flag) }

/* ---------- panel (the buy box) ------------------------------------------- */
/*
   The one place on the site that still gets a surface and a shadow, because it
   is the one place money moves and it should feel like a separate object resting
   on the page. Everything inside it is therefore flat: a card inside this card
   would undo exactly what the elevation is for.
*/
.panel {
  background: var(--surface); border: 1px solid var(--rule-firm); border-top: 2px solid var(--ink);
  border-radius: 0; padding: 1.4rem; min-width: 0;
  /* The contact shadow and the top highlight together are what make this an object.
     Ambient blur alone reads as a glow; an edge is what the eye uses for material. */
  box-shadow: var(--edge), var(--shadow);
}
/* THE ONE HAIRLINE OF METAL ON THE PAGE.
   Directly under the heavy ink rule that caps the panel, so the two read as one edge --
   ink over gilt, the way a printed certificate stacks a rule and a fine line. Spent here
   and nowhere else: a material used twice stops being a material and becomes a texture. */
.panel::before {
  content: ""; display: block; height: 1px; margin: -1.4rem -1.4rem 1.4rem;
  background: linear-gradient(90deg, transparent, var(--gild) 22%, var(--gild) 78%, transparent);
}
/* The whole rail follows, not just the panel: a sticky child is bounded by its
   parent's height, so the buy button used to slide away a third of the way down. */
@media (min-width: 64rem) { .rail { position: sticky; top: 5rem; align-self: start } }
.panel-price { font-size: var(--t-3xl); font-weight: 700; letter-spacing: -0.035em; font-variant-numeric: tabular-nums; margin: 0 }
.panel-price-sub { font-size: var(--t-sm); color: var(--muted); margin: 0.25rem 0 1.2rem }
.panel .btn { width: 100% }
.panel .note { font-size: var(--t-xs) }

/* ---------- facts --------------------------------------------------------- */
.facts { margin-top: 1.5rem; border-top: 2px solid var(--ink); min-width: 0 }
.fact { display: flex; justify-content: space-between; gap: 1rem; padding: 0.6rem 0; border-bottom: var(--hair); font-size: var(--t-sm) }
.fact-k { color: var(--muted); font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 560; padding-top: 0.1rem }
.fact-v { text-align: right; font-weight: 560; min-width: 0 }
.fact-v.is-machine { font-family: var(--mono); font-size: var(--t-xs); font-weight: 400; word-break: break-all }

/* ---------- detail layout -------------------------------------------------- */
.detail { padding-top: clamp(1.75rem, 4vw, 2.75rem); padding-bottom: 4rem }
.detail-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2.5rem }
/* Without this a grid item's `auto` minimum is its longest unbreakable line, so a
   <pre> in the guide blew the layout viewport out to 551px on a 390px phone and
   took the whole page horizontal with it. */
.detail-grid > * { min-width: 0 }
@media (min-width: 64rem) { .detail-grid { grid-template-columns: minmax(0, 1fr) 21rem; gap: 3.5rem } }
/* On a phone the grid is a single column and DOM order is description-then-buy, so the price
   and Buy panel landed ~2.6 screens down, after the full description, file list and
   disclosures -- a shopper concluded the skill could not be bought. Below the two-column
   breakpoint, float the buy rail to the top so price and Buy are the first thing seen; at
   >=64rem `order` resets and the sticky right rail returns. (U-MOBILE.) */
@media (max-width: 63.999rem) { .detail-grid > .rail { order: -1 } }
.detail-eyebrow { display: flex; flex-wrap: wrap; gap: 0.55rem; align-items: baseline; margin-bottom: 0.85rem }
.detail-name {
  font-family: var(--serif); font-size: clamp(2rem, 4.6vw, 2.9rem); font-weight: 600;
  letter-spacing: -0.02em; line-height: 1.08; text-wrap: balance;
}
.detail-desc { font-family: var(--serif); font-size: var(--t-lg); color: var(--muted); margin-top: 0.85rem; max-width: 40ch; line-height: 1.55 }

/* ---------- code, excerpts, file lists -------------------------------------- */
pre, .excerpt {
  font-family: var(--mono); font-size: var(--t-xs); line-height: 1.7;
  background: var(--surface-2); border: 0; border-left: 2px solid var(--rule-firm);
  border-radius: 0; padding: 0.85rem 1rem; margin: 0.7rem 0; overflow-x: auto; color: var(--ink);
  letter-spacing: -0.01em;
}
.excerpt { white-space: pre-wrap; word-break: break-word }
code { font-family: var(--mono); font-size: 0.88em; background: var(--surface-2); padding: 0.12em 0.35em; border-radius: 2px; letter-spacing: -0.01em }
pre code { background: none; padding: 0 }
.filelist { display: grid; gap: 0; border-top: 1px solid var(--rule-firm); border-bottom: 1px solid var(--rule-firm); min-width: 0 }
.filerow { display: flex; justify-content: space-between; gap: 1rem; padding: 0.5rem 0.15rem; font-family: var(--mono); font-size: var(--t-xs); min-width: 0; letter-spacing: -0.01em }
.filerow + .filerow { border-top: var(--hair) }
.filerow span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.filerow span:last-child { color: var(--muted); white-space: nowrap }

/* ---------- disclosure ------------------------------------------------------ */
.disclose { border: 0; border-top: var(--hair); border-radius: 0; background: none; margin: 1.1rem 0 }
.disclose > summary {
  cursor: pointer; padding: 0.75rem 0; font-size: var(--t-sm); font-weight: 560; color: var(--muted);
  min-height: var(--tap); display: flex; align-items: center; gap: 0.55rem; list-style: none;
}
.disclose > summary::-webkit-details-marker { display: none }
.disclose > summary::before {
  content: ""; width: 0.45rem; height: 0.45rem; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg) translate(-1px, -1px); transition: transform 0.18s ease;
}
.disclose[open] > summary::before { transform: rotate(45deg) translate(-2px, -2px) }
.disclose > summary:hover { color: var(--ink) }
.disclose-body { padding: 0 0 1rem; font-size: var(--t-sm); color: var(--muted) }

/* ---------- controls bar ------------------------------------------------------ */
.controls { display: flex; flex-wrap: wrap; gap: 0.65rem; align-items: center; margin-bottom: 1.4rem }
.controls .field { width: auto; flex: 1 1 15rem; min-width: 0 }
.seg { display: inline-flex; border: 1px solid var(--rule-firm); border-radius: var(--radius-sm); overflow: hidden }
.seg button {
  border: 0; background: none; color: var(--muted); cursor: pointer;
  padding: 0.4rem 0.85rem; border-radius: 0; font: inherit; font-size: var(--t-xs); font-weight: 560;
  min-height: var(--tap);   /* 44px. These were 36px — on the primary filter, on a phone. */
}
.seg button + button { border-left: 1px solid var(--rule) }
.seg button:hover { color: var(--ink) }
/* Selection is marked the same way the masthead marks the page you are on: an amber
   rule under the live one. Two inverted black blocks in the control bar outweighed
   the call to action sitting directly above them. */
.seg button[aria-pressed="true"] { color: var(--ink); font-weight: 620; background: var(--surface); box-shadow: inset 0 -2px 0 var(--signal-fill) }

/* ---------- states ------------------------------------------------------------ */
.loading, .empty { padding: 2.5rem 0; color: var(--muted); font-size: var(--t-sm) }
.buyers, .empty-box { box-shadow: var(--edge) }
.empty-box {
  border: 0; border-top: 2px solid var(--ink); border-bottom: var(--hair);
  border-radius: 0; padding: 2.75rem 0 3rem;
  color: var(--muted); background: none;
}
.empty-box h3 { font-size: var(--t-xl); color: var(--ink); margin-bottom: 0.45rem; letter-spacing: -0.02em }
.error { border-left: 2px solid var(--flag); background: var(--flag-bg); border-radius: 0; padding: 0.85rem 1rem; font-size: var(--t-sm) }
.status { margin-top: 0.85rem; font-size: var(--t-sm); line-height: 1.55; color: var(--muted) }
.status strong { color: var(--ink) }

/* Where a save/publish OUTCOME lands so it survives the section redraw a refresh performs.
   Empty until something is saved; the border makes a confirmation, or a recovery code, land
   as a distinct panel above the form rather than as a line that vanishes. (F1/F2.) */
.save-banner:empty { display: none }
.save-banner { margin: 0 0 1rem; padding: 0.85rem 1rem; border-radius: 6px; border-left: 3px solid var(--muted); background: var(--surface-2) }
.save-banner .status { margin-top: 0 }
.save-banner-ok { border-left-color: var(--signal-fill) }
.save-banner-error { border-left-color: var(--flag); background: var(--flag-bg) }

/* ---------- dropzone ----------------------------------------------------------- */
.dropzone {
  border: 1px dashed var(--rule-firm); border-radius: 0;
  min-height: 15rem; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: clamp(1.75rem, 5vw, 2.75rem) 1.25rem; text-align: center; cursor: pointer;
  background: var(--surface); transition: border-color 0.15s ease, background-color 0.15s ease;
}
.dropzone:hover, .dropzone:focus-visible, .dropzone.is-over { border-color: var(--signal-fill); background: var(--signal-bg) }
.dropzone-icon { width: 1.85rem; height: 1.85rem; margin: 0 auto 0.7rem; color: var(--signal-fill) }
.dropzone-inner { display: grid; gap: 0.4rem }
.dropzone b { display: block; font-size: var(--t-lg); font-weight: 620; margin-bottom: 0.3rem; letter-spacing: -0.02em }
.dropzone span { color: var(--muted); font-size: var(--t-sm) }

/* The cover drop target. Short, because it sits inside a form rather than being the whole
   first act of the page — a 15rem dashed box halfway down a listing form reads as an error. */
.dropzone-sm { min-height: 6.5rem; padding: 1.25rem 1rem }
.dropzone-sm b { font-size: var(--t-base) }
.dropzone-sm.has-image { border-style: solid; padding: 0; overflow: hidden; display: block }
.dropzone-sm.has-image img { display: block; width: 100%; height: auto; max-height: 14rem; object-fit: cover }

/* ---------- the listing screen --------------------------------------------------
   The heading IS the disclosure. "Everyone can see this. Forever." is set at section
   weight rather than dropped into a tooltip, because a creator publishing a client's
   filename by accident cannot take it back — Arweave has no delete — and a tooltip is
   a thing you read after you already know to look. */
.listing-h {
  font-family: var(--sans); font-size: var(--t-xl); font-weight: 660;
  letter-spacing: -0.02em; margin: 2rem 0 0.3rem;
  padding-top: 1.5rem; border-top: 2px solid var(--ink);
}
.listing-sub {
  font-family: var(--sans); font-size: var(--t-base); font-weight: 620;
  letter-spacing: -0.015em; margin: 1.85rem 0 0.25rem;
}
/* The listing's own excerpt is capped and scrolls. Twenty lines of somebody's instructions
   pushing the price field off the screen is a form nobody finishes. */
#public-excerpt { max-height: 15rem; overflow: auto; margin-top: 0.5rem }
/* SCROLLED, NOT TRUNCATED. A four-hundred-file folder is a legitimate skill and its file list is
   the thing a creator most needs to read -- but rendered at full height it put four hundred rows
   between them and the publish button. Nothing is hidden; the block has a height. */
#public-files, #locked-files { max-height: 18rem; overflow: auto; margin-top: 0.5rem }

/* The cover on a listing row. A fixed aspect box, so a row does not change height when a
   picture arrives — a catalogue that reflows as it loads is unreadable and unclickable. */
.card-cover {
  display: block; margin: 0 0 0.55rem; overflow: hidden;
  background: var(--surface); border: var(--hair);
  aspect-ratio: 16 / 9; max-width: 22rem;
}
.card-cover img { display: block; width: 100%; height: 100%; object-fit: cover }
.index > .card > .card-cover { grid-column: 2; max-width: 14rem }

/* The cover on a skill's own page. Wider, and still fixed-ratio for the same reason. */
.detail-cover {
  display: block; margin: 0 0 1.5rem; overflow: hidden;
  background: var(--surface); border: var(--hair);
  aspect-ratio: 16 / 9; max-width: 34rem;
}
.detail-cover img { display: block; width: 100%; height: 100%; object-fit: cover }

/* ---------- footer -------------------------------------------------------------- */
/* ---------- the foot ------------------------------------------------------------
   IT WAS A SLAB AND THREE GREY COLUMNS. A 2px rule across the top, everything set at one
   size in one colour, and the brand line squeezed into a third of the width -- the only part
   of the page that looked like a template rather than a product.

   What it is now is the same three things given room: a mark and a sentence with the weight
   of a signature, columns whose labels are the only loud thing in them, and a hairline rather
   than a bar. The rule is that the foot is read last and should feel like the end of
   something, not like a sitemap that fell off the bottom. */
.foot {
  border-top: 1px solid var(--rule);
  margin-top: clamp(4rem, 10vh, 7rem);
  padding: clamp(3.5rem, 8vh, 6rem) 0 0;
  color: var(--muted); font-size: var(--t-sm);
}
.foot-grid { display: grid; gap: clamp(2.5rem, 5vw, 4rem); grid-template-columns: minmax(0, 1fr) }
@media (min-width: 46rem) { .foot-grid { grid-template-columns: minmax(0, 1.6fr) repeat(auto-fit, minmax(9rem, 1fr)) } }

/* The brand block. The mark sits with the name, and the sentence under it is set at reading
   size rather than fine-print size, because it is the one piece of writing down here anybody
   actually reads. */
.foot-brand { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 1.15rem }
.foot-mark { width: 1.9rem; height: 1.9rem; flex: none; display: block }
.foot-name { font-size: var(--t-lg); font-weight: 620; letter-spacing: -0.02em; color: var(--ink) }
.foot-lede { margin: 0; max-width: 42ch; font-size: var(--t-base); line-height: 1.65; color: var(--muted) }

.foot h4 {
  font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.2em;
  color: var(--signal); margin: 0 0 1.1rem; font-weight: 620;
}
.foot ul { padding: 0; margin: 0 }
.foot li { list-style: none; margin: 0 }
/* Links in ink, not grey. A column of muted links reads as disabled; the colour is what says
   these are the way out of here. The underline grows from the left rather than switching on,
   which is the same gesture the rest of the site uses for a thing coming forward. */
.foot li a {
  display: inline-block; padding: 0.5rem 0; color: var(--ink);
  position: relative; text-decoration: none;
}
.foot li a::after {
  content: ""; position: absolute; left: 0; bottom: 0.35rem; height: 1px; width: 0;
  background: var(--signal-fill); transition: width 0.35s cubic-bezier(0.16, 0.84, 0.3, 1);
}
.foot li a:hover { color: var(--signal) }
.foot li a:hover::after, .foot li a:focus-visible::after { width: 100% }
.foot a { color: var(--ink) }

/* The fine print, on a band of its own.
   A hairline made it a paragraph that happened to be last. A ground under it makes it a
   different KIND of thing from the foot above -- the legal shelf every product has, which is
   read by whoever is looking for it and by nobody else.

   FULL BLEED, with the gutter put back inside. The band runs edge to edge so it reads as a
   band; its text stays on the same measure as everything above it, so nothing shifts left. */
.foot-fine {
  margin: clamp(3rem, 6vh, 4.5rem) calc(50% - 50vw) 0;
  padding: clamp(1.5rem, 3vh, 2.25rem) max(var(--gutter), calc(50vw - var(--max) / 2 + var(--gutter)));
  background: var(--surface);
  border-top: 1px solid var(--rule);
  display: flex; flex-wrap: wrap; gap: 0.75rem 2rem; align-items: baseline;
}
.foot-fine p { margin: 0; max-width: 62ch; font-size: var(--t-xs); color: var(--faint); line-height: 1.6 }
@media (prefers-reduced-motion: reduce) { .foot li a::after { transition: none } }

/* ---------- console -------------------------------------------------------------- */
/*
   The creator console's controls are the one part of this redesign that was left alone
   on purpose: every one of them is a call the creator's own wallet makes to a contract
   that checks `ownerOf` first, and rewriting that markup to chase a stylesheet would be
   risking the money path for a class name. So the vocabulary it already speaks is mapped
   onto the new system here instead.
*/
.terms { color: var(--muted); font-size: var(--t-xs); line-height: 1.6; margin: 0.6rem 0 0 }
.terms strong { color: var(--ink); font-weight: 600 }
.excerpt-fade { color: var(--muted); font-size: var(--t-xs); margin: 0.5rem 0 0 }
.install { font-family: var(--mono); font-size: var(--t-xs) }
.install code { display: block; white-space: pre; overflow-x: auto; padding: 0.85rem 1rem; background: var(--surface-2); border-left: 2px solid var(--rule-firm) }
.micro { font-size: var(--t-xs) }
.row-withdrawn { color: var(--deep); font-weight: 600 }
.buyers { display: grid; margin-top: 0.75rem; border-top: 1px solid var(--rule-firm); border-bottom: 1px solid var(--rule-firm); min-width: 0 }
.buyer { display: flex; gap: 0.9rem; padding: 0.45rem 0.15rem; font-family: var(--mono); font-size: var(--t-xs);
  word-break: break-all; letter-spacing: -0.01em }
.buyer + .buyer { border-top: var(--hair) }
/* A control heading. `.section-head` is a flex row built for a section title and a
   count; the console reuses it on <h4>, so it needs to read as a heading there. */
h4.section-head, h3.section-head {
  display: block; font-size: var(--t-xs); font-weight: 640; text-transform: uppercase;
  letter-spacing: 0.11em; color: var(--ink); margin: 0 0 0.5rem; padding-top: 1.4rem;
  border-top: var(--hair);
}
.form > .form-row:first-child > h4.section-head { padding-top: 0; border-top: 0 }

/* The console's own list-then-manage shell. */
.picker { display: grid; gap: 0; border-top: 1px solid var(--rule-firm); border-bottom: 1px solid var(--rule-firm); background: none }
.picker-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.15rem 1rem; align-items: center;
  padding: 0.65rem 0.6rem; border: 0; background: none; text-align: left; cursor: pointer;
  font: inherit; color: inherit; min-height: var(--tap); width: 100%;
}
.picker-row + .picker-row { border-top: var(--hair) }
.picker-row:hover { background: var(--surface) }
.picker-row[aria-current="true"] { background: var(--signal-bg); box-shadow: inset 2px 0 0 var(--signal-fill) }
.picker-row[aria-current="true"] .picker-name { color: var(--signal) }
.picker-name {
  font-family: var(--serif); font-size: var(--t-base); font-weight: 600;
  letter-spacing: -0.005em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.picker-sub { font-size: 0.6875rem; color: var(--faint); font-family: var(--mono); letter-spacing: -0.01em }
.picker-price { font-weight: 620; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap }
.picker-earn { font-size: 0.6875rem; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums }

/* ---------- table of contents ------------------------------------------------- */
.toc { border-top: 2px solid var(--ink); padding-top: 0.9rem }
.toc ul { list-style: none; padding: 0; margin: 0 }
.toc li + li { border-top: var(--hair) }
.toc a {
  display: block; padding: 0.6rem 0; font-size: var(--t-sm); color: var(--muted);
  min-height: var(--tap); align-content: center;
}
.toc a:hover { color: var(--ink); box-shadow: inset 2px 0 0 var(--signal-fill); padding-left: 0.6rem }

/* Everything the reader came for sits above the field, never inside it. */
body > header, body > main, body > .beam, body > .caution { position: relative; z-index: 1 }
.masthead { z-index: 50 }
/* AND THE PROOF BAR IS ABOVE THE SCRIM, which is not a detail on the busiest page on this site.
   The stage's scrim is fixed to the top of the viewport at this same depth and, being inside
   `main`, paints after this bar -- so the one element the design calls the alarm rendered as dark
   grey on near-black at every width, on the home page, while reading correctly everywhere else.
   The masthead already sits above it by its own rule; this puts the bar and the warning box that
   is mounted beside it there too. Nothing depends on the old ordering. */
body > .beam, body > .caution { z-index: 2 }

[id] { scroll-margin-top: 6rem }
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth } }

/* ---------- said, not shown ---------------------------------------------- */
/*
 * Text that exists only for a screen reader.
 *
 * The publishing stepper carried its whole state -- in progress, done, failed -- in a CSS
 * class, so the six-step narration was conveyed by colour alone. `step()` writes the same
 * information into one of these instead, which costs nothing visually and makes the
 * progress readable rather than merely visible.
 *
 * The clip-rect form rather than `display:none`: hidden content is not announced at all,
 * and zero-size content is skipped by some readers. This keeps it in the accessibility
 * tree while taking no space.
 */
.sr-only {
  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;
}

/* The browse page's page control. Links rather than buttons: a page of results is a place, so
   it can be opened in a tab, shared and reached with the back button. The disabled ends are
   spans rather than links, because a link that goes nowhere is a link a keyboard still lands on. */
.pager { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: 2rem }
.pager-link { padding: .45rem .9rem; border-radius: .4rem; text-decoration: none; box-shadow: var(--edge) }
.pager-link:hover { background: var(--surface-2) }
.pager-link.is-off { opacity: .35; box-shadow: none; cursor: default }
.pager-at { color: var(--muted); font-variant-numeric: tabular-nums }
