/* ============================================================
   fofiy — Design Tokens
   Single source of truth for colour, type, space, radius, motion.

   Powder is the ACTION. Stone is the ROOM. Cocoa is gone from the walls.

   It used to say "Powder is a SURFACE", and the implementation took that
   further than the sentence did: the ground, the cards, the hairlines, the
   chips, the icon tiles and *both* secondary inks were all mixed from the
   brand hue. #FBF8F7 ground, #F7EDEA cards, #EADCD8 lines, #6E5A57 and
   #7A6461 for text — nothing on screen was neutral, so nothing on screen
   could be quiet, and the one rose that means "press this" had to compete
   with a room already painted its own colour.

   The brand ramp is unchanged and still the accent: `--action` is
   `--powder-600`, the rings are powder, the moods are untouched. What
   changed is the room. `--stone-*` is a warm neutral — grey with a trace of
   warmth left in it, not pink — so the page reads as calm and the rose reads
   as a decision. Every pairing below is measured, not chosen: `--ink-3` is
   #6B655F rather than the obvious #78716C because #78716C fails AA on
   `--surface-3` (3.82), and `--action-soft` is #F7EEEC rather than
   `--powder-200` because action text on powder-200 is 4.11.
   ============================================================ */

/* ============================================================
   INTER, SELF-HOSTED. Not fonts.googleapis.com.

   A classic script in the head waits for every stylesheet above it, and a
   `<link rel="stylesheet">` blocks the first paint outright — so while the
   face came from Google, a third party decided when fofiy first drew
   anything. Measured against a font host stalled for three seconds:
   FCP 3 276 ms, the whole delay, on a page whose own bytes were ready.
   It is two files in `fonts/` now, which is what the repository already
   does with supabase-js and livekit, and it removes a company that saw
   every reader's IP address — which is why privacy.html changed in the
   same commit.

   ONE `@font-face` PER SUBSET, NOT FOUR. Google's stylesheet declares 400,
   500, 600 and 700 separately and points all four at the SAME file, because
   Inter v20 is served as a VARIABLE font: the four md5 hashes are identical,
   and the table directory carries `fvar`, `avar`, `HVAR`, `MVAR` and `STAT`.
   So `font-weight: 100 900` is what the file actually is, and declaring the
   four weights separately would ask the browser to keep four faces that are
   one face.

   The two `unicode-range` values are Google's own, copied exactly: `latin`
   carries U+0131 (the dotless i the wordmark is set in) and `latin-ext`
   carries Ğ ğ İ Ş ş, so a Turkish reader fetches both and everybody else
   fetches one. `swap` rather than `block`: the fallback stack is a system
   sans and a paragraph that arrives in it is a paragraph somebody can read.
   The URLs are relative, so `fofiy/` still moves to its own domain
   untouched.
   ============================================================ */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* ---- The powder ramp — SUPERSEDED, kept for the brand archive -----
     This was the accent until the lime, and the lime until the coral. Nothing
     in the product uses it any more: the toast and the story viewer were the
     last two, and the demo's placeholder gradients moved to `--coral-*` with
     the mark. It stays defined because `brand/index.html` and
     `brand/motion.html` document it, and an undefined custom property does not
     fall back — removing the ramp would make those pages paint nothing rather
     than paint the old palette. Do not reach for it in a component: the answer
     is `--stone-*` for a surface and `--coral-*` for the accent. */
  --powder-50:  #FBF8F7;   /* fond    */
  --powder-100: #F7EDEA;   /* bej     */
  --powder-200: #EFDDD8;
  --powder-300: #E8CFC9;   /* nude    */
  --powder-400: #D9A9A4;   /* toz pembe */
  --powder-500: #C08D93;
  --powder-600: #9E5460;   /* derin gül — ACTION */
  --powder-700: #7E4250;
  --powder-800: #56303A;
  --powder-900: #2E2422;   /* kakao */
  --powder-950: #14100F;

  /* ---- Neutral ramp (stone) — the ROOM ----------------------
     Warm, so the product does not go clinical, but with the hue taken out:
     a surface that carries no brand is a surface the brand can sit on. */
  --stone-50:  #FAFAF9;
  --stone-100: #F5F5F4;
  --stone-200: #E7E5E4;
  --stone-300: #D6D3D1;
  --stone-400: #A8A29E;
  --stone-500: #78716C;
  --stone-600: #57534E;
  --stone-700: #44403C;
  --stone-800: #292524;
  --stone-900: #1C1917;   /* INK */
  --stone-950: #0C0A09;

  /* ---- Mood colours (low saturation, never used alone) ------
     Every mood chip must carry its label; colour is secondary. */
  --mood-coffee:   #B07C5A;
  --mood-work:     #6F7F8C;
  --mood-cheers:   #8E5B6E;
  --mood-bite:     #A9744A;
  --mood-reading:  #6F7A5E;
  --mood-language: #5F7D78;
  /* open ground */
  --mood-swim:     #4E7C8C;
  --mood-sun:      #B58A46;
  --mood-play:     #7A7452;
  --mood-walk:     #6B7A6A;

  /* ---- THE MAKE MENU'S GLYPHS -------------------------------------------
     A ramp of its own, and it is the only place in the product that has one.
     `--mood-*` is a set of earth tones chosen to sit quietly behind a card's
     content, and behind fifteen glyphs whose whole job is to be told apart at a
     glance it does the opposite: three browns and two greys in one grid. These
     are what the drawing has — distinct hues at icon size, on a white card,
     never behind text and never as a surface. They are for the Make menu and
     for nothing else; a screen that needs a kind's colour still takes
     `--kind-*`, which is unchanged. */
  --make-text:   #4C5FD7;
  --make-photo:  #1F9D62;
  --make-video:  #D93B4A;
  --make-live:   #D93B4A;
  --make-event:  #C93B78;
  --make-room:   #C97A22;
  --make-place:  #1F9D62;
  --make-trip:   #2F6FE0;
  --make-offer:  #C97A22;
  --make-sell:   #D9553B;
  --make-shelf:  #7A4FD1;
  --make-route:  #6A4FC1;
  --make-blood:  #D2342B;
  --make-review: #C99A22;


  /* ---- What kind of thing this card is ----------------------
     One hue per kind, used for nothing but the small disc behind a card's
     icon — enough to sort a mixed stream at a glance, never enough to compete
     with the one lime that means "press this". They are aliases of the mood
     ramp rather than new colours: that ramp is already the palette this
     product reserves for "what kind of thing is this", it is already low
     saturation, and it is already defined in both themes — so a kind hue
     follows the theme without a second definition to keep in step. Custom
     properties resolve at use, which is what makes one alias enough. */
  --kind-people:  var(--mood-cheers);
  --kind-place:   var(--mood-reading);
  --kind-event:   var(--mood-language);
  --kind-room:    var(--mood-coffee);
  --kind-travel:  var(--mood-swim);
  --kind-thing:   var(--mood-sun);
  --kind-creator: var(--mood-work);

  /* ---- Semantic (light theme is the default) ---------------- */
  --bg:            #FFFFFF;
  --surface:       #FFFFFF;
  --surface-2:     #F4F4F3;
  --surface-3:     #E9E9E7;
  --line:          #E4E4E1;
  --line-strong:   #CFCFCA;

  --ink:           #0B0B0A;
  --ink-2:         #55534F;
  --ink-3:         #6A6862;   /* AA on --bg, --surface-2 AND --surface-3 */
  --ink-invert:    #FFFFFF;

  /* ---- Coral — the brand colour (August 2026) ----------------
     The accent was lime for a season and is coral now, at the owner's
     direction, with the ripple mark. What did NOT change is the shape of the
     decision, which is the part that keeps costing money to get wrong:

     THE ACCENT IS TWO TOKENS, NOT ONE. `--action-fill` is what a button is
     made of and is the same coral in both themes, which is what makes the
     product recognisable at a glance. `--action` is the accent *as text*, and
     the coral on white measures 2.55:1 — unreadable, and it is a text colour
     on fifty elements across five pages. So in light it is the same hue taken
     down until it passes AA, and in dark it is the coral itself. One name for
     "the brand colour" would have to be wrong in one of the two places.

     The invariant `npm run test:css` holds: `--action-ink` may only ever sit
     on `--action-fill`. Near-black on the coral is 7.72; on `--action` it
     would be 1.5, which is how the Bid button went unreadable within minutes
     of the last swap. */
  --coral-100: #FFECEE;
  --coral-200: #FFD3D8;
  --coral-300: #FFB3B9;
  --coral-400: #FA7B85;   /* THE coral — the mark, the button */
  --coral-500: #E8586A;
  --coral-600: #C2354A;   /* the accent as text, on light */
  --coral-700: #AE2F45;
  --coral-800: #7E2231;
  --coral-900: #2A1618;

  --action:        #C2354A;
  --action-fill:   #FA7B85;
  --action-hover:  #A82940;
  --action-fill-hover: #FF9AA2;
  --action-ink:    #0B0B0A;
  /* The accent's own tint. The pairing is every selected chip and every
     current rail item, and `--action` on it measures 4.74 — over AA but only
     just — so the text on it keeps its own token one step darker. */
  --action-soft:   #FFECEE;
  --action-on-soft:#AE2F45;

  --focus:         #C2354A;
  --danger:        #A6392F;
  --danger-soft:   #F6E2DF;
  --live:          #C2354A;   /* "on / verified" — the accent, because on IS the accent */

  --overlay:       rgba(11, 11, 10, .52);

  /* Presence rings — the three privacy layers, made visual. They are the mark's
     own rings now, which is the one place the ripple and the ring model say the
     same thing: further out is less. Ring 1 is a text and border colour, so it
     is the coral that passes on white rather than the coral that does not. */
  --ring-1:        #E8586A;            /* here now            */
  --ring-2:        #C2354A;            /* descriptor revealed */
  --ring-3:        var(--ink);         /* met in person       */

  /* ---- Type ------------------------------------------------- */
  /* The display face is the UI face at its heaviest weight. The serif
     belonged to the powder brand; this headline has to shout. */
  --font-display: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-ui: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, monospace;

  /* Five steps and a gap that used to be fourteen pixels wide: display was 34,
     title was 20, and nothing lived between them, so every mid-level heading
     borrowed one of the two and the page read as two sizes rather than five.
     Weights are three — 400, 500/600, 700 — and 700 belongs to display and h1
     only. */
  /* One face in the whole product, wordmark included. Playfair went to the
     wordmark alone when the interface stopped using it — a serif display over
     a sans body is what made this read as a brochure about a tool rather than
     the tool — and with the ripple mark the wordmark is a sans too, so the
     second face had nothing left to set. It is off the font link on all seven
     pages: a face that is fetched and used by nothing is a request every
     visitor pays for and nobody sees. Tight tracking is what makes a large
     sans read as considered rather than merely big. */
  /* The cap moved 36 → 48, and only the cap. The pullback recorded above was
     about phones — clamp(32…46) put the question across a third of a phone
     screen — and the floor is what governs a phone: at 412 px, 5.2vw is 21 px,
     so every width below ~690 px is untouched by this and still starts at 28.
     What changed is the desktop, where 36 px was the same size as a section
     heading and the product's one question read like a label. */
  --t-display: 800 clamp(30px, 6.4vw, 52px)/1.06 var(--font-display);
  --t-h1:      800 clamp(23px, 3.8vw, 30px)/1.15 var(--font-display);
  --t-h2:      600 clamp(20px, 2.6vw, 24px)/1.25 var(--font-ui);
  --t-title:   600 18px/1.35 var(--font-ui);
  /* The sentence under an h1, and a real role rather than a sixth size for its
     own sake: five surfaces set `font: var(--t-lead)` on it — the front door's
     lede, Poudrose's, Pacrea's, and both legal pages — and none of them had a
     token behind it, so all five inherited body copy and the opening paragraph
     of every static page in the product read at the same weight as a footnote.
     Lighter than `--t-title` because it is prose, larger than `--t-body`
     because it is the one paragraph that has to be read. */
  --t-lead:    400 clamp(16px, 1.6vw, 18px)/1.5 var(--font-ui);
  --t-body:    400 15px/1.47 var(--font-ui);
  --t-body-md: 500 15px/1.47 var(--font-ui);
  --t-caption: 500 13px/1.38 var(--font-ui);
  --t-micro:   600 11px/1.2 var(--font-ui);
  --tracking-micro: .09em;

  /* ---- Space (4-based, no in-between values) ---------------- */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;

  /* ---- Radius ----------------------------------------------- */
  --r-card:  16px;
  --r-sheet: 24px;
  --r-media: 20px;
  --r-pill:  999px;
  --r-input: 12px;
  /* A COMMIT BUTTON IS A ROUNDED RECTANGLE, NOT A PILL. The two shapes say
     different things: a pill is an inline action you may or may not take (Add
     work, Write, Follow), a rectangle the width of the screen is the thing
     this screen is for. Every full-width button in the drawings is this
     radius and every small one is a pill. */
  --r-btn:   14px;

  /* ---- Elevation (warm shadows, never neutral grey) --------
     Warm still, but mixed from the ink the room now uses rather than from
     cocoa: a brown shadow under a stone card reads as a stain. */
  --e-1: 0 1px 2px rgba(28, 25, 23, .05);
  --e-2: 0 4px 16px rgba(28, 25, 23, .07);
  --e-3: 0 12px 32px rgba(28, 25, 23, .12);

  /* ---- Motion — the ON toggle is the brand's signature ----- */
  --ease-switch: cubic-bezier(.2, .8, .25, 1);
  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --d-fast: 140ms;
  --d-switch: 240ms;
  --d-sheet: 320ms;

  /* ---- Layout ---------------------------------------------- */
  --app-max: 430px;      /* phone column            */
  --rail: 72px;          /* desktop nav rail        */
  --col-main: 600px;     /* desktop centre column   */
  --col-side: 320px;     /* desktop right rail      */
  --tap: 44px;           /* minimum touch target    */
}

