/* ═══════════════════════════════════════════════════════════════════════════
   COVETING DESIGN SYSTEM — the only stylesheet.
   Included by every page. There is no second sheet and no page-local <style>.

   It replaced two sheets that described the same product with different words:
   `catalog_style.html` called them `--muted / --label / --rule / --card-bg`, the app
   sheet called the same four things `--ink-2 / --ink-3 / --line / --panel`. Two
   vocabularies for one palette is how a colour changes in one place and not the other,
   which happened here more than once.

   Rules for editing:
   · A value that appears twice is a token. Tokens live in §1 and nowhere else.
   · A pattern that appears on two pages is a component. Components live in §4.
   · `style="…"` in a template is allowed ONLY for a value the server computes per
     element — a meter width, a media aspect ratio. Never for appearance.

   ── Contents ──────────────────────────────────────────────────────────────
   1  Tokens          2  Base            3  Layout
   4  Components      5  Surfaces        6  Responsive
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1  TOKENS ───────────────────────────────────────────────────────────── */
:root{
  /* Surface ramp, darkest first. Four steps is enough to build every panel here;
     a fifth would only ever be picked by coin-flip. */
  --bg:#000; --surface:#0c0c0f; --surface-2:#101013; --surface-3:#17171d;
  --line:#1c1c20; --line-2:#2b2b33;

  /* Ink ramp. `--ink-3` is the quietest text that still passes at 12px on --surface. */
  --ink:#f2f0f4; --ink-2:#8e8a97; --ink-3:#57545e;

  --accent:#ff4d7e; --accent-ink:#14040a; --accent-dim:#26101a; --ok:#4ade80;

  /* Entity kinds. One hue each, declared once: a card, a storybook swatch and a rail
     for the same kind must never drift apart. */
  --k-creator:#ff4d7e;  --k-site:#8e8a97;      --k-cam:#4dd4ff;
  --k-tube:#b07cff;     --k-paid:#ffc14d;      --k-ai:#4dffa8;
  --k-platform:#ff9d4d; --k-style:#e0dcff;     --k-collection:#7c8cff;

  --r-sm:7px; --r:12px; --r-lg:16px; --r-pill:999px;
  --dur:.18s; --dur-slow:.45s; --ease:cubic-bezier(.22,1,.36,1);

  --tile:52px;
  --font:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --mono:ui-monospace,SFMono-Regular,Menlo,monospace;
  --measure:34rem;            /* one readable column */
  --dock-h:8.5rem;            /* reserved for the fixed composer */
}

/* ── 2  BASE ─────────────────────────────────────────────────────────────── */
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg)}
body{color:var(--ink);font-family:var(--font);font-size:16px;line-height:1.5;
     -webkit-font-smoothing:antialiased;overscroll-behavior-y:none}
a{color:inherit;text-decoration:none}
a:focus-visible,button:focus-visible,input:focus-visible{outline:2px solid var(--accent);
     outline-offset:2px;border-radius:var(--r-sm)}
button{font:inherit}
code{font-family:var(--mono);font-size:.88em;color:var(--ink-2)}
h1{font-size:1.6rem;line-height:1.2;letter-spacing:-.02em;font-weight:600;margin:0 0 .4rem}
h2{font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);
   margin:2rem 0 .8rem;padding-bottom:.4rem;border-bottom:1px solid var(--line)}
p{margin:0 0 .9rem}
.lede{color:var(--ink-2);max-width:52ch;margin:0 0 1.4rem}
.note{font-size:.8rem;color:var(--ink-3)}
.empty{font-size:.85rem;color:var(--ink-3);font-style:italic}
.u-num{font-variant-numeric:tabular-nums}
/* The skip link. Off-screen until focused, then a real button in the top-left — the first
   tab stop on every page, so a keyboard reader is not dragged through the whole header
   before reaching the thing they came for. `clip-path` rather than `display:none`, which
   would take it out of the focus order and defeat the point. */
.skip{position:absolute;left:.5rem;top:.5rem;z-index:10000;
     padding:.5rem .85rem;border-radius:var(--r-pill);
     background:var(--accent);color:var(--accent-ink);font-size:.82rem;font-weight:600;
     clip-path:inset(50%);pointer-events:none}
.skip:focus{clip-path:none;pointer-events:auto}

/* ── 3  LAYOUT ───────────────────────────────────────────────────────────── */
/* `.stage` is the app shell (landing, style, turn). `.page` is a plain document
   (methodology, opt-out, an entity). Both centre on the same measure. */
.stage{position:relative;min-height:100dvh;display:flex;flex-direction:column;isolation:isolate}
/* `.stage` is the transparent full-viewport column the shell lives in. It used to be
   `pointer-events:none` with every child re-enabling, so a click in the gutter reached the
   field behind it; the field is gone and so is that dance. */
/* CENTRE IF IT FITS, top-align if it does not.

   `/about` and `/join` are four paragraphs in a 44rem column, and top-anchored they filled 37% of
   a 1080p viewport — measured, with the remaining 63% void black (`docs/demo/bug-log.md` BUG-06).
   `margin:auto` on a flex child is the one form of this that cannot clip: auto margins absorb free
   space when there is any and resolve to zero when there is none, so `/site/:host` at 3672px still
   starts at the top and scrolls normally. `align-items:center` would have cut its head off.

   Scoped to `.page`, the DOCUMENT family (about, join, legal, removal, claim). The app shell is
   `.stage` and owns its own full-height layout. */
