/* ═══════════════════════════════════════════════════════════════════════════
   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 collage coordinate, 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;
     pointer-events:none}
/* `.stage` is a transparent full-viewport column that paints ABOVE its sibling `.collage`
   (both effectively z-index 0, `.stage` later in the DOM), so left alone it swallows every
   pointer event over the field and no face is ever hoverable. Off on the shell, back on for
   each child. §5.1's `.main` then turns it off again for the same reason one level down —
   same specificity, later in the file — which is deliberate, not a conflict.
   `.collage` is a SIBLING of `.stage`, not a child, so it is untouched by this. */
.stage>*{pointer-events:auto}
/* 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)}
/* SHE IS ON AIR RIGHT NOW. A positive state only — there is no offline chip, because our
   word for our own gap made 3,392 of 3,418 creators look defective. The dot carries the
   meaning for a reader who cannot distinguish the accent colour. */
.chip--live{cursor:default;background:var(--accent);color:var(--accent-ink);
     border-color:transparent;font-weight:600}
.chip--live::before{content:"\2022 ";font-size:1.1em;line-height:0}
.chip--live:hover{border-color:transparent;color:var(--accent-ink)}

/* 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 The field.
   ONE composited image (`coveting_server::sheet`) on a plane much larger than the viewport,
   drifting behind the conversation and never removed.

   It must not read as a globe, and it no longer is one — the grid is flat, which removes
   the two tells a sphere could not lose: the curved silhouette and the foreshortening at
   the edges. What remains is depth from a gentle rotation and motion that never repeats,
   because the two layers run on periods that share no factor (83s and 197s) and are eased
   rather than linear.

   Two elements animate. Nothing else. The old version put 450 nodes on the page for this. */