/* ============================================================
   THE WORDMARK — here rather than in components.css, because app.html loads
   this file and does not load that one, and one shape means one definition.
   The logo is the wordmark; the mark is its first o. Four concentric circles
   standing in for the letter, and the tittle on the i in the same coral.
   Where only a mark fits — a favicon, an app icon, an avatar — the ripple
   stands alone: the same drawing at the same proportions, never a cropped
   wordmark.
   ============================================================ */
.wm {
  display: inline-flex; align-items: baseline; white-space: nowrap;
  /* NO NEGATIVE TRACKING. Tight tracking is right for a large sans heading and
     wrong here, and the measurement says why: Inter's f overhangs its own
     advance by .023em, the dotless i's stem starts .083em inside its own, and
     at −.03em the two inks ended up .03em apart — three quarters of a pixel at
     24px. The f's hook and the i's stem read as one letter, which is what made
     the tittle look like it belonged to the f rather than to the i it is
     centred on. At 0 the gap doubles and the word reads as five letters. */
  font-family: var(--font-display); letter-spacing: 0;
  /* f-i is a ligature in most faces and this i is a DOTLESS one carrying a
     drawn tittle: a ligature swallows it and puts the dot over the join. */
  font-variant-ligatures: none;
  color: var(--ink);
}
/* ONE LOGO, AND THE CALLER MAY SET ITS SIZE AND NOTHING ELSE.
   This rule was `.wm, .wm span { font-weight: 500 }` and it lost, twice, in
   the two ways a (0,1,0) declaration loses: `.brand b` is (0,1,1) and set
   `font: 700 21px/1`, and `.gate-mark` is (0,1,0) but comes later in the
   cascade and set `font: var(--t-display)`, which is 800. Measured across the
   seven surfaces the logo was drawn at THREE weights — 500 almost everywhere,
   700 in the front door's header and footer, 800 on the app's own door — and
   the two heavy ones were mixed inside one drawing, because `.wm span` at
   (0,1,1) did hold the dotless ı at 500 while the f, the f and the y around it
   went bold. A logo whose letters disagree with each other is not one logo.

   So the lock is `:root` (0,2,0) rather than a heavier hand: it is the same
   device the coarse-pointer tap floor uses in this file, for the same reason —
   a page's own rule is the same specificity and comes later. `!important`
   would work and would also say that a caller might have a legitimate reason
   to want a different weight, and there is none.

   TRACKING IS IN HERE FOR THE SAME REASON. `components.css` set −.02em on
   `.brand b` and index.html set −.01em on top of it — on the one element in
   the product whose comment above says, with the measurement, that negative
   tracking makes the f's hook and the dotless i's stem read as one letter. */