body:has(> .page){min-height:100dvh;display:flex}
.page{max-width:44rem;margin:0 auto;padding:1.5rem 1rem 4rem}
body:has(> .page) > .page{margin:auto}
.col{width:100%;max-width:var(--measure);margin-inline:auto}
/* A LINK IN A DOCUMENT HAS TO LOOK LIKE ONE. The global `a{text-decoration:none}` reset above is
   right for cards and nav, where the whole row is the target, and wrong for prose, where the only
   clue is the text itself. Measured: `Request removal` on `/claim/start`, and `AI disclosure`,
   `request removal` and `legal@nunko.ai` in the § 2257 statement, all rendered in body colour with
   no underline — an invisible contact address on the page whose job is to be servable. */
.page p a,
.page li a,
.prose a{text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px}
.page p a:hover,
.page li a:hover,
.prose a:hover{color:var(--accent)}
.section{margin:0 0 2rem}

/* Sticky, at every width. It used to be `position:relative` and scrolled away with the
   page, so 600px into a slate there was no wordmark, no meter, no menu and no way to start
   another conversation — on a surface that is one long scrolling thread, the header is the
   only chrome there is. The blur is what keeps cards legible as they pass under it. */
.topbar{display:flex;align-items:center;gap:.7rem;padding:.8rem 1rem;
        border-bottom:1px solid var(--line);position:sticky;top:0;z-index:6;
        background:color-mix(in oklab,var(--bg) 82%,transparent);
        backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}
/* EXACTLY ONE auto margin in the header. `.meter` already had `margin-left:auto`; adding a
   second on the wordmark split the free space between them and left the conversation
   controls floating in the middle of the bar with its dropdown anchored 77px short of the
   button. The group starts at `.convo` and everything after it rides along. */
.convo{margin-left:auto}
.topbar__menu{width:1.5rem;height:1.5rem;display:grid;place-items:center;color:var(--ink-2);
        background:none;border:0;padding:0;cursor:pointer;font-size:1.15rem;line-height:1}
.wordmark{font-size:1.02rem;font-weight:600;letter-spacing:-.01em}
.wordmark i{font-style:normal;color:var(--accent)}
.back{display:inline-block;color:var(--ink-3);font-size:.82rem;margin-bottom:.9rem}
.back:hover{color:var(--accent)}
/* The one action on a document page: go ask about the thing you are reading about. */
.cta{margin:0 0 1.6rem}
.footer{margin-top:2.5rem;padding-top:1.2rem;border-top:1px solid var(--line);
        color:var(--ink-3);font-size:.78rem}
.footer a:hover{color:var(--accent)}

/* ── 4  COMPONENTS ───────────────────────────────────────────────────────── */

/* 4.1 Entity card — the one way this product draws a thing.
       Creator, site, style, collection: same card, different kind rail. */
.grid{list-style:none;margin:0;padding:0;display:grid;gap:.5rem;
      grid-template-columns:minmax(0,1fr)}
.grid--wide{grid-template-columns:repeat(auto-fill,minmax(15rem,1fr))}
/* `minmax(0,…)` on BOTH, never `1fr`: a grid item defaults to `min-width:auto` and
   refuses to shrink below its content, which pushed a card's dismiss button 24px off a
   393px viewport. Measured: row right edge 417 against a 393 screen. */
.ent{display:flex;align-items:center;gap:.4rem;background:var(--surface);
     border:1px solid var(--line);border-radius:var(--r);padding:.45rem;position:relative;
     min-width:0;transition:border-color var(--dur)}
.ent:hover{border-color:var(--line-2)}
.ent::before{content:"";position:absolute;left:0;top:.7rem;bottom:.7rem;width:2px;
     border-radius:0 2px 2px 0;background:var(--kind,var(--line))}
.ent--creator{--kind:var(--k-creator)}     .ent--site{--kind:var(--k-site)}
.ent--cam{--kind:var(--k-cam)}             .ent--tube{--kind:var(--k-tube)}
.ent--paid{--kind:var(--k-paid)}           .ent--ai{--kind:var(--k-ai)}
.ent--platform{--kind:var(--k-platform)}   .ent--style{--kind:var(--k-style)}
.ent--collection{--kind:var(--k-collection)}
.ent--reacted{border-color:var(--accent)}
.ent__link{display:flex;align-items:center;gap:.65rem;flex:1;min-width:0;padding-left:.35rem}
.ent__tile{width:2.6rem;height:2.6rem;border-radius:8px;object-fit:cover;flex:0 0 auto;
     background:var(--surface-3)}
.ent__tile--empty{display:grid;place-items:center;color:var(--ink-3);font-size:1rem}
.ent__body{min-width:0;flex:1}
.ent__name{display:block;font-size:.93rem;font-weight:550;white-space:nowrap;
     overflow:hidden;text-overflow:ellipsis}
/* A collection's name is a SENTENCE, not a handle: `Premium sites in Amateur` is 24 characters
   and still clipped to `Premium sites in Amat…` at the index's `minmax(15rem,…)` column, because
   the rule above is written for handles that must stay on one line. Prose wraps. Clamped to two
   lines so a long classification cannot make one row twice the height of its neighbour. */
.ent--collection .ent__name{white-space:normal;overflow-wrap:anywhere;
     display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.25}
.ent__tick{font-style:normal;color:var(--accent);font-size:.72rem;margin-left:.25rem}
.ent__sub{display:block;font-size:.75rem;color:var(--ink-3);white-space:nowrap;
     overflow:hidden;text-overflow:ellipsis}