.collage{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;
     perspective:2200px;
     -webkit-mask-image:radial-gradient(150% 100% at 50% 45%,#000 50%,transparent 96%);
     mask-image:radial-gradient(150% 100% at 50% 45%,#000 50%,transparent 96%)}
/* THE DIMMING IS A SCRIM, NOT A GROUP OPACITY, and that one change is what makes a lit cell
   possible at all.

   `opacity:.44` here used to dim the whole subtree — which mathematically caps everything
   inside it, hit targets included, at 44% of source. Measured: a hovered cell could not
   exceed 112/255 no matter what filter it carried, and the filter it carried
   (`brightness(lift*2.4)`) applied INSIDE that cap, so it clipped every source pixel above
   106/255 to flat white and then attenuated the result — 26% mean error against the true
   frame, and cyan/magenta banding where the JPEG's chroma blocks got amplified 5.4x.

   A scrim is black paint on the layer BELOW the anchors instead of a multiply over all of
   them. The wallpaper renders identically — `--bg` is #000, so `sheet x (1 - .56)` is the
   `sheet x .44` it replaces — and an anchor, painted after it, is simply not covered.

   `.56` and `.8` are the complements of the `.44` and `.2` this replaces, which is why the
   thread state moved here too: it is the same one-property transition, one layer down. */
.collage__plane::before{content:"";position:absolute;inset:0;background:var(--bg);
     opacity:.56;transition:opacity 1.2s var(--ease)}
/* CLICKED: the field closes around the face you picked.

   An `animation`, not a transition, and that is the whole trick — it is its own timeout. A
   transition to `.92` has no way back if the navigation never lands (a hung server, a
   cancelled load, a middle-click that opened a tab and left this document sitting here), and
   the reader would be staring at a wall we turned off. This deepens over ~1.4s, holds while
   a slow page arrives, and restores itself by 8s whether anything happened or not. In the
   normal case the document is replaced long before the tail and none of it is ever seen.

   Only the SCRIM moves. The cell under the cursor keeps its highlight because it paints
   above the scrim and its `:hover` rules still match — so the wall recedes and the person
   you chose stays lit, with nothing tracking which cell that was. */
@keyframes leaving{
  0%  {opacity:.56}
  18% {opacity:.92}
  82% {opacity:.92}
  100%{opacity:.56}
}
.collage.is-leaving .collage__plane::before{animation:leaving 8s var(--ease) forwards}
/* Outer: the wobble, off-axis and on the shorter period.

   `will-change` on both animated elements is not a micro-optimisation, it is what stops the
   field being RE-RASTERISED. `swim` animates `scale`, and with no promotion hint Chrome
   re-picks the plane's raster scale as that scale drifts — on a layer this size that is a
   tiled re-raster of tens of megabytes, and the tiles land a frame late. The element that
   shows it is the topbar: it is the only thing that holds still while the field moves under
   its 18% transparency, so a late tile reads there as the bar shimmering. Promoted once, the
   plane is a texture the compositor transforms and never redraws. */
.collage__drift{position:absolute;left:50%;top:50%;width:0;height:0;
     transform-style:preserve-3d;will-change:transform;
     animation:drift 83s ease-in-out infinite}
/* Inner: the plane itself. Square, so a square sheet stretches onto it without distorting
   a face, and 210vmax so its edges are always well outside the frame.

   210vmax is also what sets how BIG a face reads: the sheet is a fixed 20x20 grid stretched
   edge to edge, so on-screen cell size is the plane over 20, and the hit targets are placed
   as percentages of that same plane. Shrinking the plane shrinks the faces with it — 150vmax
   covers the viewport just as completely (verified: all four corners contained at every point
   of the 197s x 83s cycle on square, ultrawide, retina, phone and tall-portrait viewports,
   136-366px of slack) and halves the layer, but it took cells from 200 to 143 CSS px and made
   the wall read denser and further away. Coverage is not the binding constraint here, scale
   is, so the size stays and `will-change` above carries the compositing fix instead.

   What that costs is worth writing down: on a 1728x1085 retina viewport this lays out at 3629
   CSS px and projects — after `swim`'s scale(1.08) and the 2200px perspective — to ~8936
   DEVICE px, a ~270MB layer past the 8192 max texture size plenty of GPUs still have, to show
   a sheet that is only 1760px square. Cutting that without touching apparent scale means
   cropping the sheet rather than stretching it, which moves the cell geometry into the
   compositor, not this file. */
.collage__plane{position:absolute;width:210vmax;height:210vmax;margin:-105vmax;
     background-size:100% 100%;background-repeat:no-repeat;
     will-change:transform;
     animation:swim 197s ease-in-out infinite}
@keyframes swim{
  0%  {transform:rotateZ(0deg)   translate3d(-2%,-3%,0) scale(1.00)}
  27% {transform:rotateZ(1.4deg) translate3d(3%,1%,0)   scale(1.06)}
  50% {transform:rotateZ(-.6deg) translate3d(1%,4%,0)   scale(1.02)}
  74% {transform:rotateZ(-1.8deg) translate3d(-4%,2%,0) scale(1.08)}
  100%{transform:rotateZ(0deg)   translate3d(-2%,-3%,0) scale(1.00)}
}
@keyframes drift{
  0%  {transform:rotateX(-9deg) rotateY(4deg)}
  33% {transform:rotateX(5deg)  rotateY(-6deg)}
  66% {transform:rotateX(-2deg) rotateY(7deg)}
  100%{transform:rotateX(-9deg) rotateY(4deg)}
}
/* Once the conversation starts the field steps back rather than leaving. On the scrim, not
   the group: see `.collage__plane::before`. Deeper paint, not lower opacity, so the lit
   cells above it keep their exposure while the wallpaper recedes. */
body:has(.thread > li) .collage__plane::before{opacity:.8}

/* The hit targets — anchors inside the plane, so the browser hit-tests them through the
   same 3D transform that moves the picture. No pointer handler, no matrix to invert.

   NOTHING PAINTS AT REST. A cell is an invisible hit rect until the cursor is on it, and
   that is the whole rule of this surface: the field is wallpaper, and the only brightness
   on it is the brightness under the pointer. A resting highlight is a decoration that
   claims to mean something — it was measured claiming exactly that, and claiming it wrong.
   `.lit` cells painted their portrait at `brightness(.62)` before any hover, which put 29
   of 390 cells permanently brighter than their neighbours; measured 2026-08-18 over 42
   cells in one frame, those 29 ranked 1st, 3rd, 8th and 10th of the on-screen set while 6
   plain wallpaper tiles out-shone the dimmest of them, so "brighter" tracked the source
   card's own exposure and the per-cell bake, not anything about the person. Rest is rest.

   `--hero` is a strip of REAL creator media — the same quality-scored
   `asset(owner_kind='channel')` frames a creator profile renders — and `--hx`/`--hy` index
   this cell's portrait out of it. The wallpaper underneath is the source directory's
   watermarked feed card, which is frequently a screenshot of a profile page: fine as
   texture, not what we show somebody about a person.

   It used to be an empty rect that amplified whatever was behind it with
   `backdrop-filter:brightness(calc(var(--lift)*2.4))`, and that could never work: the thing
   behind it is a directory tile baked dark in 8 bits, so the filter was reconstructing
   detail the bake had already thrown away, clipping every source pixel above 106/255 on the
   way, and then being attenuated by a group opacity applied outside the clamp. `--lift` and
   the whole filter are gone with it, and so is the `--lit` token that replaced them.

   TWO TIERS ON HOVER, because the corpus has two — but EVERY cell highlights, because
   every cell is a face somebody is pointing at.

   A plain cell paints its own wallpaper tile (`--field`, sprited by `--fs` and the cell's
   own `--x`/`--y`). That tile is already on screen, one layer down, behind a 56% scrim; an
   anchor sits ABOVE the scrim, so drawing it there shows it at 2.3x its surroundings. No
   filter, no gain, no clamp — undimming is not amplification, and no second request either,
   because it is the sheet the plane already loaded.

   A `.lit` cell has more to offer: a floored page and stored first-party media, so it swaps
   in a 256px portrait from `--hero` at the exposure it was stored at. No filter on it in
   either state — the strip is baked at `1.0`, so full exposure is simply what painting it
   looks like, and there is no resting value to return from.

   The strip is preloaded under the same fine-pointer media query by `app_shell.html`. With
   nothing painting it at rest, a lazy fetch would land on first hover — 440 KB of pop-in
   under the cursor — and touch devices, which can never paint it, still never ask for it.

   Both open a page ABOUT THAT PERSON — `/c/:handle` for the first, the `/n/:host/:handle`
   index entry for the second, never the platform root. The rings differ to match: dimmer
   for a record, brighter for a portrait. That ring is now the ONLY thing distinguishing the
   tiers, and it only exists under the pointer, which is correct — a reader learns what a
   cell is by pointing at it, not by reading a wall of pre-lit ones.

   `--x`/`--y` carry the position instead of `left`/`top` literals so ONE pair of numbers
   places the box and frames its sprite. Two pairs could disagree, and the failure would be
   a cell showing its neighbour's face.

   Everything visual here is INSIDE the fine-pointer query, not just `pointer-events`. On
   touch these stay inert and therefore stay invisible — a decoration that looks like a
   control but cannot be pressed is the defect that removed the first interactive field. */
.collage__plane>a{position:absolute;left:var(--x);top:var(--y);
     width:var(--cw);height:var(--cw);border-radius:3px}
@media(hover:hover) and (pointer:fine){
  .collage__plane>a{pointer-events:auto;
       background-size:var(--fs);background-repeat:no-repeat;
       background-position:calc(var(--x) / .95) calc(var(--y) / .95);
       transition:box-shadow .18s var(--ease)}
  .collage__plane>a:hover{background-image:var(--field);
       box-shadow:0 0 0 1px rgba(255,255,255,.5),0 0 22px 5px rgba(255,255,255,.12)}
  .collage__plane>a.lit:hover{background-image:var(--hero);background-size:var(--hs);
       background-position:var(--hx) var(--hy);
       box-shadow:0 0 0 1px rgba(255,255,255,.8),0 0 30px 8px rgba(255,255,255,.25)}
}

/* 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` is off on the column and back on for each child, so the two 400px gutters
   either side of a 608px thread fall through to the field instead of being swallowed by a
   full-width flex container with nothing in it. Without this the wall is unhoverable at every
   point in the viewport — `.main` is `z-index:2` and 1440px wide whatever the thread measures.
   Children re-enable for their whole subtree, so anything datastar patches in is interactive
   the moment it has a box. */
.main{position:relative;z-index:2;flex:1;display:flex;flex-direction:column;
     justify-content:center;padding:1.25rem 1rem 2rem;min-height:0;pointer-events:none}
.main>*{pointer-events:auto}
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 title is a <p>, not an <h1> — the dialog is named by `aria-labelledby`, and a heading
   here doubled the <h1> count on every cookieless page. Styled as the h1 it visually is. */
.age-gate__title{font-size:1.6rem;line-height:1.2;letter-spacing:-.02em;font-weight:600;margin:0 0 .4rem}
/* 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}
/* A <button> in a <form>, not an <a>: "New chat" is an action, and a link to it was a
   crawlable edge to a 303 on every page. The form contributes no box of its own. */
.convo__new-form{display:contents}
.convo__new{display:inline-flex;align-items:center;gap:.3rem;
     padding:.3rem .6rem;border-radius:var(--r-pill);
     font:inherit;font-size:.8rem;font-weight:600;color:var(--ink-2);
     background:none;cursor:pointer;
     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,.age-gate__title{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){
  /* `will-change` released with the animation: a promoted layer with nothing moving on it
     is a texture held for no reason, and here it is the largest one on the page. */
  .collage__plane,.collage__drift{animation:none;will-change:auto}
  .thread>li,.dots i{animation:none}
  /* The click still has to ANSWER — a reader who asked for less motion did not ask for less
     feedback, and this is the only signal that a 500ms navigation was heard. So the field
     still recedes, it just arrives there instead of travelling.

     `transition:none` as well as `animation:none`, or it does not arrive: the base rule
     carries `transition:opacity 1.2s`, so killing only the keyframes left a 1.2s fade —
     measured 0.709 at 120ms when this claimed to be instant. Two properties animate this
     scrim and both have to be named. */
  .collage.is-leaving .collage__plane::before{animation:none;transition:none;opacity:.92}
}