:root .wm, :root .wm * {
  font-weight: 500; font-style: normal;
  font-family: var(--font-display); letter-spacing: 0;
  /* The third property `font:` resets without being asked, and the guard found
     it on five drawings — `.scr-bar b` and `.gate-mark` among them. Ligatures
     matter more here than the other two: f-i is a ligature in most faces and
     this i is a DOTLESS one carrying a drawn tittle, so the ligature swallows
     the letter and leaves the coral dot sitting over the join. See the note on
     `.wm` below, which is where the reasoning lives. */
  font-variant-ligatures: none;
}

.wm .wmo {
  /* `.wmo` — the wordmark's o — and NOT `.ring`, which is what it was called
     for a day. `.ring` is a natural name and index.html already had one: the
     privacy-ring row. The o inherited that row's layout, its svg computed to
     0×0, and the logo rendered as "f  fiy" with a hole where the mark goes, on
     the one page that explains the product. Same family as `.vrow` and
     `.pcard` before it — a class with two meanings is two meanings.

     .82em, not the letter's own width: three rings have to stay apart at the
     smallest size the wordmark is set at, which is the app bar. */
  display: inline-block; width: .82em; height: .82em;
  margin: 0 .01em; transform: translateY(.085em);
  color: var(--action-fill);
}
.wm .wmo svg { display: block; width: 100%; height: 100%;
  fill: none; stroke: currentColor; stroke-width: 8 }