.ent__kind{font-size:.62rem;letter-spacing:.07em;text-transform:uppercase;
     color:var(--kind,var(--ink-3));flex:0 0 auto;opacity:.85}
/* WHAT WE KNOW ABOUT HER, on the card. Its own wrapping row because `.ent__sub` above is
   `white-space:nowrap` and would clip a second fact to an ellipsis. Reuses the `.chip`
   primitive and `.chip--static` (below, §4.3) rather than inventing a card-only chip, and
   only shrinks it: the shipped chip is a 13px tap target sized for a control, and these are
   labels inside a 3rem-tall card.

   `.7rem` is 11.2px — at the legibility floor, deliberately. `.ent__kind` beside it renders
   at `.62rem`/9.92px, which is UNDER that floor; raising it changes the kind label on every
   card in the corpus and is a judgement call, so it is flagged here and left alone. */
.ent__facts{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.3rem}
.ent__facts .chip{padding:.1rem .45rem;font-size:.7rem;cursor:inherit;
     max-width:100%;overflow:hidden;text-overflow:ellipsis}
.ent__react{display:flex;gap:.15rem;flex:0 0 auto}

/* 4.1b The block a card opens into. Same kind rail as the card it came from, so the
   thread reads as one object getting bigger rather than two unrelated panels. */
.detail{background:var(--surface);border:1px solid var(--line);border-left:2px solid var(--kind,var(--line));
     border-radius:var(--r);padding:.85rem;display:grid;gap:.8rem}
.detail--creator{--kind:var(--k-creator)}   .detail--cam{--kind:var(--k-cam)}
.detail--tube{--kind:var(--k-tube)}         .detail--paid{--kind:var(--k-paid)}
.detail--ai{--kind:var(--k-ai)}             .detail--platform{--kind:var(--k-platform)}
.detail--site{--kind:var(--k-site)}         .detail--style{--kind:var(--k-style)}
.detail--collection{--kind:var(--k-collection)}
.detail__head{display:flex;align-items:center;gap:.75rem}
.detail__tile{width:3.4rem;height:3.4rem;border-radius:10px;object-fit:cover;flex:0 0 auto;
     background:var(--surface-3)}
.detail__tile--empty{display:grid;place-items:center;color:var(--ink-3)}
.detail__id{min-width:0;flex:1;display:grid;gap:.1rem}
.detail__name{font-size:1.05rem;font-weight:600;white-space:nowrap;overflow:hidden;
     text-overflow:ellipsis}
.detail__sub{font-size:.78rem;color:var(--ink-3);white-space:nowrap;overflow:hidden;
     text-overflow:ellipsis}
.detail__go{display:flex;align-items:center;gap:.9rem;flex-wrap:wrap}
/* A tag on a block is a label, not a control — nothing happens when you press it, so it
   must not look pressable. */
.chip--static{cursor:default;background:var(--surface-3);border-color:transparent}
.chip--static:hover{border-color:transparent;color:var(--ink-2)}

/* 4.2 Buttons */
.btn{border:0;border-radius:var(--r-pill);padding:.4rem .85rem;font-size:.82rem;
     font-weight:600;cursor:pointer;background:var(--surface-3);color:var(--ink);
     transition:var(--dur)}
.btn:hover{filter:brightness(1.15)}
.btn--primary{background:var(--accent);color:var(--accent-ink)}
.btn--icon{width:1.9rem;height:1.9rem;padding:0;border-radius:8px;background:none;
     color:var(--ink-3);display:grid;place-items:center;font-size:.9rem;line-height:1}
.btn--icon:hover{background:var(--surface-3);color:var(--ink);filter:none}
.btn--icon[aria-pressed="true"]{color:var(--accent);background:var(--accent-dim)}
/* The heart is a toggle and has to LOOK like one in both states: an outline nobody has
   pressed, a filled pink one they have. There is no dismiss button beside it — a card the
   reader does not want is one they scroll past. */
.btn--heart{font-size:1rem}
.btn--heart[aria-pressed="true"]{transform:scale(1.06)}
/* LEAVING IS NEVER THE PRIMARY ACTION. This was a byte-for-byte copy of `btn--primary`
   with MORE padding, so on every card, every site page and every creator page the loudest
   control was the one that sent the reader to a third party — an implied endorsement of a
   destination we only index, and our one emphatic control spent on the exit. Outlined,
   quiet, same size as any other button. */
.btn--out{display:inline-block;background:none;color:var(--ink-2);
     border:1px solid var(--line-2)}
.btn--out:hover{color:var(--ink);border-color:var(--ink-3);filter:none}

/* 4.3 Chips — a suggested prompt, a facet, an A–Z letter */
.chips{display:flex;flex-wrap:wrap;gap:.4rem;margin:0;padding:0;list-style:none}
.chips--scroll{flex-wrap:wrap;justify-content:center;padding:0 0 .6rem}
body:has(.thread > li) .chips--scroll{flex-wrap:nowrap;overflow-x:auto;justify-content:flex-start;
     scrollbar-width:none;-webkit-overflow-scrolling:touch}
.chips--scroll::-webkit-scrollbar{display:none}
.chips--scroll::-webkit-scrollbar{display:none}
.chips>li{flex:0 0 auto}
.chip{display:inline-block;background:var(--surface-2);border:1px solid var(--line);
     border-radius:var(--r-pill);padding:.4rem .8rem;color:var(--ink-2);font-size:.82rem;
     cursor:pointer;white-space:nowrap;transition:var(--dur);
     max-width:100%;overflow:hidden;text-overflow:ellipsis}