.wm .wmo svg .c { fill: currentColor; stroke: none }

/* THE TITTLE, AND WHERE IT SITS. Every number here was measured in the browser
   with `measureText` rather than nudged until it looked right — which is the
   only way to get a value that holds at the other sizes too.

   AND EVERY ONE OF THEM WAS MEASURED IN THE WRONG FONT. While Inter came from
   fonts.googleapis.com, the browser test could not reach that host — the local
   server serves this repository and nothing else — so `document.fonts` was
   EMPTY on every run, the wordmark was drawn in `ui-sans-serif`, and the two
   corrections below were fitted to that face. It is visible in the old text:
   "the glyph's advance, 6.668px" is the fallback's ı to five decimal places,
   and Inter's is 6.047. The test passed the whole time, which is the point —
   a guard measuring the wrong thing agrees with itself. Self-hosting the face
   is what put the real glyph in front of it, and both numbers moved.

   HEIGHT. `bottom` resolves against the inline box, whose bottom edge is the
   font's DESCENT below the baseline: .24125em in Inter. The ink of the dotless
   i reaches .546875em above the baseline — its x-height, and the number that
   was most wrong: .5833em is the fallback's, which is a taller lower case. So
   `bottom = .546875 + gap + .24125`. At .865em the gap was .0769em, which is
   what the old comment itself called "a dot floating away from its letter";
   .828em is a gap of .04em, about a pixel at 24px, which is what a tittle is.

   HORIZONTAL. `left: 50%` centres the dot on the span's BOX, and the box is
   the glyph's advance, which in Inter is .251953em against an ink centre of
   .125em — so the correction is −.001em and the dot was essentially already
   right. The +.007em it carried was the fallback's error applied to a face
   that does not have it: .168px at 24px, in the opposite direction from the
   one the comment claimed. `test:live` measures the two centres against each
   other and allows .15px, and it now does it in Inter. */
.wm .dot { position: relative }
.wm .dot::after {
  content: ''; position: absolute; left: calc(50% - .001em); bottom: .828em;
  width: .153em; height: .153em; border-radius: 50%;
  transform: translateX(-50%); background: var(--action-fill);
}

/* THE LOGO IS THE LOGO IN CHROME TOO. It was monochrome here for a version, on
   the argument that beside a row of icons a coral o makes the wordmark a third
   thing in a bar of two. That argument is about a bar; it is not about a logo,
   and taking the colour out of a logo to make a bar tidier is the same move as
   taking a ring out of it to make it fit. The letters are ink and the o and the
   tittle are coral, everywhere the wordmark is set.

   `mark-mono.svg` still exists and is still right — for a stamp, an embroidery,
   a single-colour print, where the ground gives you no choice. A screen always
   gives you a choice. */

/* ============================================================
   Dark theme — the hero look. Powder becomes the accent.
   ============================================================ */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Same move as light: the room is stone, the rose is the action. The
       browns here (#1D1817 … #4A3D3C) were the dark half of the same
       mistake — a night-time room painted the brand's own colour. */
    --bg:          #0A0A09;
    --surface:     #141413;
    --surface-2:   #1D1D1B;
    --surface-3:   #272724;
    --line:        #2C2C29;
    --line-strong: #3E3E3A;

    --ink:         #FAFAF8;
    --ink-2:       #A3A29D;
    --ink-3:       #8F8E89;   /* AA on --bg, --surface-2 AND --surface-3 */
    --ink-invert:  #0A0A09;

    /* On this ground the accent-as-text and the accent-as-fill are the same
       colour, because the coral is legible here (7.77) and it is the point. */
    --action:      #FA7B85;
    --action-fill: #FA7B85;
    --action-hover:#FF9AA2;
    --action-fill-hover: #FF9AA2;
    --action-ink:  #0A0A09;
    --action-soft: #2A1618;
    --action-on-soft: #FA7B85;

    --focus:       #FA7B85;
    --danger:      #E8938A;
    --danger-soft: #3A2321;
    --live:        #FA7B85;

    --overlay:     rgba(0, 0, 0, .68);

    --ring-1:      #C2354A;
    --ring-2:      #FA7B85;
    --ring-3:      #FAFAF8;

    --mood-coffee:   #D6A681;
    --mood-work:     #9AAAB6;
    --mood-cheers:   #C08BA0;
    --mood-bite:     #D19E6F;
    --mood-reading:  #9BA687;
    --mood-language: #8DACA6;
    --mood-swim:     #8FB6C4;
    --mood-sun:      #DCB878;
    --mood-play:     #B6AF86;
    --mood-walk:     #9DB09B;

  /* Lifted off the black ground: the same twelve hues, brighter and a shade
     less saturated, so none of them closes into the background at 20px. */
  --make-text:   #8E9BFF;
  --make-photo:  #4FD198;
  --make-video:  #FF7A85;
  --make-live:   #FF7A85;
  --make-event:  #F080B0;
  --make-room:   #EDA85C;
  --make-place:  #4FD198;
  --make-trip:   #78A6FF;
  --make-offer:  #EDA85C;
  --make-sell:   #FF8E76;
  --make-shelf:  #B08CFF;
  --make-route:  #A08CF5;
  --make-blood:  #FF7A72;
  --make-review: #E8C45C;


    --e-1: 0 1px 2px rgba(0, 0, 0, .4);
    --e-2: 0 4px 16px rgba(0, 0, 0, .45);
    --e-3: 0 12px 32px rgba(0, 0, 0, .55);
  }
}