.chip:hover{border-color:var(--accent);color:var(--ink)}
.chip__n{color:var(--ink-3);margin-left:.35rem;font-variant-numeric:tabular-nums}

/* 4.4 Log — the conversation.
       Three kinds, deliberately unlike each other. `you` is a bubble, `her` is prose,
       and `act` is a RECORD of something that happened: rule-led, dimmer, icon-first,
       so scanning a history never reads a thing you did as advice you were given. */
.log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.log:empty{display:none}
.msg__body{font-size:.94rem}
/* A seeded page's opening bubble is its `<h1>`. It must READ as the bubble it is, not as a
   document heading — the heading is a semantic and crawl fact, never an appearance one. */
.msg__h{font:inherit;margin:0;display:inline}
.msg__cards{margin:0}
/* The folded remainder of a slate (`docs/specs/voice.md`, rule 3). The first six cards are a
   curated answer; the rest are present for the reader who wants them and for the crawler that
   always does. A `<details>` and not a signal or a handler, for the reason `.convo__past`
   already gives — the browser owns disclosure, including from the keyboard.

   Borrows `.convo__past`'s summary treatment rather than restating it, because a reader who
   has learned one pill-shaped "there is more here" control has learned both. `margin-top`
   only: an open disclosure must not indent its grid away from the six above it, or the slate
   reads as two answers. */
.msg__more{margin:.5rem 0 0}
.msg__more summary{list-style:none;cursor:pointer;display:inline-block;
     padding:.3rem .6rem;border-radius:var(--r-pill);
     font-size:.8rem;color:var(--ink-2);border:1px solid var(--line-2)}
.msg__more summary::-webkit-details-marker{display:none}
.msg__more summary:hover{color:var(--ink);border-color:var(--accent)}
.msg__more[open] summary{color:var(--ink);border-color:var(--accent);margin-bottom:.5rem}
/* An artifact's slot is the SAME box whether or not the bytes have arrived, so replacing the
   placeholder with the image moves nothing. The shape is NOT a constant: `render::shape_of` maps
   a kind to the canvas `crate::reel` submits — a still is 1024x1536 PORTRAIT, a clip 864x480
   landscape — and this rule hardcoded `aspect-ratio:512/288`, the old placeholder plate. A CSS
   ratio overrides the intrinsic one, so with the default `object-fit:fill` every real render was
   squashed into 16:9: measured 388x218 for 1024x1536 bytes, a 2.7x horizontal squash that reads
   as a wide-angle lens rather than as a bug.
   So the element carries its OWN ratio. `img`/`video` get it from the intrinsic `width`/`height`
   this crate stamps from the same `shape_of`, which reserves the right box before load AND matches
   the real bytes after it; the placeholder has no intrinsic size, so the template inlines those two
   numbers. `video` is in the selector because it was not, and an 864-wide clip overflowed the
   column at its attribute width. */
.msg__media{margin:0;max-width:512px}
.msg__media img,.msg__media video,.msg__media--waiting{display:block;width:100%;height:auto;
     border-radius:var(--r-lg)}
.msg__media--waiting{background:var(--surface-3);display:grid;place-items:center}
/* `.dots` is otherwise gated on `[data-busy]`, which belongs to the composer's request
   lifetime. A waiting artifact has no request of its own — the work sits on a queue — so it
   turns them on directly. */
.msg__media--waiting .dots{display:flex}
.msg__chips{margin:0}
.msg--you{align-items:flex-end}
.msg--you .msg__body{background:var(--surface-3);padding:.55rem .85rem;
     border-radius:var(--r-lg);border-bottom-right-radius:5px;max-width:88%}
.msg--her .msg__body{color:var(--ink-2)}
.msg--her b{color:var(--ink);font-weight:600}
/* Inline, NOT flex. `display:flex` with a `gap` makes every child a flex item, so the
   full stop after "more like <b>Tilly Harrington</b>." became its own item and the line
   rendered "Tilly Harrington ." with half a rem in front of the period. A sentence is
   inline text; only the icon needed spacing, and a margin does that without turning the
   punctuation into a layout box. */
.msg--act .msg__body{font-size:.78rem;color:var(--ink-3);
     padding:.15rem 0 .15rem .6rem;border-left:2px solid var(--line)}
.msg--act b{color:var(--ink-2);font-weight:600}
.msg__icon{color:var(--accent);font-size:.85rem;line-height:1;margin-right:.5rem}
.msg__link{display:block;margin-top:.4rem;font-size:.78rem;word-break:break-all;
     color:var(--accent)}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* 4.5 Composer.
   Two positions, one rule. With an empty thread it sits in flow directly under the opener,
   which is where a reader's eye already is and where every product of this shape puts it.
   The moment there is a conversation it becomes the fixed bottom dock and the thread scrolls
   under it.

   In flow rather than a translated fixed element: the opener wraps to two lines on a narrow
   screen and one on a wide one, so any hand-tuned offset would be wrong on one of them. The
   trade is that the change is a snap, not a tween — `position` does not transition, and
   faking it with transforms reintroduces exactly the offset problem. */
.dock{width:100%;max-width:var(--measure);margin:1.6rem auto 0}
/* `padding-inline` matches `.main`'s own, so the fixed dock sits on exactly the column the
   thread above it does. At .8rem it was 3.2px narrower on each side — invisible on desktop,
   where the `@media` rule below re-centres the composer at `--measure` anyway, and a visible
   step on a phone, which is the only width where this inset is what positions the composer. */