/* Manual override wins in both directions */
:root[data-theme="dark"] {
  /* Same declarations as the media query above, and they must move
     together: this block is what an explicit choice sets. */
  --bg:          #0A0A09;
  --surface:     #141413;
  --surface-2:   #1D1D1B;
  --surface-3:   #272724;
  --line:        #2C2C29;
  --line-strong: #3E3E3A;

  --ink:         #FAFAF8;
  --ink-2:       #A3A29D;
  --ink-3:       #8F8E89;   /* AA on --bg, --surface-2 AND --surface-3 */
  --ink-invert:  #0A0A09;

  /* On this ground the accent-as-text and the accent-as-fill are the same
     colour, because the coral is legible here (7.77) and it is the point. */
  --action:      #FA7B85;
  --action-fill: #FA7B85;
  --action-hover:#FF9AA2;
  --action-fill-hover: #FF9AA2;
  --action-ink:  #0A0A09;
  --action-soft: #2A1618;
  --action-on-soft: #FA7B85;

  --focus:       #FA7B85;
  --danger:      #E8938A;
  --danger-soft: #3A2321;
  --live:        #FA7B85;

  --overlay:     rgba(0, 0, 0, .68);

  --ring-1:      #C2354A;
  --ring-2:      #FA7B85;
  --ring-3:      #FAFAF8;

  --mood-coffee:   #D6A681;
  --mood-work:     #9AAAB6;
  --mood-cheers:   #C08BA0;
  --mood-bite:     #D19E6F;
  --mood-reading:  #9BA687;
  --mood-language: #8DACA6;
  --mood-swim:     #8FB6C4;
  --mood-sun:      #DCB878;
  --mood-play:     #B6AF86;
  --mood-walk:     #9DB09B;

  /* Lifted off the black ground: the same twelve hues, brighter and a shade
     less saturated, so none of them closes into the background at 20px. */
  --make-text:   #8E9BFF;
  --make-photo:  #4FD198;
  --make-video:  #FF7A85;
  --make-live:   #FF7A85;
  --make-event:  #F080B0;
  --make-room:   #EDA85C;
  --make-place:  #4FD198;
  --make-trip:   #78A6FF;
  --make-offer:  #EDA85C;
  --make-sell:   #FF8E76;
  --make-shelf:  #B08CFF;
  --make-route:  #A08CF5;
  --make-blood:  #FF7A72;
  --make-review: #E8C45C;


  --e-1: 0 1px 2px rgba(0, 0, 0, .4);
  --e-2: 0 4px 16px rgba(0, 0, 0, .45);
  --e-3: 0 12px 32px rgba(0, 0, 0, .55);
}