body:has(.thread > li) .dock{position:fixed;left:0;right:0;bottom:0;z-index:4;max-width:none;
      margin:0;padding-inline:1rem;padding-bottom:env(safe-area-inset-bottom,0px);
      background:linear-gradient(to top,#000 68%,rgba(0,0,0,.86) 88%,transparent)}
/* The side inset belongs to the DOCK, and only while the dock is full-bleed. It used to sit
   on the composer's own margin (and on `.chips--scroll`'s padding), which meant that in flow
   — where the dock is already a centred `--measure` column with room either side — it just
   made the composer 1.6rem narrower than every other element in that column. Measured at
   1440px: composer 582.4px against a 608px thread, and the composer SNAPPED from 582.4 to
   608 the moment the first message arrived and the `@media` rule below re-centred it. */
.composer{display:flex;align-items:center;gap:.5rem;background:var(--surface-2);
      border:1px solid var(--line);border-radius:26px;
      padding:.45rem .45rem .45rem 1.05rem;margin:.15rem 0 .7rem;
      transition:border-color var(--dur)}
.composer:focus-within{border-color:var(--line-2)}
.composer__input{flex:1;min-width:0;background:none;border:0;color:var(--ink);font:inherit;
      font-size:1rem;padding:.4rem 0}
.composer__input::placeholder{color:var(--ink-3)}
.composer__input:focus{outline:none}
.composer__send{width:2.15rem;height:2.15rem;flex:0 0 auto;border:0;border-radius:50%;
      background:var(--surface-3);color:var(--ink-2);cursor:pointer;display:grid;
      place-items:center;font-size:1.05rem;line-height:1;transition:var(--dur)}
.composer:focus-within .composer__send{background:var(--accent);color:var(--accent-ink)}
/* Busy is driven by `data-indicator`, so it is true for the REAL lifetime of the request
   and cannot desynchronise from it. */
.dots{display:none;gap:2px}
[data-busy] .dots{display:flex}
[data-busy] .composer__send{background:var(--accent)}
[data-busy] .composer__send .arrow{display:none}
.dots i{width:3px;height:3px;border-radius:50%;background:var(--accent-ink);
     animation:blink 1.05s ease-in-out infinite}
.dots i:nth-child(2){animation-delay:.16s}
.dots i:nth-child(3){animation-delay:.32s}
@keyframes blink{0%,80%,100%{opacity:.25;transform:translateY(0)}
                 40%{opacity:1;transform:translateY(-2px)}}

/* A "thinking" message shows the same three-dot spinner the composer does, but standalone —
   so it needs the display the `[data-busy]` scope grants inline. It is the accept path's
   progress frame (never an answer); task 08's reply frame replaces the whole <li> by id, so
   it lives exactly as long as the model turn. */
.msg--thinking .dots{display:flex;gap:.3rem;padding:.5rem 0}
/* The composer's SIZING does not survive the move, only its display did. Measured
   2026-08-05 at 1920x1080: the thread's progress row rendered 608px wide and THREE PIXELS
   tall, its dots coloured `--accent-ink` (#14040a) — near-black on a dark page. Inside the
   send button both are correct, because the dots sit on a filled accent circle; standalone
   in the thread it left a reader with no visible progress at all through a 15-130s turn. */
.msg--thinking .dots i{width:.4rem;height:.4rem;background:var(--accent)}

/* 4.6 Meter + save nudge */
.meter{display:flex;align-items:center;gap:.45rem;font-size:.72rem;
     color:var(--ink-3)}
.meter--saved{color:var(--accent)}
.meter__bar{width:3.25rem;height:3px;border-radius:2px;background:var(--surface-3);
     overflow:hidden}
.meter__bar i{display:block;height:100%;background:var(--accent);
     transition:width var(--dur-slow) var(--ease)}
.meter__save{background:none;border:0;color:var(--accent);cursor:pointer;padding:0;
     font-size:.72rem;font-weight:600}
/* On the measure column, like the thread and the composer above it. It is a sibling of both
   inside `.main`, and `.main` is a flex column — so without a width it stretched the full
   viewport (1408px against a 608px composer) and read as a page-wide banner rather than as
   the next thing in the conversation. */
.nudge{display:flex;align-items:center;gap:.6rem;width:100%;max-width:var(--measure);
     margin:1rem auto 0;padding:.7rem .85rem;
     border:1px solid var(--line);border-radius:var(--r);background:var(--surface);
     font-size:.84rem;color:var(--ink-2)}
.nudge b{color:var(--ink);font-weight:600}
.nudge .btn{margin-left:auto;flex:0 0 auto}

/* 4.7 Facts, lists, pager — the document surfaces */
.facts{display:grid;grid-template-columns:auto 1fr;gap:.35rem 1.1rem;margin:0}
.facts dt{color:var(--ink-3);font-size:.78rem;letter-spacing:.03em}
.facts dd{margin:0;font-size:.9rem;font-variant-numeric:tabular-nums}
.facts dd small{color:var(--ink-3);font-size:.78rem;font-weight:400}
.list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.15rem}
.list li{display:flex;align-items:baseline;gap:.6rem;padding:.3rem 0;
     border-bottom:1px solid var(--line)}
.list li:last-child{border-bottom:0}
.list a{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.list a:hover{color:var(--accent)}
/* A bare vertical stack — no rules, no bullets, just gap. `.list` is a ledger (each row
   gets a bottom border and truncates); this is for a column of whole controls, like the
   sign-in providers. It replaces `.ranklist`, which `join.html` inherited from the
   archived first-draft surface and which no stylesheet here ever defined. */
.stack{list-style:none;margin:0;padding:0;display:grid;gap:.5rem}
.stack .btn{display:block;text-align:center}
/* A table of contents, not a ledger row: the shared `.list` truncates with an ellipsis so
   a long compliance line stays on one row, which turned "18 U.S.C. § 2257 Compliance
   Statement" into "18 U.S.C. § 2257 Compliance St…" on the legal index. A legal document
   whose own title is cut off is not published in any sense that helps a reader. */
.list--toc li{align-items:flex-start;gap:1.5rem;padding:.5rem 0}
.list--toc a{white-space:normal;overflow:visible;text-overflow:clip}
.list--toc .list__n{text-align:right;max-width:22rem}
/* STACKED on a phone. Two flex columns on a 361px content width made the title wrap while
   the description wrapped over the top of it — "18 U.S.C. § 2257 Compliance Statement"
   came out six lines tall with its own blurb crashing through it. A table of contents on a
   phone is a list, not a table: title, then what it is, then the next one. */
@media(max-width:640px){
  .list--toc li{flex-direction:column;gap:.15rem;padding:.6rem 0}
  .list--toc a{font-size:.98rem}
  .list--toc .list__n{text-align:left;max-width:none}
}
.list__n{color:var(--ink-3);font-size:.76rem;font-variant-numeric:tabular-nums;
     flex:0 0 auto}
/* A verdict on a list row — the compliance ledger on a creator page. Two states, both
   named: "cleared" is the only good one, and everything else must LOOK unlike it, because
   a floor line that fails and reads as neutral is the one mistake this page cannot make. */
.list__n--ok{color:var(--ok)}
.list__n--no{color:var(--accent)}
/* Creator-uploaded media. Horizontal, because a vertical stack of portraits pushes the
   compliance ledger below the fold and the ledger is the reason the page is allowed to
   exist. */
.gallery{display:flex;gap:.5rem;overflow-x:auto;scrollbar-width:none;margin:0 0 1.5rem}
.gallery::-webkit-scrollbar{display:none}
.gallery img{height:11rem;width:auto;border-radius:var(--r);flex:0 0 auto;
     background:var(--surface-3)}
.pager{display:flex;gap:1rem;margin-top:1.5rem;font-size:.85rem}
.pager a{color:var(--accent)}
.pager span{color:var(--ink-3)}

/* 4.8 Forms on document pages — opt-out, removal, claim.
   Added because leaving `<input>` unstyled meant a white browser-default box on a black
   page: functional, and unmistakably not part of the product. A dark surface has to style
   its own fields; there is no "just inherit" on a form control. */
.form{display:grid;gap:.9rem;max-width:28rem;margin:0 0 1.5rem}
.field{display:grid;gap:.3rem}
.field>span{font-size:.78rem;color:var(--ink-3);letter-spacing:.03em}
input,textarea,select{background:var(--surface-2);border:1px solid var(--line);
     border-radius:var(--r-sm);color:var(--ink);font:inherit;font-size:.94rem;
     padding:.55rem .7rem;width:100%;transition:border-color var(--dur)}
input::placeholder,textarea::placeholder{color:var(--ink-3)}
input:focus,textarea:focus,select:focus{outline:none;border-color:var(--accent)}
textarea{min-height:6rem;resize:vertical}
input[type="hidden"]{display:none}
/* The composer is a field too, but it lives inside a pill and overrides the box. */
.composer__input,.composer__input:focus{background:none;border:0;padding:.4rem 0;
     border-radius:0}


/* ── 5  SURFACES ─────────────────────────────────────────────────────────── */

/* 5.1 removed with the field, 2026-08-26.
   A composited wall of creator tiles drifted behind the conversation here — one image, two
   animated elements, and 390 invisible anchors over it as hit targets. The background is now
   the flat `--bg` the tokens already define.

   Every selector, keyframe and custom property it owned went with it, and the list is in
   `scripts/legacy-residue.sh` rule L15 rather than here: that gate BANS those names, so a
   file naming them to explain their absence is the thing it fires on. One tombstone, in the
   place that enforces it. */

/* 5.2 The thread — a chat, pinned to the bottom.
   Normal page flow, not an inner scroller: the composer is fixed and the conversation
   scrolls under it, which is how every chat on a phone behaves and what lets the browser
   keep the scroll anchored on its own. `min-height` on the list pushes the first exchange
   down to sit just above the dock instead of stranding it under the topbar. */
/* `pointer-events:none` on this column, and on `.stage` above it, existed so the two ~400px
   gutters either side of a 608px thread fell THROUGH to the field's hit targets instead of
   being swallowed by a full-width flex container with nothing in it. There is nothing under
   them to reach any more, so both are gone with the field. */
.main{position:relative;z-index:2;flex:1;display:flex;flex-direction:column;
     justify-content:center;padding:1.25rem 1rem 2rem;min-height:0}
body:has(.thread > li) .main{justify-content:flex-end;padding-bottom:var(--dock-h)}
.thread{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:1rem;
     width:100%;max-width:var(--measure);margin-inline:auto}
.thread:empty{display:none}
.thread>li{display:flex;flex-direction:column;gap:.6rem;animation:rise .32s var(--ease)}

/* The opener owns the screen until the first message, then gets out of the way. One
   `:has()` rather than a second patch on every turn. */
.ask{text-align:center;font-size:1.75rem;line-height:1.25;font-weight:500;
     letter-spacing:-.02em;margin:0;text-wrap:balance}
.ask em{font-style:normal;color:var(--accent)}
.main:has(.thread > li) .ask{display:none}

/* 5.3 Age gate — an OVERLAY on the real page, never a replacement.
       Replacing the document is what stamped noindex on all 15,293 sitemap URLs. */
.age-gate{position:fixed;inset:0;z-index:9999;display:grid;place-items:center;
     background:rgba(0,0,0,.94);padding:1.5rem;text-align:center}
.age-gate__card{max-width:26rem;background:var(--surface);border:1px solid var(--line);
     border-radius:var(--r-lg);padding:1.75rem 1.5rem}
.age-gate__brand{font-size:1.02rem;font-weight:600;margin-bottom:.8rem}
.age-gate__brand i{font-style:normal;color:var(--accent)}
/* The affirmation is the legally operative sentence; it gets its own line and its own
   weight so it reads as a statement rather than as small print. */
.age-gate__legal{font-size:.78rem;color:var(--ink-3)}
.age-gate__actions{display:flex;gap:.6rem;justify-content:center;margin-top:1.2rem}

/* 5.4 Storybook */
.sb__swatch{display:inline-block;width:.6rem;height:.6rem;border-radius:2px;
     background:var(--kind,var(--line));vertical-align:baseline}
.sb__why{font-size:.82rem;color:var(--ink-2);margin:.5rem 0 .8rem;max-width:60ch}
.sb__row{display:flex;gap:1.5rem;align-items:center;flex-wrap:wrap;
     border:1px solid var(--line);border-radius:var(--r);padding:.8rem}
.sb__stack{display:grid;gap:.6rem}

/* 5.5 The menu, and the colophon it carries.

   ONE element, two shapes. Mobile: a drawer behind the hamburger with the legal block at
   its bottom. Desktop: a bar along the bottom of the screen, hamburger gone. The drawer
   state is a Datastar signal mirrored onto `[data-open]`; nothing here is toggled by hand.
   The attribute is matched on PRESENCE, never `="true"` — `data-attr` renders a true
   boolean as an empty attribute and removes it when false, which is the same convention
   `[data-busy]` already relies on. `[data-open=true]` silently never matches.

   The DESKTOP bar disappears once there is a conversation — it is a footer, and a footer
   pinned over a live thread is in the way. The phone DRAWER does not: it holds "New chat"
   and the conversation list, which are exactly what a reader wants mid-conversation. The
   first version hid both, which left the hamburger opening nothing the moment you typed. */

.menu__scrim{display:none;position:fixed;inset:0;border:0;padding:0;
     background:rgba(0,0,0,.6);cursor:default}
.menu__nav{list-style:none;margin:0;padding:0;display:grid;gap:.15rem}
.menu__nav a{display:block;padding:.55rem 0;font-size:1.02rem;color:var(--ink)}
.menu__nav a:hover{color:var(--accent)}
/* 5.4a The conversation control — the reader's own chrome, in the header at every width.
   It is NOT in the drawer: the drawer becomes the desktop footer, and a footer must not
   carry state that differs between two people looking at the same URL. */
.convo{display:flex;align-items:center;gap:.4rem;position:relative}
.convo__new{display:inline-flex;align-items:center;gap:.3rem;
     padding:.3rem .6rem;border-radius:var(--r-pill);
     font-size:.8rem;font-weight:600;color:var(--ink-2);
     border:1px solid var(--line-2)}
.convo__new:hover{color:var(--ink);border-color:var(--accent)}
.convo__new span[aria-hidden]{color:var(--accent);font-size:.95rem;line-height:1}
.convo__past summary{list-style:none;cursor:pointer;
     padding:.3rem .6rem;border-radius:var(--r-pill);
     font-size:.8rem;color:var(--ink-2);border:1px solid var(--line-2)}
.convo__past summary::-webkit-details-marker{display:none}
.convo__past summary:hover{color:var(--ink);border-color:var(--accent)}
.convo__past[open] summary{color:var(--ink);border-color:var(--accent)}
.convo__n{color:var(--ink-3);font-variant-numeric:tabular-nums}
/* The panel. Anchored to the header rather than in flow, so opening it never moves the
   conversation underneath. */
.convo__past .chats{position:absolute;right:0;top:calc(100% + .5rem);z-index:7;
     width:min(17rem,calc(100vw - 2rem));max-height:min(60vh,22rem);
     padding:.35rem;border:1px solid var(--line);border-radius:var(--r-lg);
     background:var(--surface);box-shadow:0 18px 40px rgba(0,0,0,.55)}

/* The list itself. Scrolls on its own so forty conversations do not grow the panel past
   the bottom of the screen. */
.chats{list-style:none;margin:0;padding:0;display:grid;gap:.1rem;
     overflow-y:auto;overscroll-behavior:contain;min-height:0}
.chats li a{display:block;padding:.4rem .5rem;margin-inline:-.5rem;border-radius:var(--r);
     font-size:.86rem;color:var(--ink-2);
     overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chats li a:hover{background:var(--surface-2);color:var(--ink)}
/* The one you are in. `border-left` rather than an inset box-shadow: the shadow follows
   the border-radius, so a 2px rail rendered as two ticks at the rounded corners with a gap
   down the middle. A border is clipped by the same radius but drawn as one edge. */
.chats__on a{background:var(--surface-2);color:var(--ink);
     border-left:2px solid var(--accent);padding-left:calc(.5rem - 2px)}

/* The colophon itself — identical in both shapes, so a rename or an address change lands
   the same way on a phone and a desktop. */
.colophon-bar{display:grid;gap:.75rem;color:var(--ink-3);font-size:.74rem;line-height:1.6}
.colophon p{margin:0}
.colophon__entity{color:var(--ink-2)}
.colophon a{text-decoration:underline;text-underline-offset:2px}
.colophon a:hover,.colophon__links a:hover{color:var(--accent)}
.colophon__links{display:flex;flex-wrap:wrap;gap:.3rem .7rem;margin:0;padding:0;
     list-style:none}
.colophon__links a{text-decoration:underline;text-underline-offset:2px}
.colophon__note{margin:0;max-width:68ch;text-wrap:pretty}
.colophon__copy{margin:0}

/* 5.5a Phone — the drawer.
   Off-canvas by transform rather than `display`, so it animates and so the links stay in
   the accessibility tree's reading order. `visibility` is what actually takes it out of
   the tab order while closed; a transform alone leaves it focusable off-screen. */
@media(max-width:560px){
  /* On a phone the two pills plus a wordmark do not fit; the New-chat label goes and the
     `+` carries it. The accessible name is on the anchor, so nothing is lost to a reader
     who cannot see the icon. */
  .convo__label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
  .convo__new{padding:.3rem .5rem}
}

@media(max-width:1023px){
  .menu{position:fixed;inset:0;z-index:9;visibility:hidden;pointer-events:none}
  .menu[data-open]{visibility:visible;pointer-events:auto}
  .menu[data-open] .menu__scrim{display:block}

  .menu__panel{position:absolute;inset:0 auto 0 0;width:min(19rem,86vw);
       display:flex;flex-direction:column;gap:1.1rem;
       padding:1.1rem 1.15rem calc(1.2rem + env(safe-area-inset-bottom));
       background:var(--surface);border-right:1px solid var(--line);
       overflow-y:auto;overscroll-behavior:contain;
       transform:translateX(-100%);transition:transform .22s ease}
  .menu[data-open] .menu__panel{transform:none}

  /* The legal block sits at the BOTTOM of the drawer — `margin-top:auto` rather than a
     spacer, so it stays pinned down on a tall phone and simply follows the nav on a short
     one instead of being pushed off. */
  .colophon-bar{margin-top:auto;padding-top:1.1rem;border-top:1px solid var(--line)}
}

/* 5.5b Desktop — the bottom bar.
   The drawer never opens here: everything it holds is on screen, so `☰` is gone. Nearly
   full width, with the nav and the legal block on one row and the long attestation
   underneath. */
@media(min-width:1024px){
  .topbar__menu{display:none}
  .menu{position:fixed;inset:auto 0 0 0;z-index:4;
        background:var(--surface);border-top:1px solid var(--line)}
  .menu__scrim{display:none}
  .menu__panel{max-width:82rem;margin-inline:auto;padding:.9rem 2rem 1rem;
       display:grid;gap:.6rem 2.5rem;grid-template-columns:minmax(0,auto) minmax(0,1fr)}
  .menu__nav{display:flex;flex-wrap:wrap;gap:.25rem 1.1rem;align-content:start}
  .menu__nav a{padding:0;font-size:.78rem;color:var(--ink-2);
       text-decoration:underline;text-underline-offset:2px}

  /* `display:contents` dissolves the colophon wrapper so its parts become grid items of
     the bar itself. Without it the attestation is trapped in one column, wraps to six
     lines, and the bar eats a quarter of the viewport.

     Three rows: links, the attestation across the full width, then the identity line.
     The POSTAL ADDRESS is dropped HERE ONLY — it is four lines of noise on a bar whose job is
     navigation, and it is still one click away on every page under `/legal`, in the 2257
     statement that legally needs it, and at the bottom of the phone drawer.

     The selector is scoped to the bar, and that scope is load-bearing. Unscoped, this rule
     also blanked the address inside the § 2257 statement's OWN custodian block — the sentence
     "Those records are held by:" followed by a name and an email and no address, on desktop
     only. Measured 2026-08-06 in Chromium: visible at 400 px, hidden at 1920 px. A custodian
     notice without a servable address is the one thing that page exists to carry, so a rule
     about a navigation bar may never reach a document body. */
  .colophon-bar{display:contents}
  .colophon-bar .colophon__addr{display:none}
  .menu__nav{grid-area:1/1}
  .colophon__links{grid-area:1/2;justify-self:end;align-self:center}
  .colophon__note{grid-area:2/1/3/-1;max-width:none;font-size:.72rem}
  .colophon{grid-area:3/1;display:flex;gap:.55rem;align-items:baseline}
  .colophon__entity::after{content:"\00b7";margin-left:.55rem;color:var(--line-2)}
  .colophon__copy{grid-area:3/2;justify-self:end}

  /* The bar is fixed, so the landing has to end above it rather than under it. Measured
     144px at 1440 and 1920, 168px at 1024 where the attestation takes a third line — the
     padding has to clear the WORST case, not the common one. */
  .stage{padding-bottom:11rem}
  body:has(.thread > li) .stage{padding-bottom:0}
  body:has(.thread > li) .menu{display:none}
}

/* ── 6  RESPONSIVE ───────────────────────────────────────────────────────── */
@media(min-width:640px){
  :root{--tile:58px}
  h1{font-size:1.9rem}
  .ask{font-size:2.4rem}
  body:has(.thread > li) .composer,
  body:has(.thread > li) .chips--scroll{max-width:38rem;margin-inline:auto}
}
@media(min-width:1024px){
  :root{--tile:64px;--measure:38rem}
  .ask{font-size:2.9rem}
}
@media(prefers-reduced-motion:reduce){
  .thread>li,.dots i{animation:none}
}