/* ============================================================
   Platform floor — the same on Android, iOS and desktop
   ------------------------------------------------------------
   Six of the seven pages were missing pieces app.html already had, which is
   what happens when a rule lives in one file's <style> instead of here.

   · `overscroll-behavior` belongs on `html`. The viewport's scroller is the
     root element, so the same declaration on `body` alone leaves it untouched
     and Android paints its edge glow — a wide grey arc no rule in these
     stylesheets can turn off, because nothing here draws it.
   · A touch target is 44 px on iOS and 48 on Android, so 44 is the floor
     either way. `--tap` has existed as a token since the beginning and was
     used by nothing; the language picker measured 28 px tall on every page.
     The rule adds hit area without moving anything: the box grows, the
     padding does not, so the layout is unchanged and the finger is not.
   · Only on coarse pointers. A 44 px minimum on a desktop rail would make a
     dense list twice as tall for a mouse that does not need it.
   ============================================================ */
html { overscroll-behavior-y: none; }

@media (pointer: coarse) {
  /* The chrome every page shares. Named individually rather than as a blanket
     rule over every button: app.html has dense rows where 44 px would be wrong,
     and a guard that changes what it was not asked to is a guard nobody keeps.

     `:root` is on the front of every selector on purpose. A floor that a page
     can silently undercut is not a floor — index.html's own
     `.langs button { min-height: 30px }` is the same specificity as a bare
     `.langs button` here and comes later in the cascade, so it won.
     `:root .langs button` outranks it and the page keeps its padding. */
  :root :is(.langs button, .tabbar a, .cats button, .rail a,
            .sect-h .more, .item .act, .more, .thm, .btn, .brand, .foot-links a,
            .chips-row button, .askbar .mic, .askbar .go,
            /* Moving a control out of the bar moved it out of this list
               too, and the list is where the floor lives: `.ask-modes
               .grab` rendered 40 px on the one device the rule exists
               for. A selector list is a dependency — and it was one again
               when the microphone moved back *into* the bar and this line
               went on naming the wrapper it had left. */
            footer nav a, footer a,
            .iconbtn, .segs button, .mini, .sect-h a, .lst .go,
            details > summary,
            /* Found the day the audit learned to walk past the first screen:
               these three were 36–40 px and had never been measured, because
               `offsetParent` is null on a screen that is not showing and the
               audit only ever loaded the app and looked. */
            /* `button.chip` rather than `.chips .chip`: the same chip is used as a
               label in a <span> — which is not a target — and as a button
               outside a `.chips` row, which is. What it *is* decides, not
               where it sits. */
            .pick button, button.chip, .langpick button, .acts button, .avbtn) {
    min-height: var(--tap);
  }
  /* A summary is a control people tap, and the audit could not see that until
     it learned to measure one: index.html's FAQ rows had been 22 px since they
     were written. Centred, so the extra height is hit area rather than a gap
     under the label. */
  :root details > summary { display: flex; align-items: center; }
  /* A card action is the one control on a wall of objects, so it is centred in
     whatever height the floor gives it rather than sitting at the top of it. */
  :root .lst .go { display: flex; align-items: center; justify-content: center; }
  /* A SECTION'S ACT — "Edit" beside Featured, "Edit" beside Interested in —
     is written as an anchor in one place and a button in the other, and both
     were in the floor's selector list above and both measured 44×24 anyway.
     Two different reasons, which is why fixing one did not fix the other.
     The anchor had no `display` of its own, so `min-height` did nothing to an
     inline box (the trap the footer links hit, one rule down, and the reason
     `display` comes first there) and the `gap: 4px` app.html gives it was
     inert as well. The button was already a block and already 44 tall — and
     44×24 is a failing target, because the floor is BOTH dimensions and
     "Edit" is four letters wide. Aligned to the end, because the act sits in
     the right corner of the heading and the extra width has to be hit area
     rather than a shifted label. */
  :root :is(.sect-h a, .sect-h .more) {
    display: inline-flex; align-items: center;
    justify-content: flex-end; min-width: var(--tap);
  }
  :root :is(.askbar .mic, .askbar .go, .avbtn) { min-width: var(--tap); }
  :root :is(.langs button, .cats button, .thm) { min-width: var(--tap); }
  /* Centred rather than top-aligned, so the extra height is hit area and not a
     visible gap under the label. */
  :root :is(.langs button, .cats button, .thm) {
    display: inline-flex; align-items: center; justify-content: center;
  }
  :root .tabbar a { display: grid; place-items: center; }
  :root .brand { align-items: center; }
  /* Standalone links in a footer are navigation, not prose. Padding rather
     than height, so the row does not grow: the gap between lines absorbs it.

     `display` first, and that is the whole trick: `min-height` does nothing to
     an inline box, so a footer full of 15 px links stayed 15 px while the rule
     above appeared to cover it. Inline-block is what makes the floor apply.

     Links inside a sentence are deliberately not here. WCAG's target-size rule
     exempts them, and giving "cnil.fr" a 44 px box in the middle of a
     paragraph would break the line it sits in to fix nothing. */
  :root :is(.foot-links a, footer a, footer nav a) {
    display: inline-block; padding-block: 10px;
  }
}

/* ============================================================
   Primitives
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

/* Form controls do not inherit colour or font from their container: the user
   agent gives every <button> its own `buttontext` colour and a system font.
   Without this, any button that relies on inherited ink — a row, a card, a
   chip — renders near-black on a dark surface. `:where()` keeps specificity at
   zero so .btn and friends still win.                                       */
:where(button, input, select, textarea) {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
}
:where(button) { background: none; border: 0; }

/* Mobile browser realities, not preferences:
   · Safari zooms the whole page when a focused input's text is under 16px,
     and --t-body is 15px. Coarse pointers therefore get 16px in fields.
   · Chrome on Android paints a grey rectangle over any tapped control.
   · A control with `touch-action: manipulation` skips the 300ms double-tap
     wait, which otherwise makes every button feel slow. */
@media (pointer: coarse) {
  :where(input, select, textarea) { font-size: 16px; }
}
:where(button, a, input, select, textarea, [role="button"]) {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* iOS rubber-bands the page behind a fixed bar and shows the page background
   through the gap; painting the safe area keeps the bar looking attached. */
body { background-attachment: fixed; }

/* The hidden attribute is display:none in the UA sheet, which any author
   `display` overrides. Panes built on flex/grid need this to actually hide. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--t-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

:where(a) { color: var(--action); text-underline-offset: 2px; }

:where(button, [role="button"], a, input, select, textarea):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Icons inherit text colour — never their own. */
.icon {
  width: 24px; height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}
.icon-sm { width: 18px; height: 18px; stroke-width: 2; }
.icon-lg { width: 32px; height: 32px; stroke-width: 1.5; }

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-2);
  min-height: var(--tap);
  padding: 0 var(--s-5);
  border: 0; border-radius: var(--r-pill);
  background: var(--action-fill); color: var(--action-ink);
  font: var(--t-body-md);
  cursor: pointer;
  transition: background var(--d-fast) var(--ease-out),
              transform var(--d-fast) var(--ease-out);
}
.btn:hover { background: var(--action-fill-hover); }
.btn:active { transform: scale(.97); }

.btn-ghost {
  background: transparent; color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}
.btn-ghost:hover { background: var(--surface-2); }

/* The commit button: the width of the screen, and a rectangle rather than a
   pill — see --r-btn. It replaced an inline `style="width:100%"` on nineteen
   buttons, which is nineteen places the shape could not be changed at once. */
.btn-wide { width: 100%; border-radius: var(--r-btn); }

/* THE HAIRLINE IS DRAWN OVER THE CHILDREN, NOT UNDER THEM. It used to be part
   of this element's own `box-shadow`, and an inset shadow paints above the
   element's background but BELOW every descendant's — so any child that paints
   to the card's edge erases the frame for its own height. Nothing showed it
   while the pictures were dark: an event listing whose photograph is a white
   poster (a venue logo padded onto a 16:9 canvas by the feed) opened the card
   down both sides, and the picture read as sitting outside its frame. Measured
   at the card's left edge: rgb(228,228,225) beside the text, rgb(255,255,255)
   beside the picture.

   An overlay rather than a `border`, because a border changes the geometry of
   every card in the product by a pixel; and `inset: 0` with `pointer-events:
   none`, so it covers the whole card, follows its radius and takes no press.
   `position: relative` is the cost — an absolute descendant of a card would
   re-anchor to it — and there is none: every absolutely-positioned element on
   all eleven walked surfaces was measured before and after and not one moved. */
.card {
  position: relative;
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: var(--e-1);
}
.card::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--line);
}

.chip {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 32px; padding: 0 var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface-2); color: var(--ink);
  font: var(--t-caption);
}

.eyebrow {
  font: var(--t-micro);
  letter-spacing: var(--tracking-micro);
  text-transform: uppercase;
  color: var(--ink-2);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}
