/* LUCID — every rule in the app.
 *
 * Lifted out of index.html on 24 August 2026, unchanged. The file was
 * 29,699 lines with 4,500 of them CSS, and finding a rule meant scrolling
 * past a fifth of the application to reach the markup that uses it.
 *
 * NO BUILD STEP. Matt deploys by dragging a folder into Netlify, so this is
 * a plain stylesheet the page links to — not something a bundler assembles.
 * Splitting it any further would mean more requests on a phone for no gain.
 */
  :root{
    /* DARK (default) — muted-card + contained-glow language: each hue stays
       flat/desaturated as a surface color, with a small bright glow pool
       anchored near it rather than washing the whole element. */
    --bg-mid:#080A1A; --bg-base:#050710; --bg-deep:#020306;
    --bloom-1: rgba(61,107,255,0.12); --bloom-2: rgba(160,125,255,0.08);
    --bloom-3: rgba(255,154,68,0.06); --bloom-4: rgba(184,225,74,0.06);
    --glass-bg: rgba(28,40,86,0.44);
    --glass-border: rgba(255,255,255,0.09); --glass-border-hi: rgba(255,255,255,0.20);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.35), 0 8px 18px -6px rgba(0,0,0,0.4), 0 28px 56px -16px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.06);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.09), rgba(255,255,255,0) 42%);
    --glass-blur: 24px; --glass-sat: 160%;
    /* ---- MOTION SYSTEM ----
       Everything that moves now resolves to these six values. Before this there
       were five ad-hoc durations (.1/.15/.2/.25/.28s) and easing was whatever
       `ease` happens to be — which is why nothing felt related to anything
       else: every component accelerated on its own curve. One decelerating
       curve for entrances (things arriving should slow as they land), one
       symmetric curve for travel between two states, one slight-overshoot
       spring for the few playful bits. */
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
    --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
    --dur-fast: 130ms;
    --dur-base: 220ms;
    --dur-slow: 320ms;
    /* The dissolve between pages. Deliberately shorter than --dur-base: a page
       transition is the one animation the eye is waiting on, so it should be
       over before it is noticed. Kept in step with VIEW_FADE_MS in JS. */
    --dur-view: 150ms;
    /* ---- THE THREE ANSWERS ROW 1822 OWED, AND WHY EACH ONE IS A NAME AND NOT
       A LITERAL. Row 1827 counted seventeen raw values left in this file and
       eleven of them were durations down here, which is the same fault this
       block was written to cure, in the one corner the block had no word for.

       SURVIVORS, DECIDED RATHER THAN INHERITED. --ease-in-out stays: 37 rules
       use it and 25 of those are `infinite` — a breath, a pulse, a wobble —
       where a decelerating curve jerks at the seam because the loop restarts
       from a standstill it had already reached. --dur-view stays: it is a
       contract with VIEW_FADE_MS in app.js and a name is the only thing
       holding the two ends together. --ease-spring stays with its single use
       named here so nobody has to grep for it: .chat-pill, the sphere you drag
       around the screen, where the slight overshoot is the difference between
       a thrown object settling and a rectangle being placed.

       AMBIENT, WHICH IS THE FOURTH ROLE AND WAS THE MISSING ONE. A state
       change at 130, something appearing at 220, something crossing at 320 —
       and then the sky, the clouds, the stars, the snow, a photograph
       crossfading in its frame and the brand mark changing colour with the
       theme, which were spending 0.9s, 1.2s, 1.4s, 1400ms and 1600ms on one
       decision. The point of an ambient fade is that you never catch it
       happening, so the exact number matters less than there being one of it.
       1.2s is the number seven of the ten already agreed on.

       A TICK IS NOT A DURATION, IT IS THE CLOCK. .mb-fuse's width is rewritten
       once a second by mbPaintClock(), and the transition has to be exactly
       that interval or the bar never arrives where it was sent — at 1.2s it
       would run permanently a sixth of a step behind the countdown it claims
       to be drawing, which is a number on screen that is not true.

       AND NEVER IS NOT A DURATION EITHER, it is a switch wearing a speed:
       Chrome paints its own yellow over an autofilled input and the only way
       to refuse it is to start a background-color transition so long it never
       arrives. It is named rather than exempted in tests/lint-the-scale.js
       because "very large numbers are fine" would wave through a great many
       things that are simply wrong, and this is the only one that is right. */
    --dur-ambient: 1.2s;
    --dur-tick: 1s;
    --dur-never: 9999s;
    /* ------------------------------------------------------------------
       THE THREE WASHES, AND THE THING THEY FIX.

       Twelve rules in this file paint a hover, or a "this is the one you are
       standing on", with `rgba(255,246,230,.045)` written out in full — a
       warm white at four and a half percent. On charcoal that is a correct
       and rather nice touch of light. ON EVERY LIGHT THEME IT IS WHITE ON
       WHITE AND DOES NOTHING AT ALL: hovering a row in the rail on Denim,
       Terracotta, Alabaster, Parchment, Light or any of the three Serenity
       papers gave no feedback whatever, because the feedback was a lighter
       version of an already-light surface.

       A hover that cannot be seen is a control that looks like it works and
       does not. So the wash becomes a token with two definitions, and the
       light one is made of the theme's OWN INK rather than of white — a hint
       of Denim's navy on Denim, of Terracotta's near-black brown on
       Terracotta. It darkens where the dark themes lighten, which is the only
       way one name can read on both.

         --wash-soft   a hover in the open rail
         --wash-hover  a hover where the row is a square around an icon, and
                       the backing behind a focus ring, both of which need more
         --wash-on     the row you are standing on

       --wash-soft WAS ALREADY BEING ASKED FOR by the matte icon-button rule
       further down this file, with the same warm white as its fallback and
       nothing anywhere defining it. Defining it here fixes that rule too, and
       it is the second dead token found this shift — see the note by
       :root[data-mode="light"] .chat-pill for the first and worse one.

       SIX RULES POINT AT THESE SO FAR: the rail rows, the collapsed rail rows,
       the current row, the spine's add button, the attention mini button and
       the matte icon button — the rail and the spine, which is the ground this
       shift owns. The other six sites of the literal are in menus and account
       switchers that other work is inside this week; they are listed in the
       shift notes rather than changed here, because a merge conflict in a
       stylesheet is how a colour goes missing.
       ------------------------------------------------------------------ */
    --wash-soft: rgba(255,246,230,.045);
    --wash-hover: rgba(255,246,230,.075);
    --wash-on: rgba(255,246,230,.05);
    --text:#F4F6FD; --text-dim:#A6AFD4; --text-faint:#8C94BC;
    --ring: rgba(255,255,255,0.42); --ring-soft: rgba(255,255,255,0.22);
    --blue:#3D6BFF; --blue-2:#7C9CFF; --blue-glow: rgba(61,107,255,0.5);
    --green:#B8E14A; --green-2:#D3EE8E; --green-glow: rgba(184,225,74,0.45);
    --violet:#A07DFF; --violet-2:#D3C2FF; --violet-glow: rgba(160,125,255,0.42);
    --orange:#FF9A44; --orange-2:#FFBF87; --orange-glow: rgba(255,154,68,0.45);
    --pink:#FF5A47; --pink-2:#FF9A87; --pink-glow: rgba(255,90,71,0.42);
    --teal:#2DD4BF; --teal-2:#8FEFE3; --teal-glow: rgba(45,212,191,0.42);
    /* Contact/Organization category colors — deliberately separate from
       the shared account-color palette above (which Mail/Calendar use).
       Deeper jewel-tones instead of the brighter neon set: the glow itself
       provides the "pop," so the base fill doesn't need to be as vivid —
       reads as premium rather than a rave with six saturated hues glowing
       across a page of cards at once. */
    --cat-blue:#2954D9; --cat-blue-2:#6E8CE8; --cat-blue-glow: rgba(41,84,217,0.45);
    --cat-green:#3F9B6B; --cat-green-2:#7EC49E; --cat-green-glow: rgba(63,155,107,0.4);
    --cat-violet:#7B5FD9; --cat-violet-2:#AD98E8; --cat-violet-glow: rgba(123,95,217,0.4);
    --cat-orange:#D97D2E; --cat-orange-2:#EAA766; --cat-orange-glow: rgba(217,125,46,0.42);
    --cat-red:#B8382E; --cat-red-2:#D97066; --cat-red-glow: rgba(184,56,46,0.42);
    --cat-teal:#1F9E8E; --cat-teal-2:#5FC4B6; --cat-teal-glow: rgba(31,158,142,0.4);
    /* FOUR MORE, TO MAKE TEN. His #5. Picked to sit alongside the six above at
       the same weight rather than shouting over them — the same mid-saturation
       band, so no category reads as more important than another by accident. */
    --cat-gold:#B08A20; --cat-gold-2:#DCC067; --cat-gold-glow: rgba(176,138,32,0.42);
    --cat-rose:#C0487A; --cat-rose-2:#E087AC; --cat-rose-glow: rgba(192,72,122,0.40);
    --cat-slate:#5A6B85; --cat-slate-2:#93A3BC; --cat-slate-glow: rgba(90,107,133,0.38);
    --cat-moss:#6E8F35; --cat-moss-2:#A6C273; --cat-moss-glow: rgba(110,143,53,0.40);
    --ink:#031225;
    --pulse-lo: color-mix(in srgb, var(--blue) 25%, transparent);
    --pulse-hi: color-mix(in srgb, var(--blue) 48%, transparent);
    --neutral-1:#EDEFFC; --neutral-2:#ffffff; --neutral-ink:#0A0E1F; --neutral-glow: rgba(255,255,255,.2);
    --radius:16px;
    /* Accent — buttons/highlights follow the selected account's color;
       defaults to blue when "All accounts" is selected. Set at runtime
       by applyAccentColor() in the main script. */
    --accent: var(--blue); --accent-2: var(--blue-2); --accent-glow: var(--blue-glow);

    /* ============================================================
       THE FIVE AREAS, AND THE METAL.
       ------------------------------------------------------------
       2 September. Matt, on the spine: "the arrows offsetting the media due to
       menu, the LCS initials, the oblong action button....I need anything we do
       to be the top of the line." Then, on the third mock: "nailed it....love
       the color per area (which scared me as I dont want a circus sideshow)."

       That fear is the whole brief for these five values. The rail's accents
       used to be --blue and --violet at full strength, which is the circus he
       meant — a saturated hue is a toy, and five of them at once is a toybox.
       These are the same five hues taken DOWN: mid-saturation, none of them
       brighter than the brass they sit beside, chosen the way --cat-* above was
       chosen and for the same stated reason ("premium rather than a rave").

       Brass is the metal everything not-an-area wears — the masthead line, the
       focus ring, a count that is currently yours. It is deliberately the same
       value as --area-work, because Work IS the brass in this scheme rather
       than a sixth colour competing with it.

       WHY THEY ARE NOT ALIASES OF --cat-*. The --cat-* set is contact and
       organization categories, and a person can retag a company tomorrow. An
       area's colour must never move, because the point of it is that the same
       green marks Life in the rail, in the flyout and on the page header. Two
       independent palettes that happen to sit in the same saturation band.

       LIGHT MODE TAKES DARKER STEPS, below. These five are lit for a near-black
       rail; on cream they measure as pastels, and a pastel hairline against
       paper is the invisible-control failure this file has hit three times.

       EVERY THEME NOW CARRIES ITS OWN FIVE, and this block is Dark's.
       ------------------------------------------------------------------
       4 September. Asked whether he wanted area colours that move with the
       theme or one fixed set for the whole wardrobe, Matt answered: "Colors
       that move with the theme." Until then five of the thirteen themes —
       Chocolate, Forest, Ink, Alabaster, Parchment — re-tinted these five for
       their own ground and the other eight shared this one set, which is how
       Light ended up wearing a metal that measured 3.56:1 against the bottom
       of its own page gradient while Parchment, two chips along in the same
       picker, held 4.83:1 for the same token. Each theme's block below now
       states its own five, measured against its own ground.

       THESE VALUES WERE DARK'S, AND DARK HAS GONE — row 2136, and the sentence
       that used to be here is the whole of row 2162. It read: "Dark is the only
       theme with no :root[data-theme=…] block — it IS the base :root, and it is
       what an unrecognised or unset theme falls back to painting… Anything that
       lands here without a theme gets Dark's colours, which is the right answer
       for the same reason it always was."

       That stopped being the right answer on 14 September, when Dark was
       deleted. The base set was still here, still built for #080A1A, and still
       what an unrecognised name painted out of — so "fell back to Dark" had
       quietly become "fell back to the leftovers of a theme that is not in the
       picker, is not in the retirement table and cannot be chosen". Nothing on
       screen said so, and the Mac titlebar took the metal on this line and wore
       it, because the page measures --brass off whatever is live and posts it
       to the shell.

       NOTHING ARRIVES HERE WITHOUT A THEME ANY MORE. parseTheme() in app.js now
       refuses a name that is not in THEME_CYCLE and returns DEFAULT_THEME
       instead, loudly, so data-theme is always a theme that exists and one of
       the blocks below is always in force. These five stay as the floor under
       that — a base every block below overrides in full, and the thing a theme
       added tomorrow inherits if somebody forgets one — rather than as a
       thirteenth room nobody can open. They measure 5.28:1 at their worst on
       #080A1A, which is Midnight's neighbourhood too, so there is nothing to
       re-tune. */
    --area-now:#8195C7; --area-people:#BE7A87; --area-work:#C8A45D;
    --area-media:#5F9E96; --area-life:#8CA36D;
    --brass:#C8A45D; --brass-dim:#8E7639;
    --titlebar: color-mix(in srgb, var(--brass) 10%, var(--bg-mid));

    /* THE RAIL'S OWN INK, AND WHY IT IS WARMER THAN THE APP'S.
       --text is #F4F6FD — a blue-white, which is a screen colour. #F3EFE6 is
       paper. Everything set on it reads as printed rather than lit, and that
       single shift is most of what separated the mock he approved from the rail
       he called a garage. It is scoped to the spine on purpose: turning the
       whole app warm is a one-line change from here, but it is a change to
       every page at once and he has not seen that yet. */
    --rail-ink:#F3EFE6; --rail-ink-2:#9E9A90; --rail-ink-3:#6B685F;

    /* ============================================================
       THE LIFE WALL'S HEAT RAMP — treatment H, and the only colour on
       that page that means anything.
       ------------------------------------------------------------
       Six steps that encode exactly ONE variable — how far away a thing
       is — because that is the only variable on the Life wall worth a
       colour, and it is what stops a coloured page becoming the circus
       he was afraid of in item 210.

       EVERY STEP IS AN ALIAS ONTO A TOKEN THAT ALREADY MOVES WITH THE
       THEME, which is why there is no second copy of this block in any
       of the twelve theme blocks below. --cat-red-2, --cat-orange-2,
       --brass, --area-life and --text-faint are each stated per theme
       already, so the ramp re-tunes itself for cream, for Forest and for
       Obsidian without anybody having to remember to come here. A literal
       #D97066 on this line would have been the one colour on the page
       that stayed a dark-room colour on paper.

       THE ONE THAT NEEDED WORK IS THE FOURTH. --cat-slate-2 is declared
       once, in this block, and inherited by all twelve themes — so on
       cream it is a #93A3BC pastel, which is the invisible-control
       failure this file has hit three times. Mixing it toward --text
       fixes it in BOTH directions at once rather than in a light-theme
       override that a thirteenth theme would miss: --text is near-black
       on paper and near-white in the dark, so the mix always moves the
       step away from the ground it is being read against. */
    --life-heat-0: var(--cat-red-2);
    --life-heat-1: var(--cat-orange-2);
    --life-heat-2: var(--brass);
    --life-heat-3: var(--area-life);
    --life-heat-4: color-mix(in srgb, var(--cat-slate-2) 62%, var(--text));
    --life-heat-5: var(--text-faint);

    /* THE GROUND, AND WHY THE WALL IS NOT THE SAME FLAT DARK AS EVERY
       OTHER PAGE. Matt on the app in general: "We have white and white,
       and everything else is dark." Every area page sits on the same
       gradient, which is a large part of why Life looked like Media.
       This is that gradient rotated toward the area's own colour — the
       same luminance with a little of Life's green put in — and it is
       derived rather than stated so that every theme's wall is that
       theme's own ground rather than Dark's. The floor, --bg-deep, is
       left exactly alone: the bottom of a page should be the same in
       every room of the app. */
    --life-wall-1: color-mix(in srgb, var(--bg-mid) 93%, var(--area-life));
    --life-wall-2: color-mix(in srgb, var(--bg-base) 95%, var(--area-life));
    --life-grid: color-mix(in srgb, var(--text-faint) 20%, transparent);
    --life-piece: color-mix(in srgb, var(--area-life) 6%, var(--glass-bg));

    /* THE LIGHT THE WALL'S DRAWINGS ARE MODELLED IN — the night half.
       See the twin block under :root[data-mode="light"] for the day half,
       and the long note at the head of THE DRAWINGS in app.js for why
       there are two halves at all.

       ON BLACK YOU DRAW BY ADDING LIGHT. A highlight is a pale colour, a
       lit face is a pale colour, and a shadow is an ABSENCE of light, so
       --la-cast runs to the floor of the page. Turn the ground over and
       every one of those moves reverses, which is why not one of these is
       stated once and inherited: a day theme that fell through to this
       block would paint a near-black shade onto cream, and a near-black
       shade on cream is not a shadow, it is a hole punched in the page.
       That is the exact complaint Matt made about the clouds and the
       graphs, and this is the shape of the fix.

       EVERY ONE IS MIXED FROM A TOKEN EVERY THEME ALREADY STATES —
       --text, --bg-mid, --bg-base, --bg-deep, --area-life, --brass — so a
       family arriving next month gets a lit wall without anybody
       remembering to come back here, and the four families the theme work
       is collapsing to get one each for free. --h, the piece's own heat,
       is deliberately not in this list: it is set per piece by the wall,
       and it is what makes the same object read hot in September and cool
       in March. */
    --la-core:  var(--text);
    --la-spec:  color-mix(in srgb, var(--text) 88%, var(--brass));
    --la-rim:   color-mix(in srgb, var(--text) 58%, var(--area-life));
    /* --la-face is the plane turned INTO the key and --la-lit the plane just
       off it. Two steps rather than one, because a solid with a lit face and a
       shade has a corner and a solid with only a body has a silhouette — which
       is exactly what the house on this wall was for one round of pictures. */
    --la-face:  color-mix(in srgb, var(--bg-mid) 24%, var(--text-faint));
    --la-lit:   color-mix(in srgb, var(--bg-mid) 56%, var(--text-faint));
    --la-body:  color-mix(in srgb, var(--bg-base) 78%, var(--text-faint));
    --la-deep:  var(--bg-deep);
    --la-cast:  var(--bg-deep);
    --la-ao:    var(--bg-deep);
    --la-haze:  color-mix(in srgb, var(--bg-mid) 62%, var(--text-faint));
    /* A SHEET OF PAPER IS A SHEET OF PAPER IN BOTH ROOMS. A document drawn
       dark because it needed to separate from black is the single most
       repeated light-mode bug in this app, and the vault's fan is six of
       them at once. */
    --la-sheet:   color-mix(in srgb, var(--text) 86%, var(--brass));
    --la-sheet-2: color-mix(in srgb, var(--text) 52%, var(--bg-mid));
    --la-ink:     var(--bg-deep);
    --la-dust:    var(--text-faint);
    /* THE PHYSICS, and it is four values rather than a second stylesheet.
       The identical bloom that ADDS light at night MULTIPLIES by day, and
       a bloom at night's strength multiplies into a bruise. */
    --la-blend: screen;
    --la-bloom: 1;
    /* How much of the ground plane is left in. A dark ground plane under an
       object is a floor at night and, on a short mount on cream, a ruled grey
       slab — because subtracting light from paper is a much louder move than
       adding it to black. Same drawing, two strengths, one number. */
    --la-plane: .22;
    --la-grain-o: .11;
    --la-grain-blend: overlay;
  }

  /* The five areas and the metal, stepped down for cream. Same hues, enough
     ink in them to hold an edge against paper. The rail's ink goes back to the
     app's own text colours — warm ink is a dark-room idea, and #F3EFE6 on
     cream is not a colour, it is a smudge.

     SINCE 4 SEPTEMBER EVERY LIGHT THEME OVERRIDES THESE FIVE, and they are
     kept anyway as the floor a new one lands on. They are no longer what Light
     or Serene paint — both now state their own, darker, against their own
     grounds. What is left here is the answer to "somebody adds a paper theme
     next month and forgets the rail": it gets a set that is at least stepped
     for cream rather than the near-black set from :root above, which on paper
     is the pastel-hairline failure the block above names. The rail ink lines
     are NOT dead, though — nothing below overrides those, so they are still
     doing the work on every light theme in the wardrobe. */
  :root[data-mode="light"]{
    --area-now:#4E5F8C; --area-people:#9B4E5C; --area-work:#8E7639;
    --area-media:#3D7A72; --area-life:#5E7440;
    --brass:#8E7639; --brass-dim:#6E5A2B;
    --rail-ink:var(--text); --rail-ink-2:var(--text-dim); --rail-ink-3:var(--text-faint);
    /* THE WASHES, INVERTED — see the long note at the top of this file. Made
       from var(--text), which every ground defines as its own ink, so this one
       declaration gives Denim a navy hover, Terracotta a clay-brown one and
       Mist a green-grey one without naming a single colour. Nothing resolves
       here: a custom property is a token stream until it is used, so --text is
       read at the row rather than at this line, and each theme's own ink is
       what arrives.
       The percentages are higher than the dark set's because a dark wash on
       paper has to beat the page's own gradient, which runs eleven points of
       lightness from the top of the rail to the bottom of it. */
    --wash-soft: color-mix(in srgb, var(--text) 6%, transparent);
    --wash-hover: color-mix(in srgb, var(--text) 10%, transparent);
    --wash-on: color-mix(in srgb, var(--text) 7%, transparent);

    /* THE LIFE WALL'S DRAWINGS, RE-DERIVED FOR PAPER — the day half, and
       this block is the whole of the light-mode fix for that page.

       Every token in the night half above was authored against black,
       where a shape is drawn by ADDING light: the highlight is a pale
       colour, the lit edge is a pale colour, and the deep end of a
       gradient runs toward the ground, which on black means toward black.
       Turn the ground over and every one of those reverses. On paper a
       shape is drawn by SUBTRACTING light: the highlight is where the ink
       STOPS, the lit face is nearly bare page, and the deep end is
       saturated hue with ink in it — never a run to black, because a run
       to black on cream is a hole rather than a shade.

       THE RIM INVERTS RATHER THAN DIMS, and that is the one move that is
       not a swap of two numbers. A rim light is the edge of a form on the
       side AWAY from the key; on black that is where light creeps round
       and so it is BRIGHTER than the form. On paper it is simply the side
       with more ink on it, so it becomes a fine dark edge. Dimming the
       night value instead leaves a pale hairline on cream, which is the
       invisible-control failure this stylesheet has already hit three
       times.

       AND THE CAST SHADOW IS NOT BLACK. A black cast shadow on a toned
       board reads as dirt on the page rather than as an object sitting on
       it, so it is the page's own ink with the area's green in it.

       Not one literal colour here either: everything is mixed from
       --bg-mid, --text, --area-life and --brass, all of which every light
       family states for itself. */
    --la-core:  color-mix(in srgb, var(--bg-mid) 94%, var(--brass));
    --la-spec:  color-mix(in srgb, var(--bg-mid) 88%, var(--brass));
    --la-rim:   color-mix(in srgb, var(--text) 46%, var(--area-life));
    --la-face:  color-mix(in srgb, var(--bg-mid) 94%, var(--area-life));
    --la-lit:   color-mix(in srgb, var(--bg-mid) 78%, var(--area-life));
    --la-body:  color-mix(in srgb, var(--area-life) 52%, var(--bg-mid));
    --la-deep:  color-mix(in srgb, var(--area-life) 46%, var(--text));
    --la-cast:  color-mix(in srgb, var(--text) 34%, var(--area-life));
    --la-ao:    color-mix(in srgb, var(--text) 52%, var(--area-life));
    --la-haze:  color-mix(in srgb, var(--bg-mid) 82%, var(--area-life));
    --la-sheet:   color-mix(in srgb, var(--bg-mid) 96%, var(--brass));
    --la-sheet-2: color-mix(in srgb, var(--bg-mid) 74%, var(--brass));
    --la-ink:     var(--text);
    --la-dust:    color-mix(in srgb, var(--text) 62%, var(--area-life));
    --la-blend: multiply;
    --la-bloom: .40;
    --la-plane: .12;
    --la-grain-o: .07;
    --la-grain-blend: multiply;
  }

  /* ============================================================
     SURFACE TREATMENTS — three dark variants beyond the default.

     The DEFAULT dark theme separates card from page by LUMINANCE: the card
     is simply lighter than what's behind it. The two "edge-lit" themes below
     (midnight, obsidian) deliberately throw that away. Their cards sit at
     almost exactly the same darkness as the page, and depth comes entirely
     from modelled light instead:

       1. A bright top border (--glass-border-hi) acting as a rim/specular
          highlight, as if a light source sits above and in front.
       2. A directional light POOL replacing the old flat top sheen —
          --glass-sheen becomes a radial gradient anchored off the card's
          top-left corner, so every card looks lit from a consistent
          direction rather than uniformly tinted.
       3. A much deeper cast shadow, which is what actually sells elevation
          once the brightness cue is gone.

     FROSTED goes the opposite way: it keeps the ambient bloom HIGH (you need
     something behind the glass for frost to be frosted *of*), makes the card
     fill very translucent, and pushes backdrop blur from 24px to 44px so the
     background reads through as diffused colour.

     All four inherit every other token from dark — accents, the --cat-*
     jewel tones, all text — so nothing but the surfaces change.
     ============================================================ */

  /* MIDNIGHT — deep blue room, cards the same darkness as the room, edge-lit. */
  :root[data-theme="midnight"]{
    --bg-mid:#030718; --bg-base:#01040E; --bg-deep:#000206;
    --bloom-1: rgba(61,107,255,0.06); --bloom-2: rgba(160,125,255,0.035);
    --bloom-3: rgba(255,154,68,0.02); --bloom-4: rgba(184,225,74,0.02);
    --glass-bg: rgba(14,24,58,0.62);
    --glass-border: rgba(150,180,255,0.10); --glass-border-hi: rgba(150,185,255,0.44);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.6), 0 12px 26px -6px rgba(0,0,0,0.66), 0 40px 80px -20px rgba(0,0,0,0.85), 0 0 40px -12px rgba(61,107,255,0.16), inset 0 1px 0 rgba(160,190,255,0.14);
    --glass-sheen: radial-gradient(130% 90% at 14% -12%, rgba(140,175,255,0.17), rgba(140,175,255,0.045) 38%, rgba(255,255,255,0) 72%);
    --glass-blur: 26px; --glass-sat: 150%;
    /* THE FIVE, IN A BLUE ROOM. Midnight's ground is #030718 — darker than
       Dark's and with a hue you can name — and the blooms over it are blue.
       Dark's five were lit for a blue-BLACK, so on this ground the two warm
       ones sat slightly apart from the room while the cool ones vanished into
       it: the rail read as gold-and-rose plus three smudges. Each hue is
       turned a few degrees toward the room's own blue and lifted a step,
       which the darker ground pays for — the tightest of the five measures
       5.88:1 on the page here against 5.28:1 for Dark's set on Dark.
       Media is the one that moved most: #5F9E96 is a green-teal that read as
       a dim cousin of Life on a blue ground, and #5CA3AC is the same teal
       turned toward cyan so the two stop being the same idea. */
    --area-now:#8AA6D8; --area-people:#BC8096; --area-work:#C6A868;
    --area-media:#5CA3AC; --area-life:#8AA97C;
    --brass:#C6A868; --brass-dim:#8C7645;
    /* THE WORK METAL — row 2141. STEEL — Matt named this one. A cool blue-grey that belongs to a blue
       room; a warm metal here would be the only warm thing in it. 9.42:1 on a
       resting card, 7.38:1 on a lit one. */
    --work-metal:#A9B8CE;
  }

  /* OBSIDIAN — the same idea with the blue taken out. Near-black, neutral,
     maximum separation between "unlit surface" and "lit edge". */
  :root[data-theme="obsidian"]{
    --bg-mid:#0A0A0D; --bg-base:#050506; --bg-deep:#000000;
    --bloom-1: rgba(61,107,255,0.035); --bloom-2: rgba(160,125,255,0.025);
    --bloom-3: rgba(255,154,68,0.015); --bloom-4: rgba(184,225,74,0.015);
    --glass-bg: rgba(23,24,29,0.66);
    --glass-border: rgba(255,255,255,0.07); --glass-border-hi: rgba(255,255,255,0.34);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.7), 0 14px 30px -8px rgba(0,0,0,0.75), 0 46px 90px -22px rgba(0,0,0,0.92), inset 0 1px 0 rgba(255,255,255,0.10);
    --glass-sheen: radial-gradient(130% 90% at 14% -12%, rgba(255,255,255,0.13), rgba(255,255,255,0.035) 38%, rgba(255,255,255,0) 72%);
    --glass-blur: 22px; --glass-sat: 130%;
    /* THE FIVE, WITH THE BLUE TAKEN OUT OF THEM TOO. This theme's whole idea
       is a room with no hue in it, and Dark's five are mixed for a ground that
       has one — on neutral near-black they are the most saturated thing on the
       screen by a distance, which is the "circus sideshow" the base block says
       these values exist to avoid. Each is pulled about a tenth of its chroma
       toward grey and given back the lightness that costs, so they still read
       as blue/rose/gold/teal/green rather than as five greys.
       The floor is the pair that always gets closest — the tightest here is
       Media against Life at ΔE 22.4, which is the same distance Chocolate and
       Ink live at, so pulling the chroma did not collapse anything. */
    --area-now:#8B9BBC; --area-people:#BE8189; --area-work:#C8A96B;
    --area-media:#6C9E97; --area-life:#95A67C;
    --brass:#C8A96B; --brass-dim:#8E7844;
    /* THE WORK METAL — row 2141. PLATINUM — the sharpest theme takes the coldest, brightest metal.
       Neutral to the point of being hueless, which is what Obsidian is.
       11.59:1 on a resting card, 8.94:1 on a lit one. */
    --work-metal:#C8CACE;
  }

  /* ============================================================
     REGAL — the one that is a different REGISTER, not another palette.
     ============================================================
     Matt sent a reference and said: "I absolutely love this look… call it
     elegant or regal." What he liked was not a colour. It was restraint —
     deep bottle-green glass, a warm bloom well away from the content, hairline
     rules instead of borders, and a great deal of air.

     So this theme changes three things the other four do not:

       1. THE GROUND IS GREEN, not blue or neutral. Deep bottle glass. It is the
          only theme here whose ground has a hue you would name.
       2. ONE ACCENT, and it is BRASS. Every other theme inherits Lucid's five
          jewel accents and uses whichever the surface calls for. This one
          overrides --accent to aged brass and leaves it there, so a page has a
          single warm note on a lot of quiet instead of five competing ones.
          That is the whole difference between "elegant" and "colourful".
       3. HAIRLINES, NOT EDGES. The border drops to a barely-there warm line and
          the lit edge is halved. Depth comes from the shadow and the bloom
          rather than from a drawn outline.

     The bloom is warm and deliberately weak — it sits behind the glass as a
     hint of a lamp somewhere off-page, which is what stops a dark green room
     from reading as a terminal.

     Text tokens are NOT overridden. They were measured against every theme and
     this ground is darker than dark's, so they pass here by construction — see
     tests/test-readability.js, which now runs this theme too. */
  :root[data-theme="regal"]{
    --bg-mid:#0A1512; --bg-base:#05100D; --bg-deep:#020706;
    /* Green-tinted ink. The inherited lilac dim measured 4.46:1 here — under
       the 4.5 floor — so the ramp is re-stepped against this ground, not
       borrowed. #B0BEB5 measures 5.0:1 on it. Dark ink on the brass fill,
       because brass is light and white on it measured 3.51:1. */
    --text:#F2F6F1; --text-dim:#B0BEB5; --text-faint:#98A79D;
    --on-accent:#0A1512;
    /* Warm, faint, and placed away from the middle: a lamp off-page rather
       than a light behind the content. */
    --bloom-1: rgba(214,168,96,0.055); --bloom-2: rgba(120,180,150,0.035);
    --bloom-3: rgba(214,168,96,0.030); --bloom-4: rgba(80,140,120,0.020);
    --glass-bg: rgba(16,36,30,0.62);
    /* A hairline, not a border. Half the lit edge of the other dark themes. */
    --glass-border: rgba(220,200,160,0.085); --glass-border-hi: rgba(226,201,148,0.26);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.62), 0 14px 32px -8px rgba(0,0,0,0.7),
                    0 48px 96px -24px rgba(0,0,0,0.9), 0 0 46px -14px rgba(214,168,96,0.10),
                    inset 0 1px 0 rgba(226,201,148,0.09);
    --glass-sheen: radial-gradient(130% 90% at 16% -14%, rgba(226,201,148,0.10),
                    rgba(226,201,148,0.028) 40%, rgba(255,255,255,0) 74%);
    --glass-blur: 28px; --glass-sat: 140%;

    /* ONE ACCENT. Brass, and only brass. */
    --brass:#D6A860; --brass-2:#EFD6A6; --brass-glow: rgba(214,168,96,0.40);
    --accent: var(--brass); --accent-2: var(--brass-2); --accent-glow: var(--brass-glow);
    /* The two that read as "this is happening now" follow the accent, or the
       page grows a second warm colour that means nothing different. */
    --pulse-lo: color-mix(in srgb, var(--brass) 22%, transparent);
    --pulse-hi: color-mix(in srgb, var(--brass) 44%, transparent);
    /* Blue is Lucid's default accent and it is wrong in this room — it is the
       one hue that fights bottle green. Warmed toward the brass rather than
       replaced, so anything hard-coded to --blue still belongs here. */
    --blue:#8FB9A8; --blue-2:#BFDCCF; --blue-glow: rgba(143,185,168,0.34);
    --violet:#B9A2D8; --violet-2:#DCCEEE; --violet-glow: rgba(185,162,216,0.30);
    --teal:#6FBFA8; --teal-2:#A9DCCC; --teal-glow: rgba(111,191,168,0.32);
    --green:#A8C878; --green-2:#CEE0AE; --green-glow: rgba(168,200,120,0.32);
    --pink:#D98C74; --pink-2:#EEBBAB; --pink-glow: rgba(217,140,116,0.32);
    /* Orange was the ONE hue this room never redefined, so it fell through to
       the default #FF9A44 — a bright modern orange in a bottle-green-and-brass
       room, which is exactly the fight the note above describes. Found while
       giving the chat source chips a hue each: nine of the ten had a version
       for this paper and the tenth was shouting. Warmed to amber to sit with
       the brass. */
    --orange:#D9A05B; --orange-2:#EFCB9A; --orange-glow: rgba(217,160,91,0.30);

    /* THE FIVE AREAS, IN THE ONE ROOM THAT HAS A COLOUR.
       Every hue above was warmed toward the brass because blue is the one hue
       that fights bottle green — and the rail was still painting Dark's five
       straight over that, so the theme that overrides ten accents to stay in
       one register kept a five-colour navigation rail from a different room.
       These are the same warming, applied to the areas.

       WORK IS THE BRASS, EXACTLY. Not a near-miss of it: #D6A860 is the value
       --accent already carries here, so the one metal in this theme and the
       Work area are the same object rather than two warm colours a shade
       apart, which is the "two metals is jewellery" mistake in miniature.

       NOW STAYS A BLUE, and that is deliberate against the grain of the block
       above. --blue was warmed all the way to #8FB9A8 — a sage — because it is
       read as text on green glass. An area colour is not text; it is a 2px bar
       and a dot, and warming Now that far would have put it inside touching
       distance of both Media and Life, leaving three greens in a five-colour
       rail. #8098B8 is a slate that has enough of the room's grey-green in it
       to belong and enough blue left to still be the blue one: the closest
       pair here is Media/Life at ΔE 29.8, the widest spread of any theme. */
    --area-now:#8098B8; --area-people:#CE8B84; --area-work:#D6A860;
    --area-media:#6BB49E; --area-life:#9FBE76;
    /* Regal never set --brass-dim and so inherited #8E7639 from :root — a
       duller, browner gold than the #D6A860 the rest of this block is built
       on. Nothing reads it today, which is exactly why it would have gone
       wrong the first time something did. */
    --brass-dim:#9A7A3C;
    /* THE WORK METAL — row 2141. BRASS, and by reference rather than by
       value: Regal IS the brass theme, so its metal is the one already stated
       three lines up. Writing the hex again would be a second copy to keep in
       step with the first for no gain. 8.34:1 on a resting card, 6.31:1 on a
       lit one. */
    --work-metal:var(--brass);
  }

  /* ==========================================================================
     THE MATTE SIX — and the finish that makes them possible.
     ==========================================================================
     2 September. Matt, after the spine went in: "our use of light has become
     almost cartoonish. Not keeping with the theme of elegance and expensive...
     brass, maroon, chocolate, and forest green as backgrounds, card, and use of
     light and shadow to make everything look elegant." Then, on the ten I
     mocked up: "you just raised the bar from a cool looking app to something
     similar to Apple....people just have to have it."

     And the right question with it: "How are we going to be able to prop up the
     things we do with color if we get away from the glow?"

     ------------------------------------------------------------------
     THE ANSWER, BECAUSE IT IS THE WHOLE DESIGN OF THIS BLOCK.

     The glow was doing five separate jobs at once, which is exactly why it had
     stopped doing any of them. Each job now has its own signal:

       "you are here"            → a 2px edge bar in the thing's own colour
       "this belongs to X"       → a 3px left keyline plus the colour on the
                                   number or the label, never on the whole row
       "this is a card"          → elevation: a 1px lit top edge and a long soft
                                   shadow. Light from above, and nothing else
       "this is current, yours"  → the metal, on about six things per screen
       "this is LATE"            → the glow. It keeps exactly one job

     That last line is the real answer. Glow did not stop working; it stopped
     MEANING anything, because eighty-five declarations had it and the overdue
     badge was competing with the navigation rail for his eye. Taking it off
     everything else makes the one alarm louder, not quieter. Search this file
     for attnPulse: that is the only glow left in a matte theme, and it fires
     only while something is actually overdue.

     ------------------------------------------------------------------
     HOW THE FINISH IS SWITCHED, and why it is an attribute rather than six
     selectors repeated eighty-five times.

     setTheme() stamps data-finish="matte" or "glow" on the root, the same way
     it already stamps data-mode. Everything below keys off that one attribute.
     Two consequences worth stating:

       1. THE OLD THEMES ARE UNTOUCHED. Dark, Midnight, Obsidian, Frosted,
          Light and all nine Serene combinations still stamp finish="glow" and
          render exactly as they did this morning. Matt: "If it doesnt
          translate we can always revert back." Reverting is choosing Dark.
       2. NEARLY ALL THE GLOW REMOVES ITSELF. Eighty of those eighty-five
          declarations read a --*-glow token, so the matte block sets those
          tokens to transparent once and every one of them collapses. Only the
          handful that hard-code a color-mix or an rgba literal need naming,
          and they are named below.

     WHAT MAKES THESE GROUNDS LOOK EXPENSIVE — five rules, followed by all six:
       1. The ground is a colour mixed down to near-black, not black with a
          tint dropped on it. Chocolate at 6% lightness still reads as brown.
       2. The card is three or four points lighter than the ground and is
          separated by a hairline and a shadow, never by contrast.
       3. One metal. Two metals is jewellery.
       4. Ink is warm and never pure white.
       5. Light from above: one lit top edge, one long shadow, no coloured
          halo hidden in the shadow stack the way Midnight and Regal hide one.
     ========================================================================== */

  /* BORDEAUX — oxblood taken to 6% lightness. A leather chair, not a red
     screen. The dim ink is re-stepped against this ground rather than
     borrowed: the inherited lilac measures under the 4.5 floor on anything
     this warm, which is the same trap Regal fell into. */
  /* ONE LAMP, NOT TWO. body's second bloom sits at 108% 18% — the top-right
     corner, over a 1000x760 radial — and on the first pass every matte theme
     gave it a second hue: rose on Bordeaux, taupe on Chocolate, mint on Forest.
     At 3% each of them was still the brightest object on Home and it was in a
     corner saying nothing. All four now carry the theme's own metal, weaker, so
     the room has one light in it rather than a light and a rumour of another.
     Found by looking at shots/matte-forest-home.png, not by reading this file:
     a 3% wash is invisible in a diff and unmistakable in a screenshot. */
  /* BORDEAUX STOOD HERE, and it was cut on 4 September on Matt's instruction:
     asked which of thirteen themes to keep, he answered "Get rid of red."
     It was the oxblood one — the only red ground in the wardrobe — and its
     whole block is gone rather than left commented out, because a theme nobody
     can choose is a theme nobody maintains, and a stylesheet that keeps its
     dead is how you end up with 11,000 lines nobody trusts. If it is ever
     wanted back it is in the history, whole.
     The other five matte themes (Chocolate, Forest, Ink, Alabaster, Parchment)
     are untouched — the finish was never the objection. */
  /* CHOCOLATE — espresso, not brown. The warmest of the six, and the one where
     cream ink stops reading as white text and starts reading as paper. */
  :root[data-theme="chocolate"]{
    --bg-mid:#16120D; --bg-base:#100D09; --bg-deep:#080604;
    --text:#F3EAD8; --text-dim:#C4B6A6; --text-faint:#A2947F;
    /* 4.47:1 on the contact-detail tabs at #BFB0A0 — three hundredths under
       the floor, which is exactly the kind of miss no eye finds and every
       warm ground produces. Same trap as Bordeaux above, one step up. */
    --bloom-1: rgba(217,179,117,0.045); --bloom-2: rgba(217,179,117,0.024);
    --bloom-3: rgba(217,179,117,0.022); --bloom-4: rgba(110,90,60,0.018);
    --glass-bg: rgba(42,35,25,0.62);
    --glass-border: rgba(255,236,205,0.075); --glass-border-hi: rgba(255,236,205,0.26);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.62), 0 14px 32px -8px rgba(0,0,0,0.72),
                    0 48px 96px -24px rgba(0,0,0,0.92), inset 0 1px 0 rgba(255,236,205,0.08);
    --glass-sheen: radial-gradient(130% 90% at 16% -14%, rgba(255,236,205,0.09),
                    rgba(255,236,205,0.024) 40%, rgba(255,255,255,0) 74%);
    --glass-blur: 26px; --glass-sat: 140%;
    --brass:#D9B375; --brass-dim:#9A7C42; --brass-2:#F0DCB8;
    --rail-ink:#F3EAD8; --rail-ink-2:#AFA294; --rail-ink-3:#7A7061;
    --area-now:#93A3C6; --area-people:#D4948A; --area-work:#D9B375;
    --area-media:#7FAFA2; --area-life:#A9B87D;
    --blue:#93A3C6; --blue-2:#C2CCE0; --violet:#B79EC8; --violet-2:#D9CBE3;
    --teal:#7FAFA2; --teal-2:#B2D0C9; --green:#A9B87D; --green-2:#CDD7AF;
    --pink:#D4948A; --pink-2:#EBC1BA; --orange:#D9B375; --orange-2:#F0DCB8;
    /* THE WORK METAL — row 2141. COPPER — Matt named this one too, and it is the pair that made the
       idea legible: espresso and copper is a real object. 6.73:1 on a resting
       card, 4.98:1 on a lit one. */
    --work-metal:#DA8E5E;
  }

  /* INK & EMERALD — the restrained one. True black, near-monochrome ink, and a
     single live green. The metal here is the emerald: a theme gets one, and on
     this ground brass would be the second. */
  :root[data-theme="ink"]{
    --bg-mid:#08090A; --bg-base:#050506; --bg-deep:#000000;
    --text:#F0F1F2; --text-dim:#B2B8BC; --text-faint:#8B9196;
    /* True black is the ground you would expect to have contrast to spare,
       and it does — everywhere except on the tab strips, which sit on a
       lifted panel rather than on the page. #A8AEB2 measured 4.37–4.45 on
       those three. The ramp is stepped against the PANEL, not the room. */
    --bloom-1: rgba(47,158,79,0.032); --bloom-2: rgba(255,255,255,0.012);
    --bloom-3: rgba(47,158,79,0.018); --bloom-4: rgba(255,255,255,0.012);
    --glass-bg: rgba(20,22,24,0.66);
    --glass-border: rgba(255,255,255,0.07); --glass-border-hi: rgba(255,255,255,0.26);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.7), 0 14px 30px -8px rgba(0,0,0,0.8),
                    0 46px 90px -22px rgba(0,0,0,0.95), inset 0 1px 0 rgba(255,255,255,0.075);
    --glass-sheen: radial-gradient(130% 90% at 14% -12%, rgba(255,255,255,0.075),
                    rgba(255,255,255,0.02) 38%, rgba(255,255,255,0) 72%);
    --glass-blur: 22px; --glass-sat: 120%;
    /* FOREST, not mint (Matt, 30 Sep: "forest green, or something stronger than
       mint"). Pure forest #228B22 reads 3.7-4.1 on the lifted panels; #2F9E4F
       is the forest that holds 6.1 on black, 5.3 on a card, 4.7 on a lit one. */
    --brass:#2F9E4F; --brass-dim:#1F6B36; --brass-2:#9BD6AC;
    --rail-ink:#F0F1F2; --rail-ink-2:#9AA0A4; --rail-ink-3:#63696E;
    --area-now:#7C99C4; --area-people:#C2848F; --area-work:#C4A96F;
    --area-media:#2F9E4F; --area-life:#8DB874;
    --blue:#7C99C4; --blue-2:#B4C4DC; --violet:#A797C4; --violet-2:#CFC6E0;
    --teal:#2F9E4F; --teal-2:#9BD6AC; --green:#8DB874; --green-2:#BFD5B0;
    --pink:#C2848F; --pink-2:#E0B6BD; --orange:#C4A96F; --orange-2:#E2D2AF;
    /* THE WORK METAL — row 2141. PEWTER — near-monochrome wants a near-monochrome metal. The emerald
       is this theme’s ONE live colour and the figures are not it; a green
       count would have spent the theme’s single accent on arithmetic.
       9.84:1 on a resting card, 7.70:1 on a lit one. */
    --work-metal:#B4BABD;
  }

  /* ALABASTER — the dark rules run backwards. Warm paper, one metal, hairlines
     instead of contrast, and a shadow soft enough to read as depth rather than
     as a drawn border. This is what the light themes were supposed to be. */
  :root[data-theme="alabaster"]{
    --bg-mid:#FDFBF7; --bg-base:#F7F4EE; --bg-deep:#EFEBE2;
    --text:#17150F; --text-dim:#57513F; --text-faint:#6E6759;
    /* THE RING IS DARKER ON PAPER THAN IT LOOKS. Row 2232. At 0.32 this
       composited to 2.0:1 against its own ground — a focus ring that clears
       midnight at 4.08:1 and all but vanishes on the two light themes and the
       three papers, which is the fault nobody finds because nobody tests in
       the theme they do not use. 0.55 is 3.3-3.8:1, over the 3:1 WCAG asks of
       a mark that is not text, and it is measured every run by
       tests/test-the-keyboard-reaches-everything.js on all thirteen grounds. */
    --ring: rgba(30,26,16,0.55); --ring-soft: rgba(30,26,16,0.16);
    --bloom-1: rgba(142,118,57,0.05); --bloom-2: rgba(120,110,90,0.035);
    --bloom-3: rgba(142,118,57,0.03); --bloom-4: rgba(90,100,70,0.025);
    --glass-bg: rgba(255,255,255,0.62);
    --glass-border: rgba(40,32,20,0.09); --glass-border-hi: rgba(40,32,20,0.17);
    --glass-shadow: 0 1px 2px rgba(80,64,48,0.07), 0 6px 14px -5px rgba(80,64,48,0.10),
                    0 22px 44px -14px rgba(80,64,48,0.18), inset 0 1px 0 rgba(255,255,255,0.9);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.6), rgba(255,255,255,0) 46%);
    --glass-blur: 22px; --glass-sat: 140%;
    /* THE GOLD, ONE STEP DOWN — the last token under the floor after the
       4 September pass over the other themes. Alabaster wrote out the shared
       cream set by hand rather than tuning one, so it carried the same #8E7639
       metal Light did, and measuring the rail against the BOTTOM of the page
       gradient — which is where it ends, since the rail runs the full height
       of the window — caught it here too: 3.68:1 on --bg-deep, against 4.18
       to 5.29 for the other four in this block. That is the pastel-hairline
       failure again, on the theme whose whole idea is hairlines.
       ONLY THE GOLD MOVED. Darkening all five to the 4.5 floor the way Light
       and Serene were done takes Now from 5.29:1 to 6.39:1 and visibly weighs
       down a theme Matt has already approved, to buy contrast a 2px bar does
       not need. #82662C is the same gold pulled a little toward its red at
       nine tenths the lightness: 4.54:1 on --bg-deep, and it widens the
       closest pair in this set (Work against Life) from ΔE 22.5 to 24.4.
       --brass follows it, because Work IS the metal here. */
    --brass:#82662C; --brass-dim:#6E5A2B; --brass-2:#B99A50;
    --rail-ink:#17150F; --rail-ink-2:#57513F; --rail-ink-3:#6E6759;
    --area-now:#4E5F8C; --area-people:#9B4E5C; --area-work:#82662C;
    --area-media:#3D7A72; --area-life:#5E7440;
    --blue:#4E5F8C; --blue-2:#8C99BC; --violet:#6A4F94; --violet-2:#A492C4;
    --teal:#3D7A72; --teal-2:#7FAAA4; --green:#5E7440; --green-2:#96A87C;
    --pink:#9B4E5C; --pink-2:#C48D96; --orange:#8E5A22; --orange-2:#BE9358;
    --cat-blue:#3E5FB0; --cat-green:#4A7D42; --cat-violet:#6B4FBD;
    --cat-orange:#A65A1C; --cat-red:#A8402E; --cat-teal:#1C7A6E;
    --ink:#17150F;
    --neutral-1:#3A342A; --neutral-2:#221E16; --neutral-ink:#FDFBF7;
    /* THE ORB'S GROUND, added 6 September with the other light grounds.
       Alabaster had never defined one, so it wore the hardcoded brown the
       rule used to fall back to. Tinted to this theme's own --glass-shadow,
       so the orb casts in the same light as every card on the page. */
    --orb-halo: rgba(140,124,100,0.20);
    --orb-shadow: 0 9px 18px rgba(80,64,48,0.26);
    --orb-shadow-hi: 0 14px 26px rgba(80,64,48,0.34);
    /* THE WORK METAL — row 2141. BRONZE — the light themes take their metals OXIDISED, and that is the
       whole rule for this half of the wardrobe: a real metal is lighter than
       paper, so a bright one on cream is invisible. What a printer does
       instead is the aged version of it — bronze, old gold, tarnished silver —
       which is dark enough to read and still unmistakably metal. 6.29:1 on a
       resting card, 5.17:1 on a lit one. */
    --work-metal:#7A5326;
  }

  /* PARCHMENT & CHOCOLATE — warmer and older. Brown ink rather than black,
     which is the single thing that stops a cream page reading as a pastel. */
  :root[data-theme="parchment"]{
    --bg-mid:#FAF5EA; --bg-base:#F3ECDE; --bg-deep:#E9E0CD;
    --text:#241A10; --text-dim:#574733; --text-faint:#6B5D48;
    /* FIFTY-EIGHT FAILURES ON THE FIRST PASS, and every one of them was the
       quiet end of the ramp: section labels, feed timestamps, field hints.
       #7C6E58 computes to 4.84:1 on this theme's flat panel and MEASURED
       4.00 in the app, because the panels are not flat — the warm bloom
       sits under them and lifts the ground by about a fifth. Cream is the
       one ground where that gap matters, and it is why these two steps are
       darker than they look like they need to be. Alabaster, whose bloom is
       weaker, passed at the lighter values on the first run. */
    --ring: rgba(36,26,16,0.55); --ring-soft: rgba(36,26,16,0.16);
    --bloom-1: rgba(122,90,34,0.055); --bloom-2: rgba(140,110,70,0.035);
    --bloom-3: rgba(122,90,34,0.03); --bloom-4: rgba(90,100,60,0.025);
    --glass-bg: rgba(255,252,244,0.66);
    --glass-border: rgba(60,44,20,0.10); --glass-border-hi: rgba(60,44,20,0.19);
    --glass-shadow: 0 1px 2px rgba(90,70,40,0.08), 0 6px 14px -5px rgba(90,70,40,0.12),
                    0 22px 44px -14px rgba(90,70,40,0.20), inset 0 1px 0 rgba(255,255,255,0.9);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0) 46%);
    --glass-blur: 22px; --glass-sat: 140%;
    --brass:#7A5A22; --brass-dim:#5C4318; --brass-2:#A67F3A;
    --rail-ink:#241A10; --rail-ink-2:#63533E; --rail-ink-3:#7C6E58;
    --area-now:#4A5A83; --area-people:#93414F; --area-work:#7A5A22;
    --area-media:#33726A; --area-life:#586D3B;
    --blue:#4A5A83; --blue-2:#8894B4; --violet:#644A8C; --violet-2:#A08EBE;
    --teal:#33726A; --teal-2:#78A49D; --green:#586D3B; --green-2:#92A478;
    --pink:#93414F; --pink-2:#BE838C; --orange:#8A5418; --orange-2:#BA8D4E;
    --cat-blue:#3A5698; --cat-green:#44713C; --cat-violet:#5F47A6;
    --cat-orange:#95521A; --cat-red:#9A3A2A; --cat-teal:#1A6E62;
    --ink:#241A10;
    --neutral-1:#3A2E1E; --neutral-2:#221A10; --neutral-ink:#FAF5EA;
    /* Same addition, same reason, this theme's own shadow colour. */
    --orb-halo: rgba(150,128,92,0.20);
    --orb-shadow: 0 9px 18px rgba(90,70,40,0.26);
    --orb-shadow-hi: 0 14px 26px rgba(90,70,40,0.34);
    /* THE WORK METAL — row 2141. ANTIQUE GOLD — deeper and yellower than Alabaster’s bronze, because
       Parchment is the older, warmer paper and these two sit next to each
       other in the picker. 6.01:1 on a resting card, 4.85:1 on a lit one —
       the tightest pair in the wardrobe, and still above the 4.5:1 that is the
       floor for body text, which this is four times the size of. */
    --work-metal:#75570F;
  }

  /* ==========================================================================
     FATHOM - a deep, heavy blue, and Lucid burning ember in it.
     ==========================================================================
     Matt, 23 September, looking at an agenda tray: "a hard orange rim - not
     ember, hard orange and not what we discussed."

     He is right, and the rim is not a bug. It is doing exactly what he asked
     for the day before - "every edge that gets opened is Lucid" - so it
     refuses the theme's border token and the event kind's colour and takes
     Lucid's own. Lucid's own, on every theme in the wardrobe until now, is
     --th-base:#FF6A1E - hue 20, saturation 100, a freshly painted orange.
     That is the colour he is looking at, on all seven surfaces lucid-rim.js
     reaches plus the thread's own panel.

     So this theme is where that gets fixed, and the fix is three lines. An
     ember is a BANKED COAL: redder, deeper and less saturated than a flame.
     The three stops below are one object at three temperatures.

        --th-base  #E0522A   hue 13, saturation 68, lightness 52
        --th-mid   #EC8138   the shoulder of the coal, an amber
        --th-hot   #FFEBD4   the core, near white and warm

     Against #FF6A1E that is seven degrees of hue toward red, thirty-two
     points of saturation taken out, and four points of lightness down. On a
     blue ground it stops reading as paint and starts reading as heat.

     THOSE THREE LINES ARE THE ONLY EMBER IN THIS BLOCK, on purpose. The next
     thought is one light theme, one dark theme, and a separate choice of what
     colour Lucid is - so changing Lucid here has to be changing three values
     and nothing else. Nothing below is mixed from them and nothing below
     repeats them. --bloom-3 is the closest thing to an exception and it is
     mixed from this theme's own --orange, which is a warning colour and a
     different job.

     WHY THE ROOM IS A COLOUR RATHER THAN A DARKNESS. Midnight is already a
     blue room, at #030718 - three parts blue in a hundred, a near-black that
     photographs blue. This page is #0C1A38 falling to #050C1C: hue 218 at 13%
     lightness, heavy enough that the ember has something to be warm against.
     That one contrast is the whole theme. An ember on charcoal is a smudge.

     AND THE METAL IS CHAMPAGNE RATHER THAN BRASS, for the same reason. Every
     other dark theme wears a gold, and a gold beside an ember is two warm
     colours arguing about which one is the accent. #CDB07A is that metal with
     the heat taken out of it, so the only saturated warm thing on screen is
     Lucid - which is what makes the ember read as an ember instead of as an
     orange that happens to be there.
     ========================================================================== */
  :root[data-theme="fathom"]{
    /* LUCID, AND THE ONLY THREE LINES THAT DECIDE WHAT COLOUR THE OPENED
       EDGES OF THIS APP ARE. Swapping Lucid's colour on this theme is
       changing these three values and touching nothing else in the file. */
    --th-hot:#FFEBD4; --th-mid:#EC8138; --th-base:#E0522A;

    --bg-mid:#0C1A38; --bg-base:#08122A; --bg-deep:#050C1C;
    --text:#EAEFFA; --text-dim:#A7B4D2; --text-faint:#8D9BBC;
    --ring: rgba(198,216,255,0.45); --ring-soft: rgba(198,216,255,0.22);
    --wash-soft: rgba(226,236,255,.05); --wash-hover: rgba(226,236,255,.08);
    --wash-on: rgba(226,236,255,.055);
    /* Two lamps and no more: the room's own blue, and one warm one mixed from
       this theme's --orange rather than from the ember above. */
    --bloom-1: rgba(99,137,255,0.055); --bloom-2: rgba(169,139,255,0.030);
    --bloom-3: rgba(239,161,92,0.022); --bloom-4: rgba(168,214,92,0.018);
    --glass-bg: rgba(20,36,74,0.62);
    --glass-border: rgba(150,180,255,0.11); --glass-border-hi: rgba(160,190,255,0.40);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.6), 0 12px 26px -6px rgba(0,0,0,0.66),
                    0 40px 80px -20px rgba(0,0,0,0.85), 0 0 40px -12px rgba(30,60,140,0.20),
                    inset 0 1px 0 rgba(170,198,255,0.13);
    --glass-sheen: radial-gradient(130% 90% at 14% -12%, rgba(150,182,255,0.16),
                    rgba(150,182,255,0.042) 38%, rgba(255,255,255,0) 72%);
    --glass-blur: 26px; --glass-sat: 145%;
    --brass:#CDB07A; --brass-dim:#8F7A47; --brass-2:#EADCBC;
    --rail-ink:#F0EEE8; --rail-ink-2:#9FA3B4; --rail-ink-3:#6C7185;
    /* THE FIVE, IN A HEAVY BLUE ROOM. Now is the trap Denim's block names:
       the page is blue and Now is the blue area, so an obvious navy reads as
       "the page, darker" rather than as a colour that means something. Now is
       a lifted periwinkle at hue 227 against the ground's 218, and Media is
       turned to cyan so it cannot be mistaken for a dim Life. Work is the
       metal, as it is everywhere. */
    --area-now:#93A8E6; --area-people:#D1889A; --area-work:#CDB07A;
    --area-media:#5FB3BE; --area-life:#96BC7C;
    --blue:#6389FF; --blue-2:#9FB6FF; --blue-glow: rgba(99,137,255,0.45);
    --green:#A8D65C; --green-2:#CDE8A0; --green-glow: rgba(168,214,92,0.42);
    --violet:#A98BFF; --violet-2:#D2C4FF; --violet-glow: rgba(169,139,255,0.40);
    --orange:#EFA15C; --orange-2:#FFCB9B; --orange-glow: rgba(239,161,92,0.42);
    --pink:#FF6E5C; --pink-2:#FFA89B; --pink-glow: rgba(255,110,92,0.40);
    --teal:#3FC9BC; --teal-2:#93E6DF; --teal-glow: rgba(63,201,188,0.40);
    --cat-blue:#4C74E0; --cat-blue-2:#8AA3EE; --cat-blue-glow: rgba(76,116,224,0.45);
    --cat-green:#4BA878; --cat-green-2:#8BCFAA; --cat-green-glow: rgba(75,168,120,0.40);
    --cat-violet:#8A6FE0; --cat-violet-2:#B9A6EE; --cat-violet-glow: rgba(138,111,224,0.40);
    --cat-orange:#D98B3E; --cat-orange-2:#EDB578; --cat-orange-glow: rgba(217,139,62,0.42);
    --cat-red:#C74A3C; --cat-red-2:#E08277; --cat-red-glow: rgba(199,74,60,0.42);
    --cat-teal:#28AD9C; --cat-teal-2:#6FCEC2; --cat-teal-glow: rgba(40,173,156,0.40);
    --cat-gold:#BE9A2E; --cat-gold-2:#E2CB78; --cat-gold-glow: rgba(190,154,46,0.42);
    --cat-rose:#CE5586; --cat-rose-2:#E895B7; --cat-rose-glow: rgba(206,85,134,0.40);
    --cat-slate:#6B7D99; --cat-slate-2:#A0AFC8; --cat-slate-glow: rgba(107,125,153,0.38);
    --cat-moss:#7C9D42; --cat-moss-2:#B0CB80; --cat-moss-glow: rgba(124,157,66,0.40);
    /* THE ACCENT AND THE PULSE, STATED RATHER THAN INHERITED. :root aliases
       these onto --blue, and this block restates them so nothing about this
       theme depends on reading another block to know what it wears. */
    --accent: var(--blue); --accent-2: var(--blue-2); --accent-glow: var(--blue-glow);
    --pulse-lo: color-mix(in srgb, var(--blue) 25%, transparent);
    --pulse-hi: color-mix(in srgb, var(--blue) 48%, transparent);
    --brass-glow: rgba(205,176,122,0.40);
    /* The ink that goes ON an accent fill. Regal is the only other theme
       that states it, because its accent is a bright brass and needs a dark
       ink; this accent is a blue, so it takes the page own light ink. */
    --on-accent:#EAEFFA;
    --ink:#040A18;
    --neutral-1:#E6ECFB; --neutral-2:#ffffff; --neutral-ink:#08122A;
    --neutral-glow: rgba(210,225,255,.2);
    --orb-halo: rgba(120,150,220,0.24);
    --orb-shadow: 0 9px 18px rgba(0,0,0,0.42);
    --orb-shadow-hi: 0 14px 26px rgba(0,0,0,0.50);
  }

  /* ==========================================================================
     THE MATTE FINISH — one attribute, and eighty-five glows go out.
     ==========================================================================
     Placed AFTER the six theme blocks deliberately. :root[data-finish] and
     :root[data-theme] have identical specificity, so the later block wins the
     tie, and that is what lets a theme set its own colours above while the
     finish takes the halo off them here.

     STEP ONE, AND IT IS MOST OF THE WORK: every --*-glow token goes
     transparent. Eighty of the eighty-five glow declarations in this file are
     of the form `box-shadow: 0 0 Npx var(--something-glow)`, and a transparent
     colour makes each of them paint nothing at all. No selector had to be
     found, and none can be missed. It also survives applyAccentColor(), which
     writes `--accent-glow: var(--blue-glow)` as an inline style on the root
     element: the inline value wins, points at --blue-glow, and --blue-glow is
     transparent here. Nothing has to know about anything else.

     --cat-red-glow is the exception and it is the point of the whole exercise:
     it is what the overdue alarm is built from, and it stays lit. */
  :root[data-finish="matte"]{
    --accent-glow: transparent;
    --blue-glow: transparent;   --green-glow: transparent;
    --violet-glow: transparent; --orange-glow: transparent;
    --pink-glow: transparent;   --teal-glow: transparent;
    --brass-glow: transparent;  --neutral-glow: transparent;
    --cat-blue-glow: transparent;  --cat-green-glow: transparent;
    --cat-violet-glow: transparent; --cat-orange-glow: transparent;
    --cat-teal-glow: transparent;  --cat-gold-glow: transparent;
    --cat-rose-glow: transparent;  --cat-slate-glow: transparent;
    --cat-moss-glow: transparent;
  }

  /* STEP TWO: THE FIVE THAT HARD-CODE THEIR OWN HALO and therefore cannot be
     switched off by a token. Each is named, and each is replaced rather than
     merely removed — a control that loses its only "I am on" signal is worse
     than one that glows. */
  :root[data-finish="matte"] .pill.active{
    background:color-mix(in srgb, var(--accent) 16%, transparent);
    box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent); }
  :root[data-finish="matte"] .ht-breathe-dot,
  :root[data-finish="matte"] .em-toggle-pill.active,
  :root[data-finish="matte"] .tlf-chip.active{ box-shadow:none; }
  /* The assistant pill keeps a cast shadow and loses the violet bloom around
     it. It is a floating object; it still needs to look like one. */
  /* The doubled attribute is not a typo. :root[data-mode="light"] .chat-pill and
     the five :root[data-tone=…] .chat-pill rules both sit LATER in this file at
     the same specificity, so a single-attribute selector here loses the tie to
     every one of them and the violet bloom survives on the two paper themes.
     Doubling data-finish buys one more attribute of specificity, which is the
     smallest change that wins without reaching for !important. */
  :root[data-finish="matte"][data-finish="matte"] .chat-pill{
    filter:drop-shadow(0 10px 26px rgba(0,0,0,.5)); }
  /* AND THE MATTE PAPERS READ THE SAME TOKEN, WHICH IS HOW THE FIRST FIX FOR
     THIS MISSED THEM. 6 September. The note by :root[data-mode="light"]
     .chat-pill explains the dead --orb-shadow-1; pointing that rule at the
     theme's real token fixed Light, Alabaster's glow twin and the three
     Serenity papers, and DID NOTHING AT ALL for Denim and Terracotta — the two
     coloured papers, which are the whole reason the shadow's hue matters. This
     rule carries one more attribute than that one and quietly won the cascade,
     so the orb on a blue page went on sitting in the same hardcoded brown.

     Found by breaking the harness on purpose: with a deliberately invalid
     --orb-shadow on Terracotta the orb still had a shadow, which it could not
     have had if that token were the one painting it.

     The matte finish takes the GLOW off the orb; it was never supposed to have
     an opinion about the cast shadow, which is the same physical fact on both
     finishes. So it defers to the same two tokens and the difference between
     matte and glow stays what it says it is. */
  :root[data-finish="matte"][data-finish="matte"][data-mode="light"] .chat-pill{
    filter:drop-shadow(var(--orb-shadow)); }
  :root[data-finish="matte"][data-finish="matte"] .chat-pill:hover{
    filter:drop-shadow(0 14px 32px rgba(0,0,0,.58)); }
  :root[data-finish="matte"][data-finish="matte"][data-mode="light"] .chat-pill:hover{
    filter:drop-shadow(var(--orb-shadow-hi)); }

  /* STEP THREE: FOCUS RINGS, WHICH WOULD OTHERWISE HAVE GONE INVISIBLE.
     Every one of these was `0 0 0 3px var(--accent-glow)` — a blurred halo
     doing the job of a focus indicator. Turning the token transparent would
     have silently removed the only mark saying which field has the keyboard,
     which is not a style regression, it is an accessibility one. A solid 2px
     ring is the same statement, sharper, and it survives every ground here. */
  :root[data-finish="matte"] .mail-search-input:focus,
  :root[data-finish="matte"] .field:focus,
  :root[data-finish="matte"] .login-input:focus,
  :root[data-finish="matte"] .chat-input:focus{
    box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent) !important;
    border-color:var(--accent); }

  /* STEP FOUR: WHAT REPLACES THE GLOW AS A SIGNAL.
     ------------------------------------------------------------------
     A CONTACT ROW'S CATEGORY was said entirely by a coloured wash bled in from
     the left edge, built out of --row-glow — so a matte theme would have taken
     the category off the contacts page altogether. It becomes a 3px keyline in
     the same colour, which is the treatment a mail row has always used for its
     account and is the more legible of the two anyway: an edge has a position,
     a bloom only has a direction. renderContacts() and the organization rows
     now write --row-color alongside --row-glow so there is a solid colour here
     to draw with; see the note at those two lines in app.js. */
  :root[data-finish="matte"] .contact-row{
    background-image:none;
    border-left:3px solid var(--row-color, transparent); }

  /* THE INNER CIRCLE keeps its halo's meaning and loses its haze: two rings,
     the outer one a tint, instead of a 16px bloom that on a near-black ground
     was mostly invisible and on cream was a smudge. */
  :root[data-finish="matte"] .avatar.has-halo{
    box-shadow:0 0 0 2px var(--accent),
               0 0 0 4px color-mix(in srgb, var(--accent) 28%, transparent);
    animation:none; }

  /* ELEVATION INSTEAD OF EMISSION. A card that was glowing to say "I am on top
     of the page" now says it with a longer shadow — the thing that is actually
     true of an object above a surface. */
  :root[data-finish="matte"] .btn.primary,
  :root[data-finish="matte"] .project-card:hover,
  :root[data-finish="matte"] .task-card:hover,
  :root[data-finish="matte"] .mail-row:hover{
    box-shadow:var(--elev-raised); }

  /* THE SIGNATURE TAB, WHICH NOTHING COULD READ IN EITHER DIRECTION.
     .sig-tab.active paints its label on color-mix(--accent 78%, #000). On the
     original themes --accent is a bright blue and that mix is dark enough for
     white; on Forest and Ink the accent is already a muted mid-tone, so the
     mix lands in the middle of the range where NEITHER white nor near-black
     reaches 4.5:1 — white measured 4.03 on Forest and dark measured 4.37 on
     Ink. There is no ink that fixes a fill like that; the fill is the bug.
     Taking the mix down to 52% black makes it properly dark, and white on it
     clears 6:1 on all six grounds. Found by tests/test-readability.js, which
     now runs every one of them. */
  :root[data-finish="matte"] .sig-tab.active{
    background:color-mix(in srgb, var(--accent) 52%, #000);
    border-color:color-mix(in srgb, var(--accent) 52%, #000);
    color:#fff; }

  /* AND THE ONE THAT KEEPS ITS GLOW. Stated here rather than left implicit,
     because the next person to read this block will otherwise assume the
     overdue badge was missed. It is the only emitting object left in a matte
     theme, and that is precisely why it works. */
  :root[data-finish="matte"] .spine-count.has-overdue,
  :root[data-finish="matte"] .spine-attn-collapsed.has-overdue svg{ /* left lit on purpose */ }

  /* ============================================================
     SERENE — the light themes.
     ============================================================

     The brief was "pastels, light creamy goodness", and the trap it named was
     the obvious approach: invert Midnight's tokens and ship it. That produces
     what every app's light mode looks like — grey-white, cold, clinical. It
     would earn the name Serene about as well as a hospital corridor does.

     Midnight works because it has a language, not just a darkness: cards the
     same value as the room, depth from a lit top edge and a contained pool of
     light in one corner. Serene needs its own language rather than a negation
     of that one, and it is this:

       WARMTH INSTEAD OF WHITE.  Every surface sits on the warm side of
       neutral. Nothing here is #fff. Warmth is most of what separates
       "serene" from "blank", and it is the first thing lost by inverting.

       SHADOW INSTEAD OF GLOW.  Midnight's contained glow pools have no light
       equivalent — light does not pool on paper. The counterpart is a soft,
       warm, low-contrast cast shadow: cards as paper resting on paper. The
       shadows are TINTED, never grey. A neutral shadow on a warm ground reads
       as dirt.

       PASTELS CARRY THE HUE, NOT THE CONTRAST.  This is the part that goes
       wrong. Pastel-on-cream is where accessibility dies, so the pastel lives
       in the -2 and -glow tokens — gradients, halos, fills behind dark text —
       while the base token stays saturated enough to be read as text on
       cream. Every base below clears 4.5:1 against its own background;
       tests/test-serene-contrast.js fails the build if one stops.

     THREE OF THEM, because the brief asked for interpretations to compare
     rather than one "correct" answer, and because "serene" is a feeling that
     could reasonably mean any of these three. Each block is deliberately
     self-contained — some duplication, but each can be adjusted without
     working out what it inherits.
  */

  /* ------------------------------------------------------------------
     TWO AXES, NOT THREE THEMES.

     Serene shipped as three fixed themes. Matt's reaction was the useful
     kind: "the first one is the base one, but I also really like the third
     one in green" — which is not indecision, it is two separate preferences
     that had been welded together.

     They are separate things. The GROUND is warmth, texture and the colour
     of the shadow a card casts. The PALETTE is which six hues the accounts
     and categories draw from. Nothing makes them one choice except that they
     were written as one block.

     So they are now two attributes, and any of the nine combinations works.
     The three original themes survive as the three matched pairs — they are
     what the presets select — and Linen paper with the Cool palette, which
     is the one he actually asked for, is simply another of the nine.

     Every combination is checked: tests/test-serene-contrast.js walks the
     whole 3x3 grid rather than the three presets, because a palette that
     passes on its own ground and fails on another would be a failure nobody
     would find until it was picked.
     ------------------------------------------------------------------ */

  /* LINEN — unbleached paper. The quietest ground: warm ivory, and shadows
     tinted toward its own brown so a card reads as paper resting on paper. */
  :root[data-paper="linen"]{
    --bg-mid: #FAF6EC;
    --bg-base: #F4EEE0;
    --bg-deep: #ECE4D2;
    --bloom-1: rgba(126,146,181,0.10);
    --bloom-2: rgba(168,146,186,0.09);
    --bloom-3: rgba(214,166,116,0.10);
    --bloom-4: rgba(150,175,138,0.09);
    --glass-bg: rgba(255,253,248,0.78);
    --glass-border: rgba(74,58,40,0.10);
    --glass-border-hi: rgba(74,58,40,0.18);
    --glass-shadow: 0 1px 2px rgba(92,72,48,0.07), 0 4px 10px -3px rgba(92,72,48,0.09), 0 18px 36px -12px rgba(92,72,48,0.14), inset 0 1px 0 rgba(255,255,255,0.85);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0) 52%);
    --glass-blur: 18px;
    --glass-sat: 128%;
    --text: #2E2620;
  /* THE PAPERS GOT DARKER SECONDARY INK, 9 OCTOBER, AND THE FLOOR IS WHY.

     --text-dim and --text-faint were chosen on these three papers to a
     COMPUTED ratio, and computed they were fine: linen --text-dim measured
     5.37:1 on its own ground. In PIXELS at 11px they were not. The darkest
     sixteenth of a crop through an 11px glyph never reaches the pure colour,
     so tests/test-readability.js -- which photographs the page rather than
     reading this file -- was getting 4.32:1 out of .ask-allow-text, 4.25 to
     4.42 out of .agenda-meta-txt, 4.39 to 4.49 out of .lane-fig and 4.50 out
     of .work-row-kind. All of them are this token, all of them are small, and
     all of them were under the 4.5 floor on the ground Matt reads on.

     Matt, 9 October, asked whether to darken the Serene text and said yes to
     all of it rather than to the worst of it. So both tokens move on all
     three papers, toward each paper own ink rather than toward grey: linen
     --text-dim goes 6.78:1 computed where it was 5.37, mist 6.70 where it was
     5.18, blush 6.80 where it was 5.17, and --text-faint lands at 6.32, 6.53
     and 6.52. That is roughly 5.3:1 once the instrument has had its say,
     which clears the floor with room rather than sitting on it.

     NOTHING DARK IS TOUCHED. These are the three paper blocks; hearth, ink
     and chocolate declare their own --text-dim and --text-faint elsewhere and
     are not reachable from here. Raising contrast cannot fail a check that
     asserts a minimum, and tests/test-serene-contrast.js asserts minimums. */
    --text-dim: #5A4F42;   /* was #6B5F52 -- see THE PAPERS GOT DARKER SECONDARY INK above */
    --text-faint: #5E554A;  /* was #6F6559 */
    --ring: rgba(46,38,32,0.55);
    --ring-soft: rgba(46,38,32,0.14);
    --ink: #2E2620;
    --neutral-1: #4A3F36;
    --neutral-2: #2E2620;
    --neutral-ink: #FAF6EC;
    --neutral-glow: rgba(74,63,54,.14);
    --orb-halo: rgba(150,130,110,0.20);
    --orb-shadow: 0 9px 18px rgba(92,72,48,0.26);
    --orb-shadow-hi: 0 14px 26px rgba(92,72,48,0.34);
    /* THE WORK METAL — row 2141. TARNISHED SILVER, turned to this paper:
       warm grey. Serenity is the cool, cerebral face of the app and a gold
       count on it would be the warm, traditional register the whole family is
       written against. The paper carries it rather than the palette, because
       what the metal has to survive is the GROUND, and the ground is the
       paper. 6.77:1 on a resting card, 5.51:1 on a lit one. */
    --work-metal:#5A564F;
  }

  /* BLUSH — the warmest ground, and the closest to the words "creamy
     goodness". Off ivory toward peach, with the shadow tinted further into
     red so it stays part of the same page rather than sitting in grey. */
  :root[data-paper="blush"]{
    --bg-mid: #FDF5EF;
    --bg-base: #F9EBE1;
    --bg-deep: #F2DFD2;
    --bloom-1: rgba(198,156,146,0.12);
    --bloom-2: rgba(186,152,178,0.10);
    --bloom-3: rgba(232,176,128,0.13);
    --bloom-4: rgba(158,182,150,0.09);
    --glass-bg: rgba(255,252,249,0.80);
    --glass-border: rgba(96,58,44,0.09);
    --glass-border-hi: rgba(96,58,44,0.17);
    --glass-shadow: 0 1px 2px rgba(122,74,54,0.07), 0 4px 10px -3px rgba(122,74,54,0.10), 0 18px 38px -12px rgba(122,74,54,0.15), inset 0 1px 0 rgba(255,255,255,0.88);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.60), rgba(255,255,255,0) 52%);
    --glass-blur: 20px;
    --glass-sat: 136%;
    --text: #33241E;
    --text-dim: #614D42;   /* was #755E52 */
    --text-faint: #605146;  /* was #736255 */
    --ring: rgba(51,36,30,0.55);
    --ring-soft: rgba(51,36,30,0.14);
    --ink: #33241E;
    --neutral-1: #54403A;
    --neutral-2: #33241E;
    --neutral-ink: #FDF5EF;
    --neutral-glow: rgba(84,64,58,.14);
    --orb-halo: rgba(190,140,120,0.22);
    --orb-shadow: 0 9px 18px rgba(122,74,54,0.28);
    --orb-shadow-hi: 0 14px 26px rgba(122,74,54,0.36);
    /* THE WORK METAL — row 2141. TARNISHED SILVER, turned to this paper:
       a red-grey. Serenity is the cool, cerebral face of the app and a gold
       count on it would be the warm, traditional register the whole family is
       written against. The paper carries it rather than the palette, because
       what the metal has to survive is the GROUND, and the ground is the
       paper. 6.84:1 on a resting card, 5.54:1 on a lit one. */
    --work-metal:#62534D;
  }

  /* MIST — the cool argument. Pale sage and stone rather than cream: the
     calm of an early morning room rather than of paper. Included because the
     brief assumed warmth, and that was worth testing rather than agreeing
     with — and it is the one Matt picked the green out of. */
  :root[data-paper="mist"]{
    --bg-mid: #F6F7F2;
    --bg-base: #EDF0E9;
    --bg-deep: #E2E7DD;
    --bloom-1: rgba(130,158,178,0.12);
    --bloom-2: rgba(158,150,186,0.09);
    --bloom-3: rgba(206,180,140,0.07);
    --bloom-4: rgba(146,178,146,0.12);
    --glass-bg: rgba(253,254,251,0.80);
    --glass-border: rgba(42,58,48,0.10);
    --glass-border-hi: rgba(42,58,48,0.18);
    --glass-shadow: 0 1px 2px rgba(52,72,60,0.07), 0 4px 10px -3px rgba(52,72,60,0.09), 0 18px 36px -12px rgba(52,72,60,0.14), inset 0 1px 0 rgba(255,255,255,0.90);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.58), rgba(255,255,255,0) 52%);
    --glass-blur: 20px;
    --glass-sat: 124%;
    --text: #232B26;
    --text-dim: #4C5550;   /* was #5C6660 */
    --text-faint: #505652;  /* was #616762 */
    --ring: rgba(35,43,38,0.55);
    --ring-soft: rgba(35,43,38,0.14);
    --ink: #232B26;
    --neutral-1: #3C4740;
    --neutral-2: #232B26;
    --neutral-ink: #F6F7F2;
    --neutral-glow: rgba(60,71,64,.14);
    --orb-halo: rgba(120,145,140,0.20);
    --orb-shadow: 0 9px 18px rgba(52,72,60,0.26);
    --orb-shadow-hi: 0 14px 26px rgba(52,72,60,0.34);
    /* THE WORK METAL — row 2141. TARNISHED SILVER, turned to this paper:
       a green-grey. Serenity is the cool, cerebral face of the app and a gold
       count on it would be the warm, traditional register the whole family is
       written against. The paper carries it rather than the palette, because
       what the metal has to survive is the GROUND, and the ground is the
       paper. 6.83:1 on a resting card, 5.51:1 on a lit one. */
    --work-metal:#4E5953;
  }

  /* ------------------------------------------------------------------
     WHICH AXIS OWNS THE FIVE AREA COLOURS — and why it is this one.

     Serene is a paper crossed with a palette, so "give Serene its own area
     colours" has three possible answers: one set for all nine combinations,
     one per paper, or one per palette. It is one per palette, and the reason
     is the split the TWO AXES note above already draws. The GROUND is warmth
     and texture and the colour of a shadow; the PALETTE is which hues this
     copy of the app is drawn in. An area colour is a hue, so it belongs to
     the palette — and the three papers are within four points of luminance of
     each other, which means a set that holds on one holds on all three. Each
     of these three blocks is measured against the deepest ground of all THREE
     papers, not just the one it came from, for exactly the reason the harness
     walks the grid rather than the presets.

     The five are drawn from the palette's own six hues — Now from its blue,
     People from its pink, Work from its orange, Media from its teal, Life
     from its green — and then taken one step down. The step is not decoration:
     those six are measured as text on a CARD, and every one of them clears
     4.5:1 there. The rail is not on a card. It runs the height of the window,
     over the page gradient, and at the bottom of that gradient the palette's
     own blue measures 4.35:1 and its orange 3.96:1. Nine tenths of the
     lightness, applied to all five so their relative weights survive, puts the
     whole set over 4.5:1 on the deepest ground of any of the three papers.

     THE METAL FOLLOWS WORK, as everywhere else in this file. Serene had no
     metal of its own at all — all nine combinations inherited #8E7639 from the
     cream fallback above, which meant picking the Cool palette changed six
     accents and left the masthead rule and every "this is yours" count sitting
     in the Warm palette's gold.
     ------------------------------------------------------------------ */

  /* SOFT — chalky and dusty, like gouache on a warm ground. Pigment rather
     than ink. Pairs most naturally with Linen, which is where it came from. */
  :root[data-palette="soft"]{
    --blue: #4A6B93;
    --blue-2: #AFC4DC;
    --blue-glow: rgba(175,196,220,0.42);
    --green: #537648;
    --green-2: #B4CDA6;
    --green-glow: rgba(180,205,166,0.42);
    --violet: #6E5A96;
    --violet-2: #C3B4DC;
    --violet-glow: rgba(195,180,220,0.40);
    --orange: #99612D;
    --orange-2: #E5C29A;
    --orange-glow: rgba(229,194,154,0.44);
    --pink: #A85462;
    --pink-2: #E0B4BC;
    --pink-glow: rgba(224,180,188,0.40);
    --teal: #3A7973;
    --teal-2: #A5CBC6;
    --teal-glow: rgba(165,203,198,0.42);
    --cat-blue: #4A6B93;
    --cat-blue-2: #AFC4DC;
    --cat-blue-glow: rgba(175,196,220,0.36);
    --cat-green: #537648;
    --cat-green-2: #B4CDA6;
    --cat-green-glow: rgba(180,205,166,0.36);
    --cat-violet: #6E5A96;
    --cat-violet-2: #C3B4DC;
    --cat-violet-glow: rgba(195,180,220,0.34);
    --cat-orange: #99612D;
    --cat-orange-2: #E5C29A;
    --cat-orange-glow: rgba(229,194,154,0.36);
    --cat-red: #A14A3E;
    --cat-red-2: #DDB0A6;
    --cat-red-glow: rgba(221,176,166,0.34);
    --cat-teal: #3A7973;
    --cat-teal-2: #A5CBC6;
    --cat-teal-glow: rgba(165,203,198,0.36);
    --pulse-lo: rgba(175,196,220,0.26);
    --pulse-hi: rgba(175,196,220,0.46);
    /* The five, chalked down for the rail. Media is the one that is not simply
       this palette's teal darkened: #3A7973 sits close enough to the green
       that the two read as one dusty pigment at dot size, and the whole point
       of a chalky palette is that it is already low in chroma — there is no
       saturation left to tell them apart with. #316E6E is the same teal turned
       a few degrees toward cyan, which takes the closest pair from ΔE 21.9 to
       26.2 without making either colour louder. */
    --area-now: #436084;
    --area-people: #974C58;
    --area-work: #7C5F2E;
    --area-media: #316E6E;
    --area-life: #4F6B3E;
    --brass: #7C5F2E;
    --brass-dim: #5D4723;
  }

  /* WARM — the same hues pushed toward apricot, rose and clay. The two cool
     hues are held further from the rest here than in the other palettes,
     because a warm palette is the one at risk of collapsing into one blur. */
  :root[data-palette="warm"]{
    --blue: #4F6A96;
    --blue-2: #B6C7E0;
    --blue-glow: rgba(182,199,224,0.44);
    --green: #55764C;
    --green-2: #B8CDAC;
    --green-glow: rgba(184,205,172,0.42);
    --violet: #7A5A93;
    --violet-2: #CFB6DC;
    --violet-glow: rgba(207,182,220,0.42);
    --orange: #A55B21;
    --orange-2: #F0C199;
    --orange-glow: rgba(240,193,153,0.48);
    --pink: #B04A57;
    --pink-2: #EFB0B7;
    --pink-glow: rgba(239,176,183,0.44);
    --teal: #377872;
    --teal-2: #A8CDC8;
    --teal-glow: rgba(168,205,200,0.42);
    --cat-blue: #4F6A96;
    --cat-blue-2: #B6C7E0;
    --cat-blue-glow: rgba(182,199,224,0.38);
    --cat-green: #55764C;
    --cat-green-2: #B8CDAC;
    --cat-green-glow: rgba(184,205,172,0.36);
    --cat-violet: #7A5A93;
    --cat-violet-2: #CFB6DC;
    --cat-violet-glow: rgba(207,182,220,0.36);
    --cat-orange: #A55B21;
    --cat-orange-2: #F0C199;
    --cat-orange-glow: rgba(240,193,153,0.40);
    --cat-red: #A8453F;
    --cat-red-2: #E8ADA6;
    --cat-red-glow: rgba(232,173,166,0.36);
    --cat-teal: #377872;
    --cat-teal-2: #A8CDC8;
    --cat-teal-glow: rgba(168,205,200,0.36);
    --pulse-lo: rgba(240,193,153,0.28);
    --pulse-hi: rgba(240,193,153,0.48);
    /* The five for the warm palette, and this is the one the block above warns
       about: "a warm palette is the one at risk of collapsing into one blur."
       Three of the five — People, Work, Life — are already on the warm side,
       so Media is held further out here than in the other two palettes
       (#2C6B6B rather than a darkened #377872) and Life is allowed a little
       more yellow. That is the same move the accents make for the same
       reason, and it buys the widest closest-pair of the three palettes. */
    --area-now: #465E85;
    --area-people: #9C414D;
    --area-work: #855C26;
    --area-media: #2C6B6B;
    --area-life: #516A3E;
    --brass: #855C26;
    --brass-dim: #63451C;
  }

  /* COOL — sage, slate and deep teal. This is the green Matt picked out of
     Mist, and it is now available on any of the three papers. */
  :root[data-palette="cool"]{
    --blue: #41688C;
    --blue-2: #A8C3DB;
    --blue-glow: rgba(168,195,219,0.42);
    --green: #4F7A46;
    --green-2: #A9C8A0;
    --green-glow: rgba(169,200,160,0.42);
    --violet: #65588F;
    --violet-2: #BAB2D8;
    --violet-glow: rgba(186,178,216,0.40);
    --orange: #98642F;
    --orange-2: #DDBE96;
    --orange-glow: rgba(221,190,150,0.42);
    --pink: #9E5060;
    --pink-2: #D9AFB8;
    --pink-glow: rgba(217,175,184,0.40);
    --teal: #2E7971;
    --teal-2: #9AC8C2;
    --teal-glow: rgba(154,200,194,0.42);
    --cat-blue: #41688C;
    --cat-blue-2: #A8C3DB;
    --cat-blue-glow: rgba(168,195,219,0.36);
    --cat-green: #4F7A46;
    --cat-green-2: #A9C8A0;
    --cat-green-glow: rgba(169,200,160,0.36);
    --cat-violet: #65588F;
    --cat-violet-2: #BAB2D8;
    --cat-violet-glow: rgba(186,178,216,0.34);
    --cat-orange: #98642F;
    --cat-orange-2: #DDBE96;
    --cat-orange-glow: rgba(221,190,150,0.36);
    --cat-red: #96453C;
    --cat-red-2: #D2A69E;
    --cat-red-glow: rgba(210,166,158,0.34);
    --cat-teal: #2E7971;
    --cat-teal-2: #9AC8C2;
    --cat-teal-glow: rgba(154,200,194,0.36);
    --pulse-lo: rgba(168,195,219,0.26);
    --pulse-hi: rgba(168,195,219,0.46);
    /* The five for the cool palette. The metal is the interesting one: a
       tobacco gold in a room of sage and slate looks like it wandered in from
       Parchment, so #715F2F is an olive gold — the same step down from this
       palette's own #98642F, which was already the coolest of the three
       oranges. Work and the masthead rule are that colour, which is what
       makes choosing Cool change the metal as well as the accents. */
    --area-now: #3A5E7E;
    --area-people: #8E4856;
    --area-work: #715F2F;
    --area-media: #296D66;
    --area-life: #476E3F;
    --brass: #715F2F;
    --brass-dim: #554723;
  }

  /* Swatch colours. Deliberately literal rather than var(--bg-mid): a swatch
     has to show the paper you are NOT currently using, so it cannot read from
     the tokens that are currently applied. These are the only duplicated
     values in the theme system, and test-serene-contrast.js checks they still
     match the blocks they represent. */
  [data-sw-paper="linen"]{ background:linear-gradient(135deg,#FAF6EC,#ECE4D2); }
  [data-sw-paper="blush"]{ background:linear-gradient(135deg,#FDF5EF,#F2DFD2); }
  [data-sw-paper="mist"] { background:linear-gradient(135deg,#F6F7F2,#E2E7DD); }
  [data-sw-palette="soft"]{ background:linear-gradient(100deg,#4A6B93 0 33%,#537648 33% 66%,#99612D 66%); }
  [data-sw-palette="warm"]{ background:linear-gradient(100deg,#4F6A96 0 33%,#B04A57 33% 66%,#A55B21 66%); }
  [data-sw-palette="cool"]{ background:linear-gradient(100deg,#41688C 0 33%,#4F7A46 33% 66%,#2E7971 66%); }

  /* ---- LITE: the same app, without the compositor melting -------------
     Only three things change, and none of them is layout or colour:
       · backdrop blurs become flat translucent fills
       · the orb's halo stops breathing
       · continuous decorative animation stops
     Everything keeps its position, its size and its palette, so switching
     between the two is close to invisible in a still frame. */
  /* A universal selector on purpose. The first version listed the six classes
     that were known to use a backdrop blur, and sixteen elements still had one
     — a panel style here, a `style="backdrop-filter:…"` on a floating button
     there. In a mode whose entire job is "draw nothing expensive", a list that
     can be incomplete is the wrong shape. This one cannot miss, it beats
     inline styles, and it costs nothing at all in the normal mode because the
     attribute is not there. */
  :root[data-fx="lite"] *{
    backdrop-filter:none !important; -webkit-backdrop-filter:none !important;
  }
  /* The blur was doing two jobs: softening what is behind the panel, and
     making the panel read as a distinct surface. Losing the first is fine;
     losing the second is not, so the fill is made more opaque to compensate —
     which is what keeps the two modes looking like the same app. */
  :root[data-fx="lite"] .glass{
    /* The sheen layer is repeated here because this rule replaces the whole
       `background` shorthand — dropping it would make cheap mode a visibly
       flatter panel rather than the same panel drawn cheaply. */
    background:var(--glass-sheen), color-mix(in srgb, var(--glass-bg) 100%, var(--bg-base) 55%);
  }
  :root[data-fx="lite"] .modal-backdrop{ background:color-mix(in srgb, var(--bg-deep) 78%, transparent); }
  /* Anything decorative that loops for ever. Deliberately NOT a blanket
     `animation:none` — the view cross-fade, toasts and the send whoosh are
     feedback, and an app whose transitions vanish reads as broken rather than
     as fast. */
  :root[data-fx="lite"] [class*="breathe"]{ animation:none !important; }

  /* Someone who has asked their system for less motion gets lite as the
     starting point, before any measurement happens. */
  @media (prefers-reduced-motion: reduce){
    :root:not([data-fx="full"]) .glass{ backdrop-filter:none; -webkit-backdrop-filter:none; }
  }

  *{ box-sizing:border-box; }
  html,body{ height:100%; margin:0; }

  /* WHY THIS IS data-mode AND NOT data-theme.
     Every one of these guards used to read :not([data-theme="light"]) — one
     theme name, hard-coded, thirty times. Adding a second light theme would
     have handed it the whole dark-mode glow treatment: contained glow pools on
     cream, which is not a design choice, it is a smudge.
     setTheme now stamps data-mode="light" or "dark" alongside the name, so
     "is this a light theme" is a question with one answer in one place. */
  /* THE RAIL LEFT THIS GROUP ON 2 SEPTEMBER, and it is the one row of the glow
     treatment that is now wrong rather than merely different.
     A 1px ring plus a 22px halo around the current nav row is three statements
     of one fact — the wash, the ring, the glow — and it is what made the spine
     read as a demo beside pages that had already been quietened. The current
     row is now a 2px bar in its area's colour; see .nav-item.active further
     down this file for the argument. Deleted here rather than overridden
     there, because this selector carries :root and would win. */
  :root:not([data-mode="light"]) .mail-row{ box-shadow:0 0 14px 0 var(--row-glow, transparent); }
  :root:not([data-mode="light"]) .mail-row:hover{ box-shadow:0 0 18px 1px var(--row-glow, var(--accent-glow)); }
  :root:not([data-mode="light"]) .mail-row.selected{
    box-shadow:0 0 0 1px color-mix(in srgb, var(--row-color, var(--accent)) 35%, transparent), 0 0 28px 4px var(--row-glow, var(--accent-glow));
  }
  
  :root:not([data-mode="light"]) .list-check.done{ box-shadow:0 0 12px 2px var(--green-glow); }
  :root:not([data-mode="light"]) .contact-row{
    background-image: linear-gradient(90deg, color-mix(in srgb, var(--row-glow, transparent) 65%, transparent) 0%, transparent 45%);
    box-shadow:
      0 1px 2px rgba(0,0,0,.35),
      0 10px 20px -6px rgba(0,0,0,.5),
      0 28px 56px -18px rgba(0,0,0,.55),
      0 0 26px -8px var(--row-glow, transparent);
  }
  :root:not([data-mode="light"]) .contact-row:hover{
    transform:translateY(-3px);
    background-image: linear-gradient(90deg, color-mix(in srgb, var(--row-glow, transparent) 80%, transparent) 0%, transparent 50%);
    box-shadow:
      0 2px 4px rgba(0,0,0,.4),
      0 14px 28px -6px rgba(0,0,0,.55),
      0 36px 72px -20px rgba(0,0,0,.6),
      0 0 38px -4px var(--row-glow, transparent);
  }
  :root:not([data-mode="light"]) .rel-strong{ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--cat-green) 35%, transparent), 0 0 16px 0 var(--cat-green-glow); }
  :root:not([data-mode="light"]) .rel-stable{ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--cat-violet) 30%, transparent), 0 0 16px 0 var(--cat-violet-glow); }
  :root:not([data-mode="light"]) .rel-cooling, :root:not([data-mode="light"]) .rel-stale{ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--cat-blue) 40%, transparent), 0 0 16px 0 var(--cat-blue-glow); }

  body{
    position:relative;
    color:var(--text); font-family:var(--font-body); -webkit-font-smoothing:antialiased; overflow:hidden;
    background:
      radial-gradient(900px 620px at 12% -8%, var(--bloom-1), transparent 60%),
      radial-gradient(1000px 760px at 108% 18%, var(--bloom-2), transparent 58%),
      radial-gradient(760px 620px at 90% 90%, var(--bloom-3), transparent 55%),
      radial-gradient(760px 620px at 10% 118%, var(--bloom-4), transparent 60%),
      linear-gradient(180deg, var(--bg-mid), var(--bg-base) 55%, var(--bg-deep));
    transition:background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
  }
  /* Subtle film-grain over the bloom gradients — without it, large soft radial
     glows read as flat/plasticky (banding, "generic AI dashboard" look). A
     faint noise layer is what most premium dark UIs use to make a gradient
     feel like it has material, not just color. Kept very low-opacity so it
     reads as texture, not visible speckle. */
  body::before{
    content:''; position:fixed; inset:0; z-index:0; pointer-events:none;
    opacity:.035; mix-blend-mode:overlay;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  }
  .app{ position:relative; z-index:1; }
  /* ---- THE EVENT BANNER (row 1922, cut to a third on 22 September) -------

     Matt, 22 September: "Popup reminder is really cool with the countdown.
     But when I X out, it keeps coming back. Need a way to dismiss
     permenatly. Also it is way too big and bulky. Needs to shrink to 1/3rd
     the current length and hight."

     It was a 640 x 97 card and it carried nine things: a 32px countdown, the
     title, the start and end times, which call it is, four faces, the names
     under them, a line about anything clashing, a line about the weather, and
     two buttons. Every one of them was true. None of them was the question at
     09:56, which is "what is it, how long have I got, and can I get rid of
     this" — and the price of the other six was a card a third of the way up
     the screen.

     212 x 32 holds the countdown he likes, which thing it is, one thing to
     press and the X. What is gone is not lost: the hours, the room and the
     people are on the event itself, one press away on the same card; the coat
     sentence is still above the agenda where it was written first, and it is
     still here as the forecast's own icon with the sentence on it; and a
     second thing on the same minute is still said, as +1 rather than as a
     sentence.

     Still a banner and never a modal, still bottom-centre, still built from
     app/tokens.css, and the rail still carries the calendar's own colour. */
  .meeting-banner{
    position:fixed; bottom:var(--space-8); left:50%; z-index:998;
    transform:translateX(-50%) translateY(140%); opacity:0; pointer-events:none;
    transition:transform var(--dur-base) var(--ease-out),
               opacity var(--dur-base) var(--ease-out);
    /* A third of 640, rounded down to the 4px grid. Narrower than the old card
       was tall. */
    width:min(212px, calc(100vw - var(--space-10)));
    /* The event's own colour, written onto the element by showMeetingBanner()
       from the calendar the event lives on. The rail, the pip and the fuse all
       read it, so the card carries one statement about which calendar this is
       rather than three. The fallback matters: this rule has to be right on a
       card drawn before the property is set, and on the bare markup a check
       builds by hand. */
    --mb-accent: var(--accent);
    /* ONE ROW, AND THE HEIGHT IS THE POINT. 24px of content between 4px of
       padding is exactly 32 — a third of 97 is 32.3 — so nothing in here may
       be taller than 24px, which is what settles every size below. */
    display:flex; align-items:center; gap:var(--space-1);
    min-height:var(--space-8);
    padding:var(--space-hair) var(--space-1) var(--space-hair) var(--space-3);
    border-radius:var(--radius-pill); overflow:hidden;
    box-shadow: var(--glass-shadow), var(--elev-floating);
  }
  .meeting-banner.show{ transform:translateX(-50%) translateY(0); opacity:1; pointer-events:auto; }
  /* THE RAIL, now a hairline inside the pill's own curve rather than a bar
     down a card's edge. Absolute, so it is not a flex item competing for the
     width the title needs. */
  .mb-rail{ position:absolute; left:var(--space-2); top:var(--space-2); bottom:var(--space-2);
            width:var(--space-hair); border-radius:var(--radius-pill);
            background:var(--mb-accent); }
  /* THE COUNTDOWN. The one thing on here he asked to keep, so it is the only
     thing given two lines.
     THE WORD ABOVE IT IS LOAD-BEARING. "4:00" on its own beside a meeting
     title reads as four o'clock, and it reads identically four minutes before
     the hour and four minutes after it, which are opposite facts. IN / AGO /
     LATE are short enough that the column is no wider than the digits, so the
     distinction costs nothing the title would otherwise have. */
  .mb-clock{ display:flex; flex-direction:column; align-items:flex-start;
             flex:none; line-height:1; }
  .mb-eyebrow{ display:flex; align-items:center; gap:var(--space-1);
               font-size:var(--type-label); font-weight:var(--weight-loud);
               letter-spacing:.06em; text-transform:uppercase; color:var(--text-faint);
               line-height:1; white-space:nowrap; }
  .mb-count{ font-size:var(--type-body); font-weight:var(--weight-loud);
             line-height:1; color:var(--text);
             font-variant-numeric:tabular-nums; white-space:nowrap; }
  /* The dot is the pip that says this is happening NOW rather than being a
     record of something. Its colour comes from the event, like the rail.
     Kept breathing: tests/test-icon-motion.js holds a list of the motion in
     this app that earned its place and "a meeting starting now" is on it. */
  .mb-dot{ width:var(--space-2); height:var(--space-2); border-radius:var(--radius-round);
           background:var(--mb-accent, var(--green));
           box-shadow:0 0 var(--space-3) var(--space-hair) var(--green-glow);
           flex:none; animation:breathe 1.6s var(--ease-in-out) infinite; }
  /* WHICH ONE. The only thing that gets the leftover width, and the only thing
     allowed to be cut short — the whole title is on the element as a tooltip,
     and one press opens the event with everything that used to be printed
     here. */
  .mb-title{ flex:1 1 auto; min-width:0;
             font-size:var(--type-body); font-weight:var(--weight-strong);
             letter-spacing:-.01em; line-height:1.2; color:var(--text);
             overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  /* Two things on one minute. Still said, because "you have a meeting at ten"
     and "you have two things at ten" are different statements and only one of
     them is true — but said as a count with the other one named on it, not as
     a sentence across the bottom of a card. */
  .mb-also{ flex:none; font-size:var(--type-label); font-weight:var(--weight-loud);
            line-height:1; color:var(--text-faint);
            padding:var(--space-hair) var(--space-1); border-radius:var(--radius-pill);
            border:1px solid var(--glass-border); cursor:default; }
  /* THE COAT (row 1757). The forecast's own icon, with the sentence on it. The
     rule about what may say this and when has not moved an inch — see
     mbCoatLine() — only how much room the answer takes. */
  .mb-coat{ flex:none; display:inline-flex; align-items:center; color:var(--blue); cursor:default; }
  .mb-coat svg{ width:var(--space-4); height:var(--space-4); }
  /* THE ACTION, and there is only ever one of it. Join when there is a link,
     Open when there is something behind it, nothing at all otherwise — a Join
     button beside a meeting with no link is the exact thing the house rule
     about controls that look like they work forbids. The service is named on
     the button rather than in it: "Join Teams" does not fit in 212 pixels and
     a tooltip that says it is better than a card that does not. */
  .mb-acts{ display:flex; align-items:center; flex:none; }
  .mb-act{ font:inherit; display:inline-flex; align-items:center; justify-content:center;
           padding:var(--space-hair); border-radius:var(--radius-round);
           border:1px solid var(--glass-border-hi, var(--glass-border));
           background:transparent; color:var(--text); cursor:pointer; white-space:nowrap;
           line-height:1;
           transition:background var(--dur-fast) var(--ease-out),
                      border-color var(--dur-fast) var(--ease-out),
                      color var(--dur-fast) var(--ease-out); }
  .mb-act svg{ width:var(--space-4); height:var(--space-4); display:block; }
  .mb-act:hover{ background:var(--glass-bg); border-color:var(--text-faint); }
  /* The primary reads in the app's own primary language — the accent gradient
     and --ink, which is the "text that sits on the accent" colour every theme
     in this file already states. A literal white here would be a hardcoded
     colour in a new rule, and it would be the wrong white on Alabaster. */
  .mb-act-go{ background:linear-gradient(135deg, var(--accent), var(--accent-2));
              border-color:transparent; color:var(--ink); }
  .mb-act-go:hover{ border-color:transparent; filter:brightness(1.08); }
  .mb-act:focus-visible{ outline:none;
    box-shadow:0 0 0 var(--ring-width) var(--accent); }
  /* THE X, AND IT MEANS IT NOW. 24 square inside a 24px row, because the press
     that has to work on the first go is this one. */
  .mb-x{ font:inherit; font-size:var(--type-lead); line-height:1; cursor:pointer;
         color:var(--text-faint); background:transparent; border:0; flex:none;
         width:var(--space-5); align-self:stretch; padding:0;
         display:inline-flex; align-items:center; justify-content:center;
         border-radius:var(--radius-pill);
         transition:color var(--dur-fast) var(--ease-out); }
  .mb-x:hover{ color:var(--text); }
  .mb-x:focus-visible{ outline:none; box-shadow:0 0 0 var(--ring-width) var(--accent); }
  /* THE FUSE. The countdown, drawn: the fraction of the five-minute lead-in
     that is left. Its width is written by mbPaintClock() once a second and it
     is empty from the moment the meeting starts. */
  .mb-fuse{ position:absolute; left:0; bottom:0; height:var(--space-hair); width:0;
            background:var(--mb-accent);
            transition:width 1s var(--ease-linear); }
  /* ON A PHONE. 212 pixels fits in 430 with room to spare, so the four-column
     rebuild the old card needed here is gone with the columns. What is left is
     the one thing that was never about width: the pill has to clear the bottom
     navigation, because a card floating over the navigation is the same fault
     as a card floating over the reading. 780px rather than 640 because 780 is
     where .bottom-nav appears. */
  @media (max-width:780px){
    .meeting-banner{
      /* Clear of the bottom bar: the nav is one 48px row plus its own padding,
         and the safe-area inset underneath it on a notched phone. */
      /* OFF THE BUTTON AS WELL AS OFF THE BAR, 26 September. This number
         was the bar worked out by hand -- a 48px row, its padding and the
         chin -- and it was right for as long as the bar was the only
         thing down there. The big Ask Lucid button stands on top of the
         bar now, and this banner was landing on it: the check in
         tests/test-the-ask-button-is-on-the-phone.js could not press the
         button at all while a meeting was near, and it named this element
         as the thing taking the press. So it reads the two names the
         pages own floor reads rather than doing the arithmetic a second
         time and disagreeing with it later. */
      bottom:calc(var(--bottom-nav-h) + var(--space-3) + env(safe-area-inset-bottom, 0px));
      transform:translateX(-50%) translateY(160%);
    }
    .meeting-banner.show{ transform:translateX(-50%) translateY(0); }
  }
  .display{ font-family:var(--font-display); }
  .mono{ font-family:var(--font-mono); }
  /* ==========================================================
     WHY THERE IS A "LITE" MODE, AND WHAT IT TURNS OFF
     ==========================================================

     Matt: "the whole UI is all herky jerky, like it is shorting out."

     It was not JavaScript. Measured over ten seconds of an idle app, the main
     thread was 5% busy and there were zero layouts. It was the GPU, and the
     count is the whole story: THIRTY-SIX visible elements with a live
     backdrop-filter — twenty of them at blur(26px) saturate(1.5).

     backdrop-filter is the most expensive thing on this page by a distance.
     Each one makes the compositor snapshot everything behind it, blur it, and
     saturate it — every frame, per element, and the cost scales with the pixel
     area. Twenty of them on a high-DPI screen is a lot of full-screen blurs
     sixty times a second. Add a WebGL orb animating continuously and a
     readPixels() that stalls the pipeline on a timer, and "shorting out" is a
     fair description.

     None of it is new. It has been creeping up with every panel added, and it
     has now crossed the line on his machine.

     So: the effects are adaptive. The app measures its own frame rate a moment
     after load, and if the machine cannot hold a smooth one it drops to
     data-fx="lite" — the same layout and the same colours, with the blurs
     replaced by flat translucency. It is barely distinguishable in a
     screenshot and it is the difference between 12fps and 60.

     Settings lets him force it either way, because a measurement taken during
     a busy first second can be wrong in both directions and he should not have
     to argue with it.
     ========================================================== */
  .glass{
    position:relative; overflow:hidden;
    /* THE SHEEN IS A BACKGROUND LAYER, NOT AN OVERLAY. THIS IS THE
       LIGHT-MODE READABILITY BUG.
       It used to be `.glass::before{ position:absolute; inset:0 }` — a
       positioned pseudo-element with no z-index, which paints ABOVE every
       non-positioned child. So the sheen was not a highlight on the glass; it
       was a sheet of translucent white laid over the text.
       In the dark themes the sheen is rgba(255,255,255,0.09–0.16) and washing
       already-pale text a little paler is invisible. In Serene it is 0.55–0.65,
       and it covered the TOP HALF of every panel and every popup at about 52%
       — which is why a weekday letter set to #796F63 rendered as
       rgb(185,179,173), and why the popups were "impossible to read when
       white/grey". Measured, not guessed: that pixel is exactly #796F63
       composited at alpha .52 over the panel.
       No amount of darkening the token could have fixed it. 52% of a darker
       colour is still 52%.
       As a background layer the sheen sits above the fill and below the
       content: the look is unchanged and it is one less painted layer. */
    background:var(--glass-sheen), var(--glass-bg);
    /* Blur/saturation are tokens now rather than hardcoded, because the
       "frosted" theme's whole identity is a much heavier blur — that is the
       one property that separates a frosted pane from a tinted one. */
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
    border:1px solid var(--glass-border); border-top-color: var(--glass-border-hi); border-radius:var(--radius);
    box-shadow: var(--glass-shadow); transition:background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
  }
  /* (.glass::before is deliberately gone — see the background layer above.) */

  /* A PANEL YOU GLANCE PAST CAN BE GLASS. A PANEL YOU READ HAS TO BE PAPER.
     ---------------------------------------------------------------------
     The sheen fix above stopped a translucent sheet being painted OVER the
     text. This is the other half of the same complaint — "the Ask window is
     too transparent to read" — and it is not the sheen, it is the fill.
     Measured with a red sheet behind the open Ask card: 42% of it came
     through. Behind that card in real use is the mail list, the home grid,
     and now a turning vortex. After this rule: 12%.

     Contrast alone does not catch this, and the readability harness was right
     to pass it: the text IS legible against the composited pixels. What it
     cannot measure is that the pixels keep MOVING. Reading a paragraph over a
     scrolling backdrop is work, and no amount of darkening the ink fixes it.

     So the fill gets an almost-opaque backing layer underneath it. The glass
     tint and the sheen sit on top of that exactly as designed, so the panel
     looks like the same panel — it simply has paper behind it now. Twelve per
     cent is deliberately not zero: the frosted theme's whole identity is what
     shows through, and a completely solid card would make it a different
     theme rather than a readable one.

     Applied only to the things that are READ: the Ask card, every modal, the
     dialog, and the search results. Tiles, the rail and the banner are
     glanced at and keep the full glass.

     ONE SYNTAX TRAP, WORTH THE COMMENT. In `background: A, B, C` only the
     LAST layer may be a colour. Writing the glass tint as the middle layer —
     which is what it is in .glass, where it happens to be last — makes the
     whole declaration invalid, and an invalid background is not a slightly
     wrong panel — it is a panel with NO background at all. The first version
     of this rule did exactly that, and the same red sheet went from 42%
     through to 94%: the fix made it twice as bad and looked plausible. So the
     tint is wrapped in a flat gradient, which is a legal image layer, and the
     paper goes last where a colour is allowed. */
  /* NOT a tray. Matt, 26 Aug: "dont like the new look you just made. I want
     them all to look like the task trays."
     He is describing a colour difference, and this rule is it. A tray is a
     .compose-modal.glass, so it was picking up the paper below — bg-base at
     96% — while the task drawer next to it stands on bg-mid. Two panels doing
     the same job, in two different tones, one click apart.
     A tray takes the drawer's ground instead; see the as-tray block. */
  .chat-card,
  .modal-backdrop:not(.as-tray) > .compose-modal.glass,
  #dlgCard.glass,
  .omni-card{
    background:var(--glass-sheen),
               linear-gradient(var(--glass-bg), var(--glass-bg)),
               color-mix(in srgb, var(--bg-base) 96%, transparent);
  }
  /* 96%, up from 88%.
     Matt, 26 Aug: "Make lucid chat panels a dark frosted glass...still have
     text bleeding in from behind." He is right and this rule already existed —
     it just was not opaque enough. Eight percent of the backdrop is nothing
     against a flat colour, which is what the harness measures, and clearly
     visible against an inbox, which is what is actually behind the chat card.
     Words on words is the one thing a reading surface cannot do.
     Deliberately NOT solid. A previous attempt at this set an opaque
     background-color on .chat-card and took the bleed to zero, which reads as
     a grey box stuck on the page — the frosted themes are frosted, and
     test-ask-readable holds a FLOOR as well as a ceiling for exactly that
     reason. Four percent is a whisper of what is behind, which is the point,
     and it is a whisper you cannot read words through. */
  /* The meeting banner carries BOTH .meeting-banner and .glass. Both set
     `position`, both have identical specificity, and .glass is declared
     later — so .glass's `position:relative` was silently winning. The
     banner therefore sat in normal document flow at the very top of the
     page: invisible (opacity:0) but 53px tall. That pushed the whole app
     down 53px and, because .app is height:100vh, pushed the same 53px off
     the bottom of the screen. THIS is why every page "sat too low".
     Restated here with higher specificity so it beats .glass on source
     order without needing !important. */
  .meeting-banner.glass{ position:fixed; }
  /* Fills #appZoomRoot, which is the thing that knows about the app's scale
     setting. Using vh/vw here instead is what caused the shrink-into-the-
     corner bug. */
  .app{ display:flex; height:100%; width:100%; }
  /* position:fixed, not the static default — added alongside 138's move from
     `zoom` to `transform:scale()`. At any scale below 100% #appZoomRoot's own
     box is deliberately counter-sized LARGER than the viewport (100/scale
     vh/vw, and in applyFontScale in app.js) so the scaled-down result still
     fills exactly one windowful. `zoom` folded that larger box back down
     before handing a footprint to its ancestor, so `<body>` never saw more
     than one viewport's worth of content. `transform` does not — it is a
     paint-only operation, so a normal-flow #appZoomRoot hands body its full
     PRE-transform size (up to 122% of the window at the smallest step), and
     document.documentElement.scrollWidth measured exactly that 122% back
     (test-ui-scale.js caught it: a real 468px of sideways scrollWidth at
     Small, on a 1440px window). `overflow:hidden`/`clip` on body do not help
     — scrollWidth measures the union of descendant boxes regardless of the
     overflow keyword, in-flow or not. Taking #appZoomRoot OUT of flow is what
     works: a fixed element is anchored to the viewport rather than in
     `<body>`'s normal flow, so its size — however much larger than the
     window — never contributes to any ancestor's scrollable area at all,
     which is the same reason #loginGate is fixed rather than trusting
     overflow:hidden to hide it.
     transition:none for an unrelated reason found alongside it: this app sets
     `transition-duration:.01ms !important` on every element under
     prefers-reduced-motion (see the *,*::before,*::after rule above), and the
     CSS default `transition-property` is `all` — so with no `transition`
     declared here, #appZoomRoot had an accidental, technically-real
     transition on every property INCLUDING transform any time reduced motion
     was on. `zoom` was never picked up by this, because it isn't a
     transitionable property; `transform` is. A transition of .01ms is
     imperceptible in a live tab, but it is a real transition with a real
     start-of-frame keyframe, and test-ui-scale.js found the failure mode: in
     a headless/software-rendered browser that doesn't paint a frame between
     two rapid style writes, getComputedStyle can keep reporting the value the
     never-rendered transition started from rather than the one it was set to
     — flaky by exactly the margin of "did a frame happen to render in
     between". A scale change is not an animation Lucid ever wants — it should
     land in the one frame it is set, the same way `zoom` always did — so
     transitions are switched off here outright rather than left to the
     accident of whether reduced motion happens to be on. */
  #appZoomRoot{ position:fixed; inset:0; height:100vh; width:100vw; transition:none; }
  /* IN THE IPHONE APP THE PAGE SCROLLS, NOT .main — see lucidShellBoot in
     app.js for the measurement. Nothing is fixed between the finger and the
     document, so the web view's own scroll takes the pan. The bottom nav and
     every sheet are position:fixed already and stay put. */
  html.ios-shell, html.ios-shell body{ height:auto; min-height:100%; overflow-x:hidden; overflow-y:auto; }
  html.ios-shell #appZoomRoot{ position:relative; inset:auto; height:auto; min-height:100vh; transform:none !important; }
  html.ios-shell .app{ height:auto; min-height:100vh; }
  html.ios-shell .main{ overflow:visible; height:auto; }
  @keyframes breathe{ 0%,100%{opacity:1;} 50%{opacity:.35;} }

  /* SIDEBAR / SPINE */
  .sidebar{ width:216px; flex:none; margin:var(--space-4) 0 var(--space-4) var(--space-4); padding:var(--space-4) var(--space-3); display:flex; flex-direction:column; transition:width var(--dur-base) var(--ease-out), padding var(--dur-base) var(--ease-out); overflow-y:auto; overflow-x:hidden; }
  .sidebar.collapsed{ width:70px; padding:var(--space-4) var(--space-3); }

  /* THE SPINE ON MAIL IS THE SPINE. Row 2166, 15 September.

     Row 2147 lived here for one day: on Mail the collapsed spine went to 88px,
     #folderRail was moved into it under Mail's own row, and the six folders
     hung off an indented spur. Matt: "Hate the new spine with the crooked email
     icons under...looks weird. Need other options."

     Every rule of it is gone rather than overridden — the 88px width, the
     pinned 46px column, the --room-c, the spur, the 38px squares and the bar
     that landed on the spur. An overridden rule is still a rule somebody has to
     read before they can be sure it does nothing, and there is nothing left for
     it to describe: the folders are a row across the top of the tray now (see
     .folder-rail below) and Mail is not a special case anywhere in this file.

     WHAT SURVIVED THE ROW IS THE PRINCIPLE, and it moved with the folders: the
     PAGE you are on is a RING round a spine square, the FOLDER you are in is a
     BAR in the same area colour, and no folder ever gets a ring. Two levels of
     "where am I" told apart by shape rather than by brightness. It is written
     out again at .folder-rail, which is where it now applies. */

  /* A HAIRLINE UNDER THE MASTHEAD. A newspaper puts a rule under its own name
     for the same reason: it says the name is not the first item in the list.
     Without it the wordmark was simply the top row of the rail. */
  .sidebar-head{ display:flex; align-items:center; justify-content:space-between;
    padding:var(--space-hair) var(--space-2) var(--space-3); margin-bottom:var(--space-2); border-bottom:1px solid var(--glass-border); }
  /* Wraps the wordmark and its masthead line so the two travel as one unit
     against the collapse button — see .sidebar-head's align-items:center,
     which now centers a two-line block instead of a single row. */
  .brand-block{ display:flex; flex-direction:column; gap:var(--space-hair); min-width:0; }
  /* 700 to 600, and a hair larger. Fraunces at 700 is a heavy face fighting a
     glowing mark beside it; at 600 with the size raised it reads as a signature
     rather than a shout — which is the difference between expensive and loud,
     and the only place in the rail a serif is allowed to appear at all. */
  .brand{ font-family:var(--font-display); font-size:var(--type-title); font-weight:600; letter-spacing:.005em; display:flex; align-items:center; gap:var(--space-2); white-space:nowrap; color:var(--rail-ink); }
  /* THE MASTHEAD LINE, quiet enough to read once. Indented to the wordmark's
     own text, not the icon beside it, so the two lines feel like one signature
     rather than a caption bolted under the logo.
     IN BRASS. --text-faint made it a caption nobody read; the metal makes it
     the one warm mark at the top of a cool rail, which is what a masthead line
     is for.
     It used to be small caps at .3em tracking as well, and that was right
     while it read "For Life" — a subtitle has to look like a subtitle rather
     than a second, competing headline. It is not a subtitle any more; see
     below. */
  /* 345 — THE SECOND LINE IS THE PRODUCT'S NAME NOW, SO IT IS SET AS THE NAME
     IS SPELLED. It used to read FOR LIFE, a caption, and uppercase at .3em
     tracking is exactly what a caption wants. "4Life" through that same rule
     comes out as 4 L I F E, which is not the word on the domain, on the
     marketing site, or in his sentence. The brass, the weight and the position
     stay; the shouting goes, the tracking comes down to something a
     mixed-case word can carry, and 9px goes to 10 because losing the caps
     loses a couple of pixels of apparent size with them. */
  .brand-tagline{ font-family:var(--font-body); font-size:var(--type-label); font-weight:600; letter-spacing:.06em; color:var(--brass); opacity:.85; margin-left:var(--space-8); margin-top:var(--space-1); white-space:nowrap; }
  /* The mark carries the brand alone whenever the rail is collapsed, since
     .nav-label (the "Lucid" wordmark) is hidden in that state — so it goes
     slightly LARGER when collapsed, not smaller. */
  .brand-mark{ width:24px; height:24px; flex:none; display:block; }
  .sidebar.collapsed .brand-mark{ width:26px; height:26px; }
  /* THE LENS IS THE LOGO, 24 September. Same object as the sign-in screen and
     the Ask Lucid header, one variable apart. */
  .brand-lens.lth-mark{ flex:none; margin:0; }
  .sidebar.collapsed .brand-lens{ --m:26px; }
  /* 70px rail − 20px padding = 50px usable. A 26px mark beside a 26px
     button overflows by 2px, so the collapse button tightens when collapsed. */
  .sidebar.collapsed .collapse-btn{ width:22px; height:22px; }
  .sidebar.collapsed .sidebar-head{ padding-left:0; padding-right:0; }
  .collapse-btn{ width:26px; height:26px; border-radius:var(--radius-control); display:flex; align-items:center; justify-content:center; color:var(--text-faint); cursor:pointer; flex:none; }
  .collapse-btn:hover{ background:rgba(120,120,140,.12); color:var(--text); }
  .collapse-btn svg{ width:14px; height:14px; transition:transform var(--dur-base); }
  .sidebar.collapsed .collapse-btn svg{ transform:rotate(180deg); }
  .sidebar.collapsed .sidebar-head{ justify-content:center; }
  /* Previously the collapse-btn hid itself while collapsed, leaving no way
     back to labeled view. It now stays visible (just rotated) so the icon
     rail is never a dead end. */

  /* ============================================================
     A ROW IN THE RAIL.
     ------------------------------------------------------------
     Four pixels less air per row, four pixels less corner, and the current row
     said with an edge instead of a lit box. Each of those is a small thing and
     together they are the entire difference between the rail he called a
     garage and the one he called regal.

     WHICH AREA A ROW BELONGS TO IS SAID IN CSS, NOT IN JAVASCRIPT. --c is set
     by the attribute selectors below, so a row carries its own colour from the
     markup out — nothing has to remember to paint it, and a row added to the
     markup tomorrow without a rule here falls back to --accent rather than to
     nothing. */
  .nav-item{ --c:var(--accent); display:flex; align-items:center; gap:var(--space-2);
    padding:var(--space-2) var(--space-3); border-radius:var(--radius-tight); color:var(--rail-ink-2); font-size:var(--type-body);
    font-weight:500; cursor:pointer; margin-bottom:1px;
    transition:background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
    white-space:nowrap; position:relative; }
  .nav-item[data-view="home"], .nav-item[data-view="today"],
  .nav-item[data-view="inbox"]{ --c:var(--area-now); }
  .nav-item[data-view="contacts"]{ --c:var(--area-people); }
  /* .nav-item, not .nav-group — Media, and since item 288 Work, are
     destinations as well as area headings, so they are ordinary rail rows
     carrying data-group. The colour belongs to the AREA, so it follows the
     attribute rather than the class, and the rail row, the front page's doors
     and the pages underneath all wear the same one. Written as
     .nav-item[data-group] rather than .nav-group[data-group] for Work as well
     now: the class went when the dropdown did, and a rule matching the old
     class would have left the Work row wearing --accent — silently, because
     nothing goes red when a row falls back to a colour that exists. See
     NAV_GROUPS.work and NAV_GROUPS.media in app.js. */
  .nav-item[data-group="work"]{ --c:var(--area-work); }
  .nav-item[data-group="media"]{ --c:var(--area-media); }
  /* AND LIFE THE SAME WAY, for exactly the reason the paragraph above gives
     about Work: this row was .nav-group until the wall gave it a `page`, and
     a rule still matching the old class would have left it wearing --accent
     silently, because nothing goes red when a row falls back to a colour that
     exists. See NAV_GROUPS.life. */
  .nav-item[data-group="life"]{ --c:var(--area-life); }
  /* BROWSE TAKES MEDIA'S HUE because it came out of the Media group on
     14 October and is still a Media place — the row moved, its area did not. */
  .nav-item[data-shell-page="window"]{ --c:var(--area-media); }

  .sidebar.collapsed .nav-item{ justify-content:center; padding:var(--space-2); border-radius:var(--radius-control); }
  /* The rounded-rect outline is the rail's visual language — make it read
     more clearly rather than replacing it. */
  .sidebar.collapsed:not(.peek) .nav-item:hover{ background:var(--wash-hover); box-shadow:inset 0 0 0 1px var(--glass-border); }
  /* THE BAR CANNOT WORK IN A 70px RAIL. There, the icon is the navigation and
     the row is a square around it, so the ring says "here" instead — the same
     statement in the shape the collapsed rail can make. */
  .sidebar.collapsed:not(.peek) .nav-item.active{ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--c) 60%, transparent); }
  .sidebar.collapsed:not(.peek) .nav-item.active::before{ display:none; }
  .sidebar.collapsed:not(.peek) .nav-item .dot{ display:none; }
  .nav-item:hover{ background:var(--wash-soft); color:var(--rail-ink); }
  /* THE CURRENT ROW, SAID WITH AN EDGE.
     It used to be a 16% wash of the accent inside a 35% ring inside a 20px
     glow — three statements of the same fact, and the glow is what made a
     $20-a-month app look like a demo. One 2px bar in the area's own colour,
     one weight of type, one barely-there wash. The bar sits in the rail's
     12px padding rather than inside the row, so it lands on the rail's edge
     and the column of them reads as a margin rule down the side. */
  .nav-item.active{ background:var(--wash-on); color:var(--rail-ink); font-weight:600; }
  .nav-item.active::before{ content:''; position:absolute; left:-6px; top:2px; bottom:2px;
    width:2px; border-radius:var(--radius-tight); background:var(--c); }
  /* ------------------------------------------------------------------------
     THE RAIL, BY KEYBOARD — and until now there was no such thing.

     Every row in the primary navigation is a bare <div> with an onclick. No
     tabindex, no role, no focus rule anywhere in this file matching .nav-item.
     Tab through Lucid and the rail is simply not there: the whole left-hand
     navigation of the application could not be reached without a mouse, and
     nothing said so because nothing that is unreachable can look wrong.
     railKeyboard() in app.js gives the rows the role and the tabindex; this
     rule is what makes the focus visible once they have it.

     THE RING IS THE AREA'S OWN COLOUR, which is row 210's language rather than
     a new one — the 2px bar beside the current row is already --c, so focusing
     Work outlines it in brass and focusing People in rose, and the rail gains a
     second use for a palette it already carries instead of a seventh hue.

     NOT `outline: … var(--accent)` like the dozen focus rules elsewhere in this
     file: the accent is one colour for the whole app, and on the one control
     where every row means a different place, one colour throws that away.

     The offset is negative so the ring sits INSIDE the row. A positive offset
     on a 226px rail puts two pixels of ring under the page edge on one side and
     over the active bar on the other, and a focus ring that is clipped reads as
     a rendering fault rather than as a state. */
  .nav-item:focus-visible{
    outline:2px solid var(--c);
    outline-offset:-2px;
    background:var(--wash-hover);
    color:var(--rail-ink);
  }
  .nav-item:focus-visible .nav-icon{ opacity:1; }
  /* The collapsed rail is a 70px column of squares, so the ring goes round the
     square rather than round a row, and it needs no inset — there is padding
     on both sides of it there. */
  .sidebar.collapsed:not(.peek) .nav-item:focus-visible{ outline-offset:1px; }
  /* 18px in the open rail, 23px collapsed. In a 70px rail the icon IS the
     navigation and needs the size; beside a 13.5px label at 18px it was
     shouting over the word it was supposed to be introducing. Hairline stroke
     for the same reason — 2.0 is a marker pen. */
  .nav-icon{ width:18px; height:18px; opacity:0.62; flex:none; stroke-width:1.5;
    transition:opacity var(--dur-fast) var(--ease-out); }
  .sidebar.collapsed .nav-icon{ width:23px; height:23px; opacity:0.8; stroke-width:1.7; }
  .nav-item:hover .nav-icon{ opacity:0.85; }
  .nav-item.active .nav-icon{ color:var(--c); opacity:1; }
  /* THE INBOX DOT NOW MEANS SOMETHING.
     It was decorative — a fixed accent dot breathing on a 2.4s loop whether or
     not a single message was unread, which is a control that lies quietly for
     months. renderSpineCounts() in app.js now hides it unless there is unread
     mail in the loaded inbox. It does not breathe: an animation is a request
     for attention, and "you have mail" is a fact, not an alarm. */
  .nav-item .dot{ width:5px; height:5px; border-radius:var(--radius-round); background:var(--c);
    margin-left:auto; flex:none; }
  /* A COUNT, NOT A BADGE.
     Tabular figures, right-aligned, no bubble — so 4, 8 and 312 line up on the
     digit and the rail gains a straight edge down its right-hand side. The
     pill it replaces was the single most dated thing in the spine. */
  .nav-n{ margin-left:auto; font-size:var(--type-body); font-weight:500; color:var(--rail-ink-3);
    font-variant-numeric:tabular-nums; letter-spacing:.01em; flex:none; }
  .nav-item.active .nav-n{ color:var(--c); }
  .nav-item .dot + .nav-n, .nav-item .nav-n + .dot{ margin-left:var(--space-2); }
  /* Labels fade (see THE SPINE, ONE GRID at the end of this file); they are never display:none, so nothing reflows between closed and open. */

  /* Generic hover-tooltip system — content comes from data-tooltip so any
     element can get one without separate markup to keep in sync. Sidebar
     icons show it to the right (they sit at the left edge); everything
     else shows it below by default. */
  [data-tooltip]{ position:relative; }
  /* A HIDDEN TOOLTIP IS STILL IN THE LAYOUT, AND THAT PUSHED THE PAGE SIDEWAYS.
     -----------------------------------------------------------------------
     This used to sit at opacity:0; visibility:hidden and nothing else — which
     hides it from the eye and NOT from the layout engine. An absolutely
     positioned box still contributes to its ancestors' scrollable overflow,
     and `transform` does not move a layout box, only its painted pixels. So
     every one of these was a nowrap label, up to a couple of hundred pixels
     wide, whose real box began at the middle of its button and ran off to the
     right of it, permanently.
     Almost everywhere there is slack to the right and nothing shows. In the
     mail toolbar at 760px there is none: "Threads, Tidy and Select" hangs off
     the last control on the row and made the whole inbox 15px wider than the
     window, at every text size — a page that scrolls sideways, which is the
     one thing tests/test-no-h-overflow.js exists to forbid. It found it; this
     is the cause it found.
     display:none is the fix, because it is the only value that takes a box out
     of layout entirely. The fade is kept where the browser can do it —
     transition-behavior:allow-discrete plus @starting-style animate a property
     that is switching between none and block, which is exactly this case — and
     a browser without them simply shows the tooltip at once, which is what a
     tooltip did for thirty years. */
  [data-tooltip]::after{
    content: attr(data-tooltip); position:absolute; left:50%; top:calc(100% + 8px); transform:translateX(-50%);
    background:var(--bg-deep); color:var(--text); font-size:var(--type-label); font-weight:600; padding:var(--space-2) var(--space-2);
    border-radius:var(--radius-control); white-space:nowrap; pointer-events:none; display:none; opacity:0;
    transition:opacity var(--dur-fast) var(--ease-out),
               display var(--dur-fast) allow-discrete; z-index:80; box-shadow:var(--elev-floating);
    border:1px solid var(--glass-border-hi);
  }
  [data-tooltip]:hover::after{ display:block; opacity:1; }
  @starting-style{ [data-tooltip]:hover::after{ opacity:0; } }
  .nav-item[data-tooltip]::after, .theme-toggle[data-tooltip]::after{ left:calc(100% + 12px); top:50%; transform:translateY(-50%); }
  .sidebar:not(.collapsed) .nav-item::after{ display:none; } /* labels already visible — no need to duplicate them */
  /* Rail tooltips were CSS ::after pseudo-elements sitting outside the rail's
     70px width — and the sidebar clips overflow-x, so they were cut off and
     mostly never appeared at all. This is a single fixed element positioned by
     JS: outside the clipping context, and shown on mouseenter with no
     transition, so it is instant rather than fading in over 130ms. */
  .sidebar [data-tooltip]::after{ display:none !important; }
  #railTip{
    position:fixed; z-index:200; pointer-events:none; opacity:0; visibility:hidden;
    background:var(--bg-deep); color:var(--text); font-size:var(--type-body); font-weight:600;
    padding:var(--space-2) var(--space-3); border-radius:var(--radius-control); white-space:nowrap;
    border:1px solid var(--glass-border-hi); box-shadow:var(--elev-floating);
  }
  #railTip.show{ opacity:1; visibility:visible; }

  .accounts{ margin-top:var(--space-5); }
  /* SECTION LABELS, SET LIKE SECTION LABELS. Smaller and much wider tracked:
     .09em is a word squeezed into caps, .2em is a heading. Same trick as the
     masthead line above and for the same reason — at 9.5px the spacing is
     doing the work the size cannot.

     AND THE TRACKING CAME DOWN TO .1em WHEN THE SIZE WENT UP TO 11px, which is
     the sentence above read the other way round. .2em was compensating for type
     too small to carry a heading on its own; at 11px it is not compensating for
     anything, it is just wide — and it was wide enough to break something. On
     the 11px step "TASKS & REMINDERS" measured 154px of text into 139px of rail
     and ellipsised itself to "TASKS & REMIND…", which is a label that no longer
     says what it is. Measured rather than guessed: .14em still overflows by
     five pixels, .12em lands exactly on the edge, .1em fits with room and is
     still unmistakably a tracked heading beside 11px body-weight text. */
  .accounts-label{ font-size:var(--type-label); font-weight:600; text-transform:uppercase; letter-spacing:0.1em; color:var(--rail-ink-3); padding:0 var(--space-3); margin-bottom:var(--space-2); }
  /* Generic pill — still used by the mobile folder switcher (folderMobile) */
  .pill{ display:inline-flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); border:1px solid var(--glass-border); color:var(--text-dim); font-size:var(--type-body); margin:0 var(--space-2) var(--space-2) 0; cursor:pointer; transition:all var(--dur-fast) var(--ease-out); }
  .pill:hover{ border-color:var(--glass-border-hi); color:var(--text); }
  .pill.active{ border-color:transparent; font-weight:700; background:color-mix(in srgb, var(--accent) 22%, transparent); color:var(--text); box-shadow:0 0 14px color-mix(in srgb, var(--accent) 30%, transparent); }
  .acct-switcher{ position:relative; padding:0 var(--space-1); }
  /* Squared with the rest of the third pass. A 10px control sitting between a
     4px nav row above it and a 4px task row below it is the one rounded thing
     left in the column, which is how a rail starts looking assembled rather
     than designed. */
  .acct-switcher-btn{ width:100%; display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-3); border-radius:var(--radius-tight); border:1px solid var(--glass-border); background:rgba(255,246,230,.03); color:var(--rail-ink-2); cursor:pointer; font-size:var(--type-body); font-family:var(--font-body); }
  .acct-switcher-btn:hover{ border-color:var(--glass-border-hi); }
  .acct-switcher-dot{ width:9px; height:9px; border-radius:var(--radius-round); background:linear-gradient(135deg,var(--neutral-1),var(--neutral-2)); flex:none; }
  .acct-switcher-label{ flex:1; text-align:left; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .acct-switcher-chevron{ width:13px; height:13px; flex:none; transition:transform var(--dur-fast); color:var(--text-faint); }
  .acct-switcher.open .acct-switcher-chevron{ transform:rotate(180deg); }
  .acct-switcher-panel{ display:none; position:absolute; top:calc(100% + 6px); left:4px; right:4px; z-index:60; background:var(--bg-deep); border:1px solid var(--glass-border-hi); border-radius:var(--radius-card); box-shadow:var(--elev-floating); padding:var(--space-2); max-height:280px; overflow-y:auto; }
  .acct-switcher.open .acct-switcher-panel{ display:block; }
  .acct-switcher-item{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-2); border-radius:var(--radius-control); cursor:pointer; font-size:var(--type-body); }
  .acct-switcher-item:hover{ background:rgba(120,120,140,.12); }
  .acct-switcher-item.active{ background:color-mix(in srgb, var(--accent) 18%, transparent); }
  .acct-switcher-item .dot{ width:8px; height:8px; border-radius:var(--radius-round); flex:none; }
  .acct-switcher-item .email{ flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .acct-switcher-item .provider-tag{ font-size:var(--type-label); text-transform:uppercase; letter-spacing:.04em; color:var(--text-faint); border:1px solid var(--glass-border-hi); border-radius:var(--radius-control); padding:1px var(--space-1); flex:none; }
  .acct-switcher-divider{ height:1px; background:var(--glass-border); margin:var(--space-2) var(--space-hair); }
  .acct-switcher-connect{ font-size:var(--type-body); color:var(--accent); font-weight:600; padding:var(--space-2) var(--space-2); cursor:pointer; border-radius:var(--radius-control); }
  .acct-switcher-connect:hover{ background:rgba(120,120,140,.12); }

  .spine-section{ margin-top:var(--space-5); overflow-y:auto; min-height:0; }
  .spine-section.grow{ flex:1; }
  /* THE CAP WAS ON THE WRONG ELEMENT, AND IT COST HIM THE ADD BUTTON.
     ------------------------------------------------------------------
     .spine-section.capped holds three things: the "Tasks & Reminders" heading,
     the list, and the "+ Add task" button under it. A max-height of 168px on
     the SECTION therefore capped all three together — so the heading ate part
     of the allowance, the fourth task was cut through its own date line, and
     the Add button was pushed below the clip and could not be seen or pressed
     at all. It has been invisible for as long as the cap has existed; nobody
     noticed because a control that is scrolled out of a hidden overflow leaves
     no gap where it used to be.
     The cap belongs on the list alone. A task row measures 41px in the browser
     (printed by tests/shots-spine.js, not estimated), so four whole rows is
     164px; the heading and the button now sit outside it, always visible, and
     tasks five and six are reached by scrolling the list itself. */
  .spine-section.capped{ flex:none; overflow:visible; }
  .spine-section.capped #taskList{ max-height:164px; overflow-y:auto; }
  /* ROLLED UP INTO A CHEVRON. (344)
     "tasks and reminders in the spine (along with the add task button) need to
      roll up into a chevron."
     The section closes to its own heading. display:none rather than a height
     animation on purpose: #taskList is a scroller with its own max-height, and
     animating the height of a box that scrolls produces a list that jumps
     under the finger on the way open. The chevron does the moving instead —
     the same quarter turn .cd-know-chevron and .tlc-chev use, so a chevron
     pointing down means open everywhere in this app.
     .spine-task-add is the full-width button, which rolls up with the list; the
     small + in the heading is what keeps adding a task to one press. */
  .spine-task-add{ margin-top:var(--space-2); }
  .spine-section.rolled #taskList,
  .spine-section.rolled .spine-task-add{ display:none; }
  .spine-toggle .spine-chevron{ transition:transform var(--dur-fast) var(--ease-out); }
  .spine-section:not(.rolled) .spine-toggle .spine-chevron{ transform:rotate(90deg); }
  .spine-head-acts{ display:flex; align-items:center; gap:1px; flex:none; }
  /* THE HEADING HAS TO SURVIVE A THREE-DIGIT COUNT.
     "TASKS & REMINDERS" is 133px of letterspaced 9.5px caps and the rail gives
     the row 186 — so the words, the number and two controls are within a few
     pixels of the edge, and the first thing that happened was the count
     dropping onto a second line, which reads as a broken heading rather than a
     tight one. The words are what gives way now, not the number: the count is
     the entire reason to look at a heading whose list is folded away, so it is
     flex:none and the words ellipsise around it. In practice they never do —
     this only comes into play past a hundred open tasks. */
  .spine-toggle-label{ display:flex; align-items:baseline; min-width:0; }
  .spine-toggle-words{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .spine-toggle .spine-count{ flex:none; margin-left:var(--space-2); }
  /* A 15px hit area is too small for a control he uses daily, so the box is
     22px and the glyph is centred in it — the + reads at 15px and the target
     is the whole square. Same ink as the chevron beside it until it is
     pointed at, because two lit controls in one heading is a heading that
     argues with itself. */
  .spine-add{
    width:20px; height:20px; display:flex; align-items:center; justify-content:center;
    border-radius:var(--radius-control); font-size:var(--type-lead); line-height:1; font-weight:600;
    color:var(--rail-ink-3); cursor:pointer; flex:none;
    transition:background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
  }
  .spine-add:hover{ background:var(--wash-soft); color:var(--text); }
  .spine-add:focus-visible{ outline:2px solid var(--accent); outline-offset:1px; }
  /* A TASK ROW, IN THE SAME LANGUAGE AS A NAV ROW. Squared to 4px, tightened,
     and the glowing blue dot replaced by a plain one — a dot with an 8px halo
     on every task turns a list of six into six small lights. The day count
     keeps the mono, because it is a number in a column of numbers. */
  .attn-mini{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-3); border-radius:var(--radius-tight); cursor:pointer; margin-bottom:1px; }
  .attn-mini:hover{ background:var(--wash-soft); }
  .attn-mini .dot{ width:5px; height:5px; border-radius:var(--radius-round); background:var(--area-now); flex:none; }
  .attn-mini-name{ font-size:var(--type-body); font-weight:500; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .attn-mini-days{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--rail-ink-3); flex:none; font-variant-numeric:tabular-nums; }

  .spine-attn-collapsed{ display:none; margin-top:var(--space-5); flex-direction:column; align-items:center; gap:var(--space-1); cursor:pointer; position:relative; }
  .sidebar.collapsed .spine-attn-collapsed{ display:flex; }
  /* A 9px unread count is the smallest type in the app and it sits on a solid
     accent. --ink is near-black, which works on the dark themes' bright blue
     and disappears on the light themes' deeper one. */
  :root[data-mode="light"] .spine-badge{ color:var(--on-accent); }
  .spine-badge{ position:absolute; top:-4px; right:8px; background:var(--blue); color:var(--ink); font-size:var(--type-label); font-weight:800; width:15px; height:15px; border-radius:var(--radius-round); display:flex; align-items:center; justify-content:center; }

  /* ---- Needs Attention, when there IS something ------------------------
     Blue is the colour of everything else in the app, so a blue warning
     triangle read as decoration. It goes red and breathes ONLY while there
     is something overdue — a permanent red glow is just a new background
     colour, and stops being noticed within a day. Nothing overdue, nothing
     glowing. */
  .spine-attn-collapsed.has-overdue svg{ color:var(--cat-red-2, #ff6b5e) !important;
               filter:drop-shadow(0 0 7px rgba(255,90,80,.8));
               animation:attnPulse 2.1s var(--ease-in-out) infinite; }
  .spine-attn-collapsed.has-overdue .spine-badge{
               background:var(--cat-red, #b8382e); color:#fff;
               box-shadow:0 0 9px 1px var(--cat-red-glow); }
  /* THE ALARM COLOUR IS A PASTEL IN LIGHT MODE.
     --cat-red-2 is the light-on-dark red: on a near-black spine it is a hot
     coral. In the light themes it is #D6907F, and white lettering on it
     measured 1.94:1 — the one badge in the app whose entire job is to be
     noticed was the least readable thing on the screen. Light mode takes the
     solid --cat-red, and the drop-shadow glow goes with it: a glow on cream
     is a smudge, not a light. */
  :root[data-mode="light"] .spine-attn-collapsed.has-overdue svg{ color:var(--cat-red) !important; filter:none; }
  :root[data-mode="light"] .spine-attn-collapsed.has-overdue .spine-badge,
  :root[data-mode="light"] .spine-count.has-overdue{
               background:var(--cat-red); color:var(--neutral-ink); box-shadow:none; }
  /* The padding and radius live HERE now rather than on .spine-count, because
     the resting state stopped being a shape when the pill was removed. An
     alarm still gets to be one: this is the only filled count left in the rail
     and it appears only while something is actually late. */
  .spine-count.has-overdue{
               background:var(--cat-red, #b8382e); color:#fff;
               border-radius:var(--radius-tight); padding:1px var(--space-2);
               box-shadow:0 0 10px 1px var(--cat-red-glow);
               animation:attnPulse 2.1s var(--ease-in-out) infinite; }
  .spine-toggle.has-overdue > span{ color:var(--cat-red-2, #ff6b5e); }
  @keyframes attnPulse{
    0%,100%{ filter:drop-shadow(0 0 5px rgba(255,90,80,.55)); }
    50%    { filter:drop-shadow(0 0 11px rgba(255,90,80,.95)); }
  }
  /* A pulsing red glow is exactly the thing someone with vestibular trouble
     asked the browser to stop doing. The colour still carries the meaning. */
  @media (prefers-reduced-motion: reduce){
    .spine-attn-collapsed.has-overdue svg, .spine-count.has-overdue{ animation:none; }
  }

  .sidebar-foot{ margin-top:var(--space-6); margin-bottom:var(--space-16); padding:var(--space-2) var(--space-3); color:var(--text-faint); font-size:var(--type-label); display:flex; flex-direction:column; align-items:flex-start; gap:var(--space-3); }
  /* THE BUILD STAMP IN THE SPINE. Quieter than "Live data" above it and set
     in the mono face, because it is an identifier to be read character by
     character and compared against another one — a proportional font is the
     wrong tool for telling 116 from 118 in a screenshot. It carries no colour
     of its own beyond the footer's --text-faint: nothing about knowing which
     build you are running is a warning. `gap` is already 10px on the column,
     so a negative top margin pulls it up under its neighbour rather than
     letting it read as a third, unrelated row. Empty until buildStamp() fills
     it, and :empty collapses the gap it would otherwise leave in a working
     copy that has no build number to print. */
  .sf-build{ font-family:var(--font-mono); font-size:var(--type-label); letter-spacing:.02em; margin-top:-6px; opacity:.85; }
  .sf-build:empty{ display:none; }

  /* HALF OF ONE DEPLOY AND HALF OF ANOTHER, SAID ACROSS THE TOP.
     ======================================================================

     Built by showStaleBuildBanner() in app.js and only ever present when
     index.html and app.js disagree about which build they belong to. It sits
     ABOVE everything for the reason it exists: every number and every control
     under it may be from a different build, so it must not be something a
     person scrolls past.

     position:FIXED, NOT sticky, and that is measured rather than stylistic.
     `body` is `height:100%; overflow:hidden` and `.app` inside it is
     `height:100%` — so a bar inserted into the normal flow pushes the whole
     application down by its own height and the bottom of it is clipped away by
     that overflow:hidden. Taking the bar out of flow costs the top forty pixels
     of a screen that is already showing the wrong code, which is the right
     trade; silently cropping the app while telling somebody their app is
     unreliable is not.

     --cat-red rather than --orange, and this is the one place in the app where
     that is right. Orange is Lucid's colour for "this is not quite what it
     looks like" — a mailbox that failed, a search that is not in date order —
     and those are all statements about DATA. This is a statement about the
     application itself: the screen is not showing the code that was deployed,
     so nothing on it can be trusted, including the orange warnings. */
  .stale-build-bar{ position:fixed; top:0; left:0; right:0; z-index:9999; display:flex; align-items:center; justify-content:center;
                    gap:var(--space-4); flex-wrap:wrap; padding:var(--space-3) var(--space-4); font-size:var(--type-body); line-height:1.45;
                    color:#fff; background:var(--cat-red); box-shadow:var(--elev-raised); }
  .stale-build-bar b{ font-family:var(--font-mono); font-weight:600; }
  /* The button inherits .btn's shape from elsewhere; only the colours are
     overridden, because .btn's own palette is invisible on a red field. */
  .stale-build-bar .btn{ background:rgba(255,255,255,.16); border-color:rgba(255,255,255,.5); color:#fff; flex:none; }
  .stale-build-bar .btn:hover{ background:rgba(255,255,255,.28); }

  /* THE SEARCH ROW, BACK ON THE SPINE. (1726)
     ------------------------------------------------------------------
     It is a .nav-item and it takes everything a nav row already is — the hover
     wash, the icon opacity, the collapsed square, the focus ring. Three rules
     are all it needs on top of that, and each one exists for a reason:

     1. IT TAKES THE ACCENT RATHER THAN AN AREA HUE. The rail is coloured five
        ways by area — now, people, work, media, life — and search belongs to
        none of them because it reaches all of them. --accent is what .nav-item
        already falls back to when nothing overrides --c, so this rule is really
        just the absence of an override, written down so nobody adds one.
     2. THE KEYBOARD HINT SITS WHERE A COUNT SITS. margin-left:auto puts it
        against the right edge on the same line the nav-n counts line up on, so
        the rail keeps the straight edge those counts were given it for. It is
        mono and one step quieter than the label, because it is reference rather
        than news — the same argument as the build stamp in the foot.
     3. IT NEVER SURVIVES THE COLLAPSE. It wears .nav-label, so the 70px rail
        drops it with every other word and the tooltip carries it instead; this
        rule is what stops the flex row leaving a gap where it was. */
  .nav-item.nav-search{ --c:var(--accent); }
  /* --rail-ink-2, NOT --rail-ink-3, AND THAT IS A MEASUREMENT. The faint rail
     ink is what .nav-n's counts take, and on a hint it does not carry: pixel
     contrast against the wash behind it came back between 2.4:1 and 3.1:1 on
     every dark theme and 3.6 to 4.3 on the papers — under the 4.5 floor
     everywhere, in all sixteen. Tracked mono capitals at 10px put less ink on
     the page than their size suggests, which is the same finding the Home
     labels wrote down four thousand lines below this. One step up clears it. */
  .nav-kbd{
    margin-left:auto; flex:none;
    font-family:var(--font-mono);
    font-size:var(--type-label); letter-spacing:.04em; color:var(--rail-ink-2);
    padding:var(--space-hair) var(--space-2); border-radius:var(--radius-control);
    border:1px solid var(--glass-border);
    /* NO FILL, AND THAT IS THE SECOND HALF OF THE MEASUREMENT ABOVE. It was
       drawn on --wash-soft first, and a wash is ink laid over the ground: on a
       paper theme it darkens the paper under dark lettering, on a dark theme it
       lightens the ground under light lettering, and either way it takes
       contrast off the smallest type in the rail. Dropping it lifted Blush and
       Mist from 4.44 and 4.47 to over the floor and moved every other theme up
       with them. A hairline still reads as a key cap; a fill was never what
       made it one. */
    background:transparent;
    transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
  }
  .nav-item.nav-search:hover .nav-kbd{ color:var(--c); border-color:color-mix(in srgb, var(--c) 45%, transparent); }

  /* THE COG SITS ON THE RAIL'S ICON LINE, AND IT DID NOT. 9 October.
     Matt: "Setting button is not inline with the rest of the buttons in
     spine. (currently offset) Fix".

     MEASURED, at his own 1481x690, before anything was changed:

       every .nav-item icon, and the + on New ....... centre x = 50.0
       the Settings cog ............................. centre x = 59.0
       collapsed: every icon 51.0, the cog still ..... centre x = 59.0

     THE CAUSE IS A BOX, NOT A NUDGE. A rail row is a full-width box (190px
     at 216) whose 18px .nav-icon sits --space-3 in from the row's own left
     edge, so every glyph on the rail centres at 29 + 12 + 9 = 50. The cog is
     not a row: it is a 36px square, and .sidebar-foot lays it flush with the
     foot's own --space-3, so its centre was its own half-width out — 41 + 18
     = 59. Nine pixels, and the number was never written anywhere; it fell out
     of the two boxes being different sizes and both being flush left.

     So the foot's icon column IS THE RAIL'S ICON COLUMN — 18px wide, the
     width of a .nav-icon — and the square centres inside it. Nothing is
     pushed anywhere: the square is aimed at the column the icons are in
     instead of at the foot's left edge, and the correction moves on its own
     if either box is ever resized.

     COLLAPSED IS THE SAME SENTENCE IN THE OTHER SHAPE. There the icon IS the
     navigation, and .sidebar.collapsed .nav-item centres it in the whole
     70px row, so the column is the whole row too. Both the row's padding and
     the foot's are symmetric, which is why a full-width column lands on the
     same centre with no number in it at all.

     tests/test-the-spine-lines-up.js holds every rail glyph to one centre
     line within a pixel, open and collapsed, so a future change to either
     box goes red here rather than crooked on his screen. */
  .sidebar-foot-icons{ display:flex; flex-direction:column; gap:var(--space-2);
    width:18px; align-items:center; }
  .sidebar.collapsed .sidebar-foot-icons{ width:100%; }
  .theme-toggle{ width:36px; height:36px; border-radius:var(--radius-card); display:flex; align-items:center; justify-content:center; cursor:pointer; background:rgba(120,120,140,.10); flex:none; color:var(--text-dim); position:relative; transition:all var(--dur-fast) var(--ease-out); }
  :root:not([data-mode="light"]) .theme-toggle:hover{ box-shadow:0 0 12px 0 var(--accent-glow); color:var(--text); }
  .theme-toggle:hover{ color:var(--text); }
  .theme-toggle svg{ width:15px; height:15px; }

  /* MAIN */
  /* A THIN BAND OF TOP PADDING, WHICH IS WHAT IT WAS BEFORE THE SEARCH BOX AND
     IS WHAT IT IS AGAIN. Between 2 and 11 September this was 54px, and every
     pixel of that was reserved space for .top-search-row — a compact search box
     that sat absolutely positioned in this corner of every page. Row 1726 put
     search back on the spine and took the box away, so the padding was holding a
     gap open for a control that had left the building, on every page in the app.
     It is a hairline rather than nothing because .home-band pulls itself up 4px
     and still wants room above it at the top of a scroll. .main.main-no-topsearch,
     the class that used to say this on Home alone, went with the box.
     14px is written out rather than taken from the spacing ramp on purpose:
     the ramp is on a 4px grid and has no 14, and the 11 September rewrite
     reached for the nearest token (--space-4, 16px) and moved this number
     two pixels without saying so. Off-grid is the point — this is a
     hairline above a band that pulls itself up 4px, not a spacing
     decision on the same ramp as the padding of a dialog. */
  /* THE BAND IS RESERVED HERE AND NOWHERE ELSE. Every page in the app is a
     .view inside .main, so one rule covers all of them and none of them can
     drift. It was 54px for a search box until 11 September; it holds the
     thread's corner open now. --lth-band is declared with the thread. */
  .main{ flex:1; overflow-y:auto; min-width:0;
    --main-pad-x: var(--space-8); --main-pad-top: var(--lth-band); --main-pad-bottom: var(--space-3);
    padding: var(--lth-band) var(--main-pad-x) var(--main-pad-bottom); }

  .view{ display:none; }
  .view.active{ display:block; animation:rise var(--dur-base) var(--ease-out); }
  /* Inbox and Today are the two views meant to FILL the window rather than
     scroll. They used to size themselves with hardcoded magic numbers
     (`height: calc(100vh - 176px)` and `- 158px`) that were hand-tuned back
     when the meeting-banner bug was stealing 53px off the top — so once that
     was fixed the numbers over-subtracted and left a dead band at the bottom
     of the screen. Replaced with flex, which measures itself: the view fills
     .main, the header rows take what they need, and the grid/wrap absorbs
     whatever is left. No numbers to drift out of sync again.
     `min-height:0` on both is what actually lets the inner lists scroll
     instead of overflowing their flex parent. */
  /* The Board is the third: a canvas that scrolls itself and must never make
     the page scroll. Without this it collapses to the height of its content
     and the dot grid stops halfway down the window — which also made "Fit"
     frame everything into the top-left quarter, because it was fitting to a
     box that was not the size it looked. */
  #view-inbox.view.active, #view-today.view.active, #view-board.view.active{
    display:flex; flex-direction:column; height:100%; min-height:0;
  }
  /* Was translateY(6px) scale(.99). During a cross-fade that vertical nudge
     reads as the page lurching into place. A dissolve should not also move. */
  @keyframes rise{ from{opacity:0;} to{opacity:1;} }

  /* ---- VIEW CHANGES ----
     switchView used to be a hard cut: the outgoing view got display:none in a
     single frame, then the incoming one played `rise`. Nothing eased OUT, so
     every navigation read as a pop followed by a slide.
     Now it is a genuine cross-dissolve: the content pane eases out, the swap
     happens while nothing is visible, and the new view eases back in.
     Deliberately NOT built on the View Transitions API — that defers its
     callback until the browser produces a paint frame, and under a throttled
     or backgrounded tab that leaves navigation looking like it did nothing.
     Verified in testing: it ran a full click behind. A timed opacity swap is
     boring, but it is deterministic and it cannot strand the user. */
  /* The previous attempt faded the whole content pane to nothing, swapped, then
     faded back. That is not a cross-fade, it is a blink — for 130ms the app
     showed an empty rectangle, which reads as "it broke" rather than "it moved".
     A real cross-fade needs both views on screen at once, so the outgoing view
     is lifted out of flow and fades out UNDER the incoming one, which fades in
     over the top. Nothing is ever blank. */
  .main{ position:relative; }
  .view.leaving{
    position:absolute; top:0; left:0; right:0; height:100%;
    opacity:0; pointer-events:none; z-index:0;
    transition: opacity var(--dur-view) var(--ease-out);
  }
  .view.active{ z-index:1; }

  /* Elements that had no transition at all, so their hover/selected states
     snapped between frames. Mail rows are the most-touched surface in the
     whole app and had none. */
  .mail-row{ transition: background var(--dur-fast) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), border-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); }
  .cd-tab{ transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
  .vt-btn, .more-item, .tb-btn, .modal-x, .mc-day, .list-check, .collapse-btn{
    transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), transform var(--dur-fast) var(--ease-out);
  }

  /* Scroll jumps were instant everywhere. */
  html{ scroll-behavior:smooth; }
  .main, .mail-list, .reading-pane, .agenda-scroll, .sidebar{ scroll-behavior:smooth; }

  /* Respect the OS setting. If someone has asked for less motion, honour it —
     this also makes the whole system easy to sanity-check by toggling one
     system preference. */
  @media (prefers-reduced-motion: reduce){
    *, *::before, *::after{
      animation-duration:var(--dur-none) !important; animation-iteration-count:1 !important;
      transition-duration:var(--dur-none) !important; scroll-behavior:auto !important;
    }
  }
  .page-title{ font-family:var(--font-display); font-size:var(--type-page); font-weight:600; margin:0 0 var(--space-1); }
  .page-sub{ color:var(--text-dim); font-size:var(--type-body); margin:0 0 var(--space-3); }
  /* WHY THIS WENT FROM 660 TO 1240.
     Matt, 25 Aug: "Fix all the cards (not just these) to be wide like in other
     areas. (should be site wide for consistency)."
     .maxw is worn by exactly two views — Projects and Timeline — and it was
     pinning both to 660px while every other page in Lucid runs to 1240
     (.pulse-two, .media-listen, .pj-home-queue, .today-grid). That is why those
     two screens looked like a phone layout left open on a monitor. One number,
     and it is the number the rest of the app already uses. */
  .maxw{ max-width:1240px; }
  /* Same reasoning: 11px uppercase at weight 400, with letter-spacing pulling
     the strokes further apart, rendered at roughly half its nominal contrast.
     700 costs nothing and is what makes a small label readable. */
  /* --text-faint KEEPS LANDING ON THE FLOOR RATHER THAN UNDER OR OVER IT.
     Four separate elements measured between 4.40 and 4.50 against a 4.50
     requirement on 20 September — this label, .wl-panel-note, .sec-menu and
     the inbox timestamp — which is not four coincidences, it is one token
     tuned to exactly the line and then asked to sit on grounds of slightly
     different brightness. Whether each one passes is decided by the ground,
     which is the same as deciding it by luck.
     Lifted one step at the call sites rather than by moving the token, which
     a great deal that already passes also depends on. --text-faint itself is
     worth a deliberate look in daylight; it is named here so the next person
     to meet this has the pattern rather than a single odd reading. */
  .section-label{ font-size:var(--type-label); font-weight:700; text-transform:uppercase; letter-spacing:0.09em; color:var(--text-dim); margin:var(--space-5) 0 var(--space-2); }
  .section-label:first-of-type{ margin-top:0; }

  
  
  
  /* Below the mobile breakpoint both views go back to normal document flow
     and scroll, so the fill-the-window behaviour must be switched off. */
  @media (max-width:780px){
    #view-inbox.view.active, #view-today.view.active{ display:block; height:auto; }
    
  }
  @media (hover:none) and (pointer:coarse) and (max-height:600px){
    #view-inbox.view.active, #view-today.view.active{ display:block; height:auto; }
    
    .inbox-wrap{ height:auto; }
  }

  
  
  /* AND THE ORB'S KEEP-OUT ARRIVES WITH THE STACK. (Row 2022.)
     Above 1100px the agenda is a 288px rail down the LEFT of .today-grid and
     the sphere cannot reach it. The moment those columns stack — the same
     1100px breakpoint, thirty lines up — the rail becomes the full width of the
     page, and its ⋯ button, 28px pinned to the row's own top-right corner,
     lands in the sphere's: measured at 1024×768, three quarters of it covered
     and nine presses out of nine landing on the orb.
     THE PADDING GOES ON THE SCROLLER RATHER THAN ON .agenda-act, and that is
     the whole of the trick. The button is absolutely positioned against the
     row, so moving the button alone would leave 82px of hole inside the row it
     is the corner of; narrowing the scroller takes the row's right edge with it
     and the button stays exactly where it belongs.
     IT IS WRITTEN HERE RATHER THAN BESIDE .today-grid because the base rule
     above sets `padding` whole. Two declarations of equal specificity, and a
     media query adds none — so the shorthand would have won wherever it was
     written later in the file, which is how the first attempt at this quietly
     did nothing at all. */
  
  
  
  
  
  
  
  
  
  
  
  
  
  

  
  
  
  

  
  
  
  
  /* THE FLAG STOPPED BREATHING. 265 — "any other circus animation of icons."
     A flag is a fact about a row: somebody marked it, at some point, and it is
     no more urgent this second than it was an hour ago. It was fading to 35%
     and back for ever, which is the same signal the overdue alarm uses — so
     the one mark in the app whose whole job is to be noticed was competing
     with a bookmark. Same argument as the matte pass (215): the glow came off
     everything except the one alarm. The blue still says flagged. */
  .flag{ color:var(--blue); }
  
  
  
  

  .mini-link{ font-size:var(--type-label); color:var(--blue); margin-top:var(--space-1); display:inline-block; cursor:pointer; }
  .mini-link:hover{ text-decoration:underline; }

  /* A NAME YOU CAN PRESS — item 521.
     ------------------------------------------------------------------------
     "I hate you cant drill directly into projects or boards, or anything...
      HATE IT"                                                       (Matt.)

     The rule the item asks for is that a thing named on screen opens THAT
     THING, and the doors that answer it are spread over a dozen renderers:
     a project pill in the task drawer, a company on a contact card, a task
     title in the rail, a sender in the reading pane. Every one of them went
     out with an inline style="cursor:pointer" because this file belonged to
     somebody else that night. This is the class they should always have had.

     WHAT IT DELIBERATELY DOES NOT DO is make them all look like links. These
     names are already the loudest text in the thing they sit in — a project
     name IS the pill, a task title IS the row — so a blue underline on every
     one of them would repaint half the app to say something the layout has
     already said. It changes the CURSOR always, and shows the underline only
     under the pointer, which is the affordance a person actually reads: the
     hand appears, then the line, then they press.

     The underline is drawn from --text-dim rather than --blue so it inherits
     whatever colour the name already has in this theme — a door on a done
     subtask stays dimmed, a door on a pill stays a pill. Nothing here is a
     hex; every value resolves through the theme's own tokens. */
  .lucid-door{ cursor:pointer; }
  .lucid-door:hover{ text-decoration:underline;
    text-decoration-color:color-mix(in srgb, var(--text-dim) 62%, transparent);
    text-underline-offset:2px; }
  /* Keyboard reachability is NOT claimed here. These are spans, not buttons,
     and giving them a focus ring without a tabindex and an Enter handler would
     be a style that promises something the markup cannot do. The ring is for
     the ones that DO carry tabindex, so the two never disagree. */
  .lucid-door[tabindex]:focus-visible{ outline:2px solid var(--ring); outline-offset:2px; border-radius:var(--radius-control); }
  /* On a pill the underline reads as damage — a rounded chip with a line
     through half its text. The chip itself lifts instead. */

  
  
  
  
  
  
  

  
  .mini-cal-head{ display:flex; align-items:center; gap:var(--space-2); margin-bottom:var(--space-2); }
  /* THE TWO ARROWS ON THE MINI-CALENDAR, AND WHY THEY ARE HEAVIER THAN THEY
     LOOK LIKE THEY NEED TO BE.
     A single ‹ at 13px in --text-dim measures 2.19:1 against the panel behind
     it in dark and 1.81:1 in linen — under the 4.5:1 floor, at full size, and
     worse again at the Small interface size where it is rendered at 10.7px. The
     cause is not the token: a chevron is ONE hairline stroke, thinner than a
     device pixel, so no pixel of it ever receives the nominal colour and every
     one is a blend with the paper. Darkening the token alone moves the number
     in the stylesheet and almost nothing on the glass — the same finding the
     readability harness recorded about the calendar's weekday letters.
     What fixes it is more ink: full --text rather than --text-dim, a heavier
     weight, and a size that survives being scaled to 82%. Found by
     tests/test-ui-scale.js, which measures both sizes and compares them. */
  .mc-nav{ width:20px; height:20px; border-radius:var(--radius-control); display:flex; align-items:center;
    justify-content:center; color:var(--text); cursor:pointer; font-size:var(--type-lead); font-weight:700; }
  .mc-nav:hover{ background:rgba(120,120,140,.10); color:var(--text); }
  .mc-label{ font-size:var(--type-body); font-weight:700; flex:1; }
  
  
  /* NINE PIXELS IS TOO SMALL TO HAVE A COLOUR.
     After --text-faint was darkened to a proper 4.78:1, these letters still
     measured 1.4:1 on screen — because at 9px the strokes are thinner than a
     pixel and every pixel is an antialiased blend of text and paper. The
     nominal contrast was fine; nothing was rendering at it.
     Bigger and bolder is the fix, not darker still. Weight is what small text
     actually needs: it puts more ink in each glyph rather than making the same
     one-pixel smear darker. */
  /* AND THEN THE GROUND MOVED UNDER THEM. Putting the calendar panels on glass
     — which is what he asked for — raised the surface luminance behind this
     whole rail, and every "faint" thing sitting on it lost about half a point
     of contrast: the weekday letters to 4.13:1, the neighbouring month's dates
     to 4.33:1, the hour labels to 3.72:1 on dark. All of it nominally
     unchanged, none of it touched, all of it now failing.
     The fix is the HIERARCHY, not a brighter faint: the in-month dates go up to
     full text colour (which is what the note below always claimed they were),
     so the out-of-month ones can sit at dim and still read as another month.
     One step lighter each, the same three ranks, and everything clears 4.5. */
  .mc-weekdays{ display:grid; grid-template-columns:repeat(7,1fr); font-size:var(--type-label); font-weight:700; color:var(--text-dim); text-align:center; margin-bottom:var(--space-1); }
  /* SEVEN COLUMNS THAT MAY BE NARROWER THAN THEIR CONTENTS.

     MEASURED, real WebKit at 393 x 852:

         at 100%   nothing on the Today screen hangs over an edge
         at 115%   the month grid hangs 11 points past the right edge,
                   taking the whole Saturday column with it

     WHY, AND IT IS THE 44x44 WORK ITSELF. The comment further down this
     file that raised .mc-cell to min-height:44px says, in as many words,
     that the rail is about 398px wide AT 430. It was measured at 430 and
     applied everywhere. A cell with aspect-ratio:1 and min-height:44px has
     a smallest possible width of 44 as well, and 1fr is short for
     minmax(auto, 1fr) - the auto being a floor at the contents smallest
     width. Seven 44s and six 1px gaps is a 314px floor for the grid, and at
     115 percent there is nowhere near 314px of rail to put it in.

     minmax(0, 1fr) removes the floor from the COLUMN, not from the cell.
     min-height:44px is untouched, so the cells are still 44 tall - taller
     than 44 real points once the 115 percent scale is applied - and they
     are now as wide as a seventh of whatever rail there is, instead of
     insisting on 44 and shoving the last column off the phone. Both
     measurements are in claude/PL3-25-SEPT-THE-SCREENS-ARE-NOT-SLOW-THE-OPENING-IS.md.

     This is the whole shape of his complaint in one rule: the thing that
     made the buttons pressable is the thing that crowded the screen. */
  .mc-grid{ display:grid; grid-template-columns:repeat(7,minmax(0,1fr)); gap:1px; }
  /* 30px was already modest; in a 316px rail the seven columns land near 40px
     and the aspect-ratio would make them 40px tall, which is a lot of month for
     a glance. Capped so the rail stays about the agenda. */
  /* WEIGHT CARRIES "THIS MONTH", NOT DIMNESS. 20 September.
     At the Small size the app is drawn at 82%, so these numerals land at 9px,
     and at 9px antialiasing eats contrast that measures fine at full size.
     Out-of-month days were --text-dim and came out at 2.64:1 on light and
     3.51:1 on dark — the same fault as the .28 opacity they were rescued from
     in the first place, arriving by a different road. A px floor is no
     defence here: the size control is a zoom, so a min() in the stylesheet is
     zoomed along with everything else.
     So the distinction stops being colour. This month's days are heavier,
     last month's are plain, and both are legible. The load shading above
     still goes to 600 and 700, which stays clear of this. */
  .mc-cell{ aspect-ratio:1; max-height:34px; display:flex; align-items:center; justify-content:center; font-size:var(--type-label); font-weight:600; border-radius:var(--radius-control); cursor:pointer; position:relative; color:var(--text); }
  .mc-cell:hover{ background:rgba(120,120,140,.10); }
  .mc-pad{ color:var(--text); font-weight:500; cursor:default; }
  /* OPACITY WAS THE WRONG LEVER, in both modes. At .28 the neighbouring
     month's dates measured 1.34:1 on midnight and 1.4:1 on cream — they were
     not dimmed, they were gone; and at the ~.9 it would take to pass they no
     longer read as another month at all. The distinction is now the TOKEN:
     faint against the in-month cells' full text colour. Readable, and a
     clearer signal than a wash. */
  .mc-today{ background:color-mix(in srgb, var(--blue) 16%, transparent); color:var(--text); font-weight:700; box-shadow:0 0 0 1px color-mix(in srgb, var(--blue) 40%, transparent); }
  .mc-selected{ background:linear-gradient(135deg,var(--green),var(--green-2)); color:var(--ink); font-weight:700; box-shadow:0 0 12px var(--green-glow); }
  /* The selected day is the primary-button recipe in a 30px square, and it
     fails for the same reason: dark ink over a fill that fades to its pastel.
     Same inversion — the fill deepens, the numeral becomes the paper colour. */
  :root[data-mode="light"] .mc-selected{
    background:linear-gradient(135deg,var(--green),color-mix(in srgb, var(--green) 82%, var(--neutral-2)));
    color:var(--on-accent);
  }
  :root[data-mode="light"] .mc-selected .mc-dot{ background:var(--on-accent); }
  /* HOW BUSY EACH DAY IS, AS SHADE. The month used to be a grid of identical
     grey numbers with a dot under the days that had anything at all — which
     told you nothing about the difference between a fifteen-minute call and a
     day with six hours booked. The level is computed from real hours, see
     dayLoad().

     SPECIFICITY: these are attribute selectors at (0,2,0) and .mc-today is a
     bare class at (0,1,0), so without the two rules underneath, heat would
     paint straight over today and over the selected day. Same trap the orb
     shapes fell into. */
  .mc-cell[data-load="1"]{ background:color-mix(in srgb, var(--accent) 9%, transparent); color:var(--text); }
  .mc-cell[data-load="2"]{ background:color-mix(in srgb, var(--accent) 17%, transparent); color:var(--text); }
  .mc-cell[data-load="3"]{ background:color-mix(in srgb, var(--accent) 27%, transparent); color:var(--text); font-weight:600; }
  .mc-cell[data-load="4"]{ background:color-mix(in srgb, var(--accent) 38%, transparent); color:var(--text); font-weight:700; }
  .mc-cell.mc-today[data-load]{ background:color-mix(in srgb, var(--blue) 16%, transparent); }
  .mc-cell.mc-selected[data-load]{ background:linear-gradient(135deg,var(--green),var(--green-2)); color:var(--ink); }
  /* THE SAME INVERSION AS THE RULE TWENTY LINES UP, AND IT WAS MISSING HERE.
     5 September, found by tests/test-readability.js while two new light themes
     were being measured — and it was failing on the four light themes already
     shipped, not on the new ones. The light-mode fix above deepens the fill AND
     lightens the numeral. This variant restored the PASTEL fill (same
     specificity, later in the file, so it wins the tie) and the light-mode line
     under it only ever put the colour back — which left the paper-coloured
     numeral sitting on the pastel end of a green gradient at 2.69:1 on Serenity
     Mist, 2.74 on Light, 2.95 on Blush and 3.86 on Alabaster.
     It is today's date with something in the diary on it, which is why nobody
     had seen it: the cell only takes data-load on a day that has events, so
     the bug is invisible on an empty calendar and unmissable on a real one. */
  :root[data-mode="light"] .mc-cell.mc-selected[data-load]{
    background:linear-gradient(135deg,var(--green),color-mix(in srgb, var(--green) 82%, var(--neutral-2)));
    color:var(--on-accent); }
  .omni-row.omni-ask{ border:1px solid var(--glass-border-hi); border-radius:var(--radius-card);
      background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent);
      margin-bottom:var(--space-2); }
  .omni-row.omni-ask .omni-t{ font-weight:700; }
  
  
  
  
  
  .mc-dot{ position:absolute; bottom:2px; width:3px; height:3px; border-radius:var(--radius-round); background:var(--blue); }
  .mc-selected .mc-dot{ background:var(--ink); }
  
  
  
  
  

  /* THE FORECAST, ON THE DAY IT BELONGS TO. See wxDayChipHtml().
     Deliberately quiet: it is context for the things on the day, not a thing
     on the day, so it sits at the faint end of the type scale and never
     competes with an event title. */
  .wx-day{ display:inline-flex; align-items:center; gap:var(--space-1); font-size:var(--type-body); color:var(--text-faint);
      font-family:var(--font-mono); white-space:nowrap; margin-left:var(--space-2); }
  .wx-day .wx-ico{ width:13px; height:13px; display:block; opacity:.85; }
  .wx-day .wx-ico svg{ width:100%; height:100%; display:block; }
  .wx-day .wx-lo{ opacity:.62; margin-left:var(--space-hair); }
  .wx-day .wx-rain{ color:var(--blue); margin-left:var(--space-1); }
  .wx-small{ font-size:var(--type-label); }
  .wx-small .wx-ico{ width:11px; height:11px; }
  
  

  
  
  
  
  
  
  
  
  

  
  
  
  
  

  
  
  
  
  

  
  
  
  
  
  
  
  
  
  
  
  
  
  

  /* ---------------------------------------------------------------------
     CALENDARS WORTH SUBSCRIBING TO. Collapsed to one row by default — it is
     a thing you use twice a year, and a permanently open list of holidays
     above the feed would be furniture.
     --------------------------------------------------------------------- */
  .cal-picks{ margin-bottom:var(--space-4); }
  .cal-picks-head{ display:flex; align-items:baseline; gap:var(--space-3); width:100%; text-align:left;
      background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:var(--radius-card);
      padding:var(--space-3) var(--space-4); cursor:pointer; color:var(--text); font:inherit;
      transition:border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
  .cal-picks-head:hover{ border-color:color-mix(in srgb, var(--accent) 45%, transparent); }
  .cal-picks-title{ font-weight:700; font-size:var(--type-body); }
  .cal-picks-sub{ font-size:var(--type-body); color:var(--text-faint); flex:1; }
  .cal-picks-chev{ color:var(--text-faint); font-size:var(--type-lead); line-height:1; }
  .cal-picks-body{ border:1px solid var(--glass-border); border-top:none;
      border-radius:0 0 var(--radius-card) var(--radius-card); padding:var(--space-1) var(--space-4) var(--space-4); margin-top:calc(var(--space-1) * -1); }
  .cal-pick-group{ margin-top:var(--space-3); }
  .cal-pick{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) 0;
      border-bottom:1px solid var(--glass-border); }
  .cal-pick:last-child{ border-bottom:none; }
  .cal-pick-body{ flex:1; min-width:0; }
  .cal-pick-name{ font-size:var(--type-body); font-weight:600; }
  .cal-pick-why{ font-size:var(--type-body); color:var(--text-faint); margin-top:var(--space-hair); }
  .cal-pick-btn{ flex:none; font-size:var(--type-body); padding:var(--space-2) var(--space-3); }
  .cal-pick-btn[disabled]{ opacity:.55; cursor:default; }
  .cal-how{ padding:var(--space-2) 0; border-bottom:1px solid var(--glass-border); }
  .cal-how:last-of-type{ border-bottom:none; }
  .cal-how-what{ font-size:var(--type-body); font-weight:600; margin-bottom:var(--space-1); }
  .cal-how-txt{ font-size:var(--type-body); color:var(--text-dim); line-height:1.55; }
  .cal-picks-note{ font-size:var(--type-body); color:var(--text-faint); line-height:1.55; margin:var(--space-3) 0 0; }

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  /* Anyone who has asked their machine to stop moving things gets the colour
     and none of the motion. The row is still obviously late. */
  
  
  
  
  

  /* THE COAT LINE. Row 1757, and the note above agendaWeatherLine() in app.js
     is where the argument for it lives.
     ------------------------------------------------------------------------
     IT IS NOT THE LATE BAR AND MUST NOT LOOK LIKE IT. The bar above this one
     is pink, tinted, bordered and loud, because it is an accusation — things
     you meant to have done. This is a courtesy: a sentence about a coat. So it
     borrows the day's own ink rather than a warning colour, sits on a hairline
     rule instead of inside a panel, and gives the whole of its weight to one
     small blue glyph. Two bars in a row both shouting would make neither of
     them worth reading, and this is the one of the two that appears on a good
     day.
     Blue, because --blue is what this app already draws rain and weather in —
     the day chip on the calendar header, the Home tile, the forecast row — and
     because a coat sentence in the accent colour would be competing with every
     other accented thing on the page for the same glance. */
  .agenda-wx-bar{ display:flex; align-items:flex-start; gap:var(--space-2); margin:0 0 var(--space-2);
    padding:0 var(--space-hair) var(--space-2); font-size:var(--type-body); line-height:1.45;
    color:var(--text-dim);
    border-bottom:1px solid var(--glass-border); }
  .agenda-wx-bar .awx-ico{ flex:none; width:16px; height:16px; margin-top:1px;
    color:var(--blue-2); }
  .agenda-wx-bar .awx-ico svg{ width:100%; height:100%; display:block; }
  /* ON PAPER THE GLYPH HAS TO STOP BEING THE LIGHT END OF THE BLUE, and this is
     the same correction — and the same reason — as the late bar three rules
     above. --blue-2 is the LIFTED step of the accent, drawn to sit on a dark
     ground; on the paper themes it is a pale sage against an off-white panel
     and the icon all but disappears, which was visible the moment the light
     screenshot was taken beside the dark one. --blue is the full-strength step
     and is what those themes tint everything else with. */
  :root[data-mode="light"] .agenda-wx-bar .awx-ico{ color:var(--blue); }
  /* The sentence names a real meeting and a real hour, and the two proper
     nouns in it — what it is and where — are the part he is scanning for. One
     step up in weight and into full-strength ink does that without a second
     colour. */
  .agenda-wx-bar .awx-said{ min-width:0; }
  .agenda-wx-bar strong{ color:var(--text); font-weight:600; }

  
  
  @keyframes pulseGlow{ 0%,100%{box-shadow:0 0 14px var(--pulse-lo);} 50%{box-shadow:0 0 24px var(--pulse-hi);} }
  
  

  /* INBOX */
  .inbox-wrap{ display:flex; gap:var(--space-4); flex:1; min-height:0; }
  /* ======================================================================
     ROW 2166 — THE FOLDERS ARE A ROW ACROSS THE TOP OF THE TRAY.
     ======================================================================

     "Hate the new spine with the crooked email icons under...looks weird. Need
      other options. How about we take your email icons (write, mail, sent,
      draft, archive, snoozed and trach and make the icons a row right above the
      inbox tray. (we had a couple written out before and I did not like, but
      love the current icons)"               (Matt, 15 September 2026.)

     THREE SHAPES IN FIVE DAYS, AND THIS IS THE FOURTH WORD ON IT. A dropdown
     reading "Inbox ▾" at the top of the list column (before row 1725); a 60px
     vertical rail standing between the spine and the tray (1725); a 24px spine
     sliver that expanded on hover (2139, removed the same week — it covered the
     rail it was making room for); the folders folded into the spine on a spur
     (2147, removed the next day — the sentence above). What every one of those
     was trying to solve is that two narrow columns of round icons on one left
     edge do not say which is the application and which is the mailbox.

     A ROW SOLVES IT BY NOT BEING A COLUMN. There is one vertical rail on the
     screen again — the spine — and the mailbox's own controls run the other way
     across the head of the tray. Nothing has to be told apart by size or by
     glass any more, because nothing is parallel to anything.

     THE ICONS DID NOT CHANGE. "love the current icons": the same seven
     controls, the same glyphs, the same order — the pencil, then Inbox, Sent,
     Drafts, Archive, Snoozed, Trash. Everything below is about direction and
     distance. Nothing below picks an icon.

     WHERE IT IS: the first child of .mail-list-col, so it is exactly the tray's
     width and directly above the first message. It is not a full-width band
     across .inbox-wrap, which would take fifty pixels of height off the LETTER
     to carry controls that do nothing to the letter, and it is not above the
     toolbar, which would put a list of destinations above the page's own search
     box. It is BELOW the All / Unread / Has-attachments chips because those are
     page furniture and live in .mail-toolbar-full with search and the bulk
     actions; a folder is not a filter and is not filed with them.

     THE SIZES, AND THE ONE PIECE OF ARITHMETIC THAT DECIDES THEM. The tray is
     clamp(300px, 34vw, 400px), so the narrowest this row is ever asked to be is
     300px, and inside --space-3's gutters that is 276px of room. Compose (36) +
     the hairline and its margins (1 + 16) + six folders (216) is 269. So 36px
     squares fit at every width the tray has, with seven pixels to spare at the
     worst of them. 46px — the spine's square, which this borrowed while it
     stood beside the spine — comes to 339 and does not fit; it is still the
     number on a phone, where the tray is the whole window and a finger is doing
     the pressing.

     AND THE AIR BETWEEN THEM IS SPACE-BETWEEN, NOT A GAP, which is why both
     gaps here are nought rather than --space-hair. A gap is a floor under the
     row's width: two pixels between six folders is ten more pixels the row must
     have before it is allowed not to wrap, and at a 300px tray it does not have
     them — the row broke onto three lines at an 800px window with a two-pixel
     gap on it. Written as space-between the six destinations take whatever the
     pencil and the hairline leave and share it out, which is about eighteen
     pixels each at a 400px tray and about one at 300, and the row never wraps
     on a window with a spine in it. flex-wrap is still underneath as the
     safety valve for a phone at 320px.

     WHICH FOLDER YOU ARE IN IS A BAR, AND IT IS NEVER A RING. This is row
     2147's one surviving idea. The spine says which PAGE with an inset ring
     round a square; if the current folder also wore a ring, two different
     statements would be drawn in one shape and the screen would be saying "you
     are here" twice without saying what either "here" is. So the folder is a
     2px bar in the area's colour along the bottom edge of its square, landing
     on the hairline that closes this row off from the messages below it — the
     same relationship the spur and the bar had in the spine, turned through
     ninety degrees with everything else.

     NO WORDS, NO EXPAND, NO HOVER-ONLY ANYTHING. Row 1725's rules are
     unchanged: the floating #railTip is the only text and it appears beside the
     button rather than in it, nothing here grows under the pointer, and every
     one of the seven is a real <button> in the tab order with a 2px focus ring
     of its own. Row 2139 was removed for hover behaviour and this row does not
     reintroduce any.

     THE AREA COLOUR IS --area-now, because mail is one of the three rooms that
     wear it in the spine — Home, Today and Mail all carry --c:var(--area-now)
     a thousand lines up. A row of six mail folders lighting up in the accent
     would be the one place in the app where mail is not that colour.
     ====================================================================== */
  .folder-rail{ --c:var(--area-now); position:relative; flex:none;
    display:flex; flex-direction:row; align-items:center;
    padding:var(--space-2) var(--space-3); gap:0; }
  /* THE HAIRLINE THIS ROW SITS ON. It is the spur from row 2147 laid flat: it
     runs the width of the row, it is what the bar under the current folder
     lands on, and it is what separates seven controls from the list of messages
     they change. Inset to the gutters so it lines up with the message rows
     rather than with the tray's own edge. */
  .folder-rail::after{ content:''; position:absolute; left:var(--space-3);
    right:var(--space-3); bottom:0; height:1px; border-radius:var(--radius-tight);
    background:color-mix(in srgb, var(--c) 28%, var(--glass-border)); }
  /* The six destinations take whatever the pencil and the hairline leave and
     spread themselves across it, so the row ends flush with the tray at every
     width instead of bunching at the left with a gap on the right. flex-wrap is
     the safety valve underneath that, and it is the same one the phone uses. */
  /* The hairline that separates making from going — row 1729. On a row it is a
     vertical rule, which says the same thing turned the same ninety degrees as
     everything else here. */
  .rail-btn{ --c:var(--area-now); position:relative; flex:none; width:36px; height:36px;
    display:flex; align-items:center; justify-content:center; padding:0; border:0;
    border-radius:var(--radius-control); background:transparent; color:var(--rail-ink-2); cursor:pointer;
    font-family:inherit; -webkit-tap-highlight-color:transparent;
    transition:background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out),
               box-shadow var(--dur-fast) var(--ease-out); }
  .rail-ico{ width:20px; height:20px; stroke-width:1.7; opacity:.8;
    transition:opacity var(--dur-fast) var(--ease-out); }
  .rail-btn:hover{ background:var(--wash-hover); color:var(--rail-ink);
    box-shadow:inset 0 0 0 1px var(--glass-border); }
  .rail-btn:hover .rail-ico{ opacity:1; }
  /* THE CURRENT FOLDER, SAID ONCE AND SAID WITH A BAR. A wash under the square
     so it reads at a glance, and the bar on the hairline so it cannot be
     confused with the spine's ring. Not a wash AND a bar AND a ring AND a glow,
     which is how a rail starts looking like a demo of a rail. */
  .rail-btn::before{ content:''; position:absolute; left:var(--space-1);
    right:var(--space-1); bottom:calc(-1 * var(--space-2)); height:2px;
    border-radius:var(--radius-tight); background:transparent; }
  .rail-btn.active{ background:var(--wash-on); color:var(--rail-ink); box-shadow:none; }
  .rail-btn.active::before{ background:var(--c); }
  .rail-btn.active .rail-ico{ color:var(--c); opacity:1; }
  .rail-btn:focus-visible{ outline:2px solid var(--c); outline-offset:1px;
    background:var(--wash-hover); color:var(--rail-ink); }
  .rail-btn:focus-visible .rail-ico{ opacity:1; }
  /* ROW 1829 — A DOT, NEVER A NUMBER. The argument is written out over
     renderFolders() in app.js; the short version is that mailData holds about
     the newest 25 messages per folder, so any figure printed here would be a
     count of what happens to be in memory wearing the clothes of a fact about
     the mailbox. The same 5px circle in the same --c the spine's inbox dot
     uses, and it does not breathe: an animation is a request for attention,
     and "you have mail" is a fact rather than an alarm. It survived the move
     from the column to the row with its corner and its meaning intact. */
  .rail-dot{ position:absolute; top:5px; right:5px; width:5px; height:5px; border-radius:var(--radius-round);
    background:var(--c); }
  /* The pencil, squared up with the folders beside it. Everything else about it
     — the accent tint, the hairline, the glow that only arrives on hover — is
     .tb-compose further down and is unchanged.
     Written as .tb-compose.rail-compose rather than .rail-compose on its own,
     and that is not tidiness. .tb-compose sets 36x34 and a 10px radius, it is
     declared several hundred lines further down this file, and two selectors of
     equal weight are settled by which comes last — so a plain .rail-compose was
     losing every one of its three declarations to it and the pencil was drawing
     at 34px beside the folders. Caught by tests/test-the-folder-rail.js, which
     measures the box rather than reading the rule. */
  /* THE TRAY GIVES WAY BEFORE THE LETTER DOES.
     It was flex:none at exactly 400px, which was fine while the mail screen
     was two columns and stopped being fine the moment row 1725 put a third one
     beside it: at 820px with the text size at 125% the rail's sixty pixels came
     straight off the reading pane, and the pane cannot go below the width of
     its own action strip, so fifteen pixels of it hung outside the window.
     Measured by tests/test-no-h-overflow.js, which is the only reason it was
     caught — the same layout looks perfect on a 1500px screen.
     So the tray is now allowed to shrink, down to 300px and no further, and it
     is the FIRST thing asked to: the reading pane's flex-basis is 0, so a flex
     row with too little room takes the deficit out of this column rather than
     out of the letter.
     The clamp is the other half, and it is about how the screen READS rather
     than about overflow. A rigid 400px tray on a 900px laptop left the letter
     292 pixels wide — the subject wrapped onto two lines and the sender's date
     onto three, which is the mail screen looking cramped rather than dense. The
     tray takes a third of the window instead, so the letter gets the room the
     rail took and the balance stays the same shape as the window narrows. Above
     about 1180px the clamp is already at its ceiling and the tray sits at 400,
     which is exactly what it has always done. No media query: a breakpoint here
     would snap the column by sixty pixels at one width and hold it still
     everywhere else, and there is nothing about this layout that happens at a
     particular size. */
  .mail-list-col{ flex:0 1 auto; width:clamp(300px, 34vw, 400px); min-width:300px;
    display:flex; flex-direction:column; min-height:0; }

  /* THE TRAY PAYS FOR THE FOLDER LIST, NOT THE LETTER. Opening the spine
     takes 240px out of .inbox-wrap. Taken off the reading pane it would
     leave 346px to read an email in at the width he actually uses; taken
     off the tray, which is a list of one-line rows and has 300px of useful
     width in it, nothing is lost. Measured both ways in
     tests/test-mail-spine.js. This is also why the app's own navigation did
     NOT have to be pinned narrow to find the room. */
  .inbox-wrap.spine-open .mail-list-col{ width:clamp(300px, 26vw, 340px); }

  .msp-group{ display:flex; flex-direction:column; }
  /* The mailbox name, and it is a label rather than a heading rank: the
     page already has one h1 and these are not sections of the document. */
  .msp-group-head{ font-size:var(--type-label); font-weight:600;
    letter-spacing:.04em; text-transform:uppercase; color:var(--rail-ink-2);
    opacity:.75; padding:var(--space-2) var(--space-3) var(--space-1);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .msp-row .rail-ico{ flex:none; }
  /* min-width:0 with the ellipsis is what stops a long folder name pushing
     its own count off the end of the row -- the fault a sibling worker is
     fixing on Home tonight, and not one to re-create here. */
  .msp-label{ flex:1 1 auto; min-width:0; white-space:nowrap;
    overflow:hidden; text-overflow:ellipsis; }

  /* WHERE YOU ARE, AND IT IS A FILLED ROW RATHER THAN A BAR OR A RING.
     The app spine says "this room" with a ring and the old folder row said
     "this folder" with a 2px bar; a third statement in either of those
     shapes would be a fourth thing to learn. A filled row is what every
     folder list in every mail client does, and it cannot be mistaken for
     the ring beside it. */
  .msp-row.active{ background:color-mix(in srgb, var(--c) 18%, transparent);
    color:var(--rail-ink); font-weight:600; }
  .msp-row.active .rail-ico{ opacity:1; }
  .msp-row.active::before{ display:none; }

  /* THE NUMBER SITS ON THE FOLDER IT COUNTS, right-aligned, and it is wide
     enough for a five-figure count and the plus that says there is more
     behind it -- 55,388+ is a real reading in this mailbox. Tabular figures
     so a column of them lines up instead of shimmering. */
  /* AND THE NUMBER IS NOT DIMMED EITHER. 9 October, measured in pixels on
     all twelve grounds: the counts came in at 4.09:1 on Hearth and 3.3:1 on
     the Serene papers, under the 4.5 floor on every ground in the app. The
     note above this one already made the argument -- "The distinction this
     rule exists to draw is a dash against a number; it was never the
     dimness" -- and it is the same argument here: what tells a count from a
     folder name is where it sits and how big it is, not how faint it is.
     Colour and position unchanged, nothing moves, and the active row still
     differs by taking --rail-ink. */
  .msp-count{ flex:none; margin-left:auto; font-size:var(--type-label);
    font-variant-numeric:tabular-nums; color:var(--rail-ink-2); opacity:1;
    white-space:nowrap; }
  .msp-row.active .msp-count{ color:var(--rail-ink); opacity:1; }
  /* NOT KNOWN YET, AND VISIBLY NOT A ZERO. A folder Lucid has never read is
     drawn with a dash. A folder it has read and found empty carries no
     number at all. Those are different facts and this is the one place in
     the app where they are told apart on sight. */
  /* THE FADE IS GONE AND THE DASH IS DOING THE WORK. opacity:.45 measured
     2.38:1 on Hearth and 1.89:1 on Serene Mist Soft -- so the one mark in the
     app that says "Lucid has never read this folder" was the least readable
     thing on the screen. The distinction this rule exists to draw is a dash
     against a number, as the note above says; it was never the dimness. At
     opacity 1 the dash is 5.27:1 on Hearth and 5.28:1 on Serene Mist Soft,
     and it is still unmistakably not a count. Colour only: the letter-spacing
     stays, the glyph stays, nothing moves. Active rows were already at
     opacity 1 through .msp-row.active .msp-count, so this changes the
     inactive rows and only those. */
  /* AND THE DASH IS GIVEN WEIGHT, WHICH IS THE ONLY THING THAT MOVED IT.
     An em dash at 11px is a hairline: its stroke is under a pixel tall, so
     no pixel ever receives the nominal colour and every one of them is a
     blend with the paper. That is the exact failure tests/test-readability.js
     was written about, and its lesson is that the fix is MORE INK rather than
     a darker token. Measured on Hearth: 2.20:1 at the regular weight, 4.24
     to 4.44:1 at 700, with no change of colour, glyph, size or column width.
     It was the worst-reading mark in the spine on all twelve grounds before
     tonight as well as after -- see tests/test-the-spine-reads.js. */
  .msp-count-unknown{ opacity:1; letter-spacing:.02em; font-weight:700; font-size:var(--type-body); }
  /* WHY THE DASH IS BIGGER AND BOLDER AND NOTHING ELSE ABOUT IT CHANGED.
     Weight alone took it from 2.20:1 to 4.3:1 on Hearth but only to 3.21:1
     on the Serene blush papers, still under the floor on nine of the twelve
     grounds. The size is the rest of the same lever: at the body scale,
     beside the label it sits next to, it measures over 4.5 on every ground
     in the app. Same glyph, same colour, same column -- .msp-count is
     flex:none against a label that is flex:1 1 auto with min-width:0, so a
     wider mark is taken out of slack and never out of a folder's name;
     tests/test-mail-spine.js and tests/test-the-folder-rail.js both measure
     that and both still pass. */

  .msp-row .rail-dot{ position:static; flex:none; margin-left:var(--space-1); }

  /* ONE GROUP PER MAILBOX, AND THEY NEED SEPARATING. #mailSpineAccounts is a
     single child of .msp-inner, so .msp-inner's gap holds it apart from All
     accounts and does nothing whatever between the mailboxes inside it. */
  #mailSpineAccounts{ display:flex; flex-direction:column; gap:var(--space-4); }

  /* THE SPUR. One mailbox, one unbroken line down the left of its folders --
     the mark that answers "which of these belong together" from the far side
     of the room, before a word has been read. */
  .msp-group{ position:relative; }
  .msp-group::before{ content:''; position:absolute; left:3px;
    top:var(--space-2); bottom:var(--space-1); width:2px;
    border-radius:var(--radius-tight);
    background:color-mix(in srgb, var(--c) 70%, transparent); }

  /* THE HEAD, BRIGHTER THAN IT WAS, AND IN THE TOKEN THAT IS ACTUALLY INK.
     It was --rail-ink-2 at opacity .75: a 10px uppercase label, which is the
     one size the readability check exists to catch, and it was the dimmest
     text in the spine.

     IT IS --text AND NOT --rail-ink, AND THAT IS NOT A PREFERENCE. The first
     version of this rule said --rail-ink, and the light theme's screenshot
     showed the three mailbox names as near-white lettering on near-white
     paper -- the same dark-ink-on-a-dark-slab fault that shipped in this app
     earlier today, turned over. --rail-ink and --rail-ink-2 are declared in
     the base :root, which is a DARK ground, and the light themes do not
     redeclare them; --text is redeclared by every theme and is the only
     token here that means "this paper's ink" on all of them. Caught by
     looking at the picture, not at the rule. */
  .msp-group-head{ color:var(--text); opacity:1;
    margin:0 var(--space-3) var(--space-1);
    padding:var(--space-2) 0 var(--space-1);
    border-bottom:1px solid color-mix(in srgb, var(--c) 45%, var(--glass-border)); }
  /* Inline-block rather than a flex item on purpose: .msp-group-head
     ellipsises a long mailbox name, and a flex container would not. */
  .msp-group-head::before{ content:''; display:inline-block; vertical-align:middle;
    width:5px; height:5px; margin-right:var(--space-2); position:relative; top:-1px;
    border-radius:var(--radius-round); background:var(--c); }

  /* ALL ACCOUNTS IS NOT A MAILBOX, SO ITS SPUR IS NOT IN A MAILBOX COLOUR.
     --area-now and --blue are the SAME VALUE on Hearth (#7C99C4), so an
     area-coloured spur over the All accounts group was drawn in exactly the
     colour a Gmail mailbox defaults to -- two different statements in one
     colour, which is the thing this whole block exists to stop. Only the
     three marks that answer WHICH MAILBOX go quiet here, because the honest
     answer for this group is "none of them". The rows keep --area-now: mail
     wearing its room's colour is a decision made a thousand lines up this
     file and this is not the place to undo it. */
  #folderRail > .msp-group::before{ background:color-mix(in srgb, var(--rail-ink-3) 60%, transparent); }
  #folderRail > .msp-group > .msp-group-head{ border-bottom-color:var(--glass-border-hi); }
  #folderRail > .msp-group > .msp-group-head::before{ background:var(--rail-ink-3); }

  @media (prefers-reduced-motion: reduce){
    .mail-spine, .msp-row{ transition:none; }
  }

  /* Full-width toolbar bar — sits above the whole inbox (list + reading pane)
     instead of being squeezed into the list column. Icon-first and dense —
     tooltips (via the generic [data-tooltip] system) carry the labels instead of
     permanent text, which is what actually makes a toolbar feel tight rather than webby.
     Buttons sized up from the original 30px — the old size read as fiddly next to
     everything else on the page. */
  /* NO RIGHT-HAND PADDING, AND THE GUTTER IS A FLEX ITEM INSTEAD. This looks
     like a pointless way to write `padding:6px 12px`, and it is the entire fix
     for "at some narrow widths the toolbar's buttons run past the bar's own
     right edge". The full argument is in the narrow-width block below, under
     THE TWELVE PIXELS fitMailToolbar COULD NOT SEE; the short version is that
     scrollWidth — the only thing fitMailToolbar() has to tell it the row does
     not fit — does not count content that spills into a box's own right
     padding, so twelve pixels of padding here bought twelve pixels of spill
     that nothing could detect. Padding-right of nought leaves scrollWidth with
     nowhere to hide the spill, and .tb-gutter puts the twelve pixels back as
     something the row has to make room for rather than something it can bleed
     into. */
  .mail-toolbar-full{ display:flex; align-items:center; gap:var(--space-hair); padding:var(--space-2) 0 var(--space-2) var(--space-3); margin-bottom:var(--space-2); flex-wrap:nowrap; flex:none; box-shadow:var(--glass-shadow); }
  .tb-select{ background:rgba(120,120,140,.10); border:1px solid var(--glass-border); color:var(--text-dim); font-size:var(--type-label); border-radius:var(--radius-control); padding:var(--space-1) var(--space-2); cursor:pointer; }
  /* ==========================================================================
     THE MAIL TOOLBAR AT A NARROW WIDTH.       "Mail toolbar overflows at narrow
     widths. Buttons run off the edge." — open since the toolbar was first
     built.
     --------------------------------------------------------------------------
     THE PREVIOUS ANSWER WAS flex-wrap:wrap, and it was a real fix for a real
     failure: nothing hung off the side any more, because the bar grew a
     second and third row instead. But a second and third row is still the
     toolbar overflowing — just downward, into the mail, rather than sideways
     off the glass — and #mailToolbarFull is deliberately overflow:visible (so
     the chevron's own dropdown is not clipped by it), which means wrapping
     was never optional: turn it off with nothing else in place and the row
     does not clip, it genuinely spills past the right edge of the bar, which
     is the exact bug this is named for.

     So wrapping is off for good, and what replaces it is fitMailToolbar() in
     app.js: every button below the search box and the filter chips carries a
     data-tb-collapse priority, and the moment the row would not fit them all
     on one line, the lowest-priority ones move into the chevron's own menu —
     the SAME menu Threads, Select and Tidy already live in — instead of
     wrapping onto a row of their own or hanging off the edge. Widen the
     window and they come back, in the same order, the moment there is room.
     See the note over fitMailToolbar for which button goes first and why.

     What is left here is the two things that were never about wrapping:

       THE SEARCH BOX had min-width:170px, which is a floor a flex item cannot
       shrink past — so once the row got tight, search stopped shrinking and
       pushed on whatever sat next to it. It shrinks to 120, then 88 on a
       phone, and takes the leftover space, which is what a search box should
       do — fitMailToolbar() only has to fit what search and the chips choose
       not to take for themselves.

       THE FOUR FILTER CHIPS are 300px of text that wraps into two rows of
       their own if left alone. Below 900 they become one row that scrolls
       sideways instead — safe on this element in a way it is not on the bar,
       because the chip strip has no dropdown menu hanging off it to clip.

     tests/test-attention-and-toolbar.js is the harness: it drives a real
     browser from 1500px down to 320px, with and without a selection, and
     checks the same three things the old version did — nothing sticks out of
     the bar's own box, the page never scrolls sideways, and the chevron menu
     still opens and reaches every control — plus the thing wrapping could
     never prove: that a button moved into the menu still does exactly what it
     did on the row.
     --------------------------------------------------------------------------
     THE TWELVE PIXELS fitMailToolbar COULD NOT SEE.        (236, 3 September.)

     The harness above went red on one line and one line only: at 500px, with
     mail selected, #tbBulkActions ended 8px past the right edge of the bar's
     content box, Delete with it. Every other sampled width passed, and both
     other modes passed, which is what made it look like a one-off.

     It was not a one-off. Sweeping the window four pixels at a time from 320
     to 1000 instead of at the eleven widths the harness samples, the same
     failure appears at 328, 364, 396, 432, 464, 496, 532, 564 and 596 — a
     band of about a dozen pixels recurring every 34, which is exactly one
     toolbar button plus its gap. The harness was not finding a width that
     happened to be unlucky. It was sampling a sawtooth and landing in the
     tooth once.

     THE CAUSE IS THE MEASUREMENT, NOT THE LAYOUT. fitMailToolbar() folds a
     button away while `bar.scrollWidth > bar.clientWidth + 1`, and that
     comparison cannot see the first twelve pixels of spill, because of how
     the two numbers are defined:

         clientWidth  = content width + padding-left + padding-right
         scrollWidth  = the overflow region measured from the PADDING box,
                        so content may run the whole width of the right
                        padding before it counts as overflow at all

     With `padding: 6px 12px` those cancel to "fold when the spill exceeds
     padding-right, plus the 1px slack in the comparison" — thirteen pixels of
     spill that the function is structurally blind to. Meanwhile the harness
     measures against the CONTENT box, with 1.5px of tolerance. The two
     thresholds are 11.5px apart, so every time the row's irreducible
     remainder lands in that gap — once per button-width of window, forever —
     the function reports a row that fits and the harness reports one that
     does not. Measured at 500px selecting: bar.scrollWidth 470,
     bar.clientWidth 470, and the bulk strip's right edge 8.8px past the
     content edge all the same.

     THE FIX IS TO STOP GIVING SPILL SOMEWHERE TO HIDE. padding-right becomes
     nought, so the padding box and the content box end at the same place and
     any spill at all raises scrollWidth; and the twelve pixels of clear space
     that padding used to draw come back as .tb-gutter, a real, unshrinkable
     10px flex item at the end of the row (10 + the bar's 2px gap = 12). The
     row now has to make room for the gutter the way it makes room for a
     button, so the bar keeps precisely the breathing space it always had, and
     the moment anything would eat into it fitMailToolbar() folds one more
     button into the chevron — which is what it was always meant to do and what
     it could not previously tell it needed to.

     The gutter is a real element in index.html rather than an ::after for one
     reason: the harnesses walk `bar.querySelectorAll('*')`, which does not
     return pseudo-elements. With padding-right at nought their "inside the
     bar" line moves out to the border, so without something real standing in
     that twelve pixels they would have gone twelve pixels laxer than before.
     The gutter is what they now catch being shoved out of the bar, so the
     assertion means the same thing it meant this morning.
     ========================================================================== */
  @media (max-width: 900px){
    /* Two class names, not one, and that is load-bearing: the base .mail-search
       rule is defined 4,000 lines further down the file, so a rule with the same
       specificity up here loses to it no matter what the media query says. This
       one is (0,2,0) and wins. */
    .mail-toolbar-full .mail-search{ flex:1 1 140px; min-width:120px; }
    /* THE INPUT ITSELF HAS A FLOOR TOO, and it was the one actually pushing the
       bar 48px wider than its own box at 620. An <input> is a flex item with
       min-width:auto, and its automatic minimum is the intrinsic width of a
       twenty-character text field — about 240px with this padding — so telling
       the WRAPPER it may shrink to 120 achieved nothing. */
    .mail-search-input{ min-width:0; }
    }
  @media (max-width: 430px){
    /* THE LAST FEW PIXELS. The floor drops rather than the count being
       hidden: a search box you can still type in is worth more than a wide
       one, and taking the count away would be taking away the only thing that
       says how many messages an action is about to touch — fitMailToolbar()
       folds buttons away first and only reaches this floor once there is
       nothing left to fold. */
    .mail-toolbar-full .mail-search{ flex:1 1 88px; min-width:88px; }
    /* AND AT 88px THE PLACEHOLDER IS A LIE — 16 SEPTEMBER. Photographed at
       430px (docs/mail-screen.html, shot 11): "Search all mail…" is clipped to
       the three letters "Sea". A word cut off mid-syllable reads as a broken
       layout, not as a hint, and it is the first thing the eye lands on at the
       top of the screen.
       The floor above is right and stays — a box you can type in beats a wide
       one. What goes is the text, not the box: the magnifier is already inside
       the field, and an icon with no caption is what every mail app on a phone
       shows. The full placeholder is still there for a screen reader, which is
       the half that actually needed the words. */
    /* AND IT NEVER TOOK EFFECT - 25 September, found while measuring the
       chips beside it. One class name, and the base rule for the same
       selector is 8,500 lines further down this file at the same specificity,
       so the later one wins and the placeholder has been fully coloured at
       phone width ever since. Photographed at 430px this morning: Search all
       mail is still showing as the three letters Sea, which is the exact
       thing the paragraph above says was taken away in September.
       This is the same trap, and the same fix, as the two-class note at the
       top of the 900px block. The rule is unchanged; only the selector is. */
    .mail-toolbar-full .mail-search-input::placeholder{ color:transparent; }
  }
  /* AND THE FOUR-PIXEL RAMP IS A LUXURY THIS ROW CANNOT AFFORD AT PHONE WIDTH.
     (The sweep, 12 September.) Putting the whole app on one spacing unit moved
     the mail toolbar's own furniture up a step — the filter chips from 6/10 to
     8/12, the selected-count from 6 to 8 — and the toolbar is the one row in
     Lucid where a handful of pixels is the difference between fitting and not.
     tests/test-mail-toolbar-fit.js caught it immediately: at 360px the compose
     pencil and the gutter were standing three and fifteen pixels outside the
     bar's own content box, because fitMailToolbar() had already folded
     everything foldable and there was nothing left to give.

     So at phone width these three come back down a step. That is not the sweep
     undoing itself: the ramp is the vocabulary and this is a sentence spoken in
     it, at one breakpoint, for a reason written down. The row is measured at
     eight widths from 1500 to 320 by the harness above, which is where this
     number came from and where it will be checked again. */
  @media (max-width: 430px){
    /* AND THE DISPLAY FACE IS WIDER THAN THE ROW WAS FITTED TO — 21 September,
       the merge. "Mail" is set in var(--font-display); that is Fraunces in
       live's work and was Newsreader on master. Measured at 320px with every
       message ticked: the word is 41.44px in Fraunces against 39.2px in
       Newsreader, and the gutter — the unshrinkable 10px at the end of the row
       whose whole job is to be the thing that gets shoved out first, see THE
       TWELVE PIXELS above — finished 3.44px outside the bar's own content box.
       Master finished 1.2px outside it, inside the 1.5px the checks allow, so
       this row has been within a whisker of spilling for some time and the
       typeface is only what pushed it over. fitMailToolbar() had already folded
       all eight collapsible buttons into the chevron, so there was nothing left
       for it to fold.
       Six pixels come back off the title's own margins, which is the only
       furniture on this row that is spacing rather than a control. Nothing is
       lost: the word keeps its size and its face, the row's own 2px gap still
       stands between it and the search box, and no button moves. Measured
       after, same width, same selection: the gutter ends 2.56px INSIDE the bar,
       which is more clearance than master ever had. */
    .mail-toolbar-full .tb-title{ margin-right:var(--space-1); margin-left:0; }
  }
  /* THE LABEL BUTTON IS A WIDE-WINDOW CONVENIENCE. Row 2220.
     It comes off the row before the row has to give up anything it cannot spare
     — the count that says how many messages an action will touch, or the gutter
     that keeps the last button inside the glass. Nothing is lost by it going:
     the same control is in the chevron at every width (see #tbLabelsItem), and
     an active label filter is named in the line above the list, with the press
     that clears it, whether this button is on screen or not. 500 rather than
     430 because the bulk strip is the widest state this row has and it appears
     the moment anything is ticked. */
  @media (max-width: 500px){
    #mailLabelWrap{ display:none; }
  }
  .mail-list{ width:auto; flex:1; overflow-y:auto; padding:var(--space-3); }
  .mail-row{ display:flex; gap:var(--space-3); padding:var(--space-3) var(--space-3); border-radius:var(--radius-card); cursor:pointer; align-items:flex-start; margin-bottom:var(--space-1); border-left:3px solid transparent; position:relative; touch-action:pan-y; overflow:hidden; }
  .mail-row:hover{ background:rgba(120,120,140,.08); }
  .mail-row.selected{ background:color-mix(in srgb, var(--row-color, var(--accent)) 16%, transparent); box-shadow:0 0 0 1px color-mix(in srgb, var(--row-color, var(--accent)) 35%, transparent); }
  /* THE PREVIEW LINE. This rule is the one that was left behind when the element
     was deleted — a `.mail-row.selected .mail-preview` with no base rule
     anywhere, which is the fingerprint of half a removal. The element is back
     and so is its own rule.

     min-width:0 and the ellipsis are load-bearing, not tidiness. A row is a
     flex column, and a flex item's implicit floor is its min-content width —
     one unbreakable run in a real Gmail snippet (a long URL, a tracking token)
     sets that floor to the length of the run and shoves the whole inbox
     sideways. That is the exact mechanism that pushed the right-hand column off
     the contact card in August.

     text-dim rather than text-faint: the readability harness measures every
     painted glyph against 4.5:1 and faint-on-glass does not clear it. */
  /* A PREVIEW LINE AND A TIMESTAMP ARE THE TWO THINGS A PERSON SCANS AN INBOX
     WITH, and on the dark themes both were sitting just under the readable
     floor — 4.40 and 4.49 at full size, 4.42 and 4.43 once the app is drawn at
     82%. Not "a bit quiet": measurably too faint, on the screen he uses most.
     Lifted one step each rather than by moving --text-dim or --text-faint,
     which are shared by a great deal that already passes. The ordering they
     had is kept: the preview stays brighter than the time. */
  .mail-preview{ font-size:var(--type-body); color:color-mix(in srgb, var(--text) 25%, var(--text-dim)); margin-top:var(--space-1);
    min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .mail-row.selected .mail-preview{ color:color-mix(in srgb, var(--text) 25%, var(--text-dim)); }
  /* 138 — a hair more than plain --text-dim, added when the app's scale
     control moved from `zoom` to `transform:scale()`. Zoom re-rasterizes text
     at the scaled size, so at Small (82%) this 10.5px mono timestamp draws
     crisp at a genuine 8.6px; transform draws it at full size and resamples
     the pixels down, which softens a stroke this thin just enough that
     test-ui-scale.js measured the selected row's copy of it under the 4.5:1
     floor (4.46:1) in dark — the one theme it was already closest to the
     edge in. Its own rule, split from .mail-preview above, because the same
     small nudge on that larger, denser preview text was not needed and would
     have been change for no reason. */
  .mail-row.selected .mail-time{ color:color-mix(in srgb, var(--text-dim) 82%, var(--text) 18%); }
  /* Anything that drills through to a record says so on hover. Without this an
     avatar is indistinguishable from decoration. */
  .avatar.is-jump{ cursor:pointer; transition:box-shadow var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); }
  .avatar.is-jump:hover{ transform:scale(1.06); box-shadow:0 0 0 2px var(--accent), 0 0 16px var(--accent-glow); }
  .new-badge.is-jump:hover{ text-decoration:underline; }
  .avatar{ width:38px; height:38px; border-radius:var(--radius-round); flex:none; background:linear-gradient(135deg, color-mix(in srgb, var(--blue) 35%, transparent), color-mix(in srgb, var(--green) 25%, transparent)); border:1px solid var(--glass-border-hi); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:var(--text); }
  .mail-row .avatar{ background:linear-gradient(135deg, color-mix(in srgb, var(--row-color, var(--blue)) 45%, transparent), color-mix(in srgb, var(--row-color, var(--blue)) 15%, transparent)); border-color:color-mix(in srgb, var(--row-color, var(--blue)) 40%, var(--glass-border-hi)); }
  /* The initials sit on the account colour at 45%, which on the brighter dark
     themes left them at 4.32:1. Eased to 36% — the account is still legible as
     a colour, and the two letters on top of it are legible as letters. */
  :root:not([data-mode="light"]) .mail-row .avatar{ background:linear-gradient(135deg, color-mix(in srgb, var(--row-color, var(--blue)) 36%, transparent), color-mix(in srgb, var(--row-color, var(--blue)) 12%, transparent)); }
  .avatar.lg{ width:120px; height:120px; font-size:38px; }
  /* On a dark room, mixing an accent toward TRANSPARENT lets the dark show
     through and the disc reads as lit glass. Over cream the same mix lets the
     paper through and two half-strength hues average into a grey-green
     smudge — the avatars were the muddiest thing on the Contacts page.
     Mixing toward white instead keeps the hue and turns the disc into tinted
     paper, which is the light-mode equivalent of the same idea. */
  :root[data-mode="light"] .avatar{
    background:linear-gradient(135deg,
      color-mix(in srgb, var(--blue) 26%, #fff),
      color-mix(in srgb, var(--teal) 20%, #fff));
    border-color:color-mix(in srgb, var(--blue) 24%, var(--glass-border-hi));
    color:var(--text);
  }
  :root[data-mode="light"] .mail-row .avatar{
    background:linear-gradient(135deg,
      color-mix(in srgb, var(--row-color, var(--blue)) 30%, #fff),
      color-mix(in srgb, var(--row-color, var(--blue)) 12%, #fff));
    border-color:color-mix(in srgb, var(--row-color, var(--blue)) 30%, var(--glass-border-hi));
  }
  .mail-body{ flex:1; min-width:0; }
  .mail-top{ display:flex; justify-content:space-between; gap:var(--space-2); }
  /* THE SENDER GIVES WAY, AND THE TIME STAYS ON THE SCREEN.

     Matt, 25 September: Regular size is tiny need to go to 115 to have
     decent buttons but others get crowded. This is the worst of the
     crowding, and it is on the screen he opens most.

     MEASURED, real WebKit at 393 x 852, the size of his phone, on the mail
     list, with an ordinary address like karen.whitfield at example.com:

         at 100%   the time sits 70 to 76 points past the right edge
         at 115%   the time sits 139 to 147 points past the right edge

     Eight rows out of eight, both times. The row is overflow:hidden, so the
     time is not pushed into view by scrolling - it is simply gone. In the
     picture the second row reads 9:5 and stops.

     WHY. .mail-top is a flex row holding .mail-from and .mail-time.
     .mail-time is flex:none, which is right - a half-printed time is worse
     than no time. So the one that has to give way is .mail-from, and it
     could not: a flex item defaults to min-width:auto, which for text means
     it will not shrink below its longest unbreakable run of characters. An
     email address has no spaces in it, so the whole address is one
     unbreakable run and .mail-from simply refused to get any narrower. The
     time was pushed out of the row and clipped.

     .mail-body one level up already carries min-width:0 for exactly this
     reason. This is the same lesson, one level further in.

     Nothing gets smaller. The address now trails off in an ellipsis when it
     runs out of room, which is what every mail list does, and the time is
     on the screen at both sizes. */
  .mail-from{ font-size:var(--type-body); font-weight:700;
    min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .mail-time{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim); flex:none; }
  .mail-empty{ text-align:center; color:var(--text-faint); font-size:var(--type-body); padding:var(--space-12) var(--space-5); }
  .mail-empty-act{ margin-top:var(--space-3); }

  .more-item.on{ color:var(--text); background:color-mix(in srgb, var(--accent) 14%, transparent); }
  .more-item em{ font-style:normal; font-weight:600; color:var(--text-faint); font-size:var(--type-label); }
  .reading-pane{ flex:1; padding:var(--space-6) var(--space-8); overflow-y:auto; }

  .tx-compose{ flex:none; display:flex; gap:var(--space-3); align-items:flex-end;
    padding:var(--space-3) var(--space-5); border-top:1px solid var(--glass-border); }
  .tx-compose textarea{ flex:1; resize:none; min-height:38px; max-height:120px; margin:0; }
  .tx-compose-note{ flex:none; padding:var(--space-4) var(--space-5); border-top:1px solid var(--glass-border);
    font-size:var(--type-body); color:var(--text-dim); line-height:1.5; }


          
  /* Snooze on the left, the dark/original toggle pushed to the right. Was a
     bare block with one child; now it has two and needs to say so. */
  /* Themed system messages. Matt: "everything is so beautiful and then we get
     this plain ugly card." These replace the native toast(, 'ok') for anything that
     isn't a genuine question, and — more importantly — they give the app a way
     to say "that didn't save" at all. Until now a failed write was reported
     only to the browser console, which nobody reads, so the screen kept showing
     the change and the next refresh silently undid it. */
  /* The checkbox that made every bulk action honest.
     toggleMailSelected and toggleSelectAllMail existed with zero callers, and
     no checkbox was ever rendered — so selectedMailIds was permanently empty
     and every toolbar button labelled as a bulk action silently operated on the
     single open email. It appears on hover or when something is selected, so
     the list stays clean until you reach for it. */
  /* SELECT MODE, rather than a checkbox that lurks on hover.
     Two earlier attempts were both wrong. The first animated a checkbox open on
     row hover, which slid every row sideways as the mouse crossed it — the
     shudder. The second put the tick on the avatar, which stopped the shudder
     but took away clicking an avatar to drill into that person.
     This is what Matt actually asked for, and it is what Mail on a phone does:
     nothing at all until you say "Select", then the whole list steps aside and
     a column of checkboxes appears in the gap. Off by default, so the ordinary
     inbox has no chrome in it whatsoever, and the avatar is only ever an
     avatar. */
  .mail-row{ transition: background var(--dur-fast) var(--ease-out),
             box-shadow var(--dur-base) var(--ease-out),
             border-color var(--dur-fast) var(--ease-out),
             padding-left var(--dur-base) var(--ease-out); }
  /* ===================================================================
     ROW 1730 — WHAT IS UNDER A ROW YOU ARE SLIDING
     ===================================================================
     "Slide to archive and delete is glitchy. Needs to track the finger and
      settle, not jump."                              (Matt, 11 September.)

     Sliding a row used to uncover the page. A row moving over nothing is a row
     that has come loose, and the only feedback was the row itself going faintly
     green — which it did at twenty pixels of travel while the archive did not
     fire until eighty, so for sixty pixels it looked armed and was not.

     This is the thing it slides off. One element, parked over whichever row is
     being dragged (see swipeTray in app.js) rather than two spans in every row
     of a thousand-row list. Delete on the left because a row swiped RIGHT
     uncovers its left; archive on the right for the same reason.

     --mst-reveal is nought to one across the run-up to the threshold, so the
     icon and the word grow the whole way rather than appearing at a number
     nobody can see, and .is-armed is the moment letting go would genuinely do
     something. Below it, dim and flat; at it, full strength and a filled
     ground. Nothing here is ever lit about an action that will not happen.

     Colours are the row's own account colour for archive — the same argument
     the gesture code already made about an orange Gmail row not flashing green
     — and --pink for delete regardless of whose mailbox it is, because that
     signal must not change meaning with the account. */
  /* position:relative so the tray can be laid over one row by offsetTop, and
     overflow-x:hidden because of a thing that was measured here and turned out
     to be part of "glitchy" all along.

     A row swiped RIGHT is translated up to a hundred pixels past the list's own
     right edge, and .mail-list is overflow-y:auto — which makes its computed
     overflow-x `auto` as well. Measured: mid-delete the list's scrollWidth went
     398 → 475 against a clientWidth of 398, so for the length of the gesture a
     mail list was a horizontally scrollable box. An archive swipe never did it,
     because overflow to the LEFT of a left-to-right box is not scrollable at
     all — which is exactly the sort of asymmetry that makes one direction feel
     worse than the other without anybody being able to say why.

     WHAT WAS AND WAS NOT MEASURED. The overflow is measured, and so is the
     asymmetry. Whether a scrollbar is actually PAINTED over it was not: this
     Chromium draws overlay scrollbars, and the list's clientHeight stayed 620
     either way, so the "a bar appears and takes height off the list" version of
     this is belief and not a reading. The overflow on its own is enough reason:
     a list that can be dragged sideways is a list that will be, and there is
     nothing out there to find.

     Clipped, not scrollable. A row leaving to the right now disappears at the
     column edge the same way a row leaving to the left already did. */
  .mail-list{ position:relative; overflow-x:hidden; }
  /* Two labelled buttons plus search plus four filter chips is more than fits
     on a narrow window, and adding Tidy is what tipped it over. Below 1200px
     they become icons and the tooltip carries the name — which is the same
     trade the toolbar icons already make. */
  @media (max-width: 500px){
    /* THE MENU CAME OUT OFF THE LEFT OF THE SCREEN.
       It is anchored to the chevron's right edge, which is fine on anything
       wide: right:0 under a button near the right of the bar. On a 320px screen
       the button sits at x≈194 and the panel is 264 wide, so it started at −44
       and its first 44 pixels were outside the window — the same "looks open and
       is not" failure the chevron already had once, in a different direction.
       Measured by tests/test-mail-toolbar-fit.js, which opens it at 320 and
       reads its box.
       Below this width it is anchored to the TOOLBAR instead of to the button
       and spans it with a margin either side. The whole change is which element
       is the containing block: the wrapper stops being positioned and the bar
       starts, so top:100% now means "under the whole toolbar", which is where a
       full-width menu belongs anyway. */
    .mail-toolbar-full{ position:relative; }
    }
  /* "All" only makes sense once you are selecting. */

  /* Tidy */
  .tidy-list{ max-height:52vh; overflow-y:auto; display:flex; flex-direction:column; gap:var(--space-2); }
  .tidy-row{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-3) var(--space-3); border-radius:var(--radius-card);
    background:var(--glass-bg); border:1px solid var(--glass-border); }
  .tidy-row.is-protected{ opacity:.62; }
  .tidy-pick{ flex:none; width:16px; height:16px; accent-color:var(--blue); cursor:pointer; }
  .tidy-sweepbar, .tidy-running{ display:flex; align-items:center; gap:var(--space-2); flex-wrap:wrap;
    margin-bottom:var(--space-3); padding:var(--space-2) var(--space-3); border-radius:var(--radius-card);
    border:1px solid var(--glass-border-hi); background:var(--glass-bg); font-size:var(--type-body); }
  .tidy-sweep-n{ font-weight:650; }
  .tidy-btn:disabled{ opacity:.4; cursor:default; }
  .tidy-running{ border-color:color-mix(in srgb, var(--blue) 40%, transparent); }
  .tidy-main{ flex:1; min-width:0; }
  .tidy-name{ font-weight:600; font-size:var(--type-body); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .tidy-sub{ font-size:var(--type-body); color:var(--text-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

  /* ---- Themed dialog --------------------------------------------------- */
  .dlg-body{ font-size:var(--type-lead); line-height:1.55; color:var(--text-faint); white-space:pre-wrap;
             margin-top:var(--space-hair); max-height:46vh; overflow-y:auto; overflow-wrap:anywhere; }
  /* MY SOURCE CODE'S INDENTATION, RENDERED ON HIS SCREEN.
   *
   * pre-wrap is right for the plain-text half of this dialog: a confirmation
   * written as a sentence with line breaks in it should keep them, and it is
   * the only way that works without turning every message into markup.
   *
   * It is wrong for the other half. lucidPick and friends put HTML in this same
   * element, built from template literals that are indented to match the code
   * around them — so under pre-wrap every newline and every leading space in
   * MY FILE became blank space in HIS DIALOG. That is the inch of nothing
   * between two project names in the picker: not a spacing choice, my own
   * formatting leaking through the screen.
   *
   * The class is set by dlgOpen whenever the body is markup rather than
   * prose, so each half gets the whitespace rule it actually needs. */
  .dlg-body.has-html{ white-space:normal; }
  .dlg-body b{ color:var(--text); font-weight:600; }
  /* THE DIALOG IS ALWAYS ON TOP, AND IT WAS NOT.
   *
   * Every overlay in the app is a .modal-backdrop at z-index 50, so which one
   * paints above which came down to the order they happen to appear in this
   * file — and #dlgBackdrop is declared near the top, which put it UNDER the
   * composer, the vault, the event editor and everything else declared later.
   * A dialog raised from inside one of those was visible and unclickable: the
   * modal underneath swallowed the pointer, so the buttons could not be
   * pressed. It is the one overlay whose whole job is to be answered, so it
   * gets a number of its own rather than a position in a list. */
  #dlgBackdrop{ z-index:120; }
  /* THE ORANGE BAR ACROSS THE TOP — GONE.
   *
   * It was a solid orange rule sitting on a deep blue card, in an app that is
   * otherwise disciplined about colour: colour in Lucid means an ACCOUNT, a
   * CATEGORY or a WARNING, and this meant none of the three. It was decoration
   * with the visual weight of a signal, which is the specific thing Matt was
   * pointing at.
   *
   * So an ordinary dialog wears the same hairline every other panel wears, and
   * the coloured edge is kept for the case where it says something — a
   * destructive question, in the red that means destructive everywhere else. */
  #dlgCard{ border-top-color:var(--glass-border-hi); }
  #dlgCard.danger{ border-top-color:var(--cat-red-2, #ff6b5e); }
  .toast-undo{ margin-left:var(--space-2); cursor:pointer; font-weight:600; color:var(--accent);
               text-decoration:underline; text-underline-offset:2px; flex:none; }
  /* Unsubscribe lives on the row, deliberately — the top bar is already full
     and this is a per-sender decision, not a mode. */
  .tidy-btn.unsub{ border-color:rgba(80,200,140,.45); }
  .tidy-btn.unsub:hover{ background:rgba(80,200,140,.14); }
  .tidy-done{ font-size:var(--type-body); color:var(--text-faint); white-space:nowrap; }
  .tidy-done b{ color:var(--text); font-weight:600; }
  .tidy-bulkline{ font-size:var(--type-body); color:var(--text-faint); margin:0 0 var(--space-3); line-height:1.45; }
  .tidy-bulkline a{ color:var(--accent); cursor:pointer; text-decoration:underline; }
  .tidy-probe{ font-size:var(--type-label); color:var(--text-faint); margin:0 0 var(--space-3); }

  /* ---- Signature panel ------------------------------------------------- */
  .sig-tabs{ display:flex; gap:var(--space-2); margin:0 0 var(--space-3); }
  .sig-tab{ font:inherit; font-size:var(--type-body); padding:var(--space-2) var(--space-3); border-radius:var(--radius-control); cursor:pointer;
            background:transparent; color:var(--text-faint); border:1px solid var(--glass-border); }
  .sig-tab:hover{ color:var(--text); }
  .sig-tab.active{ background:color-mix(in srgb, var(--accent) 78%, #000); border-color:color-mix(in srgb, var(--accent) 78%, #000); color:var(--on-accent,#fff); }
  .sig-opt{ display:flex; align-items:flex-start; gap:var(--space-2); margin:var(--space-2) 0; cursor:pointer; }
  .sig-opt input{ width:15px; height:15px; margin-top:var(--space-hair); flex:none; cursor:pointer; accent-color:var(--accent); }
  .sig-opt span{ font-size:var(--type-body); color:var(--text); line-height:1.35; }
  .sig-opt em{ display:block; font-style:normal; font-size:var(--type-body); color:var(--text-faint); margin-top:var(--space-hair); }
  .sig-opt code{ font-family:var(--font-mono); font-size:var(--type-label); }
  .sig-accounts-label{ font-size:var(--type-label); letter-spacing:.06em; text-transform:uppercase;
                       color:var(--text-faint); margin:var(--space-4) 0 var(--space-2); }
  /* The dot on a mailbox chip that already has its own signature. The accent
     and nothing else — a second colour here would be a second meaning nobody
     asked for, and the whole page spends its colour in one place. */
  /* ---- THE FLOATING JACKET ----------------------------------------------
     The bar across the bottom is gone: it cost 66px of every screen and it sat
     on top of the orb, which is not moving for a music player. What is left is
     the cover and a ring that fills as the track plays — "is something on, and
     how far in", answered without a single word. */
  .dock{ position:fixed; z-index:420; width:58px; height:58px; border-radius:var(--radius-card);
    cursor:grab; touch-action:none; user-select:none;
    box-shadow:var(--elev-raised);
    transition:transform var(--dur-base) var(--ease-out),
               box-shadow var(--dur-base) var(--ease-out); }
  .dock:hover{ transform:scale(1.06); }
  .dock.dragging{ cursor:grabbing; transform:scale(1.1); transition:none;
    box-shadow:var(--elev-floating); }
  .dock[hidden]{ display:none; }
  .mo-art{ position:absolute; inset:3px; width:calc(100% - 6px); height:calc(100% - 6px);
    border-radius:var(--radius-card); object-fit:cover; display:block; pointer-events:none; }
  .mo-empty{ background:var(--glass-bg); }
  /* The ring IS the progress. A conic gradient to --mo-turn, which
     renderMusicDock sets from the position Spotify last reported. */
  .mo-ring{ position:absolute; inset:0; border-radius:var(--radius-card); pointer-events:none;
    background:conic-gradient(var(--accent) var(--mo-turn, 0turn),
      rgba(255,255,255,.14) var(--mo-turn, 0turn)); }
  /* A slow breath while it is playing, and perfectly still when it is not —
     which is the difference between "paused" and "on" at a glance. */
  .dock.playing .mo-ring{ animation:moPulse 3.2s var(--ease-out) infinite; }
  @keyframes moPulse{ 0%,100%{ opacity:1 } 50%{ opacity:.62 } }
  [data-fx="lite"] .dock.playing .mo-ring{ animation:none; }
  @media(prefers-reduced-motion:reduce){ .dock.playing .mo-ring{ animation:none; } }

  /* The kind-of-thing chips over the music search. Scrolls rather than wraps:
     seven chips on two lines pushes the results below the fold on a laptop. */
  .music-types{ display:flex; gap:var(--space-2); overflow-x:auto; padding:var(--space-3) 0 var(--space-hair);
    scrollbar-width:none; -ms-overflow-style:none; }
  .music-types::-webkit-scrollbar{ display:none; }
  .music-types .chip{ flex:none; white-space:nowrap; }

  .sig-has{ color:var(--accent); margin-left:var(--space-2); }
  .sig-accounts{ display:flex; flex-direction:column; gap:var(--space-2); }
  .sig-accounts label{ display:flex; align-items:center; gap:var(--space-2); font-size:var(--type-body); cursor:pointer; }
  .sig-accounts input{ width:15px; height:15px; flex:none; cursor:pointer; accent-color:var(--accent); }
  .sig-report{ margin-top:var(--space-3); display:flex; flex-direction:column; gap:var(--space-2); }
  .sig-note{ font-size:var(--type-body); line-height:1.4; padding:var(--space-2) var(--space-3); border-radius:var(--radius-control);
             border:1px solid var(--glass-border); color:var(--text-faint); }
  .sig-note.good{ border-color:rgba(80,200,140,.35); color:var(--text); }
  .sig-note.warn{ border-color:rgba(230,170,60,.4); color:var(--text); }
  .sig-note.bad{ border-color:rgba(230,90,90,.45); color:var(--text); }
  .sig-note b{ color:var(--text); }
  .sig-preview{ width:100%; min-height:170px; border:1px solid var(--glass-border); border-radius:var(--radius-card);
                background:#fff; margin-top:var(--space-2); }

  /* ==========================================================================
     ROW 1731, SECOND LOOK — ONE SHEET OF PAPER
     --------------------------------------------------------------------------
     The signature was in the box and editable, and Matt's third fault was still
     on screen. Rendered and looked at, in midnight and in alabaster: the body
     was a bordered, lit, rounded box that ENDED, and the signature was four
     lines of type lying loose on the pane below it, between that box and the
     Attach files button, with a lone resize grabber floating out at the right
     margin. Read as a picture rather than as a description, that is not a
     signature grouped too close to a message. It is a signature that has come
     off the message altogether — two controls stacked up, which is exactly what
     "assembled with cheap parts" looks like when you say it about a form.

     SO THE BORDER GOES ROUND BOTH OF THEM. Gmail, Apple Mail, Outlook and
     Superhuman all put the signature inside the writing surface, because it IS
     the bottom of the letter: one sheet, one edge, one focus ring, with a blank
     line and a rule between the paragraph and the sign-off the way a typed page
     has always done it. The sheet now carries the chrome .field used to carry,
     and the fields inside it give theirs up.

     THE SHEET IS WHAT RESIZES, not the textarea inside it. Dragging the corner
     of the message box and watching the signature stay put — or worse, watching
     a second grabber appear beside the first — is the seam this is closing, so
     the grabber belongs to the sheet and the body grows into whatever room it
     is given. resize needs a non-visible overflow to work at all, hence auto:
     at rest nothing overflows, and a sheet dragged shorter than its contents
     scrolls rather than clipping the last line of somebody's address. */
  /* ==========================================================================
     ROW 2240 — THE LETTER IS WHITE, AND THAT IS DELIBERATE
     --------------------------------------------------------------------------
     Matt, 15 September: "need the emails to be white so they look natural while
     composing. New emails, reply and forward all have dark boxes inside. Need
     the write window to be clean."

     WHAT HE WAS LOOKING AT. The New Message dialog was Lucid's dark chrome all
     the way through, and floating in the middle of it was his HTML signature —
     a white card, because renderComposeSignature has always drawn it on white
     and the note further down explains why. So the composer showed a bright
     rectangle adrift in a dark box: it looked broken, and worse, it looked
     nothing like the thing the recipient was going to open.

     THE LINE THIS FILE NOW DRAWS, AND IT IS NOT A THEME FAULT.
     Everything from the first line of the message down to the end of the
     signature is THE LETTER, and a letter is a white page with dark type in
     every mail client anybody has ever used. The chrome around it — From, To,
     Subject, Cc, Bcc, Attach files, the send row, the read-receipt line — is
     Lucid, and it stays in the theme, because that half is the app and not the
     mail. The sheet is the boundary between the two.

     SO THESE EIGHT VALUES MUST NOT FOLLOW THE THEME, which is unusual enough in
     a file organised entirely around themes that it needs saying here rather
     than being discovered by whoever "fixes" it back. They are tokens rather
     than literals for two reasons: the armed scale lint (tests/lint-the-scale.js)
     counts a raw 14px as a decision with no name, and a name is somewhere to
     write the reason down. renderComposeSignature reads --letter-ink back out
     of the live page to set the ink inside the signature frame's own document,
     so the colour of email paper is decided in exactly one place.

     AND THE TYPE IS NOT ON LUCID'S SCALE EITHER, ON PURPOSE. sans-serif at 14px
     is not a step of --type-*; it is character for character what
     app/netlify/functions/_mime.js puts on every message this app sends —
     `const BODY_FONT = 'font-family:sans-serif;font-size:14px;'`. What he types
     is now set in the face and at the size it will arrive in, which is the
     whole of "so they look natural while composing". If that constant ever
     moves, these two move with it, and the check that says so is
     tests/test-the-letter-is-white.js.

     THE INKS ARE ALL AA ON WHITE, measured rather than picked: #1a1a1a is
     16.9:1, #444444 is 9.7:1, #6b6b6b is 5.3:1. --letter-rule is the hairline
     that stands in for the "--" a reader gets above a signature, and it is a
     grey rather than --glass-border because --glass-border is a translucent
     white on the dark themes and would be invisible on paper. --letter-wash is
     the hover and focus tint on the editable signature, and it is neutral for
     the same reason an accent tint would be wrong: the letter does not know
     what colour the app is. */
  :root{
    --letter-paper:     #ffffff;
    --letter-ink:       #1a1a1a;
    --letter-ink-dim:   #444444;
    --letter-ink-faint: #6b6b6b;
    --letter-rule:      #dcdcdc;
    --letter-wash:      rgba(0, 0, 0, .035);
    --letter-font:      sans-serif;
    --letter-type:      14px;
  }
  .tidy-count{ font-family:var(--font-mono); font-size:var(--type-body); font-weight:700; flex:none;
    min-width:52px; text-align:right; }
  .tidy-acts{ display:flex; gap:var(--space-2); flex:none; }
  .tidy-btn{ font-size:var(--type-body); padding:var(--space-2) var(--space-3); border-radius:var(--radius-control); cursor:pointer;
    background:var(--glass-bg); border:1px solid var(--glass-border); color:var(--text-dim);
    font-family:var(--font-body); transition:all var(--dur-fast) var(--ease-out); }
  .tidy-btn:hover{ color:var(--text); border-color:color-mix(in srgb, var(--accent) 50%, transparent); }
  .tidy-btn.danger:hover{ color:var(--text); border-color:color-mix(in srgb, var(--pink) 60%, transparent);
    background:color-mix(in srgb, var(--pink) 15%, transparent); }
  .tidy-btn:disabled{ opacity:.4; cursor:default; }
  .tidy-tag{ font-size:var(--type-label); color:var(--green); }

  /* The title, folded into the toolbar it used to sit above. */
  .tb-title{ font-family:var(--font-display); font-size:var(--type-title); font-weight:600; letter-spacing:-.01em;
    margin:0 var(--space-2) 0 var(--space-hair); flex:none; line-height:1; }

  .mstrip-btn{ display:inline-flex; align-items:center; gap:var(--space-2); flex:none;
    height:34px; padding:0 var(--space-4) 0 var(--space-3); border-radius:var(--radius-card); cursor:pointer;
    font-family:var(--font-body); font-size:var(--type-body); font-weight:650;
    letter-spacing:.005em; color:var(--text);
    background:var(--glass-bg); border:1px solid var(--glass-border);
    transition:background var(--dur-fast) var(--ease-out),
               border-color var(--dur-fast) var(--ease-out),
               box-shadow var(--dur-base) var(--ease-out); }
  .mstrip-btn svg{ width:16px; height:16px; flex:none; }
  .mstrip-btn:hover, .mstrip-btn:focus-visible{
    border-color:color-mix(in srgb, var(--act, var(--accent)) 55%, transparent);
    background:color-mix(in srgb, var(--act, var(--accent)) 16%, transparent);
    box-shadow:0 0 16px 1px var(--act-glow, var(--accent-glow)); outline:none; }
  .mstrip-btn:active{ background:color-mix(in srgb, var(--act, var(--accent)) 26%, transparent); }
  /* The one you press nine times out of ten. */
  .mstrip-btn.lead{
    background:color-mix(in srgb, var(--act, var(--accent)) 17%, transparent);
    border-color:color-mix(in srgb, var(--act, var(--accent)) 38%, transparent); }
  .mstrip-btn.lead:hover, .mstrip-btn.lead:focus-visible{
    background:color-mix(in srgb, var(--act, var(--accent)) 27%, transparent);
    border-color:color-mix(in srgb, var(--act, var(--accent)) 60%, transparent); }

  /* The right-hand end: the chevron in Mail, Open-in-Mail on a timeline, and
     the snooze and dark-body controls the reading pane passes in between them.
     Square rather than a pill, so the eye separates "things that answer this
     message" from "things that do something to it" without a rule between. */
  .mstrip-icon{ display:inline-flex; align-items:center; justify-content:center; gap:var(--space-2);
    flex:none; height:34px; min-width:34px; padding:0 var(--space-2); border-radius:var(--radius-card);
    cursor:pointer; font-family:var(--font-body); font-size:var(--type-body); font-weight:600;
    color:var(--text-dim);
    background:var(--glass-bg); border:1px solid var(--glass-border);
    transition:background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out),
               border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-base) var(--ease-out); }
  .mstrip-icon svg{ width:16px; height:16px; flex:none; }
  .mstrip-icon:hover, .mstrip-icon:focus-visible{ color:var(--text);
    border-color:color-mix(in srgb, var(--act, var(--accent)) 50%, transparent);
    background:color-mix(in srgb, var(--act, var(--accent)) 13%, transparent);
    outline:none; }
  /* Snoozed is a STATE of this message and it says so on the button, with the
     date beside the clock. A snooze you have to open a menu to discover is a
     snooze you will be surprised by. */
  .mstrip-icon.on{ color:var(--text);
    background:color-mix(in srgb, var(--blue) 16%, transparent);
    border-color:color-mix(in srgb, var(--blue) 42%, transparent); }
  /* ON A PHONE, ONE WORD SURVIVES AND IT IS THE RIGHT ONE.
     Measured at 390px, where the reading pane is 362 wide: three labelled
     buttons and three icons wrapped onto THREE rows, which is 102px of controls
     standing between the top of the screen and the message. Reply all and
     Forward lose their labels and keep their titles — the same trade
     .tb-selectmode already makes below 1200px — and Reply keeps its word,
     because it is the one pressed nine times out of ten and the one a thumb
     goes for without looking. One row at 390px after the change. */
  @media (max-width: 480px){
    .mstrip-btn:not(.lead) span{ display:none; }
    .mstrip-btn:not(.lead){ padding:0 var(--space-2); }
    }

  /* The disposal rows in the ⋯ menu. Same list, one of them coloured, because
     "Delete" reading exactly like "Archive" one line above it is how the wrong
     one gets pressed. */
  .more-item.danger{ color:var(--pink); }
  .more-item.danger:hover{ background:color-mix(in srgb, var(--pink) 14%, transparent); }

  /* Admin centre */
  .ai-spend{ display:flex; align-items:baseline; gap:var(--space-4); flex-wrap:wrap; padding:var(--space-4) var(--space-4); border-radius:var(--radius-card);
    background:var(--glass-bg); border:1px solid var(--glass-border); margin-bottom:var(--space-1); }
  .ai-spend-num{ font-size:var(--type-page); font-weight:700; letter-spacing:-.02em; }

  /* MEMORY — the list of what Lucid thinks it knows.
     Deliberately plain and complete rather than clever. Every row shows the
     fact, where it came from, and a way to delete it, because a memory he
     cannot find is a memory he cannot correct. */
  .mem-add{ display:flex; gap:var(--space-2); align-items:stretch; margin-bottom:var(--space-3); }
  .mem-add .field{ flex:1; min-width:0; }
  /* The overnight switch. Sits with the box he types a memory into because the
     two are the same idea from opposite directions — one is him telling Lucid
     something, the other is Lucid noticing on its own. The whole row is the hit
     target, not just the 13px box. */
  .mem-job{ display:flex; gap:var(--space-3); align-items:flex-start; cursor:pointer;
    padding:var(--space-3) var(--space-3); margin-bottom:var(--space-3); border-radius:var(--radius-card);
    border:1px solid var(--glass-border); background:var(--glass-bg); }
  .mem-job:hover{ border-color:var(--glass-border-hi); }
  .mem-job input{ margin-top:var(--space-hair); flex:none; }
  .mem-job-text{ display:block; min-width:0; }
  .mem-job-text b{ display:block; font-size:var(--type-body); font-weight:600; }
  .mem-job-text .page-sub{ margin:var(--space-hair) 0 0; }
  .mem-row{ display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-3); border-radius:var(--radius-card);
    border:1px solid var(--glass-border); background:var(--glass-bg); margin-bottom:var(--space-2); }
  .mem-row:hover{ border-color:var(--glass-border-hi); }
  /* ITEM 84 — a memory nothing has reinforced in MEM_STALE_DAYS reads a
     little quieter than the rest, so relevance is visible on the row itself
     rather than only in an ORDER a person cannot see. Opacity, not colour —
     it must never look like an error or a thing that needs fixing, only like
     something that has gone quiet. */
  .mem-row.mem-quiet{ opacity:.6; }
  .mem-row.mem-quiet:hover{ opacity:1; }
  .mem-body{ flex:1; min-width:0; }
  .mem-text{ font-size:var(--type-body); line-height:1.5; word-break:break-word; }
  .mem-meta{ font-size:var(--type-label); text-transform:uppercase; letter-spacing:.05em; color:var(--text-faint); margin-top:var(--space-1); }
  /* An email address inside the label is drawn as written, not in capitals
     (memSourceTagHtml, 15 October): it is a stranger's address he may need to
     recognise or search for. */
  .mem-meta .mem-as-written{ text-transform:none; }
  .mem-pin, .mem-del{ flex:none; cursor:pointer; color:var(--text-faint); font-size:var(--type-lead); line-height:1;
    width:26px; height:26px; border-radius:var(--radius-control); display:flex; align-items:center; justify-content:center; }
  .mem-pin:hover, .mem-del:hover{ background:color-mix(in srgb, var(--text) 8%, transparent); color:var(--text); }
  .mem-pin.on{ color:var(--orange); }
  .mem-kind{ display:inline-block; font-size:var(--type-label); font-weight:700; text-transform:uppercase; letter-spacing:.05em;
    padding:var(--space-hair) var(--space-2); border-radius:var(--radius-control); margin-right:var(--space-2);
    background:color-mix(in srgb, var(--blue) 16%, transparent); color:var(--blue-text, var(--blue)); }
  :root{ --blue-text:var(--blue-2); }
  /* ON PAPER, THE TEXT COLOUR CANNOT BE THE FILL COLOUR. Aliasing --blue-text
     straight to --blue gives a blue chosen to read as a fill on a dark ground,
     and on a light one it lands at 4.31:1 -- .px-tag.waiting measured as
     #4f6a96 on #e3e4e6 on Serene Linen Warm, three weeks under the floor.
     An eighteen-per-cent pull toward the theme's own ink takes it to 5.17:1.
     NOT A NEW COLOUR: both sides are tokens, so every paper theme still gets
     its own blue and its own ink, and dark mode (the line above) is untouched. */
  :root[data-mode="light"]{ --blue-text:color-mix(in srgb, var(--blue) 82%, var(--text)); }
  .mem-kind.k-inferred{ background:color-mix(in srgb, var(--orange) 16%, transparent); color:var(--orange-text, var(--orange)); }
  :root{ --orange-text:var(--orange-2); }
  /* THE SAME PULL, AND THE SAME REASON. --orange on paper measured 4.27:1 on
     .px-act.px-rm and 3.89:1 on .px-d.late -- #99612d on #e1e3e5. At 82% of
     itself against --text it is 4.73:1. This token is read in 21 places and
     every one of them is small text on paper, so all of them gain. */
  :root[data-mode="light"]{ --orange-text:color-mix(in srgb, var(--orange) 82%, var(--text)); }
  .ai-spend-of{ font-size:var(--type-body); color:var(--text-faint); }
  .ai-bar{ width:100%; height:6px; border-radius:var(--radius-tight); background:color-mix(in srgb, var(--text) 10%, transparent); overflow:hidden; margin-top:var(--space-1); }
  .ai-bar span{ display:block; height:100%; border-radius:var(--radius-tight); background:var(--green); transition:width var(--dur-base) var(--ease-out); }
  .ai-bar.warn span{ background:var(--orange); }
  .diag-row{ display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-2) var(--space-hair); border-bottom:1px solid var(--glass-border); font-size:var(--type-body); }
  .diag-row:last-child{ border-bottom:none; }
  .diag-dot{ width:8px; height:8px; border-radius:var(--radius-round); margin-top:var(--space-2); flex:none; }
  .diag-ok   .diag-dot{ background:var(--green);  box-shadow:0 0 8px 1px var(--green-glow); }
  .diag-bad  .diag-dot{ background:var(--cat-red-2, #ff6b5e); box-shadow:0 0 8px 1px var(--cat-red-glow); }
  .diag-warn .diag-dot{ background:var(--orange); }
  /* COULD NOT CHECK. A hollow ring, and deliberately not a colour: the three
     dots above are verdicts and this one is the absence of a verdict, so it
     must not read as a quieter version of any of them. A filled grey dot still
     scans as "fine, nothing to see" at a glance, which is the exact reading
     that has cost days — see connectionRow() in app.js. Existing tokens only;
     no new colour is introduced here. */
  .diag-unknown .diag-dot{ background:transparent; border:2px solid var(--text-dim); box-shadow:none; }
  /* The raw reply from Ticketmaster, printed so the tour-date parser can be
     checked against a real answer rather than taken on trust — see
     probeTicketmaster(). Monospace because it is JSON, scrolling because it
     is somebody else's JSON and its width is not ours to control. */
  .diag-raw{ font-family:var(--font-mono); font-size:var(--type-label); line-height:1.5;
    background:rgba(120,120,140,.08); border:1px solid var(--glass-border);
    border-radius:var(--radius-control); padding:var(--space-3) var(--space-3); margin:var(--space-2) 0 0;
    max-height:260px; overflow:auto; white-space:pre; color:var(--text-dim); }
  .diag-name{ font-weight:600; min-width:150px; }
  /* A heading inside the diagnostics list. The set-up jobs are a different KIND
     of thing from the live checks above them — those report on the machine,
     these report on a person's to-do list — and running them together as one
     undifferentiated column made a nineteen-row page nobody would read to the
     bottom of. */
  .diag-head{ font-size:var(--type-label); font-weight:700; letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-dim); margin:var(--space-5) 0 var(--space-hair); padding-top:var(--space-3);
    border-top:1px solid var(--glass-border); }
  .diag-row:last-child{ border-bottom:none; }
  .diag-detail{ color:var(--text-dim); flex:1; overflow-wrap:anywhere; }
  .diag-counts{ display:flex; gap:var(--space-4); flex-wrap:wrap; font-size:var(--type-body); color:var(--text-dim); padding:var(--space-2) var(--space-hair); }
  .app-row{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-3) var(--space-hair); border-bottom:1px solid var(--glass-border); font-size:var(--type-body); }
  .app-row:last-child{ border-bottom:none; }
  .toast-stack{ position:fixed; right:22px; bottom:22px; z-index:9000; display:flex; flex-direction:column; gap:var(--space-3);
    align-items:flex-end; pointer-events:none; max-width:min(420px, calc(100vw - 44px)); }
  .toast{ pointer-events:auto; display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) var(--space-4); border-radius:var(--radius-card);
    font-size:var(--type-body); line-height:1.45; background:var(--glass-bg); border:1px solid var(--glass-border);
    backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px); color:var(--text);
    box-shadow:var(--elev-floating); max-width:100%;
    animation:toastIn var(--dur-base) var(--ease-out); overflow-wrap:anywhere; }
  .toast.leaving{ animation:toastOut var(--dur-base) var(--ease-in-out) forwards; }
  .toast-dot{ width:8px; height:8px; border-radius:var(--radius-round); margin-top:var(--space-2); flex:none; }
  .toast.ok    .toast-dot{ background:var(--green);  box-shadow:0 0 10px 1px var(--green-glow); }
  .toast.warn  .toast-dot{ background:var(--orange); box-shadow:0 0 10px 1px var(--orange-glow, rgba(255,150,60,.5)); }
  .toast.error .toast-dot{ background:var(--cat-red-2, #ff6b5e); box-shadow:0 0 10px 1px var(--cat-red-glow); }
  .toast-x{ margin-left:var(--space-2); cursor:pointer; color:var(--text-faint); flex:none; }
  .toast-x:hover{ color:var(--text); }
  @keyframes toastIn{ from{ opacity:0; transform:translateY(8px) scale(.98); } to{ opacity:1; transform:none; } }
  @keyframes toastOut{ to{ opacity:0; transform:translateY(6px) scale(.98); } }
  @media (prefers-reduced-motion: reduce){ .toast, .toast.leaving{ animation:none; } }

  /* THE CONNECT BANNER — the whole of Lucid's first-run guidance.
     -----------------------------------------------------------------------
     It used to be built entirely out of a cssText string in showConnectBanner,
     including `background: rgba(10,14,26,0.85)` — a dark-navy plate painted
     under all fourteen themes, so on Light, Alabaster and Parchment it was a
     black slab with --text-dim written on it. Every colour here is a token
     now, which is the only way the contrast harness can see it at all.

     The dot is --orange because this is a state, not a fault: something is
     waiting to be done rather than something having gone wrong. Same reading
     as the amber svc-dot in Settings. */
  .connect-banner{ position:fixed; top:14px; left:50%; transform:translateX(-50%); z-index:999;
    display:flex; align-items:center; gap:var(--space-3); max-width:calc(100vw - 28px);
    padding:var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius:var(--radius-pill); font-size:var(--type-body); color:var(--text);
    background:var(--glass-bg); border:1px solid var(--glass-border-hi);
    backdrop-filter:blur(18px) saturate(var(--glass-sat)); -webkit-backdrop-filter:blur(18px) saturate(var(--glass-sat));
    box-shadow:var(--elev-raised); }
  .connect-banner-dot{ width:6px; height:6px; border-radius:var(--radius-round); flex:none;
    background:var(--orange); box-shadow:0 0 8px 1px var(--orange-glow); }
  .connect-banner-said{ min-width:0; }
  /* AND THE SAME STRIP SAYING THE OTHER THING — a mailbox that IS connected
     and has stopped reading. That one IS a fault, it is hers to fix and the
     fix is the button beside it, so the mark goes from amber to --pink. The
     sentence is what carries it; the colour is the second signal rather than
     the only one, because this app draws on fourteen grounds and for people
     who do not see red. */
  .connect-banner.stopped .connect-banner-dot{
    background:var(--pink); box-shadow:0 0 8px 1px var(--pink-glow); }
  .connect-banner.stopped{
    border-color:color-mix(in srgb, var(--pink) 40%, var(--glass-border-hi)); }
  /* Two real buttons rather than the one bare link this had. A press that is
     styled as running text reads as a footnote, and this is the only
     instruction on the screen. */
  .connect-banner-go{ flex:none; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); font-size:var(--type-body); font-weight:600;
    cursor:pointer; color:var(--text); background:rgba(120,120,140,.14);
    border:1px solid var(--glass-border-hi); transition:all var(--dur-fast) var(--ease-out); }
  .connect-banner-go:hover{ background:rgba(120,120,140,.24); border-color:var(--accent); }
  /* Under about 720px the sentence and two buttons cannot share a line without
     one of them being clipped, and a clipped Connect button is worse than a
     stacked one. */
  @media (max-width: 720px){
    .connect-banner{ flex-wrap:wrap; border-radius:var(--radius-card); padding:var(--space-3) var(--space-3); justify-content:center; text-align:center; }
    .connect-banner-said{ flex:1 1 100%; }
  }

  /* ITEM 337 — THE BANNER THAT SAYS LUCID IS HAVING A BAD DAY.
     ---------------------------------------------------------------------
     The status page was built and honest and NOTHING IN THE APP MENTIONED
     IT, so somebody already signed in learned nothing — the row's own words
     were "there is NO BANNER IN THE APP fed by a flag, so a person already
     signed in learns nothing". This is that banner. It is drawn by
     /lucid-health.js, which is a separate file from app.js on purpose:
     app.js failing to parse is itself one of the outages, and a banner
     living inside it would be missing on the worst day.

     THE SAME STRIP AS .connect-banner, deliberately, rather than a second
     visual language for "Lucid is telling you something". One place on the
     screen where the app speaks in its own voice; what changes is the
     colour of the mark and the sentence beside it.

     THREE TONES AND NO GREEN. There is no "all is well" state and there
     never will be: a working Lucid is a silent one, and a strip that says
     everything is fine is a strip that is wrong for the whole of a failed
     request. `.notice` is a sentence somebody typed into LUCID_STATUS_NOTICE;
     `.down` is Lucid answering and saying it cannot reach its database;
     `.unknown` is nothing coming back at all, twice — which is doubt rather
     than either of the other two and must never be drawn as a quieter
     version of "fine". Colour is the second signal here as everywhere: the
     sentence carries it, because this app draws on fourteen grounds and for
     people who do not see red. */
  .status-banner{ position:fixed; top:14px; left:50%; transform:translateX(-50%); z-index:1000;
    display:flex; align-items:center; gap:var(--space-3); max-width:calc(100vw - 28px);
    padding:var(--space-2) var(--space-3) var(--space-2) var(--space-4); border-radius:var(--radius-pill); font-size:var(--type-body); color:var(--text);
    background:var(--glass-bg); border:1px solid var(--glass-border-hi);
    backdrop-filter:blur(18px) saturate(var(--glass-sat)); -webkit-backdrop-filter:blur(18px) saturate(var(--glass-sat));
    box-shadow:var(--elev-raised);
    /* THIS STRIP USED TO EAT CLICKS MEANT FOR THE PAGE UNDERNEATH IT.
       It is position:fixed above everything, and at the length of the
       "could not be reached" sentence it is about 700px wide — which on the
       mail screen is exactly where the search box is. Not dimmed: covered.
       Playwright retried a click on #mailSearchInput 62 times over thirty
       seconds on 20 September and never once reached it, and a person sitting
       in front of the same screen would have had the same experience with
       less patience.
       The strip is a sentence and a dot. Nothing in it is meant to be
       clicked except the one button, so nothing else in it takes a click. */
    pointer-events:none; }
  .status-banner-go, .status-banner a, .status-banner button{ pointer-events:auto; }
  .status-banner-dot{ width:6px; height:6px; border-radius:var(--radius-round); flex:none;
    background:var(--orange); box-shadow:0 0 8px 1px var(--orange-glow); }
  .status-banner-said{ min-width:0; }
  /* Lucid answered and said it is broken. That is a fault, and it is the one
     of the three that is certainly about Lucid rather than possibly about the
     reader's own connection. */
  .status-banner.down .status-banner-dot{
    background:var(--pink); box-shadow:0 0 8px 1px var(--pink-glow); }
  .status-banner.down{
    border-color:color-mix(in srgb, var(--pink) 40%, var(--glass-border-hi)); }
  /* "We could not tell." Dimmer than the other two on purpose — it is the
     least certain thing this strip ever says, and overstating it would teach
     somebody on a train to ignore the strip on the day it matters. */
  .status-banner.unknown .status-banner-dot{
    background:var(--text-dim); box-shadow:none; }
  .status-banner-go{ flex:none; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); font-size:var(--type-body); font-weight:600;
    cursor:pointer; color:var(--text); background:rgba(120,120,140,.14); text-decoration:none;
    border:1px solid var(--glass-border-hi); transition:all var(--dur-fast) var(--ease-out); }
  .status-banner-go:hover{ background:rgba(120,120,140,.24); border-color:var(--accent); }
  /* ONE NUMBER, MEASURED, FOR EVERYTHING THAT HAS TO GET OUT OF THE STRIP'S WAY.

     62px stood here, described as "the same height the connect pill has always
     used" and "measured against the same height". It was never that height.
     Measured on 20 September against the strip's own bounding box, at twelve
     widths and on all three tones, its bottom edge lands between 65px and
     208px, and the clearance was short EVERY time: by 3px at 1440, by 15px at
     900, by 52px at 720, and by 112px at 320, where the top of the page sat
     under a strip that had wrapped to four lines. A page that starts 112px
     under the thing it was moved down to clear still has the fault it was
     moved for.

     No constant can be right here. The strip's height follows the window's
     width, which of the three sentences it is carrying, and, on the 'notice'
     tone, LUCID_STATUS_NOTICE, which is an arbitrary sentence somebody types
     into a web form and whose length nothing in this file can know.

     So lucid-health.js measures the strip it has just drawn and publishes the
     answer as --status-banner-h, and everything that has to clear it reads
     that instead of guessing. 14px is where the strip starts and 14px is the
     gap the page keeps from the top of the window, so this reads: whatever
     used to measure from the top of the window measures from the bottom of
     the strip instead. The fallback is the measured desktop height, and it is
     only ever used for the frame between the strip being drawn and being
     measured. */
  body.has-status-banner{ --status-banner-clear: calc(14px + var(--status-banner-h, 51px) + 14px); }

  /* The connect pill lives at exactly these coordinates. Neither of them knows
     about the other — the body carries the fact and this rule moves the pill
     down — which is what makes it come out right whichever appeared first. */
  body.has-status-banner .connect-banner{ top: var(--status-banner-clear); }

  /* AND THE PAGE ITSELF STARTS BELOW THE STRIP RATHER THAN UNDER IT.
     pointer-events above means the strip can no longer block anything; this
     means it does not sit on top of anything either. Two different repairs to
     the same fault, and both are wanted — a control a person can click but
     cannot see is not much better than one they cannot click.
     The distance is measured rather than written down here now; the block
     above says why. */
  body.has-status-banner .main{ padding-top: var(--status-banner-clear); }

  /* AND THE ASK SHEET HANGS BELOW IT. The Lucid face drops 14px from the top
     of the window and the strip sits at 14px, z-index 1000, pointer-events
     none -- so with the strip up, Earlier, Changes and New were drawn under a
     blurred sentence and could still be clicked but not read (test-readability
     measured New at 1.13:1 on Hearth). The rooms drawer runs the full height
     of the right wall and its crown sat under the same strip. Both read the
     same measured clearance the page does. The docked panel is anchored at the
     bottom and is left alone. */
  body.has-status-banner .one-backdrop:not(.lucid-dock) .one-surface[data-one-face="lucid"]{
    margin-top: var(--status-banner-clear) !important;
    max-height: calc(100vh - var(--status-banner-clear) - 66px) !important; }
  body.has-status-banner .one-surface[data-one-face="rooms"]{
    padding-top: var(--status-banner-clear) !important; }

  @media (max-width: 720px){
    .status-banner{ flex-wrap:wrap; border-radius:var(--radius-card); padding:var(--space-3) var(--space-3); justify-content:center; text-align:center; }
    .status-banner-said{ flex:1 1 100%; }
    /* Wrapped, the strip is two to four lines tall rather than one. There is
       no second number for that here any more, and none for the widths in
       between either: --status-banner-h is measured from the wrapped strip,
       so it already carries the width. 96px lived here and was short by
       between 52px and 112px on every screen it applied to. */
  }

  /* ======================================================================
     THE RECIPIENT ROWS — 16 SEPTEMBER, "when I click reply the cc box is
     enormous".
     ======================================================================

     MEASURED BEFORE IT WAS TOUCHED, at his font size and his window width:
     the Cc row was 124px tall, against a To row of 85px, holding two
     addresses. At 100% type in a wide window both are 43px, which is why it
     had never been seen here. Three separate things compounded:

       A CHIP WAS AS WIDE AS ITS ADDRESS. .chip-token is white-space:nowrap
       with no ceiling, so "tfoster@pilotenergy.com" is a 250px unbreakable
       object. Two of those need 500px before anything else is allowed on the
       line, and the wrap for Cc at his width is 509.

       THE INPUT DEMANDED 120px. .chip-input-field carried min-width:120px, so
       once the chips had taken the line the caret went to a line of its OWN —
       a third row of height for an empty text box nobody was typing in.

       AND THE LABEL CENTRED ITSELF against whatever that came to. That is the
       part he actually photographed: "CC" floating twenty-four pixels below
       the addresses it belongs to, in the middle of a tall empty box, looking
       like a mistake — because it is one.

     The answers, in the same order: a chip may not be wider than half the
     line and truncates with an ellipsis (the full address is the title and is
     still in the field); the caret tucks into whatever is left of the last
     line and only wraps when there is genuinely nothing; and the label sits on
     the FIRST line and stays there, however many lines follow it.

     Tighter as well, because he asked for tight: 6px of padding instead of 8,
     a 22px chip instead of 26, and a row that comes to 34 — the same 34 the
     buttons above it now use. One height on the screen. */
  .rp-compose-field-row{ display:flex; align-items:flex-start; gap:var(--space-3);
    padding:var(--space-hair) 0 var(--space-2); border-bottom:1px solid var(--glass-border); }
  /* Recipient chip-input — shared across New Message / Reply / Forward. */
  .chip-input-wrap{ position:relative; flex:1; min-width:0; }
  .chip-input-chips{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-hair) var(--space-2); min-height:26px; }
  /* max-width is what stops one long address from setting the width of the
     line and pushing everything else onto the next one. Half the box, so two
     always sit side by side; the label inside truncates and the whole address
     is on the element's title and in the field itself, so nothing is lost. */
  .chip-token{ display:inline-flex; align-items:center; gap:var(--space-1);
    /* Half the line, less the room the caret needs. Plain 50% is the obvious
       number and it is wrong by exactly one gap: two chips at 50% plus the gap
       between them is wider than the box, so the second one wraps and takes
       the caret with it. Measured at his 115% type in an 1150px window — 66px
       with 50%, 43px with this. */
    max-width:calc(50% - var(--space-8)); min-width:0;
    padding:0 var(--space-1) 0 var(--space-2); height:22px;
    border-radius:var(--radius-control); background:color-mix(in srgb, var(--accent) 16%, transparent); border:1px solid color-mix(in srgb, var(--accent) 35%, transparent); font-size:var(--type-body); font-weight:600; white-space:nowrap; }
  .chip-token-lab{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  .chip-token-x{ width:15px; height:15px; border-radius:var(--radius-round); display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-size:var(--type-body); color:var(--text-faint); flex:none; }
  .chip-token-x:hover{ background:rgba(255,255,255,.12); color:var(--text); }
  /* 40px, not 120. The caret tucks into whatever is left of the last line and
     grows as the line empties; it no longer demands a line of its own the
     moment two addresses are present. It is still a real input at 40px —
     typing scrolls it, and the suggestions list below is where the address is
     actually read. */
  .chip-input-field{ flex:1 1 40px; min-width:40px; background:none; border:none; outline:none; color:var(--text); font-family:var(--font-body); font-size:var(--type-body); padding:0; height:22px; }
  .chip-input-suggestions{ position:absolute; top:calc(100% + 4px); left:0; right:0; background:var(--bg-mid); border:1px solid var(--glass-border-hi); border-radius:var(--radius-card); box-shadow:var(--elev-floating); z-index:30; overflow:hidden; display:none; max-height:220px; overflow-y:auto; }
  .chip-input-suggestions.open{ display:block; }
  .chip-suggestion{ display:flex; flex-direction:column; padding:var(--space-2) var(--space-4); cursor:pointer; border-bottom:1px solid var(--glass-border); }
  .chip-suggestion:last-child{ border-bottom:none; }
  .chip-suggestion:hover, .chip-suggestion.active{ background:rgba(120,120,140,.12); }
  .chip-suggestion-name{ font-size:var(--type-body); font-weight:700; }
  .chip-suggestion-email{ font-size:var(--type-label); color:var(--text-faint); }
  .rp-compose-field-row .field{ margin:0; border:none; background:none; padding:0; }
  /* line-height matched to the chip so the label sits ON the first line of
     addresses rather than being centred against however many lines follow.
     This is the "CC floating in the middle of a tall box" in his screenshot,
     and it is fixed by alignment rather than by hoping the box stays short. */
  .rp-compose-label{ font-size:var(--type-label); font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--text-faint); width:56px; flex:none; line-height:22px; }
  .rp-trail-meta{ font-size:var(--type-body); color:var(--text-faint); margin-bottom:var(--space-3); padding-top:var(--space-4); border-top:1px solid var(--glass-border); }
  .rp-trail-body{ border-radius:var(--radius-card); overflow:hidden; }
  .rp-subject{ font-family:var(--font-display); font-size:var(--type-title); font-weight:600; margin:0 0 var(--space-1); }
  .rp-who{ min-width:0; }
  .rp-name{ font-size:var(--type-lead); font-weight:650; color:var(--text); }
  /* ROW 2233 — this is an <a> now and it used to be a <div>, so it has to be
     told both of the things the browser assumes about a link and the div was
     never asked: that it fills its line, and that it is not underlined by
     default. Underlined on hover instead, which is what every other pressable
     word in this header does. Still a <div> on an address that will not parse
     — see rpAddrHtml — so both spellings are styled by the one rule. */
  .rp-addr{ display:block; text-decoration:none;
            font-size:var(--type-body); color:var(--text-dim); font-family:var(--font-mono);
            word-break:break-all; }
  a.rp-addr:hover, a.rp-addr:focus-visible{ color:var(--text); text-decoration:underline; }
  .rp-onbehalf{ font-size:var(--type-body); color:var(--amber, #E0A458); margin-top:var(--space-1); }
  /* A LINK FOUND IN A PLAIN-TEXT EMAIL — 16 SEPTEMBER. See linkifyMailText.
     A text/plain message has no markup at all, so until now a URL in one was
     twenty-eight characters he could read and not press. Underlined rather
     than merely coloured: the run was indistinguishable from the sentence
     round it a moment ago, and colour alone is not a signal somebody
     colour-blind can use. word-break because a tracking URL is longer than
     the pane and must wrap rather than set the column's width. */
  .mail-text-link{ color:var(--accent); text-decoration:underline;
    text-underline-offset:2px; text-decoration-thickness:1px; cursor:pointer;
    word-break:break-word; transition:color var(--dur-fast) var(--ease-out); }
  .mail-text-link:hover, .mail-text-link:focus-visible{ color:var(--text); }
  .btn-danger{ color:var(--red, #FF8080); border-color:color-mix(in srgb, var(--red, #FF8080) 35%, transparent); }
  /* The dialog's destructive button. It should read as the dangerous one at a
     glance — the whole point of a confirmation is that the two choices do not
     look interchangeable. */
  .btn.danger{ color:var(--cat-red-2, #ff6b5e);
               border-color:color-mix(in srgb, var(--cat-red-2, #ff6b5e) 45%, transparent);
               background:color-mix(in srgb, var(--cat-red-2, #ff6b5e) 10%, transparent); }
  /* The -2 variants are the PASTEL ones — they exist to sit on a dark ground.
     As text on cream, --cat-red-2 measures 1.26:1: a Delete button with
     nothing legible on it. Light mode takes the base hue instead, which is the
     variant that was tuned to be read rather than to glow. */
  :root[data-mode="light"] .btn.danger{ color:var(--cat-red); }
  :root[data-mode="light"] .btn.danger:hover{ background:color-mix(in srgb, var(--cat-red) 14%, transparent); }
  .btn.danger:hover{ background:color-mix(in srgb, var(--cat-red-2, #ff6b5e) 20%, transparent);
                     border-color:var(--cat-red-2, #ff6b5e); }
  .btn-danger:hover{ background:color-mix(in srgb, var(--red, #FF8080) 14%, transparent);
    border-color:color-mix(in srgb, var(--red, #FF8080) 60%, transparent); }
  .btn{ padding:var(--space-3) var(--space-5); border-radius:var(--radius-card); font-size:var(--type-body); font-weight:600; cursor:pointer; border:1px solid var(--glass-border-hi); background:rgba(120,120,140,.08); color:var(--text); transition:all var(--dur-fast) var(--ease-out); }
  .btn:hover:not(:disabled){ background:rgba(120,120,140,.14); }
  /* NOT A CONTROL — A LABEL SHAPED LIKE ONE, said out loud.
     -----------------------------------------------------------------------
     For the four buttons that could never work on a Lucid whose deployment
     variables are unset: Spotify's Connect, Instagram's Connect, and the two
     that follow them. Until now those were fully enabled and 400'd, which is
     this repo's oldest named prohibition committed in the open.

     It is a class of its own rather than a plain `.btn:disabled` rule on
     purpose, and the reason is blast radius: a dozen buttons in this app go
     `disabled` for a second while they work ("Counting…", "Following…"), and
     those are not unavailable, they are busy — their own changed label already
     says so. This says something permanent about this deployment.

     --text-faint rather than an opacity fade, deliberately: opacity multiplies
     against whatever is behind it and lands somewhere different in each of the
     fourteen themes, while --text-faint is a token the contrast harness
     already measures. The dashed edge is what carries "this is off" without
     making the words harder to read. The label stays legible; what goes is the
     invitation. */
  .btn.is-unavailable, .btn.primary.is-unavailable{ cursor:default; color:var(--text-faint);
    background:transparent; border:1px dashed var(--glass-border); box-shadow:none; }
  .btn.is-unavailable:hover{ background:transparent; }
  /* HALF-HEIGHT, for a button that sits INSIDE something else.
     "add contact button should be half the size it is."   (Matt, 3 September.)
     The full .btn is 39px tall and is right at the top of a page, where it is
     one of the page's own controls. The in-email "+ Add contact" is not that:
     it sits in a message header beside 11.5px lines and a badge, and at full
     size it was the heaviest thing in the header — louder than the sender.
     11.5px and 4px of padding is ~22px, and both numbers are already the
     app's own small-control sizes rather than new ones invented here. */
  .btn.sm{ padding:var(--space-1) var(--space-3); border-radius:var(--radius-control); font-size:var(--type-body); }
  .btn.primary{ background:linear-gradient(135deg, var(--accent), var(--accent-2)); border-color:transparent; color:var(--ink); box-shadow:0 4px 20px var(--accent-glow); }
  /* Muted-glow variant — same shape as .btn.primary but tinted/translucent
     instead of a solid saturated fill, matching the "flat, muted, then
     enlightened" language the rest of the redesign uses. Reserved for
     contextual, account-colored buttons (like the in-email "+ Add contact")
     rather than replacing bold CTAs like Compose/New Message, which are
     meant to stay punchy. */
  /* THE LETTERING TAKES THE LIGHT VARIANT ON A DARK THEME.
     --accent is a saturated mid-blue. On a 14% tint over a near-black panel it
     measured 4.04:1 against a 4.5 floor — the one button on the page asking to
     be pressed, and the hardest thing on it to read. --accent-2 is the same
     hue's pastel and is what every other coloured label in the app already
     uses on dark. */
  .btn.primary.tinted{
    background:color-mix(in srgb, var(--accent) 14%, transparent);
    border:1px solid color-mix(in srgb, var(--accent) 42%, transparent);
    color:var(--accent-2, var(--accent)); box-shadow:none;
  }
  /* AND IT CAME BACK, because this button sets its own colours.
     The in-email "+ Add contact" is tinted with the ACCOUNT's colour and sets
     --accent and --accent-2 to the same value inline, which defeats the fix
     above: --accent-2 is only the pastel when nobody has overridden it. So the
     lightening is done here instead of relying on a token, and it holds
     whatever colour the mailbox happens to be. */
  :root:not([data-mode="light"]) .btn.primary.tinted{
    color:color-mix(in srgb, var(--accent-2, var(--accent)) 58%, #fff);
  }
  :root[data-mode="light"] .btn.primary.tinted{
    color:color-mix(in srgb, var(--accent) 78%, #000);
  }
  :root:not([data-mode="light"]) .btn.primary.tinted{ box-shadow:0 0 18px 1px var(--accent-glow); }
  /* The glow is scaled with the button, or a half-size button keeps a
     full-size halo and stops reading as smaller at all. */
  :root:not([data-mode="light"]) .btn.primary.tinted.sm{ box-shadow:0 0 11px 1px var(--accent-glow); }
  .btn.primary:hover{ filter:brightness(1.08); }
  /* THE PRIMARY BUTTON WAS BUILT FOR A DARK APP.
     `linear-gradient(--accent, --accent-2)` with `color:var(--ink)` means dark
     text on a fill that runs from a solid colour to its PASTEL. On a dark
     theme that is a bright button with near-black lettering: correct. In light
     mode the same recipe is dark-on-mid-blue at one end and dark-on-pale at
     the other — "Send" measured 3.76:1.
     Light mode inverts it: the fill darkens across the gradient instead of
     fading out, and the lettering is the theme's own paper colour. */
  :root[data-mode="light"] .btn.primary{
    background:linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 82%, var(--neutral-2)));
    color:var(--on-accent);
  }
  /* The tinted variant keeps its wash — it is deliberately quiet — but its
     lettering is an ACCOUNT colour chosen by Matt, so it cannot be replaced,
     only deepened. Mixing toward the theme's darkest neutral keeps the hue he
     picked and buys the contrast. */
  :root[data-mode="light"] .btn.primary.tinted{
    color:color-mix(in srgb, var(--accent) 80%, var(--neutral-2));
  }
  /* AND THE WASH ITSELF HAS TO BE SAID AGAIN IN LIGHT MODE, 13 October.
     "In serene theme, many of the buttons have dark text so when there is a
     colored button (like the email button in a contact) it is not readable."
     (Matt.) The ✉ Email button on a contact is .btn.primary.tinted, and the
     solid light-mode recipe above it is written as :root[data-mode="light"]
     .btn.primary -- specificity 0,4,0, which BEATS .btn.primary.tinted at
     0,3,0. So on paper the quiet button got the full solid fill, while its
     lettering kept the deepened ACCOUNT colour from the rule just above: the
     category colour written on the category colour. It measured 1.03:1 on
     every Serene paper -- the label was not dim, it was not there. The wash is
     restored at the light rule's own weight, so the tinted variant reads the
     same way it does in the dark: a pale wash of his colour, lettered in a
     deeper shade of it. tests/test-filled-controls-contrast.js renders it. */
  :root[data-mode="light"] .btn.primary.tinted{
    background:color-mix(in srgb, var(--accent) 14%, transparent);
    border-color:color-mix(in srgb, var(--accent) 42%, transparent);
  }
  /* --on-accent: THE LETTERING ON A SOLID COLOURED FILL, ONE TOKEN, 13 October.

     Every filled control in this file was written for the dark themes, where
     --ink is near-black and the accent is bright, so "dark letters on the
     colour" was right. On paper the accents are DEEP -- they have to be, to
     read as text on cream -- and the same --ink on them measured 2.5 to 2.9:1.
     Five rules had already been corrected one at a time with --neutral-ink
     (the primary button, the calendar's selected day, the + on the month, the
     spine badge, the Ask send button); four more had not (the message bar's
     main action, a tag chip that is on, the hour toggle on Home, the player's
     play button) and there was nothing to stop a fifth.

     So the light half of the answer is one token. Light mode declares it as
     the paper's own ink, which test-serene-contrast.js already proves reads
     4.5:1 on every accent fill of every paper; every filled control on paper
     letters with it; and test-filled-controls-contrast.js renders every rule
     in this file that paints an accent fill and fails the build if any of
     them, on any light theme, puts its words under 4.5:1.

     DARK IS NOT TOUCHED. Regal and Fathom already declare --on-accent for
     their own reasons; the base :root deliberately does not, so every dark
     rule keeps the --ink it had. The fill on paper follows the primary
     button's recipe -- it DARKENS across the gradient toward the paper's
     deepest neutral rather than fading out to its pastel -- because light
     lettering on a pastel stop is the same failure the other way round. */
  :root[data-mode="light"]{ --on-accent: var(--neutral-ink); }
  /* AND THE DARK HALF OF THE SAME TOKEN, 13 October.

     The light-mode run measured the rooms it had been told to leave alone and
     found them failing the same way. Every theme is in
     tests/test-filled-controls-contrast.js now, and before this block it
     reported: his own sent texts at 2.2 to 2.9:1 on Hearth, Ink, Chocolate,
     Banked, Coal and Regal (white on a pastel accent); every primary button,
     Send, tag chip and + on Midnight and Obsidian at 4.25:1 (#031225 on
     #3D6BFF -- a blue in the middle of the range, where only a near-black
     clears 4.5); the active signature tab at 4.30:1 on Fathom; the brass
     toggle and the life call-to-action at 3.9 and 4.15:1 on Banked, whose coal
     is so dim that pure black on it measures 4.17.

     So dark mode states the token too. --on-accent is the lettering on an
     accent fill and --ink follows it, because --ink is what forty dark rules
     already letter their fills with; a theme changes one value, not forty
     rules. --on-brass is the lettering on a brass fill, which is a different
     colour from the accent on Banked, Coal, Hearth and Ink. The defaults sit in
     :where() so Regal's own --on-accent, declared in its block, still wins.
     Light mode is untouched: everything here is behind :not([data-mode="light"]). */
  :root:where(:not([data-mode="light"])){ --on-accent:#031225; --on-brass:var(--bg-deep); }
  :root:not([data-mode="light"]){ --ink:var(--on-accent); }
  /* #3D6BFF is lighter than it looks: #031225 on it is 4.25:1, white 4.43. */
  :root:not([data-mode="light"])[data-theme="midnight"],
  :root:not([data-mode="light"])[data-theme="obsidian"]{ --on-accent:#000206; }
  /* Fathom's block gives its accent a light ink (#EAEFFA). On #6389FF that is
     3.2:1; its own dark --ink reads it at 6+, so the dark ink is the token. */
  :root:not([data-mode="light"])[data-theme="fathom"]{ --on-accent:#040A18; }
  /* The coal is the one brass no dark letter can read on. */
  :root:not([data-mode="light"])[data-theme="banked"]{ --on-brass:#FFF8F2; }
  /* The signature tab darkened the accent so white would read on it, and on
     Fathom white-ish did not (4.30:1) while on Regal only a dark letter does.
     In the dark it is now drawn like every other filled control: the accent,
     lettered with the token. Matte included -- the 52%-black fill under
     [data-finish="matte"] was the same workaround. */
  :root:not([data-mode="light"]) .sig-tab.active{
    background:var(--accent); border-color:var(--accent); color:var(--on-accent); }
  :root:not([data-mode="light"]) .hs-tog button[aria-pressed="true"]{ color:var(--on-brass); }
  :root[data-mode="light"] .mb-act-go{
    background:linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 82%, var(--neutral-2)));
    color:var(--on-accent); }
  :root[data-mode="light"] .pl-btn.main{
    background:linear-gradient(135deg, var(--green), color-mix(in srgb, var(--green) 82%, var(--neutral-2)));
    color:var(--on-accent); }
  .btn-row{ display:flex; gap:var(--space-2); margin-top:var(--space-2); }
  .field{ width:100%; background:rgba(120,120,140,.08); border:1px solid var(--glass-border); border-radius:var(--radius-card); color:var(--text); font-family:var(--font-body); font-size:var(--type-lead); padding:var(--space-3) var(--space-4); margin-bottom:var(--space-3); transition:all var(--dur-fast) var(--ease-out); }
  :root:not([data-mode="light"]) .field:focus{ outline:none; border-color:color-mix(in srgb, var(--accent) 45%, var(--glass-border)); box-shadow:0 0 14px 0 var(--accent-glow); }
  .field:focus{ outline:none; border-color:color-mix(in srgb, var(--accent) 50%, transparent); }
  textarea.field{ resize:vertical; min-height:80px; font-family:var(--font-body); }
  .new-badge{ display:inline-block; font-size:var(--type-label); text-transform:uppercase; letter-spacing:0.05em; color:var(--blue); border:1px solid color-mix(in srgb, var(--blue) 40%, transparent); padding:var(--space-hair) var(--space-2); border-radius:var(--radius-control); margin-bottom:var(--space-3); }
  /* Solid blue lettering on a near-black row: 3.95:1. The pastel is what that
     variant is for. */
  :root:not([data-mode="light"]) .new-badge{ color:var(--blue-2); border-color:color-mix(in srgb, var(--blue-2) 40%, transparent); }

  /* AN ICONS GLYPH SITTING IN A LINE OF TEXT.
     265. Matt: "get rid of the carton clock on snooze." The clock was ⏰ — the
     emoji, which macOS draws as a red plastic alarm clock with two bells on
     top. It was the only thing on the mail row not drawn in this app's own
     hand, and next to a 10.5px mono timestamp it was a sticker.
     ICONS.clock has existed all along; the reason nobody used it is that an
     emoji sizes and colours itself and an inline <svg> does not. This is that
     missing piece, and nothing more: em-relative so it tracks whatever type it
     lands beside (a 13.5px button, a 10.5px badge, a 10px task line), and
     currentColor via ico(), so it is the colour of the sentence rather than
     arriving pre-painted in somebody else's palette.
     -0.16em is optical, not arithmetic: the clock face is a circle inset in a
     24-grid, so centring on the cap height sits it slightly high. */
  .ico-inline{ width:1.05em; height:1.05em; vertical-align:-.16em; flex:none; }


  /* CONTACTS */
  .chip{ font-size:var(--type-body); padding:var(--space-2) var(--space-4); border-radius:var(--radius-pill); border:1px solid var(--glass-border); color:var(--text-dim); cursor:pointer; font-weight:600; transition:all var(--dur-fast) var(--ease-out); }
  .chip.active{ background:rgba(120,120,140,.12); color:var(--text); border-color:var(--glass-border-hi); }
  :root:not([data-mode="light"]) .chip.active{ box-shadow:0 0 14px 1px var(--accent-glow); }
  /* Settings ▸ Lock (iPhone app): a phone with no passcode cannot lock, so its chips are shown and not pressable. */
  #phoneLockPicker .chip[aria-disabled="true"]{ opacity:.45; cursor:default; pointer-events:none; }
  /* THREE ACROSS, AND STILL TWO AND THEN ONE WHEN THERE IS NO ROOM FOR THREE.
     "make contact and company cards 3 across, not 2." — 3 September.

     It already asked for three. What it did not ask was the right question: the
     count was fixed and the fallbacks hung off @media(max-width:1180px), and
     1180 is a VIEWPORT width while this grid lives in the content column, which
     is about 144px narrower than the window once the spine is beside it. So a
     window anywhere under 1180 dropped to two cards even though the column
     could hold three of them at 340px each — which is what he was looking at.

     Track sizing rather than a column count fixes the cause instead of moving
     the breakpoint: the grid now asks how wide its own COLUMN is, not how wide
     the screen is. Three cards from a content column of 880px (about a 1024px
     window), two from 580px, one below that — so it degrades the way it always
     did, and it cannot overflow, which is what a hardcoded three-up would do on
     a 1000px screen and would be a worse bug than the one being fixed.

     READ THE TRACK SIZE FROM THE INSIDE OUT.

       (100% - var(--cl-gap) * 2) / 3   one third of the column, gaps removed —
                                        a floor this high means three tracks is
                                        the MOST that can ever fit. Three is
                                        what he asked for; a bare auto-fill put
                                        four across a 1600px window, which is
                                        not what "3 across" means.
       max(280px, …)                    …unless a third would be narrower than
                                        280px, in which case the floor rises and
                                        only two, then one, will go.
       min(100%, …)                     and never wider than the column itself,
                                        or a 250px phone gets a 280px card
                                        hanging off the edge of it.

     auto-fill, not auto-fit: a last row with two cards on it keeps them
     card-sized instead of stretching them across half the page — which is what
     repeat(3, 1fr) already did, and what a grid of cards should do.

     Nothing else shares this rule, and both ids are surfaces he named:
     #contactList is In Touch's "All people" and #organizationList is
     Organizations. .contact-row is drawn in only those two places. */
  /* SUPERSEDED, AND KEPT ONLY AS THE RECORD OF WHY THREE. Every declaration
     below is overridden by the .tile-grid rule near the bottom of this file,
     which names these two ids and therefore matches at the same specificity
     and wins on source order. What the browser actually reports on
     #contactList at a 1100px window is gap:18px and max-width:938px, from
     --tile-gap and --tile-span-2 — not the 20px and the absent cap declared
     here. The ladder that replaced it puts a CEILING on the card as well as a
     floor, so at 1100 it holds two cards at --tile-max rather than shaving
     three to 299px; read the long note above .tile-grid for the reasoning.
     A harness spent a week asserting the behaviour of these four lines, so
     the supersession is written down rather than left to be re-derived. */
  #contactList, #organizationList{ display:grid; --cl-gap:20px; gap:var(--cl-gap);
    grid-template-columns:repeat(auto-fill,
      minmax(min(100%, max(280px, (100% - var(--cl-gap) * 2) / 3)), 1fr));
    align-items:start; }
  /* THE ORB'S KEEP-OUT, ON THE PAGE RATHER THAN ON THE GRID OR THE CARD.
     (Row 2022.)
     .cc-head puts a 17px tick-box in the top-right corner of every card, and
     the right-hand column of this grid runs to the edge of the page — so at
     1500×950 the box on whichever card is passing the bottom corner is ENTIRELY
     under the sphere: measured, nine presses out of nine landing on the orb. A
     tick-box you cannot tick is the plainest version of a control that looks
     like it works and does not, and Tag them, Set a category and Pick all shown
     are three more controls that quietly depend on this one.
     THREE PLACES IT COULD GO, AND ONLY ONE OF THEM LOOKS RIGHT. On the card:
     82px out of .cc-head strands the box a quarter of the way across a header.
     On the grid alone: the cards then stop 82px short of the search field
     directly above them, and a column of cards misaligned with the box they are
     filtered by is worse than the fault. On the PAGE: the heading, the search,
     the sort row and the cards all move together, the gutter is simply wider,
     and nothing is out of line with anything.
     THIS IS THE GUTTER-DOWN-THE-PAGE ANSWER THAT WAS REJECTED FOR .main, and it
     is right here for the reason it was wrong there: the thing it broke on Home
     was .home-band, a full-bleed hero that reflowed. These three pages are one
     column of cards under a toolbar and have nothing to break.
     The cost is honest and it is here: below about 1100px the grid's own 280px
     floor means the narrower box holds two columns where it used to hold three.
     Two columns of readable cards beats three with a dead tick-box on one. */
  #view-comms, #view-contacts, #view-organizations{ padding-inline-end:var(--orb-keepout-x); }
  .contact-row{ display:flex; flex-direction:column; gap:var(--space-3); padding:var(--space-5); cursor:pointer; border-left:none; position:relative; transition:box-shadow var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out); }
  .cc-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-3); }
  .contact-row .avatar{ width:52px; height:52px; font-size:16px; }
  .contact-row:hover{ border-color:var(--glass-border-hi); }
  .contact-name{ font-size:var(--type-lead); font-weight:600; }
  .contact-org{ font-size:var(--type-body); color:var(--text-dim); }

  /* ==========================================================================
     LIST MODE — the other half of the toggle above these two grids.
     ==========================================================================
     "toggle on contacts and companies to switch to list mode."  (Matt, 4 Sept.)

     See the block above setViewMode() in app.js for why the choice sticks. This
     is what it switches TO, and the thing it is not is the card made thinner: a
     card scaled down keeps the padding, the avatar, the tags and the notes and
     simply gives you less of each. What a list is for is running the eye down
     ONE column, so there are three of them, they line up, and everything that
     does not help you find a name is gone.

     THE PANE IS THE CONTAINER AND THE ROWS ARE DRAWN ON IT — the idiom the mail
     list has always used, and the reason is arithmetic rather than taste: a
     hundred and forty .glass rows is a hundred and forty backdrop-filters to
     composite on every scroll, and stacked with no gap they read as a deck of
     cards rather than as a list. The container takes .glass in JS and the rows
     take nothing.

     THE ID CARRIES THE CLASS so this beats the grid rule above it without an
     !important — #contactList.cl-list is one id and one class against one id. */
  #contactList.cl-list, #organizationList.cl-list{ display:block; padding:var(--space-2) var(--space-2) var(--space-2); }
  /* cl-row AND NOT list-row, WHICH IS WHAT IT WAS CALLED FOR AN HOUR.
     .list-row already exists in this file — about seven hundred lines below,
     a dead mockup-era rule kept because the pre-split page in attic/ still
     loads this stylesheet. It is `display:flex; gap:12px; padding:13px 15px`,
     it has the SAME specificity as this rule, and it comes LATER, so it won
     every one of these declarations. The result was not a broken screen, which
     is why it needed measuring to find: three flex items in a row look very
     like three grid columns until you notice they are content-sized, that the
     date column does not line up down the page, and that the narrow layout
     never arrives because the grid-row placements have nothing to place into.
     Caught by shots-list-mode.js printing the used track sizes.

     The heading and the rows share one grid definition so the columns cannot
     drift apart. minmax(0,…) on the flexible tracks because the default `auto`
     minimum refuses to shrink below the longest name and pushes the date column
     off the end of a narrow window. */
  .cl-head, .cl-row{
    display:grid; grid-template-columns:minmax(0,1.5fr) minmax(0,1fr) 92px;
    gap:var(--space-3); align-items:center; padding:var(--space-2) var(--space-3); }
  .cl-head{
    font-size:var(--type-label); font-weight:700; letter-spacing:.05em; text-transform:uppercase;
    color:var(--text-faint); border-bottom:1px solid var(--glass-border);
    padding-top:var(--space-2); padding-bottom:var(--space-2); }
  .cl-row{ cursor:pointer; border-radius:var(--radius-card); font-size:var(--type-body);
    transition:background var(--dur-fast) var(--ease-out); }
  /* A HAIRLINE BETWEEN ROWS, NOT AROUND THEM. inset box-shadow rather than a
     border so the row's height never changes when it is hovered or when the
     first row (which has no line above it) is drawn. Same neutral scrim the
     mail rows and the chips already use — it is a value that works on all
     fourteen grounds precisely because it is a grey with alpha and not a
     colour, which is why it is not a token. */
  .cl-row + .cl-row{ box-shadow:inset 0 1px 0 rgba(120,120,140,.20); }
  .cl-row:hover{ background:rgba(120,120,140,.08); }
  .cl-row .lr-who{ display:flex; align-items:center; gap:var(--space-2); min-width:0; }
  .cl-row .avatar{ width:26px; height:26px; font-size:10px; }
  .lr-name{ font-weight:600; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .lr-org{ color:var(--text-dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .lr-org em{ font-style:normal; }
  .lr-when{ color:var(--text-dim); font-size:var(--type-body); text-align:right; white-space:nowrap; }
  /* Not an absence of ink — a mark that says "there is no date here", with the
     reason on hover. See lastTouchCell() for the three different silences it
     stands in for and why none of them is printed 140 times down the page.
     --text-dim AND NOT --text-faint, which is what it was written as first and
     what the readability harness caught: a 12px em dash in --text-faint
     measured 4.47:1 on `light`, three hundredths under the line. The quietness
     is carried by the GLYPH — an em dash next to a column of dates is already
     obviously not one — rather than by taking ink out of it, which is the
     lesson this harness has taught the file several times. */
  .lr-none{ color:var(--text-dim); }
  /* The halo means the same thing here as on a card: one of the people he
     speaks to most. The rings are box-shadows so they scale to any avatar. */
  .cl-row.inner-circle .lr-name{ color:var(--text); }
  .cl-list > .section-label{ margin:var(--space-4) var(--space-3) var(--space-1); }
  .cl-list > .section-label:first-child{ margin-top:var(--space-2); }

  /* THE TOGGLE ITSELF, AND WHY IT IS LOUDER THAN AN ORDINARY CHIP ROW.
     "whichever mode is on must be unmistakable" — a toggle whose state you
     cannot read is a control that looks like it does nothing. .chip.active is a
     12% neutral tint, which is right for a row of five filters where the lit
     one is read against its four neighbours; with exactly two chips and no
     third to compare against, a 12% grey is a chip that might just be hovered.
     So the live half takes the accent, which is the one colour in this app that
     always means "this one". Mixed with transparent rather than painted solid
     so the text keeps its own token and the contrast harness is measuring ink
     on the page's real ground in all fourteen themes. */
  .sort-and-shape{ display:flex; flex-wrap:wrap; align-items:center;
    justify-content:space-between; gap:var(--space-3); margin-bottom:var(--space-4); }
  .sort-and-shape .chip-select{ margin-bottom:0; }
  .view-mode{ flex:none; }
  .view-mode .chip{ min-width:66px; text-align:center; }
  .view-mode .chip.active{
    background:color-mix(in srgb, var(--accent) 22%, transparent);
    color:var(--text);
    border-color:color-mix(in srgb, var(--accent) 60%, transparent); }
  .view-mode .chip:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

  /* NARROW: THE COMPANY DROPS UNDER THE NAME rather than being squeezed into a
     column too thin to hold a word. Two columns and two rows — the date keeps
     its own column and spans both, so it stays a straight edge down the right
     which is the thing that makes a date column worth having. The heading loses
     its middle label, because a label over a column that is no longer there is
     worse than no label. 900px and not 700: this grid lives in the content
     column, which is about 144px narrower than the window once the spine is
     beside it — the same correction the card grid above documents. */
  @media (max-width: 900px){
    .cl-head{ grid-template-columns:minmax(0,1fr) 84px; }
    .cl-head .lh-org{ display:none; }
    .cl-row{ grid-template-columns:minmax(0,1fr) 84px; row-gap:1px; }
    .cl-row .lr-who{ grid-column:1; grid-row:1; }
    .cl-row .lr-org{ grid-column:1; grid-row:2; font-size:var(--type-body); }
    .cl-row .lr-when{ grid-column:2; grid-row:1 / span 2; }
  }
  /* A CATEGORY COLOUR IS PAINTED ON THE PANEL, AND THE PANEL CHANGES.
     --cat-blue is the solid variant and --cat-blue-2 the pastel; which one can
     be read depends entirely on what is behind it. "· Prospect" measured
     2.78:1 on midnight because it used the solid one on a near-black row.
     One token per category that already knows which ground it is on — the
     bare --cat-* names stay exactly as they are for fills, borders and glows,
     where the solid colour is still the right answer. */
  :root{
    --cat-green-text:var(--cat-green-2);   --cat-blue-text:var(--cat-blue-2);
    --cat-violet-text:var(--cat-violet-2); --cat-orange-text:var(--cat-orange-2);
    --cat-red-text:var(--cat-red-2);       --cat-teal-text:var(--cat-teal-2);
    --cat-gold-text:var(--cat-gold-2);     --cat-rose-text:var(--cat-rose-2);
    --cat-slate-text:var(--cat-slate-2);   --cat-moss-text:var(--cat-moss-2);
  }
  :root[data-mode="light"]{
    --cat-green-text:var(--cat-green);     --cat-blue-text:var(--cat-blue);
    /* --cat-orange-text takes the same 82% pull as --orange-text above, for
       the same reading: the home lane figure on a failed lane measured 4.32:1
       on Serene Mist Cool, #98642f on #edefea. The violet is left as it is --
       nothing measured tonight came near the floor on it, and a token nobody
       has a number for is not a token to change. */
    --cat-violet-text:var(--cat-violet);
    --cat-orange-text:color-mix(in srgb, var(--cat-orange) 82%, var(--text));
    --cat-red-text:var(--cat-red);         --cat-teal-text:var(--cat-teal);
    --cat-gold-text:var(--cat-gold);       --cat-rose-text:var(--cat-rose);
    --cat-slate-text:var(--cat-slate);     --cat-moss-text:var(--cat-moss);
    /* THE FOUR NEWER CATEGORIES HAD NO PAPER OF THEIR OWN, 13 October.
       Partner, Supplier, Advisor and Past were added with one set of solids
       written for the dark room and never given a light one, so on every
       paper they painted the dark theme's bright gold and moss. The six older
       categories are deepened per palette; these four are not part of the
       palettes, so they are deepened here, once, along their own hues. The ✉
       Email button on a Partner measured 3.57:1 lettered in it, a Supplier
       4.01; these read 5.3 to 6.0 on the darkest paper, and the paper ink on
       them as a fill clears 4.5 as well. Pastels and glows are untouched. */
    --cat-gold:#7A5E12;  --cat-rose:#9E3A68;
    --cat-slate:#4F5F78; --cat-moss:#506826;
  }
  /* THE REST OF THE TEN. Family had no chip rule at all, which is why it was
     the one category that rendered unstyled — and the four new ones need theirs
     for the same reason. Same 16% wash so no category shouts over another. */



      
  /* ==========================================================================
     IN TOUCH → RECENT, AS THE SAME CARDS ALL PEOPLE DRAWS — rows 1738, 1741.
     ==========================================================================
     "The Recent tab in contacts shows long blocks. Should be three across,
      like All People."

     THE TRACK DEFINITION BELOW IS A DELIBERATE COPY of the one on
     `#contactList, #organizationList` about 250 lines above this, and that is
     worth saying out loud rather than leaving for somebody to find. Extending
     that rule with a third selector would mean editing a line another pane
     also depends on; duplicating it means two places can drift apart. The
     second risk is the smaller one and it is GUARDED: tests/test-people-
     screens.js reads both declarations out of this file and fails if they are
     not character-for-character the same, so a change to the card grid cannot
     silently leave this page one column wide.

     Everything else about these cards IS .contact-row — same padding, same
     52px avatar, same name weight — because the whole point of the row is
     that Recent and All People are two views of the same people and should
     not look like two products. */
  .people-grid{ display:grid; --cl-gap:20px; gap:var(--cl-gap);
    grid-template-columns:repeat(auto-fill,
      minmax(min(100%, max(280px, (100% - var(--cl-gap) * 2) / 3)), 1fr));
    align-items:start; }

  /* ==========================================================================
     ONE PERSON, TWO CARDS — the comparison screen, row 1739.
     ==========================================================================
     What this replaces was two buttons, "Keep Dan" and "Keep Daniel", on a row
     that showed neither card. Both had the same consequence and neither said
     what it was.

     THE LAYOUT IS THE ARGUMENT. Three columns: a label, and then the two
     cards' answers in the same two columns all the way down, so the eye runs
     straight down each card rather than pairing values by hand. The two heads
     sit over their own columns for the same reason — which column is which
     has to be true for the whole table, not remembered from the top.

     WHICH ONE SURVIVES IS SAID IN WORDS, not only in colour. "Survives" and
     "Goes" are on the heads; the accent only reinforces them. A decision this
     size must not depend on somebody seeing a tint. */
  /* CAPPED, for the reason the add-contact form is capped: nothing on this
     panel gets better at 1,780 pixels. Two columns of about 440 hold a home
     address and a line of notes, which are the widest things here, and beyond
     that the eye is just travelling further between a value and its pair —
     which is the complaint the whole wave started from. */
  .dup-panel{ padding:var(--space-5) var(--space-5) var(--space-5); margin-bottom:var(--space-4); max-width:980px; }
  /* THE HEADS STICK. This table is nineteen fields long on a full pair of
     cards, and which column is which is the one thing every row depends on —
     scrolled past the heads, "Daniel" and "Dan" in two columns is a coin
     toss. Sticky rather than repeated, because a repeating header inside one
     panel reads as two panels.

     The background is opaque on purpose: .glass is translucent, and a
     see-through header with rows sliding under it is worse than no header. */
  .dup-heads{ display:grid; grid-template-columns:1fr auto 1fr; gap:var(--space-3);
    align-items:stretch; margin:var(--space-4) 0 var(--space-2);
    position:sticky; top:0; z-index:3;
    background:var(--bg-mid); padding:var(--space-2) 0 var(--space-3);
    box-shadow:0 1px 0 var(--glass-border); }
  .dup-arrow{ align-self:center; color:var(--text-faint); font-size:var(--type-title); }
  .dup-head{ display:flex; align-items:center; gap:var(--space-3); min-width:0; text-align:left;
    padding:var(--space-3) var(--space-3); border-radius:var(--radius-card); cursor:pointer; font:inherit;
    border:1px solid var(--glass-border); background:rgba(120,120,140,.06);
    color:var(--text); transition:border-color var(--dur-fast) var(--ease-out); }
  .dup-head:hover{ border-color:var(--glass-border-hi); }
  .dup-head:focus-visible{ outline:2px solid var(--ring); outline-offset:2px; }
  .dup-head.is-keeper{ border-color:color-mix(in srgb, var(--accent) 60%, transparent);
    background:color-mix(in srgb, var(--accent) 10%, transparent); }
  .dup-head .avatar{ width:38px; height:38px; font-size:var(--type-body); flex:none; }
  .dup-head-who{ display:flex; flex-direction:column; gap:var(--space-hair); min-width:0; flex:1; }
  .dup-head-name{ font-size:var(--type-body); font-weight:700;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .dup-head-sub{ font-size:var(--type-body); color:var(--text-dim);
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .dup-head-tag{ flex:none; font-size:var(--type-label); font-weight:700; letter-spacing:.06em;
    text-transform:uppercase; color:var(--text-faint); }
  .dup-head.is-keeper .dup-head-tag{ color:var(--accent); }

  .dup-rows{ margin-top:var(--space-3); }
  .dup-row{ display:grid; grid-template-columns:1fr auto 1fr; gap:var(--space-3);
    align-items:stretch; padding:var(--space-hair) 0; }
  /* The label sits ABOVE the two values rather than in a column of its own,
     because a fourth column at this width leaves each value about 150px and a
     home address does not fit in 150px. It spans the grid and the two options
     sit under it — which also means a narrow window needs no media query. */
  .dup-label{ grid-column:1 / -1; font-size:var(--type-label); font-weight:700;
    letter-spacing:.05em; text-transform:uppercase; color:var(--text-faint);
    display:flex; align-items:center; gap:var(--space-2); margin-top:var(--space-3); }
  .dup-differs{ font-size:var(--type-label); letter-spacing:.04em; padding:var(--space-hair) var(--space-2); border-radius:var(--radius-pill);
    background:color-mix(in srgb, var(--cat-orange) 18%, transparent);
    color:var(--cat-orange-text); }
  /* The two answers. A button, because it is one, and a big one — the whole
     value is the target rather than a radio dot beside it. */
  .dup-opt{ text-align:left; font:inherit; font-size:var(--type-body); line-height:1.4;
    padding:var(--space-2) var(--space-3); border-radius:var(--radius-control); cursor:pointer; min-width:0;
    border:1px solid var(--glass-border); background:rgba(120,120,140,.06);
    color:var(--text-dim); word-break:break-word;
    transition:border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
  .dup-opt:hover{ border-color:var(--glass-border-hi); color:var(--text); }
  .dup-opt:focus-visible{ outline:2px solid var(--ring); outline-offset:2px; }
  /* THE CHOSEN ONE IS MARKED WITH A TICK AS WELL AS A TINT, for the same
     reason the heads carry a word: this is a decision about somebody's
     records and it must not be legible only in colour. */
  .dup-opt.is-on{ color:var(--text); font-weight:600;
    border-color:color-mix(in srgb, var(--accent) 65%, transparent);
    background:color-mix(in srgb, var(--accent) 12%, transparent); }
  .dup-opt.is-on::before{ content:'✓ '; color:var(--accent); font-weight:700; }
  /* PLACED BY CLASS, NOT BY POSITION. These were :nth-of-type(2) and (3) for
     about ten minutes, which counts among siblings OF THE SAME TAG — the
     label above them is a div, so the two buttons are type 1 and 2 and the
     second rule matched nothing at all. The result was both answers stacked in
     the left column under a head that said the right one was over there, which
     is a screen actively lying about which card a value came from. Found by
     looking at it. */
  .dup-opt-keep{ grid-column:1; }
  .dup-opt-gone{ grid-column:3; }
  /* Where the two cards agree, or only one of them has anything. One value,
     spanning both columns, quieter — it is not a question. */
  .dup-agreed{ grid-column:1 / -1; font-size:var(--type-body); color:var(--text-dim);
    padding:var(--space-hair) 0 0; word-break:break-word; }
  .dup-note{ margin-left:var(--space-2); font-size:var(--type-label); color:var(--text-faint); }
  .dup-empty{ font-style:italic; color:var(--text-faint); }

  /* THE PROMISE, IMMEDIATELY ABOVE THE BUTTON THAT KEEPS IT. Every address and
     every number named, because a promise about somebody's own data is worth
     nothing in the abstract — "nothing will be lost" is what every merge in
     every product says. */
  .dup-keeps{ margin-top:var(--space-5); padding:var(--space-3) var(--space-4); border-radius:var(--radius-card);
    border-left:3px solid var(--cat-green);
    background:color-mix(in srgb, var(--cat-green) 9%, transparent);
    font-size:var(--type-body); line-height:1.55; color:var(--text-dim); }
  .dup-keeps-line + .dup-keeps-line{ margin-top:var(--space-2); }
  .dup-keeps-line b{ color:var(--text); }
  .dup-keeps-moves{ color:var(--text-faint); }

  @media (max-width: 700px){
    .dup-heads, .dup-row{ grid-template-columns:1fr; }
    .dup-arrow{ display:none; }
    .dup-opt-keep, .dup-opt-gone{ grid-column:1; }
  }

  /* PROFILE */
  /* THE ONE WAY BACK, AND IT WAS FIFTEEN PIXELS TALL.
     Measured on a phone this was 82 × 15 — the height of its own line of text,
     because a bare flex row has no padding of its own. It is the primary way
     out of a contact, so on a phone it was the hardest thing on the screen to
     hit and the one most needed. The negative left margin keeps the arrow
     optically aligned with the heading below it, so the target grows without
     the layout moving. */
  .profile-back{ display:flex; align-items:center; gap:var(--space-2); color:var(--text-dim); font-size:var(--type-body);
    cursor:pointer; margin:0 0 var(--space-3) -10px; padding:var(--space-3); border-radius:var(--radius-card);
    min-height:42px; box-sizing:border-box; width:fit-content;
    -webkit-tap-highlight-color:transparent; }
  .profile-back:hover{ color:var(--text); background:rgba(120,120,140,.10); }
  .profile-back:active{ background:rgba(120,120,140,.18); }
  /* THE ONE PRESS ON THE SETTINGS PAGE, HELD TO THE SAME FLOOR AS THE ONE
     ABOVE IT. A bare .btn is 11px of padding around a 13.5px line, which
     measures 39 — one pixel under the 40 that tests/test-mobile.js holds every
     control to, and it went red there rather than being noticed on a phone. It
     is the only thing to press on that page, so it takes .profile-back's 42
     rather than the general button size. */
  #profileContent .btn{ min-height:42px; }
  /* Two things here were wrong and together they pushed the right-hand boxes
     clean off the screen, which is the "can't see the rest of the contact card"
     with a horizontal scrollbar running to infinity.
       · `520px` is a FIXED track. Once the window is narrower than
         (left min-content + 18 + 520) the grid simply overflows its parent —
         it has no permission to shrink. `minmax(0, 520px)` makes 520 a maximum
         rather than a floor, so it gives ground instead of overflowing.
       · `1fr` carries an implicit `min-width:auto`, i.e. a min-content floor.
         One unbreakable run in an email preview — and previews are full of
         non-breaking spaces and long URLs — sets that floor to the width of the
         whole run. `minmax(0, 1fr)` removes the floor.
     Measured: the old rule started overflowing at ~1030px of viewport, while
     the single-column fallback below did not fire until 900px. That ~130px gap
     is exactly where Matt's window was. */
  /* Container-based, NOT viewport-based, and that distinction is the bug.
     A @media query measures the real browser window. This app runs inside a
     scale wrapper for the text-size setting (`transform:scale()` on
     #appZoomRoot — see 138 and applyFontScale in app.js; it was CSS `zoom`
     until then and neither one affects media queries) — so at 110% text size
     the window still reads as (say) 1140px while the layout inside it only
     has 1035px to work with. The breakpoint never fires, the two columns
     don't fit, and the right-hand boxes go off the edge. Raising the
     breakpoint only moves where that happens.
     auto-fit + minmax asks the CONTAINER how much room there actually is, so
     it is correct at every window size and every scale level, with no
     breakpoint to get wrong. Two columns when two fit, one when they don't.

     AND THAT IS STILL THE PRINCIPLE — ROW 2160 CHANGED THE SHAPE, NOT THE RULE.
     ------------------------------------------------------------------------
     "I need the timeline twice as wide and text to be larger (I think 3rd
      request)" … "A bunch of empty space on the contact card n far right. Move
      card row over to right and expand timeline length and size."
                                                      (Matt, 15 September.)

     MEASURED BEFORE ANYTHING WAS TOUCHED, at 1440 on a real browser: this grid
     was 1290px wide and it had opened FOUR TRACKS OF 309px for TWO children.
     The timeline got 309, the boxes got 309, and 654px — half the page — was
     two empty columns down the right-hand side. That is his sentence, exactly,
     and it is not a width somebody chose: .profile-grid had been swept into the
     CARD-GRID selector at the foot of this file (row 1752/2146), which fills a
     row with as many card-width tracks as will fit. A two-pane page layout is
     not a row of cards. The comment on that rule already lists .today-grid,
     .cal-body, .gv-wrap, .settings-cols and the rest as deliberately left out
     for that reason; this one was inside the list by mistake, so it is out of
     it now and states its own proportions here.

     FOUR TO ONE, WITH A FLOOR AND A GUTTER, and no breakpoint — flex-wrap asks
     the container the same question auto-fit did. The pane takes four fifths of
     what is spare, the boxes one fifth, and the boxes never go under --tile-min
     because below that they stop being cards. The pane's flex-basis is what
     decides when the two stop sharing a line: two ideal cards and the gutter
     between them, which is the narrowest a pane can be and still be worth
     having a column of its own. Under that the boxes wrap below the timeline
     and both take the whole width, which is what they did before.

     MEASURED AFTER, same browser, same fixture: 1440 — timeline 309 → 932
     (3.0×), boxes 309 → 340, nothing empty. 1280 — 365 → 804 (2.2×). 1100 —
     305 → 660 (2.2×). 700 — 325 → 668 stacked (2.1×). 400 is one column at both
     ends and cannot be doubled; it was already the whole page. */
  .profile-grid{ display:flex; flex-wrap:wrap; gap:var(--tile-gap); align-items:flex-start;
    max-width:var(--tile-span); }
  .profile-grid > *{ min-width:0; }
  .profile-grid > :first-child{ flex:4 1 var(--pane-min); }
  /* The cards column. No max-width: the ratio already keeps it under
     --tile-max at every width this grid can reach (361px at the widest, which
     is the --tile-span ceiling), and a max-width here would leave a strip of
     nothing beside it on the wrapped, single-column phone layout — which is
     the bug this row is about, reintroduced one line lower down. */
  .profile-grid > .box-grid{ flex:1 1 var(--tile-min); min-width:var(--tile-min); }
  /* auto-fit means the two boxes drop to one column on their own once there
     isn't room, instead of squeezing to unreadable slivers. */
  .box-grid{ display:grid; grid-template-columns:repeat(auto-fit, minmax(210px, 1fr)); gap:var(--space-4); }
  .box-grid .box{ margin-bottom:0; min-width:0; }
  /* THE CATEGORY DROPDOWN WAS CUT OFF, AND .glass IS WHY.
     Matt, 25 Aug: "category dropdown is cutoff on contact card."
     .glass is `overflow:hidden` — it has to be, or the sheen layer and the
     rounded corners come apart. The Category menu is an absolutely
     positioned .from-menu inside one of these boxes, so it was clipped at
     the box edge: the first item or two showed and the rest was simply not
     painted. Nothing was wrong with the menu.
     Only the boxes that CONTAIN a menu are opened up, matched on the
     selector rather than by adding a class to the markup, so any future box
     with a dropdown in it is fixed the day it is written. The rest keep the
     clip, and with it their corners. */
  .box-grid .box:has(.from-menu){ overflow:visible; }
  /* And it must be able to draw over the box below it as well as outside
     its own edge — grid items paint in source order, so without this the
     menu appears behind the next card down. */
  .box-grid .box:has(.from-menu.open){ z-index:5; }
  .box-grid{ position:relative; }

  /* ==========================================================
     AND THE SAME BUG, EVERYWHERE ELSE, ONCE.

     This is the THIRD time a menu inside .glass has been found
     invisible and fixed one element at a time:

       25 Aug  "category dropdown is cutoff on contact card"
               -> .box-grid .box:has(.from-menu){ overflow:visible }
       31 Aug  "The action button... I have not been able to open
               since you rolled into a chevron"
               -> #mailToolbarFull{ overflow:visible }

     Neither was a JavaScript fault. Both menus opened correctly
     every single time, into a region the browser had been told
     to clip. No error, no console line, nothing to find - which
     is why each one cost a report and a day of looking.

     A walk of index.html finds THIRTEEN absolutely-positioned
     menus nested inside a .glass ancestor. Two were fixed. The
     other eleven - every dropdown in the compose modal, the
     event editor, the task editor, both category pickers - are
     the same construction waiting for the same report.

     So: one rule, keyed on the menu being OPEN. The clip returns
     the moment it closes, so the rounded corners and background
     layer that overflow:hidden protects are unaffected for the
     whole of the time no menu is showing.

     Every popup in Lucid is opened by a CLASS - some on
     themselves, some on a wrapper - which is what makes this
     expressible at all. A menu shown by an inline style would
     have to be added here by hand, and the assertion in
     tests/test-nothing-is-clipped-away.js is what says so: it
     reads the popup classes out of THIS file and goes red on any
     that is not covered below.

     The sidebar is deliberately NOT included. It sets
     overflow-y:auto and overflow-x:hidden because it scrolls, so
     lifting the clip there is a different change with a
     different risk, and it wants looking at on a screen rather
     than reasoning about in a stylesheet. Written up rather than
     guessed at.
     ========================================================== */
  .glass:not(.sidebar):has(.from-menu.open),
  .glass:not(.sidebar):has(.chip-input-suggestions.open),
  .glass:not(.sidebar):has(.snooze-menu.open),
  .glass:not(.sidebar):has(.acct-switcher.open),
  .glass:not(.sidebar):has(.tb-more-wrap.open),
  .glass:not(.sidebar):has(.st-drop.open){ overflow:visible; }

  /* ==========================================================
     AND THE FOURTH TIME, WHICH IS THE SAME BUG ONE LAYER IN:
     THE MENU THAT IS NOT CLIPPED AND IS STILL NOT VISIBLE.
     (Row 1828.)

     Matt, 11 September, cursor on the chevron beside "1 open":
     "I cant see what is already rolled up in it because it is
      once again blocked by the email window sitting in front of
      it."

     "ONCE AGAIN" IS THE WHOLE INSTRUCTION. The three fixes above
     lifted the CLIP; this one is about the ORDER things paint
     in, and every menu the rule above rescued has it.

     MEASURED, NOT REASONED. With the effects set to Full,
     opening the chevron and hit-testing the middle of its own
     menu returns #readingPane — the reading pane is what a click
     there would land on, so the menu is not merely dim, it is
     behind. Set the effects to Lite and the same hit-test
     returns the menu. That difference is the diagnosis: .glass
     carries backdrop-filter, backdrop-filter makes a STACKING
     CONTEXT, and a stacking context traps its children's
     z-index inside itself. So .tb-more-menu's z-index:60 is not
     60 against the page — it is 60 inside the toolbar, and the
     toolbar itself is a position:relative card at z-index:auto
     that paints in document order, which is BEFORE the reading
     pane card that comes after it in the markup. The menu could
     carry z-index:9999 and still lose.

     So the lift belongs on the CARD, not on the menu, and it is
     keyed on exactly the same "a menu is open inside me"
     condition as the clip above — one statement, every menu,
     rather than a fourth bespoke selector.

     WHY 5 AND NOT 60. It only has to beat its own siblings, and
     a positioned element with any positive z-index paints after
     every positioned sibling left at auto, so 5 does the whole
     job. Going higher would start winning fights it has no
     business in: .bottom-nav is the fixed phone navigation at
     z-index:10, and a tall card that happened to have a menu
     open would draw straight over it. 5 is above every card and
     below the furniture.
     It is also the number already in this file. Scroll up forty
     lines: `.box-grid .box:has(.from-menu.open){ z-index:5 }`
     was written for the contact card in August, for the same
     reason in a grid rather than a flex row — "grid items paint
     in source order, so without this the menu appears behind the
     next card down". That fix was right and it was one card
     wide; this is the same fix made general.
     ========================================================== */

  .profile-header{ display:flex; gap:var(--space-5); align-items:center; padding:var(--space-6); margin-bottom:var(--space-4); }
  .tab{ padding:var(--space-2) var(--space-4); border-radius:var(--radius-control); font-size:var(--type-body); font-weight:600; color:var(--text-dim); cursor:pointer; }
  .tab.active{ background:color-mix(in srgb, var(--blue) 18%, transparent); color:var(--text); box-shadow:0 0 0 1px color-mix(in srgb, var(--blue) 35%, transparent); }
  .timeline-item{ display:flex; gap:var(--space-4); padding:var(--space-5) var(--space-5); margin-bottom:var(--space-3); min-width:0; }
  /* A TEXT ON THE TIMELINE.
   *
   * "and where are the texts"  (Matt, 26 August.)
   *
   * Smaller than an email, and that difference is the information. An email is
   * a subject you open; a text is one line that is already the whole thing. Row
   * after row of identical cards would make eleven thousand texts and forty
   * emails read as one undifferentiated pile, which is the state he was already
   * complaining about on the Media page.
   *
   * DIRECTION IS A LEFT EDGE, because that is what an account colour is
   * everywhere else in Lucid — teal for theirs, the accent for yours, matching
   * the two markers on the how-you-know line above. "We spoke Tuesday" and "I
   * sent three and heard nothing" are the same row without it. */
  /* The 2px left edge that used to be here is gone — see the full rim
     further down. "rim the texts with the color (blue/green) not just the
     left side." Leaving it would have put a second, differently coloured
     stripe down one side of the bubble. */
  .tl-text{ padding:var(--space-3) var(--space-4); margin-bottom:var(--space-2); gap:var(--space-3); }
  /* The dimmed, shrunken chat icon that used to live here is gone — the ring
     carries a monogram at full strength now instead of a faded glyph. See
     .tl-avatar, further down, for the replacement. */
  .tl-text .timeline-text{ font-size:var(--type-body); line-height:1.5; }
  /* The words wrap and are never clipped — a text truncated at one line is a
     text you have to open something else to read, which is the fault this whole
     screen exists to fix. */
  .tl-said{ overflow-wrap:anywhere; white-space:pre-wrap; }
  /* --no-text mode: the fact of a message without its words. Said as a phrase
     rather than left blank, so an empty row never reads as a bug. */
  .tl-nowords{ color:var(--text-faint); font-style:italic; }
  /* THE KIND OF CONTACT, AS A COLOUR AND AN ICON.
   *
   * "illuminate the message type (email, text (icons) and have a color denote
   *  the type of message. (video call would have its own color)"
   *
   * --tl-kind is set per row from TL_KIND in app.js, so a new kind is one line
   * of JavaScript and nothing here. The colour lands on the ICON rather than on
   * the text, because the icon is the part being classified — tinting the words
   * would make a whole card read as coloured noise, which is the complaint that
   * started this. */
  .tl-row .timeline-icon{ color:var(--tl-kind, var(--text-dim)); }
  .tl-row .timeline-icon svg{ width:16px; height:16px; }

  /* AND THE CARD ITSELF CARRIES THE KIND, not only the icon.
   *
   * 1 September. Matt: "I want text messages in the timeline to either have a
   * different color card, or different color glowing rim. (whatever is best
   * and not too noisy)".
   *
   * A rim rather than a glow, and on every kind rather than only on texts.
   * A glow on each of forty text rows is a lit-up wall — the exact noise he
   * warned about — while one coloured edge reads at a glance and disappears
   * when you are not looking for it. Doing it for every kind costs nothing
   * extra (the hue is already on the row as --tl-kind) and means the timeline
   * is legible by shape rather than by remembering which icon means what:
   * blue is mail, teal is a text, green a call, violet a video call.
   *
   * The tint is 5%. Above about 8% the cards start to read as coloured panels
   * competing with the words on them, which is what "too noisy" means. */
  .tl-row{ border-left:2px solid color-mix(in srgb, var(--tl-kind, var(--text-dim)) 55%, transparent);
    background:color-mix(in srgb, var(--tl-kind, var(--text-dim)) 5%, transparent); }
  /* His own messages sit a shade lighter, so a conversation reads as two sides
     rather than as one column of identical cards. */
  .tl-row.tl-mine{ background:color-mix(in srgb, var(--tl-kind, var(--text-dim)) 9%, transparent); }
  /* A note is ink, not a channel — it keeps the quiet edge it always had. */
  .tl-row.tl-note{ border-left-color:var(--glass-border); background:transparent; }

  /* A ROLLED-UP RUN.
   *
   * "they need to rollup under a single one and expand if clicked … we just
   *  need it to not take up all the space."
   *
   * It is a row you press, so it has to look like one: a hover state, a cursor,
   * and a count on the right that says what opening it will cost. */
  .tl-run{ cursor:pointer; align-items:center;
    transition:border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
  .tl-run:hover{ border-color:color-mix(in srgb, var(--tl-kind, var(--teal)) 45%, transparent); }
  .tl-run:focus-visible{ outline:2px solid var(--tl-kind, var(--teal)); outline-offset:2px; }
  .tl-runmore{ flex:none; font-size:var(--type-label); font-weight:600; letter-spacing:.02em;
    color:var(--tl-kind, var(--teal)); white-space:nowrap; padding-left:var(--space-3); }

  /* The header that stays behind once a run is open, so there is somewhere to
     press to close it. Deliberately not a card — it is a label on a group, and
     making it a card would add back a row of the height this is saving. */
  .tl-runhead{ display:flex; align-items:center; gap:var(--space-2); cursor:pointer;
    padding:var(--space-2) var(--space-1) var(--space-2) var(--space-hair); margin:var(--space-1) 0 var(--space-hair); border-radius:var(--radius-control);
    font-size:var(--type-body); font-weight:600; letter-spacing:.04em; text-transform:uppercase;
    color:var(--tl-kind, var(--teal)); }
  .tl-runhead:hover{ background:color-mix(in srgb, var(--tl-kind, var(--teal)) 10%, transparent); }
  .tl-runhead:focus-visible{ outline:2px solid var(--tl-kind, var(--teal)); outline-offset:2px; }
  .tl-runicon{ display:inline-flex; }
  .tl-runicon svg{ width:14px; height:14px; }
  .tl-runlabel{ flex:1; min-width:0; }
  .tl-runhead .tl-runmore{ text-transform:none; letter-spacing:0; font-weight:600; }

  /* ========================================================================
     THE CONVERSATION LEDGER.
     ------------------------------------------------------------------------
     "I love the spirit of threads, but clunky and in the wrong place. I want
      same message threads to roll up under the most recent message, like in
      close. I want them grouped by conversation."   (Matt, 3 September.)

     A LEDGER, NOT A CHAT, and every rule below exists to hold that line. No
     card, no glass, no 32px avatar ring, no rim, no lean — those belong to a
     text bubble, where the message IS the whole thing. Here the subject is
     the unit and the job is scanning, so what is left is type on a page: a
     header, then rows you run an eye down, times aligned in one column at the
     right so a week of activity has a shape.

     THE ONE PIECE OF COLOUR is the 2px edge down the left of a group, in
     --tl-kind — the same channel colour the rest of the timeline uses, blue
     for mail. Not a tinted background: forty tinted panels is the noise the
     bubbles were already accused of, and a single edge is enough to tell a
     conversation apart from the texts above and below it.
     ======================================================================== */
  .tlc-group{ margin:0 0 var(--space-3); padding:var(--space-hair) 0 var(--space-hair) var(--space-3);
    border-left:2px solid color-mix(in srgb, var(--tl-kind, var(--blue)) 45%, transparent); }
  .tlc-head{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-hair) var(--space-hair) var(--space-1); min-width:0; }
  .tlc-icon{ display:inline-flex; flex:none; color:var(--tl-kind, var(--blue)); }
  .tlc-icon svg{ width:14px; height:14px; }
  /* The subject is the loudest thing in the group and the only serif-weight
     line in it. It wraps rather than clipping: a subject you cannot read is a
     conversation you cannot identify, which is the whole point of the header. */
  .tlc-subject{ font-size:var(--type-lead); font-weight:700; color:var(--text); min-width:0;
    overflow-wrap:anywhere; }
  /* How many messages, said on the header so a conversation of four says it is
     four without being opened. Same pill the mail list uses for the same fact. */
  .tlc-n{ flex:none; font-family:var(--font-mono); font-size:var(--type-label); font-weight:600;
    color:var(--text-dim); background:color-mix(in srgb, var(--tl-kind, var(--blue)) 16%, transparent);
    border-radius:var(--radius-pill); padding:1px var(--space-2); }

  /* ONE MESSAGE. Grid rather than flex, so the time column lines up across
     every row of every group on the page — the alignment is what makes a
     ledger readable, and a flex row with `margin-left:auto` on the time gives
     a ragged edge the moment one preview is shorter than another. */
  /* THE LEDGER READS AT --type-lead, NOT --type-body — row 2160, and it is the
     second half of "twice as wide and text to be larger". Every other row on
     this same timeline — a text, a note, a run of texts — is already at
     --type-lead through .timeline-text; the mail ledger was the one kind of
     entry still at 13px, so the page had two sizes of the same sentence
     depending on which channel it arrived on. The times, the chevron and the
     "n more in this conversation" line come up one step with it, to --type-body
     and --type-label's old relationship to the line beside them. The narrow
     rule below still drops the whole ledger back to --type-body under 560px. */
  .tlc-row{ display:grid; grid-template-columns:12px auto 6px minmax(0,1fr) auto auto;
    align-items:baseline; gap:0 var(--space-2); padding:var(--space-1) var(--space-1); border-radius:var(--radius-control);
    font-size:var(--type-lead); line-height:1.55; }
  .tlc-openable{ cursor:pointer; }
  .tlc-openable:hover{ background:color-mix(in srgb, var(--tl-kind, var(--blue)) 9%, transparent); }
  .tlc-openable:focus-visible{ outline:2px solid var(--ring); outline-offset:1px; }
  .tlc-chev{ font-size:var(--type-body); color:var(--text-faint); transition:transform var(--dur-fast) var(--ease-out); }
  .tlc-row.tl-open .tlc-chev{ transform:rotate(90deg); color:var(--text); }
  .tlc-who{ font-weight:600; color:var(--text); white-space:nowrap; }
  .tlc-dot{ color:var(--text-faint); }
  /* THE FIRST LINE, AND ONLY THE FIRST LINE. Clipped with an ellipsis rather
     than wrapped: a preview that runs to a second line turns the ledger back
     into the pile of cards it replaced. The whole message is one press away. */
  .tlc-said{ color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
  .tlc-clip{ display:inline-flex; align-self:center; color:var(--text-faint); }
  .tlc-clip svg{ width:12px; height:12px; }
  .tlc-when{ color:var(--text-faint); font-size:var(--type-body); white-space:nowrap; padding-left:var(--space-2);
    text-align:right; }
  /* The opened email spans the whole row rather than sitting in the preview
     column, and stops being a button so selecting a line of it does not fold
     it away again — the same rule the old tall card needed. */
  .tlc-body{ grid-column:1 / -1; }
  .tlc-row.tl-open .tlc-body{ cursor:auto; padding:var(--space-2) 0 var(--space-2) var(--space-5); }
  .tlc-more{ font-size:var(--type-body); font-weight:600; color:var(--tl-kind, var(--blue));
    cursor:pointer; padding:var(--space-1) var(--space-1) var(--space-1) var(--space-6); border-radius:var(--radius-control); display:inline-block; }
  .tlc-more:hover{ background:color-mix(in srgb, var(--tl-kind, var(--blue)) 10%, transparent); }
  .tlc-more:focus-visible{ outline:2px solid var(--ring); outline-offset:1px; }

  /* NARROW: the sender and the time keep their columns, the preview gives up
     what it must. Dropping the time to a second line would break the one
     alignment the ledger is built on. */
  @media (max-width: 560px){
    .tlc-row{ grid-template-columns:10px auto 5px minmax(0,1fr) auto auto; font-size:var(--type-body); gap:0 var(--space-2); }
    .tlc-subject{ font-size:var(--type-body); }
  }
  /* PULSE — MEASUREMENT, NOT THREE MORE LISTS.
   *
   * It used to be read receipts, upcoming events and unread mail in three
   * columns, and Matt was right about every one of them: the events are the
   * agenda with a longer horizon, the unread is the inbox with a number on it,
   * and 300 read receipts is a log nobody reads. This is a tile grid instead —
   * a number, how it compares with your own average, and twelve weeks of shape
   * behind it. */
  .pulse-tiles{ display:grid; gap:var(--space-3); grid-template-columns:repeat(auto-fit, minmax(272px, 1fr));
    max-width:1240px; margin-bottom:var(--space-6); }
  .pulse-tile{ display:flex; flex-direction:column; gap:var(--space-2); padding:var(--space-4) var(--space-4) var(--space-3); border-radius:var(--radius-card);
    border:1px solid var(--glass-border); background:var(--glass-bg); text-align:left;
    font-family:inherit; cursor:pointer; min-width:0; }
  .pulse-tile:hover{ border-color:var(--glass-border-hi); }
  .pulse-tile:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }
  .pulse-tile.flat{ cursor:default; }
  .pulse-tile.flat:hover{ border-color:var(--glass-border); }
  .pt-label{ font-size:var(--type-label); font-weight:700; text-transform:uppercase; letter-spacing:.06em;
    color:var(--text-dim); }
  .pt-figure{ display:flex; align-items:baseline; gap:var(--space-2); flex-wrap:wrap; }
  .pt-n{ font-family:var(--font-mono); font-size:var(--type-figure); font-weight:500; line-height:1.05;
    font-variant-numeric:tabular-nums; color:var(--text); }
  .pt-unit{ font-size:var(--type-body); color:var(--text-dim); }
  /* Direction is carried by the arrow and the word, never by colour alone —
   * and "up" is not automatically good here. More mail arriving is not a win. */
  .pt-delta{ font-size:var(--type-body); font-weight:600; color:var(--text-dim); }
  .pt-spark{ display:block; width:100%; height:34px; margin-top:var(--space-hair); }
  .pt-foot{ font-size:var(--type-body); color:var(--text-faint); }

  /* A task's marker is not a person's initials, so it is not drawn as them —
   * a tick in the same disc, at the same size, so the two queues read as one
   * surface without pretending a task has a face. */
  .pw-av.pw-task{ display:grid; place-items:center; color:var(--text-dim); }
  .pw-av.pw-task svg{ width:15px; height:15px; }

  .pulse-wait{ display:flex; flex-direction:column; gap:var(--space-2); }
  /* The row is a link AND a set of controls, so the link is its own button
     inside rather than the whole row being one — a row that is a button cannot
     contain buttons, and nesting them is how a "Snooze" click ends up opening
     the contact instead. */
  .pw-row{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-2) var(--space-2) var(--space-2); border-radius:var(--radius-card);
    border:1px solid var(--glass-border); background:var(--glass-bg); }
  .pw-row:hover{ border-color:var(--glass-border-hi); }
  .pw-open{ display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-2) var(--space-2); border:0; border-radius:var(--radius-control);
    background:none; cursor:pointer; font-family:inherit; text-align:left; flex:1; min-width:0; }
  /* "Needs doing", on the house page — the one .pw-row that is also a task. */
  .pw-row .list-check{ cursor:pointer; margin-left:var(--space-1); }
  .pw-open:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }
  .pw-acts{ display:flex; align-items:center; gap:var(--space-1); flex:none; opacity:0; transition:opacity var(--dur-fast) var(--ease-out); }
  .pw-row:hover .pw-acts, .pw-row:focus-within .pw-acts{ opacity:1; }
  /* Always reachable without a mouse, and always visible on a touch screen —
     an action that only appears on hover is an action a phone cannot use. */
  @media (hover:none){ .pw-acts{ opacity:1; } }
  .pw-act{ font-family:inherit; font-size:var(--type-label); font-weight:600; padding:var(--space-1) var(--space-2); border-radius:var(--radius-pill);
    border:1px solid var(--glass-border); background:rgba(120,120,140,.10); color:var(--text-dim); cursor:pointer; }
  .pw-act:hover{ color:var(--text); background:rgba(120,120,140,.20); }
  .pw-act:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }
  .pw-act.x{ padding:var(--space-1) var(--space-2); font-size:var(--type-body); line-height:1; }
  .pw-act.x:hover{ color:var(--pink, var(--text)); }
  .pw-av{ flex:none; width:30px; height:30px; border-radius:var(--radius-round); display:flex; align-items:center;
    justify-content:center; font-size:var(--type-label); font-weight:700; background:rgba(120,120,140,.16);
    color:var(--text); }
  .pw-main{ min-width:0; flex:1; display:flex; flex-direction:column; gap:var(--space-hair); }
  .pw-name{ font-size:var(--type-body); font-weight:650; color:var(--text); }
  .pw-sub{ font-size:var(--type-body); color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  /* THE LINE THAT SAYS WHERE A FACT CAME FROM.
     Every scan in Lucid ends with one: the sentence a warranty date, a job
     change or a filter size was read out of. It wants to sit BACK from the
     row above it, and the obvious way to do that — style="opacity:.72" on a
     .pw-sub — took an 11.5px line that passes at 4.6:1 down to 3.16:1. The
     readability harness caught it the moment the house page was added to the
     scene list, in eight places at once.
     So the recession is done with a COLOUR that was chosen to be readable,
     not by fading a colour that was. */
  .pw-from{ color:var(--text-faint); }
  .pw-days{ flex:none; font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim);
    font-variant-numeric:tabular-nums; }
  .pw-days.late{ color:var(--orange-text, var(--orange)); font-weight:700; }

  .rp-receipt{ margin:0 0 var(--space-4); padding:var(--space-3) var(--space-4); border-radius:var(--radius-card);
    border:1px solid color-mix(in srgb, var(--green) 34%, transparent);
    background:color-mix(in srgb, var(--green) 8%, transparent); }
  .rp-receipt-head{ display:flex; justify-content:space-between; gap:var(--space-3); align-items:baseline; }
  .rp-receipt-who{ font-size:var(--type-body); font-weight:700; color:var(--text); }
  .rp-receipt-when{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim); flex:none; }
  .rp-receipt-sub{ font-size:var(--type-body); color:var(--text-dim); margin-top:var(--space-1); }

  .timeline-icon{ width:32px; height:32px; border-radius:var(--radius-round); background:rgba(120,120,140,.10); display:flex; align-items:center; justify-content:center; flex:none; margin-top:1px; }
  .timeline-icon svg{ width:15px; height:15px; color:var(--text-dim); }
  .timeline-date{ font-family:var(--font-mono); font-size:var(--type-body); color:var(--text-faint); float:right; padding-top:var(--space-hair); margin-left:var(--space-3); white-space:nowrap; }
  /* `anywhere` rather than `break-word`: break-word will not break a run that
     is longer than the line on its own, which is precisely the case that blows
     a column open. Email previews are full of those — tracking URLs, message
     ids, base64 — so this needs to be the strong version. The 2-line clamp
     keeps a row a row: a preview is a glance, and the full message is one click
     away under "Read here". */
  .timeline-text{ font-size:var(--type-lead); line-height:1.5; overflow-wrap:anywhere; word-break:break-word;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
  .timeline-from{ font-weight:700; }
  .box{ padding:var(--space-5); margin-bottom:var(--space-4); border-top-width:3px; border-top-style:solid; }
  :root:not([data-mode="light"]) .box{ box-shadow:0 8px 26px -8px rgba(0,0,0,.5), 0 -2px 20px -6px var(--box-accent, transparent); }
  /* --text-dim, not --text-faint. Ten-and-a-half-pixel uppercase measured
     2.84:1 on midnight and 4.2:1 on cream — the same failure the art credit
     had, for the same reason: the faint token is for text that is larger or
     denser than this, and a heading nobody can read is not a heading. */
  .box-title{ font-size:var(--type-label); text-transform:uppercase; letter-spacing:0.08em; color:var(--text-dim); margin-bottom:var(--space-3); }
  .box-title-note{ float:right; text-transform:none; letter-spacing:0; font-weight:700; }
  /* The measured relationship score. Three bars, because a single number with
     no working shown is a number nobody can argue with. */
  .sig-score{ font-size:var(--type-page); font-weight:800; line-height:1; margin-bottom:var(--space-hair); }
  .sig-sub{ font-size:var(--type-body); color:var(--text-faint); margin-bottom:var(--space-3); }
  .sig-part{ display:flex; align-items:center; gap:var(--space-2); margin-bottom:var(--space-2); font-size:var(--type-body); color:var(--text-dim); }
  .sig-part-name{ width:74px; flex:none; }
  .sig-bar{ flex:1; height:5px; border-radius:var(--radius-tight); background:var(--glass-border); overflow:hidden; }
  .sig-bar span{ display:block; height:100%; border-radius:var(--radius-tight); background:currentColor; }
  .sig-part-value{ width:30px; flex:none; text-align:right; font-family:var(--font-mono); font-size:var(--type-label); }
  .sig-caveat{ margin-top:var(--space-2); font-size:var(--type-body); color:var(--orange); }
  .rel-strong{ background:color-mix(in srgb, var(--cat-green) 16%, transparent); color:var(--cat-green); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--cat-green) 35%, transparent); }
  .rel-stable{ background:color-mix(in srgb, var(--cat-violet) 16%, transparent); color:var(--cat-violet); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--cat-violet) 30%, transparent); }
  .rel-cooling, .rel-stale{ background:color-mix(in srgb, var(--cat-blue) 18%, transparent); color:var(--cat-blue); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--cat-blue) 40%, transparent); }
  .cd-tabs{ display:flex; gap:var(--space-3); border-bottom:1px solid var(--glass-border); margin-bottom:var(--space-6); }
  .cd-tab{ padding:var(--space-3) var(--space-5); font-size:var(--type-lead); font-weight:700; color:var(--text-dim); cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; }
  .cd-tab:hover{ color:var(--text); }
  .cd-tab.active{ color:var(--text); border-bottom-color:var(--blue); }
  /* About sits in the header row with Edit and Share rather than in the strip
     above, so it carries the open state itself — the same blue the strip uses,
     on the border, so the row keeps one shape whichever tab is showing. */
  .btn.is-open{ border-color:var(--blue); color:var(--blue); }
  .cd-note{ padding:var(--space-3) var(--space-3); margin-bottom:var(--space-2); }
  .cd-note-date{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-faint); margin-bottom:var(--space-1); }
  /* pre-line, and it arrived WITH the escaping rather than despite it.
     The body used to be dropped into the page as raw markup, so a note was at
     the mercy of whatever was pasted into it — and its line breaks collapsed
     anyway, because HTML collapses whitespace. Escaping it fixes the first and
     would have made the second permanent; this fixes the second properly, so
     a note now keeps the shape it was typed in. */
  .cd-note-body{ font-size:var(--type-body); white-space:pre-line; overflow-wrap:anywhere; }
  .cd-action-item{ display:flex; align-items:flex-start; gap:var(--space-2); padding:var(--space-3) var(--space-3); margin-bottom:var(--space-2); }
  .cd-action-item.done{ opacity:.5; text-decoration:line-through; }
  /* Two rules were here for the bar chart of how often you two had spoken: the
     flex row and the bars inside it. The ONLY thing that ever built those bars
     was pulseTicks() in app.js, and that went on 30 August with its call site —
     the contact card stopped grading the friendship, and the cadence is said in
     words now. Deleted with the producer rather than left as a shape nothing
     can fill; the class name is written out in the note over where pulseTicks
     used to be, for anyone reading the attic copy. */
  .info-row{ font-size:var(--type-body); padding:var(--space-2) 0; border-bottom:1px solid var(--glass-border); }
  .em-alt-row{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) 0; border-bottom:1px solid var(--glass-border); }
  .em-alt-email{ flex:1; min-width:0; font-size:var(--type-body); word-break:break-all; }
  .em-toggle-pill{ flex:none; font-size:var(--type-label); font-weight:700; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); cursor:pointer; border:1px solid var(--glass-border-hi); transition:all var(--dur-fast) var(--ease-out); }
  .em-toggle-pill.active{ background:color-mix(in srgb, var(--green) 16%, transparent); color:var(--green); border-color:color-mix(in srgb, var(--green) 40%, transparent); }
  :root:not([data-mode="light"]) .em-toggle-pill.active{ box-shadow:0 0 12px 0 var(--green-glow); }
  .em-toggle-pill.inactive{ background:rgba(120,120,140,.12); color:var(--text-faint); }
  .em-alt-remove{ flex:none; width:22px; height:22px; border-radius:var(--radius-round); display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text-faint); font-size:var(--type-lead); }
  .em-alt-remove:hover{ background:color-mix(in srgb, var(--pink) 18%, transparent); color:var(--pink); }
                        /* THE "HOW YOU KNOW EACH OTHER" CHEVRON. Same mechanism as the inactive-email
     toggle above — a rotating stroke, no image, no library — because the box it
     opens is deliberately not the first thing on the card any more, and a
     control that looks unfamiliar here would make people miss that it opens at
     all. */
  .cd-know-head{ display:flex; align-items:center; justify-content:space-between; cursor:pointer; }
  .cd-know-head:hover .box-title{ color:var(--text-dim); }
  /* Worth a line folds with the same head, and its heading is a .section-label
     rather than a .box-title — a band on a page, not a card. */
  .cd-know-head:hover .section-label{ color:var(--text); }
  .cd-know-head:hover .cd-know-chevron{ color:var(--text-dim); }
  .cd-know-chevron{ width:12px; height:12px; color:var(--text-faint); flex:none; transition:transform var(--dur-fast); }
  .cd-know-chevron.open{ transform:rotate(90deg); }
  /* Also knows — one row per person, the whole row is the button. */
  .cd-also-list{ display:flex; flex-direction:column; gap:2px; margin:2px -6px 0; }
  .cd-also-row{ display:flex; align-items:center; gap:10px; width:100%; padding:6px; border:0; border-radius:var(--radius-control); background:transparent; color:var(--text); font:inherit; text-align:left; cursor:pointer; }
  .cd-also-row:hover, .cd-also-row:focus-visible{ background:color-mix(in srgb, var(--text) 6%, transparent); }
  .cd-also-av{ width:28px; height:28px; font-size:10.5px; }
  .cd-also-text{ display:flex; flex-direction:column; min-width:0; }
  .cd-also-name{ font-size:13px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .cd-also-sub{ font-size:11.5px; color:var(--text-dim); }
  .cd-also-foot{ margin-top:8px; font-size:11.5px; color:var(--text-faint); }
/* "This is me" under the Also knows box: a quiet verb, the same size as the
   foot above it, because it is a correction he makes once per card. */
.cd-also-me{ margin-top:6px; font-size:11.5px; }
  .info-row:last-child{ border:none; }
  .info-label{ font-size:var(--type-label); text-transform:uppercase; color:var(--text-faint); letter-spacing:.05em; margin-bottom:var(--space-hair); }

  /* ROW 2020 — A NUMBER YOU CAN RING, AND THE THREE TIMES THERE ISN'T ONE.
     The Phone cell of the contact card, which used to be a div with an em dash
     in it. Four states, and only one of them draws a control: see
     cdPhoneCellHtml() in app.js for why an absence gets a sentence and a verb
     rather than a greyed-out button.
     Wraps, because a Call button beside a number carrying a country code is
     wider than this sidebar box on a laptop, and a button pushed off the edge
     is a button nobody presses. */
  .cd-reach{ display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap; }
  /* overflow-wrap arrived with row 2233: the Email cell goes through this same
     class, and an address is routinely longer than this sidebar box is wide.
     A phone number never was, which is why nothing here needed it before. */
  .cd-reach-value{ font-size:var(--type-body); color:var(--text); text-decoration:none;
                   min-width:0; overflow-wrap:anywhere; }
  /* A tel: link is a link and says so on hover, the same as the LinkedIn one
     three boxes down. It is NOT underlined at rest: the number is the value in
     a field first and a control second, and underlining every one of them turns
     the Contact Info box into a wall of rules. */
  a.cd-reach-value:hover{ color:var(--accent); text-decoration:underline; }
  /* The absence, and the reason for it. --text-faint is the same weight the
     alternate-address line uses for "checking…", because neither is a warning —
     a person with no phone number is an ordinary person. */
  .cd-reach-note{ font-size:var(--type-label); color:var(--text-faint); line-height:1.5; }
  /* EXCEPT WHEN LUCID DOES NOT KNOW. A contacts read that FAILED is not an
     absence and must not read like one, so it takes --orange — the colour this
     same card already uses for "their other addresses could not be read". */
  .cd-reach-note.is-unsure{ color:var(--orange); }
  .cd-reach-verb{ color:var(--accent); cursor:pointer; font-weight:var(--weight-strong); }
  .cd-reach-verb:hover{ text-decoration:underline; }
  /* The button is an <a href="tel:">, so it needs the flex box .btn gives a
     <button> and does not give an anchor, and it must not be underlined by the
     sheet's general link rules. */
  .cd-call-btn{ display:inline-flex; align-items:center; gap:var(--space-2); text-decoration:none; }
  .cd-call-btn:hover{ text-decoration:none; }
  .cd-call-icon{ width:14px; height:14px; flex:none; }

  .p-tag{ font-size:var(--type-label); border:1px solid var(--glass-border-hi); color:var(--text-dim); padding:var(--space-1) var(--space-3); border-radius:var(--radius-pill); }

  .proj-title-row{ display:flex; align-items:center; justify-content:space-between; gap:var(--space-3); margin-bottom:var(--space-4); flex-wrap:wrap; }
  .proj-title-row .page-title{ margin:0; }
  .view-toggle{ display:flex; gap:var(--space-2); padding:var(--space-2); border-radius:var(--radius-card); background:rgba(120,120,140,.08); }
  .vt-btn{ padding:var(--space-2) var(--space-5); border-radius:var(--radius-control); font-size:var(--type-body); font-weight:600; color:var(--text-dim); cursor:pointer; }
  :root:not([data-mode="light"]) .vt-btn.active{ box-shadow:0 0 14px 1px var(--accent-glow); }
  .vt-btn.active{ background:color-mix(in srgb, var(--blue) 20%, transparent); color:var(--text); }
  /* Drag and drop. The dragged card fades and tilts a touch; the column it is
     over lights up. Without a clear drop target, dragging in a dark UI is
     guesswork. */
  .task-card[draggable="true"]{ cursor:grab; }
  .task-card.dragging{ opacity:.4; transform:rotate(1.5deg); }
  .ps-on_track{ color:#7EC49E; background:rgba(63,155,107,.16); }
  .ps-at_risk{ color:#EAA766; background:rgba(217,125,46,.16); }
  .ps-off_track{ color:#D97066; background:rgba(184,56,46,.16); }
  .ps-on_hold{ color:var(--text-dim); background:rgba(120,120,140,.16); }
  .ps-complete{ color:#6E8CE8; background:rgba(41,84,217,.16); }
  /* Those five are hardcoded hex picked against a near-black panel — pastels,
     which is right there and unreadable on cream. The light themes take the
     solid tokens instead. */
  :root[data-mode="light"] .ps-on_track{ color:var(--cat-green); background:color-mix(in srgb, var(--cat-green) 15%, transparent); }
  :root[data-mode="light"] .ps-at_risk{ color:var(--cat-orange); background:color-mix(in srgb, var(--cat-orange) 15%, transparent); }
  :root[data-mode="light"] .ps-off_track{ color:var(--cat-red); background:color-mix(in srgb, var(--cat-red) 15%, transparent); }
  :root[data-mode="light"] .ps-complete{ color:var(--cat-blue); background:color-mix(in srgb, var(--cat-blue) 15%, transparent); }

  /* One line, and an ellipsis rather than a wrap: these feet now name a task,
     and a title long enough to wrap would make one tile taller than its
     neighbours — which is the thing the row is about. The whole name is in the
     tile's own title attribute for anyone who wants it. */


  /* SENDING, AND FAILED, ARE TWO DIFFERENT ROWS. A file on its way is dimmed
     and says "sending…"; one that did not make it keeps its place, stops being
     dim, and carries the actual reason in the line where the size was. Neither
     of them is a spinning bar that knows nothing. */

  /* A READ THAT FAILED LOOKS DIFFERENT FROM ONE THAT CAME BACK WITH NOTHING.
     "Nothing attached yet" about a table that does not exist is the sentence
     this codebase has been caught writing more often than any other. */

  /* The panel while something is being dragged over it. The hint is a plate in
     the middle rather than a border that changes colour, because a dashed edge
     that goes from grey to blue is not something anybody notices while holding
     a file over a window. */

  /* Both of these are sentences ABOUT the list rather than items in it, so
     they take the whole width instead of sitting in the first cell with three
     empty cells beside them. */


  /* THE FAMILY IS A CUSTOM PROPERTY ON THE CARD, and every skin below reads it,
     so adding a seventh kind of file is one line rather than four. The fallback
     in each var() is the accent, which is what every plate used to be — a file
     with a family nobody has named still looks deliberate. */
  /* TWO PROPERTIES PER FAMILY, NOT ONE, AND THE SECOND ONE IS THE LESSON THIS
     FILE ALREADY LEARNED. --cat-red is the solid variant and --cat-red-2 the
     pastel; which of them can be READ depends entirely on what is behind it,
     which is why --cat-*-text exists two thousand lines up — "· Prospect"
     measured 2.78:1 on midnight for exactly this reason. The first version of
     these plates used the solid colour for both the fill and the letters and
     made the same mistake: measured off real painted pixels, the DOCX plate
     came to 2.37:1 on frosted and 2.39 on chocolate and forest. The fill still
     wants the solid colour — it is a 13% wash, not text — so the two are
     separate properties and every skin below reads the right one. */
  /* THE TINT GOES THE OTHER WAY ON PAPER, and it is worth saying why rather
     than leaving it as a number somebody later "tidies". On a dark ground the
     wash LIGHTENS the plate and the pastel letters sit on it comfortably. On a
     light ground the same wash DARKENS the plate while the letters are the
     solid, dark variant — so more tint is less contrast, not more. Ten per
     cent, measured: the whole set lands between 4.2 and 5.7:1 on the light
     papers instead of 3.2 to 4.1. The ring keeps its strength, because a
     border is not something anybody reads. */
  /* The picture covers the plate completely once it has decoded — the letters
     stay in the markup underneath so that a thumbnail which never arrives
     leaves a labelled square rather than a hole. */

  /* Dim at rest, legible under the pointer, red once it is the thing being
     pressed — the same three-step the agenda's action button uses. */

  /* Inherits --text-faint from the column head, which is the right weight for
     the heading and not enough ink for an 11px numeral on its own tinted pill. */
  /* 4.44–4.61:1 across the warm papers against a 4.5 floor — a count that
     passes on one theme and fails on the next is failing. An 11px numeral on a
     tinted pill has too little ink for --text-dim; it takes full ink, and the
     pill is what keeps it subordinate to the heading beside it. */
  .task-card{ background:rgba(120,120,140,.06); border:1px solid var(--glass-border); border-radius:var(--radius-card); padding:var(--space-3) var(--space-4); margin-bottom:var(--space-3); cursor:pointer; transition:all var(--dur-fast) var(--ease-out); }
  :root:not([data-mode="light"]) .task-card:hover{ box-shadow:0 0 18px 1px var(--accent-glow); border-color:color-mix(in srgb, var(--accent) 35%, var(--glass-border)); }
  .task-card:hover{ border-color:var(--glass-border-hi); }
  .task-due{ font-size:var(--type-label); color:var(--text-faint); }
  /* .list-row is no longer drawn by the app — the project List view was the
     last thing using it and it draws .task-card in a grid now (see
     .pj-task-grid). Left in place because the pre-split page in attic/ still
     loads this stylesheet; .list-check and .list-title below ARE still live,
     inside those cards and in the vault. */
  .list-check{ width:15px; height:15px; border-radius:var(--radius-round); border:2px solid var(--glass-border-hi); flex:none; }
  .list-check.done{ background:var(--green); border-color:var(--green); }
  /* .status-pills / .status-pill / .status-pill.active and its dark-mode glow
     were here: the mockup-era project status row. The one the app builds is
     .proj-status-pill with a .ps-<status> beside it, ninety lines below, and
     the two never coexisted on a screen. Worth saying why the scan could be
     trusted on a name this generic: `proj-status-pill` is ONE class token, not
     two, so it never matched `.status-pill` in the browser either — an element
     carrying it was never getting these rules. */
  /* A SIXTH OF THE WAY BACK TOWARDS THE INK, and the reason is a number.
     --text-faint at 11.5px measures 4.50:1 on serene:blush:warm — exactly the
     minimum, with nothing in hand. Rebuilding the media band in treatment B
     made the band taller, which moved the Follow section a few pixels down
     the page's own vertical wash and took that same line to 4.45; a check
     that passes by 0.00 was always going to go red for a reason that had
     nothing to do with it. This is the line that was on the edge, so this is
     the line that moves. It is still the quietest thing on a card. */
  .ht-why{ font-size:var(--type-body); color:color-mix(in srgb, var(--text-faint) 84%, var(--text));
    margin-top:var(--space-1); line-height:1.5; }

  /* Ghost verbs. The old ones were solid blue text on every single card, which
     is six accents per tile pretending to be one. */
  /* THE GHOST VERB THAT CAME OUT SOLID GREY, AND WHY IT IS THIS RULE'S FAULT.
     Every .ht-verb in the app is written as a <span> except one: app.js:23959
     writes the fashion shelf's Add as a <button>. A bare <button> carries the
     browser's own ButtonFace fill, and this rule never said otherwise — so
     that one chip came out a mid-grey pill with --text-dim lettering on it,
     measured between 2.45:1 and 2.84:1 on eight dark themes, while its
     identical-looking siblings on the same screen read fine. Found on 20
     September by cropping the contrast check's own screenshot and looking at
     it, after the numbers alone had been read as a colour problem twice.
     background and appearance here rather than on the one <button>: the next
     verb written as a button should not have to rediscover this. */
  .ht-verb{ font-size:var(--type-label); font-weight:600; color:var(--text-dim); cursor:pointer;
    background:transparent; -webkit-appearance:none; appearance:none; font-family:inherit;
    padding:var(--space-1) var(--space-3); border-radius:var(--radius-pill); white-space:nowrap; border:1px solid var(--glass-border);
    transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out),
               background var(--dur-fast) var(--ease-out); }
  .ht-verb:hover{ color:var(--text); border-color:var(--glass-border-hi);
    background:color-mix(in srgb, var(--text) 8%, transparent); }
  .ht-verb.primary{ color:var(--text); border-color:color-mix(in srgb, var(--accent) 50%, transparent); }
  :root:not([data-mode="light"]) .ht-verb.primary:hover{ box-shadow:0 0 16px -3px var(--accent-glow); }

  /* A face is worth more than a chip for telling one person from another, and
     it costs no colour — this variant is deliberately ink, not the rainbow
     gradient the mail rows use. */
  .ht-av{ width:30px; height:30px; border-radius:var(--radius-round); flex:none; margin-top:1px;
    display:flex; align-items:center; justify-content:center;
    font-size:var(--type-label); font-weight:700; color:var(--text);
    background:color-mix(in srgb, var(--text) 8%, transparent);
    border:1px solid var(--glass-border); position:relative; z-index:1; }

  /* 4.50:1 against a 4.5 floor is not a pass, it is a coin toss. Empty states
     are the copy a person reads most carefully — it is the only thing in the
     tile — so this one takes --text-dim too. */
  /* THE ROLL-UP. See rollUp() in app.js for why this exists rather than a
     .slice(0, 6) — a slice hides the rest and says nothing, which on a tile
     headed "Nobody owes you a reply" is a lie by omission.

     display:contents when open, not block. These rows are cards laid out by
     the tile's own container; wrapping the overflow in a block box would take
     them out of that layout and stack them differently from the three above
     them, which is worse than not opening at all. contents makes the wrapper
     disappear and hands its children straight back to the parent.

     No height animation. Animating to a height you have to measure first is
     how a list that grows while it opens ends up clipped, and the honest
     version of "expand" here is instant. */
  .roll-rest{ display:none; }
  .roll-rest.open{ display:contents; }
  .roll-more{ display:block; width:100%; margin-top:var(--space-2); padding:var(--space-2) var(--space-3);
    background:transparent; border:1px dashed var(--glass-border-hi); border-radius:var(--radius-control);
    font-size:var(--type-body); font-weight:600; letter-spacing:0.02em;
    color:var(--text-dim); cursor:pointer; text-align:center;
    transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
  .roll-more:hover{ color:var(--text); border-color:var(--accent); }
  .roll-more:focus-visible{ outline:2px solid var(--ring); outline-offset:2px; }

  .ht-empty{ font-size:var(--type-body); color:var(--text-dim); line-height:1.6; padding:var(--space-2) 0; }
  .ht-line{ display:flex; align-items:baseline; gap:var(--space-3); padding:var(--space-2) 0; font-size:var(--type-body); cursor:pointer; }
  .ht-line + .ht-line{ border-top:1px solid var(--glass-border); }
  .ht-line-t{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

  .ht-time{ font-size:var(--type-label); color:var(--text-faint); flex:none; width:68px;
    letter-spacing:.02em; white-space:nowrap; }
  /* Numerals belong to the serif everywhere on this page. That single rule is
     most of what stops it looking like a web app. */
  .ht-num, .ht-time{ font-family:var(--font-mono); }
  .ht-frame-dot{ width:5px; height:5px; border-radius:var(--radius-round); background:rgba(255,255,255,.45); }
  .ht-frame-dot.on{ background:#fff; }

  /* ---- THE REASON, AND THE WEEK ------------------------------------------
     What turned this tile from wallpaper into something worth looking at is
     the line that says WHY this photograph is the one on screen this morning.
     It is set above the paragraph and slightly brighter than it, because it is
     the sentence somebody reads in the half-second before deciding whether to
     read the rest.

     WHITE, NOT A THEME TOKEN, AND THAT IS NOT A HARDCODED COLOUR IN THE SENSE
     THE RULE FORBIDS. These words sit on top of a photograph — a bright sky in
     one theme is a bright sky in all twelve — so their contrast comes from the
     gradient underneath them and not from the page. Every piece of text in
     Lucid that is over a picture works this way, and the same block's
     `.ht-frame-say` two rules up is a theme token precisely because it is NOT
     over one. */
  .frame-reason{ color:#fff; font-size:var(--type-body); font-weight:600; letter-spacing:.005em;
    text-shadow:0 1px 3px rgba(0,0,0,.55); }
  .frame-cap-line{ color:#fff; font-size:var(--type-body); font-weight:500; }
  .frame-story{ margin:var(--space-2) 0 0; color:rgba(255,255,255,.94); font-size:var(--type-body);
    font-weight:400; line-height:1.5; max-width:62ch;
    text-shadow:0 1px 3px rgba(0,0,0,.5); }
  /* A week Lucid has nothing on file for, or one still being written. Dimmer
     than a real paragraph so the two are never mistaken for each other, and
     italic so the difference survives a glance. */
  .frame-story.dim{ color:rgba(255,255,255,.78); font-style:italic; }
  .frame-story.big{ font-size:var(--type-lead); line-height:1.62; max-width:70ch;
    display:block; overflow:visible; }
  .frame-stale{ display:block; margin-top:var(--space-2); color:rgba(255,255,255,.72); font-size:var(--type-label); }
  /* The painting is a BACKGROUND here, not the subject — the words are. Held
     back so they can be read over it. */
  .frame-dim{ opacity:.42; filter:saturate(.85); }

  /* ---- THE AMBIENT SCREEN ------------------------------------------------
     The frame filling the screen, which is what makes it the kind of thing that
     can sit on a Mac when he walks away. A `.modal-backdrop` so that Escape and
     click-outside come free, with its own opaque ground rather than the usual
     half-transparent scrim: a picture you are meant to stand back and look at
     should have nothing showing through it. */
  .modal-backdrop.frame-amb{ align-items:stretch; justify-content:stretch;
    background:#06070d; padding:0; }
  .modal-backdrop.frame-amb.open{ backdrop-filter:none; -webkit-backdrop-filter:none; }
  .frame-amb-stage{ position:absolute; inset:0; }
  .frame-amb-img{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain;
    opacity:0; transition:opacity var(--dur-ambient) var(--ease-out); }
  .frame-amb-img.on{ opacity:1; }
  /* The words sit in the bottom band with their own gradient, exactly as they
     do on the tile, so a photograph with a white sky at the bottom does not
     eat the paragraph.
     THE TOP PADDING IS THE LENGTH OF THE FADE AND NOT A STEP OF SPACING, which
     is why it is a clamp like the two beside it rather than a --space. It was a
     flat 120px: a fifth of the picture on a laptop and an eighth on a desk
     monitor, which is the same gradient doing two different jobs. 12vh is the
     proportion 120px already was on the window this was drawn at, so nothing
     changes on a full screen and a short window stops losing an eighth of the
     photograph to a fade. The floor is --space-16 doubled and the ceiling is
     where it was. */
  .frame-amb-words{ position:absolute; left:0; right:0; bottom:0; z-index:2;
    padding:clamp(64px, 12vh, 120px) clamp(24px, 6vw, 90px) clamp(28px, 5vh, 64px);
    background:linear-gradient(to top, rgba(0,0,0,.86), rgba(0,0,0,.35) 45%, transparent);
    pointer-events:none; }
  .frame-amb-words .frame-reason{ font-size:var(--type-fluid-1); margin-bottom:var(--space-2); }
  .frame-amb-bar{ position:absolute; z-index:3; right:clamp(16px, 3vw, 36px);
    top:clamp(16px, 3vh, 32px); display:flex; align-items:center; gap:var(--space-2);
    transition:opacity var(--dur-base) var(--ease-out); }
  .frame-amb-n{ color:rgba(255,255,255,.72); font-size:var(--type-body); min-width:52px; text-align:center; }
  .frame-amb-btn{ min-width:34px; height:34px; padding:0 var(--space-3); border-radius:var(--radius-card); cursor:pointer;
    border:1px solid rgba(255,255,255,.3); background:rgba(12,14,24,.55); color:#fff;
    font-size:var(--type-lead); line-height:1; font-family:inherit;
    -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px); }
  .frame-amb-btn.wide{ font-size:var(--type-body); font-weight:600; padding:0 var(--space-4); }
  .frame-amb-btn:hover{ background:rgba(12,14,24,.8); }
  /* THREE SECONDS OF STILLNESS AND THE CONTROLS GO. What is left is the
     photograph and the sentence under it, which is the entire point of an
     ambient mode — anything else on the glass is a page, not a picture. The
     words stay: they are the feature, not the chrome. */
  .frame-amb.resting .frame-amb-bar{ opacity:0; pointer-events:none; }
  .frame-amb.resting{ cursor:none; }
  /* ONE PHOTOGRAPH, OR NONE: the chevrons are removed rather than left in place
     doing nothing. A button that looks like it works and does not is the one
     thing this app does not ship, and it holds as much for a control with
     nothing to move to as for one that is not wired up. */
  .frame-amb.solo .frame-amb-btn:not(.wide),
  .frame-amb.solo .frame-amb-n{ display:none; }

  /* ---- THE PICTURE, FULL SIZE ----
     Even at 60vh the painting is still something on a dashboard rather than
     something being looked at, so pressing it opens it over the window. See
     openArt().

     SHAPED LIKE THE WEB WINDOW, NOT THE VIDEO ONE. The video window is a fixed
     16:9 box because every video is 16:9. Paintings share no ratio at all — a
     Rothko and a predella panel are barely the same kind of rectangle — so the
     card is a large one and the picture takes whatever of it its own shape
     asks for, exactly as it does in the tile.

     THE STAGE IS BLACK, AND THAT IS NOT DECORATION. A picture cut out against
     the app's own translucent glass reads as a rendering fault: you cannot see
     where the canvas ends and the sheet begins, and half the paintings in the
     Met have dark edges. Against black it reads as hung. */
  .artwin{ max-width:min(1400px, 96vw); width:100%; height:min(92vh, 1100px);
    padding:0; overflow:hidden; display:flex; flex-direction:column; border-radius:var(--radius-card); }
  .artwin-stage{ position:relative; flex:1; min-height:0;
    display:grid; place-items:center; background:#0a0a0d; }
  .artwin-img{ display:block; width:auto; height:auto;
    max-width:100%; max-height:100%; object-fit:contain; }
  /* The controls sit OVER the picture rather than beside it, because every
     pixel they do not take is a pixel of painting. Dark translucent discs, so
     they survive both a Turner sky and a Caravaggio shadow — the two things
     anything laid over a picture has to stay visible against. */
  .artwin-nav, .artwin-x{ position:absolute; border:none; padding:0; cursor:pointer;
    background:rgba(8,8,13,.55); color:#fff; border-radius:var(--radius-round);
    display:grid; place-items:center; line-height:1; opacity:.6;
    transition:opacity var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
  .artwin-nav:hover, .artwin-x:hover{ opacity:1; background:rgba(8,8,13,.82); }
  .artwin-nav:focus-visible, .artwin-x:focus-visible{ opacity:1;
    outline:2px solid var(--accent); outline-offset:2px; }
  .artwin-nav{ top:50%; transform:translateY(-50%); width:44px; height:44px; font-size:24px; }
  .artwin-prev{ left:14px; }
  .artwin-next{ right:14px; }
  .artwin-x{ top:14px; right:14px; width:34px; height:34px; font-size:var(--type-title); }
  /* The caption keeps the tile's own type at the tile's own weights, so this
     is plainly the same picture and not a second presentation of it. It is a
     band under the stage rather than a column beside it: the stage gets every
     pixel the words do not need, and the words scroll inside their own band on
     the rare record with a long credit line. */
  .artwin-cap{ flex:none; display:flex; flex-direction:column; gap:var(--space-2);
    padding:var(--space-4) var(--space-5) var(--space-4); max-height:34vh; overflow-y:auto; overscroll-behavior:contain;
    border-top:1px solid var(--glass-border); }
  .artwin-t{ font-family:var(--font-display); font-size:var(--type-title); font-weight:600;
    line-height:1.25; letter-spacing:-.01em; }
  .artwin-facts{ font-size:var(--type-body); color:var(--text-dim); line-height:1.5; }
  /* The written paragraph stays marked as the written paragraph, in the same
     rule the tile uses. A bigger picture is no reason to stop telling a reader
     which half of the words came from the museum. */
  .artwin-note{ font-size:var(--type-body); line-height:1.6; margin:var(--space-2) 0 0; max-width:78ch;
    padding-left:var(--space-3); border-left:2px solid var(--glass-border); color:var(--text); }
  .artwin-foot{ display:flex; align-items:baseline; gap:var(--space-4); margin-top:var(--space-1); }
  .artwin-n{ font-family:var(--font-mono); font-size:var(--type-label); font-weight:600;
    letter-spacing:.08em; color:var(--text-dim); font-variant-numeric:tabular-nums; flex:none; }
  /* --text-dim, not --text-faint, for the same reason as the tile's credit:
     ten-pixel uppercase has failed the 4.5:1 floor every time it was set in the
     fainter token. */
  .artwin-credit{ font-size:var(--type-label); font-weight:600; letter-spacing:.09em; text-transform:uppercase;
    color:var(--text-dim); line-height:1.5; flex:1; min-width:0; }
  /* THE MUSEUM, STILL ONE PRESS AWAY. Leaving Lucid used to be what a click on
     the painting did, which was the wrong default and not the wrong
     destination. Here it is what it always should have been: a labelled link
     somebody chooses. */
  .artwin-out{ font-size:var(--type-label); font-weight:700; letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-dim); text-decoration:none; flex:none; }
  .artwin-out:hover{ color:var(--text); }
  @media(max-width:620px){
    /* The backdrop bottom-anchors below 781px, so the sheet meets the bottom
       edge and only its top corners are rounded. */
    .artwin{ height:94vh; border-radius:var(--radius-card) var(--radius-card) 0 0; }
    .artwin-nav{ width:38px; height:38px; font-size:21px; }
    .artwin-prev{ left:8px; }
    .artwin-next{ right:8px; }
    .artwin-x{ top:10px; right:10px; }
    .artwin-cap{ padding:var(--space-3) var(--space-4) var(--space-4); max-height:40vh; }
    .artwin-note{ max-width:none; }
  }
  /* One box, everything in it. A dialog rather than a page: the answer to
     "where did I put that" should arrive over whatever you were doing and then
     get out of the way, not be somewhere you navigate to. */
  .omni-card{ max-width:640px; width:100%; padding:0; overflow:hidden; }
  .omni-input{ border:none; border-radius:0; border-bottom:1px solid var(--glass-border);
    font-size:var(--type-lead); padding:var(--space-5) var(--space-6); background:transparent; }
  .omni-input:focus{ box-shadow:none; outline:none; }
  .omni-results{ max-height:min(60vh, 520px); overflow-y:auto; padding:var(--space-2) var(--space-6) var(--space-5); scrollbar-width:thin; }
  .omni-results .section-label{ margin-top:var(--space-4); }
  .omni-row{ display:flex; align-items:baseline; gap:var(--space-4); padding:var(--space-2) 0; cursor:pointer;
    border-bottom:1px solid var(--glass-border); }
  .omni-row:last-child{ border-bottom:none; }
  .omni-row:hover .omni-t{ color:var(--accent-2, var(--accent)); }
  :root[data-mode="light"] .omni-row:hover .omni-t{ color:var(--accent); }
  /* A result with nowhere to go. It stays in the list — knowing the thing
     exists is most of what a search is for — but it stops pretending to be a
     link, because a row that looks pressable and swallows the press is the
     worst of the three possible behaviours. Colour is deliberately NOT dimmed:
     these are readable rows, and text-faint on glass measures under the 4.5:1
     floor the readability harness holds every label to. */
  .omni-row.omni-dead{ cursor:default; }
  .omni-row.omni-dead:hover .omni-t{ color:inherit; }
  .omni-t{ flex:1; min-width:0; font-size:var(--type-body); font-weight:600; overflow:hidden;
    text-overflow:ellipsis; white-space:nowrap; }
  .omni-s{ font-size:var(--type-body); color:var(--text-dim); flex:none; max-width:44%; overflow:hidden;
    text-overflow:ellipsis; white-space:nowrap; }

  /* THE SENTENCE THE MATCH WAS ACTUALLY IN.
     search-everything.js computes this for every mail row (snippetAround) for
     one stated reason: a message found by its BODY usually has a subject that
     does not contain the word, so without it the row reads as a wrong result —
     "Re: insurance" with no visible reason it is on the page. It was computed,
     scored against, and sent over the wire, and the browser dropped it.
     Its own line under the title rather than beside it: it is a fragment of a
     sentence and squeezing it into the 44% the subtitle gets would show four
     words of it. flex-basis:100% on a wrapping row is what puts it there
     without any row that HAS no snippet changing shape. */
  .omni-row{ flex-wrap:wrap; }
  .omni-snip{ flex-basis:100%; font-size:var(--type-body); line-height:1.45; color:var(--text-dim);
    margin-top:var(--space-1); overflow:hidden; display:-webkit-box; -webkit-line-clamp:2;
    -webkit-box-orient:vertical; }

  /* ======================================================================
     THE COMMAND PALETTE.   Row 1825.
     ----------------------------------------------------------------------
     The box above is the same room it has always been; what changed is that
     every row in it is now a thing that HAPPENS on Return rather than a record
     it found, and a list of verbs has to look different from a list of results.
     Three differences, and each one is doing a job:

       A ROW IS A BUTTON, NOT A TABLE ROW. The hairline under every row went
       away and the rows got a corner and a ground instead. A ruled list reads
       as data you are meant to scan; a stack of soft rectangles reads as things
       you are meant to press, which is what these are.

       ONE ROW IS ALWAYS LIT. A palette without a selected row is a palette
       where Return is a guess. The lit row carries the accent down its left
       edge, the accent on its glyph, and the ⏎ mark on its right — three cues
       rather than one, because on the matte themes a background wash alone is
       nearly invisible and on the coloured papers it is nearly invisible the
       other way.

       EVERY SIZE, CORNER, SHADOW AND DISTANCE IS A TOKEN. This surface is new
       as of today, so there was no reason to write a single raw number in it:
       the type is --type-label and --type-body, the corners are
       --radius-control inside --radius-card (concentric — see tokens.css), the
       lift is --elev-floating, and every gap and pad is a multiple of the 4px
       unit. It is the first part of the app built entirely to the scale, and it
       is here rather than at the top of the file because it belongs beside the
       rules it overrides.

     APPENDED, NOT MERGED INTO THE RULES ABOVE. Those rules still dress the
     search results the palette inherited and the harnesses that measure their
     contrast still read them; these sharpen what they produce. ====================================================================== */

  /* .compose-modal.omni-card AND NOT .omni-card, AND THAT IS A BUG REPORT.
     The block at the head of this section sets the box to 640px wide with no
     padding and no scrolling of its own. NONE OF THOSE THREE HAVE EVER TAKEN
     EFFECT. .compose-modal, three thousand lines further down this file, sets
     max-width:860px, padding:26px and max-height with overflow-y:auto — one
     class against one class, and the later rule wins. So the search box has
     been 860px wide inside a 26px inset for as long as both rules have existed,
     and the card itself was the thing that scrolled, which is why anything at
     the foot of the list could be scrolled clean out of the card.
     Measured, not read: getBoundingClientRect on a 1440px window returns 834.
     The dead rules are left exactly as they are — this file is shared and a
     reformat is a guaranteed conflict — and these carry one more class so they
     genuinely apply. */
  .compose-modal.omni-card{ max-width:680px; padding:0; overflow:hidden;
    border-radius:var(--radius-card); box-shadow:var(--elev-floating);
    max-height:min(78vh, 660px); display:flex; flex-direction:column; }

  /* THE HEAD. A magnifier, the field, and the key that gets you out — one row,
     so the field is no longer a lone rectangle floating in a card. */
  .omni-head{ flex:none; display:flex; align-items:center; gap:var(--space-3);
    padding:0 var(--space-5); border-bottom:1px solid var(--glass-border); }
  .omni-glass{ width:18px; height:18px; flex:none; color:var(--text-faint); }
  /* .omni-card .omni-head input.omni-input AND NOT SOMETHING SHORTER, because
     of one rule 2,700 lines above this: `:root:not([data-mode="light"])
     .field:focus` gives every focused field in the app a 14px accent glow. That
     is right for a form — a lit box is how you find the field you are in — and
     wrong for this one, where the box IS the dialog and a glowing rectangle
     inside a glowing card reads as two overlapping cards. Its specificity is
     four classes, so a two-class selector here loses to it silently, which is
     what the first version of this block did: the screenshot showed a blue
     halo nobody had written. This wins on the tie by being later in the file.
     margin-bottom too — .field carries 10px of it for stacked form rows, and
     inside a flex head it was an unexplained gap under the caret. */
  .omni-card .omni-head input.omni-input{ flex:1; min-width:0; border:none;
    border-radius:0; margin:0; padding:var(--space-5) 0;
    font-size:var(--type-lead); background:transparent;
    /* appearance:none because the field is the dialog here. Without it some
       builds still paint the platform's own text-control chrome — a pale fill
       and a blue underline — underneath a box whose computed background,
       border, outline and box-shadow are all zero. It showed up in the
       screenshots for this row as a rectangle nobody had written. */
    appearance:none; -webkit-appearance:none; }
  .omni-card .omni-head input.omni-input:focus{ box-shadow:none; outline:none;
    border:none; background:transparent; }
  /* "esc" rather than a ⌘ hint: there is exactly one Escape key and it is on
     every keyboard, so unlike the ⌘K on the rail this needs no correction at
     boot for the machine it happens to be running on. */
  .omni-esc{ flex:none; font-size:var(--type-label); font-weight:var(--weight-medium);
    letter-spacing:.06em; color:var(--text-dim); border:1px solid var(--glass-border);
    border-radius:var(--radius-tight); padding:var(--space-hair) var(--space-2); }

  /* THE LIST SCROLLS AND THE FOOT DOES NOT, which is the whole reason the list
     is wrapped in a second element. What the palette could not read today is
     the one sentence in this box that must never be below the fold: a source
     that failed silently is the failure this control exists to avoid, and a
     warning you have to scroll to find is a warning that was not given.
     #omniResults itself stays the element three harnesses read the text of, so
     it holds both halves and simply stops being the thing with a scrollbar. */
  .omni-card .omni-results{ flex:1; min-height:0; max-height:none; overflow:visible;
    padding:0; display:flex; flex-direction:column; }
  .pal-list{ flex:1; min-height:0; overflow-y:auto; scrollbar-width:thin;
    padding:var(--space-2) var(--space-3) var(--space-3); position:relative; }
  /* A LIST THAT CONTINUES BELOW THE FOLD HAS TO SAY SO, AND ONE THAT DOES NOT
     MUST NOT. Without the fade the last visible row sits flush against the
     foot's hairline and reads as the end of the results — so a query with
     twenty matches looks like a query with six. With the fade applied
     unconditionally the opposite lie is told: the last row of a short list is
     dimmed as though something were behind it. So it is a class, and
     paletteRender puts it on only when scrollHeight actually exceeds
     clientHeight. Item 211 applies to a gradient exactly as it does to a
     number: it is a claim about what is there.

     A MASK RATHER THAN A GRADIENT OVERLAY, because an overlay needs a colour
     and the only colour it could be is whatever the glass resolves to on this
     particular theme — a literal that is wrong on the other twelve. A mask
     fades the list's own pixels and is right on every ground. currentColor
     rather than black for the opaque stop: a mask reads alpha, not hue, and
     currentColor keeps a hex out of a file where a hex would be a bug. */
  .pal-list.pal-more{
    -webkit-mask-image:linear-gradient(to bottom, currentColor calc(100% - 20px), transparent);
    mask-image:linear-gradient(to bottom, currentColor calc(100% - 20px), transparent); }
  .omni-results .section-label{ margin:var(--space-4) 0 var(--space-1);
    padding-inline:var(--space-3); }
  .pal-list > .section-label:first-child{ margin-top:var(--space-2); }

  /* THE ROW. The hairline goes, the corner and the padding arrive. */
  .omni-results .omni-row{ align-items:center; gap:var(--space-3);
    padding:var(--space-2) var(--space-3); border-bottom:none;
    border-radius:var(--radius-control); position:relative;
    transition:background var(--dur-fast) var(--ease-out); }
  /* THE SUBTITLE SITS BESIDE THE TITLE, NOT AT THE OTHER END OF THE ROW.
     It was right-aligned, which is correct for a column of dates or counts and
     wrong for these: "Northfield Partners" and "Searches every mailbox for
     dan@northfield.co" are the second half of the sentence the title starts,
     and a 300px gulf between the two halves reads as a table somebody forgot
     to rule. Left-aligned they read as one line, and the right end of the row
     is left to the one thing that genuinely belongs there — the ⏎. */
  .omni-results .omni-row .omni-t{ flex:0 1 auto; font-size:var(--type-body);
    font-weight:var(--weight-strong); }
  .omni-results .omni-row .omni-s{ flex:1 1 auto; min-width:0; max-width:none;
    text-align:left; font-size:var(--type-label); }
  .omni-results .omni-snip{ font-size:var(--type-label); margin-top:var(--space-1);
    padding-left:calc(16px + var(--space-3)); }

  /* The glyph. 16px so it sits inside the cap height of the 13px title rather
     than beside it — a row of icons taller than the words they label is the
     thing that makes a list look like a toolbar. */
  .pal-ico{ width:16px; height:16px; flex:none; color:var(--text-dim);
    transition:color var(--dur-fast) var(--ease-out); }
  /* A result the server sent with nowhere to open keeps the indent so the
     titles line up, and gets no glyph, because it is not a thing you press. */
  .pal-ico-none{ width:16px; height:16px; flex:none; display:inline-block; }

  /* Its own width, so the glyph is never the thing the ellipsis eats — which is
     exactly what happened to the "· ⏎" that used to live on the end of the ask
     row's subtitle. See omniAskRow(). */
  .pal-key{ flex:none; width:14px; text-align:right; font-size:var(--type-body);
    color:var(--accent-2); opacity:0; transition:opacity var(--dur-fast) var(--ease-out); }

  /* THE SELECTED ROW, IN THREE CUES. The wash alone was tried and it is not
     enough: on the matte themes it is almost nothing and on the light papers it
     is almost nothing the other way. The accent edge is what actually reads. */
  .omni-results .omni-row.pal-sel{ background:var(--wash-hover); }
  .omni-results .omni-row.pal-sel::before{ content:''; position:absolute;
    left:0; top:var(--space-2); bottom:var(--space-2); width:2px;
    border-radius:var(--radius-pill); background:var(--accent); }
  .omni-row.pal-sel .pal-ico{ color:var(--accent-2); }
  .omni-row.pal-sel .pal-key{ opacity:1; }
  :root[data-mode="light"] .omni-row.pal-sel .pal-ico,
  :root[data-mode="light"] .omni-row.pal-sel .pal-key{ color:var(--accent); }
  /* The old hover recoloured the title. With a lit row doing that job, a second
     colour change under the cursor is two controls saying the same thing. */
  .omni-results .omni-row:hover .omni-t,
  :root[data-mode="light"] .omni-results .omni-row:hover .omni-t{ color:inherit; }

  /* THE QUESTION ROW keeps its own outline, because it is the one row that is
     not about something Lucid already holds — it is an offer to go and think.
     Re-cornered to --radius-control so it is the same shape as its neighbours
     rather than the 10px it was written at before there was a scale. */
  .omni-results .omni-row.omni-ask{ border:1px solid var(--glass-border-hi);
    border-radius:var(--radius-control); padding:var(--space-2) var(--space-3); }
  .omni-results .omni-row.omni-ask.pal-sel{ border-color:var(--accent); }

  /* WHILE THE SLOW HALF IS STILL OUT. Set at body size and in the dim ink
     rather than the faint one: it is a sentence about what the app is doing,
     and --text-faint on glass measures under the 4.5:1 floor the readability
     harness holds every label to. */
  .pal-wait{ font-size:var(--type-body); color:var(--text-dim);
    padding:var(--space-2) var(--space-3); }

  /* THE FOOT — what this box covers, and under it, in the warning ink, whatever
     it could not reach today. --orange rather than the --amber the rest of the
     file reaches for: --amber is defined nowhere in this stylesheet, so all
     sixteen of those rules are running on their hardcoded fallback and are the
     same colour on every theme. --orange is defined thirteen times, once per
     ground, which is what a warning colour has to be. */
  .pal-foot{ flex:none; font-size:var(--type-label); line-height:1.5;
    color:var(--text-dim); padding:var(--space-3) var(--space-5) var(--space-2);
    border-top:1px solid var(--glass-border); }
  .pal-foot-warn{ color:var(--orange); border-top:none;
    padding-top:0; padding-bottom:var(--space-3); }

  /* A PHONE HAS NO ⌘K AND NO ROOM FOR A 46% SUBTITLE. The sub drops under the
     title on its own line — the same trick .omni-snip already uses — so a long
     "Searches every mailbox for dan@northfield.co" is readable instead of four
     words and an ellipsis. */
  @media(max-width:560px){
    .omni-head{ padding:0 var(--space-4); }
    .omni-head .omni-input{ padding:var(--space-4) 0; font-size:var(--type-body); }
    .omni-esc{ display:none; }
    .omni-results .omni-row .omni-s{ flex-basis:100%; max-width:100%;
      padding-left:calc(16px + var(--space-3)); }
    .pal-key{ display:none; }
  }

  /* The brief is prose, so it is set as prose — wider measure, looser line, and
     the serif, because it is the only thing on the page written in sentences. */
  .ht-brief{ font-family:var(--font-display); font-size:var(--type-lead); font-weight:400; line-height:1.6;
    letter-spacing:-.005em; max-width:60ch; }

  /* WHAT LUCID KNOWS ABOUT THEM — the list the paragraph above was written
     from. Deliberately quieter than the paragraph: that is the read, these are
     the receipts, and receipts are checked rather than read. No card border,
     because they are part of the same tab and not a separate object; a hairline
     down the left instead, which is what the rest of the app uses for
     "this came from somewhere". */
  .ck-head{ font-size:var(--type-label); font-weight:600; letter-spacing:.03em;
    text-transform:uppercase; color:var(--text-faint); margin-bottom:var(--space-3); }
  .ck-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column;
    gap:var(--space-3); max-width:60ch; }
  .ck-item{ padding-left:var(--space-3); border-left:2px solid var(--glass-border); }
  .ck-text{ font-size:var(--type-body); line-height:1.5; color:var(--text); }
  /* The provenance line. Smaller and dimmer than the fact, because a date and
     a source are how you check a claim and never the claim itself. */
  .ck-foot{ font-size:var(--type-label); color:var(--text-faint); margin-top:2px; }

  /* TELL LUCID (14 Oct). The box sits between the heading and the list, so
     what he types lands directly above where it will appear. One line, not a
     textarea: these are facts, and a fact that needs a paragraph is a note. */
  .tlk-add{ display:flex; gap:var(--space-2); align-items:center; max-width:60ch; margin-bottom:var(--space-3); }
  .tlk-add .tlk-in{ flex:1; min-width:0; margin:0; }
  .tlk-add .tlk-btn{ flex:none; }
  /* HIS OWN LINES are marked by the rule down their left in the accent rather
     than the hairline — the same "this came from somewhere" mark, saying the
     somewhere is him. */
  .ck-item.tlk-mine{ border-left-color:var(--accent); }
  .tlk-verbs{ margin-left:var(--space-2); }
  .tlk-verbs .ht-verb{ margin-right:var(--space-2); }
  .tlk-note{ margin:var(--space-2) 0 0; }
  /* In the company card's narrow side column the button goes under the box. */
  .tlk-box .tlk-add{ flex-wrap:wrap; }
  .tlk-box .tlk-add .tlk-in{ flex-basis:100%; }
  .tlk-box .ck-list{ gap:var(--space-2); }
  /* Edit and Delete on their own line there, or Delete wraps alone under the date. */
  .tlk-box .tlk-verbs{ display:block; margin:var(--space-1) 0 0; }

        
  .dg-line{ font-size:var(--type-body); line-height:1.55; color:var(--text); padding:var(--space-2) var(--space-2); border-radius:var(--radius-control); }
  .dg-line.is-go{ cursor:pointer; }
  .dg-line.is-go:hover{ background:rgba(120,120,140,.12); }


  /* ==========================================================
     THE LAUNCH PAD.  (1728)
     ----------------------------------------------------------
     "Launch pad on home with quick links to projects, board, music, the whole
      gambit."                                    (Matt, 11 September.)

     IT IS NOT A ROW OF TILES AND IT MUST NOT LOOK LIKE ONE. .home-grid below
     is content — what is happening today — and this is navigation. Three
     decisions keep the two apart at a glance without a divider between them:

       · IT SITS ABOVE THE GRID AND UNDER THE BAND, in the gap the two already
         leave each other, so it reads as the bottom edge of the masthead
         rather than as the first row of the page.
       · THE THREE LARGE PADS ARE THE ONLY FILLED SURFACES IN THIS PART OF THE
         PAGE. Every tile under them is transparent with a hairline over its
         label; a filled, bordered, liftable panel therefore reads as a button
         without a single word saying so.
       · SIZE IS THE RANKING. The first three get a panel each, everything else
         gets a pill. That is the whole of what "the two or three he uses every
         day given more room than the rest" means in pixels.

     THE COLOUR IS THE RAIL'S FIVE-AREA PALETTE, borrowed, not invented — work,
     media, life — and it is carried by --lp-c so that one token per pad tints
     the icon, the hover border and the chip's dot together. Matt's own
     constraint: "love the color per area (which scared me as I dont want a
     circus sideshow)". A hue per ROOM would be the sideshow; a hue per area is
     what makes sixteen doors readable in one glance.
     ========================================================== */
  .launchpad{ margin:0 0 var(--space-6); }
  .lp-head{ display:flex; align-items:center; gap:var(--space-3); margin-bottom:var(--space-3); }
  /* Set exactly like .ht-title, because it is the same KIND of thing — the
     label over a region of the front page — and inventing a second heading
     style for one band is how a page stops looking like it was drawn by one
     person. */
  /* THE SENTENCE THAT SAYS HOW MUCH OF THIS ORDER IS HIS. Item 211 lives here:
     launchPadHtml() writes one of three different sentences depending on how
     much history there actually is, and this is where it is read. Quiet, but
     --text-dim rather than --text-faint, for the reason the Home labels give
     four hundred lines up: tracked small type puts less ink on the page than
     its size suggests. */
  /* --text-dim rather than --text-faint, for the reason the Home labels give in
     their own note: small tracked type puts less ink on the page than its size
     suggests. Measured from pixels across all sixteen themes, faint put this at
     4.64:1 on Linen — over the floor by four hundredths, which is not a margin,
     it is luck. */

  /* THREE ACROSS, AND THE SAME THREE STACK RATHER THAN SHRINK. A pad narrower
     than about 240px cannot hold its description on two lines, and a pad with
     its description clipped is a pad that has stopped explaining itself —
     which is the only thing separating this from a row of unlabelled squares. */
  .lp-big{ display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:var(--space-4); }
  @media(max-width:1180px){ .lp-big{ grid-template-columns:repeat(2, minmax(0,1fr)); } }
  @media(max-width:760px){ .lp-big{ grid-template-columns:1fr; } }

  /* ONE PIXEL OF LIFT, NOT SIX. The page has a sky over it and a grid under it;
     a card that jumps on hover would be the busiest thing on the screen. */
  /* Two lines and then it stops. The descriptions come from NAV_GROUPS and are
     written to one length; clamping is the guard against the day somebody adds
     a room with a paragraph under it and knocks a row of pads out of line. */
  /* THE ARITHMETIC BEHIND THE RANKING, and it is only on pads that have some.
     Mono because it is figures, and quieter than the line above it because it
     is the evidence rather than the point. A pad he has never opened has no
     .lp-meta element at all — an empty line where a number goes reads as a
     number that failed to load.

     AND IT IS --text-dim, NOT --text-faint. Faint measured 4.48:1 on Linen from
     real pixels — under the floor, on the theme with the least ink to spare.
     The separation from the line above it is carried by the monospace and the
     size, which is enough: this is the only mono on the pad. */

  .lp-rest{ display:flex; flex-wrap:wrap; gap:var(--space-2); margin-top:var(--space-3); }
  .lp-chip{
    --lp-c:var(--accent);
    display:inline-flex; align-items:center; gap:var(--space-2); cursor:pointer;
    font-family:inherit; font-size:var(--type-body); font-weight:500; color:var(--text-dim);
    padding:var(--space-2) var(--space-3) var(--space-2) var(--space-3); border-radius:var(--radius-pill);
    border:1px solid var(--glass-border); background:transparent;
    transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out),
               background var(--dur-fast) var(--ease-out);
  }
  .lp-chip[data-fam="work"] { --lp-c:var(--area-work); }
  .lp-chip[data-fam="media"]{ --lp-c:var(--area-media); }
  .lp-chip[data-fam="life"] { --lp-c:var(--area-life); }
  .lp-chip:hover{ color:var(--text); background:var(--wash-soft);
    border-color:color-mix(in srgb, var(--lp-c) 45%, transparent); }
  .lp-chip:focus-visible{ outline:2px solid var(--lp-c); outline-offset:2px; }
  /* The dot is the only colour on a chip, and it is doing the job the icon does
     on a pad: saying which part of the app this door leads into. */

  /* THE SAME PAD, OVER WHATEVER YOU WERE DOING. ⌘J.
     ------------------------------------------------------------------
     IT IS THE ONE PANEL IN LUCID THAT IS NOT GLASS, AND THAT WAS MEASURED
     RATHER THAN CHOSEN. It was built taking the omnibox's own ground — four
     percent of the page showing through, which the note beside that rule
     defends — and a screenshot of it open over the Vault had "Who to call",
     "House manual" and "Subscriptions" legible THROUGH the pads. The omnibox
     gets away with four percent because what sits on it is one input and a list
     of rows on a flat field; this card is eleven bordered surfaces with three
     sizes of type on them, and every extra edge gives the page behind it
     another place to show. So the last layer is --bg-mid, which is opaque in
     every theme, and the glass tint sits on top of it — same colour, same
     sheen, nothing behind it. A launcher is not a reading surface you want to
     see your work through; it is a thing you want gone in half a second.

     WIDER THAN A DIALOG, because it is not one: 880px is what lets three pads
     hold their descriptions on two lines rather than three, which is the same
     240px-per-pad floor the band on Home is built to. */
  /* The count and the "when" are one statement and they read as two the moment
     they break across a line. Inside the overlay they have less room than on
     Home, so they are held on one line and clipped if a theme's figures are
     wider than expected. */

  /* THE PHONE, WHERE TWO OF THESE THINGS STOP BEING TRUE.
     ------------------------------------------------------------------
     THE KEYBOARD HINT GOES, because there is no ⌘ and no Ctrl on a phone.
     Item 211 is usually about a count, and it is the same rule: a control
     printing a chord that this machine cannot produce is a sentence that is
     not true of the thing it sits beside. The pad's other door — the band
     itself — is right underneath it, and on a phone that is the only one.

     AND THE DESCRIPTIONS GO, because the pads stack into one column here and
     three descriptions is four hundred pixels of Home spent explaining rooms
     whose names already say what they are. What is kept is the part that could
     not be guessed: the name, and the count that says why it is at the top.
     The chips below have never carried a description either, so this makes the
     two halves of the pad agree with each other rather than making one of them
     worse. */

  /* ==========================================================
     THE WALL — one column, whatever it came from.
     ----------------------------------------------------------
     A YouTube video, a Reddit thread, a Bluesky post and a newspaper article
     are five fields each — who, what, when, a link, maybe a picture — so they
     get one card, not five. Anything else would be a design that has to be
     redrawn every time a network is added.
     ========================================================== */
  /* THE RIVER.
     It used to be an 820px column of rows with a 104x72 thumbnail — a list of
     links on a page twice as wide as itself, which is what "I don't like the
     layout" was about. Now: cards in a responsive grid, media at 16:9 and big
     enough to be worth looking at, and the whole thing uses the width it has.

     Masonry via CSS columns rather than grid, because feed items are wildly
     different heights — a Bluesky post is two lines and a YouTube video is a
     picture plus a paragraph — and a grid row is as tall as its tallest cell,
     which leaves holes under everything else. */
  /* WATCH · LISTEN · READ used to be four buttons here. They are gone — see the
     long note over mediaListening in app.js. Nothing in the app draws a
     .media-mode any more, so the rules that styled them went with them. */

  /* TILES OR RIVER — a smaller control than Watch/Listen/Read, on purpose.
   *
   * Those four are WHAT you are looking at and this is HOW, which is a lesser
   * question. Two pills the same size and weight as the mode row would read as
   * six equal choices and make the page look like a settings screen. So: text
   * buttons, underlined when live, sitting just under the modes they modify. */

  .media-listen{ max-width:1240px; margin-bottom:var(--space-6); }
  .pod-shelf{ display:grid; gap:var(--space-4); grid-template-columns:repeat(auto-fill, minmax(148px, 1fr)); }
  .pod-card{ display:flex; flex-direction:column; gap:var(--space-2); padding:0; border:0; background:none;
    font-family:inherit; cursor:pointer; text-align:left; min-width:0; }
  .pod-card:focus-visible{ outline:2px solid var(--blue); outline-offset:3px; border-radius:var(--radius-card); }
  .pod-art{ width:100%; aspect-ratio:1; border-radius:var(--radius-card); object-fit:cover; display:block;
    background:rgba(120,120,140,.14); border:1px solid var(--glass-border); }
  .pod-card:hover .pod-art{ border-color:var(--glass-border-hi); }
  .pod-name{ font-size:var(--type-body); font-weight:650; color:var(--text); line-height:1.35;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
  .pod-by{ font-size:var(--type-label); color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

  .pod-head{ display:flex; gap:var(--space-4); align-items:flex-start; margin-bottom:var(--space-5); }
  .pod-head .pod-art{ width:132px; flex:none; }
  .pod-head-main{ min-width:0; flex:1; display:flex; flex-direction:column; gap:var(--space-2); }
  .pod-title{ font-size:var(--type-title); font-weight:700; color:var(--text); line-height:1.25; }
  .pod-desc{ font-size:var(--type-body); color:var(--text-dim); line-height:1.55; max-width:70ch;
    display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical; overflow:hidden; }

  .ep-row{ display:flex; gap:var(--space-3); align-items:center; padding:var(--space-3) var(--space-3); border-radius:var(--radius-card);
    border:1px solid var(--glass-border); background:var(--glass-bg); margin-bottom:var(--space-2);
    font-family:inherit; text-align:left; width:100%; cursor:pointer; }
  .ep-row:hover{ border-color:var(--glass-border-hi); }
  .ep-row:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }
  .ep-row.playing{ border-color:color-mix(in srgb, var(--green) 46%, transparent); }
  .ep-play{ flex:none; width:32px; height:32px; border-radius:var(--radius-round); display:flex; align-items:center;
    justify-content:center; background:rgba(120,120,140,.16); color:var(--text); }
  .ep-play svg{ width:13px; height:13px; }
  .ep-main{ min-width:0; flex:1; display:flex; flex-direction:column; gap:var(--space-1); }
  .ep-title{ font-size:var(--type-body); font-weight:650; color:var(--text); }
  .ep-sub{ font-size:var(--type-body); color:var(--text-dim); }
  .ep-len{ flex:none; font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim);
    font-variant-numeric:tabular-nums; }
  /* How far in you already are. A number would be noise; the bar is the whole
     message, and it is only drawn once there is something to say. */
  .ep-progress{ height:3px; border-radius:var(--radius-tight); background:rgba(120,120,140,.20); margin-top:var(--space-2); }
  .ep-progress i{ display:block; height:100%; border-radius:var(--radius-tight); background:var(--green); }
  .ep-done{ color:var(--green); font-weight:700; }

  /* ---- THE PLAYER --------------------------------------------------------
     Fixed to the bottom of the app rather than living on the Media page,
     because the whole point is that it survives walking to another page. An
     episode that stops when you open your mail is not a player, it is a
     preview. */
  .player-bar{ position:fixed; left:0; right:0; bottom:0; z-index:70; display:none;
    align-items:center; gap:var(--space-4); padding:var(--space-3) var(--space-4) calc(10px + env(safe-area-inset-bottom));
    background:color-mix(in srgb, var(--bg-deep) 92%, transparent);
    backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px);
    border-top:1px solid var(--glass-border-hi); }
  .player-bar.open{ display:flex; }
  .pl-art{ width:44px; height:44px; border-radius:var(--radius-control); flex:none; object-fit:cover;
    background:rgba(120,120,140,.16); }
  .pl-main{ min-width:0; flex:1; display:flex; flex-direction:column; gap:var(--space-1); }
  .pl-title{ font-size:var(--type-body); font-weight:650; color:var(--text); white-space:nowrap;
    overflow:hidden; text-overflow:ellipsis; }
  .pl-show{ font-size:var(--type-label); color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .pl-seek{ display:flex; align-items:center; gap:var(--space-2); }
  .pl-time{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim);
    font-variant-numeric:tabular-nums; flex:none; }
  .pl-range{ flex:1; min-width:80px; height:4px; accent-color:var(--green); cursor:pointer; }
  .pl-btns{ display:flex; align-items:center; gap:var(--space-2); flex:none; }
  .pl-btn{ width:34px; height:34px; border-radius:var(--radius-round); border:1px solid var(--glass-border);
    background:rgba(120,120,140,.10); color:var(--text); cursor:pointer; display:flex;
    align-items:center; justify-content:center; font-family:inherit; font-size:var(--type-label); font-weight:700; }
  .pl-btn:hover{ background:rgba(120,120,140,.20); }
  .pl-btn:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }
  .pl-btn.main{ width:40px; height:40px; background:var(--green); border-color:transparent; color:var(--ink, #06101F); }
  .pl-btn svg{ width:14px; height:14px; }
  .pl-rate{ min-width:42px; border-radius:var(--radius-pill); width:auto; padding:0 var(--space-3); }
  /* THE HEIGHT OF THE BAR, WHICH TWO OTHER RULES HAVE TO KNOW AND BOTH USED TO
     GUESS. The bar is 12px of padding, a 44px piece of cover art and 10px back —
     sixty-six, before whatever the phone's chin adds. It was written out as 66
     in one place and rounded up to 96 in the other, and neither said where the
     number came from. Named here, beside the rule that makes it true, so that a
     taller bar takes the nav and the page's floor with it. */
  :root{ --player-h: 66px; }
  /* The bar covers the bottom nav on a phone, so the nav moves up out of it. */
  body.playing .bottom-nav{ bottom:var(--player-h); }
  /* The bar is fixed, so the page has to make room or its last row hides
     behind it — which on the Media page is an episode you cannot press.
     The bar's own height plus a step of air, and written onto .main's padding
     VARIABLE rather than its padding property so that .view.leaving, which
     reads the same three names to stay where it was standing while it fades,
     does not hang eighty pixels past the bottom while something is playing. */
  body.playing .main{ --main-pad-bottom: calc(var(--player-h) + var(--space-8)); }
  @media(max-width:780px){
    .player-bar{ bottom:0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); }
    .pl-seek{ display:none; }
  }

  .feed-list{ column-width:330px; column-gap:var(--space-4); max-width:1180px; }
  /* A sentence is not a card. Without this the empty-state paragraph is poured
   * into the masonry like everything else and reads down one column and up the
   * next — "Paste a YouTube channel, a subreddit… / so or not." Seen in a
   * screenshot, not caught by an assertion. */
  .feed-list > .ht-empty{ column-span:all; }
  .feed-item{ break-inside:avoid; display:block; margin:0 0 var(--space-4); padding:0;
    cursor:pointer; border:1px solid var(--glass-border); border-radius:var(--radius-card);
    overflow:hidden; background:var(--glass-bg); position:relative;
    transition:transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out); }
  .feed-item:hover{ transform:translateY(-2px); }
  :root:not([data-mode="light"]) .feed-item:hover{
    box-shadow:0 0 0 1px var(--glass-border-hi), 0 16px 40px -16px rgba(0,0,0,.6); }
  :root[data-mode="light"] .feed-item:hover{ box-shadow:var(--elev-raised); }
  .feed-img{ display:block; width:100%; aspect-ratio:16/9; object-fit:cover;
    background:color-mix(in srgb, var(--text) 6%, transparent); }
  /* The play badge only appears on something that will actually play here.
     A triangle over a blog post is a promise the card cannot keep. */
  .feed-play{ position:absolute; top:calc(28.125% - 17px); left:50%; transform:translateX(-50%);
    width:46px; height:46px; border-radius:var(--radius-round); display:flex; align-items:center;
    justify-content:center; background:rgba(0,0,0,.55); color:#fff; pointer-events:none; }
  .feed-play svg{ width:18px; height:18px; margin-left:var(--space-hair); }
  .feed-body{ min-width:0; padding:var(--space-3) var(--space-4) var(--space-4); }
  .feed-src{ font-size:var(--type-label); font-weight:700; letter-spacing:.1em; text-transform:uppercase;
    color:var(--text-dim); display:flex; gap:var(--space-2); align-items:baseline; flex-wrap:wrap; }
  .feed-kind, .feed-when{ font-weight:600; letter-spacing:.06em; color:var(--text-faint); }
  .feed-lead{ font-size:var(--type-lead); font-weight:600; line-height:1.38; margin-top:var(--space-2); }
  .feed-item:hover .feed-lead{ color:var(--accent-2, var(--accent)); }
  :root[data-mode="light"] .feed-item:hover .feed-lead{ color:var(--accent); }
  /* Unfollow, one press, from the thing you are tired of — rather than from a
     dialog three clicks away that lists twenty sources by handle. */
  .feed-drop{ position:absolute; top:9px; right:9px; width:24px; height:24px; border-radius:var(--radius-round);
    display:flex; align-items:center; justify-content:center; font-size:var(--type-lead); line-height:1;
    background:rgba(0,0,0,.5); color:#fff; opacity:0; cursor:pointer;
    transition:opacity var(--dur-fast) var(--ease-out); }
  .feed-item:hover .feed-drop{ opacity:1; }
  .feed-drop:hover{ background:rgba(0,0,0,.75); }
  :root[data-mode="light"] .feed-drop{ background:rgba(255,255,255,.86); color:var(--text); }
  /* THE SPECULAR EDGE. One pixel along the top of every card, and it is most
     of the difference between "a panel" and "a surface". Premium reads as
     depth and light, not as more blur — and this survives reduced effects,
     which blur does not. */
  .feed-item::after{ content:''; position:absolute; inset:0 0 auto 0; height:1px;
    background:linear-gradient(90deg, transparent, rgba(255,255,255,.16) 26%,
      rgba(255,255,255,.16) 74%, transparent); pointer-events:none; }
  :root[data-mode="light"] .feed-item::after{ display:none; }

  /* A CARD WITH NO PICTURE IS NOT A BROKEN CARD.
     In a masonry of photographs the imageless ones become grey gaps, and a
     wall with holes in it reads as a wall failing to load. So they become
     quotes instead: the words set large, in the serif, on a card that is its
     own thing rather than an apology for a missing photo. */
  .feed-quote{ background:linear-gradient(152deg,
    color-mix(in srgb, var(--accent) 9%, var(--glass-bg)),
    var(--glass-bg) 70%); }
  .feed-quote .feed-lead{ font-family:var(--font-display);
    font-size:var(--type-title); font-weight:500; line-height:1.36; letter-spacing:-.01em;
    margin-top:var(--space-2); }
  .feed-quote .feed-body{ padding:var(--space-4) var(--space-4) var(--space-5); }

  .feed-warn{ color:var(--orange-text, var(--orange)); font-weight:700; }
  /* The end of the wall, which is not an end — it is where the next page is
     fetched from. Spans the columns so it cannot be poured into one of them. */
  .feed-more{ column-span:all; text-align:center; padding:var(--space-6) 0 var(--space-2);
    font-size:var(--type-body); letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-faint); }
  @media(max-width:560px){ .feed-list{ column-width:auto; columns:1; } }

  /* An embed is somebody else's page running inside this one, so it is given a
     frame of its own rather than dropped into the column of cards. The border
     and the label are there to make that obvious: what is inside came from X or
     Instagram and is styled by them, and it should not be mistaken for Lucid. */
  /* One line until there is more than one line in it. */
  textarea.feed-add{ flex:1; min-width:0; min-height:0; height:43px; max-height:220px;
    resize:none; overflow-y:auto; line-height:1.4; margin-bottom:0;
    font-family:var(--font-body); white-space:pre-wrap; }

  /* THE SUBJECT ROW'S OWN RULES ARE GONE, because the row is gone: the two
     boxes became one on 25 September and the merged box is the .feed-add
     textarea above. What used to stand here argued that "sharing one box would
     mean a placeholder listing six kinds of thing, which is how a control ends
     up meaning nothing" — a fair observation whose answer turned out to be a
     shorter sentence rather than a second box. The placeholder is now
     "Paste a link, or type a subject", which is 217px wide against the 273px
     the box has at 430 wide. tests/test-follow-one-box.js holds that number. */

  /* People, not posts. These cards are wider and quieter than the feed rows
     below them because they are about somebody rather than about something to
     read — and the ones carrying a fact Lucid can keep are marked. */
  /* The story box. A quote from the first email they ever sent is the line
     that makes somebody remember the moment, so it is set as a quote rather
     than as another row of small grey text. */
  /* THE SPINE. Matt, 26 Aug: "change how you know each other to a vertical
     timeline showing first contact to present with some markers in between."

     The line is a border on the list rather than a pseudo-element, so it grows
     with the content and cannot come adrift from it. The dots sit ON the line
     — half their width to the left of it — which is the only reliable way to
     centre a dot on a border without hard-coding the border's position twice.

     Colour carries meaning here and is not the only thing that does: every
     marker has a dated line and a sentence, so a quiet stretch reads as "Quiet
     for 8 months" whether or not the amber lands. */
  .know-tl{ list-style:none; margin:0 0 var(--space-4); padding:0 0 0 var(--space-5);
    border-left:2px solid var(--glass-border); }
  .know-tl-row{ position:relative; padding:0 0 var(--space-4) 0; }
  .know-tl-row:last-child{ padding-bottom:0; }
  .know-tl-dot{ position:absolute; left:-25px; top:4px; width:9px; height:9px; border-radius:var(--radius-round);
    background:var(--text-faint); box-shadow:0 0 0 3px var(--bg-mid); }
  .know-tl-when{ font-family:var(--font-mono); font-size:var(--type-label); font-weight:600;
    letter-spacing:.06em; text-transform:uppercase; color:var(--text-dim); }
  .know-tl-title{ font-size:var(--type-body); font-weight:600; line-height:1.45; margin-top:var(--space-hair); }
  .know-tl-quote{ font-size:var(--type-body); line-height:1.5; color:var(--text-dim); margin-top:var(--space-1);
    padding-left:var(--space-2); border-left:2px solid var(--glass-border);
    overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
  .know-tl-first .know-tl-dot{ background:var(--green); box-shadow:0 0 0 3px var(--bg-mid), 0 0 9px var(--green-glow); }
  .know-tl-intro .know-tl-dot{ background:var(--violet); box-shadow:0 0 0 3px var(--bg-mid); }
  .know-tl-note  .know-tl-dot{ background:var(--blue); box-shadow:0 0 0 3px var(--bg-mid); }
  .know-tl-quiet .know-tl-dot{ background:transparent; border:2px dashed var(--text-faint);
    width:11px; height:11px; left:-26px; box-shadow:none; }
  .know-tl-quiet .know-tl-title{ color:var(--text-dim); font-weight:500; font-style:italic; }
  .know-tl-owed  .know-tl-dot{ background:var(--orange); box-shadow:0 0 0 3px var(--bg-mid), 0 0 9px var(--orange-glow); }
  .know-tl-owed  .know-tl-title{ color:var(--orange); }
  .know-tl-now   .know-tl-dot{ background:var(--accent); box-shadow:0 0 0 3px var(--bg-mid); }
  /* A TEXT IS NOT AN EMAIL, and the line has to say so at a glance.
   *
   * Green is the first email, blue is a note, orange is a debt. Texting gets
   * teal — near enough the messaging colour everybody already reads as
   * "messages", far enough from the green that "you started texting" and "they
   * emailed first" are not the same dot on a line he scans rather than reads.
   * A hollow centre, because these two markers stand for a whole run of
   * messages rather than one moment. */
  .know-tl-text  .know-tl-dot{ background:var(--teal, #3ec9c0);
    box-shadow:0 0 0 3px var(--bg-mid), 0 0 9px var(--teal-glow, rgba(62,201,192,.45)); }
  /* The two facts that belong to the whole relationship rather than to any one
     moment in it — cadence, and how many addresses — sit under the spine. */
  .know-summary{ padding-top:var(--space-3); border-top:1px solid var(--glass-border); }

  .know-line{ font-size:var(--type-body); line-height:1.55; margin:0 0 var(--space-2); color:var(--text); }
  .know-line:last-child{ margin-bottom:0; }
  .know-dim{ color:var(--text-dim); }
  .know-quote{ font-size:var(--type-body); line-height:1.5; margin:var(--space-2) 0; padding-left:var(--space-3);
    border-left:2px solid var(--glass-border-hi); color:var(--text-dim); font-style:italic; }
  .know-owed{ font-weight:650; }
  .sig-wrap{ max-width:1180px; margin-bottom:var(--space-6); }
  .sig-grid{ display:grid; gap:var(--space-2); grid-template-columns:repeat(auto-fill, minmax(248px, 1fr)); }
  .sig{ display:flex; gap:var(--space-3); align-items:flex-start; padding:var(--space-3) var(--space-3); border-radius:var(--radius-card);
    border:1px solid var(--glass-border); background:var(--glass-bg); font-size:var(--type-body); }
  .sig.learn{ border-color:color-mix(in srgb, var(--green) 44%, transparent); }
  .sig-more{ display:flex; align-items:center; gap:var(--space-2); margin-top:var(--space-2); padding:var(--space-2) var(--space-3);
    border-radius:var(--radius-pill); border:1px solid var(--glass-border); background:var(--glass-bg);
    font-family:inherit; font-size:var(--type-body); font-weight:600; color:var(--text-dim); cursor:pointer; }
  .sig-more:hover{ color:var(--text); border-color:var(--glass-border-hi); }
  .sig-more[aria-expanded="true"] .sig-more-chev{ transform:rotate(180deg); }
  .sig-more-chev{ transition:transform var(--dur-fast) var(--ease-out); }
  .sig-av{ flex:none; width:30px; height:30px; border-radius:var(--radius-round); display:flex; align-items:center;
    justify-content:center; font-size:var(--type-label); font-weight:700; letter-spacing:.02em;
    background:color-mix(in srgb, var(--text) 10%, transparent); color:var(--text); }
  .sig-main{ flex:1; min-width:0; }
  .sig-head{ font-weight:600; line-height:1.35; }
  .sig-why{ font-size:var(--type-label); color:var(--text-dim); margin-top:var(--space-hair); }
  /* Not the green accent. At 11px bold it measured 4.45:1 on cream paper —
     under the floor. The emphasis comes from the ink being full strength next
     to a dimmed label, which is a stronger signal than a colour anyway. */
  .sig-count{ color:var(--text); }
  .sig-act{ font-size:var(--type-body); font-weight:650; color:var(--green-text, var(--green)); cursor:pointer;
    display:inline-block; margin-top:var(--space-2); }
  .sig-act:hover{ text-decoration:underline; }
  .sig-x{ flex:none; cursor:pointer; color:var(--text-faint); font-size:var(--type-lead); line-height:1; }
  .sig-x:hover{ color:var(--text); }
  /* FOLDERS BESIDE THE RIVER — "one river, folders, no counts".
     Narrow first, same as everything else on this page: the rail sits above
     the river and scrolls sideways when there is only one column to put things
     in. Past 1100px it becomes a real left rail.
     There are no numbers in here and there is no rule that could put one in —
     renderFeedFolders() prints a name and nothing else. */
  .feed-split{ display:block; }
  .feed-splitmain{ min-width:0; }
  .feed-folders{ display:flex; gap:var(--space-2); overflow-x:auto; padding-bottom:var(--space-2);
    margin-bottom:var(--space-4); }
  .folder-row{ flex:none; display:flex; align-items:center; gap:var(--space-2);
    padding:var(--space-2) var(--space-4); border-radius:var(--radius-pill);
    font-size:var(--type-body); color:var(--text-dim); background:rgba(120,120,140,.10);
    border:1px solid transparent; cursor:pointer; white-space:nowrap;
    /* transform/opacity only — animating anything that moves a box costs a
       re-layout, and this list redraws on every folder press. */
    transition:opacity .12s ease, transform .12s ease; }
  .folder-row:hover{ color:var(--text); opacity:.92; }
  .folder-row:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }
  .folder-row.active{ color:var(--text);
    background:color-mix(in srgb, var(--accent) 18%, transparent);
    border-color:color-mix(in srgb, var(--accent) 34%, transparent); }
  .folder-new{ background:transparent; border:1px dashed var(--glass-border); color:var(--text-faint); }
  .folder-new:hover{ color:var(--text-dim); }
  @media (min-width: 1100px){
    .feed-split{ display:flex; align-items:flex-start; gap:var(--space-5); }
    .feed-folders{ flex:none; width:186px; display:block; overflow:visible;
      padding:var(--space-2); margin-bottom:0;
      border:1px solid var(--glass-border); border-radius:var(--radius-card);
      background:var(--glass-bg, transparent); position:sticky; top:var(--space-4); }
    .feed-splitmain{ flex:1; }
    .feed-folders .folder-row{ display:flex; width:100%; border-radius:var(--radius-control);
      background:transparent; margin-bottom:2px; }
    .feed-folders .folder-row .nm{ flex:1; min-width:0; overflow:hidden;
      text-overflow:ellipsis; white-space:nowrap; }
    .feed-folders .folder-row.active{ background:color-mix(in srgb, var(--accent) 16%, transparent); }
    .feed-folders .folder-new{ margin-top:var(--space-2); }
  }
  /* THE ONLY MARK ON THE RIVER, AND IT IS NOT A NUMBER.
     column-span:all because the river is a CSS multi-column masonry — without
     it the line would land halfway down one column and mean nothing. With it
     the columns break, everything new sits above, everything already seen sits
     below, and the line reads across the page the way it is meant to. */
  .feed-here{ column-span:all; display:flex; align-items:center; gap:var(--space-3);
    margin:var(--space-4) 0; color:var(--text-faint); }
  .feed-here::before, .feed-here::after{ content:''; height:1px; flex:1;
    background:var(--glass-border); }
  .feed-here b{ color:var(--text-dim); font-weight:700; letter-spacing:.06em;
    text-transform:uppercase; font-size:var(--type-label); }

  /* NARROW FIRST. One column, embeds above the river — which is the right
     answer when there is only one column to put things in. The rail below is
     the wide-screen case, not the default. */
  .feed-two{ display:block; max-width:1180px; }
  .feed-embeds{ display:flex; flex-direction:column; gap:var(--space-6); margin-bottom:var(--space-6); }
  .feed-embeds:empty{ display:none; margin:0; }

  /* THE RAIL. Past 1100px there is room for the embeds to sit beside the feed
     instead of on top of it, which is the whole fix: following one Facebook
     page used to push every card 640 pixels down the screen and leave the
     right-hand half of the page empty next to it.

     360px is not arbitrary — it is a hair over Facebook's own minimum for the
     page plugin (180–500), wide enough that a post's picture is worth looking
     at, and narrow enough to leave two full masonry columns beside it. */
  @media (min-width: 1100px){
    .feed-two{ display:flex; align-items:flex-start; gap:var(--space-5); max-width:1560px; }
    /* Not sticky. Two embeds are already taller than the window, so pinning
       the rail would pin something that cannot fit and the bottom of it would
       become unreachable. */
    .feed-two > .feed-embeds{ flex:none; width:360px; margin-bottom:0; }
    .feed-two > .feed-list{ flex:1; min-width:0; max-width:none; }
    /* An empty rail must not leave a 380px hole where it would have been. */
    .feed-two > .feed-embeds:empty + .feed-list{ max-width:1180px; }
  }

  .feed-embed{ border:1px solid var(--glass-border); border-radius:var(--radius-card); overflow:hidden;
    background:var(--glass-bg, transparent); }
  .feed-embed .feed-src{ padding:var(--space-3) var(--space-4); border-bottom:1px solid var(--glass-border); }
  .feed-embed .x{ margin-left:auto; cursor:pointer; color:var(--text-faint); font-size:var(--type-lead);
    line-height:1; padding:0 var(--space-hair); flex:none; }
  .feed-embed .x:hover{ color:var(--text); }
  /* Starts at a readable height and is resized to fit by the frame itself — see
     the note by renderEmbeds(). If the frame never reports back, this height is
     what he is left with, so it is a usable one rather than a placeholder. */
  .feed-embed-frame{ display:block; width:100%; height:520px; border:0; }

  /* Sports. A win and a loss have to be tellable apart at a glance and without
     colour alone, so the letter carries it and the tint only reinforces. */
  .sport-row{ gap:var(--space-3); }
  .sport-badge{ width:18px; flex:none; font-family:var(--font-mono); font-size:var(--type-label);
    font-weight:700; color:var(--text-dim); }
  .sport-badge.won{ color:var(--green-text, var(--green)); }
  .sport-badge.lost{ color:var(--text-faint); }
  :root{ --green-text:var(--green-2, var(--green)); }
  :root[data-mode="light"]{ --green-text:var(--green); }

  .sport-story-img{ width:100%; height:120px; object-fit:cover; border-radius:var(--radius-card); margin-bottom:var(--space-2); }

  /* ==========================================================
     THE SPORTS DESK'S TEAMS RAIL — ?rail=teams
     ==========================================================
     The left column of the native Sports Desk is a 340-point web view holding
     nothing but this: crest, record, next fixture, one team per card. The
     whole of the rest of Lucid is switched off by the first two rules — the
     app root is hidden outright rather than restyled, because a spine and a
     dock squeezed into 340 points is what the rail looked like before this
     shipped, and it was described accurately in mac/SPORTS-DESK.md as
     "cramped, and real".

     THE MAX-WIDTH IS FOR THE BROWSER, NOT FOR THE APP. Inside the Mac app the
     view is already 340 wide and this rule never bites. Opened in an ordinary
     tab — which is the only way anybody will ever debug it — it keeps the
     column at the width it will really have, instead of stretching four cards
     across a monitor and hiding every layout fault the real thing has.

     No glass, no backdrop-filter, no bloom of its own: the body's gradient is
     already behind it and this column is 340 points of a page that is mostly
     somebody else's website. */
  #railRoot{ display:none; }
  :root[data-rail="teams"] #appZoomRoot{ display:none; }
  :root[data-rail="teams"] #railRoot{
    display:block; position:fixed; inset:0; z-index:1;
    overflow-y:auto; overflow-x:hidden;
    max-width:340px; padding:var(--space-4) var(--space-3) var(--space-6); }

  .rail-card{ border:1px solid var(--glass-border); border-radius:var(--radius-card); background:var(--glass-bg);
    padding:var(--space-3) var(--space-3); margin-bottom:var(--space-2); }
  .rail-team{ display:flex; align-items:center; gap:var(--space-3);
    text-decoration:none; color:inherit; border-radius:var(--radius-card); }
  a.rail-team:hover .rail-team-name{ text-decoration:underline; text-underline-offset:3px; }
  /* Keyboard users get the same affordance the pointer gets. --accent is the
     theme's, so this stays legible across all twelve. */
  a.rail-team:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
  .rail-crest{ width:34px; height:34px; object-fit:contain; flex:none; }
  /* A team whose crest could not be found still gets the space, so the names
     stay in one column rather than jumping left on the odd row. Dashed, and
     empty, because it is a gap and should look like one. */
  .rail-crest-none{ display:block; border-radius:var(--radius-control); border:1px dashed var(--glass-border); }
  .rail-team-name{ font-size:var(--type-body); font-weight:650; color:var(--text); flex:1; min-width:0; }
  .rail-record{ font-size:var(--type-body); color:var(--text-dim); flex:none; }
  .rail-next{ font-size:var(--type-body); color:var(--text-faint); margin-top:var(--space-2); line-height:1.45; }
  .rail-note{ font-size:var(--type-body); color:var(--text-dim); line-height:1.55; padding:var(--space-2) var(--space-hair); }
  .rail-acts{ padding:var(--space-1) var(--space-hair); }

  /* ==========================================================
     THE TEAMS FOLD — "fold up to hide and expand screen"
     ==========================================================
     9 October 2026. "On sports desk, my teams should fold up to hide and
     expand screen. Just like Lucid spine, click to open tray and teams like i
     see now."

     THE SAME FOUR DECISIONS AS THE MAIL SPINE, NOT FOUR NEW ONES — the state
     is one localStorage flag that defaults to open, the class goes on the
     WRAPPER (#railRoot), the list is GONE rather than narrowed, and the toggle
     sits outside the part that goes. See applyTeamsSpine() in app.js, and
     .inbox-wrap.spine-open for the rail this copies.

     THE HEAD IS WHAT STAYS. #railRoot is the entire document inside the native
     pane; there is no action bar out here to hang a hamburger on the way the
     mail screen has one, so the rail carries its own one-row head and that row
     is all that is left when the teams fold away.

     AND THE RAIL GIVES ITS WIDTH BACK. The rule above pins #railRoot to
     inset:0 with max-width:340px — the real pane width, kept so the column can
     be debugged in an ordinary tab at the size it will actually have. Folded,
     it claims only as much as its head needs: right:auto and width:max-content
     instead. THIS IS THE HALF OF HIS SENTENCE THE WEB OWNS. The native pane is
     still SportsDesk.railWidth = 340 points whatever this does — a constant in
     mac/Lucid/SportsDeskView.swift, not state — so until that line becomes
     state the site beside it does not widen. Measured both ways in
     tests/test-sportsdesk-folds.js. */
  :root[data-rail="teams"] #railRoot:not(.spine-open){
    right:auto; max-width:none; width:max-content; }

  .rail-head{ display:flex; align-items:center; gap:var(--space-2);
    padding:0 var(--space-hair) var(--space-3); }
  /* min-width:0 with the ellipsis is what keeps a long head from pushing its
     own count off the end of the row — the same rule .msp-label follows. */
  .rail-head-name{ flex:1 1 auto; min-width:0; font-size:var(--type-label);
    font-weight:600; letter-spacing:.04em; text-transform:uppercase;
    color:var(--text-dim); white-space:nowrap; overflow:hidden;
    text-overflow:ellipsis; }
  .rail-head-count{ flex:none; font-size:var(--type-label);
    font-variant-numeric:tabular-nums; color:var(--text-dim); }
  /* NOT KNOWN YET, AND VISIBLY NOT A ZERO — the folder spine's distinction,
     drawn the same way: a dash, at full ink. The 8 October measurement that
     took .msp-count-unknown off opacity:.45 applies here for the same reason,
     so this mark is never dimmer than the numbers it stands in for. */
  .rail-head-unknown{ letter-spacing:.02em; }

  .rail-spine-toggle{ flex:none; display:inline-flex; align-items:center;
    justify-content:center; width:32px; height:32px; padding:0;
    border:1px solid var(--glass-border); border-radius:var(--radius-control);
    background:transparent; color:var(--text); cursor:pointer; }
  .rail-spine-toggle:hover{ background:var(--glass-bg); }
  .rail-spine-toggle:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

  .teams-spine{ display:none; }
  #railRoot.spine-open .teams-spine{ display:block; }

  /* A THUMB'S WORTH OF BUTTON AT PHONE WIDTH, and as a SQUARE rather than a
     stretched row: this control is an icon button, which is the case a
     44-pixel square is right for. A folder row takes its 44 from its height
     instead — applying the square to one of those cut all six folder names to
     nothing on 8 October, which is the mistake not to repeat here. */
  @media (max-width: 500px){
    .rail-spine-toggle{ width:44px; height:44px; }
  }

  /* ==========================================================
     THE MEDIA FRONT PAGE — his #250, "media page is a travesty".
     ==========================================================
     Built out of .wl-panel / .wl-empty / .ht-line, the same pieces the
     Watchlist and Sports pages under it are made of, so walking from the
     summary into the page it summarises is not a change of typeface. What
     is new here is only the two things those pages have no equivalent of:
     the band of channel cards, and the now-playing block.

     EVERY COLOUR IS A TOKEN, and the accent is --area-media — the same teal
     the rail row and the group panel wear. See .nav-item[data-group="media"].
     ---------------------------------------------------------- */

  /* ==========================================================
     THE PLAYER.
     ----------------------------------------------------------
     Transport controls are muscle memory, so the shapes are the ones every
     other player uses and the middle one is the biggest. Nothing here is
     labelled with a word; a triangle has meant play for fifty years.
     ========================================================== */
  .mp-bar{ display:flex; align-items:center; gap:var(--space-2); margin-top:var(--space-3); }
  .mp-btn{ width:32px; height:32px; border-radius:var(--radius-round); border:1px solid transparent;
    background:none; color:var(--text-dim); cursor:pointer; display:grid; place-items:center;
    transition:color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
  .mp-btn:hover{ color:var(--text); background:color-mix(in srgb, var(--text) 8%, transparent); }
  .mp-btn:disabled{ opacity:.35; cursor:default; }
  .mp-btn:disabled:hover{ background:none; color:var(--text-dim); }
  .mp-btn svg{ width:15px; height:15px; fill:currentColor; }
  .mp-btn.big{ width:40px; height:40px; color:var(--text);
    border-color:var(--glass-border); }
  .mp-btn.big svg{ width:17px; height:17px; }
  :root:not([data-mode="light"]) .mp-btn.big:hover{ box-shadow:0 0 16px 0 var(--accent-glow);
    border-color:color-mix(in srgb, var(--accent) 45%, var(--glass-border)); }
  .mp-btn.on{ color:var(--accent-2, var(--accent)); }
  :root[data-mode="light"] .mp-btn.on{ color:var(--accent); }
  .mp-spacer{ flex:1; }

  /* The scrubber is a real input so it can be dragged, arrowed and tabbed to.
     A div with a mousedown handler is none of those things. */
  .mp-seek{ -webkit-appearance:none; appearance:none; width:100%; height:14px; background:none;
    cursor:pointer; display:block; margin:var(--space-3) 0 var(--space-hair); }
  .mp-seek::-webkit-slider-runnable-track{ height:3px; border-radius:var(--radius-tight);
    background:color-mix(in srgb, var(--text) 16%, transparent); }
  .mp-seek::-moz-range-track{ height:3px; border-radius:var(--radius-tight);
    background:color-mix(in srgb, var(--text) 16%, transparent); }
  .mp-seek::-webkit-slider-thumb{ -webkit-appearance:none; width:11px; height:11px; border-radius:var(--radius-round);
    background:var(--text); margin-top:calc(var(--space-1) * -1); border:none; }
  .mp-seek::-moz-range-thumb{ width:11px; height:11px; border-radius:var(--radius-round); background:var(--text); border:none; }
  .mp-seek:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
  .mp-times{ display:flex; justify-content:space-between; font-family:var(--font-mono);
    font-size:var(--type-label); color:var(--text-dim); }

  /* The video window. Sized to the video rather than the other way round: a
     16:9 box that grows with the panel, so the player is never letterboxed
     inside a box that is itself letterboxed. */
  .compose-modal.watch-card{ max-width:min(1080px, 94vw); width:100%; padding:0; overflow:hidden; }
  .watch-head{ display:flex; align-items:center; gap:var(--space-4); padding:var(--space-3) var(--space-5);
    border-bottom:1px solid var(--glass-border); }
  .watch-t{ font-size:var(--type-body); font-weight:600; flex:1; min-width:0;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .watch-out{ font-size:var(--type-label); font-weight:700; letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-dim); text-decoration:none; flex:none; }
  .watch-out:hover{ color:var(--text); }
  .watch-x{ cursor:pointer; color:var(--text-dim); font-size:var(--type-title); line-height:1; flex:none; padding:0 var(--space-hair); }
  .watch-x:hover{ color:var(--text); }
  .watch-frame{ position:relative; width:100%; aspect-ratio:16 / 9; background:#000; }
  .watch-frame iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; display:block; }

  /* WIDE RATHER THAN TALL.
     At 720px the panel was a column: track, then scrubber, then buttons, then
     speakers, then results — five stacked rows and a page that ran off the
     bottom of the screen. There is plenty of width going spare, so above 820px
     the controls sit BESIDE what is playing rather than under it, and the
     results run in two columns. Below that it stacks, which is right on a
     phone. */
  /* Two class names, so this beats .compose-modal's own 720px whichever of the
     two rules the stylesheet reaches first. */
  .compose-modal.music-card{ max-width:940px; width:100%; padding:0; overflow:hidden; }
  .music-now{ padding:var(--space-5) var(--space-6) var(--space-4); border-bottom:1px solid var(--glass-border); }
  .music-head{ display:flex; gap:var(--space-4); align-items:center; min-width:0; }
  .music-seek{ min-width:0; }

  @media(min-width:820px){
    .music-now{ display:grid; grid-template-columns:minmax(0,1fr) auto; gap:0 var(--space-8);
      grid-template-areas:"head bar" "seek seek" "dev dev"; align-items:center; }
    .music-now .music-head{ grid-area:head; }
    .music-now .music-seek{ grid-area:seek; }
    .music-now .mp-bar{ grid-area:bar; margin-top:0; }
    .music-now .music-devices{ grid-area:dev; }
    /* The spacer only exists to push volume to the far end of a full-width row;
       in the two-column layout the row is already as wide as its contents. */
    .music-now .mp-spacer{ display:none; }
    .music-now .mp-bar{ gap:var(--space-1); }
    .music-vol{ margin-left:var(--space-4); }
    #musicResults{ display:grid; grid-template-columns:1fr 1fr; column-gap:var(--space-6); align-content:start; }
    #musicResults .section-label{ grid-column:1 / -1; }
    #musicResults .ht-empty{ grid-column:1 / -1; }
  }
  /* Shorter, because it is now twice as wide. */
  #musicResults{ max-height:min(44vh, 360px); }
  .music-art{ width:76px; height:76px; border-radius:var(--radius-card); object-fit:cover; flex:none;
    background:color-mix(in srgb, var(--text) 6%, transparent); }
  :root:not([data-mode="light"]) .music-art{ box-shadow:var(--elev-raised); }
  .music-t{ font-size:var(--type-lead); font-weight:600; line-height:1.3; }
  .music-s{ font-size:var(--type-body); color:var(--text-dim); margin-top:var(--space-1); }
  .music-on{ font-size:var(--type-label); font-weight:700; letter-spacing:.14em; text-transform:uppercase;
    color:var(--text-dim); margin-top:var(--space-2); }
  .music-vol{ display:flex; align-items:center; gap:var(--space-2); min-width:0; }
  /* Sized explicitly: an inline SVG with no dimensions falls back to 300×150,
     which shoves the slider off the end of the row. */
  .music-vol > svg{ width:15px; height:15px; flex:none; fill:var(--text-dim); }
  .music-vol .mp-seek{ width:88px; margin:0; }
  .music-devices{ display:flex; gap:var(--space-2); flex-wrap:wrap; margin-top:var(--space-4); }
  .music-dev{ font-size:var(--type-body); font-weight:600; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); cursor:pointer;
    border:1px solid var(--glass-border); color:var(--text-dim); background:none;
    transition:all var(--dur-fast) var(--ease-out); }
  .music-dev:hover{ color:var(--text); }
  .music-dev.active{ color:var(--text); border-color:color-mix(in srgb, var(--accent) 55%, var(--glass-border)); }
  :root:not([data-mode="light"]) .music-dev.active{ box-shadow:0 0 14px 0 var(--accent-glow); }

  .music-row{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) 0; cursor:pointer;
    border-bottom:1px solid var(--glass-border); }
  .music-row:last-child{ border-bottom:none; }
  .music-row-art{ width:40px; height:40px; border-radius:var(--radius-control); object-fit:cover; flex:none;
    background:color-mix(in srgb, var(--text) 6%, transparent); }
  .music-row-art.round{ border-radius:var(--radius-round); }
  .music-row-b{ min-width:0; flex:1; }
  .music-row-t{ font-size:var(--type-body); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .music-row:hover .music-row-t{ color:var(--accent-2, var(--accent)); }
  :root[data-mode="light"] .music-row:hover .music-row-t{ color:var(--accent); }
  .music-row-s{ font-size:var(--type-body); color:var(--text-dim); overflow:hidden;
    text-overflow:ellipsis; white-space:nowrap; }
  .music-queue{ font-size:var(--type-label); font-weight:700; letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-dim); flex:none; opacity:0; padding:var(--space-1) var(--space-2); border-radius:var(--radius-control); }
  .music-row:hover .music-queue{ opacity:1; }
  .music-queue:hover{ color:var(--text); background:color-mix(in srgb, var(--text) 8%, transparent); }
  @media(max-width:560px){ .music-queue{ opacity:1; } }

  /* ==========================================================
     THE VAULT — a filing cabinet nobody has to open.
     ----------------------------------------------------------
     Two shapes only: the renewal strip at the top, which is the reason to come
     here at all, and cards below it. No table. A table of a house, three cars
     and forty subscriptions is a spreadsheet, and a spreadsheet is a thing you
     export rather than a thing you read.
     ========================================================== */
  .vault-head{ display:flex; align-items:flex-start; justify-content:space-between;
    gap:var(--space-4); flex-wrap:wrap; margin-bottom:var(--space-5); }
  .vault-cats{ display:flex; gap:var(--space-1); flex-wrap:wrap; margin-bottom:var(--space-6); }

  .vault-cat{ font-size:var(--type-body); font-weight:600; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill);
    cursor:pointer; color:var(--text-dim); white-space:nowrap; display:inline-flex;
    align-items:center; gap:var(--space-2);
    transition:color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
  .vault-cat:hover{ color:var(--text); background:color-mix(in srgb, var(--text) 8%, transparent); }
  .vault-cat.active{ color:var(--text); background:color-mix(in srgb, var(--text) 11%, transparent); }
  :root:not([data-mode="light"]) .vault-cat.active{
    box-shadow:0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent), 0 0 18px -3px var(--accent-glow); }
  .vault-cat-n{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim); }

  /* Gifts. A row, not a card: the whole value is being able to run your eye
     down "what did I give" without opening anything. */
  .contact-tags{ display:inline-flex; gap:var(--space-2); flex-wrap:wrap; margin-left:var(--space-2); vertical-align:middle; }
  /* The strength band on a contact row. The WORD is always there — colour on
     its own would make the order unreadable to anyone who cannot separate the
     four, and unsearchable to everyone. */
  .cs-chip{ font-size:var(--type-label); font-weight:700; text-transform:uppercase; letter-spacing:.07em;
    padding:var(--space-hair) var(--space-2); border-radius:var(--radius-pill); margin-left:var(--space-2); vertical-align:1px; }
  .cs-strong{ color:var(--green,#3E9E70); background:color-mix(in srgb, var(--green,#3E9E70) 16%, transparent); }
  .cs-active{ color:var(--blue,#5B8DEF);  background:color-mix(in srgb, var(--blue,#5B8DEF) 16%, transparent); }
  .cs-fading{ color:var(--amber,#C08A2E); background:color-mix(in srgb, var(--amber,#C08A2E) 18%, transparent); }
  .cs-dormant{ color:var(--text-dim);     background:rgba(120,120,140,.16); }
  .cs-circle{ color:var(--violet,#9A7BE0); background:color-mix(in srgb, var(--violet,#9A7BE0) 18%, transparent); }
  .cs-circle.faded{ color:var(--text-faint); background:rgba(120,120,140,.12); }
  .contact-tag{ font-size:var(--type-label); font-weight:600; padding:1px var(--space-2); border-radius:var(--radius-pill);
    border:1px solid var(--glass-border-hi); color:var(--text-dim); }
  .found-x{ flex:none; cursor:pointer; color:var(--text-dim); font-size:var(--type-label); font-weight:600; }
  .found-x:hover{ color:var(--text); }
  .gift-row{ display:flex; align-items:baseline; gap:var(--space-3); padding:var(--space-3) 0;
    border-bottom:1px solid var(--glass-border); font-size:var(--type-body); }
  .gift-row:last-child{ border-bottom:none; }
  /* The whole of "make a folder": a box with a name in it, on the source it
     belongs to. Narrow on purpose — a folder name is one or two words. */
  .feed-folder-in{ flex:none; width:118px; padding:4px 8px; font-size:var(--type-label); }
  .gift-dir{ font-size:var(--type-label); font-weight:700; letter-spacing:.1em; text-transform:uppercase;
    flex:none; width:52px; color:var(--text-dim); }
  /* The one place a second colour is allowed on this screen: an idea is not a
     record of something that happened, and reading it as one would be wrong. */
  .gift-idea{ color:var(--accent-2, var(--accent)); }
  :root[data-mode="light"] .gift-idea{ color:var(--accent); }
  .gift-what{ flex:1; min-width:0; font-weight:600; }
  .gift-meta{ font-size:var(--type-body); color:var(--text-dim); flex:none; }
  .gift-row .x{ cursor:pointer; color:var(--text-faint); font-size:var(--type-lead); line-height:1; padding:0 var(--space-1); flex:none; }
  .gift-row .x:hover{ color:var(--cat-red-text); }

  /* GOOGLE'S HUNDRED, WHEN IT IS RUNNING OUT. Item 316. The line is ordinary
     page prose until eighty of the hundred places are gone, at which point it
     takes the same amber every other "this is about to stop working" line in
     Lucid uses — the themed --orange-text where a theme defines one, falling
     back to --orange where it does not, exactly as .feed-warn does. Never a
     hex: fourteen themes and a contrast harness both have opinions. */
  .allow-cap.warn{ color:var(--orange-text, var(--orange)); font-weight:650; }

  .vault-row2{ display:grid; grid-template-columns:1fr 1fr; gap:var(--space-4); }
  @media(max-width:560px){ .vault-row2{ grid-template-columns:1fr; } }


  /* ---- chips: the whole language of the board ---- */
  /* A CHIP IS A TINT WITH TEXT ON IT, AND THE TINT IS THE PROBLEM IN LIGHT MODE.
     An 18% wash of a hue over cream is nearly cream; the hue at full strength on
     top of it lands around 4:1 — close enough to look deliberate and still under
     the line at 10.5px. Deepening the lettering toward the theme's darkest
     neutral keeps the hue recognisable and buys the contrast. Measured, not
     guessed: these were 3.67–4.11 before, 5.0–6.4 after. */
  /* And the mirror of it on the dark side: the red chips put the pastel red on
     a red wash, which is two versions of the same hue a shade apart. Lifted
     toward white so the lettering separates from its own background. */
  :root[data-mode="light"] .ps-on_track{ color:color-mix(in srgb, var(--cat-green) 72%, var(--neutral-2)); }
  :root[data-mode="light"] .ps-at_risk{ color:color-mix(in srgb, var(--cat-orange) 72%, var(--neutral-2)); }
  :root[data-mode="light"] .ps-off_track{ color:color-mix(in srgb, var(--cat-red) 72%, var(--neutral-2)); }
  :root[data-mode="light"] .ps-complete{ color:color-mix(in srgb, var(--cat-blue) 72%, var(--neutral-2)); }
  :root[data-mode="light"] .ps-on_hold{ color:var(--text); }
  /* The "-2" pastels are the light-on-dark variants; on cream they vanish. One
     token per hue that already knows which ground it is on. */
  :root{ --violet-text:var(--violet-2); --teal-text:var(--teal-2); --green-text:var(--green-2); }
  :root[data-mode="light"]{ --violet-text:var(--violet); --teal-text:var(--teal); --green-text:var(--green); }

  /* ---- header ---- */

  /* ---- board ---- */

  /* And the add box is one more slot, not a dashed bar across the whole page —
     which is the exact shape the cards were just rescued from. */
  /* Children ride INSIDE the parent card now. Loose indented rows under a card
     belong to whichever card the grid happened to place beneath them. */

  /* PAST DUE, AND IT HAS TO CATCH THE EYE.
     The same answer the agenda reached in August (see .agenda-item.is-late):
     a slow two-second swell rather than a blink. A blink is an alarm and is
     read as one for about a day, after which it is read as nothing; a breath
     is noticed on the way past and does not have to be dismissed.
     The hue is --cat-red rather than the agenda's --pink because the chip
     already on this card is .pj-chip-overdue, which is drawn in --cat-red — a
     card glowing one red beside a chip printed in another is two warnings.
     The wash uses the solid token and the ring uses --cat-red-text, which is
     the pastel step on dark grounds and the solid one on paper, so the glow is
     visible in both directions instead of only in the theme it was drawn in. */
  .task-card.is-late{
    border-color:color-mix(in srgb, var(--cat-red-text) 50%, var(--glass-border));
    background:color-mix(in srgb, var(--cat-red) 9%, transparent);
    animation:pjLateBreath 2.4s var(--ease-in-out) infinite; }
  /* Anyone who has asked their machine to stop moving things gets the colour
     and none of the motion. The card is still obviously late. */
  @media (prefers-reduced-motion: reduce){
    .task-card.is-late{ animation:none;
      box-shadow:0 0 12px -3px color-mix(in srgb, var(--cat-red-text) 50%, transparent); }
  }

  /* A grid, not a table. Every bar is positioned in the same left-to-right
     pixel space as the day headings above it, so a bar and its date column can
     never disagree — which is the one bug every hand-built gantt has. */

  /* ---- reports ---- */

  /* THE MEETING HEADER IS THIS HEADER, not a copy of it.
     "I like the trays, but I want them like the task trays." Twice. The first
     attempt restated the same declarations under .ed-* and they drifted apart
     again within the day, because two blocks that must agree and are 700 lines
     apart do not stay in agreement. So .ed-crumb is ON these selectors: there
     is one kicker rule in this stylesheet and both headers wear it. */
  .pj-drawer-crumb, .ed-crumb{ font-size:var(--type-label); font-weight:700; text-transform:uppercase; letter-spacing:.06em;
    color:var(--text-faint); margin-bottom:var(--space-2); display:flex; align-items:center; gap:var(--space-2); }
  .pj-drawer-crumb > span:first-child, .ed-crumb > span:first-child{ display:flex; align-items:center; gap:var(--space-2); min-width:0;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  /* 3.64:1 on dark. The one control on this drawer that destroys work should
     not be the one control you have to squint at — and a word that is hard to
     read is a word that gets clicked by accident. --text-dim; the red arrives
     on hover, where it belongs. */
  .pj-del-link, .ed-del-link{ font-size:var(--type-label); font-weight:700; letter-spacing:.06em; color:var(--text-dim);
    cursor:pointer; padding:var(--space-hair) var(--space-2); border-radius:var(--radius-control); }
  .pj-del-link:hover, .ed-del-link:hover{ color:var(--cat-red-text); background:color-mix(in srgb, var(--cat-red) 12%, transparent); }
  button.pj-del-link, button.ed-del-link{ padding:var(--space-1) var(--space-3); font-size:var(--type-label);
    font-weight:700; letter-spacing:.04em; line-height:1.5; flex:none; white-space:nowrap; }
  .pj-drawer-crumb .sp, .ed-crumb .sp{ margin-left:auto; display:flex; align-items:center; gap:var(--space-2); }
  /* color-scheme:dark here used to be the whole fix for a native date field —
     it got the CLOSED box to stop looking like a hole punched in the card,
     and did nothing at all for the picker itself, which still opened as the
     browser's own white calendar. lucidEnhanceDateInput() (app.js) now hides
     the real <input type="date"> and draws both states, so there is no
     native chrome left here to theme. */
  /* A photo on a task (15 October, shared from the iPhone). */

  /* ROW 2143 — WHERE THE CARDS WOULD BE, WHEN THERE ARE NO CARDS AND THAT IS
     NEWS. Four states share this block and only two of them carry a button, so
     the block must read as a standing statement rather than as an error banner:
     the rule down the left is the device .med-note and .vault-renew already use,
     at the same 13% mix, so a page that could not be read looks like part of
     Lucid rather than like a browser error. Nothing here is red. A failed read
     is not a warning to be dismissed, it is a fact about the page, and the one
     thing it must not do is become something the eye learns to skip.
     Every colour is a token — thirteen themes and a contrast harness. */
  /* The status or the exception, under the sentence a person reads. Quieter
     than the line above it and deliberately still on the page: it is what he
     photographs and sends back, and it is the difference between "it did not
     work" and a fault somebody can go and fix. */

  /* Off/on is carried by COLOUR alone, never by opacity. Fading a 13px glyph to
     50% halves the ink in every stroke of it — the star measured 2.18:1 that
     way, against 3.57:1 at full strength in the same colour. Opacity is the one
     lever that cannot be compensated for by choosing a better colour. */
  .create-type-row{ display:flex; gap:var(--space-2); margin-bottom:var(--space-4); }
  .create-type{ flex:1; text-align:center; padding:var(--space-2) var(--space-2); border-radius:var(--radius-card); border:1px solid var(--glass-border-hi); font-size:var(--type-body); font-weight:600; cursor:pointer; color:var(--text-dim); }
  .create-type.active{ background:color-mix(in srgb, var(--accent) 18%, transparent); color:var(--text); border-color:transparent; }
  
  
  
  
  
  
  
  
  .weather-chip{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-3); border-radius:var(--radius-card); flex:none; }
  .weather-icon{ width:19px; height:19px; color:var(--blue-2); flex:none; }
  /* THE "-2" TOKENS ARE THE LIGHT-ON-DARK VARIANT. --blue-2 is a pastel: it
     exists so a blue can be read against a near-black panel. Painted on cream
     it is barely there — the rain figure measured 1.46:1, which is not a
     colour, it is a rumour. In light mode these take the solid variant. */
  :root[data-mode="light"] .weather-icon,
  :root[data-mode="light"] .weather-rain,
  :root[data-mode="light"] .weather-hero-icon,
  :root[data-mode="light"] .forecast-icon,
  :root[data-mode="light"] .forecast-rain{ color:var(--blue); }
  /* Same story for the overdue markers, which are written from script. A class
     rather than an inline style, so light mode can override it at all. */
  .weather-temps{ display:flex; flex-direction:column; line-height:1.15; }
  .weather-hi{ font-size:var(--type-body); font-weight:700; }
  .weather-lo{ font-size:var(--type-label); color:var(--text-faint); }
  .weather-chip{ cursor:pointer; transition:all var(--dur-fast) var(--ease-out); }
  .weather-chip:hover{ border-color:var(--glass-border-hi); }
  .weather-rain{ display:flex; align-items:center; gap:var(--space-1); font-size:var(--type-label); color:var(--blue-2); font-weight:700; border-left:1px solid var(--glass-border); padding-left:var(--space-2); margin-left:1px; }
  .weather-rain svg{ width:11px; height:11px; }
  /* 400px and a scrolling ten-day list: the narrow-and-tall shape, in the one
     card that has the least reason to be either. Six detail tiles across three
     columns and ten forecast rows fit side by side with room over on any
     desktop, and then nothing inside it scrolls at all. */
  .weather-modal{ max-width:620px; }
  @media(min-width:781px){
    .weather-modal{ max-width:820px; }
    .weather-modal .weather-split{ display:grid; grid-template-columns:1fr 1fr; gap:0 var(--space-6); align-items:start; }
    .weather-modal .weather-split > *{ min-width:0; }
    .weather-modal .forecast-list{ max-height:none; overflow:visible; }
    .weather-modal .weather-detail-grid{ grid-template-columns:repeat(2,1fr); }
  }
  .weather-hero{ display:flex; align-items:center; gap:var(--space-4); margin-bottom:var(--space-4); }
  .weather-hero-icon{ width:42px; height:42px; color:var(--blue-2); flex:none; }
  .weather-hero-temp{ font-family:var(--font-display); font-size:var(--type-figure); font-weight:600; line-height:1; }
  .weather-hero-cond{ font-size:var(--type-body); color:var(--text-dim); margin-top:var(--space-1); }
  .weather-detail-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-2); margin-bottom:var(--space-1); }
  .wd-item{ background:rgba(120,120,140,.06); border:1px solid var(--glass-border); border-radius:var(--radius-card); padding:var(--space-2) var(--space-3); }
  .wd-label{ font-size:var(--type-label); text-transform:uppercase; color:var(--text-faint); letter-spacing:.05em; margin-bottom:var(--space-1); }
  .wd-value{ font-size:var(--type-lead); font-weight:700; }
  .forecast-list{ max-height:280px; overflow-y:auto; margin:0 calc(var(--space-1) * -1); padding:0 var(--space-1); }
  .forecast-row{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) var(--space-hair); border-bottom:1px solid var(--glass-border); font-size:var(--type-body); }
  .forecast-row:last-child{ border:none; }
  .forecast-day{ width:38px; font-weight:600; flex:none; }
  .forecast-icon{ width:17px; height:17px; color:var(--blue-2); flex:none; }
  .forecast-rain{ font-size:var(--type-label); color:var(--blue-2); width:32px; text-align:right; flex:none; }
  .forecast-temps{ margin-left:auto; font-family:var(--font-mono); font-size:var(--type-body); display:flex; gap:var(--space-2); }
  .forecast-lo{ color:var(--text-faint); }
  /* THE PICKER, SPEAKING THE SAME LANGUAGE AS EVERY OTHER LIST.
   *
   * "in various places this crappy menu is running loose ... can we make this
   *  look professional and not a kid with a crayon box"   (Matt, 26 August.)
   *
   * Most of what he was looking at was one bug — see .dlg-body, where the
   * dialog was rendering THE INDENTATION OF MY OWN SOURCE CODE as vertical
   * space between the rows. This rule is the rest of it: bare text with a tint
   * on hover, while every comparable list in Lucid — .tidy-row, the task
   * drawer, the trays — is a row of glass cards with a hairline round it. One
   * surface doing the same job in a different dialect is the whole of what
   * "off-brand" means here.
   *
   * So the rows become cards, at the drawer's radius, the drawer's border and
   * the tidy list's 6px gap. Nothing invented for this screen. */
  #dlgList{ display:flex; flex-direction:column; gap:var(--space-2); }
  .dlg-pick{ padding:var(--space-3) var(--space-3); border-radius:var(--radius-card); cursor:pointer;
    background:var(--glass-bg); border:1px solid var(--glass-border);
    transition:border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }
  .dlg-pick:hover{ background:color-mix(in srgb, var(--accent) 12%, var(--glass-bg));
    border-color:var(--glass-border-hi); }
  /* A row with a picture in it — today only the team picker, where the crest
     came free in a payload the list was already fetching. Scoped to .has-icon
     so every other picker in the app keeps the block layout it has always had
     rather than being quietly turned into a flex row. */
  .dlg-pick.has-icon{ display:flex; align-items:center; gap:var(--space-3); }
  .dlg-pick-icon{ width:26px; height:26px; object-fit:contain; flex:none; }
  .dlg-pick.has-icon .dlg-pick-t{ min-width:0; flex:1; }

  

  

  
  
  
  

  

  
  
  

  

  

  

  

  
  
  

  

  
  

  
  

  
  
  

  

  

  

  
  

  .btn.has-reminder{ color:var(--blue); border-color:color-mix(in srgb, var(--blue) 50%, transparent); box-shadow:0 0 10px var(--blue-glow); }
  .chip-select{ display:flex; flex-wrap:wrap; gap:var(--space-2); margin-bottom:var(--space-4); }

  /* ------------------------------------------------------------------
     THE TYPEFACE PICKER'S TWO ROWS AND ITS SAMPLE. Row 2170.

     .settings-sub-label is quieter than .section-label because Text and Titles
     are two halves of one setting — Typeface — and giving each of them the
     full uppercase eyebrow would read as two settings rather than one with two
     parts.

     THE SAMPLE IS DRAWN IN THE TOKENS, NOT IN A FACE, and that is the whole
     point of it: it cannot show a face other than the one the app is actually
     in, because it reads the same two custom properties every other rule
     reads. A sample with its own font-family would be a picture of a decision
     rather than the decision.

     And the row of times is --font-mono deliberately. It is the part of the
     screen this picker CANNOT change, shown while the choice is being made, so
     the sentence above it is visibly true rather than merely claimed.
     ------------------------------------------------------------------ */
  .settings-sub-label{ font-size:var(--type-label); font-weight:var(--weight-strong); color:var(--text-dim);
    margin:0 0 var(--space-2); }
  .ui-face-sample{ border:1px solid var(--glass-border); border-radius:var(--radius-card);
    padding:var(--space-3) var(--space-4); margin-bottom:var(--space-3); background:rgba(120,120,140,.05); }
  .ui-face-sample-title{ font-family:var(--font-display); font-size:var(--type-title);
    font-weight:var(--weight-strong); color:var(--text); margin-bottom:var(--space-1); }
  .ui-face-sample-body{ font-family:var(--font-body); font-size:var(--type-body);
    color:var(--text-dim); margin:0 0 var(--space-2); }
  .ui-face-sample-figs{ display:flex; gap:var(--space-4); font-family:var(--font-mono);
    font-size:var(--type-body); color:var(--text-faint); font-variant-numeric:tabular-nums; }

  /* ------------------------------------------------------------------
     C13: A CHIP ROW, FOLDED.
     "All chips need to fold into chevrons with dropdowns."

     The button is the closed state and the menu holds the chip row itself —
     see foldChipRow() in app.js, which moves the existing element rather than
     replacing it, so the swatches and the handlers are the originals.

     The menu is absolutely positioned on purpose: opening one must not push
     the fourteen controls under it down the card, because a settings pane that
     reflows every time you look at something is worse than the wall of chips
     this replaces. */
  .st-drop{ position:relative; margin-bottom:var(--space-4); }
  .st-drop-btn{
    width:100%; display:flex; align-items:center; justify-content:space-between; gap:var(--space-3);
    padding:var(--space-2) var(--space-3); border-radius:var(--radius-card); border:1px solid var(--glass-border);
    background:rgba(120,120,140,.08); color:var(--text);
    font-family:var(--font-body); font-size:var(--type-body); font-weight:600; text-align:left;
    cursor:pointer; transition:border-color var(--dur-fast), background var(--dur-fast);
  }
  .st-drop-btn:hover{ border-color:var(--glass-border-hi); background:rgba(120,120,140,.14); }
  /* Nothing chosen yet reads as a prompt, not as a value. */
  .st-drop.is-unset .st-drop-value{ color:var(--text-faint); font-weight:600; }
  .st-drop-value{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .st-drop-chev{ width:15px; height:15px; flex:none; color:var(--text-faint);
    transition:transform var(--dur-base) var(--ease-out); }
  .st-drop.open .st-drop-chev{ transform:rotate(180deg); }
  .st-drop-menu{
    display:none; position:absolute; top:calc(100% + 5px); left:0; right:0; z-index:22;
    background:var(--bg-mid); border:1px solid var(--glass-border-hi); border-radius:var(--radius-card);
    box-shadow:var(--elev-floating); padding:var(--space-3);
    max-height:min(52vh, 360px); overflow-y:auto; overscroll-behavior:contain;
  }
  .st-drop.open .st-drop-menu{ display:block; }
  /* The row inside a menu has no margin of its own to give — the menu's padding
     is the spacing now, and a trailing 14px would look like a mistake. */
  .st-drop-menu .chip-select{ margin-bottom:0; }
  /* One choice per line inside the menu. A wrapped grid of chips in a dropdown
     is the same reading problem in a smaller box. */
  .st-drop-menu .chip{ width:100%; text-align:left; }

  /* The two Serene axes. The swatches are painted from the live theme tokens,
     via the [data-paper]/[data-palette] rules further down, so a swatch cannot
     end up showing a colour the theme no longer uses — which is what happens
     every time a picker hard-codes its own hex values. */
  .serene-axis{ display:flex; align-items:center; gap:var(--space-3); margin-bottom:var(--space-hair); }
  .serene-axis-label{ width:56px; flex:none; font-size:var(--type-label); text-transform:uppercase;
    letter-spacing:.08em; color:var(--text-faint); }
  .serene-axis .chip-select{ margin-bottom:var(--space-2); }
  .serene-sw{ width:13px; height:13px; border-radius:var(--radius-tight); display:inline-block; margin-right:var(--space-2);
    vertical-align:-2px; border:1px solid var(--glass-border-hi); }
  /* A palette swatch shows three of its six hues at once — one colour cannot
     say whether a palette is warm or cool, and the difference between these
     three palettes is exactly the relationship between the hues. */
  .serene-sw.sw-pal{ border:none; box-shadow:inset 0 0 0 1px var(--glass-border-hi); }
  /* Relationship score slider — thin track, small glowing thumb, color and
     fill driven entirely by --rel-color/--rel-glow/--rel-pct set inline by
     JS as the value changes. Native <input type="range"> needs full
     appearance resets per-browser to look like this instead of the stock
     OS slider. */
  .rel-slider-row{ display:flex; align-items:center; gap:var(--space-3); }
  .rel-slider{
    flex:1; -webkit-appearance:none; appearance:none; height:4px; border-radius:var(--radius-tight); outline:none; cursor:pointer;
    background:linear-gradient(90deg, var(--rel-color, var(--blue)) 0%, var(--rel-color, var(--blue)) var(--rel-pct, 50%), rgba(255,255,255,.12) var(--rel-pct, 50%), rgba(255,255,255,.12) 100%);
  }
  .rel-slider::-webkit-slider-thumb{
    -webkit-appearance:none; width:14px; height:14px; border-radius:var(--radius-round); margin-top:0;
    background:var(--rel-color, var(--blue)); box-shadow:0 0 0 3px var(--bg-mid), 0 0 14px 3px var(--rel-glow, var(--blue-glow));
    cursor:pointer; transition:box-shadow var(--dur-fast);
  }
  .rel-slider::-webkit-slider-thumb:hover{ box-shadow:0 0 0 3px var(--bg-mid), 0 0 20px 5px var(--rel-glow, var(--blue-glow)); }
  .rel-slider::-moz-range-track{ height:4px; border-radius:var(--radius-tight); background:rgba(255,255,255,.12); }
  .rel-slider::-moz-range-progress{ height:4px; border-radius:var(--radius-tight); background:var(--rel-color, var(--blue)); }
  .rel-slider::-moz-range-thumb{
    width:14px; height:14px; border-radius:var(--radius-round); border:none;
    background:var(--rel-color, var(--blue)); box-shadow:0 0 14px 3px var(--rel-glow, var(--blue-glow));
    cursor:pointer;
  }
  /* The number takes the ordinary ink; the COLOUR of the relationship is
     carried by the slider track beside it, which is a shape rather than text.
     At 13px the accent measured 3.46:1 on dark — and a score you cannot read
     is worse than one with no colour on it. */
  .rel-slider-value{ font-family:var(--font-mono); font-size:var(--type-body); font-weight:700; min-width:28px; text-align:right; color:var(--text); }
  .rel-slider-label{ font-size:var(--type-label); color:var(--text-faint); margin-top:var(--space-2); text-transform:uppercase; letter-spacing:.04em; font-weight:700; }
  .spine-toggle{ display:flex; justify-content:space-between; align-items:center; cursor:pointer; padding:var(--space-hair) var(--space-1) var(--space-hair) 0; }
  .spine-toggle:hover{ color:var(--text-dim); }
  /* THE PILL IS GONE. A count in a rounded grey bubble at 9.5px was the most
     dated object in the spine and the smallest readable thing in the app at
     once. It is now a bare tabular numeral in the same column as the nav counts
     — the overdue state below still fills it, because THAT one is an alarm and
     an alarm is allowed to be a shape. */
  .spine-count{ font-size:var(--type-body); font-weight:600; color:var(--rail-ink-3); margin-left:var(--space-2);
    font-variant-numeric:tabular-nums; }
  .spine-chevron{ width:11px; height:11px; color:var(--rail-ink-3); flex:none; }
  
  
  
  
  
  
  
  
  
  
  
  
  
  /* Below tablet width seven columns stop being readable — stack them. */
  
  
  
  
  
  

  /* Search. Sits in the mail toolbar rather than in a global header, because
     it searches mail specifically — a single box that means different things
     depending on which page you are on is worse than an honest, scoped one. */
  .mail-search{ position:relative; display:flex; align-items:center; flex:0 1 300px; min-width:170px; }
  .mail-search-icon{ position:absolute; left:11px; width:15px; height:15px; color:var(--text-faint); pointer-events:none; }
  .mail-search-input{
    width:100%; padding:var(--space-2) var(--space-8) var(--space-2) var(--space-8); border-radius:var(--radius-pill);
    border:1px solid var(--glass-border); background:rgba(120,120,140,.08);
    color:var(--text); font-family:var(--font-body); font-size:var(--type-body);
    outline:none; transition:border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
  }
  .mail-search-input::placeholder{ color:var(--text-faint); }
  .mail-search-input::-webkit-search-cancel-button{ display:none; }
  .mail-search-input:focus{ border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-glow); }
  .mail-search-clear{ position:absolute; right:10px; color:var(--text-faint); cursor:pointer; font-size:var(--type-lead); line-height:1; display:none; }
  .mail-search.has-value .mail-search-clear{ display:block; }
  .search-status{ padding:var(--space-3) var(--space-3); font-size:var(--type-body); color:var(--text-faint); border-bottom:1px solid var(--glass-border); }
  .search-status b{ color:var(--text-dim); font-weight:600; }
  .search-status .search-warn{ color:var(--orange); }
  @keyframes mail-search-pulse{
    0%   { opacity:.35; transform:scale(.8); }
    50%  { opacity:1;   transform:scale(1.15); }
    100% { opacity:.35; transform:scale(.8); }
  }
  /* A dot pulsing forever is the kind of thing that makes somebody motion-sick,
     and it carries nothing the sentence beside it does not already carry. Held
     still at full strength, so the state stays legible without moving. */
  .shortcut-sheet{ position:fixed; inset:0; z-index:60; display:flex; align-items:center; justify-content:center;
    background:rgba(4,6,16,0.55); backdrop-filter:blur(4px); }
  .shortcut-card{ width:min(520px, calc(100vw - 40px)); padding:var(--space-6) var(--space-6); border-radius:var(--radius-card); }
  .shortcut-grid{ display:grid; grid-template-columns:auto 1fr; gap:var(--space-2) var(--space-5); align-items:center; margin-top:var(--space-4); }
  .shortcut-grid kbd{ font-family:var(--font-mono); font-size:var(--type-body); color:var(--text);
    background:rgba(120,120,140,.13); border:1px solid var(--glass-border-hi); border-radius:var(--radius-control);
    padding:var(--space-1) var(--space-2); white-space:nowrap; text-align:center; }
  .shortcut-grid span{ font-size:var(--type-body); color:var(--text-dim); }

  /* MODAL / FAB */
  /* FIFTEEN FULL-SCREEN BLURS THAT NOBODY COULD SEE.
     Every modal backdrop in the app is in the DOM from the first paint,
     covering the whole viewport at opacity:0. They were each declaring
     backdrop-filter unconditionally, so the compositor was maintaining fifteen
     full-screen blur layers permanently — for dialogs that were closed.
     The blur now belongs to .open. Nothing looks different, because a blur
     behind an invisible element was never visible. */
  .modal-backdrop{ display:flex; position:fixed; inset:0; background:rgba(4,6,16,0.5); z-index:50; align-items:flex-end; justify-content:center;
    opacity:0; pointer-events:none; transition:opacity var(--dur-base) var(--ease-out), backdrop-filter var(--dur-base) var(--ease-out); }
  .modal-backdrop.open{ opacity:1; pointer-events:auto; backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); }
  /* WIDE AND SHORT, NOT NARROW AND TALL.
     Matt, 25 Aug: "Every card is too tall. Edit-a-contact especially. The
     prevailing theme is that cards are narrow and tall when they should be wide
     and short."
     He is describing one rule, not eleven bugs. Every popup in Lucid is a
     .compose-modal, and it was 720px wide with NO height limit — so a form with
     fourteen fields simply grew downwards until it ran off the screen, on a
     desktop with 700 unused pixels either side of it.
     Two changes fix all of them: more width, and a ceiling on height with the
     overflow scrolling inside the card rather than growing it. */
  /* A4: the notification card is a press. Only the cursor and a hover lift say
     so — a border or a colour would make twenty-four of them into a wall of
     buttons, which is the thing the collapsed tray already exists to avoid. */
  .sig.is-press{ cursor:pointer; }
  .sig.is-press:hover{ background:color-mix(in srgb, var(--accent) 9%, transparent); }
  /* The two controls inside it stay their own targets. */
  .sig .sig-act, .sig .sig-x{ position:relative; z-index:1; }

  /* C15: THE MEDIA GRID. A card per feed.
     Artwork-first, because artwork is what a person actually recognises — a
     podcast has a cover and a channel has a face, and a row of small grey words
     gave you neither. Sized so a shelf of eight fits without scrolling on a
     laptop and reflows to three on a phone. */
  .mf-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(132px, 1fr)); gap:var(--space-3); }
  .mf-card{ position:relative; display:flex; flex-direction:column; align-items:center;
    gap:var(--space-2); padding:var(--space-3) var(--space-3) var(--space-4); border-radius:var(--radius-card); cursor:pointer; text-align:center;
    border:1px solid var(--glass-border); background:transparent; color:var(--text-dim);
    font-family:inherit; transition:border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); }
  .mf-card:hover{ color:var(--text); transform:translateY(-2px);
    border-color:color-mix(in srgb, var(--accent) 45%, transparent);
    background:color-mix(in srgb, var(--accent) 10%, transparent); }
  .mf-art{ width:72px; height:72px; border-radius:var(--radius-card); object-fit:cover; flex:none;
    background:rgba(120,120,140,.14); }
  /* The source's own initial rather than a generic icon: eight identical grey
     squares are as hard to tell apart as eight grey chips were. */
  .mf-art-none{ display:flex; align-items:center; justify-content:center;
    font-family:var(--font-display); font-size:var(--type-page); font-weight:600; color:var(--text); }
  .mf-name{ font-size:var(--type-body); font-weight:600; line-height:1.25; color:var(--text);
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
  .mf-meta{ font-size:var(--type-label); color:var(--text-faint); }
  .mf-card.has-error{ border-color:color-mix(in srgb, var(--orange) 50%, transparent); }
  .mf-warn{ position:absolute; top:6px; right:8px; color:var(--orange); font-weight:700; }

  /* ---- THE IN-LUCID WINDOW ----
     "when I click it, instead of going to facebook, can it open in our window
      to view?"                                          (Matt, 27 August.)

     A browser window inside the app, so it is sized like one: as much of the
     screen as it can take without becoming the screen. Taller than it is wide,
     unlike the video window above, because a web page scrolls vertically and a
     letterbox is the wrong shape for reading one. */
  .webwin{ max-width:min(1180px, 96vw); width:100%; height:min(86vh, 900px);
    padding:0; overflow:hidden; display:flex; flex-direction:column; border-radius:var(--radius-card); }
  .webwin-bar{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-3) var(--space-4); flex:none;
    border-bottom:1px solid var(--glass-border); }
  .webwin-mark{ width:22px; height:22px; border-radius:var(--radius-control); flex:none;
    display:flex; align-items:center; justify-content:center;
    font-size:var(--type-label); font-weight:700; color:var(--text);
    background:color-mix(in srgb, var(--accent) 22%, transparent); }
  .webwin-name{ font-size:var(--type-body); font-weight:600; color:var(--text); flex:none;
    max-width:38%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  /* --text-dim rather than --text-faint: this is the only thing on the bar that
     says WHICH site you are looking at, and that matters most on the one sheet
     whose content belongs to somebody else. Same 4.5:1 floor as everywhere. */
  .webwin-host{ font-size:var(--type-body); color:var(--text-dim); font-family:var(--font-mono);
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  .webwin-sp{ flex:1; min-width:8px; }
  .webwin-btn{ font-size:var(--type-body); font-weight:600; color:var(--text-dim); flex:none;
    background:none; border:1px solid var(--glass-border); border-radius:var(--radius-control);
    padding:var(--space-2) var(--space-3); cursor:pointer; font-family:inherit;
    transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
  .webwin-btn:hover{ color:var(--text); border-color:color-mix(in srgb, var(--accent) 45%, transparent); }
  .webwin-x{ font-size:var(--type-lead); line-height:1; padding:var(--space-hair) var(--space-2) var(--space-1); }
  /* WHITE, NOT TRANSPARENT. An iframe with no background of its own lets the
     sheet's glass show through any page that does not paint one — which reads
     as a rendering fault rather than as a design. */
  .webwin-body{ position:relative; flex:1; min-height:0; background:#fff; }
  .webwin-body iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; display:block; }
  /* The apology, for a site that refuses and leaves the frame empty. Over the
     frame rather than under it, because by then there is nothing underneath it
     to read. */
  .webwin-note{ position:absolute; inset:0; display:none; align-items:center; justify-content:center;
    text-align:center; padding:0 14%; font-size:var(--type-body); line-height:1.7;
    color:var(--text-dim); background:var(--glass-bg); }
  .webwin-note.show{ display:flex; }

  /* ---- ITEM 1751: THE WINDOW OPENS EVEN WHEN IT CANNOT SHOW THE PAGE ----
     "'What I follow' opens a separate tab instead of the Lucid window."
     A site that refuses framing used to skip the window entirely and put him in
     Chrome with nothing said. Now the window opens and this is what is in it.
     .blank takes the white off the body underneath: .webwin-body paints #fff so
     a page with no background of its own does not show the glass through it,
     and that is exactly wrong behind an apology on a dark theme — a sheet of
     white where a page should be reads as the fault he is being told is not a
     fault. --bg-mid is the token, so all sixteen themes get their own ground. */
  .webwin-body.blank{ background:var(--bg-mid); }
  /* SIZED TO WHAT IS IN IT. The window is a browser and is shaped like one —
     86vh, up to 900px — and four lines of text floating in the middle of that
     reads as a page that failed to arrive, which is the exact impression this
     panel exists to correct. Height goes back to the content, the note stops
     being absolutely positioned over a frame there is no point having, and the
     sheet is as big as what it has to say. */
  .webwin.refusing{ height:auto; max-width:min(560px, 92vw); }
  .webwin.refusing .webwin-body{ flex:none; }
  .webwin.refusing .webwin-body iframe{ display:none; }
  .webwin.refusing .webwin-note{ position:static; padding:var(--space-10) 10% var(--space-10); }
  .webwin-refused{ display:flex; flex-direction:column; align-items:center; gap:var(--space-3);
    max-width:34rem; }
  /* Serif, and the size of a sentence rather than of a heading. This is the one
     thing in the window that has to be read before anything is pressed, and a
     shouted heading over an explanation reads as an error dialog — which is
     what this deliberately is not. */
  .webwin-refused-h{ font-family:var(--font-display); font-size:var(--type-title); line-height:1.35;
    color:var(--text); }
  .webwin-refused-p{ margin:0; font-size:var(--type-body); line-height:1.7; color:var(--text-dim); }
  /* The second press is a way back, not a second door, so it is drawn as one
     word rather than as a button beside a button. Two equal buttons would make
     this a question; there is only one thing to decide. */
  .webwin-refused-x{ border:none; background:none; font-family:inherit; font-size:var(--type-body);
    font-weight:600; letter-spacing:.02em; color:var(--text-faint); cursor:pointer;
    padding:var(--space-hair) var(--space-2); transition:color var(--dur-fast) var(--ease-out); }
  .webwin-refused-x:hover{ color:var(--text-dim); }

  /* A6: THE LOGO TOOK ITS COLOUR FROM THE ORB, AND STOPPED.
     The mark is three brand arcs around a lit core. The arcs are the brand and
     stay fixed; the CORE is the orb, and when the orb became configurable the
     core kept the blue it was born with — so changing the accent left a blue
     dot sitting in the middle of a green app.
     Done by setting the GRADIENT STOPS rather than the fill. `fill` on an SVG
     shape does not accept a CSS gradient in any shipping browser — it needs a
     paint server — but `stop-color` is an ordinary CSS property on a <stop>,
     so the gradient stays exactly where it is and only its colours move. */
  .brand-mark .lm-core-in{  stop-color:color-mix(in srgb, var(--accent) 25%, #fff); }
  .brand-mark .lm-core-mid{ stop-color:var(--accent); }
  .brand-mark .lm-core-out{ stop-color:color-mix(in srgb, var(--accent) 70%, #000); }
  .brand-mark .lm-pool-in{  stop-color:var(--accent); stop-opacity:0.42; }
  .brand-mark .lm-pool-mid{ stop-color:var(--accent); stop-opacity:0.13; }
  .brand-mark .lm-pool-out{ stop-color:var(--accent); stop-opacity:0; }

  /* 257 — "logo on spine should follow theme colors." The core already did (see
     above); the three ARCS did not. They were three fixed hexes in the markup
     — #2954D9 blue, #3F9B6B green, #D97D2E orange — and the only thing moving
     them per theme was the hue-rotate below, which is driven by the ORB, not by
     the theme. With the default orb (rimCross on halo's slider) that filter
     resolved to the SAME rotation in eight of the eleven themes, and rotated
     the family off to a magenta/cyan that no theme is wearing. Measured, not
     guessed: --mark-rotate came back -49.6deg for dark, midnight, obsidian and
     frosted alike.
     So the arcs now read the theme's own five area colours — the same custom
     properties the five areas are painted from (item 210) — rather than a hue
     shift applied to a fixed set. The mapping is the one the original mark was
     already drawn in: blue arc = Now, green arc = Life, orange/brass arc =
     Work. Every theme states all five, so there is no theme this can miss.
     The faint inner ring is the same colour lifted off the ground rather than a
     second hue: toward white on a dark ground, toward ink on paper, because a
     70%-white mix at opacity .5 is invisible on cream. */
  .brand-mark{ --lm-lift:#FFFFFF; }
  :root[data-mode="light"] .brand-mark{ --lm-lift:#16130D; }
  .brand-mark .lm-arc-now{  stroke:var(--area-now); }
  .brand-mark .lm-arc-life{ stroke:var(--area-life); }
  .brand-mark .lm-arc-work{ stroke:var(--area-work); }
  .brand-mark .lm-ring-now{  stroke:color-mix(in srgb, var(--area-now) 64%,  var(--lm-lift)); }
  .brand-mark .lm-ring-life{ stroke:color-mix(in srgb, var(--area-life) 64%, var(--lm-lift)); }
  .brand-mark .lm-ring-work{ stroke:color-mix(in srgb, var(--area-work) 64%, var(--lm-lift)); }

  .compose-modal{ width:100%; max-width:860px; padding:var(--space-6); border-radius:var(--radius-card) var(--radius-card) 0 0;
    max-height:min(88vh, 860px); overflow-y:auto; overscroll-behavior:contain;
    transform:translateY(18px) scale(.97); transition:transform var(--dur-base) var(--ease-out); }
  .modal-backdrop.open .compose-modal{ transform:none; }
  @media(min-width:781px){ .modal-backdrop{ align-items:center; } .compose-modal{ border-radius:var(--radius-card); margin-bottom:6vh; } }

  /* ============================================================
     THE TRAY.

     Matt, 26 Aug: "lets discuss doing away with popups, and replace with the
     slick task tray we already have but could be for meetings and tasks, and
     reminders". Then: "build it now".

     ONE RULE, NOT NINE REWRITES. Every popup in Lucid is already the same two
     elements — a .modal-backdrop holding a .compose-modal — so a tray is not a
     new component, it is a different way of presenting the ones that exist. Add
     `as-tray` to the backdrop and the card stops being a centred box and
     becomes a rail down the right-hand edge. Nothing moves in the markup, every
     id keeps working, every open and close function keeps working, and the nine
     surfaces that migrate did not have to be rebuilt to do it.

     WHY THE PAGE BEHIND IS NOT BLURRED. A modal blurs its backdrop because it
     wants your whole attention. A tray wants the opposite: the list you came
     from stays legible, so you can press the next row and have the tray
     re-point rather than closing and re-aiming. The scrim is the drawer's own
     0.28 wash, and there is no backdrop-filter.

     THE WIDTHS ARE FIXED BY TYPE AND NEVER BY CONTENT. Two of them. A tray that
     resized itself to fit whatever was in it would make the page behind reflow
     every time you opened a different kind of thing, which is the same
     complaint — layout moving under you while you read — in a new coat.

       standard  min(560px, 100vw)   a record: task, vault item, reminder
       wide      min(820px, 100vw)   a record with a document in it: an event
                                     with a description and a map, contact edit

     560 rather than the 420 I first proposed, because 560 is what the task
     drawer already is and that is the one Matt named as the thing he likes.
     ============================================================ */

  /* ABOVE THE TASK DRAWER, which sits at 61 because it was the only slide-over
     in the app when it was written. A tray opened FROM the drawer — pressing
     "add a subtask" on a task, say — would otherwise slide in underneath it and
     be invisible. Still below #dlgBackdrop at 120, which has to stay on top of
     everything because a confirm can be raised from inside any of this. */
  .modal-backdrop.as-tray{ align-items:stretch; justify-content:flex-end; z-index:62;
    background:rgba(4,6,16,0.28); }
  :root[data-mode="light"] .modal-backdrop.as-tray{ background:rgba(62,50,38,0.18); }
  /* No blur, at any size, deliberately — see above. This overrides the shared
     .modal-backdrop.open rule, which is why it repeats the selector weight. */
  .modal-backdrop.as-tray.open{ backdrop-filter:none; -webkit-backdrop-filter:none;
    /* AND THE PAGE BEHIND STAYS LIVE. This is the difference between a tray and
       a modal wearing a tray's clothes. A backdrop that swallows clicks means
       pressing the next row in the list closes the tray instead of opening that
       row — and "open the wrong one, press the right one, it re-points" is the
       single biggest reason to do this at all. So the scrim is decoration and
       nothing else; the card takes its clicks back below. */
    pointer-events:none; }
  .modal-backdrop.as-tray.open > .compose-modal{ pointer-events:auto; }

  .modal-backdrop.as-tray > .compose-modal{
    width:min(560px, 100vw); max-width:none;
    height:100%; max-height:none;
    margin:0; border-radius:0;
    border-top:none; border-right:none; border-bottom:none;
    /* EXACTLY THE TASK DRAWER'S GROUND — and "exactly" took measuring.
       Matt, 26 Aug: "dont like the new look you just made. I want them all to
       look like the task trays. (frosted in a bit)".
       Put side by side, the drawer measured 75% opaque and the tray 100%: the
       drawer is FROSTED and the tray was a flat block. The reason is
       specificity — the drawer is `.pj-drawer` at one class, so the
       `[data-fx="lite"] .glass` variant beats it and supplies a translucent
       ground; my `.modal-backdrop.as-tray > .compose-modal` is three classes
       and beat that variant, so the tray never got it.
       Both variants are therefore restated here, at the tray's own weight, so
       the two panels are the same panel in every effects mode. Repeating them
       is not duplication for its own sake: the selector cannot be lowered to
       the drawer's weight without also styling every centred modal. */
    /* Sheen over bg-mid, and NOTHING BETWEEN THEM — which is what the drawer
       resolves to, because .glass's shorthand supplies glass-bg as the
       background-COLOUR and .pj-drawer then overrides that with bg-mid,
       leaving the sheen as the only image layer. A glass-bg gradient in the
       middle looked identical in the stylesheet and measured as a third layer
       the drawer does not have. */
    background:var(--glass-sheen), var(--bg-mid);
    /* The drawer's numbers, not the modal's: 22px sides, and the body ends 40px
       above the floor so the last field is never flush against it. */
    padding:0 var(--space-6) var(--space-10);
    overflow-y:auto; overscroll-behavior:contain; scrollbar-width:thin;
    transform:translateX(102%); transition:transform var(--dur-slow) var(--ease-out); }
  .modal-backdrop.as-tray.open > .compose-modal{ transform:none; }
  .modal-backdrop.as-tray.tray-wide > .compose-modal{ width:min(820px, 100vw); }
  /* Cheap-effects mode, mirroring `:root[data-fx="lite"] .glass` exactly — this
     is the one the drawer actually resolves to on most machines, and the one
     that makes it frosted rather than solid. */
  :root[data-fx="lite"] .modal-backdrop.as-tray > .compose-modal{
    background:var(--glass-sheen),
               color-mix(in srgb, var(--glass-bg) 100%, var(--bg-base) 55%); }

  /* STICKY, NOT RESTRUCTURED.
     The obvious way to make one child of the card scroll is to turn the card
     into a flex column and give the scrolling child flex:1 — which means every
     migrated card needs a wrapper element it does not currently have, in nine
     different pieces of markup. Sticky does the same job with no markup at all:
     the card scrolls as it always has, and the two things that must not go
     anywhere pin themselves. */
  /* The same header band as .pj-drawer-head — 20px 22px 12px, a hairline under
     it, and the same ground — so a task and a meeting open as the same object. */
  .modal-backdrop.as-tray > .compose-modal > .modal-title{
    position:sticky; top:0; z-index:3;
    margin:0 -22px var(--space-3); padding:var(--space-5) var(--space-6) var(--space-3);
    /* The panel's own ground, not a solid one: an opaque strip across the top
       of a frosted panel is exactly the seam this was meant to avoid. */
    background:color-mix(in srgb, var(--bg-mid) 92%, transparent);
    backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
    border-bottom:1px solid var(--glass-border); }
  /* The floor of the tray. A fourteen-field form should not require scrolling
     to the bottom to find Save. */
  .modal-backdrop.as-tray > .compose-modal > .btn-row:last-child{
    position:sticky; bottom:0; z-index:3;
    margin:var(--space-5) -22px calc(var(--space-10) * -1); padding:var(--space-4) var(--space-6) max(14px, env(safe-area-inset-bottom));
    border-top:1px solid var(--glass-border);
    background:color-mix(in srgb, var(--bg-mid) 92%, transparent);
    backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); }
  /* .event-modal already manages its own inner scroll region and declares the
     flex column itself, so it keeps them — the card must not scroll twice. */
  /* The drawer's padding split, not one flat number. `.pj-drawer` puts NO
     padding on the card: the header band owns `20px 22px 12px` and the
     scrolling body owns `16px 22px 40px`, and that 40px of floor is why you
     can scroll to the end of a task without the last line touching the
     bottom of the screen. The tray had `20px 22px` on the card itself, so its
     header had no band, its content had no floor, and the whole thing sat
     tighter than the drawer beside it. */
  .modal-backdrop.as-tray > .compose-modal.event-modal{
    overflow:hidden; padding:0 var(--space-6) var(--space-10); }

  /* TWO COLUMNS CANNOT FIT IN 560 PIXELS, and .cm-grid's second column is on a
     VIEWPORT media query — so on a wide screen the grid would happily lay out
     two 500px columns inside a 560px rail and hang off the end. Forced back to
     one, which is also the right shape for a form you fill top to bottom. */
  .modal-backdrop.as-tray .cm-grid{ grid-template-columns:1fr; }
  .modal-backdrop.as-tray.tray-wide .cm-grid{ grid-template-columns:1fr; }
  @media(min-width:1180px){
    .modal-backdrop.as-tray .cm-grid:has(> .cm-col:nth-child(3)){ grid-template-columns:1fr; }
  }
  /* The width classes are about a centred card and mean nothing here. */
  .modal-backdrop.as-tray > .compose-modal.cm-wide,
  .modal-backdrop.as-tray > .compose-modal.cm-mid,
  .modal-backdrop.as-tray > .compose-modal.cm-narrow{ max-width:none; }
  /* THE GLOW, AND IT IS LUCID'S NOW — 23 September.

     The leading edge used to be the event's kind: green for a meeting, violet
     for a task, blue for a reminder, set from JS through `--ed-edge`. Matt:
     "The tray that opens for events, and tasks and stuff on the agenda has a
     rim decided by theme. How about every edge that gets opened is Lucid".
     So it is the thread's colour on every tray, and the kind is still said by
     the dot in the kicker line above the title. */
  .modal-backdrop.as-tray > .compose-modal.event-modal{
    border-top-width:0; border-left-width:4px; border-left-style:solid;
    border-left-color:color-mix(in srgb, var(--th-base) 72%, transparent); }
  :root[data-mode="light"] .modal-backdrop.as-tray > .compose-modal.event-modal{
    border-left-color:color-mix(in srgb, var(--th-ink) 62%, transparent); }
  /* And it glows, in the dark themes only — this is the thing he asked to
     keep, so it is now doing something it never did. */
  :root:not([data-mode="light"]) .modal-backdrop.as-tray > .compose-modal.event-modal{
    box-shadow:var(--glass-shadow),
      -8px 0 30px -12px color-mix(in srgb, var(--th-base) 44%, transparent); }

  /* STACKED. Pressing "Create a task" from inside a meeting pushes a second
     tray over the first rather than opening a second popup on top of a popup.
     The one underneath slides back and dims — it is still there, and the back
     arrow returns to it. */
  .modal-backdrop.as-tray.tray-under{ pointer-events:none; }
  .modal-backdrop.as-tray.tray-under > .compose-modal{
    transform:translateX(-26px) scale(.985); filter:brightness(.72); }
  /* Above the sticky title, which is at 3. It was at 2, which put the arrow
     underneath the very bar it sits on — present in the DOM, invisible on
     screen, and unclickable. Caught in a screenshot, not by the test that
     asserted the button exists. */
  .tray-back{ position:absolute; top:18px; left:16px; z-index:4;
    width:30px; height:30px; padding:0; display:flex; align-items:center; justify-content:center;
    background:transparent; border:none; border-radius:var(--radius-control); cursor:pointer;
    font-size:var(--type-title); line-height:1; color:var(--text-dim); }
  .tray-back:hover{ color:var(--text); background:rgba(120,120,140,.14); }
  .tray-back:focus-visible{ outline:2px solid var(--ring); outline-offset:1px; }
  /* The card is the positioning context for it, and the title shifts out of the
     way so the arrow never sits on top of a word. */
  .modal-backdrop.as-tray > .compose-modal{ position:relative; }
  .modal-backdrop.as-tray.has-back > .compose-modal > .modal-title,
  .modal-backdrop.as-tray.has-back > .compose-modal > .ed-head{ padding-left:var(--space-8); }
  /* .ed-head sets a padding SHORTHAND now (it is a header band with a hairline
     under it, matching the drawer), and a shorthand later in the file would
     otherwise reset the left inset the back arrow needs. Restated so the arrow
     still has its 34px whatever order the two rules land in. */
  .modal-backdrop.as-tray.has-back > .compose-modal > .ed-head{ padding-left:var(--space-8); }

  /* ON A PHONE it is the whole screen, which is the only honest answer at
     380px — and it is a slide from the right rather than a sheet from the
     bottom, so it matches the task drawer instead of introducing a third
     gesture. */
  @media(max-width:600px){
    .modal-backdrop.as-tray > .compose-modal,
    .modal-backdrop.as-tray.tray-wide > .compose-modal{ width:100vw; }
    .modal-backdrop.as-tray.tray-under > .compose-modal{ transform:translateX(-18px) scale(.99); }
  }

  @media (prefers-reduced-motion: reduce){
    .modal-backdrop.as-tray > .compose-modal{ transition:none; }
    /* Closed still has to be off-screen, or a tray that is not open would sit
       on top of the app permanently. */
  }

  /* ============================================================
     A POPUP IS FOR READING. AN AMBIENT PANEL IS FOR LOOKING AT.
     ------------------------------------------------------------
     "All of the popups are a cool frosted look, but when white/grey it is
     impossible to read."

     The two are not the same job and they should not have the same fill. A
     panel in the page sits on the app's own background: at 55% opacity it is
     55% white over more cream, which is still cream. A dialog sits above the
     WHOLE PAGE with a dimming scrim between — so the same 55% white was 55%
     white over a darkened, blurred inbox. That mixture is the grey, and no
     text colour survives being asked to sit on a background that changes
     depending on what happens to be scrolled behind it.

     Two changes, both light-mode only, because the dark themes are the ones
     that work and are the ones he uses:
       1. the scrim stops being near-black. Dimming a cream page toward black
          is what dragged the card grey in the first place;
       2. the card itself becomes almost opaque. The frost is still there in
          the blur, the sheen and the border — it is just no longer allowed to
          decide what colour the paper is.
     ============================================================ */
  :root[data-mode="light"] .modal-backdrop{ background:rgba(62,50,38,0.30); }
  :root[data-mode="light"] .modal-backdrop .glass{
    background:var(--glass-sheen), color-mix(in srgb, var(--bg-mid) 96%, transparent);
  }
  /* Same reasoning, same problem: a menu floating over the page. These already
     use an opaque --bg-mid, but the ones built on .glass did not. */
  :root[data-mode="light"] .from-menu,
  :root[data-mode="light"] .acct-switcher-menu,
  :root[data-mode="light"] .more-menu,
  :root[data-mode="light"] .global-fab-menu{
    background:color-mix(in srgb, var(--bg-mid) 97%, transparent);
  }

  /* Settings was 939px tall against a 700px window — cut off at BOTH ends, with
     max-height:none and overflow hidden, so there was nothing to scroll and no
     way to reach the bottom half. Measured, not guessed.
     Two fixes, and they are different problems: the card now has a height
     ceiling with its own scroll region (so it can never exceed the window), and
     the General pane runs in two columns (so on any normal screen it doesn't
     need to scroll at all). Declared after .glass, which sets overflow and
     would otherwise win the cascade. */
  .compose-modal.settings-modal{
    max-width:900px; margin-bottom:0;
    max-height:min(86vh, 780px);
    display:flex; flex-direction:column;
    padding-bottom:var(--space-5); overflow:hidden;
  }
  .settings-body{
    flex:1; min-height:0; overflow-y:auto; overflow-x:hidden;
    /* Pull the scrollbar to the card edge without the content hugging it. */
    padding-right:var(--space-3); margin-right:-10px;
    scrollbar-width:thin;
  }
  .settings-body::-webkit-scrollbar{ width:8px; }
  .settings-body::-webkit-scrollbar-thumb{ background:rgba(255,255,255,0.10); border-radius:var(--radius-tight); }
  .settings-body::-webkit-scrollbar-thumb:hover{ background:rgba(255,255,255,0.18); }
  .settings-cols{ display:grid; grid-template-columns:1fr 1fr; gap:0 var(--space-8); align-items:start; }
  /* A column must not be able to push the grid wider than the card — long
     account emails and signatures will otherwise do exactly that. */
  .settings-cols > *{ min-width:0; }
  .settings-col-sep{ border-left:1px solid var(--glass-border); padding-left:var(--space-8); margin-left:0; }
  @media(max-width:900px){
    .settings-cols{ grid-template-columns:1fr; }
    .settings-col-sep{ border-left:none; padding-left:0; }
  }
  /* SETTINGS -> ACCOUNT: the one line that answers back. Item 523.
     Three kinds, and they have to be told apart at a glance, because the
     difference between "that is not your current password" and "your password
     is changed" is the difference between having been locked out and not. The
     colours are the theme's own --pink and --green rather than a red and a
     green of this rule's invention: every one of the fourteen themes tunes both
     against its own ground, and a hardcoded hex here would be legible on eight
     of them and a bruise on the other six. Sized and spaced to sit under the
     three boxes it is about rather than to shout across the pane. */
  .acct-msg{ margin:var(--space-3) 0 0; font-size:var(--type-body); line-height:1.5; max-width:420px; overflow-wrap:anywhere; }
  .acct-msg:empty{ display:none; }
  .acct-msg.info{ color:var(--text-dim); }
  .acct-msg.ok{ color:var(--green); }
  .acct-msg.err{ color:var(--pink); }
  /* AND DEEPENED ON THE LIGHT GROUNDS, WHICH THE RULE ABOVE ALONE DOES NOT
     SURVIVE. Measured on the settings card rather than assumed: the theme's own
     --green and --pink are tuned to sit on a dark page and come out between
     2.4:1 and 4.2:1 on the eight light themes — 2.44:1 on Terracotta — which is
     a refusal somebody has to lean in to read. Mixed toward black rather than
     replaced with a fixed maroon, so each theme keeps its own hue and the two
     coloured light themes (Denim's blue, Terracotta's clay) do not suddenly
     have a red from somebody else's palette in them. Same technique, same
     reason, as .sig-tab.active further down this file. */
  :root[data-mode="light"] .acct-msg.ok{ color:color-mix(in srgb, var(--green) 55%, #000); }
  :root[data-mode="light"] .acct-msg.err{ color:color-mix(in srgb, var(--pink) 55%, #000); }

  /* =========================================================================
     C14 — SETTINGS AS A SHORT LIST OF GROUPS. Row 1753.

     "Clean up the settings. TOO MANY WORDS. Too many buttons laying around.
      Chevrons everywhere, 1 or 2 word descriptions, not 2 sentences."

     Two surfaces and nothing else: a rail of eleven words on the left, and a
     column of cards on the right. Every colour below is mixed from the theme's
     own --text, --accent and --glass-border rather than picked, because there
     are fifteen themes and a literal would be right on one of them. The tint
     under a hovered row is a percentage of --text, which means it darkens the
     light themes and lightens the dark ones without a second rule.

     The rail is sticky rather than fixed: .settings-body is the scroll box,
     and eleven rows are shorter than every group's contents, so the words stay
     put while the cards move. The dividing line is on the DETAIL side because
     the detail is the taller of the two columns and a border on the rail would
     stop halfway down the card.
     ========================================================================= */
  .stg{ position:relative; display:grid; grid-template-columns:206px minmax(0, 1fr); align-items:start; }
  /* THE LINE BETWEEN THE TWO COLUMNS IS DRAWN BY NEITHER OF THEM.
     Put on the rail it stopped at the eleventh word while the cards ran on
     below it; put on the cards it stopped short whenever a group held two.
     Measured both ways on screen before this pseudo-element replaced them:
     it is the only thing here as tall as the taller column. */
  .stg::before{ content:''; position:absolute; left:206px; top:0; bottom:0; width:1px;
                background:var(--glass-border); }
  .stg-rail{ position:sticky; top:0; display:flex; flex-direction:column; gap:1px; padding-right:var(--space-6); }
  .stg-row{
    display:flex; align-items:center; justify-content:space-between; gap:var(--space-3);
    width:100%; text-align:left; padding:var(--space-3) var(--space-3); border-radius:var(--radius-card);
    border:1px solid transparent; background:none; cursor:pointer;
    font-family:var(--font-body); font-size:var(--type-body); font-weight:650; color:var(--text-dim);
    transition:background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
  }
  .stg-row:hover{ color:var(--text); background:color-mix(in srgb, var(--text) 7%, transparent); }
  /* The chosen group is marked by a tint of the theme's own accent rather than
     by a second typeface or a heavier weight — the eleven words have to stay
     the same shape as each other when you run your eye down them. */
  .stg-row.active{
    color:var(--text); background:color-mix(in srgb, var(--accent) 15%, transparent);
    border-color:color-mix(in srgb, var(--accent) 30%, transparent);
  }
  .stg-row-label{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  /* The eight light grounds take the same tint far less well — 15% of an accent
     tuned for a dark page is a smudge on cream rather than a selection. The
     edge does the work there instead, and it is the theme's own accent either
     way, so no light theme suddenly has somebody else's blue in it. */
  :root[data-mode="light"] .stg-row.active{
    background:color-mix(in srgb, var(--accent) 11%, transparent);
    border-color:color-mix(in srgb, var(--accent) 48%, transparent);
  }
  .stg-row-chev{ width:15px; height:15px; flex:none; color:var(--text-faint); opacity:.75; }
  .stg-row.active .stg-row-chev{ color:var(--accent); opacity:1; }

  /* KEYBOARD. Every row and every card head is a real <button>, so they are
     already in the tab order; what they did not have is a ring that says which
     one you are on. :focus-visible rather than :focus, so a mouse press does
     not leave a halo behind it. */
  .stg-row:focus-visible, .stg-sec-head:focus-visible, .stg-back:focus-visible{
    outline:2px solid color-mix(in srgb, var(--accent) 70%, transparent); outline-offset:2px;
  }
  .stg-detail{ padding-left:var(--space-8); min-width:0; }
  .stg-detail-head{ display:flex; align-items:center; gap:var(--space-3); margin:0 0 var(--space-4); }
  .stg-detail-title{ margin:0; font-size:var(--type-title); font-weight:800; letter-spacing:-0.01em; color:var(--text); }
  /* The way back to the list exists only where the list is not already on
     screen. On a wide card both columns are visible at once and a back button
     would be a control that undoes nothing. */
  .stg-back{ display:none; width:30px; height:30px; flex:none; padding:0; border-radius:var(--radius-control);
             border:1px solid var(--glass-border); background:none; color:var(--text-dim); cursor:pointer;
             align-items:center; justify-content:center; }
  .stg-back:hover{ color:var(--text); border-color:var(--glass-border-hi); }
  .stg-back svg{ width:17px; height:17px; }

  /* A CARD. One or two words, a chevron, and everything else folded behind it.
     The border is the theme's glass line and the fill is a two-percent lift off
     the page, which is enough to read as a surface on all fifteen grounds and
     not enough to look like a button. */
  .stg-sec{
    border:1px solid var(--glass-border); border-radius:var(--radius-card); margin-bottom:var(--space-2);
    background:color-mix(in srgb, var(--text) 2.5%, transparent);
    transition:border-color var(--dur-fast) var(--ease-out);
  }
  .stg-sec:hover{ border-color:var(--glass-border-hi); }
  .stg-sec.open{ background:color-mix(in srgb, var(--text) 4%, transparent); }
  .stg-sec-head{
    display:flex; align-items:center; justify-content:space-between; gap:var(--space-3);
    width:100%; text-align:left; padding:var(--space-3) var(--space-4); background:none; border:none;
    cursor:pointer; font-family:var(--font-body); border-radius:var(--radius-card);
  }
  /* The heading element inside is the SAME .section-label the markup has always
     had — kept so that #stReminders and its neighbours still exist to be
     scrolled to. It is a title here rather than a spacer above a column, so the
     eleven-pixel faint uppercase it wears everywhere else is undone. */
  .stg-sec-head .stg-sec-title{
    font-size:var(--type-lead); font-weight:700; text-transform:none; letter-spacing:-0.005em;
    color:var(--text); margin:0;
  }
  .stg-sec-chev{ width:16px; height:16px; flex:none; color:var(--text-faint);
                 transition:transform var(--dur-fast) var(--ease-out); }
  .stg-sec.open .stg-sec-chev{ transform:rotate(180deg); color:var(--text-dim); }
  .stg-sec-body{ display:none; padding:var(--space-hair) var(--space-4) var(--space-4); }
  .stg-sec.open > .stg-sec-body{ display:block; }
  /* Whatever was first in the old column came with a margin meant to separate
     it from the heading above it. Inside a card the heading is the lid. */
  .stg-sec-body > :first-child{ margin-top:0; }
  .stg-sec-body .section-label:first-child{ margin-top:0; }
  /* The explanations moved inside with their controls, which is the only place
     they were ever any use. Dimmer and smaller than the title above them, so
     the card still reads as two words and a chevron at a glance. */
  .stg-sec-body .page-sub{ font-size:var(--type-body); line-height:1.55; }
  /* TWO THINGS THE OLD TWO-COLUMN PANE WAS HIDING, both found by looking at
     the rebuilt cards rather than by reading the markup.

     A .btn-row is a plain flex row with no wrap, and three of these cards hold
     more buttons than any row can carry — the diagnostics bench is nine, the
     spreadsheet exports are seven. They did not wrap, they squashed: "What
     would we index" became a column of one word per line and the last two ran
     off the edge of the card entirely. "Too many buttons laying around" is
     exactly what he called this, and wrapping is the least this can do about
     it while every one of them still works.

     And an <a class="btn"> is an INLINE box, because .btn never sets a display
     and a button element is inline-block by itself. Every other .btn in this
     panel happens to sit inside a .btn-row, which makes it a flex item and
     hides the problem; the one that does not — "Your data: export or delete" —
     had its padding overflowing its own line and the sentence after it ran
     straight through the middle of the button. */
  .stg-sec-body .btn-row{ flex-wrap:wrap; row-gap:var(--space-2); }
  .stg-sec-body a.btn{ display:inline-block; }

  /* NARROW: the rail IS the screen, and choosing a group replaces it. Same two
     levels, one at a time, with the back chevron that earns its place here. */
  @media(max-width:700px){
    .stg{ display:block; }
    .stg::before{ display:none; }
    .stg-rail{ position:static; padding-right:0; gap:var(--space-hair); }
    .stg-detail{ display:none; padding-left:0; }
    .stg.is-open .stg-rail{ display:none; }
    .stg.is-open .stg-detail{ display:block; }
    .stg-back{ display:inline-flex; }
    .stg-row{ padding:var(--space-3) var(--space-3); font-size:var(--type-lead); }
  }
  /* From-selector: single button + dropdown instead of every account shown
     as its own chip — saves space now that there are 3+ accounts. */
  .from-selector{ position:relative; margin-bottom:var(--space-3); }
  .from-btn{ width:100%; text-align:left; padding:var(--space-3) var(--space-4); border-radius:var(--radius-card); border:1px solid var(--glass-border); background:rgba(120,120,140,.08); color:var(--text); font-family:var(--font-body); font-size:var(--type-body); font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:space-between; transition:box-shadow var(--dur-fast); }
  :root:not([data-mode="light"]) .from-btn{ box-shadow:0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent), 0 0 18px 0 var(--accent-glow); }
  :root:not([data-mode="light"]) .rp-close-btn{ box-shadow:0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent), 0 0 18px 0 var(--accent-glow); transition:box-shadow var(--dur-fast); }
  :root:not([data-mode="light"]) .rp-close-btn:hover{ box-shadow:0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent), 0 0 26px 2px var(--accent-glow); }
  :root:not([data-mode="light"]) .from-btn:hover{ box-shadow:0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent), 0 0 26px 2px var(--accent-glow); }
  .send-split{ display:flex; position:relative; }
  .send-split .btn{ border-radius:0; }
  .send-split .send-main{ border-radius:var(--radius-card) 0 0 var(--radius-card); }
  .send-split .send-chevron{ border-radius:0 var(--radius-card) var(--radius-card) 0; padding:var(--space-3) var(--space-3); }
  .send-split .send-main.tinted{ border-right:1px solid color-mix(in srgb, var(--accent) 30%, transparent); }
  .from-btn::after{ content:'⌄'; color:var(--text-faint); font-size:var(--type-body); margin-left:var(--space-2); }

  /* =====================================================================
     THE COMPOSER'S CONTROLS ARE THE HEIGHT OF THE STRIP ABOVE THEM
     — 16 SEPTEMBER. "The buttons on an email for close, send, reply, add
     contact are too tall. Slim them down to be elegant, not bloated."
     =====================================================================

     He is describing one screen with two button systems on it, and they
     disagreed by thirteen pixels.

       Reply / Reply all / Forward are .mstrip-btn — an explicit height:34px,
       set when that strip was designed as a strip.

       Close, the From selector and Send are the app's generic .btn, which is
       padding-driven: --space-3 top and bottom, plus a line of --type-body,
       plus two borders. 47px, and no two of them the same unless the text
       happens to wrap the same way.

     So the reading pane opened with a 34px row of controls and, a few lines
     below it, a 47px row doing the same kind of job. That is what reads as
     bloated — not the absolute size, the disagreement. This makes the second
     row match the first: one height, one radius, one row of controls on the
     screen.

     SCOPED TO .rp-compose-bar AND THE READING PANE, not to .btn. The same
     button is the right size in a settings panel and in a dialog, where it is
     the only control in its row and has nothing to be out of step with. A
     global change here would have quietly reshaped forty screens to fix one.

     The height is a literal 34px on purpose and matches .mstrip-btn exactly —
     these two numbers have to be the same number, and a token that rounds
     differently between them would be worse than a repeated literal. The lint
     in tests/lint-the-scale.js covers font sizes, spacings and durations; a
     control height is none of those. */
  /* THE WHOLE MESSAGE SURFACE, not only the top bar. The first version of this
     rule was scoped to .rp-compose-bar and left "Attach files" sitting under
     the letter at 47px — one bloated button on an otherwise slim screen, which
     is exactly the disagreement this is for. :not(.sm) so the small variants
     below keep their own smaller size rather than being inflated to meet this. */
  #readingPane .btn:not(.sm),
  .rp-compose-bar .btn:not(.sm),
  .rp-compose-bar .from-btn{
    height:34px; min-height:34px; padding:0 var(--space-4);
    display:inline-flex; align-items:center; }

  .from-btn{ min-width:0; }
  .from-btn-label{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  .from-menu{ position:absolute; top:calc(100% + 4px); left:0; right:auto; min-width:230px; background:var(--bg-mid); border:1px solid var(--glass-border-hi); border-radius:var(--radius-card); box-shadow:var(--elev-floating); z-index:20; display:none; overflow:hidden; }
  .from-menu.open{ display:block; }
  /* Set by placeMenuToFit() in app.js when the menu would run off the bottom of the window. Two classes, so it outranks the .from-menu rule above whichever copy of it wins. */
  .from-menu.opens-up{ top:auto; bottom:calc(100% + 4px); }
  /* A MENU LONGER THAN THE CARD IT DROPS OUT OF.
     The category list went from six to ten on 27 August and the last four
     landed below the bottom of the panel: painted nowhere, clickable by
     nothing, with `overflow:hidden` above making them invisible rather than
     merely awkward. Caught by a harness that measures where the last item
     ACTUALLY IS rather than counting the items.
     A ceiling and a scroll, so the list can grow again without this returning.
     44vh because the trigger sits mid-panel; taller and the menu runs off the
     bottom of the window instead of off the bottom of the card. */
  .from-menu{ max-height:44vh; overflow-y:auto; overscroll-behavior:contain; }
  .from-menu .chip{ display:block; border-radius:0; border:none; border-bottom:1px solid var(--glass-border); padding:var(--space-3) var(--space-4); }
  .from-menu .chip:last-child{ border-bottom:none; }
  .from-menu .chip.active{ background:color-mix(in srgb, var(--accent) 14%, transparent); }

  /* =========================================================================
     LUCID FIELDS — the custom date, time and select controls (app.js,
     lucidEnhanceDateInput / lucidEnhanceTimeInput / lucidEnhanceSelect). Built
     entirely from tokens and shapes this file already has: the button is a
     .from-btn, the popover is a .from-menu, a choice in it is a .chip, and the
     calendar grid is the sidebar's own .mc-* classes. Nothing here invents a
     second design language — it reuses the one Repeat/Show as/Alert already
     prove works as a themed dropdown, and the one the mini-calendar already
     proves works as a themed month grid.
     ========================================================================= */
  .lucid-field .lp-field-btn{ gap:var(--space-2); }
  .lp-field-main{ display:flex; align-items:center; gap:var(--space-2); min-width:0; overflow:hidden; }
  .lp-field-ico{ width:14px; height:14px; flex:none; color:var(--text-faint); }
  .lp-field-label{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  /* Nothing picked yet reads as a prompt, the same convention .st-drop.is-unset
     already uses in Settings — full weight, faint colour, never italic. */
  .lucid-field.is-unset .lp-field-label{ color:var(--text-faint); }
  /* .ed-in-time is the narrowest slot any of these ever sit in — 128px, wide
     enough for "02:00 PM" and nothing else (see the note by .ed-in-time
     itself). The clock glyph is worth its width everywhere there is room to
     spare; here it is the first thing to give. */
  .ed-in-time.lp-field-btn, .ed-in-time .lp-field-btn{ padding:var(--space-2) var(--space-3); }
  .ed-in-time .lp-field-ico{ display:none; }

  /* THE CALENDAR POPOVER. A .from-menu by default cuts itself off at 44vh and
     scrolls — right for a list of thirty choices, wrong for a seven-column
     grid that is exactly as tall as it is ever going to be. */
  .from-menu.lp-cal-menu{ max-height:none; overflow:visible; padding:var(--space-3); min-width:264px; }
  .lp-cal-menu .mc-grid{ min-width:224px; }
  .lp-cal-actions{ display:flex; justify-content:space-between; margin-top:var(--space-2); padding-top:var(--space-2); border-top:1px solid var(--glass-border); }
  .lp-cal-action{ font-size:var(--type-body); font-weight:700; color:var(--text-dim); cursor:pointer; padding:var(--space-1) var(--space-2); border-radius:var(--radius-control); }
  .lp-cal-action:hover{ color:var(--text); background:rgba(120,120,140,.10); }

  /* THE TIME POPOVER. A plain .from-menu already scrolls and already lists
     .chip rows one per line — exactly what 96 quarter-hours need — so only the
     type-to-jump box on top is new. */
  .lp-time-menu{ min-width:170px; padding-top:var(--space-2); }
  .lp-time-type{ display:block; width:calc(100% - 20px); box-sizing:border-box; margin:0 var(--space-3) var(--space-2); background:rgba(120,120,140,.08); border:1px solid var(--glass-border); border-radius:var(--radius-control); color:var(--text); font-family:var(--font-body); font-size:var(--type-body); padding:var(--space-2) var(--space-3); }
  .lp-time-type:focus{ outline:none; border-color:color-mix(in srgb, var(--accent) 50%, transparent); }

  /* THE SELECT POPOVER needs nothing beyond .from-menu + .chip — see above. A
     disabled option (an optgroup-style heading with nothing behind it) reads
     as inert rather than as a choice that does nothing when pressed. */
  .lp-chip-disabled{ color:var(--text-faint); cursor:default; }

  .more-item{ display:flex; align-items:center; justify-content:space-between; gap:var(--space-4); padding:var(--space-3) var(--space-4); font-size:var(--type-body); font-weight:600; cursor:pointer; border-bottom:1px solid var(--glass-border); white-space:nowrap; }
  .more-item-label{ display:flex; align-items:center; gap:var(--space-2); }
  .more-item-icon{ width:15px; height:15px; flex:none; color:var(--text-dim); }
  .more-item:last-child{ border-bottom:none; }
  .more-item:hover{ background:rgba(120,120,140,.10); }
  /* Wide, not tall — the create modal has too many fields to read well as one
     long vertical scroll on a desktop screen with room to spare. Two columns
     side by side instead; collapses back to one column on narrow screens. */
  /* AND WIDER STILL ON A REAL MONITOR.
   *
   * Matt, 25 Aug: "New event card is too tall. Make it wider." Then the general
   * form: "Prevailing theme is all the cards are too tall. Make them all wider
   * to fit screen."
   *
   * An 820px card on a 2,560px screen leaves seventeen hundred pixels of dimmed
   * inbox either side while the card itself runs off the bottom. That width was
   * chosen when two columns was all there was to put in it.
   *
   * The card grows in steps and the grid grows with it, because extra width is
   * only worth having when it turns into fewer rows. The three-column rule is
   * written as a rule about the CHILDREN — a card that has a third .cm-col gets
   * a third column, and one that does not keeps two rather than growing an
   * empty stripe. */
  .cm-wide{ max-width:780px; }
  .cm-grid{ display:grid; grid-template-columns:1fr; gap:0 var(--space-8); }
  /* The second column arrives at 700px rather than 781px. Between those two
     numbers sits a very common window width where the card had room for two
     columns and used one, which is precisely how it got tall. */
  @media(min-width:700px){ .cm-wide{ max-width:1000px; } .cm-grid{ grid-template-columns:1fr 1fr; } }
  @media(min-width:1180px){
    .cm-wide{ max-width:min(1120px, calc(100vw - 96px)); }
    .cm-grid:has(> .cm-col:nth-child(3)){ grid-template-columns:repeat(3, 1fr); }
  }
  .cm-col{ min-width:0; }
  /* ------------------------------------------------------------------
     THE WHOLE WIDTH SCALE, IN ONE PLACE.

     Matt, 25 Aug: "Fix all the cards (not just these) to be wide like in other
     areas. (should be site wide for consistency)."

     Consistency is the operative word. Before this there were eleven widths in
     the app and seven of them were written inline in index.html — 560, 440,
     440, 560, 620, 360, 470 — and an inline style beats every rule in this
     stylesheet, which is why the two previous card-width passes changed
     nothing on those seven. The inline values are gone; every card now wears
     one of three classes and the numbers live here.

       .cm-narrow  a question with one answer  — confirm, one-field prompts
       .cm-mid     a short form                — three or four fields
       .cm-wide    anything with real content  — pairs with .cm-grid

     A card only gets .cm-wide if it has enough in it to fill two columns. A
     three-field dialog stretched to 1,120px is not "wide and short", it is a
     letterbox with three fields floating in it, so the scale has a middle. */
  .cm-narrow{ max-width:470px; }
  .cm-mid{ max-width:620px; }
  @media(min-width:700px){ .cm-mid{ max-width:720px; } }
  /* Native date/time inputs have a hard intrinsic width; min-width:0 is what
     actually lets them share a row without overflowing the column. */
  .ev-time-row{ display:flex; align-items:center; gap:var(--space-2); margin-bottom:var(--space-3); }
  .ev-time-row .field{ flex:1 1 0; min-width:0; }
  .ev-time-dash{ flex:none; font-size:var(--type-body); color:var(--text-faint); }
  .modal-title{ font-family:var(--font-display); font-size:var(--type-title); font-weight:600; margin-bottom:var(--space-4); display:flex; justify-content:space-between; }
  @keyframes itemFade{ from{opacity:0; transform:translateY(4px);} to{opacity:1; transform:none;} }
  /* The single worst offender in the "everything pops" complaint.
     This entrance animation used to apply to every list item unconditionally.
     Lists are rebuilt with innerHTML on every poll (mail 60s, read receipts
     15s, tasks 5min), so the ENTIRE inbox re-ran its fade-in once a minute
     while Matt was just sitting there reading it — a full-list flash, entirely
     unprompted. Now it only runs while a view is actually being entered
     (.entering is added by switchView and removed when it finishes), so
     background refreshes are silent, and arriving at a view gets a soft
     cascade instead of every row firing on the same frame. */
  /* The staggered row cascade lived here and has been removed deliberately.
     Measured first: nothing in the shell actually moves during a view switch —
     sidebar width, main offset and scrollbar width are identical before,
     during and after. So the "herky-jerky" was never a layout shift; it was
     this animation. Eight rows fading in at 18ms intervals, each over 220ms,
     meant the page kept ARRIVING for about half a second after the click. And
     because `.entering` stayed on the view for 500ms, any re-render inside that
     window (a mail poll, say) restarted the whole cascade and flickered rows
     that were already on screen.

     What replaces it is nothing: the outgoing view dissolves underneath and the
     incoming one is simply there, at full opacity, on the first frame. A page
     that is instantly present reads as faster and calmer than one that
     performs its own arrival. */
  .modal-x{ cursor:pointer; color:var(--text-dim); font-size:20px; line-height:1; }
  .field-row{ display:flex; gap:var(--space-2); }
  .field-row .field{ flex:1; }
  /* THE EDIT CARD'S FIRST NAME / LAST NAME, side by side. Two boxes stacked
     would cost the About column another ~70px, and that card already has to
     fit a 690px-high window with Photo URL at the foot. minmax(0,1fr) rather
     than 1fr, so a long surname cannot widen its own column. */
  .ce-namepair{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:0 var(--space-3); }

  /* ROW 1723 — the line under "Who it is about" in the New Reminder card.
     Three states and each one is a SENTENCE, because this box has to be able to
     say the uncomfortable one: a name that matches nobody. Colour only ever
     confirms what the sentence already said — the found state is the theme's
     own accent because being attached to somebody is the good outcome, and the
     unknown state is the same amber the app uses everywhere else for "this is
     not wrong yet, but it will be if you press Save". */
  .rm-person-note{ font-size:var(--type-body); line-height:1.35; margin:0 0 var(--space-3);
    padding:var(--space-2) var(--space-3); border-radius:var(--radius-control); color:var(--text-dim);
    border:1px solid var(--glass-border); background:var(--wash-soft); }
  .rm-person-note.is-found{ color:var(--text);
    border-color:color-mix(in srgb, var(--accent) 38%, transparent);
    background:color-mix(in srgb, var(--accent) 9%, transparent); }
  .rm-person-note.is-unknown{ color:var(--orange);
    border-color:color-mix(in srgb, var(--orange) 42%, transparent);
    background:color-mix(in srgb, var(--orange) 10%, transparent); }
  /* On paper the ink steps toward black, the same argument as the late bar:
     a warm hue washed over its own colour at ten percent leaves too little
     between the letters and the panel they sit on. */
  :root[data-mode="light"] .rm-person-note.is-unknown{
    color:color-mix(in srgb, var(--orange) 72%, #000); }
  /* Was a fixed 520px with no height limit, so a meeting with a long invitee
     list ran straight off the bottom of the screen with no way to scroll.
     Wider, capped, and scrolling inside — same treatment as Settings. */
  /* A7: "New-event card is too tall. Make it wider." Same fix, and it is the
     card that shows the problem most clearly — it already had the height
     ceiling, so all its content did was squeeze into a narrow column and
     scroll. Width was the missing half. */
  .event-modal{ max-width:820px; padding:var(--space-6); border-top-width:4px; border-top-style:solid;
    max-height:min(86vh, 760px); display:flex; flex-direction:column; overflow:hidden; }
  .event-modal > *{ flex:none; }
  /* .em-body is the general form of the two ids below it.
     .event-modal is `overflow:hidden` with `> * { flex:none }`, so a child that
     is not named here cannot scroll AND cannot grow — it is simply clipped at
     the card edge with no way to reach the rest. That is what was happening to
     Needs Attention, which wears .event-modal for its coloured top border and
     had no scroll region of its own: with more than about eight items the list
     just stopped, mid-row, with nothing to drag. Any card in this family now
     marks its scrolling region with .em-body and the two ids stay as they were
     so nothing that already worked has to change. */
  .event-modal .em-body,
  .event-modal #eventDetailContent, .event-modal #taskDetailContent{
    flex:1; min-height:0; overflow-y:auto; overflow-x:hidden;
    padding-right:var(--space-2); margin-right:calc(var(--space-2) * -1); scrollbar-width:thin;
  }
  /* A wide card in this family needs the wide ceiling too — .event-modal's own
     820px would otherwise beat .cm-wide on source order. */
  .event-modal.cm-wide{ max-width:1000px; }
  @media(min-width:1180px){ .event-modal.cm-wide{ max-width:min(1120px, calc(100vw - 96px)); } }
  .event-modal ::-webkit-scrollbar{ width:8px; }
  .event-modal ::-webkit-scrollbar-thumb{ background:rgba(255,255,255,0.10); border-radius:var(--radius-tight); }
  /* =====================================================================
     "I LIKE THE TRAYS, BUT I WANT THEM LIKE THE TASK TRAYS — NOT THIS NEW
      CARTOONISH VERSION. LIKE THE GLOW, BUT CARD AND FONT NO GO."
     =====================================================================

     Second time he has said it, so this time it is measured rather than
     eyeballed. The card was never the problem — the drawer and the tray are
     both `.glass`, both min(560px,100vw), both sheen-over-`--bg-mid`, both
     carrying the same `--glass-shadow`. Line for line the boxes were already
     the same. What made this one read as a different, louder object was
     everything INSIDE it:

       · 22px Fraunces where the drawer uses 21px
       · a 14px MONOSPACE date under it — the drawer has no mono anywhere
       · labels at weight 400 where every label in the drawer is 700
       · fields with no box until you hover, where the drawer's are always
         a visible input
       · fully-round 999px chips where the drawer's are 20px
       · one flat 20px padding, where the drawer has a header band with a
         hairline under it and a body with 40px of floor beneath it

     Six small differences, none of them wrong on its own, all pulling the
     same way. Every value below is now the drawer's value, taken from the
     `.pj-*` rules rather than chosen again.

     THE GLOW STAYS. That is the 4px coloured leading edge at the end of this
     block, and it is the one thing he said to keep. */
  /* .pj-drawer-head. A band with a hairline under it, and TWO LINES inside it:
     a small uppercase kicker carrying the controls, then the title on its own
     line underneath. The single-line version — dot, title, × — is why the two
     surfaces still did not match after the type was fixed: a task announces
     what it belongs to before it says its name, and the meeting did not. */
  .ed-head{ padding:var(--space-5) var(--space-6) var(--space-3); margin:0 -22px 0;
    border-bottom:1px solid var(--glass-border); }
  /* 9px, like .ed-swatch and the drawer's project dot. The 12px version was a
     bullet beside a 21px title; at kicker size it is punctuation. */
  .ed-dot{ width:9px; height:9px; border-radius:var(--radius-round); flex:none; }
  /* 21px and line-height 1.25 — .pj-drawer-title, exactly. */
  .ed-title{ font-family:var(--font-display); font-size:var(--type-title); font-weight:600; line-height:1.25; }
  /* NOT MONOSPACE. A 14px mono line sitting under a 21px serif title was the
     single loudest "this is a different app" signal in the tray, and the task
     drawer sets no date in mono anywhere. */
  .ed-time{ font-size:var(--type-body); color:var(--text-dim); margin:var(--space-4) 0 var(--space-5); }
  .ed-rows{ margin-bottom:var(--space-4); }
  /* .pj-field: label gutter on the left, value filling the rest. NOT
     space-between — that pushed every value hard against the right edge and
     made a column of unrelated ragged text. */
  .ed-row{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) 0;
    border-bottom:1px solid var(--glass-border); font-size:var(--type-body); }
  .ed-row:last-child{ border:none; }
  .ed-row > :last-child{ flex:1; min-width:0; }
  /* Weight 700 and .06em, which is what every label in the drawer is. */
  .ed-label{ color:var(--text-faint); font-size:var(--type-label); font-weight:700;
    text-transform:uppercase; letter-spacing:.06em; width:112px; flex:none; }
  .ed-swatch{ width:9px; height:9px; border-radius:var(--radius-round); display:inline-block; margin-right:var(--space-2); }
  .ed-muted{ color:var(--text-faint); }
  .teams-btn{ width:100%; margin-bottom:var(--space-4); background:color-mix(in srgb, #5B5FC7 22%, transparent); border-color:color-mix(in srgb,#5B5FC7 48%, transparent); color:var(--text); font-weight:700; }
  .teams-btn:hover{ background:color-mix(in srgb, #5B5FC7 32%, transparent); }
  .ed-invitee{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) 0; font-size:var(--type-body); cursor:pointer; }
  .ed-invitee:hover{ text-decoration:underline; }

  /* ---------------------------------------------------------------------
     THE EVENT PANEL, NOW THAT IT IS AN EDITOR.

     Every control below has to read as PART OF THE CARD rather than as a form
     dropped onto it — that is the whole difference between Fantastical's popup
     and the three-column dialog this replaces. So the inputs have no box until
     you are over them or in them: at rest they are text at the same size and
     colour as the text beside them, and the affordance appears when you reach
     for it. A tray full of visible input boxes is a form, and he already told
     me what he thinks of those.
     --------------------------------------------------------------------- */
  /* .pj-drawer-title's box and its NEUTRAL focus ring. The accent ring was the
     third thing shouting on a card that already has a coloured edge. */
  .ed-title-edit{ outline:none; border-radius:var(--radius-control); padding:var(--space-hair) var(--space-1); margin:calc(var(--space-hair) * -1) calc(var(--space-1) * -1); cursor:text; }
  .ed-title-edit:focus{ background:rgba(120,120,140,.10);
                        box-shadow:0 0 0 1px var(--glass-border-hi); }

  .ed-when{ margin-bottom:var(--space-4); }
  /* .pj-done-row wearing .pj-toggle — the same switch, in the same place at the
     top of the panel, as "Mark complete" on a task. It was a raw platform
     checkbox: a square with the OS tick in it, 15px, sitting under a serif
     title. Nothing else in Lucid renders one, and it was the loudest single
     object in the "cartoonish" screenshot.
     THE INPUT IS STILL AN INPUT — hidden, not replaced. It keeps the label
     association, the keyboard, and evAllDayToggle()'s `.checked`; only the box
     is ours. A div pretending to be a checkbox is not reachable by Tab. */
  .ed-switch{ display:flex; align-items:center; gap:var(--space-3); font-size:var(--type-body); font-weight:600;
              color:var(--text); cursor:pointer; margin-bottom:var(--space-3); user-select:none; }
  .ed-switch input{ position:absolute; opacity:0; width:0; height:0; }
  /* ONE LABEL GUTTER FOR THE WHOLE PANEL. Starts and Ends had their own, at
     52px, so the two date fields began 60px to the left of every other value
     on the card and the eye had two columns to follow instead of one. The
     drawer has exactly one, at 112px, and this is now the same 112px. */
  .ed-stamp{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) 0;
             border-bottom:1px solid var(--glass-border); }

  /* .pj-input, exactly. A field that is invisible until you reach for it is a
     nice idea and it is not this app's idea — every editable thing in the task
     drawer is a visible box at rest, and a tray full of text that turns out to
     be typeable when you touch it is the "cartoonish" half of the complaint.
     No hover state either: the drawer has none. */
  .ed-in{ background:rgba(120,120,140,.08); border:1px solid var(--glass-border);
          border-radius:var(--radius-control); color:var(--text); font-family:var(--font-body);
          font-size:var(--type-body); padding:var(--space-2) var(--space-3); min-width:0; }
  .ed-in:focus{ outline:none; border-color:color-mix(in srgb, var(--accent) 50%, transparent); }
  /* THE DROPDOWNS WERE PLATFORM DROPDOWNS. Same trap as the checkbox: a native
     <select> paints the OS arrow in the OS font at the OS size, so the two
     alert rows were the only objects in the tray not drawn by this stylesheet.
     appearance:none plus our own chevron. The chevron is a flat grey rather
     than currentColor because it lives in a data URI, and #8a8a99 clears 3:1
     against both the dark ground and the light one. */
  select.ed-in{ appearance:none; -webkit-appearance:none; cursor:pointer; padding-right:var(--space-8);
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7'><path d='M1 1l4 4 4-4' fill='none' stroke='%238a8a99' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat:no-repeat; background-position:right 11px center; }
  /* Left, like every value in the task drawer. Right-aligned text in a left-
     labelled row gives you a column of ragged nothing down the middle. */
  .ed-in-wide{ flex:1; }
  /* 104px cut the M off "02:00 PM" — a native time field sizes itself from the
     platform's own font, which is not the one this rule sets. */
  .ed-in-time{ width:128px; flex:none; }
  /* The invert(.75) that used to live here recoloured the native picker's
     own light-on-light calendar icon — the one lever CSS had over a widget it
     otherwise could not touch. lucidEnhanceDateInput()/lucidEnhanceTimeInput()
     (app.js) now hide edStartDate/edEndDate/edStartTime/edEndTime and draw a
     themed field and popover in their place, so there is no native icon left
     to invert. */
  .ed-allday-on .ed-in-time{ display:none; }

  /* "an event crossing midnight shows as 'ends at 12:40AM' on the next day" */
  .ed-span{ margin-top:var(--space-2); font-size:var(--type-body); color:var(--orange);
            background:color-mix(in srgb, var(--orange) 12%, transparent);
            border:1px solid color-mix(in srgb, var(--orange) 26%, transparent);
            border-radius:var(--radius-control); padding:var(--space-2) var(--space-2); display:inline-block; }

  .ed-row-chips{ align-items:center; }
  /* Left, with everything else. .pj-seg starts at the gutter; these ended at
     the right edge, so Busy/Maybe/Free/Out was the one row on the card that
     began somewhere different from the row above it. */
  .ed-chips{ display:flex; gap:var(--space-2); flex-wrap:wrap; }
  /* .chip / .chip.active — the same shape and weight the drawer's Repeats row
     uses. 999px on a 12px chip is what makes a control look like a toy. */
  .ed-chip{ display:inline-flex; align-items:center; gap:var(--space-2); font:inherit; font-size:var(--type-body);
            font-weight:600; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); cursor:pointer;
            color:var(--text-dim); background:transparent; border:1px solid var(--glass-border);
            transition:all var(--dur-fast) var(--ease-out); }
  .ed-chip i{ width:7px; height:7px; border-radius:var(--radius-round); display:inline-block; }
  .ed-chip:hover{ color:var(--text); }
  .ed-chip.active{ color:var(--text); background:rgba(120,120,140,.12);
                   border-color:var(--glass-border-hi); }

  .ed-row-stack{ display:block; }
  .ed-row-stack .ed-label{ display:block; margin-bottom:var(--space-2); }
  .ed-alerts{ display:flex; flex-direction:column; gap:var(--space-2); }
  .ed-alert{ display:flex; align-items:center; gap:var(--space-2); font-size:var(--type-body); }
  .ed-alert select.ed-in{ flex:1; }
  .ed-alert-fixed{ flex:1; color:var(--text-dim); }
  /* NOT UPPERCASE. In this panel uppercase means "this is the name of the row",
     and every one of those sits in the left gutter. A second uppercase run to
     the RIGHT of a control read as a second label rather than as the note it
     is — .pj-unit in the drawer is 11px sentence case, and this is that. */
  .ed-alert-tag{ font-size:var(--type-label); color:var(--text-faint); white-space:nowrap; }
  .ed-alert-x{ background:none; border:none; color:var(--text-faint); font-size:var(--type-lead); line-height:1;
               cursor:pointer; padding:0 var(--space-1); }
  .ed-alert-x:hover{ color:var(--pink); }
  .ed-alert-plus{ background:none; border:1px dashed var(--glass-border); border-radius:var(--radius-control);
                  color:var(--text-dim); font:inherit; font-size:var(--type-body); padding:var(--space-1) var(--space-3); cursor:pointer;
                  white-space:nowrap; }
  .ed-alert-plus:hover{ color:var(--text); border-color:color-mix(in srgb, var(--accent) 55%, transparent);
                        border-style:solid; }

  /* The description. pre-wrap because an invitation's line breaks ARE its
     structure — a dial-in block flattened into a paragraph is unusable — and
     word-break because conference URLs are longer than any tray is wide. */
  .ed-desc{ font-size:var(--type-body); line-height:1.6; color:var(--text-dim); white-space:pre-wrap;
            overflow-wrap:anywhere; max-height:280px; overflow-y:auto; margin-bottom:var(--space-4);
            padding-right:var(--space-1); }
  .ed-desc a{ color:var(--accent); text-decoration:underline; text-underline-offset:2px; }
  .ed-desc a:hover{ color:var(--text); }

  .ed-inv-name{ flex:1; }
  .ed-inv-mark{ font-size:var(--type-body); width:18px; height:18px; border-radius:var(--radius-round); flex:none;
                display:inline-flex; align-items:center; justify-content:center; font-weight:700; }
  .ed-inv-mark.ev-yes{ color:var(--green); background:color-mix(in srgb, var(--green) 16%, transparent); }
  .ed-inv-mark.ev-no{ color:var(--pink); background:color-mix(in srgb, var(--pink) 16%, transparent); }
  .ed-inv-mark.ev-maybe{ color:var(--orange); background:color-mix(in srgb, var(--orange) 16%, transparent); }
  .ed-inv-mark.ev-org{ color:var(--accent); background:color-mix(in srgb, var(--accent) 16%, transparent); }
  /* "NO REPLY YET" IS A STATE, NOT AN ABSENCE. 20 September, Matt's call.
     This dot measured 1.61:1 on parchment and 1.68:1 on midnight — the last
     two contrast failures in the whole app, and the only ones left after the
     sweep on the 20th. It was drawn faint on the argument that nothing having
     come back should look like nothing; but the four states beside it each
     get a colour AND a filled ground, and this one got neither, so "we have
     not heard" was indistinguishable from "there is nobody here". That is
     item 211 in its original form: a failed read must not render like an
     empty one.
     Full text colour, and the same 16% ground its siblings carry, in the
     neutral dim rather than an accent — it is still the quietest of the five,
     and now it is legible. */
  .ed-inv-mark.ev-none{ color:var(--text); background:color-mix(in srgb, var(--text-dim) 16%, transparent);
    /* AND THE GLYPH IS BIG ENOUGH TO BE MEASURED, WHICH IS THE SAME AS BIG
       ENOUGH TO BE SEEN. A middle dot at 13px is three or four solid pixels
       with a halo of antialiasing round them; brightening its colour moved
       the reading from 1.68:1 only to 2.89:1, because most of what anybody —
       eye or meter — is looking at is the blend and not the ink. The other
       four states are read from their FILL, which is why they never had this
       problem. This one is read from the mark, so the mark has to be a mark.
       1.5x the size and a heavier weight gives it a solid centre. */
    font-size:var(--type-lead); font-weight:800; line-height:1; }
  /* An invitee row is only a door when it has somewhere to go. The old rule
     underlined all of them on hover, including the ones that do nothing. */
  .ed-invitee{ cursor:default; }
  .ed-invitee[onclick]{ cursor:pointer; }
  .ed-invitee:hover{ text-decoration:none; }
  .ed-invitee[onclick]:hover .ed-inv-name{ text-decoration:underline; }

  /* ---------------------------------------------------------------------
     THE LOCATION MAP. Six OpenStreetMap tiles and no library. The frame is a
     fixed height and the tile layer is translated by the pin's offset, so the
     place stays in the middle of the frame at any panel width — see the note
     by evMapHtml about why the layer is dragged rather than scaled.
     --------------------------------------------------------------------- */
  .ev-map{ margin:0 0 var(--space-4); }
  .ev-map-frame{ position:relative; height:168px; border-radius:var(--radius-card); overflow:hidden;
      border:1px solid var(--glass-border); background:var(--bg-mid); }
  .ev-map-tiles{ position:absolute; left:50%; top:50%; }
  .ev-map-t{ position:absolute; width:256px; height:256px; display:block; }
  /* OSM's own tiles are a bright paper white, which in a dark theme is a torch
     in the middle of the panel. Knocked back and warmed towards the app rather
     than left to shout — the streets are still legible, which is the job. */
  :root:not([data-mode="light"]) .ev-map-tiles{ filter:invert(.9) hue-rotate(180deg) saturate(.75) brightness(1.05); }
  .ev-map-pin{ position:absolute; left:50%; top:50%; width:13px; height:13px; margin:-6.5px 0 0 -6.5px;
      border-radius:var(--radius-round); background:var(--pink); border:2.5px solid var(--bg-base);
      box-shadow:0 0 0 1px color-mix(in srgb, var(--pink) 60%, transparent), 0 2px 8px rgba(0,0,0,.45); }
  .ev-map-wx{ position:absolute; left:9px; top:9px; display:flex; align-items:center; gap:var(--space-2);
      padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); font-size:var(--type-body); font-family:var(--font-mono);
      color:var(--text); background:color-mix(in srgb, var(--bg-base) 78%, transparent);
      border:1px solid var(--glass-border); backdrop-filter:blur(6px); }
  .ev-map-wx-ico{ width:14px; height:14px; display:block; }
  .ev-map-wx-ico svg{ width:100%; height:100%; display:block; }
  .ev-map-wx-lo{ opacity:.62; }
  .ev-map-wx-rain{ color:var(--blue); }
  .ev-map-open{ position:absolute; right:9px; bottom:9px; font-size:var(--type-body); text-decoration:none;
      padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); color:var(--text);
      background:color-mix(in srgb, var(--bg-base) 78%, transparent);
      border:1px solid var(--glass-border); backdrop-filter:blur(6px); }
  .ev-map-open:hover{ border-color:color-mix(in srgb, var(--accent) 60%, transparent); }
  /* Required by the tile usage policy, and it is why the map is free. */
  .ev-map-credit{ position:absolute; left:9px; bottom:7px; font-size:var(--type-label); color:var(--text-faint);
      background:color-mix(in srgb, var(--bg-base) 62%, transparent); padding:1px var(--space-2); border-radius:var(--radius-tight); }

  .ed-foot{ margin:var(--space-3) 0 0; font-size:var(--type-body); color:var(--text-faint); text-align:center; }
  .global-fab-wrap{ position:fixed; bottom:26px; left:26px; z-index:998; }
  /* Moved into the spine. It keeps its look — same round, glowing, accent
     button — but it now sits in flow at the top of the rail instead of
     floating over the sidebar and the reading pane. Event / Compose /
     Reminder were removed from the rail because this menu already contains
     all three; having both was the same action in two places. */
  /* ============================================================
     "THE OBLONG ACTION BUTTON" — his words, 2 September, third on the list of
     things making the spine look like a garage.
     ------------------------------------------------------------
     It was a 48px violet disc with three shadows on it — a 26px shadow, a 26px
     violet halo and a 46px teal one — sitting at the top of a rail whose rows
     had just been quietened to a 2px bar. Whatever it was in isolation, next to
     the new rows it was a bauble, and it was the largest object in the spine.

     THE ARGUMENT IT WAS BUILT ON STILL HOLDS: create is a different KIND of
     control from a destination and must not read as a seventh place to go. But
     "different kind" does not have to be said with a glow. It is now the only
     row in the rail with a border and the only one with a label in brass —
     which distinguishes it from six borderless rows more clearly than a violet
     disc did, and does it without shouting.

     FULL WIDTH, AND SQUARE. A disc in a column of squared rows is the shape
     that made it read as bolted on; a bordered row of the same width as
     everything below it reads as the top of the list, which is what it is. */
  .global-fab-wrap.in-spine{ position:static; z-index:auto; display:block; margin:0 0 var(--space-3); }
  .global-fab-wrap.in-spine .global-fab{
    width:100%; height:34px; border-radius:var(--radius-tight);
    display:flex; align-items:center; justify-content:flex-start; gap:var(--space-2);
    padding:0 var(--space-3);
    background:transparent;
    border:1px solid color-mix(in srgb, var(--brass) 34%, transparent);
    color:var(--brass);
    font-family:var(--font-body); font-size:var(--type-body); font-weight:600;
    letter-spacing:.02em;
    box-shadow:none;
    transition:border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
  }
  /* The label is a real span in the markup — see the note beside it in
     index.html for why a ::after could never have worked here. */
  .gf-label{ font-size:var(--type-body); font-weight:600; letter-spacing:.02em; }
  .global-fab-wrap.in-spine .global-fab svg{ width:15px; height:15px; stroke-width:2; flex:none; }
  .global-fab-wrap.in-spine .global-fab:hover{
    transform:none;
    background:rgba(255,246,230,.045);
    border-color:color-mix(in srgb, var(--brass) 70%, transparent);
    box-shadow:none;
  }
  /* Collapsed, there is no room for the word and a full-width bordered row in a
     70px rail is a box round a plus sign — which is exactly what it should be. */
  .sidebar.collapsed .global-fab-wrap.in-spine .global-fab{
    justify-content:center; padding:0; height:38px; border-radius:var(--radius-control); }
  /* Opens downward now that the button is at the top, not the bottom. */
  /* The menu is 170px wide and the collapsed rail is 70px with
     overflow-x:hidden, so an absolutely-positioned menu was simply clipped
     out of existence — and because .in-spine is position:static it also
     resolved its coordinates against the wrong ancestor and landed off the
     bottom of the screen. Fixed positioning takes it out of the sidebar's
     clipping context entirely; toggleGlobalFab sets the coordinates. */
  /* The menu cannot live inside the sidebar at all. .sidebar carries .glass,
     which sets BOTH backdrop-filter and overflow:hidden — and backdrop-filter
     makes an element a containing block for position:fixed descendants. So a
     "fixed" menu was still being positioned against the sidebar and still
     clipped by it. The only reliable fix is to move the element out of the
     sidebar in the DOM; it is re-parented to <body> on first open and
     positioned in viewport coordinates from then on. */
  body > .global-fab-menu{ position:fixed; top:auto; bottom:auto; left:auto; right:auto; z-index:999; }
  .global-fab{ width:52px; height:52px; border-radius:var(--radius-round); border:1px solid color-mix(in srgb, var(--blue) 45%, transparent); cursor:pointer; display:flex; align-items:center; justify-content:center; background:color-mix(in srgb, var(--blue) 20%, rgba(18,18,32,.7)); backdrop-filter:blur(10px); color:var(--blue-2); box-shadow:0 8px 26px -4px rgba(0,0,0,.55), 0 0 28px 2px var(--blue-glow); transition:box-shadow var(--dur-fast), transform var(--dur-fast); }
  .global-fab:hover{ transform:scale(1.08); box-shadow:0 10px 30px -4px rgba(0,0,0,.6), 0 0 42px 4px var(--blue-glow); }
  .global-fab svg{ width:22px; height:22px; }
  .global-fab-menu{ top:auto; bottom:calc(100% + 8px); left:0; right:auto; min-width:170px; }

  /* THE TWO PLACES A DARK VALUE WAS HARD-CODED.
     Both of these mix their colour into rgba(18,18,32,…) — a literal, written
     when there was only ever a dark room to sit in. On cream they are navy
     blobs: the create button reads as a hole in the page, and it is the first
     thing the eye lands on in the rail.
     The mix here is the same idea against the light ground: the accent, held
     at a tint that keeps the glyph readable, over the card surface rather than
     over an invented dark. */
  :root[data-mode="light"] .global-fab{
    background:color-mix(in srgb, var(--blue) 16%, rgba(255,255,255,.92));
    border-color:color-mix(in srgb, var(--blue) 40%, transparent);
    color:var(--blue);
    box-shadow:var(--elev-raised);
  }
  :root[data-mode="light"] .global-fab:hover{
    transform:scale(1.08);
    box-shadow:var(--elev-floating);
  }
  /* THE IN-SPINE COPY IS NOT THE FLOATING DISC, and light mode has to be told
     so twice — once for each property the block above sets. That block is
     right about the disc: a blue tint on cream keeps the glyph readable where
     a dark mix would not. It is wrong about the rail button, which is a
     hairline and a word in brass, and which came out blue in the light themes
     because :root[data-mode="light"] .global-fab outranks the unprefixed
     .global-fab-wrap.in-spine .global-fab on specificity — the light rule
     carries an attribute selector the dark one has no equivalent of. Caught by
     looking at shots/spine-light.png; it does not show up in a dark screenshot
     and it does not show up in the diff.
     --brass is already stepped down for cream at the top of this file, so this
     restates the tokens rather than inventing a second set of colours. */
  :root[data-mode="light"] .global-fab-wrap.in-spine .global-fab{
    background:transparent;
    border-color:color-mix(in srgb, var(--brass) 45%, transparent);
    color:var(--brass);
    box-shadow:none;
  }
  :root[data-mode="light"] .global-fab-wrap.in-spine .global-fab:hover{
    transform:none;
    background:rgba(120,100,60,.06);
    border-color:color-mix(in srgb, var(--brass) 80%, transparent);
    box-shadow:none;
  }

  /* CHAT ASSISTANT */
  /* The chat window prefers the orb-derived tint and falls back to the app
     accent, so turning "chat follows orb" off needs no other code path — the
     variables simply stop existing.

     AND THE FALLBACK NAMED THE WRONG VARIABLE UNTIL 30 AUGUST. It said
     var(--blue), which is a raw hue, not the app's accent — so switching "chat
     follows orb" off did not hand the window back to --accent as the comment
     above claimed; it handed it to blue, whatever the theme was wearing. A
     brass room with a blue chat window in the corner of it. --accent IS
     var(--blue) under the default theme, which is why nobody noticed, and it is
     var(--brass) under the other one, which is when it shows. */
  .chat-card{ border-top-color: var(--orb-accent, var(--accent)) !important;
    box-shadow: 0 24px 60px -18px rgba(0,0,0,.8), 0 0 44px -14px var(--orb-glow, var(--accent-glow)); }
  /* Real class names, verified against the markup rather than assumed. */
  .msg-user{ background:linear-gradient(135deg, var(--orb-accent, var(--accent)), var(--orb-accent-2, var(--accent-2))) !important;
    color:var(--orb-ink, var(--ink)) !important; }
  .chat-chip{ border-color:color-mix(in srgb, var(--orb-accent, var(--accent)) 40%, transparent) !important; }
  .chat-empty-icon{ color:var(--orb-accent-2, var(--accent-2)) !important; }
  .chat-send, .chat-card .btn.primary{ background:linear-gradient(135deg, var(--orb-accent, var(--accent)), var(--orb-accent-2, var(--accent-2))) !important;
    color:var(--orb-ink, var(--ink)) !important; }
  .chat-input:focus{ border-color:color-mix(in srgb, var(--orb-accent, var(--accent)) 55%, transparent) !important;
    box-shadow:0 0 14px -3px var(--orb-glow, var(--accent-glow)) !important; }
  /* AND ON PAPER THE SAME RECIPE AS EVERY OTHER PRIMARY BUTTON, 22 September.
     Accent-to-its-own-pastel with near-black lettering is a dark app's
     button; in light mode it is dark-on-pale at the far stop, which measured
     1.46:1 on Denim and 1.70:1 on Terracotta the moment these two rules
     stopped being handed a colour computed in JavaScript. The fix is the one
     :root[data-mode="light"] .btn.primary already makes 8,000 lines up and
     these !important rules were overriding: the fill DARKENS across the
     gradient instead of fading out, and the lettering is the theme's own
     paper. A hand-picked colour still beats it, and still brings its own ink. */
  :root[data-mode="light"] .msg-user,
  :root[data-mode="light"] .chat-send,
  :root[data-mode="light"] .chat-card .btn.primary{
    background:linear-gradient(135deg, var(--orb-accent, var(--accent)),
      color-mix(in srgb, var(--orb-accent, var(--accent)) 82%, var(--neutral-2))) !important;
    color:var(--orb-ink, var(--on-accent)) !important; }
  /* The literal here was the same mistake in a third spelling: a lilac nobody
     could reach from any setting, sitting under the button the orb lives in. */
  /* ==========================================================================
     THE ORB'S EXCLUSION ZONE.  (Row 2022.)
     --------------------------------------------------------------------------
     "The Ask orb sits on top of whatever scrolls into the bottom-right corner."
     12 September, measured rather than noticed.

     THE ORB IS FIXED TO THE VIEWPORT AND THE PAGE SCROLLS UNDER IT. At 1500×950
     its left edge is at 1386 and the content column runs to 1468, so the sphere
     stands on the last 82px of every page — and because .main scrolls beneath
     it rather than moving it, ANY right-hand control can be carried into that
     82px and parked there. On Home it was the 'Your digest' header's Settings
     link, 1343→1410: twenty-four pixels of the word under an opaque sphere, so
     it read "Settin" and the last quarter of it did nothing. That is precisely
     the thing this app does not ship — a control that looks like it works and
     does not.

     THE NUMBER BELOW IS MEASURED, NOT CHOSEN, and it is written three lines
     above the rule that produces it so that the two cannot drift apart:

       --orb-keepout-x  how far the sphere reaches INTO the content column:
                        its width plus its right offset, less .main's own right
                        padding — 88 + 26 − 32 = 82px on a desktop.

     It is re-stated inside every breakpoint that moves or resizes the sphere,
     and nowhere else. There is deliberately no --orb-keepout-y to go with it;
     see the first bullet below.

     WHY THE ZONE IS SPENT WHERE IT IS, AND WHY THE OTHER TWO ANSWERS LOSE.
     A sweep of all 27 pages at five window sizes, each scrolled through eleven
     positions, found thirty-eight interactive elements that pass under the
     sphere and exactly six that it can swallow whole — four section-header
     verbs, the ⋯ on an agenda row, and the tick-box on a contact card. The
     other thirty-two are cards and full-width rows with 82px shaded off a
     four-hundred-pixel right end: they stay legible, they stay pressable, and
     buying them anything would cost the page more than they are losing.

       · BOTTOM PADDING ON .main is the blunt answer and it does not answer
         this. It reserves the band only at the very END of a scroll, and the
         orb's band is crossed by every row on the way past it — so the digest's
         Settings link, halfway down a long Home board, is under the sphere at
         whatever position the page happens to stop at, and .main is a hundred
         pixels shorter on every page for nothing. It is not here BECAUSE it was
         measured rather than assumed: with the keep-outs below in place, the
         sweep scrolls every page and every inner pane to its last pixel and
         finds nothing resting in the corner, so the strip would have been dead
         space bought with no fault to spend it on.

       · A RIGHT GUTTER ON .main — the version that makes the corner unreachable
         by construction, one rule, nothing to rot — was built and photographed
         before it was rejected. It moves the whole page 82px left: .home-band,
         which is full-bleed and pinned to the TOP of the flow where the orb can
         never reach it, loses its right edge at 1500 and REFLOWS at 1280,
         dropping the overdue count onto a second row and making the hero band
         half as tall again. Matt has signed off how the front door looks;
         paying for the corner with the hero band is not a trade worth making.

       · SO THE ZONE IS SPENT ONE CONTENT COLUMN AT A TIME, on whichever
         container is cheapest to narrow on the page in question. That is four
         rules and each of them works out its own arithmetic where it sits:
             #view-home, all of it
             but the bleeding band — so the cost is shared out at ~7px a column
                                     across the twelve-column track, rather than
                                     82px off any one tile. (Home.)
             #view-comms, and the
             two beside it         — the page, so the toolbar and the cards stay
                                     in line with each other. (In Touch and
                                     Organizations.)
             .agenda-scroll        — only below 1100px, where the agenda stops
                                     being a left rail. (Today.)
             .wl-panel-head        — a wrapping heading rather than a right
                                     edge; the padding stops the door flowing
                                     into the corner. (Media and Fashion.)
         Four pages carry it and the other twenty-three need nothing, which is
         the measurement talking rather than a policy.

     AND THAT LIST IS NOT THE GUARANTEE — THE TEST IS.
     tests/test-the-orb-covers-nothing.js.

     THAT SENTENCE WAS WRITTEN BEFORE THE FILE WAS. For a week it named a safety
     net by filename and the filename was the whole of it — which is worse than
     having no check, because the next person to read this block stops worrying.
     It also described a sweep nobody could have run: "every page at every size
     through eleven scroll positions".

     The file exists from 28 September, and this is what it does rather than what
     it was said to do. Eight pages — the four that pay for the zone above, plus
     Mail, Projects, the board and Home again — at three widths, five scroll
     stops each: 120 page-and-scroll positions, 34,360 controls in view across
     them. It fails if a floating control in this corner takes more than a
     quarter of another control's box, or covers a press target whole.

     AND IT KNOWS THE SPHERE IS GONE. Nothing floats in that corner today, so a
     sweep that finds nothing has proved nothing — it would pass just as happily
     looking in the wrong place or measuring an invisible box. So the check
     ALWAYS plants a stand-in with .chat-pill's exact old geometry (88px,
     bottom:26px, right:26px) and sweeps again. That watches the sweep find
     something, and it answers the question the orb reference (2731) will ask:

         45  controls touched at all
          5  losing more than a quarter of themselves — and all five are the
             SAME control at the same width, seen at five scroll positions:
             "Open Listening" on Home at 1100px, at 65%
          0  swallowed whole

     So the whole measured cost of putting an 88px sphere back in this corner
     today is one control, on one page, below 1280px. What that is worth buying
     with, and whether it is worth buying at all, is Matt's call — the number is
     here so it is a call and not a guess. The full list is rewritten to
     shots-ui/the-corner/planted-census.json on every run.

     The next right-edge control somebody puts in that corner turns the build red
     instead of reaching him. This is a class of fault rather than two buttons,
     and four selectors on their own would rot inside a week. */
  /* THE SPHERE WENT ON 21 SEPTEMBER AND THE ZONE GOES WITH IT. There is no
     .chat-pill in index.html any more, so every pixel reserved below was being
     held for something that cannot appear. Zero rather than deleted: the four
     rules that read this and the whole argument above them stay, so putting a
     floating control back in that corner is one number. Measured before and
     after at a 1200px window, rail collapsed: the page column goes 968 -> 1050
     and a contact card in a row of four 229 -> 249. */
  :root{ --orb-keepout-x:0px; }
  .chat-pill{
    position:fixed; bottom:26px; right:26px; z-index:20;
    width:88px; height:88px; border-radius:var(--radius-round);
    display:flex; align-items:center; justify-content:center; cursor:grab;
    background:transparent; border:none; box-shadow:none; padding:0;
    /* touch-action, so a drag on the sphere is a drag rather than the page
       scrolling underneath the finger; user-select so a slow press does not
       start selecting the screen behind it. Both copied from .dock, which has
       been draggable for a fortnight and needed exactly these two. */
    touch-action:none; user-select:none; -webkit-user-select:none;
    transition: transform var(--dur-base) var(--ease-spring), filter var(--dur-base) var(--ease-out);
    filter: drop-shadow(0 10px 26px rgba(0,0,0,.55)) drop-shadow(0 0 26px rgba(160,125,255,.32));
  }
  /* WHILE IT IS BEING MOVED. The transition is switched off for the same reason
     .dock switches it off: left/top are written on every pointermove, and a
     transform easing over 240ms on top of that reads as the sphere lagging
     behind the cursor. */
  .chat-pill.dragging{ cursor:grabbing; transition:none; }
  /* A DIFFUSE OBJECT CANNOT WEAR A SOLID OBJECT'S SHADOW.
     drop-shadow follows the alpha of what it is shadowing, so an offset one
     under a vortex draws a second, darker vortex ten pixels below it — which
     reads as a dark ball with the disc sitting on top. Symmetric and tighter
     when the vortex is up; the original when it is a sphere. */
  /* None at all, in the end. A drop-shadow follows the ALPHA of what it is
     shadowing, and a vortex is mostly a wide field of very low alpha — so even
     a symmetric shadow renders as a dark disc the size of the whole canvas
     with the bright part floating in the middle of it. The vortex carries its
     own glow; it does not need a shadow underneath it. */
  .chat-pill:hover{ transform:scale(1.06);
    filter: drop-shadow(0 14px 32px rgba(0,0,0,.6)) drop-shadow(0 0 40px var(--orb-glow, var(--accent-glow))); }
  :root[data-glow="off"] .chat-pill:hover{ filter: drop-shadow(0 14px 32px rgba(0,0,0,.6)); }
  .chat-pill:active{ transform:scale(.97); }

  /* THE ORB ON PAPER.
     The brief called this the make-or-break question for Serene, and it is:
     the orb is an ADDITIVE light model — the shader starts at black and adds
     energy — so it is a dark object by construction, and it is Lucid's
     identity. It cannot simply be dropped onto cream.
     What breaks on a light ground is not the sphere. It is the treatment
     around it: a violet halo, which on cream reads as a smudge, and a heavy
     black drop-shadow, which reads as dirt. Both exist to separate a glowing
     object from a dark room, and neither has a job here.
     Replaced with what a physical object on paper actually has — one soft
     warm cast shadow, offset downward, tinted toward the ground's own colour.
     The sphere then reads as an iridescent marble resting on the page, which
     is a better answer than a dark blob and a better one than washing the orb
     out until it disappears. See also the uLight uniform in the shader, which
     lifts the sphere itself toward pearl rather than bubble. */
  /* LUCID HAS SOMETHING TO SAY.
     One dot, on the orb, and nothing else — no panel, no sound, no badge with a
     number in it. The smallest mark that can mean "there is something" without
     taking the screen away from what he was doing. It appears at most once a
     day and only when the thing is genuinely urgent; on a quiet day it never
     appears at all, which is the feature working rather than failing. */
  .chat-pill.has-word::after{
    content:''; position:absolute; top:12px; right:12px; width:11px; height:11px;
    border-radius:var(--radius-round); background:var(--accent);
    box-shadow:0 0 0 2px var(--bg-mid), 0 0 12px 2px var(--accent-glow);
    animation: wordPulse 2.6s var(--ease-out) infinite;
  }
  @keyframes wordPulse{
    0%, 100%{ transform:scale(1); opacity:1; }
    50%{ transform:scale(1.18); opacity:.82; }
  }
  @media (prefers-reduced-motion: reduce){
    .chat-pill.has-word::after{ animation:none; }
  }
  /* WHAT LUCID SAYS FIRST.
     One line, above the window rather than instead of it, marked with the
     accent rule the Home lead uses so it reads as the app speaking rather than
     as another row in a list. */
  .ask-first{ border-left:2px solid var(--accent); padding:var(--space-hair) 0 var(--space-hair) var(--space-3); margin:0 0 var(--space-4); }
  .ask-first-line{ font-size:var(--type-body); color:var(--text); line-height:1.5; }
  .ask-first-acts{ display:flex; gap:var(--space-2); margin:var(--space-2) 0 0; }

  /* THE ORB'S SHADOW ON PAPER, AND A TOKEN THAT NOTHING HAS EVER READ.
     ------------------------------------------------------------------------
     6 September. These two rules asked for --orb-shadow-1 and --orb-shadow-2.
     GREP THE FILE: nothing has ever defined either one. Not a theme, not a
     paper, not :root. Every light theme in the wardrobe has therefore been
     rendering the hardcoded fallback below — a warm BROWN cast shadow — for as
     long as the rules have existed.

     Five of the eight light grounds meanwhile define `--orb-shadow`, singular,
     each one correctly tinted to its own ground: Denim's is a night blue,
     Mist's is a cool green, Terracotta's is a burnt clay. Every one of those
     was written, measured and committed, and not one of them has ever painted
     a pixel, because the name in the theme and the name in the rule were never
     the same name. So the orb on Matt's blue page has been sitting in a brown
     puddle, and the theme that says otherwise was right all along and unread.

     THIS IS THE FAILURE MODE THE HARNESS EXISTS FOR, and it is worth being
     precise about why nothing caught it: a var() with a fallback CANNOT fail
     loudly. It renders something plausible for ever. That is why
     tests/test-presence.js now asserts that every token these rules name is
     defined by every ground that can wear them, and why the fallbacks below
     are gone rather than merely corrected — a fallback here is a second,
     undesigned appearance that only shows up when somebody makes a mistake,
     which is precisely when you want to be told.

     The hover is a step further out and further down, which is what an object
     lifting off paper does. It is its own token rather than a filter tweak
     because a shadow's SPREAD and its COLOUR have to move together on a
     coloured ground, and only the theme knows the colour.

     BOTH TOKENS ARE ONE SHADOW EACH, AND THAT IS A CONSTRAINT RATHER THAN A
     STYLE. filter:drop-shadow() takes a single shadow; hand it the comma-
     separated PAIR that box-shadow accepts — which is the shape the five
     themes had written --orb-shadow in, since nothing was reading it to
     complain — and the whole filter is invalid and the orb loses its shadow
     entirely. So the five were rewritten to one shadow apiece rather than
     merely renamed, and the harness checks the comma count. */
  :root[data-mode="light"] .chat-pill{
    filter: drop-shadow(var(--orb-shadow));
    box-shadow:none;
  }
  :root[data-mode="light"] .chat-pill:hover{
    filter: drop-shadow(var(--orb-shadow-hi));
  }
  /* The halo becomes ambient occlusion instead of a glow: a soft darkening
     UNDER the sphere rather than a coloured bloom around it. */
  /* REDUCED EFFECTS AND REDUCED MOTION LAND ON A POSE, NOT A PAUSE.
     animation-play-state:paused freezes wherever the cycle happened to be,
     which is how you get an orb stopped at 24% opacity looking broken. These
     stop the animation and state the resting appearance instead, so a still
     orb still looks like something somebody drew. */
  /* THE ORB IS NO LONGER PART OF WHAT CHEAP MODE TURNS OFF.
   *
   * "it does not pulse spin move or have any characteristics of life … does
   *  not work in pulse, or any other setting"        (Matt, 26 August.)
   *
   * He was describing a real bug and I spent a while looking past it. The orb
   * DID animate — for about a second. measureFx() runs 2.6s after load, and on
   * any machine that samples slow once it sets data-fx="lite", which used to
   * carry `animation:none` for every layer here. Measured in a real browser:
   * frames 3 through 7 of a capture were pixel-for-pixel identical. One
   * unlucky measurement on one busy afternoon and the orb was dead for good.
   *
   * AND IT WAS THE WRONG THING TO SWITCH OFF ANYWAY. Cheap mode exists to kill
   * BLURS — backdrop-filter over half the screen, which genuinely costs. This
   * is three composited layers on an 88-pixel element animating transform and
   * opacity: no repaint, no filter, no layout. Turning it off saves nothing
   * measurable and removes the one thing in the app that says it is running.
   *
   * The vortex canvas still respects cheap mode, because that one is a real
   * per-frame cost. And reduced motion still stops everything — that is a
   * person asking, not a machine guessing, and the two are not the same. */
  
  /* ---- the other two shapes -------------------------------------------
     Same object, same colour, same breath underneath — one extra layer on
     top. Both are a single rotating or scaling element behind a mask, which
     is one composited layer and no repaint, so neither costs measurably more
     than the halo it sits on. */
  /* CHILD SELECTORS, NOT CLASS SELECTORS, AND THAT IS NOT A STYLE CHOICE.
     `.lorb > span` above sets display:block at specificity (0,1,1). A bare
     `.lorb-storm{display:none}

  /* THE FLOW. Veins TURN, they do not slide: a texture translating across
     a sphere reads as a sheet of paper pulled past it, while rotating
     about the centre reads as the sphere itself turning over. Linear, not
     eased — fire has no beat, and an ease-in-out puts one in.
     26s against 41s, sharing no factor, so the two sets drift in and out
     of phase for ever instead of locking into a pattern. */
  @keyframes stormVeinA{
    from{ transform:rotate(0deg)   scale(1.02); }
    50% { transform:rotate(180deg) scale(1.12); }
    to  { transform:rotate(360deg) scale(1.02); }
  }
  @keyframes stormVeinB{
    from{ transform:rotate(360deg) scale(1.08); }
    50% { transform:rotate(180deg) scale(.98); }
    to  { transform:rotate(0deg)   scale(1.08); }
  }
  @keyframes stormEye{
    0%,100%{ transform:translate(0,0) scale(1); }
    38%    { transform:translate(6%, -4%) scale(1.1); }
    70%    { transform:translate(-5%, 5%) scale(.92); }
  }
  @keyframes stormPool{
    0%,100%{ transform:translate(0,0) scale(1) rotate(0deg); }
    42%    { transform:translate(-9%, -7%) scale(1.14) rotate(16deg); }
    74%    { transform:translate(7%, 4%) scale(.9) rotate(-11deg); }
  }
  /* Reduced motion: the flow stops and the ball stays a ball. Nothing is
     hidden — a still molten orb is still a molten orb, which is more than
     could be said for a frozen blob.
     Same specificity note as liquid: the animated rules end in a class
     (.s1, .b1), so the stop rule has to sit above them, not level. */
  

  /* Nova, Storm, Ribbon and Ring came out on 3 September — "remove opal,
     rose, nova, storm, ribbon, and ring" — in favour of the liquid family
     below. Their CSS went with them rather than staying dead in the file;
     see git history if any of it is ever wanted back.

     THE LIQUID FAMILY'S TWO INPUTS. --lq-1 / --lq-2 are read by
     ORB_TINT_VARS in app.js exactly the way storm's --st-cold2/--st-hot2
     are, so applyOrbTint() derives --orb-accent/--orb-accent-2/--orb-glow/
     --orb-ink from them with no new code. Every liquid-family layer below
     paints itself from those two derived variables.

     THESE ARE NOT INVENTED — they are lifted straight from --c1/--c2 in
     round three of tools/orb-demo/lucid-orb-ideas.html, the file Matt spent
     four rounds narrowing down and then approved ("It was perfect"). That
     demo gives every theme its own real identity: navy-to-periwinkle for
     Dark, near-black to white for Obsidian, dark green to warm gold for
     Regal/Forest, deep mint to bright mint for Ink, brown-to-cream for
     Chocolate/Alabaster/Parchment/Light. What shipped before this instead
     derived every theme's --lq-1/--lq-2 from ONE shared "brass" channel
     (see gen-theme-colors.js) — so ten of eleven themes rendered as
     near-identical gold, and the five liquid styles, which differ only by
     ANIMATION and share this same two-colour pair, all read as one orb.
     That is what "every orb looks the same" meant. --lq-1 is the dark/base
     half (--c1 in the demo), --lq-2 the light/lit half (--c2) — same
     base/lit convention applyOrbTint() already expects. */

/* ===========================================================================
   THE RETIRED GROUNDS, WHICH STILL HAVE TO RENDER.
   ===========================================================================
   None of these can be chosen any more — the picker offers four and
   parseTheme() turns every one of these names into a survivor on read. They
   are here because a row in the database, or a localStorage value on a machine
   that has not been opened since, still carries one, and a theme that renders
   as nothing is a white screen. They came back after the merge of 24 September
   deleted them: the live site had dropped them on 14 September and site.css
   quotes their colours. See THEME_CYCLE and RETIRED_THEMES in app.js. */
  :root[data-theme="frosted"]{
    --bg-mid:#0B1030; --bg-base:#070B22; --bg-deep:#03050F;
    --bloom-1: rgba(61,107,255,0.20); --bloom-2: rgba(160,125,255,0.15);
    --bloom-3: rgba(255,154,68,0.09); --bloom-4: rgba(184,225,74,0.08);
    /* A 10% wash of a LIGHT blue over a 190%-saturated 44px blur made this
       theme's panels far brighter than any other dark theme's — bright enough
       that the shared quiet-text tokens landed at 3.1–3.7:1 on them. Deepening
       the fill rather than lifting the text is the fix that keeps a hierarchy:
       pushing "faint" up to pass here would have made it brighter than "dim". */
    --glass-bg: rgba(22,32,76,0.42);
    --glass-border: rgba(255,255,255,0.16); --glass-border-hi: rgba(255,255,255,0.40);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.3), 0 10px 24px -8px rgba(0,0,0,0.4), 0 32px 64px -18px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.18);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.02) 46%, rgba(255,255,255,0) 100%);
    --glass-blur: 44px; --glass-sat: 190%;
    /* Frosted's panels are a 44px blur at 190% saturation over a 10% fill —
       far brighter than the other dark themes' glass, and the shared faint
       token measured 3.07:1 against them. A brighter ground needs lighter
       lettering; this is the only theme where that is true in this direction. */
    --text-dim:#B6BDDA; --text-faint:#A9B1D2;
    /* THE FIVE, LIFTED FOR THE BRIGHTEST DARK GROUND IN THE WARDROBE.
       This is the same argument as the two text tokens directly above and it
       applies for the same measured reason: a 44px blur at 190% saturation
       over #0B1030 makes every panel here far lighter than any other dark
       theme's, and Dark's five were mixed against #080A1A with none of that
       bloom behind them. They did not fail on the page — they went soft on the
       glass, which is where the rail's counts and the flyout's keylines
       actually sit. Each is lifted roughly a step and pushed a little toward
       the violet the blooms carry, so the set belongs to the light in this
       room rather than surviving it: the tightest measures 6.34:1 on a panel
       here, where Dark's set measured 5.24:1. */
    --area-now:#93A6D6; --area-people:#C88C97; --area-work:#D0AE6C;
    --area-media:#6FAEA6; --area-life:#9AB07E;
    --brass:#D0AE6C; --brass-dim:#96803F;
  }

  :root[data-theme="frosted"]{    --lq-1:26,36,64;   --lq-2:207,224,255; }

  :root[data-theme="forest"]{
    --bg-mid:#0A1310; --bg-base:#070E0B; --bg-deep:#040705;
    --text:#EBF2EA; --text-dim:#A8BCAC; --text-faint:#8CA093;
    --bloom-1: rgba(198,168,103,0.040); --bloom-2: rgba(198,168,103,0.022);
    --bloom-3: rgba(198,168,103,0.022); --bloom-4: rgba(60,110,90,0.018);
    --glass-bg: rgba(20,40,32,0.62);
    --glass-border: rgba(225,255,235,0.07); --glass-border-hi: rgba(225,255,235,0.24);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.62), 0 14px 32px -8px rgba(0,0,0,0.72),
                    0 48px 96px -24px rgba(0,0,0,0.92), inset 0 1px 0 rgba(225,255,235,0.075);
    --glass-sheen: radial-gradient(130% 90% at 16% -14%, rgba(225,255,235,0.08),
                    rgba(225,255,235,0.02) 40%, rgba(255,255,255,0) 74%);
    --glass-blur: 26px; --glass-sat: 140%;
    --brass:#C6A867; --brass-dim:#8A7440; --brass-2:#E5D2A8;
    --rail-ink:#EBF2EA; --rail-ink-2:#9AAC9E; --rail-ink-3:#65786B;
    --area-now:#8BA4C8; --area-people:#C98795; --area-work:#C6A867;
    --area-media:#63A899; --area-life:#9BC17F;
    --blue:#8BA4C8; --blue-2:#BCCBE0; --violet:#AF9CC9; --violet-2:#D4C9E3;
    --teal:#63A899; --teal-2:#A0CCC2; --green:#9BC17F; --green-2:#C5DAB2;
    --pink:#C98795; --pink-2:#E4B8C1; --orange:#C6A867; --orange-2:#E5D2A8;
  }

  :root[data-theme="light"]{
    --bg-mid:#FBF7F0; --bg-base:#F5EFE4; --bg-deep:#EFE7D8;
    --bloom-1: rgba(110,160,255,0.16); --bloom-2: rgba(180,150,255,0.14);
    --bloom-3: rgba(255,180,120,0.13); --bloom-4: rgba(150,210,120,0.14);
    --glass-bg: rgba(255,255,255,0.55);
    --glass-border: rgba(30,24,20,0.08); --glass-border-hi: rgba(30,24,20,0.16);
    --glass-shadow: 0 1px 2px rgba(60,45,80,0.05), 0 6px 14px -5px rgba(60,45,80,0.1), 0 22px 44px -12px rgba(60,45,80,0.16), inset 0 1px 0 rgba(255,255,255,0.75);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.65), rgba(255,255,255,0) 46%);
    --glass-blur: 24px; --glass-sat: 160%;
    --text:#241E30; --text-dim:#655D74; --text-faint:#666275;
    --ring: rgba(35,28,45,0.32); --ring-soft: rgba(35,28,45,0.16);
    --blue:#647653; --blue-2:#C3D6AD; --blue-glow: rgba(195,214,173,0.38);
    --green:#537A2C; --green-2:#B7E08C; --green-glow: rgba(183,224,140,0.35);
    --violet:#7C51DE; --violet-2:#CBB4FF; --violet-glow: rgba(203,180,255,0.32);
    --orange:#A55B14; --orange-2:#FFC98A; --orange-glow: rgba(255,201,138,0.32);
    --pink:#C63C2A; --pink-2:#FFB09E; --pink-glow: rgba(255,176,158,0.32);
    --teal:#0C7B71; --teal-2:#9EE5DC; --teal-glow: rgba(158,229,220,0.35);
    --cat-blue:#3E5FB0; --cat-blue-2:#8FA5D6; --cat-blue-glow: rgba(143,165,214,0.35);
    --cat-green:#4A7D42; --cat-green-2:#9BC08E; --cat-green-glow: rgba(155,192,142,0.32);
    --cat-violet:#6B4FBD; --cat-violet-2:#B39FE0; --cat-violet-glow: rgba(179,159,224,0.3);
    --cat-orange:#A65A1C; --cat-orange-2:#E0A363; --cat-orange-glow: rgba(224,163,99,0.32);
    --cat-red:#A8402E; --cat-red-2:#D6907F; --cat-red-glow: rgba(214,144,127,0.3);
    --cat-teal:#1C7A6E; --cat-teal-2:#7FBBB1; --cat-teal-glow: rgba(127,187,177,0.32);
    --pulse-lo: rgba(195,214,173,0.24); --pulse-hi: rgba(195,214,173,0.42);
    --ink:#031225;
    --neutral-1:#3A3345; --neutral-2:#221C2E; --neutral-ink:#FBF7F0; --neutral-glow: rgba(40,32,50,.14);
    /* Same addition. Light's cards cast a violet grey rather than a brown,
       which is the clearest single example of why the shared fallback had
       to go: it was the wrong hue here and nothing could say so. */
    --orb-halo: rgba(126,112,140,0.20);
    --orb-shadow: 0 9px 18px rgba(60,45,80,0.24);
    --orb-shadow-hi: 0 14px 26px rgba(60,45,80,0.32);
    /* THE FIVE, AND THE WORST NUMBER IN THE FILE BEFORE THIS.
       Light was wearing the shared cream set from :root[data-mode="light"],
       and the rail is not a card: it runs the full height of the window, so it
       crosses the page gradient from --bg-mid at the top all the way down to
       --bg-deep at the bottom. Measured against that bottom, #8E7639 — the
       metal, and therefore Work, and therefore the masthead rule and every
       count that is currently yours — came out at 3.56:1. Parchment's own
       metal holds 4.83:1 in the same place. The gold was not a little weak
       here, it was the weakest thing anywhere in the wardrobe, and it was weak
       on the theme most likely to be somebody's first sight of the app.

       Every one of these five now clears 4.55:1 against the DEEPEST of this
       theme's three grounds rather than against the lightest, which is the
       only honest way to measure a control that runs top to bottom.

       And they are cooler than Parchment's, which is the difference between
       the two themes rather than a preference: Light's ink is #241E30, a
       violet-black, and its ground is a cream with the yellow taken out.
       Parchment's browns on this page look like a smudge left by a warmer
       theme, so Now carries the ink's violet and Work is an olive gold rather
       than the tobacco one two blocks down. */
    --area-now:#4C588F; --area-people:#96435E; --area-work:#7B652E;
    --area-media:#2D7069; --area-life:#536D3C;
    --brass:#7B652E; --brass-dim:#5C4B21;
  }

  :root[data-theme="denim"]{
    /* THE GROUND WENT DOWN TWO AND A HALF POINTS AFTER THE FIRST RENDER, and
       that is the one change in this block that came from looking rather than
       from measuring. At #E7EEFA — 94% lightness, which is already three
       points below anything else in the wardrobe — Mail at 1440 photographed
       as a white app with a blue edge: the cards cover most of the screen, so
       a page you can only see around them is a page nobody sees. 92% falling
       to 81% is a room you are in rather than a tint you can find. */
    --bg-mid:#DDE7F7; --bg-base:#CBD9EF; --bg-deep:#B9CAE4;
    --text:#101827; --text-dim:#3B4761; --text-faint:#44506A;
    --ring: rgba(16,24,39,0.32); --ring-soft: rgba(16,24,39,0.16);
    /* The blooms are the theme's own ink and metal, weak — one lamp, not two,
       which is the rule the matte block above arrived at by looking at a
       screenshot rather than at a diff. */
    --bloom-1: rgba(42,53,120,0.06); --bloom-2: rgba(94,76,30,0.045);
    --bloom-3: rgba(42,53,120,0.035); --bloom-4: rgba(18,88,81,0.028);
    /* AND THE CARD IS NOT WHITE, which is the other half of that finding. The
       first pass used a 72% white fill and the panels came out at #F1F6FE —
       white, in every practical sense, over a page that was nearly white too.
       This fill is a pale blue in its own right, so the card is the theme's
       colour LIFTED rather than the theme's colour covered up. It still reads
       as a card because it is separated the matte way — six points of
       luminance, a hairline and a long soft shadow — rather than by being the
       only uncoloured thing on screen. */
    --glass-bg: rgba(240,245,253,0.88);
    --glass-border: rgba(18,28,52,0.13); --glass-border-hi: rgba(18,28,52,0.24);
    --glass-shadow: 0 1px 2px rgba(30,45,86,0.10), 0 6px 14px -5px rgba(30,45,86,0.14),
                    0 22px 44px -14px rgba(30,45,86,0.24), inset 0 1px 0 rgba(255,255,255,0.85);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.45), rgba(255,255,255,0) 46%);
    --glass-blur: 22px; --glass-sat: 140%;
    --brass:#5E4C1E; --brass-dim:#463919; --brass-2:#8E7436;
    --rail-ink:#101827; --rail-ink-2:#3B4761; --rail-ink-3:#44506A;
    --area-now:#26306E; --area-people:#843546; --area-work:#5E4C1E;
    --area-media:#125851; --area-life:#345824;
    --blue:#26306E; --blue-2:#7C89C4; --violet:#66337A; --violet-2:#AA8CBA;
    --teal:#125851; --teal-2:#6FA69E; --green:#345824; --green-2:#84A874;
    --pink:#843546; --pink-2:#C2818E; --orange:#734211; --orange-2:#B48B52;
    --cat-blue:#26306E; --cat-green:#345824; --cat-violet:#66337A;
    --cat-orange:#734211; --cat-red:#8B2E1C; --cat-teal:#125851;
    --ink:#101827;
    --neutral-1:#2A3348; --neutral-2:#161E2E; --neutral-ink:#F2F6FE;
    --orb-halo: rgba(90,110,160,0.22);
    --orb-shadow: 0 9px 18px rgba(30,45,86,0.30);
    --orb-shadow-hi: 0 14px 26px rgba(30,45,86,0.38);
  }

  :root[data-theme="terracotta"]{
    /* 88% falling to 74%, for the reason given in Denim's block above and one
       more of its own: this is the theme sitting beside Parchment in the
       picker, and at 95% it would have been a fourth cream with a slightly
       different bias. Fourteen points below Parchment's page is what makes it
       a different object rather than a different opinion about cream. */
    --bg-mid:#F2DECD; --bg-base:#E9CDB6; --bg-deep:#DDB99C;
    --text:#26150C; --text-dim:#523A2A; --text-faint:#5B422F;
    --ring: rgba(38,21,12,0.32); --ring-soft: rgba(38,21,12,0.16);
    --bloom-1: rgba(110,65,16,0.06); --bloom-2: rgba(110,65,16,0.04);
    --bloom-3: rgba(40,64,106,0.03); --bloom-4: rgba(15,87,80,0.025);
    /* A lit clay rather than white — same argument as Denim's fill. */
    --glass-bg: rgba(253,244,236,0.88);
    --glass-border: rgba(62,32,16,0.13); --glass-border-hi: rgba(62,32,16,0.24);
    --glass-shadow: 0 1px 2px rgba(102,54,26,0.10), 0 6px 14px -5px rgba(102,54,26,0.15),
                    0 22px 44px -14px rgba(102,54,26,0.26), inset 0 1px 0 rgba(255,255,255,0.82);
    --glass-sheen: linear-gradient(180deg, rgba(255,255,255,0.42), rgba(255,255,255,0) 46%);
    --glass-blur: 22px; --glass-sat: 140%;
    --brass:#6E4110; --brass-dim:#53310C; --brass-2:#A0682D;
    --rail-ink:#26150C; --rail-ink-2:#523A2A; --rail-ink-3:#5B422F;
    --area-now:#28406A; --area-people:#872E43; --area-work:#6E4110;
    --area-media:#0F5750; --area-life:#3A531E;
    --blue:#28406A; --blue-2:#8195B8; --violet:#553880; --violet-2:#9C89BE;
    --teal:#0F5750; --teal-2:#6CA49E; --green:#3A531E; --green-2:#8AA46F;
    --pink:#872E43; --pink-2:#C57C8B; --orange:#7E3D0C; --orange-2:#BC8449;
    --cat-blue:#28406A; --cat-green:#3A531E; --cat-violet:#553880;
    --cat-orange:#7E3D0C; --cat-red:#8E2A20; --cat-teal:#0F5750;
    --ink:#26150C;
    --neutral-1:#3E2A1C; --neutral-2:#26150C; --neutral-ink:#FDF4EC;
    --orb-halo: rgba(170,110,70,0.22);
    --orb-shadow: 0 9px 18px rgba(102,54,26,0.30);
    --orb-shadow-hi: 0 14px 26px rgba(102,54,26,0.38);
  }

  :root{                          --lq-1:16,25,58;   --lq-2:124,156,255; }
  :root[data-theme="midnight"]{   --lq-1:12,20,64;   --lq-2:138,166,255; }
  :root[data-theme="obsidian"]{   --lq-1:20,20,22;   --lq-2:255,255,255; }
  :root[data-theme="regal"]{      --lq-1:14,31,25;   --lq-2:239,214,166; }
  :root[data-theme="chocolate"]{  --lq-1:36,27,18;   --lq-2:240,220,184; }
  :root[data-theme="forest"]{     --lq-1:18,33,26;   --lq-2:229,210,168; }
  
  /* BANKED — INK, IN BLUE, WITH A BANKED COAL.
     Matt, 24 September: "in my mind it was the ink theme reimagined... with
     dark blue and ember instead of black and green", and "I would like banked
     coal if the light was not so bright".
     So this is Ink's own block with two substitutions and nothing else
     invented: the near-black ground becomes a near-black BLUE, and the green
     that was Lucid becomes a coal. Everything that makes Ink feel like Ink —
     matte, the 7% white borders, the barely-lifted card, the almost-absent
     bloom — is carried over untouched. A coal is dim on purpose. If it reads
     as a bright orange it is the wrong colour for this room. */
  :root[data-theme="banked"]{
    --bg-mid:#070A11; --bg-base:#04060C; --bg-deep:#000104;
    --text:#EEF1F5; --text-dim:#AFB8C4; --text-faint:#8892A0;
    --bloom-1: rgba(168,90,50,0.026); --bloom-2: rgba(255,255,255,0.011);
    --bloom-3: rgba(168,90,50,0.015); --bloom-4: rgba(255,255,255,0.011);
    --glass-bg: rgba(17,21,29,0.66);
    --glass-border: rgba(200,216,255,0.07); --glass-border-hi: rgba(210,224,255,0.26);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.7), 0 14px 30px -8px rgba(0,0,0,0.8),
                    0 46px 90px -22px rgba(0,0,0,0.95), inset 0 1px 0 rgba(210,224,255,0.07);
    --glass-sheen: radial-gradient(130% 90% at 14% -12%, rgba(210,224,255,0.07),
                    rgba(210,224,255,0.019) 38%, rgba(255,255,255,0) 72%);
    --glass-blur: 22px; --glass-sat: 120%;
    /* LUCID. Ink puts its one colour here and so does this. Change these
       three and the coal changes everywhere the app opens an edge. */
    --brass:#A85A32; --brass-dim:#6E3A20; --brass-2:#D99A76;
    --th-hot:#D99A76; --th-mid:#A85A32; --th-base:#6E3A20;
    --rail-ink:#EEF1F5; --rail-ink-2:#98A2B0; --rail-ink-3:#616B79;
    /* The five areas, at Ink's own restraint. Media takes the coal, exactly
       as Ink gives Media its green. */
    --area-now:#7C99C4; --area-people:#C2848F; --area-work:#C4A96F;
    --area-media:#A85A32; --area-life:#8DB874;
    --blue:#7C99C4; --blue-2:#B4C4DC; --violet:#A797C4; --violet-2:#CFC6E0;
    --teal:#A85A32; --teal-2:#D99A76; --green:#8DB874; --green-2:#BFD5B0;
    --pink:#C2848F; --pink-2:#E0B6BD; --orange:#A85A32; --orange-2:#D99A76;
  }
  /* COAL — INK, IN BLUE, WITH A BANKED COAL.
     Matt, 24 September: "in my mind it was the ink theme reimagined... with
     dark blue and ember instead of black and green", and "I would like banked
     coal if the light was not so bright".
     So this is Ink's own block with two substitutions and nothing else
     invented: the near-black ground becomes a near-black BLUE, and the green
     that was Lucid becomes a coal. Everything that makes Ink feel like Ink —
     matte, the 7% white borders, the barely-lifted card, the almost-absent
     bloom — is carried over untouched. A coal is dim on purpose. If it reads
     as a bright orange it is the wrong colour for this room. */
  :root[data-theme="coal"]{
    --bg-mid:#080C14; --bg-base:#05080F; --bg-deep:#000205;
    --text:#EEF1F5; --text-dim:#AFB8C4; --text-faint:#8892A0;
    --bloom-1: rgba(190,106,56,0.026); --bloom-2: rgba(255,255,255,0.011);
    --bloom-3: rgba(190,106,56,0.015); --bloom-4: rgba(255,255,255,0.011);
    --glass-bg: rgba(19,24,33,0.66);
    --glass-border: rgba(200,216,255,0.07); --glass-border-hi: rgba(210,224,255,0.26);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.7), 0 14px 30px -8px rgba(0,0,0,0.8),
                    0 46px 90px -22px rgba(0,0,0,0.95), inset 0 1px 0 rgba(210,224,255,0.07);
    --glass-sheen: radial-gradient(130% 90% at 14% -12%, rgba(210,224,255,0.07),
                    rgba(210,224,255,0.019) 38%, rgba(255,255,255,0) 72%);
    --glass-blur: 22px; --glass-sat: 120%;
    /* LUCID. Ink puts its one colour here and so does this. Change these
       three and the coal changes everywhere the app opens an edge. */
    --brass:#BE6A38; --brass-dim:#7C4522; --brass-2:#E3A981;
    --th-hot:#E3A981; --th-mid:#BE6A38; --th-base:#7C4522;
    --rail-ink:#EEF1F5; --rail-ink-2:#98A2B0; --rail-ink-3:#616B79;
    /* The five areas, at Ink's own restraint. Media takes the coal, exactly
       as Ink gives Media its green. */
    --area-now:#7C99C4; --area-people:#C2848F; --area-work:#C4A96F;
    --area-media:#BE6A38; --area-life:#8DB874;
    --blue:#7C99C4; --blue-2:#B4C4DC; --violet:#A797C4; --violet-2:#CFC6E0;
    --teal:#BE6A38; --teal-2:#E3A981; --green:#8DB874; --green-2:#BFD5B0;
    --pink:#C2848F; --pink-2:#E0B6BD; --orange:#BE6A38; --orange-2:#E3A981;
  }
  /* HEARTH — INK, IN BLUE, WITH A BANKED COAL.
     Matt, 24 September: "in my mind it was the ink theme reimagined... with
     dark blue and ember instead of black and green", and "I would like banked
     coal if the light was not so bright".
     So this is Ink's own block with two substitutions and nothing else
     invented: the near-black ground becomes a near-black BLUE, and the green
     that was Lucid becomes a coal. Everything that makes Ink feel like Ink —
     matte, the 7% white borders, the barely-lifted card, the almost-absent
     bloom — is carried over untouched. A coal is dim on purpose. If it reads
     as a bright orange it is the wrong colour for this room. */
  :root[data-theme="hearth"]{
    --bg-mid:#0A0E17; --bg-base:#060A12; --bg-deep:#000307;
    --text:#EEF1F5; --text-dim:#AFB8C4; --text-faint:#8892A0;
    --bloom-1: rgba(206,122,66,0.026); --bloom-2: rgba(255,255,255,0.011);
    --bloom-3: rgba(206,122,66,0.015); --bloom-4: rgba(255,255,255,0.011);
    --glass-bg: rgba(21,27,37,0.66);
    --glass-border: rgba(200,216,255,0.07); --glass-border-hi: rgba(210,224,255,0.26);
    --glass-shadow: 0 1px 2px rgba(0,0,0,0.7), 0 14px 30px -8px rgba(0,0,0,0.8),
                    0 46px 90px -22px rgba(0,0,0,0.95), inset 0 1px 0 rgba(210,224,255,0.07);
    --glass-sheen: radial-gradient(130% 90% at 14% -12%, rgba(210,224,255,0.07),
                    rgba(210,224,255,0.019) 38%, rgba(255,255,255,0) 72%);
    --glass-blur: 22px; --glass-sat: 120%;
    /* LUCID. Ink puts its one colour here and so does this. Change these
       three and the coal changes everywhere the app opens an edge. */
    --brass:#CE7A42; --brass-dim:#8A5128; --brass-2:#ECB78F;
    --th-hot:#ECB78F; --th-mid:#CE7A42; --th-base:#8A5128;
    --rail-ink:#EEF1F5; --rail-ink-2:#98A2B0; --rail-ink-3:#616B79;
    /* The five areas, at Ink's own restraint. Media takes the coal, exactly
       as Ink gives Media its green. */
    --area-now:#7C99C4; --area-people:#C2848F; --area-work:#C4A96F;
    --area-media:#CE7A42; --area-life:#8DB874;
    --blue:#7C99C4; --blue-2:#B4C4DC; --violet:#A797C4; --violet-2:#CFC6E0;
    --teal:#CE7A42; --teal-2:#ECB78F; --green:#8DB874; --green-2:#BFD5B0;
    --pink:#C2848F; --pink-2:#E0B6BD; --orange:#CE7A42; --orange-2:#ECB78F;
  }
:root[data-theme="ink"]{        --lq-1:14,34,20;   --lq-2:155,214,172; }
  :root[data-theme="banked"]{   --lq-1:20,14,10;   --lq-2:217,154,118; }
  :root[data-theme="coal"]{     --lq-1:24,16,11;   --lq-2:227,169,129; }
  :root[data-theme="hearth"]{   --lq-1:28,19,12;   --lq-2:236,183,143; }
  :root[data-theme="alabaster"]{  --lq-1:130,102,44; --lq-2:255,247,232; }
  :root[data-theme="parchment"]{  --lq-1:92,67,24;   --lq-2:216,184,121; }
  :root[data-theme="light"]{      --lq-1:92,75,33;   --lq-2:245,234,210; }
  /* The two coloured papers. Each takes its own ground's hue rather than the
     shared brown the paper themes above use — an orb that reads as Parchment's
     is exactly the "every orb looks the same" complaint this block was written
     to answer, and it would be worse on a theme whose whole point is that its
     page is a colour. Denim is its own ultramarine to its own periwinkle;
     Terracotta is its burnt sienna to a lit clay. */
  :root[data-theme="denim"]{      --lq-1:38,48,110;  --lq-2:170,190,240; }
  :root[data-theme="terracotta"]{ --lq-1:110,65,16;  --lq-2:240,205,170; }
  :root[data-theme="fathom"]{     --lq-1:12,26,56;   --lq-2:150,176,235; }
  /* The intermediate angles are within a degree of proportional (133 of 360
     at 37%, 209 of 360 at 58%), so the TURN is linear and only the swell has
     a shape. Rotation that speeds up and slows down is a hand turning
     something; a cloud has nobody turning it. */
  @keyframes auraDriftC{
    from{ transform:rotate(0deg)   scale(1.03); }
    37% { transform:rotate(133deg) scale(1.09); }
    to  { transform:rotate(360deg) scale(1.03); }
  }
  @keyframes auraDriftD{
    from{ transform:rotate(360deg) scale(1.07); }
    58% { transform:rotate(151deg) scale(1.01); }
    to  { transform:rotate(0deg)   scale(1.07); }
  }

  /* REDUCED MOTION LANDS ON A POSE, NOT A PAUSE — the same rule the resting
     orb already follows twenty lines up, for the same reason: a stopped
     animation freezes wherever the cycle happened to be, and an orb halted at
     55% opacity looks broken rather than still.

     THINKING IS NOT DROPPED HERE, IT IS RESTATED WITHOUT MOTION. Somebody who
     has asked for less movement still needs to know their question is being
     worked on, and taking the only signal away would be worse for them than
     for anyone else. A brighter, wider bloom held steady says the same thing
     the breath says, and says it without moving. */
  

  /* Halo sits behind the orb so it separates from the page without putting a
     hard edge around it — the orb should look like it is floating, not pasted. */
  @media (max-width:640px){
    .chat-pill{ width:66px; height:66px; }
    /* 66px is small enough that the bloom starts to lose the edge — see
       orbEdgeFor(), which is where this number comes from. */
    #orbMain{ --d:66px; --edge:.47; }
  }
  @keyframes auroraShift{ 0%,100%{ background-position:0% 50%; } 50%{ background-position:100% 50%; } }
  /* On a narrow screen the label costs more than it earns — collapse to the
     original circle rather than letting a wide pill crowd the content. */
  @media (max-width:640px){
    .chat-pill{ width:56px; padding:0; }
  }
  @keyframes pillRing{ 0%{ transform:scale(.88); opacity:.5; } 100%{ transform:scale(1.4); opacity:0; } }
  .chat-card{ position:fixed; bottom:26px; right:26px; z-index:20; width:380px; max-width:calc(100vw - 32px); height:560px; max-height:calc(100vh - 52px); display:none; flex-direction:column; border-top:3px solid var(--accent); border-top-style:solid; padding:0; overflow:hidden; }
  .chat-card.open{ display:flex; animation:chatRise var(--dur-base) var(--ease-out); }
  @keyframes chatRise{ from{opacity:0; transform:translateY(14px) scale(.96);} to{opacity:1; transform:none;} }
  /* AND THE WAY OUT, WHICH DID NOT EXIST.
     The card rose on a curve and then vanished on a frame: .open came off and
     display went to none in the same tick. An entrance without an exit is the
     single most common reason an interface feels cheap — the eye is told where
     the thing came from and never told where it went, so it reads as a bug you
     got away with rather than as a window closing.
     .closing keeps the box on screen for one short fall and takes the pointer
     off it immediately, so nothing can be clicked in a card that is leaving.
     It is FASTER than the rise on purpose: arrivals should be watched and
     departures should be got out of the way. See toggleChat() in app.js, which
     owns the timer and which cancels this the instant it is reopened. */
  .chat-card.closing{ display:flex; pointer-events:none;
    animation:chatFall var(--dur-fast) var(--ease-out) both; }
  @keyframes chatFall{ from{opacity:1; transform:none;} to{opacity:0; transform:translateY(10px) scale(.97);} }
  /* Reduced motion gets the state change and not the travel: the card still
     goes, it simply does not slide there. */
  @media (prefers-reduced-motion: reduce){
    .chat-card.open{ animation:none; }
    .chat-card.closing{ animation:chatFadeOut var(--dur-fast) linear both; }
    @keyframes chatFadeOut{ from{opacity:1;} to{opacity:0;} }
  }
  .chat-header{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-4) var(--space-4) var(--space-3); border-bottom:1px solid var(--glass-border); flex:none; }
  /* The orb button hides when the chat opens (toggleChat), so the big sphere
     literally becomes this small one — there are never two on screen. */
  .chat-header-icon{ width:30px; height:30px; border-radius:var(--radius-round); background:transparent; display:flex; align-items:center; justify-content:center; flex:none; position:relative; }
  /* THE HUE-ROTATE IS GONE, 3 September (257). It used to read:
       .brand-mark{ filter: hue-rotate(var(--mark-rotate, 0deg)); }
     so the logo shifted with the ORB. That was written when the orb carried its
     own colour; the orb now takes the theme's, and the mark takes the theme's
     directly from --area-now/-life/-work (see the block by .lm-core-*). Leaving
     the filter on top of that would rotate correct theme colours off to
     something no theme is wearing, which is the bug 257 reported. --mark-rotate
     is still set by applyOrbTint and still used by the orb itself — only the
     logo has stopped listening to it.
     The transition moved onto stroke so a theme change still crossfades. */
  .brand-mark path{ transition: stroke var(--dur-ambient) var(--ease-out); }
  .chat-header-icon svg{ width:14px; height:14px; color:var(--ink); }
  /* flex:1 so the buttons sit at the right-hand end, nowrap so it never
     becomes two lines — see the note on .chat-clear, which is what made that
     possible again. */
  .chat-title{ font-family:var(--font-display); font-weight:600; font-size:var(--type-lead); flex:1; white-space:nowrap; }
  .chat-close{ cursor:pointer; color:var(--text-dim); font-size:18px; width:26px; height:26px; display:flex; align-items:center; justify-content:center; border-radius:var(--radius-control); }
  .chat-close:hover{ background:rgba(120,120,140,.1); color:var(--text); }
  .chat-thread{ flex:1; overflow-y:auto; padding:var(--space-4); display:flex; flex-direction:column; gap:var(--space-3); }
  .tl-row{ display:flex; gap:var(--space-4); padding:var(--space-2) 0 var(--space-2) 0; }
  /* A category with nothing in it is still shown — it is the list of the seven
     kinds an entry can be, and that list is worth reading in the week before
     there is something under every one of them — but it does not compete with
     the ones that have something behind them.

     AND IT IS NOT A CONTROL. Row 2156. The comment that stood here said
     "turning it on is how he finds out it is empty", which was never what the
     page did: every chip starts ON, so a press turns one OFF, and turning off a
     category that holds nothing changes not one mark on the ring, not one card
     in the strip and not one word in the sentence above them. The chip dimmed
     and nothing else moved — a control that looks like it works and does not.
     renderTimeline() now draws this one with no onclick and aria-disabled; the
     pointer comes off here so that it stops inviting a press it no longer
     takes. */
  .chip.is-empty{ opacity:.55; cursor:default; }
  /* A chart in the answer. See renderAskChart(): the numbers are always
     printed underneath as well, so the picture is never the only copy. */
  .ask-chart{ align-self:flex-start; max-width:100%; width:100%; margin:var(--space-hair) 0 var(--space-1);
      padding:var(--space-3) var(--space-3) var(--space-3); border-radius:var(--radius-card);
      background:rgba(120,120,140,.06); border:1px solid var(--glass-border); }
  .ask-chart-t{ font-size:var(--type-body); font-weight:700; color:var(--text); margin-bottom:var(--space-hair); }
  .ask-chart-s{ font-size:var(--type-label); color:var(--text-faint); margin-bottom:var(--space-2); }
  .ask-chart svg{ display:block; width:100%; height:auto; overflow:visible; }
  .ask-chart .axis{ font-size:var(--type-label); fill:var(--text-faint); }
  .ask-chart .vlab{ font-size:var(--type-label); font-weight:700; fill:var(--text-dim); }
  .ask-chart-legend{ display:flex; flex-wrap:wrap; gap:var(--space-3); margin-top:var(--space-2);
      font-size:var(--type-label); color:var(--text-dim); }
  .ask-chart-legend i{ width:9px; height:9px; border-radius:var(--radius-tight); display:inline-block;
      margin-right:var(--space-2); vertical-align:-1px; }
  .ask-chart-num{ font-family:var(--font-display); font-size:var(--type-figure); font-weight:600; color:var(--text);
      line-height:1.1; }
  .ask-chart-table{ margin-top:var(--space-2); }
  .ask-chart-table summary{ font-size:var(--type-label); color:var(--text-faint); cursor:pointer;
      list-style:none; }
  .ask-chart-table summary::-webkit-details-marker{ display:none; }
  .ask-chart-table summary:hover{ color:var(--text-dim); }
  .ask-chart-table table{ width:100%; border-collapse:collapse; margin-top:var(--space-2); font-size:var(--type-label); }
  .ask-chart-table th{ text-align:left; font-weight:700; color:var(--text-faint);
      padding:var(--space-hair) var(--space-2) var(--space-1) 0; }
  .ask-chart-table td{ padding:var(--space-hair) var(--space-2) var(--space-hair) 0; color:var(--text-dim); }
  /* The opening window. Left-aligned and dense on purpose: this is a briefing,
     and a briefing centred like a splash screen reads as decoration. */
  .ask-open{ text-align:left; padding:var(--space-4) var(--space-1) var(--space-2); }
  .ask-open-hello{ font-family:var(--font-display); font-size:var(--type-lead); font-weight:600;
      color:var(--text); margin:0 0 var(--space-hair); }
  .ask-open-sub{ font-size:var(--type-body); color:var(--text-faint); margin:0 0 var(--space-4); }
  .ask-note{ display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-2) var(--space-3); border-radius:var(--radius-card);
      cursor:pointer; border:1px solid transparent; }
  .ask-note:hover{ background:rgba(120,120,140,.09); border-color:var(--glass-border); }
  .ask-note-mark{ width:3px; align-self:stretch; border-radius:var(--radius-tight); flex:none;
      background:color-mix(in srgb, var(--accent) 70%, transparent); }
  .ask-note.hot .ask-note-mark{ background:var(--pink); }
  .ask-note-body{ min-width:0; flex:1; }
  .ask-note-t{ font-size:var(--type-body); font-weight:600; color:var(--text); overflow:hidden;
      text-overflow:ellipsis; white-space:nowrap; }
  .ask-note-w{ font-size:var(--type-body); color:var(--text-dim); margin-top:var(--space-hair); }
  .ask-note-go{ font-size:var(--type-label); color:var(--text-faint); flex:none; align-self:center; }
  /* WAVE IT OFF. Matt, 26 Aug: "I opened it 4 times and it recommends
     something I dont want to look at". Present at all times rather than on
     hover — hover does not exist on a phone, and a control only a mouse can
     find is a control half his devices do not have. Quiet until the row is
     under the cursor, then legible. text-dim, not text-faint: the readability
     harness measures every painted glyph against 4.5:1. */
  .ask-note-x{ flex:none; width:22px; height:22px; padding:0; margin-left:var(--space-hair);
    display:flex; align-items:center; justify-content:center;
    background:transparent; border:none; border-radius:var(--radius-control); cursor:pointer;
    font-size:var(--type-lead); line-height:1; color:var(--text-dim); }
  .ask-note:hover .ask-note-x{ color:var(--text); background:rgba(120,120,140,.14); }
  .ask-note-x:hover{ color:var(--text) !important; background:rgba(120,120,140,.22) !important; }
  .ask-note-x:focus-visible{ outline:2px solid var(--ring); outline-offset:1px; }
  .ask-note:hover .ask-note-go{ color:var(--accent); }
  .ask-open-label{ font-size:var(--type-label); font-weight:700; letter-spacing:.06em; text-transform:uppercase;
      color:var(--text-faint); margin:var(--space-4) 0 var(--space-2); }
  .chat-empty{ text-align:center; padding:var(--space-6) var(--space-3) var(--space-3); }
  .chat-empty-icon{ font-size:var(--type-page); margin-bottom:var(--space-3); color:var(--blue); }
  /* On cream paper this measured 1.7:1 — a decorative sparkle, but the harness
     counts every glyph as text and it was right to: a mark that faint reads as
     a smudge rather than as anything. The accent it inherits is tunable by the
     orb sliders, so no accent value can be trusted here; on light papers it
     wears the ordinary dim-text token instead, which is the one colour on the
     page that has already been measured against every background. */
  :root[data-mode="light"] .chat-empty-icon{ color:var(--text-dim) !important; }
  .chat-empty-title{ font-size:var(--type-body); color:var(--text-dim); margin-bottom:var(--space-5); }
  .chat-suggestions{ display:flex; flex-direction:column; gap:var(--space-2); }
  .chat-chip{ padding:var(--space-3) var(--space-4); border-radius:var(--radius-card); border:1px solid var(--glass-border-hi); font-size:var(--type-body); text-align:left; cursor:pointer; background:rgba(120,120,140,.05); }
  .chat-chip:hover{ background:rgba(120,120,140,.1); border-color:color-mix(in srgb, var(--accent) 40%, transparent); }
  .msg{ max-width:85%; padding:var(--space-3) var(--space-4); font-size:var(--type-body); line-height:1.55; white-space:pre-line; }
  .msg-user{ align-self:flex-end; background:linear-gradient(135deg,var(--accent),var(--accent-2)); color:var(--ink); border-radius:var(--radius-card) var(--radius-card) var(--radius-tight) var(--radius-card); font-weight:500; }
  /* WHERE THIS CAME FROM. An assistant that reads your mail and then answers
     with no visible provenance is asking to be believed. These are the actual
     records the answer was built from, and each one opens the thing itself. */
  .ask-src{ align-self:flex-start; max-width:100%; display:flex; flex-direction:column;
      gap:var(--space-2); margin:calc(var(--space-hair) * -1) 0 var(--space-hair); align-items:flex-start; }
  .ask-src-label{ font-size:var(--type-label); font-weight:700; letter-spacing:.05em; text-transform:uppercase;
      color:var(--text-faint); margin-right:var(--space-hair); align-self:center; }

  /* ROLLED UP BY DEFAULT. Provenance is something to check occasionally, not to
     read every time — so it is one quiet line that opens on click. */
  .ask-src-toggle{ display:inline-flex; align-items:center; gap:var(--space-2); padding:var(--space-hair) var(--space-1) var(--space-hair) var(--space-hair);
      background:none; border:0; cursor:pointer; font:inherit; font-size:var(--type-label); font-weight:700;
      letter-spacing:.05em; text-transform:uppercase; color:var(--text-faint); border-radius:var(--radius-control); }
  .ask-src-toggle:hover{ color:var(--text-dim); }
  /* "This was wrong" under a recorded answer (row 1924). Quiet on purpose: it is
     for the answer that needs it, not a verdict every answer asks for. */
  .ask-wrong{ align-self:flex-start; display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-2);
      margin:0 0 var(--space-1); font-size:var(--type-label); color:var(--text-faint); }
  .ask-wrong-btn{ background:none; border:0; padding:var(--space-hair) var(--space-1); cursor:pointer; font:inherit;
      color:var(--text-faint); text-decoration:underline; text-underline-offset:2px; border-radius:var(--radius-control); }
  .ask-wrong-btn:hover{ color:var(--text-dim); }
  .ask-wrong-bad{ color:var(--pink); }
  @media (pointer:coarse){ .ask-wrong-btn{ min-height:44px; } }
  .ask-src-chev{ display:inline-block; font-size:var(--type-body); line-height:1; transition:transform var(--dur-fast) var(--ease-out); }
  .ask-src-toggle.open .ask-src-chev{ transform:rotate(90deg); }
  .ask-src-list{ display:flex; flex-wrap:wrap; gap:var(--space-2); align-items:center; }
  .ask-src-list[hidden]{ display:none; }

  .ask-src-chip{ display:inline-flex; align-items:baseline; gap:var(--space-2); max-width:320px;
      padding:var(--space-1) var(--space-2); border-radius:var(--radius-control); font-size:var(--type-body); cursor:default;
      background:rgba(120,120,140,.10); border:1px solid var(--glass-border); color:var(--text-dim); }
  .ask-src-chip.can-open{ cursor:pointer; }
  .ask-src-chip.can-open:hover{ color:var(--text); border-color:var(--glass-border-hi);
      background:rgba(120,120,140,.16); }
  .ask-src-kind{ font-size:var(--type-label); font-weight:800; letter-spacing:.06em; text-transform:uppercase;
      color:var(--src-hue, var(--accent)); flex:none; }
  .ask-src-t{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

  /* TEN KINDS, TEN HUES.
     Every chip painted its kind in var(--accent), so a person, an email and a
     calendar entry were three identical blue words and the only way to tell
     them apart was to read them. Each kind now sets one variable, and the
     chip's label, border and tint all follow it — the colour does the work at a
     glance and the text confirms it.

     The values are the THEME's own hue variables, never literals: Serene, the
     dark papers and every other mode redefine --blue and --teal for themselves,
     so this table follows the paper instead of fighting it. */
  .ask-src-chip[data-kind="mail"]    { --src-hue:var(--blue); }
  .ask-src-chip[data-kind="person"]  { --src-hue:var(--teal); }
  .ask-src-chip[data-kind="event"]   { --src-hue:var(--green); }
  .ask-src-chip[data-kind="task"]    { --src-hue:var(--orange); }
  .ask-src-chip[data-kind="note"]    { --src-hue:var(--pink); }
  .ask-src-chip[data-kind="project"] { --src-hue:var(--violet); }
  .ask-src-chip[data-kind="vault"]   { --src-hue:var(--orange-2); }
  .ask-src-chip[data-kind="reading"] { --src-hue:var(--green-2); }
  .ask-src-chip[data-kind="web"]     { --src-hue:var(--teal-2); }
  .ask-src-chip[data-kind="life"]    { --src-hue:var(--violet-2); }
  .ask-src-chip[data-kind="text"]    { --src-hue:var(--pink-2); }
  .ask-src-chip[data-kind]{ border-color:color-mix(in srgb, var(--src-hue) 32%, transparent);
      background:color-mix(in srgb, var(--src-hue) 9%, transparent); }
  .ask-src-chip[data-kind].can-open:hover{
      border-color:color-mix(in srgb, var(--src-hue) 55%, transparent);
      background:color-mix(in srgb, var(--src-hue) 16%, transparent); }
  /* ===================================================================
     WHEN LUCID IS ABOUT TO DO SOMETHING — the amber and red cards.
     ===================================================================
     Green never draws one of these: it has already happened and it is
     announced on the ordinary Undo toast, which is where every other
     reversible thing in Lucid is already announced.

     These two are the ones that have NOT happened, and the card's whole job is
     to make that obvious before he presses anything. So it is deliberately not
     a message bubble: full width, square-ish, a colour bar down the side, and a
     heading whose first words are "nothing has been scheduled" or "nothing has
     been sent". A confirmation that looks like part of the conversation is a
     confirmation that gets skimmed.

     The hue comes from --act-hue, set per lane below, and both values are the
     THEME's own variables rather than literals — there are ten grounds now
     (row 2136 retired six names) and tests/shots-the-failure-card.js reads
     every one of them. */
  .ask-act{ align-self:stretch; max-width:100%; margin:var(--space-1) 0 var(--space-hair);
      border:1px solid color-mix(in srgb, var(--act-hue) 34%, var(--glass-border));
      border-left:3px solid var(--act-hue);
      background:color-mix(in srgb, var(--act-hue) 7%, transparent);
      border-radius:var(--radius-card); padding:var(--space-3) var(--space-3); font-size:var(--type-body); line-height:1.5; }
  .ask-act-amber{ --act-hue:var(--orange); }
  .ask-act-red{ --act-hue:var(--pink); }
  /* GREEN IS A RECEIPT, NOT A DECISION, so it is drawn quieter than the other
     two: no tint behind it, just the bar and the sentence. It exists because a
     toast lasts twelve seconds and "undoable in one press" has to outlive a
     four-round question — see askActGreenCard(). */
  .ask-act-green{ --act-hue:var(--green); background:none; }
  .ask-act-said{ color:var(--text); }
  /* THE LANE'S HUE AS INK IS NOT THE SAME DECISION AS THE LANE'S HUE AS A
     RULE, and on paper it is the difference between a heading and a stain.
     Row 268, 15 September: the failure card was photographed on all ten
     surviving grounds for the first time, and the two coloured lines on it —
     this heading and the warning box below — came back UNDER WCAG AA on all
     five of the light ones. "NOTHING WAS CHANGED" measured 4.02:1 on Serene
     blush against the 4.5 floor, and the warning that carries the real reason
     measured 3.50:1. Both are legible enough that nobody looking at the
     screenshot would call them broken, which is exactly why this had gone
     eleven days without being caught: it is the kind of wrong that costs a
     reader with tired eyes and nobody else.

     The hue is kept and the ink is darkened toward the theme's own --text,
     which is the one colour on every paper guaranteed to read on it. 80% and
     65% are not chosen for looks: 85 and 70 are the worst case each line
     needs to clear 4.5 (Serene blush both times), and these sit one step
     inside that so a paper tuned slightly darker tomorrow does not quietly
     drop back under. The dark grounds are left alone — they measured 6.3:1 to
     18:1 and mixing ink into them would take contrast AWAY. */
  .ask-act-title{ font-size:var(--type-label); font-weight:800; letter-spacing:.06em; text-transform:uppercase;
      color:var(--act-ink, var(--act-hue)); margin-bottom:var(--space-2); }
  :root[data-mode="light"] .ask-act{ --act-ink:color-mix(in srgb, var(--act-hue) 80%, var(--text)); }
  /* THE SPECIFICS, NOT "PROCEED?". A two-column list because he is checking
     four separate facts — who, when, where, which mailbox — and a sentence
     hides all four in the middle of itself. */
  .ask-act-rows{ display:grid; grid-template-columns:auto 1fr; gap:var(--space-1) var(--space-3); margin:0 0 var(--space-2); }
  .ask-act-rows dt{ font-size:var(--type-label); font-weight:700; letter-spacing:.04em; text-transform:uppercase;
      color:var(--text-faint); padding-top:var(--space-hair); }
  .ask-act-rows dd{ margin:0; color:var(--text); overflow-wrap:anywhere; }
  .ask-act-warn{ margin:0 0 var(--space-2); padding:var(--space-2) var(--space-2); border-radius:var(--radius-control); font-size:var(--type-body);
      color:var(--orange-text); background:color-mix(in srgb, var(--orange) 12%, transparent);
      border:1px solid color-mix(in srgb, var(--orange) 30%, transparent); }
  /* See the note by .ask-act-title. --orange-text is already the light-mode
     branch of that token pair — on paper it resolves to plain --orange, which
     is a 3.50:1 amber on cream. This is the same correction, on the line that
     carries the actual reason a write failed, which is the one line on this
     card nobody can afford to skim. */
  :root[data-mode="light"] .ask-act-warn{ color:color-mix(in srgb, var(--orange-text) 65%, var(--text)); }
  /* The draft itself, and the invitation body. Scrollable rather than clipped:
     a truncated draft is one he approves without having read the end of. */
  .ask-act-body{ white-space:pre-wrap; color:var(--text-dim); font-size:var(--type-body);
      max-height:220px; overflow:auto; padding:var(--space-2) var(--space-3); border-radius:var(--radius-control);
      background:rgba(120,120,140,.07); border:1px solid var(--glass-border); }
  /* BEFORE AND AFTER, on the amber card where the press destroys something.
     The old text carries the lane's rule down its edge and the new text does
     not, so the eye can tell at a glance which of the two paragraphs is the one
     about to stop existing without having to read either. Both scroll rather
     than clip, for the same reason the draft body does above: a truncated
     paragraph is one he approves without having read the end of. */
  .ask-act-diff{ margin:0 0 var(--space-2); }
  .ask-act-diff-head{ font-size:var(--type-label); font-weight:700; letter-spacing:.04em; text-transform:uppercase;
      color:var(--text-dim); margin:0 0 var(--space-1); }
  .ask-act-diff .ask-act-now{ margin-top:var(--space-2); }
  .ask-act-was, .ask-act-now{ white-space:pre-wrap; font-size:var(--type-body); max-height:200px; overflow:auto;
      padding:var(--space-2) var(--space-3); border-radius:var(--radius-control); background:rgba(120,120,140,.07);
      border:1px solid var(--glass-border); }
  .ask-act-was{ color:var(--text-dim); border-left:3px solid var(--act-hue); }
  .ask-act-now{ color:var(--text); }
  .ask-act-buttons{ display:flex; gap:var(--space-2); align-items:center; margin-top:var(--space-3); flex-wrap:wrap; }
  .ask-act-go{ font:inherit; font-size:var(--type-body); font-weight:600; padding:var(--space-2) var(--space-4); border-radius:var(--radius-pill);
      cursor:pointer; color:var(--ink); border:1px solid transparent; background:var(--act-hue); }
  .ask-act-go:hover{ filter:brightness(1.08); }
  .ask-act-go:disabled{ cursor:default; filter:none; opacity:.65; }
  .ask-act-go:focus-visible, .ask-act-no:focus-visible{ outline:2px solid var(--ring); outline-offset:2px; }
  .ask-act-no{ font:inherit; font-size:var(--type-body); font-weight:600; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill);
      cursor:pointer; background:none; color:var(--text-dim); border:1px solid var(--glass-border-hi); }
  .ask-act-no:hover{ color:var(--text); border-color:var(--text-faint); }
  /* Once it has been taken the card stays, faded, as the record of what he
     approved. Removing it would leave the sentence above referring to a card
     that is no longer on screen. */
  .ask-act-taken{ opacity:.55; }
  .ask-act-taken .ask-act-no{ display:none; }
  .msg-assistant{ align-self:flex-start; background:rgba(120,120,140,.08); border:1px solid var(--glass-border); border-radius:var(--radius-card) var(--radius-card) var(--radius-card) var(--radius-tight); color:var(--text); }
  .msg-action{ align-self:flex-start; font-size:var(--type-body); color:var(--blue); margin-top:calc(var(--space-hair) * -1); cursor:pointer; padding-left:var(--space-hair); font-weight:600; }
  .msg-action:hover{ text-decoration:underline; }
  .typing{ align-self:flex-start; display:flex; align-items:center; gap:var(--space-1); padding:var(--space-3) var(--space-4); background:rgba(120,120,140,.08); border-radius:var(--radius-card) var(--radius-card) var(--radius-card) var(--radius-tight); }
  /* THE WAY OUT, WHERE THE EYE ALREADY IS.
     "eed to be able to cancel a lucid ask." A Stop button anywhere else on the
     screen is a Stop button nobody finds while they are watching the dots,
     which is the only moment it is wanted. Deliberately not a big red control —
     stopping is an ordinary thing to want, not an emergency. */
  .typing-stop{ margin-left:var(--space-2); background:none; border:1px solid var(--glass-border-hi);
    color:var(--text-dim); font:inherit; font-size:var(--type-label); font-weight:600;
    padding:var(--space-hair) var(--space-2); border-radius:var(--radius-pill); cursor:pointer; line-height:1.7; }
  .typing-stop:hover{ color:var(--text); border-color:var(--text-faint); }
  .typing-stop:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
  .typing i{ width:6px; height:6px; border-radius:var(--radius-round); background:var(--text-faint); display:block; animation:typingDot 1.2s infinite; }
  .typing i:nth-child(2){ animation-delay:.15s; } .typing i:nth-child(3){ animation-delay:.3s; }
  @keyframes typingDot{ 0%,60%,100%{ opacity:.3; transform:translateY(0);} 30%{ opacity:1; transform:translateY(-3px);} }
  /* Ask Lucid, now that it does something.
     .ask-status is the "Searching your mail…" line that sits above the typing
     dots — the browser drives the tool loop, so it knows what is happening and
     there is no reason to make you guess. */
  .ask-free{ font-size:var(--type-body); color:var(--text-faint); padding:var(--space-hair) var(--space-1) var(--space-2); }
  .ask-free .mini-link{ text-decoration:underline; }
  .ask-status{ font-size:var(--type-body); color:var(--text-faint); padding:var(--space-hair) var(--space-1) var(--space-2); font-style:italic;
    animation:askPulse 1.6s var(--ease-in-out) infinite; }
  @keyframes askPulse{ 0%,100%{ opacity:.55; } 50%{ opacity:1; } }
  @media (prefers-reduced-motion: reduce){ .ask-status{ animation:none; opacity:.8; } }
  /* Month-to-date spend, quietly visible. Matt pays per message now, and
     finding out at the end of the month is how people get surprised. */
  .chat-cost{ font-size:var(--type-label); color:var(--text-faint); text-align:right; padding:0 var(--space-4) var(--space-2); min-height:14px; }
  /* THREE OF THESE NOW FIT WHERE TWO DID — "Changes" arrived on 4 September
     and the header stopped fitting on one line: "Ask Lucid" wrapped to two,
     and the whole row overflowed by 15px at the card's own width and by more
     than that on a phone, where the card is 14px from each edge.
     Two things were paying for it twice. Every button had margin-right:10px
     ON TOP OF the header's own gap:10, so there were 30px of spacing nobody
     had asked for; and the padding was set for a row with room in it. The
     margin is gone (the gap was always doing that job), the padding and the
     type are a notch tighter, and nothing in here wraps — a title that goes
     to two lines makes the whole panel look broken before it has said
     anything. Measured at 380px and at a 375px phone. */
  .chat-clear{ font-size:var(--type-label); color:var(--text-faint); cursor:pointer; margin-left:auto;
    padding:var(--space-1) var(--space-2); border-radius:var(--radius-control); border:1px solid var(--glass-border); white-space:nowrap; }
  .chat-clear:hover{ color:var(--text); }
  /* The drawer of earlier conversations. Sits over the top of the thread
     rather than replacing it, so pressing Earlier and changing your mind
     costs nothing. */
  .ask-past{ border-bottom:1px solid var(--glass-border); max-height:230px; overflow-y:auto;
    padding:var(--space-2) var(--space-4) var(--space-3); }
  .ask-past-head{ display:flex; align-items:center; gap:var(--space-3); font-size:var(--type-label); font-weight:700;
    letter-spacing:.1em; text-transform:uppercase; color:var(--text-dim); padding:var(--space-1) 0 var(--space-2); }
  .ask-past-head span:first-child{ flex:1; }
  .ask-past-row{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) 0; font-size:var(--type-body);
    border-bottom:1px solid var(--glass-border); cursor:pointer; }
  .ask-past-row:last-child{ border-bottom:none; }
  .ask-past-row:hover .ask-past-title{ color:var(--blue); }
  .ask-past-title{ flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .ask-past-when{ flex:none; font-size:var(--type-label); color:var(--text-dim); }
  /* THE RECORD OF WHAT LUCID CHANGED. Same drawer as the one above it, and
     deliberately a different row: a conversation is a title you click into,
     this is a sentence you read. Nothing here is clickable and nothing here
     has a delete — a record with a tidy-up button on it is a record that gets
     tidied away, which is the one thing this list must not be. The lane's
     colour is the only decoration, because "it drafted" against "it changed"
     is the distinction worth seeing from across the card. */
  .ask-log-row{ display:flex; align-items:flex-start; gap:var(--space-2); padding:var(--space-2) 0;
    border-bottom:1px solid var(--glass-border); font-size:var(--type-body); }
  .ask-log-row:last-child{ border-bottom:none; }
  .ask-log-dot{ flex:none; width:7px; height:7px; border-radius:var(--radius-round); margin-top:var(--space-2); }
  .ask-log-body{ flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--space-hair); }
  /* Wrapped rather than clipped. The sentence naming which task moved to when
     IS the row, and half of it is not a record. */
  .ask-log-did{ color:var(--text); overflow-wrap:anywhere; }
  .ask-log-meta{ font-size:var(--type-label); color:var(--text-dim); }
  /* THIS WEEK, MARKED (D3). A plain block at the head of the record, not a
     card of its own: the verdict in body type, a rule on its left in the
     colour of the news, then one line per kind. The bad answer is drawn in
     --orange and is not made smaller than the good one. */
  .ask-marks{ border-left:2px solid var(--glass-border); padding:var(--space-1) 0 var(--space-2) var(--space-3);
    margin:0 0 var(--space-3); display:flex; flex-direction:column; gap:var(--space-2); }
  .ask-marks[data-tone="bad"]{ border-left-color:var(--orange); }
  .ask-marks[data-tone="good"]{ border-left-color:var(--green); }
  .ask-marks[data-tone="failed"]{ border-left-color:var(--pink); }
  .ask-marks-head{ font-size:var(--type-label); font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--text-dim); }
  .ask-marks-verdict{ font-size:var(--type-body); color:var(--text); font-weight:600; overflow-wrap:anywhere; }
  .ask-marks-row{ display:flex; flex-direction:column; gap:var(--space-hair); font-size:var(--type-body); }
  .ask-marks-kind{ color:var(--text-dim); font-size:var(--type-label); }
  .ask-marks-said{ color:var(--text); overflow-wrap:anywhere; }
  .ask-marks-limit{ font-size:var(--type-label); color:var(--text-faint); overflow-wrap:anywhere; }
  .ask-marks-more{ display:flex; flex-direction:column; gap:var(--space-2); }
  .ask-marks-more > summary{ cursor:pointer; font-size:var(--type-label); color:var(--text-dim); }
  .ask-marks-more[open] > summary{ margin-bottom:var(--space-2); }
  .ask-marks-more > .ask-marks-row, .ask-marks-more > .ask-marks-limit{ margin-bottom:var(--space-2); }
  .chat-input:disabled{ opacity:.5; }
  .msg-assistant b{ font-weight:700; }
  .msg-assistant code{ font-family:var(--font-mono); font-size:var(--type-body);
    background:color-mix(in srgb, var(--text) 8%, transparent); padding:1px var(--space-2); border-radius:var(--radius-control); }
  .chat-input-row{ display:flex; gap:var(--space-2); padding:var(--space-3) var(--space-4); border-top:1px solid var(--glass-border); flex:none; }
  .chat-input{ flex:1; background:rgba(120,120,140,.08); border:1px solid var(--glass-border); border-radius:var(--radius-pill); padding:var(--space-2) var(--space-4); color:var(--text); font-family:var(--font-body); font-size:var(--type-body); }
  .chat-input:focus{ outline:none; border-color:color-mix(in srgb, var(--blue) 50%, transparent); }
  .chat-send{ width:36px; height:36px; border-radius:var(--radius-round); background:linear-gradient(135deg,var(--accent),var(--accent-2)); color:var(--ink); border:none; cursor:pointer; flex:none; font-weight:800; font-size:var(--type-lead); }

  /* Both hidden by default and revealed by the phone breakpoint below — never
     the other way round. A sheet that is display:none only inside a media query
     is a sheet sitting in the middle of the desktop layout at every other
     width. */
  .bottom-nav{ display:none; }
  .bn-sheet{ display:none; }
  /* The big Ask Lucid button is the same story: a phone control, hidden at
     every other width by default rather than inside a media query, so it is
     never a stray bar across the middle of the Mac layout. What it is and why
     it is a bar is by .ask-big in the phone block near the end of this file. */
  .ask-big{ display:none; }
  /* The breakpoint that used to be here is gone — .profile-grid is now
     container-driven and does not need one. See the note there. */
  @media (max-width: 780px){
    /* THE BOTTOM NAV'S OWN HEIGHT, for the page above it to clear. It is the
       bar's --space-2 of padding, .bn-item's 48px target floor and the 10px it
       sits back from the chin — the three numbers in the .bottom-nav rule a few
       hundred lines below, restated where the page can read them. Inside the
       breakpoint because the bar does not exist at any other width. */
    :root{ --bottom-nav-h: calc(var(--space-2) + 48px + 10px); }
    /* AND THE BUTTON'S, FOR THE SAME REASON, DECLARED IN THE SAME PLACE.
       The big Ask Lucid button sits on top of that bar, so every floor below
       that clears the bar has to clear the button too — and it does it by
       adding this name rather than by anybody doing the arithmetic again in
       their head. 56px is above Apple's 44pt floor with room for the mark and
       the words at --type-lead. The rules that draw it are in the phone block
       at the end of this file, beside the rest of the thread's phone rules. */
    /* --ask-big-h retired 5 October: THE BIG BUTTON IS GONE. */
    .sidebar{ display:none; }
    .app{ flex-direction:column; }
    /* 16px rather than 64px: the 64 was reserved for .top-search-row, and that
       box went back to the spine on 11 September (1726). On a phone the spine
       does not exist at all, so search is the first row of the More sheet —
       see the note on it in index.html.
       THE FLOOR IS THE BOTTOM NAV'S HEIGHT AND A STEP OF AIR, not the 90px it
       used to say — which was the same arithmetic done once in somebody's head
       and then left as a number that could not be checked. The nav is 8px of
       padding, a 48px target and 10px back. Set on the variable rather than the
       property so the leaving view keeps reading the same three numbers. */
    /* The band comes to the phone too, off the same variable, and it is
       smaller here only because --th-mt is 13px below 780 rather than 15. */
    .main{ flex:1; order:1;
      --main-pad-top: var(--lth-band);
      --main-pad-x: var(--space-4);
      --main-pad-bottom: calc(var(--bottom-nav-h) + var(--space-6));
      padding: var(--lth-band) var(--main-pad-x) var(--main-pad-bottom); }
    .inbox-wrap{ height:auto; }
    .mail-list-col{ width:100%; }
    /* THE LAST ROWS WERE UNDERNEATH THE NAVIGATION BAR — 16 SEPTEMBER.
       Measured at 430x900: the list ends at y=844 and .bottom-nav starts at
       y=828, so sixteen pixels of the list were behind it — and "That's the
       whole folder", which ends at 832, was sitting four pixels under the bar
       with its descenders cut off. A list whose end you cannot see is a list
       you do not believe you have reached the end of.
       --bottom-nav-h is declared at the top of this breakpoint for exactly
       this, and the mail list is the one scroller that never used it. The
       padding goes on the SCROLLER rather than the column so the bar is
       cleared at the bottom of the scroll rather than cropping the whole
       column short. */
    .mail-list{ padding-bottom:calc(var(--bottom-nav-h) + var(--space-4)); }
    /* THE PHONE SHAPE IS NOW THE SHAPE, AND ALL THAT IS LEFT HERE IS A FINGER.
       Row 1725 turned this rail through ninety degrees below 780px, and for
       four days the phone was the only width where it was a row. Row 2166 makes
       that the shape at every width, so the direction, the position, the
       hairline and the bar all come from the desktop rules now and the phone
       states one thing: the squares are bigger, because a finger is pressing
       them and a mouse is not.

       SEVEN BUTTONS ACROSS A PHONE, AND THE ARITHMETIC IS TIGHT ENOUGH TO SAY
       OUT LOUD. At 390px the tray is the window less .main's gutters — 358px —
       and inside this row's own gutters that is 334. Seven 44px squares and the
       hairline between them come to 325, so they fit with nine pixels over. 44
       is Apple's own minimum; 46, which this rail used when it was a column,
       comes to 339 and puts the last folder off the edge — which
       tests/test-the-folder-rail.js catches, because it requires every button
       to be inside the window AND still big enough for a finger. flex-wrap is
       the safety valve underneath that, inherited from the desktop rule: at
       320px seven finger-sized targets genuinely do not fit on one line, and a
       second line is the honest answer where squeezing them to 35px is not. */
    .rail-btn{ width:44px; height:44px; }
    .rail-ico{ width:23px; height:23px; }
    /* Reading a message is a whole screen at this width, so the tray goes — and
       the folder row goes with it, because it lives inside the tray now. There
       used to be a second rule here hiding .folder-rail by name; it was deleted
       rather than kept, because a rule that hides something already hidden is a
       rule somebody has to read twice to be sure of. */
    .inbox-wrap.reading .mail-list-col{ display:none; }
    .inbox-wrap:not(.reading) .reading-pane{ display:none; }

    .inbox-wrap.spine-open .mail-list-col{ width:100%; }
    .inbox-wrap{ position:relative; }
    .profile-header{ flex-direction:column; text-align:center; }
    .bottom-nav{ display:flex; order:2; position:fixed; bottom:0; left:0; right:0; background:color-mix(in srgb, var(--bg-base) 85%, transparent); backdrop-filter:blur(16px); border-top:1px solid var(--glass-border); padding:var(--space-2) 0 max(10px, env(safe-area-inset-bottom)); z-index:10; }
    /* THE TARGET IS THE ITEM, NOT THE TEXT.
       The padding used to sit on the bar, so each item was exactly as tall as
       its own 11px line — a twelve-pixel target under a finger. Moving the
       padding onto the item and setting a floor is the whole fix, and 48px is
       Google's number; Apple's 44pt lands inside it. */
    .bn-item{ flex:1; text-align:center; font-size:var(--type-label); color:var(--text-dim);
      display:flex; flex-direction:column; align-items:center; justify-content:center;
      gap:var(--space-1); cursor:pointer; min-height:48px; padding:var(--space-2) var(--space-hair); border-radius:var(--radius-card);
      -webkit-tap-highlight-color:transparent; }
    .bn-item:active{ background:rgba(120,120,140,.16); }
    .bn-item.active{ color:var(--blue); }
    .bn-icon{ width:21px; height:21px; flex:none; }

    /* THE OTHER SIX. A sheet from the bottom, because that is the one place a
       thumb reaches on a phone held in one hand. */
    .bn-sheet{ position:fixed; inset:0; z-index:60; display:none;
      align-items:flex-end; background:rgba(0,0,0,.42); backdrop-filter:blur(3px); }
    .bn-sheet.open{ display:flex; }
    /* A CAPSULE RADIUS ON A FULL-WIDTH SHEET IS A DOME — 23 September.

       999px is --radius-pill, which means "however tall this is, halved". On
       a 430-wide sheet the browser scales it to fit and the top corners come
       out at 215px each: a half-circle, and the card's fill really was
       clipped to it. Nobody could see it, because the fill is 94% of the
       page's own colour and there was no edge to give the shape away — the
       Lucid rim is the first thing ever to draw this sheet's outline, and it
       drew a dome. --radius-card is what tokens.css names for sheets. */
    .bn-sheet-card{ width:100%; border-radius:var(--radius-card) var(--radius-card) 0 0; padding:var(--space-2) var(--space-4) max(18px, env(safe-area-inset-bottom));
      background:color-mix(in srgb, var(--bg-base) 94%, transparent);
      border-top:1px solid var(--glass-border);
      animation:bnSheetUp var(--dur-base) var(--ease-out) both; }
    @keyframes bnSheetUp{ from{ transform:translateY(18px); opacity:0; } to{ transform:none; opacity:1; } }
    .bn-sheet-grip{ width:38px; height:4px; border-radius:var(--radius-tight); margin:var(--space-hair) auto var(--space-3);
      background:var(--glass-border-hi); }
    .bn-sheet-grid{ display:grid; grid-template-columns:repeat(4, 1fr); gap:var(--space-2); }
    .bn-more-item{ display:flex; flex-direction:column; align-items:center; justify-content:center;
      gap:var(--space-2); min-height:70px; padding:var(--space-2) var(--space-1); border-radius:var(--radius-card); cursor:pointer;
      font-size:var(--type-label); line-height:1.25; text-align:center; color:var(--text-dim);
      -webkit-tap-highlight-color:transparent; }
    .bn-more-item:active{ background:rgba(120,120,140,.16); }
    .bn-more-item.active{ color:var(--blue); }
    .accounts-mobile{ display:flex; overflow-x:auto; gap:var(--space-2); padding-bottom:var(--space-4); margin-bottom:var(--space-2); border-bottom:1px solid var(--glass-border); }
    .chat-pill{ bottom:96px; right:20px; }
    :root{ --orb-keepout-x:0px; }
    .chat-card{ bottom:96px; right:14px; left:14px; width:auto; height:min(560px, calc(100vh - 180px)); }
    /* IN THE IPHONE APP THE CARD IS A SHEET THAT FOLLOWS THE KEYBOARD. 13
       October, on his phone: "misshapen, cut off, hard to use". A card fixed
       96px off the bottom at a fixed height sits UNDER the keyboard the
       moment he types, and the page behind it now scrolls. So it is sized to
       the visual viewport — the part of the screen the keyboard leaves —
       through --ask-top / --ask-h, which lucidAskFitViewport() in app.js keeps
       current. The bottom nav steps aside while it is open. */
    html.ios-shell .chat-card{ top:var(--ask-top, max(env(safe-area-inset-top), 10px)); bottom:auto;
      left:8px; right:8px; height:var(--ask-h, calc(100dvh - 20px)); max-height:none; }
    html.ios-shell body.ask-open .bottom-nav{ display:none; }
  }
  /* THE KEEP-OUT, RESTATED FOR THE BREAKPOINT THAT SHRINKS THE SPHERE.
     Row 2022 — the arithmetic is beside :root above .chat-pill. In the 780
     block just above, the orb is 20px from the right rather than 26 and .main's
     side padding is 16 rather than 32, so it stands on 88 + 20 − 16 = 92px of
     the column. Below 640 it is 56px wide (there are two rules by
     .chat-pill-aurora and the second one narrows it), so 56 + 20 − 16 = 60.

     THIS BLOCK IS BELOW THE 780 ONE AND NOT INSIDE IT, and that is not
     tidiness. Both media queries match at 400px, both set the same custom
     property at the same specificity, and the later one wins — so the narrow
     value has to be written after the wide one, or a phone would silently
     reserve thirty-two pixels of column that the orb is not standing on. */
  @media (max-width:640px){
    :root{ --orb-keepout-x:0px; }
  }
  .accounts-mobile{ display:none; }
  /* ---- Sign-in gate -----------------------------------------------------
     Sits above everything and outside #appZoomRoot, so the font-scale zoom
     can't shrink the one screen you need to be able to read. The app behind
     it is fully built but has loaded no data — nothing is fetched until a
     session exists. */
  #loginGate{
    position:fixed; inset:0; z-index:99999;
    display:none; align-items:center; justify-content:center;
    background:
      radial-gradient(900px 620px at 50% 34%, rgba(61,107,255,0.14), transparent 70%),
      radial-gradient(700px 520px at 82% 78%, rgba(160,125,255,0.10), transparent 72%),
      linear-gradient(180deg, #080A1A 0%, #050710 55%, #020306 100%);
    font-family:var(--font-body);
    -webkit-font-smoothing:antialiased;
  }
  #loginGate.show{ display:flex; }
  .login-card{
    width:min(400px, calc(100vw - 40px));
    padding:var(--space-10) var(--space-8) var(--space-8);
    text-align:center;
    border-radius:var(--radius-card);
    background:rgba(20,28,60,0.42);
    border:1px solid rgba(255,255,255,0.10);
    box-shadow:var(--elev-floating);
    backdrop-filter:blur(22px) saturate(1.3);
    -webkit-backdrop-filter:blur(22px) saturate(1.3);
    animation:loginRise 520ms var(--ease-out) both;
  }
  @keyframes loginRise{ from{ opacity:0; transform:translateY(14px) scale(0.985); } to{ opacity:1; transform:none; } }

  /* The orb is Lucid's whole identity, so the first thing you ever see is it —
     not a form. The same element as Ask Lucid at 104px, where there is room
     for the edge to be at zero and the glow to be the whole object. */
  /* THE BOX THE ORB USED TO NEED. It reserved 104 square pixels because the
     orb was 104 square pixels; the thread is one pixel tall and a line of
     light, so the rest of that box was a hole between the mark and the word
     Lucid. Spotted in the picture rather than in any number — the layout
     check was perfectly happy with a correct card containing a gap. */
  .login-orb{ width:100%; height:auto; margin:var(--space-4) auto var(--space-6);
              position:relative; }

  .login-title{ font-family:var(--font-display); font-size:var(--type-figure); font-weight:600;
    color:#F2F5FF; letter-spacing:-0.01em; margin:0 0 var(--space-2); }
  .login-sub{ font-size:var(--type-body); color:rgba(226,232,255,0.56); margin:0 0 var(--space-6); line-height:1.5; }

  .login-btn{
    display:flex; align-items:center; justify-content:center; gap:var(--space-2);
    width:100%; padding:var(--space-3) var(--space-4); margin:0 0 var(--space-3);
    border-radius:var(--radius-card); border:1px solid rgba(255,255,255,0.13);
    background:rgba(255,255,255,0.05); color:#EEF2FF;
    font-family:inherit; font-size:var(--type-lead); font-weight:600; cursor:pointer;
    transition:background var(--dur-fast) var(--ease-out), border-color var(--dur-fast), transform var(--dur-fast);
  }
  .login-btn:hover{ background:rgba(255,255,255,0.09); border-color:rgba(255,255,255,0.22); }
  .login-btn:active{ transform:translateY(1px); }
  .login-btn[disabled]{ opacity:0.55; cursor:default; transform:none; }
  .login-btn.primary{
    /* #3A66F8, not #3D6BFF: white on the brand blue is 4.43:1, a hair under
       the floor; this is the same blue a step deeper, 4.73:1. A chosen chat
       colour brings its own ink. */
    background:linear-gradient(180deg, var(--orb-accent, #3A66F8), var(--orb-accent-2, #2954D9));
    border-color:transparent; color:var(--orb-ink, #fff);
    box-shadow:0 8px 26px var(--orb-glow, rgba(61,107,255,0.34));
  }
  .login-btn.primary:hover{ filter:brightness(1.08); }
  .login-btn svg{ width:17px; height:17px; flex:none; }

  .login-btn.ghost{ background:transparent; border-color:transparent;
    color:rgba(226,232,255,0.5); font-weight:500; font-size:var(--type-body); padding:var(--space-2); }
  .login-btn.ghost:hover{ background:rgba(255,255,255,0.05); color:#EEF2FF; }
  .login-or{ display:flex; align-items:center; gap:var(--space-3); margin:var(--space-4) 0 var(--space-4);
    color:rgba(226,232,255,0.32); font-size:var(--type-label); letter-spacing:0.09em; text-transform:uppercase; }
  .login-or::before, .login-or::after{ content:''; flex:1; height:1px; background:rgba(255,255,255,0.09); }

  .login-input{
    width:100%; padding:var(--space-3) var(--space-4); margin:0 0 var(--space-3);
    border-radius:var(--radius-card); border:1px solid rgba(255,255,255,0.13);
    background:rgba(6,10,26,0.55); color:#EEF2FF;
    font-family:inherit; font-size:var(--type-lead); outline:none;
    transition:border-color var(--dur-fast), box-shadow var(--dur-fast);
  }
  .login-input::placeholder{ color:rgba(226,232,255,0.34); }
  .login-input:focus{ border-color:var(--orb-accent, #3D6BFF);
    box-shadow:0 0 0 3px var(--orb-glow, rgba(61,107,255,0.20)); }

  .login-msg{ min-height:18px; margin:var(--space-3) 0 0; font-size:var(--type-body); line-height:1.5; }
  .login-msg.err{ color:#FF9A9A; }
  .login-msg.ok{ color:#8FE3B0; }
  .login-msg.info{ color:rgba(226,232,255,0.55); }
  .login-foot{ margin:var(--space-5) 0 0; font-size:var(--type-body); color:rgba(226,232,255,0.30); }

  /* Post-sign-in nudge to register Touch ID, shown once. Deliberately a small
     card in the corner rather than another full-screen step — the person is
     already in and working, and this is an offer, not a requirement. */
  #passkeyNudge{
    position:fixed; right:18px; bottom:126px; z-index:9000;
    display:none; width:290px; padding:var(--space-4) var(--space-5);
    border-radius:var(--radius-card); background:rgba(20,28,60,0.72);
    border:1px solid rgba(255,255,255,0.12);
    box-shadow:var(--elev-floating);
    backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px);
    font-family:var(--font-body); color:#EEF2FF;
    animation:loginRise 420ms var(--ease-out) both;
  }
  #passkeyNudge.show{ display:block; }
  #passkeyNudge h4{ margin:0 0 var(--space-2); font-size:var(--type-lead); font-weight:600; }
  #passkeyNudge p{ margin:0 0 var(--space-3); font-size:var(--type-body); line-height:1.5; color:rgba(226,232,255,0.6); }
  #passkeyNudge .pn-row{ display:flex; gap:var(--space-2); }
  #passkeyNudge .pn-row button{ flex:1; padding:var(--space-2) var(--space-3); font-size:var(--type-body); margin:0; }

  /* THE ONBOARDING GATE — item 34. Same fixed-overlay shape as #loginGate
     immediately above, right down to the .show toggle, because a person who
     just signed in for the first time is in no position to notice a different
     visual language for "one more screen before the app". The only things
     genuinely new are the voice-chip row (borrowing .voice-chip/.voice-chips
     from Settings' picker so a third copy of that styling does not exist) and
     its label, styled here. */
  #onboardGate{
    position:fixed; inset:0; z-index:99998;
    display:none; align-items:center; justify-content:center;
    background:
      radial-gradient(900px 620px at 50% 34%, rgba(61,107,255,0.14), transparent 70%),
      radial-gradient(700px 520px at 82% 78%, rgba(160,125,255,0.10), transparent 72%),
      linear-gradient(180deg, #080A1A 0%, #050710 55%, #020306 100%);
    font-family:var(--font-body);
    -webkit-font-smoothing:antialiased;
  }
  #onboardGate.show{ display:flex; }

  /* CREATING AN ACCOUNT — 14 October. .signing-up on the sign-in card puts
     every way IN away at once (each carries .login-route in index.html) and
     brings the sign-up block out. !important because showLogin() and the
     passkey check write inline display on several of those elements, and the
     sign-up mode has to win over all of them without either knowing it exists. */
  #loginSignup{ display:none; }
  .login-card.signing-up #loginSignup{ display:block; }
  .login-card.signing-up .login-route{ display:none !important; }
  #signupNote{ margin-bottom:var(--space-4); }

  /* GETTING A NEW ACCOUNT'S MAIL IN — 14 October. Painted on the gate's own
     fixed dark ground like everything else in #onboardGate, so the colours are
     literal for the reason written beside .ob-connect above. The card can be
     taller than a phone, so it scrolls inside itself rather than losing its
     top edge to the centring. */
  #firstRunCard{ max-height:calc(100vh - var(--space-8)); overflow-y:auto; }
  .frun-row{ padding:var(--space-3) 0; border-top:1px solid rgba(255,255,255,0.08); }
  .frun-row:first-child{ border-top:0; }
  .frun-head{ display:flex; justify-content:space-between; align-items:baseline; gap:var(--space-2);
    font-size:var(--type-body); color:#EEF2FF; font-weight:600; }
  .frun-head .frun-what{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .frun-head .frun-state{ flex:none; font-weight:500; color:rgba(226,232,255,0.56); }
  .frun-head .frun-state.ok{ color:#8FE3B0; }
  .frun-head .frun-state.err{ color:#FF9A9A; }
  .frun-say{ font-size:var(--type-body); color:rgba(226,232,255,0.62); line-height:1.5; margin:var(--space-1) 0 0; }
  .frun-bar{ height:var(--space-1); border-radius:var(--space-1); margin-top:var(--space-2);
    background:rgba(255,255,255,0.08); overflow:hidden; }
  .frun-bar > span{ display:block; height:100%; border-radius:inherit;
    background:var(--orb-accent, #3D6BFF); transition:width var(--dur-slow) var(--ease-out); }
  .frun-bar.done > span{ background:#8FE3B0; }
  /* NO FRACTION IS KNOWN — older mail, or a Microsoft first pass. Stripes
     rather than a bar at some invented width: a bar at 40% says 40%. */
  .frun-bar.unknown > span{ width:100% !important; opacity:0.55;
    background:repeating-linear-gradient(135deg, rgba(61,107,255,0.75) 0 var(--space-2), rgba(61,107,255,0.25) var(--space-2) var(--space-4)); }
  .frun-checked{ margin:var(--space-3) 0 0; text-align:center; }
  .ob-voice-label{ font-size:var(--type-body); color:rgba(226,232,255,0.5); text-align:left;
    margin:var(--space-hair) 0 var(--space-2); }
  /* THE CONNECT STEP — item 295. Two buttons side by side rather than stacked,
     because stacked they read as a first choice and a second-best one, and
     Google and Microsoft are the same single press. Its label gets the space
     above it that a new section needs; the voice label below it gets the same,
     so the card reads as three questions rather than one long form.
     The colours here are literal rather than tokens for the same reason every
     other rule in this block is: #onboardGate is painted outside the theme, on
     one fixed dark ground, so that a half-loaded theme can never make the first
     screen anybody sees unreadable. */
  .ob-connect{ display:flex; gap:var(--space-2); margin:0 0 var(--space-1); }
  /* nowrap and a shade smaller, because "Connect Microsoft" wrapped to two
     lines in a half-width button while "Connect Google" stayed on one — two
     presses that are the same single press, drawn as though one of them were
     bigger. Found in the screenshot, not in the markup. */
  .ob-connect .login-btn{ flex:1; margin:0; white-space:nowrap; font-size:var(--type-lead); padding-left:var(--space-2); padding-right:var(--space-2); }
  .ob-connect + .ob-voice-label{ margin-top:var(--space-4); }
  #onboardGate .voice-chips{ justify-content:center; margin:0 0 var(--space-1); }
  #onboardGate .voice-chip{
    border-color:rgba(255,255,255,0.13); color:rgba(226,232,255,0.75);
  }
  #onboardGate .voice-chip:hover{ border-color:rgba(255,255,255,0.3); color:#EEF2FF; }
  #onboardGate .voice-chip.on{
    background:var(--orb-accent, #3D6BFF); border-color:transparent; color:#fff;
  }
  /* THE BUTTON'S OWN BLOOM, said on the ROOT rather than on the orb.
     Custom properties inherit downward and .chat-pill is the orb's
     PARENT, so a variable set on .lorb can never reach it — which is how
     a molten orb kept a lilac corona for three rounds.
     ONE RULE NOW, NOT FIVE. --orb-glow is computed live by applyOrbTint()
     from whichever two colours the current family+theme actually renders,
     so it is already correct for any style, tone-that-no-longer-exists,
     or theme at once — see ORB_TINT_VARS in app.js. data-glow="off" is
     "still an option to have the back glow", set by the checkbox in
     Settings; it drops the glow and keeps the plain drop shadow. */
  .chat-pill{
    filter:drop-shadow(0 10px 26px rgba(0,0,0,.55))
           drop-shadow(0 0 24px var(--orb-glow, rgba(0,0,0,0)));
  }
  :root[data-glow="off"] .chat-pill{
    filter:drop-shadow(0 10px 26px rgba(0,0,0,.55));
  }
  /* ONE DIRECTION. Both of them. That is the entire trick. */
  @keyframes crossRun{
    from{ transform:rotate(0deg); }
    to  { transform:rotate(360deg); }
  }
  /* THE TWO TRAVEL SHAPES, kept from the Lava style that is gone. One
     lingers at the bottom, the other at the top, so two blobs never queue
     up behind each other doing the same motion at an offset. */
  @keyframes lavaRise{
    0%   { transform:translateY(46%) scale(1.16, .78); }
    18%  { transform:translateY(20%) scale(.9, 1.14); }
    46%  { transform:translateY(-34%) scale(1.06, .94); }
    58%  { transform:translateY(-42%) scale(1.12, .86); }
    82%  { transform:translateY(6%) scale(.92, 1.1); }
    100% { transform:translateY(46%) scale(1.16, .78); }
  }
  @keyframes lavaRiseB{
    0%   { transform:translateY(18%) scale(1.1, .84); }
    30%  { transform:translateY(-30%) scale(.92, 1.12); }
    52%  { transform:translateY(-46%) scale(1.08, .9); }
    74%  { transform:translateY(-10%) scale(.94, 1.08); }
    100% { transform:translateY(18%) scale(1.1, .84); }
  }

  /* THE WHOLE TILE OPENS THE EMAIL.
     "emails in timeline show open on click, (not have to click the tiny
      chip)"                                          (Matt, 27 August.)
     The chip was an 11px link inside a 60px-tall card — a target about
     four percent of the thing you are actually aiming at. The card is
     the target now, and the chevron is a signpost rather than a button. */
  /* An opened tile stops being a button for the text inside it — otherwise
     selecting a line of the email you just opened folds it away again. */
  .timeline-item.tl-open [id^="cdBody"],
  .timeline-item.tl-open [id^="odBody"]{ cursor:auto; }

  /* RIM THE TEXT ROWS, ALL THE WAY ROUND.
     "rim the texts with the color (blue/green) not just the left side"
     A 2px left edge is a bullet point. A full rim is a bubble, which is
     what a text message looks like everywhere else a person has seen one. */
  .timeline-item.tl-text{
    border:1px solid color-mix(in srgb, var(--tl-kind, #4ec9b0) 55%, transparent);
    box-shadow:0 0 0 1px color-mix(in srgb, var(--tl-kind, #4ec9b0) 14%, transparent),
               inset 0 0 22px color-mix(in srgb, var(--tl-kind, #4ec9b0) 7%, transparent);
  }
  /* SENT AND RECEIVED ARE DIFFERENT COLOURS, which is the blue/green he
     means — the same convention as the Messages app the texts came out of.
     Green is the one you sent.

     This used to be done here, as `.timeline-item.tl-text.tl-mine{ --tl-kind:
     #34C759 }` overriding the `style="--tl-kind:..."` textRowHtml also wrote
     on the very same element — and it never once won. A declaration inside a
     style="" attribute outranks a class selector in the cascade regardless of
     how many classes that selector carries, so every bubble silently
     rendered in whatever colour the inline style said (var(--teal), always)
     and this rule sat here doing nothing. Fixed at the source instead — see
     TL_TEXT_SENT_COLOR / TL_TEXT_RECEIVED_COLOR in app.js, which compute the
     real colour and write it into that inline style directly, so there is
     only one number for it to come from. */

  /* THE RING CARRIES WHO SAID IT.
     "love the ring around text messages in timeline, but them should be the
      avatar initials (SH) for my wife, and their bubble should have a
      different color halo than mine."               (Matt, 2 September.)

     The ring already existed — the 32px circle every timeline row draws on
     its left, filled until now with the same chat icon on every single text.
     Every one of those circles was interchangeable; the only way to know who
     sent a bubble was to read it. Two letters fix that at a glance, the way a
     name in an address book saves dialling a number you would not otherwise
     recognise — MH for the ones he sent, SH for the ones she did.

     SOLID, NOT OUTLINED. .avatar.has-halo (the ten-most-connected mark,
     above) rings a photograph that already carries the person's likeness, so
     a thin ring reads as an addition to it. A monogram has nothing under it —
     filling the circle with the sender's colour is what makes white letters
     legible AND is the halo, in one shape instead of two. Extra specificity
     (three classes, not the base rule's two) so this wins outright rather
     than depending on where it happens to sit in the file. */
  .timeline-item .timeline-icon.tl-avatar{
    color:#fff; font-size:var(--type-body); font-weight:700; letter-spacing:.01em;
    background:var(--tl-kind, var(--text-dim));
    box-shadow:
      0 0 0 1px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 78%, black 12%),
      0 0 0 4px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 18%, transparent),
      0 0 16px 2px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 42%, transparent);
  }
  :root[data-mode="light"] .timeline-item .timeline-icon.tl-avatar{
    box-shadow:
      0 0 0 1px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 82%, black 6%),
      0 0 0 4px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 14%, transparent),
      0 0 12px 1px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 30%, transparent);
  }
  /* Deliberately still — twenty breathing haloes on one screen of texts is
     the exact "wallpaper, not a distinction" failure .avatar.has-halo's own
     comment warns about, and a timeline can show a lot more than twenty. */

  /* ==========================================================================
     SEARCHING A TIMELINE.
     "Build a real search over a contact's or company's timeline: text, plus
      filters that match what the entries actually are (kind, date range,
      direction). Make it fast and make it feel like Lucid, not like a form."

     The box borrows Lucid's own search shape — .mail-search, the same three
     classes the Mail page's box uses — rather than inventing a second one, so
     this reads as the app's search rather than a form bolted onto a card. The
     chips underneath are what keep it from BEING a form: no labels, no
     "Filter by:", just the same coloured-pill language TL_KIND already
     taught on every row above them. Clicking one is the filter; there is
     nothing to submit. ========================================================================== */
  /* ROW 2333 — "close the gap between timeline search and text box".
     The bar held two stacked rows (search + date control, then two chip
     groups) and spent --space-3 under itself on top of --space-2 between them.
     With the date control and the Sent/Received chips gone there is one row of
     kind chips left, and on an organization's timeline often none at all — so
     the bar was reserving the height of a filter panel to show a search box.
     One step of air under it, and the gap above the chips only when there are
     chips. */
  .tl-search-bar{ margin-bottom:var(--space-1); }
  .tl-search-bar .tlf-row:empty{ display:none; }
  /* SEARCH AND WHEN ON ONE LINE — row 2161. The date control used to sit on the
     chip row below this one, which on a narrow card meant a whole line of the
     page spent standing a calendar over the timeline. Here it shares the line
     the search box was already taking: the box gives up exactly what the button
     needs and the entries start higher at every width. It WRAPS rather than
     squeezing, so on a phone the button drops underneath instead of leaving
     room for four characters of a search. */
  .tlf-top{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-2); }
  .tlf-top:not(:last-child){ margin-bottom:var(--space-1); }
  /* flex-basis 0, not .mail-search's own 300px: the basis is what decides
     whether the two fit on one line, and 300 + the gap + the button is 400 —
     so on a phone the button wrapped to a line of its own DIRECTLY UNDER THE
     SEARCH BOX, which is higher up the timeline than where it started. The box
     keeps .mail-search's 170px floor, so it never shrinks to a slot with no
     room for a word; between 170 and that floor the button wraps as it should. */
  .tlf-top .tlf-search{ flex:1 1 0; }
  .tlf-search{ margin-bottom:0; }
  .tlf-row{ display:flex; flex-wrap:wrap; align-items:center; gap:var(--space-2); }
  .tlf-kinds{ display:flex; flex-wrap:wrap; gap:var(--space-2); }
  .tlf-chip{ font-size:var(--type-body); padding:var(--space-2) var(--space-3); }
  .tlf-chip.active{
    background:color-mix(in srgb, var(--tl-kind, var(--accent)) 16%, transparent);
    border-color:color-mix(in srgb, var(--tl-kind, var(--accent)) 55%, transparent);
    color:var(--text);
  }
  /* .chip.active's own dark-mode glow (":root:not([data-mode='light']) .chip.
     active", up near .contact-filters) is FOUR selector classes deep and would
     otherwise win this fight regardless of source order — matched here
     structurally, not just in specificity number, so this genuinely overrides
     it rather than winning by accident on a version of the cascade that
     happens to hold today. */
  :root:not([data-mode="light"]) .tlf-chip.active{
    box-shadow:0 0 10px 1px color-mix(in srgb, var(--tl-kind, var(--accent)) 30%, transparent);
  }
  :root[data-mode="light"] .tlf-chip.active{
    box-shadow:0 0 8px 1px color-mix(in srgb, var(--tl-kind, var(--accent)) 22%, transparent);
  }

  /* ROW 2333 — THE WHEN PICKER'S STYLES WENT WITH ITS MARKUP.
     About a hundred and seventy lines: .tlw and its button, .tlw-menu, the
     shaded .mc-cell.tlw-in bands, .tlw-years, .tlw-note, the seam between the
     kind chips and the Sent/Received group, and .tlf-dates. Every one of them
     styled an element the timeline no longer draws — see the note by
     tlItemText in app.js. CSS for markup that does not exist is worse than
     unused code, because the next person to read it has to go and check. */

  /* WHICH SPEAKER, AND WHY NOT.
     ------------------------------------------------------------------------
     Six things can stop Lucid being a speaker, and from the outside all six
     look identical: press play, nothing happens. This line is the difference
     between a bug report that says "it does not work" and one that says which
     of the six it is. Four tones, because the reply differs: nothing to do,
     wait, all well, act.

     Deliberately quiet when everything is fine — a green sentence sitting
     under the search box on every good day is noise. It only earns weight
     when it is telling you to go and do something. */
  .speaker-status{
    margin:var(--space-3) var(--space-5) 0; padding:var(--space-2) var(--space-3); border-radius:var(--radius-card);
    font-size:var(--type-body); line-height:1.45; letter-spacing:.005em;
    border:1px solid transparent; transition:color var(--dur-base), background var(--dur-base), border-color var(--dur-base);
  }
  .speaker-status[data-tone="good"]{ color:var(--text-faint); background:transparent; border-color:transparent; padding-left:0; padding-right:0; }
  .speaker-status[data-tone="wait"]{ color:var(--text-faint); background:transparent; border-color:transparent; padding-left:0; padding-right:0; }
  .speaker-status[data-tone="off"] { color:var(--text-faint); background:transparent; border-color:transparent; padding-left:0; padding-right:0; }
  /* The one that has to be read. */
  .speaker-status[data-tone="bad"]{
    color:var(--pink-2); background:rgba(255,90,71,.10); border-color:rgba(255,90,71,.28);
  }

  /* THE FIX, IN THE WARNING.
     A message that names a fix and then leaves you hunting for the control is
     worse than one that says nothing — it costs the time and the trust. Every
     speaker failure Spotify can be talked out of now carries its own button. */
  .speaker-fix{
    margin-left:var(--space-3); padding:var(--space-1) var(--space-3); border-radius:var(--radius-pill);
    font:inherit; font-size:var(--type-body); font-weight:600; line-height:1.4;
    color:var(--text); background:rgba(255,255,255,.09);
    border:1px solid rgba(255,255,255,.20); cursor:pointer;
    white-space:nowrap; vertical-align:baseline;
    transition:background var(--dur-fast), border-color var(--dur-fast);
  }
  .speaker-fix:hover{ background:rgba(255,255,255,.16); border-color:rgba(255,255,255,.34); }

  /* THE FINDER — one box, two jobs.
     ------------------------------------------------------------------------
     "Search anything (or paste a link and it gives you the rss)."
     It sits under the follow box because that is the order of the questions:
     "add this thing I already know about", then "find me the thing I don't". */
  .finder{ margin:0 0 var(--space-5); }
  .finder-row{
    display:flex; align-items:center; gap:var(--space-3);
    padding:0 var(--space-4); border-radius:var(--radius-card);
    background:var(--glass-bg); border:1px solid var(--glass-border);
    backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  }
  [data-fx="lite"] .finder-row{ backdrop-filter:none; }
  .finder-icon{ width:17px; height:17px; color:var(--text-faint); flex:none; }
  .finder-input{
    flex:1; min-width:0; background:none; border:none; padding:var(--space-3) 0;
    font-size:var(--type-lead); color:var(--text);
  }
  .finder-input:focus{ outline:none; box-shadow:none; }
  .finder-input::-webkit-search-cancel-button{ filter:invert(.6); }
  .finder-hint{ font-size:var(--type-body); color:var(--text-faint); white-space:nowrap; flex:none; }

  .finder-out{ margin-top:var(--space-3); }
  .finder-head{
    font-size:var(--type-label); letter-spacing:.09em; text-transform:uppercase;
    color:var(--text-faint); margin:var(--space-4) var(--space-hair) var(--space-2);
  }
  .finder-head:first-child{ margin-top:var(--space-1); }
  .finder-none, .finder-wait{
    padding:var(--space-4) var(--space-hair); font-size:var(--type-body); color:var(--text-dim); line-height:1.55;
  }

  /* A FOUND FEED, WITH ITS HEADLINES.
     The headlines are the feature. A list of URLs tells you nothing; three
     recent headlines tell you instantly whether it is the feed you meant. */
  .finder-feed{
    padding:var(--space-3) var(--space-4); margin-bottom:var(--space-2); border-radius:var(--radius-card);
    background:rgba(255,255,255,.035); border:1px solid var(--glass-border);
  }
  .finder-feed-top{ display:flex; align-items:center; gap:var(--space-3); }
  .finder-feed-t{ flex:1; min-width:0; font-size:var(--type-lead); font-weight:600;
                  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .finder-follow{ flex:none; padding:var(--space-2) var(--space-4); font-size:var(--type-body); }
  .finder-feed-u{ font-size:var(--type-body); color:var(--text-faint); margin-top:var(--space-1);
                  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .finder-peek{ margin:var(--space-2) 0 0; padding:var(--space-2) 0 0 var(--space-4); list-style:disc;
                border-top:1px solid var(--glass-border); }
  .finder-peek li{ font-size:var(--type-body); color:var(--text-dim); line-height:1.65;
                   overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

  .finder-src{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-hair); font-size:var(--type-body); }
  .finder-src img{ width:18px; height:18px; border-radius:var(--radius-control); object-fit:cover; flex:none; }
  .finder-src-k{ font-size:var(--type-label); color:var(--text-faint); }

  .finder-item{
    display:flex; align-items:center; gap:var(--space-3); padding:var(--space-2) var(--space-hair);
    border-bottom:1px solid var(--glass-border); text-decoration:none; color:inherit;
  }
  .finder-item:last-child{ border-bottom:none; }
  .finder-item:hover .finder-item-t{ color:var(--blue-2); }
  .finder-item-img{ width:40px; height:40px; border-radius:var(--radius-control); object-fit:cover; flex:none; }
  .finder-item-t{ flex:1; min-width:0; font-size:var(--type-body); line-height:1.45;
                  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .finder-item-s{ font-size:var(--type-body); color:var(--text-faint); white-space:nowrap; flex:none; }
  @keyframes pearlTurn{
    0%   { background-position:  0% 50%; }
    50%  { background-position:100% 50%; }
    100% { background-position:  0% 50%; }
  }

  /* THE WOBBLE USED TO MORPH border-radius — a blob breathing in and out of
     round. It shipped, and within the hour Matt's Settings panel froze solid:
     "cant select anything." border-radius is a LAYOUT property. Animating it
     forces the browser to re-measure the box on every single frame, on every
     .lorb wearing this family, forever — cheap enough for Chromium's
     compositor to hide, and exactly the property this file's own storm-family
     rule already knew to avoid (see 8j's test, elsewhere: "none of them
     animates anything that costs a layout"). This engine is new, so it never
     got held to that rule until it took down the one thing Settings is for.
     transform and box-shadow are composited; border-radius is not. The sphere
     stays sealed here for the same reason it stays sealed in every other
     family — not a style choice, a performance one. */
  @keyframes liquidWobble{
    0%,100%{ transform:scale(1); }
    30%{ transform:scale(1.015); }
    60%{ transform:scale(.99); }
  }
  @keyframes lqIriSpin{ to{ transform:rotate(360deg); } }
  @keyframes lqAliveA{
    0%{   transform:translate(-10%,-8%)  scale(1);    }
    22%{  transform:translate(12%,-16%)  scale(1.2);  }
    48%{  transform:translate(16%,10%)   scale(.85);  }
    74%{  transform:translate(-8%,16%)   scale(1.1);  }
    100%{ transform:translate(-10%,-8%)  scale(1);    }
  }
  @keyframes lqAliveB{
    0%{   transform:translate(8%,10%)   scale(1);    }
    30%{  transform:translate(-16%,4%)  scale(.8);   }
    58%{  transform:translate(-4%,-14%) scale(1.25); }
    82%{  transform:translate(14%,-4%)  scale(.95);  }
    100%{ transform:translate(8%,10%)   scale(1);    }
  }

  

  /* CONNECTED SERVICES — always present, never conditional.
     ------------------------------------------------------------------------
     "No way to disconnect no red line. This is stupid."
     Every control I pointed him at appeared only when Lucid had decided
     something was wrong — so a working connection had no way to be reset, and
     a broken one sent him to a button that had never rendered. This row is
     always here, states what the connection is doing whatever that is, and
     always carries both actions. */
  .svc-list{ display:flex; flex-direction:column; gap:var(--space-2); margin-top:var(--space-1); }
  .svc-row{
    display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap;
    padding:var(--space-3) var(--space-4); border-radius:var(--radius-card);
    background:rgba(255,255,255,.035); border:1px solid var(--glass-border);
  }
  .svc-dot{ width:8px; height:8px; border-radius:var(--radius-round); flex:none; background:var(--text-faint); }
  .svc-row[data-state="good"] .svc-dot{ background:var(--green); }
  .svc-row[data-state="bad"]  .svc-dot{ background:var(--pink); }
  .svc-row[data-state="off"]  .svc-dot{ background:var(--text-faint); }
  .svc-name{ font-size:var(--type-lead); font-weight:650; flex:none; }
  .svc-detail{ flex:1; min-width:200px; font-size:var(--type-body); color:var(--text-dim); line-height:1.5; }
  .svc-acts{ display:flex; gap:var(--space-2); flex:none; }
  .svc-acts .btn{ padding:var(--space-2) var(--space-4); font-size:var(--type-body); }

  /* WHAT IS CONNECTED — item 302's two marks, and both of them are type rather
     than colour alone.
     ------------------------------------------------------------------------
     .svc-flag is the one exception to that panel's no-dots rule. A connection
     that has actually STOPPED is a fault, it IS hers to fix, and there is a
     button beside it that fixes it. Every other state on that screen is either
     a choice she has not made or a thing this deployment does not have, and a
     coloured mark beside either of those is a bug report addressed to the
     wrong person. It carries the word "Stopped" so that it survives the six
     matte grounds and a reader who does not see red.

     THE TWO KINDS OF UPGRADE, item 286, held here as well as on the area
     bands: CONNECT AN ACCOUNT asks for a password and involves somebody else's
     service, so it carries the accent; MAKE A CHOICE asks for a preference and
     is ordinary ink, because it is an ordinary thing to do. Which one a row is
     is declared in its config as `kind` and written to data-upgrade, so a
     harness rather than whoever is reading the screen that day holds the rule.

     NEITHER OF THEM CARRIES ITS COLOUR IN THE INK, and that is a measurement
     rather than a preference. tests/first-ten-minutes.js measures both on the
     darkest and the lightest ground the app has. "Stopped" set in --pink comes
     back at 6.64:1 on midnight and 4.50:1 on light — the second one exactly on
     the 4.5:1 line this app holds every other piece of type to, on two of
     fourteen grounds, at 10px. The accent as ink on a 12.5px button is worse
     still. Sitting on a threshold is not passing it, so both keep ordinary
     ink, which is the highest contrast every theme has, and their colour moves
     to a rule underneath — where it is a second signal beside the word rather
     than the thing being read. */
  .svc-flag{
    margin-left:var(--space-2); font-size:var(--type-label); font-weight:700; letter-spacing:.05em;
    text-transform:uppercase; color:var(--text); vertical-align:1px;
    border-bottom:1.5px solid var(--pink); }
  .svc-row[data-state="trouble"]{
    border-color:color-mix(in srgb, var(--pink) 34%, var(--glass-border)); }
  .svc-row .btn[data-upgrade="connect"]{
    border-color:color-mix(in srgb, var(--accent) 55%, transparent);
    box-shadow:inset 0 -1.5px 0 color-mix(in srgb, var(--accent) 55%, transparent); }
  .svc-row .btn[data-upgrade="connect"]:hover{
    border-color:var(--accent); box-shadow:inset 0 -1.5px 0 var(--accent); }

  
  

  
  
  

  /* AN EVENT WITH SOMEWHERE TO PUT ITS SECOND LINE.
     The block was one nowrap row, so a Cubs fixture and a client call looked
     identical at a glance: same height, same weight, no clue which calendar
     either came from. Now the title can sit above its source when the block is
     tall enough to hold both, and stays one line when it is not. */
  


  
  
  

  
  

  
  

  
  
  

  
  
  
  

  
  

  
  
  .ht-line{ font-size:var(--type-lead); padding:var(--space-3) 0; }
  .ht-line-t{ font-weight:550; letter-spacing:-.006em; }
  /* The first row of a tile is the one worth seeing from across the room. */
  .ht-line:first-of-type .ht-line-t{ font-weight:700; font-size:var(--type-lead); }
  .ht-num, .ht-time{ font-size:var(--type-body); font-weight:600; opacity:.92; }
  .ht-empty{ font-size:var(--type-body); }

  /* Tiles keep their own scale on a small screen — 15px rows in a 360px column
     is a different page, and the point of this pass is the desktop one. */
  @media (max-width:760px){
    .ht-line{ font-size:var(--type-lead); padding:var(--space-3) 0; }
    .ht-line:first-of-type .ht-line-t{ font-size:var(--type-lead); }
  }

  /* ==========================================================================
     THE INNER CIRCLE HALO
     --------------------------------------------------------------------------
     "Have your 10-20 most connected people have a ring/halo around their cards
      to denote they are the people you speak to the most."
     
     Two marks, deliberately quiet, because twenty haloes on one screen is not a
     distinction — it is wallpaper:
     
       A RING ON THE AVATAR. The person is the thing being marked, not the row,
       so the mark goes on the face. Two rings: a solid inner one at the
       avatar's edge and a soft outer bloom, which is what reads as a halo
       rather than as a border.
       
       AND A WHISPER ON THE CARD. A faint warm wash from the left edge, so a
       column of cards has a visible rhythm when you are scanning rather than
       reading. Barely there on purpose.
     
     NOT A COLOUR OF ITS OWN. It uses the accent, so it moves with the theme and
     does not add a fifteenth hue to a page that has a rule against exactly that.

     ITEM 30 — "the people closest to him should look different at a glance —
     a stronger halo on their avatar wherever they appear." The ring was only
     ever drawn on the Contacts list; the class is the same one .avatar
     carries everywhere, so wherever isInnerCircle() is asked in app.js —
     the sender of a message in the inbox, the header of his own page — this
     is the rule that answers. One ring, everywhere the face shows up,
     rather than a second mark invented per screen.
     ========================================================================== */
  .avatar.has-halo{
    box-shadow:
      0 0 0 2px var(--accent),
      0 0 0 5px color-mix(in srgb, var(--accent) 22%, transparent),
      0 0 16px 2px var(--accent-glow);
  }
  :root[data-mode="light"] .avatar.has-halo{
    box-shadow:
      0 0 0 2px var(--accent),
      0 0 0 5px color-mix(in srgb, var(--accent) 16%, transparent),
      0 0 12px 1px color-mix(in srgb, var(--accent) 28%, transparent);
  }
  .contact-row.inner-circle{
    background-image:linear-gradient(90deg,
      color-mix(in srgb, var(--accent) 7%, transparent) 0%,
      transparent 42%);
  }
  /* THE HALO STOPPED BREATHING. 265 — "any other circus animation of icons."
     It used to run @keyframes innerHalo, 4.5s, for ever, under data-fx="full".
     The reason that had to go is arithmetic rather than taste: .has-halo is
     worn by the sender avatar on every mail row (renderMail) as well as by
     contact rows, so an inbox with eight inner-circle senders in it was eight
     avatars pulsing out of phase — a screen of fireflies, and none of them
     announcing anything. Inner circle is a standing fact about a person, not
     something happening right now; the two rings say it perfectly well while
     sitting still, which is exactly what the matte finish had already decided
     for itself (see :root[data-finish="matte"] .avatar.has-halo, ~line 764,
     which carried animation:none months before this). This just stops making
     the glow themes the exception.
     @keyframes innerHalo went with it — that rule was its only reference in
     the repo, checked before deleting. */


  /* THE ACTION MENU ON A MESSAGE — drawn above everything, clipped by nothing.
     See the note by placeFixedMenu in app.js. It is positioned from JS in
     viewport coordinates, so it needs to sit above the trays and popups it may
     overlap rather than in the reading pane's own stacking order. */
  #rpActMenu{ position:fixed; z-index:9000; display:block; }

  /* Parented to <body>, so it is measured against the window and nothing can
     clip it. See placeFixedMenu in app.js for why that had to change. */
  #rpActMenu{ position:fixed; z-index:9000; display:block; }

  .kit-facts{ font-size:var(--type-body); color:var(--text-dim); margin-bottom:var(--space-3); }

  /* WHAT LUCID READ. The lost lines are marked and nothing else is: a tick
     beside forty correct lines is forty pieces of reassurance nobody needs,
     and it makes the two that matter harder to find. Never red — a line with
     no quantity is very often correct. */
  .kit-readas{ border:1px solid var(--glass-border); border-radius:var(--radius-card); padding:var(--space-3) var(--space-3);
    margin:var(--space-3) 0; background:rgba(120,120,140,.06); }
  .kit-readas-row{ display:flex; gap:var(--space-3); font-size:var(--type-body); padding:var(--space-1) 0; align-items:baseline; }
  .kit-readas-raw{ flex:1; min-width:0; color:var(--text-dim); }
  .kit-readas-as{ flex:1; min-width:0; color:var(--text); text-align:right; }
  .kit-readas-row.is-lost .kit-readas-as{ color:var(--amber, #C08A2E); }

  .kit-night-x{ color:var(--text-faint); cursor:pointer; font-size:var(--type-lead); padding:0 var(--space-1);
    margin-left:auto; flex:none; }
  .kit-night-x:hover{ color:var(--text); }


  /* ==========================================================================
     THE WATCHLIST.
     ==========================================================================

     Two rules run through every class below, and both are refusals.

     NOTHING ON THIS PAGE IS STYLED AS A SCORE. There is no ring, no bar, no
     star row, no percentage and no colour that means "better". The ordering of
     tonight's picks is three stated facts (see _watch-pick.js) and the screen
     prints those facts as sentences; the moment one of them is compressed into
     a number, the promise that the ranking can be argued with is gone and
     nobody can tell that it went.

     AND NOTHING HERE IS RED. The one state this feature spends most of its
     early life in — no TMDB key — is not a fault. Half the feature works
     without a key: the list, the services, tonight's arithmetic. A red box
     over the other half would teach him the page is broken when it is doing
     exactly what it promised. .wl-setup is therefore an ordinary card.
     ========================================================================== */
  .wl-panel{ margin-bottom:var(--space-6); }
  /* THE ORB'S KEEP-OUT ON AN INLINE ROW, WHICH IS A DIFFERENT SHAPE OF THE SAME
     FAULT. (Row 2022.)
     This heading is not a right-aligned action strip — title, note and door sit
     one after another and WRAP. So the door is not pinned to the corner; it
     wanders into it. On Fashion at 400px, "Sites ›" landed at 304→356 with the
     sphere's left edge at 324, which measured 62% of the control and 74% of its
     word gone, and it was nine presses out of nine onto the orb.
     Padding at the end of the row is the exactly-right lever for a wrapping
     line rather than a blunt one: it takes the orb's column out of the space
     the words are allowed to flow into, so the door wraps to the next line a
     little sooner instead of coming to rest under the sphere. Nothing moves on
     a wide window, where the row never fills. */
  .wl-panel-head{ display:flex; align-items:baseline; gap:var(--space-3); flex-wrap:wrap;
    margin-bottom:var(--space-1); padding-inline-end:var(--orb-keepout-x); }
  .wl-panel-title{ font-family:var(--font-display); font-size:var(--type-title); font-weight:600; color:var(--text); }
  /* See the note on .section-label: --text-faint sits on the readable floor
     and this measured 4.48:1 on serene:linen. */
  .wl-panel-note{ font-size:var(--type-body); color:var(--text-dim); }
  /* THE QUALIFICATION LINE, and it is a paragraph rather than a tooltip on
     purpose. `new.basis` says that the catalogue knows where a title streams
     TODAY and not the day it landed there — which is the difference between
     "new on Netflix" and the truth. A sentence that only appears on hover is a
     sentence most people never read, and this one is the reason the heading
     above it is honest. */
  .wl-basis{ font-size:var(--type-body); color:var(--text-dim); line-height:1.6; margin:var(--space-hair) 0 var(--space-3); max-width:74ch; }
  .wl-small{ font-size:var(--type-body); color:var(--text-faint); line-height:1.6; margin:var(--space-2) 0 0; max-width:74ch; }
  .wl-empty{ font-size:var(--type-body); color:var(--text-dim); line-height:1.6; padding:var(--space-3) 0; }

  .wl-card{ border:1px solid var(--glass-border); border-radius:var(--radius-card); background:var(--glass-bg);
    padding:var(--space-3) var(--space-4); margin-bottom:var(--space-3); }
  .wl-card-name{ font-size:var(--type-lead); font-weight:650; color:var(--text); }
  .wl-card-facts{ font-size:var(--type-body); color:var(--text-dim); margin-top:var(--space-hair); }
  .wl-acts{ display:flex; gap:var(--space-2); flex-wrap:wrap; margin-top:var(--space-3); }
  .wl-acts .btn{ font-size:var(--type-body); padding:var(--space-2) var(--space-4); }

  .wl-services{ display:flex; gap:var(--space-2); flex-wrap:wrap; margin:var(--space-2) 0 var(--space-1); }
  .wl-service{ display:inline-flex; align-items:center; gap:var(--space-2); font-size:var(--type-body); font-weight:600;
    color:var(--text); border:1px solid var(--glass-border-hi); border-radius:var(--radius-pill); padding:var(--space-2) var(--space-3); }
  /* --text-dim, NOT --text-faint. This × is the only way off the list — on the
     watchlist and now on Sports, where the Following row is the only unfollow
     control there is. Faint measured 4.50:1 on serene:linen — dead on the line
     — and a control that removes something should not be the palest thing on
     the panel anyway. Caught by tests/test-readability.js on the sports scene. */
  .wl-service-x{ color:var(--text-dim); cursor:pointer; font-size:var(--type-lead); line-height:1; }
  .wl-service-x:hover{ color:var(--text); }

  .wl-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(178px, 1fr)); gap:var(--space-4); }
  
  
  

  /* ==========================================================
     TEMPERATURE ON THE AGENDA — ROW 1720.

     "Change color and temp on agenda items based on due time
      and importance level."   (Matt, 11 September.)

     The rule above this one is the priority halo, and it is
     still live — on the WEEK grid, where a row is three pixels
     of bar and a halo is all there is room for. The day rail
     has a whole row, so it gets the thing he actually asked
     for: one ramp that carries how soon AND how much at once.
     agendaHeat() in app.js is the arithmetic and says why each
     band is where it is. `.agenda-item` no longer gets
     .has-priority, so the halo simply stops applying here —
     nothing above was edited to make that happen.

     FIVE STEPS AND ONE RAMP, NOT SIX HUES. The cold end is the
     theme's quiet ink, the middle IS the theme's accent, and
     the hot end is a step short of --pink — the colour this
     file already spends on late. So the warmest thing on the
     day and the late bar above it read as the same idea rather
     than as two competing alarms, and no theme gains a colour
     it did not already own.

     WHY THE TOKENS ARE DECLARED HERE RATHER THAN IN THE BLOCK
     AT THE TOP OF THIS FILE. Every one of them is derived from
     values that are ALREADY stated per theme — --accent,
     --text-faint, --orange, --pink — so a single :root is all
     twelve themes need, exactly as --life-heat-* is a single
     :root built out of --cat-red-2 and --brass. Declaring it
     beside the rules that use it keeps the whole idea in one
     place to read, and keeps five other people out of a merge
     in the busiest forty lines in the stylesheet.
     ========================================================== */
  :root{
    --ag-heat-0: var(--text-faint);
    --ag-heat-1: color-mix(in srgb, var(--accent) 45%, var(--text-faint));
    --ag-heat-2: var(--accent);
    --ag-heat-3: color-mix(in srgb, var(--orange) 72%, var(--pink));
    --ag-heat-4: var(--pink);
  }
  
  
  
  
  

  
  
  
  

  
  

  
  

  
  
  
  

  

  /* ==========================================================
     THE SPINE'S GROUPS, AND THE PANEL THEY OPEN.

     Eighteen items down a rail is a list you read rather than
     navigation. Fourteen of them now sit under three groups; the
     five that are opened many times a day do not.

     The panel gives each page a SENTENCE, not just a name.
     "Vault" means nothing until you have opened it once, and the
     tooltip that explained it was invisible on a touch screen.
     ========================================================== */
  .nav-group{ position:relative; }
  /* 1 September: measured at 1.89:1 in serene:linen. It was --text-dim, which
     passes on its own, knocked down to 45% opacity — and this is the control he
     could not find when he went looking for the vault. Full strength, and the
     open state is now said by the rotation rather than by brightness. */
  /* THE CHEVRONS ARE GONE, AND .nav-chev WITH THEM.
     -----------------------------------------------------------------
     They were fixed twice for contrast — a hairline › at 12.3px measured
     3.14:1 in the light theme — and the second fix (ink and weight, at 16px
     and 700) is what finally made Matt see them: "The arrows offsetting the
     media due to menu." He was right and the contrast work was solving the
     wrong problem. An arrow that has to be that heavy to be legible is heavy
     enough to push the label it belongs to out of line with the four labels
     above it, and three arrows in a rail of eight rows is three ragged rows.

     A group is not a page, so it should not look like a page with an arrow
     bolted on. Which group is open is now said by aria-expanded and by the
     panel being on screen; which page you are on is said by the bar on
     .nav-item.active. Nothing was lost that the eye was using.

     Deleted rather than left dead, because a class with three rules and no
     markup is the thing someone re-adds in six months. */

  /* THE PANEL WAS TRANSPARENT, AND IT IS WORTH SAYING WHY.
     It asked for a background of --glass-solid falling back to --glass, and
     NEITHER OF THOSE NAMES HAS EVER EXISTED in this file. The faint panel fill
     is called --glass-bg; the opaque one is --bg-mid.
     A CSS variable nothing defines does not warn, does not throw, and does not
     show up in any test that reads the markup — it resolves to nothing. So the
     panel had a blur, a border, a shadow and NO BACKGROUND AT ALL, and the
     whole app showed through the words.
     The same typo was in eighteen other places, which is why the Kitchen,
     Body, Travel, Routines and Watchlist cards had no fill either. All
     repaired together.
     Every other menu in Lucid uses --bg-mid, opaque and defined for all seven
     themes. This one now does too, and tests/lint.js fails on any var() that
     is neither defined in this stylesheet nor given a fallback — an invisible
     menu is not a symptom anybody should have to diagnose twice. */
  /* THE PANEL WEARS ITS AREA'S COLOUR, and it reads it off its own
     data-group — navGroupOpen() already stamps that attribute to know which
     menu is open, so the colour costs nothing and cannot get out of step with
     the rail row that opened it. That is the whole point of a colour per area:
     the same green marks Life in the rail, in this panel and on the page. */
  .nav-group-menu{
    --c:var(--accent);
    position:fixed; z-index:1400; min-width:270px; max-width:330px;
    padding:0 0 var(--space-1); border-radius:var(--radius-control);
    border:1px solid var(--glass-border-hi); background:var(--bg-mid);
    box-shadow:var(--elev-floating);
    max-height:min(70vh, 560px); overflow:auto;
  }
  .nav-group-menu[data-group="work"]{ --c:var(--area-work); }
  .nav-group-menu[data-group="media"]{ --c:var(--area-media); }
  .nav-group-menu[data-group="life"]{ --c:var(--area-life); }
  /* THE ONE SERIF IN THE MENU, matching the one serif in the rail. Fraunces
     appears exactly twice in the spine — the wordmark and this heading — and
     never on anything you press. A serif on a button is a costume; a serif on
     a title is a masthead, and that distinction is most of what separates
     regal from decorated. */
  .ngm-head{ font-family:var(--font-display); font-size:var(--type-lead); font-weight:600;
    letter-spacing:.005em; text-transform:none; color:var(--rail-ink);
    padding:var(--space-3) var(--space-4) var(--space-3); border-bottom:1px solid var(--glass-border); margin:0; }
  .ngm-head::after{ content:''; display:block; width:20px; height:2px; border-radius:var(--radius-tight);
    background:var(--c); margin-top:var(--space-2); }
  .ngm-head:first-child{ border-top:none; margin-top:0; }
  /* A HAIRLINE BETWEEN ROWS. With a see-through panel the rows ran into
     whatever was behind them; with an opaque one they ran into each other.
     Squared off with the rest of the third pass — a 10px radius inside a 6px
     panel was a rounded row in a sharp box. */
  .ngm-item{ padding:var(--space-2) var(--space-4); border-radius:0; cursor:pointer; position:relative;
    border-bottom:1px solid var(--glass-border); }
  .ngm-item:last-child{ border-bottom:none; }
  .ngm-item::before{ content:''; position:absolute; left:0; top:0; bottom:0; width:2px;
    background:transparent; }
  .ngm-item:hover{ background:rgba(255,246,230,.045); }
  .ngm-item:hover::before{ background:var(--c); }
  /* THE PAGE YOU ARE ON, said with a bar rather than a wash. A 14% tint of the
     accent on a transparent panel was invisible; on an opaque one it is still
     only a shade. A left bar reads at a glance — and it is now the area's
     colour rather than the account accent, so the bar in the panel and the bar
     in the rail are the same bar. */
  .ngm-item.on{ background:rgba(255,246,230,.05); }
  .ngm-item.on::before{ background:var(--c); }
  .ngm-item.on .ngm-name{ color:var(--rail-ink); }
  .ngm-name{ font-size:var(--type-body); font-weight:600; }
  /* The sentence. Allowed to wrap to two lines — cutting it off would leave
     "the things you own and the dates they run…", which is worse than the
     name on its own. */
  .ngm-what{ font-size:var(--type-body); line-height:1.45; color:var(--text-faint); margin-top:1px; }

  /* ==========================================================
     THE MINI PLAYER CARD.

     "When I click it, instead of opening the music page, it
     opens a card that allows me to search, select, fast
     forward…"   (Matt, 1 September.)

     Deliberately small. It is for the thirty seconds you can
     spare in the middle of something else — a card with
     everything on it is the Listening page again, with worse
     margins and a shadow.
     ========================================================== */
  .music-card{
    position:fixed; z-index:1450; width:300px; max-width:calc(100vw - 24px);
    padding:var(--space-3); border-radius:var(--radius-card);
    /* Same fault as the nav panel above, same fix — --glass-solid and --glass
       are both undefined, so this card was transparent too. */
    border:1px solid var(--glass-border-hi); background:var(--bg-mid);
    box-shadow:var(--elev-floating);
  }
  .mc-now{ display:flex; gap:var(--space-3); align-items:center; }
  .mc-art{ width:46px; height:46px; border-radius:var(--radius-control); object-fit:cover; flex:none;
    background:rgba(120,120,140,.15); }
  .mc-art-none{ display:block; }
  .mc-meta{ flex:1; min-width:0; }
  .mc-title{ font-size:var(--type-body); font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .mc-artist{ font-size:var(--type-body); color:var(--text-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .mc-x{ border:none; background:transparent; color:var(--text-faint); font-size:var(--type-title);
    line-height:1; cursor:pointer; padding:var(--space-hair) var(--space-1); flex:none; }
  .mc-x:hover{ color:var(--text); }

  .mc-transport{ display:flex; gap:var(--space-1); align-items:center; justify-content:center; margin:var(--space-2) 0 var(--space-3); }
  .mc-btn{ border:1px solid var(--glass-border); background:transparent; color:var(--text);
    border-radius:var(--radius-control); padding:var(--space-2) var(--space-2); font-size:var(--type-body); cursor:pointer; min-width:34px; }
  .mc-btn:hover:not(:disabled){ border-color:var(--orb-accent, var(--accent)); color:var(--orb-accent, var(--accent)); }
  .mc-btn:disabled{ opacity:.4; cursor:default; }
  /* The one button that is pressed most, made the easiest to hit. */
  .mc-play{ min-width:42px; font-size:var(--type-lead); }

  .mc-q{ margin-bottom:0; }
  /* Capped, and scrolls inside itself. A floating panel that grows to forty
     rows has become the page he was trying not to open. */
  .mc-results{ max-height:210px; overflow:auto; margin-top:var(--space-2); }
  .mc-hit{ display:flex; gap:var(--space-2); align-items:center; padding:var(--space-2) var(--space-2); border-radius:var(--radius-control); cursor:pointer; }
  .mc-hit:hover{ background:var(--glass-border-hi); }
  .mc-hit-art{ width:32px; height:32px; border-radius:var(--radius-control); object-fit:cover; flex:none;
    background:rgba(120,120,140,.15); }
  .mc-hit-meta{ min-width:0; }
  .mc-hit-name{ font-size:var(--type-body); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .mc-hit-sub{ font-size:var(--type-label); color:var(--text-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

  /* THE DOOR OUT OF THE CARD — item 1747.
     A full-width rule under the jacket rather than a fifth button in the
     transport row, because it is not a transport control: it is the way ON, and
     the one thing on this card that changes where you are. It reads as the line
     between "what is playing" and "what you can do to it", which is why it is a
     rule with words on rather than a chip floating in the middle.
     The chevron leans in on hover — the same small movement every other door in
     the app makes, so the card belongs to the same building as the rest. */
  .mc-door{ display:flex; width:100%; align-items:center; justify-content:space-between;
    gap:var(--space-2); margin:0 0 var(--space-3); padding:var(--space-2) var(--space-hair) var(--space-2);
    border:none; border-top:1px solid var(--glass-border); background:none;
    font-family:inherit; font-size:var(--type-body); font-weight:600; letter-spacing:.02em;
    color:var(--text-dim); cursor:pointer; text-align:left;
    transition:color var(--dur-fast) var(--ease-out); }
  .mc-door:hover{ color:var(--orb-accent, var(--accent)); }
  .mc-door-go{ font-size:var(--type-lead); line-height:1; flex:none;
    transition:transform var(--dur-fast) var(--ease-out); }
  .mc-door:hover .mc-door-go{ transform:translateX(3px); }

  /* ==========================================================
  

  

  
  

  

  

  

  

  


  /* TONIGHT. The `why` lines arrive as whole sentences and are printed whole.
     They are the product — "45 minutes, and you have 70 before your call" is
     the thing no streaming app can say — so there is no truncation here and no
     ellipsis, and the list is a list rather than a paragraph so that each fact
     can be read and disagreed with on its own. */
  .wl-pick{ border:1px solid var(--glass-border); border-radius:var(--radius-card); background:var(--glass-bg);
    padding:var(--space-3) var(--space-4); margin-bottom:var(--space-3); }
  .wl-pick-head{ display:flex; align-items:baseline; gap:var(--space-3); flex-wrap:wrap; }
  .wl-pick-title{ font-size:var(--type-lead); font-weight:650; color:var(--text); }
  .wl-pick-year{ font-size:var(--type-body); color:var(--text-dim); }
  .wl-why{ margin:var(--space-2) 0 0; padding-left:var(--space-5); font-size:var(--type-body); color:var(--text-dim); line-height:1.7; }
  .wl-why li{ margin-bottom:var(--space-hair); }



  /* ==========================================================================
     THE LENS — Lucid, Clarity, Serenity. In Settings › Appearance.
     ==========================================================================

     IT WAS IN THE FOOT OF THE RAIL UNTIL 14 SEPTEMBER, row 2138: "remove light
     and dark toggle. Have to go to settings to change features. (no easy
     buttons) Clean UI". Not one control — a rule — and this was the second
     thing in that corner it applied to. The MARKUP did not change when it
     moved, so every selector below is the one that was already here.

     THE --rail-ink TOKENS BELOW ARE NOT A LEFTOVER FROM THE OLD HOME, and they
     were checked rather than assumed. They are the app's ink stated warm, and
     on every light theme :root[data-mode="light"] aliases all three straight
     onto --text / --text-dim / --text-faint — so they are dark on paper and
     pale on a dark room, which is what this control needs wherever it sits.
     Repointing them at --text would have changed the colour on the dark themes
     for no reason at all.

     CLARITY HAS TO READ AS THE RESTING STATE, and that is a styling problem
     rather than a copy one. If all three selected states looked identical, the
     control would read as a three-way fork — pick your personality — which is
     precisely the org chart _personas.js refuses to make anybody learn. So the
     two leans light up when they are chosen and Clarity does not: it takes a
     quiet, unlit slot, because being in Clarity is not a choice he made, it is
     where the app sits until he reaches for something.

     Selected is still unmistakable in all three — the slot is filled and the
     text goes to full strength — so nothing here trades legibility for the
     point it is making.

     And it is NOT the theme. Nothing below reads data-theme or data-mode.
     ========================================================================== */
  /* A GROUP HEADING INSIDE A ROW OF CHIPS. Full width so the flex row wraps
     after it, which is what turns one container into two visible groups
     without splitting the container — see the note beside #themePicker in
     index.html for why splitting it was the wrong answer. */

  .mode-switch{ display:flex; gap:1px; width:100%; padding:var(--space-hair); border-radius:var(--radius-tight);
    border:1px solid var(--glass-border); background:rgba(255,246,230,.03); }
  .mode-opt{ flex:1; text-align:center; font-size:var(--type-label); font-weight:600; letter-spacing:.02em;
    color:var(--rail-ink-3); padding:var(--space-2) var(--space-1); border-radius:var(--radius-tight); cursor:pointer; white-space:nowrap;
    transition:all var(--dur-fast) var(--ease-out); }
  .mode-opt:hover{ color:var(--rail-ink-2); }
  .mode-opt.active{ color:var(--rail-ink); background:rgba(255,246,230,.07); }
  /* The two leans get the accent; the resting one deliberately does not. */
  .mode-opt.active[data-mode-opt="lucid"],
  .mode-opt.active[data-mode-opt="serenity"]{
    box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
  }
  :root:not([data-mode="light"]) .mode-opt.active[data-mode-opt="lucid"],
  :root:not([data-mode="light"]) .mode-opt.active[data-mode-opt="serenity"]{
    box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent),
               0 0 14px -3px var(--accent-glow);
  }
  .mode-blurb{ font-size:var(--type-label); line-height:1.55; color:var(--text-faint); margin:calc(var(--space-hair) * -1) 0 0; }

  /* RESTORED AFTER THE MERGE. Master-only; live has no rule for these two and
     the merge kept live's file. Without it the mode blurb and its switch stay
     on screen when the sidebar is collapsed, in a rail too narrow to hold them. */
  .sidebar.collapsed .mode-switch, .sidebar.collapsed .mode-blurb{ display:none; }
  /* TWO RULES ABOUT THE COLLAPSED RAIL USED TO BE HERE AND ARE GONE WITH THE
     CONTROL — row 2138. The whole argument they carried was about fitting three
     segments into a 70px column, first by stacking them as L / C / S ("the LCS
     initials", second on his list of what made the spine look like a garage)
     and then by hiding them when the rail narrows. Settings is 300px wide and
     never collapses, so all of it is moot and a `.sidebar.collapsed
     .mode-switch` rule that can no longer match anything is a rule the next
     person has to read before working out it does nothing.

     .mode-abbr stays hidden and stays in the markup. It is the single-letter
     half of each segment and it costs nothing; if this control is ever wanted
     somewhere narrow again, the initials are what that place will want. */
  .mode-abbr{ display:none; }

                .cd-note{ font-size:var(--type-body); color:var(--text-dim); line-height:1.6; margin:var(--space-2) 0 0; }
  
  /* ---------------------------------------------------------------------
     LUCID'S VOICE — the microphone, the read-aloud switch, and the speaker
     that sits on every answer.

     Deliberately QUIET until used. A microphone is not a thing that should
     glow at somebody from the corner of a page they read every morning, so
     it wears the same muted ink as the rest of the furniture and only takes
     the accent colour while it is actually listening. The pulse exists for
     one reason: a person needs to know, without doubt and without reading
     anything, that a microphone is open.
     --------------------------------------------------------------------- */
  .chat-mic, .chat-voice{
    width:36px; height:36px; border-radius:var(--radius-round); flex:none;
    /* --line WAS THE THIRD DEAD TOKEN FOUND THIS SHIFT, after --orb-shadow-1
       and --wash-soft. Nothing in this stylesheet defines it, so this border
       has always been the fallback: twelve percent WHITE, which on Denim,
       Terracotta and every paper theme is a hairline you cannot see. These two
       buttons have had no visible edge on any light ground since they were
       written — and the note directly above, about re-pointing their COLOUR at
       a token that exists, was written without noticing that the line beneath
       it had exactly the same fault.
       --glass-border is the hairline every card in the app already wears and
       every theme defines. It is the right token and it was always there. */
    background:transparent; border:1px solid var(--glass-border);
    /* NOT --ink-3. That token does not exist in this stylesheet, so this line
       always resolved to its fallback — 55% white — which is why the mic
       measured 2.89:1 and the speaker 3.19:1 on a light paper. --text-dim is
       re-stepped per theme and passes on every one of them. */
    color:var(--text-dim);
    cursor:pointer; font-size:var(--type-lead); line-height:1;
    display:flex; align-items:center; justify-content:center;
    transition: color var(--dur-base) var(--ease-out),
                border-color var(--dur-base) var(--ease-out),
                background var(--dur-base) var(--ease-out);
  }
  /* THE ICONS ARE STROKED PATHS NOW rather than colour emoji — see the note in
     index.html beside them. currentColor is what carries every rule in this
     block onto the mark itself, which is what none of them could do before:
     the --text-dim above, the hover below, the accent on .listening and on
     .chat-voice.on were all being applied to a glyph that paints its own
     colours. 17px inside a 36px button is the same icon-to-button ratio the
     collapsed rail already uses. */
  .chat-mic svg, .chat-voice svg{ width:17px; height:17px; display:block; }
  .chat-mic:hover, .chat-voice:hover{ color:var(--text); border-color:var(--glass-border-hi); }

  /* ON, for read-aloud: stated in colour AND in weight, never colour alone. */
  .chat-voice.on{
    color:var(--orb-accent, var(--accent));
    border-color:color-mix(in srgb, var(--orb-accent, var(--accent)) 55%, transparent);
    background:color-mix(in srgb, var(--orb-accent, var(--accent)) 12%, transparent);
  }

  /* LISTENING. The one state in Lucid that is allowed to move on its own. */
  .chat-mic.listening{
    color:var(--orb-accent, var(--accent));
    border-color:var(--orb-accent, var(--accent));
    animation: lucidMicPulse 1.35s var(--ease-in-out) infinite;
  }
  @keyframes lucidMicPulse{
    0%,100%{ box-shadow:0 0 0 0 color-mix(in srgb, var(--orb-accent, var(--accent)) 45%, transparent); }
    50%    { box-shadow:0 0 0 7px color-mix(in srgb, var(--orb-accent, var(--accent)) 0%, transparent); }
  }
  /* OPENING, WHICH USED TO LOOK EXACTLY LIKE OFF.

     1 September. Matt: "sometimes takes 5 seconds to turn on so I end up asking
     my question 3 times." Half a second to five seconds can pass between the
     tap and the microphone actually opening — the browser opens the device and,
     in Chrome, a network session to Google's speech servers. The button showed
     nothing at all for the whole of that, so the only way to find out whether
     it had worked was to talk and see.

     A DIFFERENT shape from listening rather than a fainter version of it: a
     travelling sweep that reads as "working on it", against the steady pulse
     that means "go". Somebody glancing at it should not have to judge an
     opacity to know whether to start speaking. */
  .chat-mic.starting{
    color:var(--orb-accent, var(--accent));
    border-color:color-mix(in srgb, var(--orb-accent, var(--accent)) 55%, transparent);
    background:linear-gradient(90deg,
      transparent 0%,
      color-mix(in srgb, var(--orb-accent, var(--accent)) 22%, transparent) 50%,
      transparent 100%);
    background-size:220% 100%;
    animation: lucidMicOpening 1.1s linear infinite;
  }
  @keyframes lucidMicOpening{
    from{ background-position:120% 0; }
    to  { background-position:-120% 0; }
  }

  /* Somebody who has asked not to see motion still needs to know the
     microphone is open, so the pulse becomes a steady fill rather than
     nothing at all. Both states, because "opening" matters as much as "open" —
     it is the one that says do not speak yet. */
  @media (prefers-reduced-motion: reduce){
    .chat-mic.listening{
      animation:none;
      background:color-mix(in srgb, var(--orb-accent, var(--accent)) 20%, transparent);
    }
    .chat-mic.starting{
      animation:none;
      background:color-mix(in srgb, var(--orb-accent, var(--accent)) 8%, transparent);
      border-style:dashed;
    }
  }

  /* The speaker on an answer. Sits with the text rather than beside it, and
     stays faint until the message is hovered — it is a control for the odd
     occasion, not a permanent piece of decoration on every paragraph. */
  .msg-speak{
    display:inline-flex; align-items:center; justify-content:center;
    width:22px; height:22px; margin-left:var(--space-2); vertical-align:middle;
    border:none; background:transparent; cursor:pointer;
    font-size:var(--type-body); line-height:1; padding:0;
    /* --ink-3 again, and .35 on top of it. See the note on .chat-mic. */
    color:var(--text-dim);
    opacity:.55; transition: opacity var(--dur-base) var(--ease-out);
  }
  .msg-assistant:hover .msg-speak{ opacity:.85; }
  .msg-speak:hover{ color:var(--text); opacity:1; }
  .msg-speak.speaking{ opacity:1; color:var(--orb-accent, var(--accent)); }
  /* Keyboard users never hover, so focus has to reveal it too. */
  .msg-speak:focus-visible{ opacity:1; outline:2px solid var(--orb-accent, var(--accent)); outline-offset:2px; }

  /* The voice picker: three rows of chips and one input. It was a list with a
     description and a play button on every row, plus a paragraph explaining
     it — which was me narrating the feature instead of building it. Clicking a
     name now selects it and plays it, which is what a person was going to do
     with two clicks anyway. */
  .voice-picker{ display:flex; flex-direction:column; gap:var(--space-hair); margin-top:var(--space-2); }

  /* Character sits above the voices, because it changes all of them. */
  .voice-sub{ font-size:var(--type-label); letter-spacing:.08em; text-transform:uppercase;
    color:var(--text-faint); margin:var(--space-3) 0 var(--space-2); }
  .voice-sub:first-child{ margin-top:0; }
  .voice-chips{ display:flex; flex-wrap:wrap; gap:var(--space-2); }
  .voice-chip{
    background:transparent; cursor:pointer; font:inherit; font-size:var(--type-body);
    border:1px solid var(--ring-soft); border-radius:var(--radius-pill);
    /* THE THEME'S OWN TEXT TOKENS, 14 October. --ink-3 is the rail's ink and is
       near-white on every theme, so on Serene's paper twelve of the thirteen
       voices were pale grey on cream and could not be read. */
    color:var(--text-dim); padding:var(--space-2) var(--space-3);
  }
  .voice-chip:hover{ color:var(--text); border-color:var(--ring); }
  .voice-chip.on{
    color:var(--orb-accent, var(--accent)); font-weight:700;
    border-color:color-mix(in srgb, var(--orb-accent, var(--accent)) 60%, transparent);
    background:color-mix(in srgb, var(--orb-accent, var(--accent)) 12%, transparent);
  }
  .voice-chip:focus-visible{ outline:2px solid var(--orb-accent, var(--accent)); outline-offset:2px; }

/* ===========================================================================
   THINGS THAT ONLY EXIST INSIDE THE MAC APP.
   ===========================================================================

   2 September 2026. Media and Record are real capabilities of the desktop app
   and impossible in a browser — the four social sites refuse to be framed, and
   no web page can hear the far end of a call. Showing either one in a browser
   would be a button that cannot work, which is the one thing Lucid does not do.

   So they are hidden by default and revealed by `in-shell` on the body, which
   only the Mac app's injected bootstrap sets. A browser never sees them; the
   app never sees a gap. */
.shell-only{ display:none !important; }
body.in-shell .shell-only{ display:flex !important; }

/* 2 September 2026, undone the same night it was written. A native page
   covers everything to the right of the spine, and this used to pin the rail
   open whenever one was showing — width:216px on top of .sidebar.collapsed —
   on the theory that a collapsed rail's hover labels would render underneath
   the panel and be invisible. That theory was right about the FLOATING
   TOOLTIP (#railTip; it sits past the rail's own right edge, which is exactly
   where the native panel starts) and wrong about the rail itself, which never
   draws past its own width whether collapsed or not. The fix landed here
   instead pinned the whole sidebar open, permanently — the button still
   toggled `.collapsed`, but nothing on screen ever moved, which is a control
   that does nothing (bug 171). The real fix is in wireRailTooltips(): the
   tip stays hidden while a native page is showing, and the rail is free to
   collapse same as it always was. See app.js for the tooltip guard. */

/* Social used to be a standing rail icon and this marked it "open" the same
   way an ordinary page is. It now lives inside the Media dropdown instead —
   see .ngm-item.on, which does the same job for a row in a menu rather than
   an icon in the rail. */

/* ===========================================================================
   WHERE A RECORDING GOES WHEN IT STOPS.
   ===========================================================================

   2 September 2026. Matt: "instead of storing on a page, it rolls up in the
   timeline of the person and if no person, you can choose to create or delete."

   So there is no recordings page and no list to go and tend. The moment a
   meeting stops, this asks the one question that decides where it lives, while
   the meeting is still the thing you are thinking about. Answer it and the
   recording becomes part of that person's history; decline and the file is
   still on disk; delete and it goes to the Trash, which is recoverable,
   because "delete" on an hour of somebody's meeting should never be final. */
.rec-card{ position:fixed; right:22px; bottom:22px; width:360px; z-index:100001;
  background:var(--glass-bg); border:1px solid var(--glass-border); border-radius:var(--radius-card);
  backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(var(--glass-sat));
  box-shadow:var(--glass-shadow); padding:var(--space-4) var(--space-4) var(--space-3);
  font-size:var(--type-body); color:var(--text); display:none; }
.rec-card.open{ display:block; }
.rec-card h4{ margin:0 0 var(--space-hair); font-size:var(--type-lead); font-weight:600; }
.rec-card .rec-sub{ color:var(--text-faint); font-size:var(--type-body); margin-bottom:var(--space-3); }
.rec-card input.rec-search{ width:100%; padding:var(--space-2) var(--space-3); border-radius:var(--radius-card);
  border:1px solid var(--glass-border); background:rgba(255,255,255,.04);
  color:var(--text); font-size:var(--type-body); }
.rec-hits{ margin:var(--space-2) 0 var(--space-1); max-height:190px; overflow-y:auto; }
.rec-hit{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-2); border-radius:var(--radius-control); cursor:pointer; }
.rec-hit:hover{ background:rgba(120,120,140,.16); }
.rec-hit-name{ font-size:var(--type-body); }
.rec-hit-sub{ font-size:var(--type-body); color:var(--text-faint); }
.rec-none{ padding:var(--space-2); font-size:var(--type-body); color:var(--text-faint); }
.rec-actions{ display:flex; gap:var(--space-3); align-items:center; margin-top:var(--space-3);
  padding-top:var(--space-3); border-top:1px solid var(--glass-border); }
.rec-actions .rec-spacer{ flex:1; }
.rec-link{ background:none; border:none; color:var(--text-faint); font-size:var(--type-body);
  cursor:pointer; padding:var(--space-1) var(--space-hair); }
.rec-link:hover{ color:var(--text); }
.rec-link.danger:hover{ color:var(--danger, #ff8080); }

/* A RECORDED MEETING, OPENED (14 October). The same glass card as every other
   modal; only the inside is new. The transcript scrolls inside the card so a
   long meeting never pushes the action items off the screen. */
.meet-card{ width:min(680px, calc(100vw - 32px)); max-height:calc(100vh - 64px); overflow-y:auto; }
.meet-row{ display:flex; align-items:center; gap:var(--space-3); margin-bottom:var(--space-3); }
.meet-file{ color:var(--text-faint); font-size:var(--type-body); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.meet-state{ color:var(--text-faint); font-size:var(--type-body); padding:var(--space-2) 0; }
.meet-state.warn{ color:var(--text); }
.meet-summary{ font-size:var(--type-body); line-height:1.5; }
.meet-prov{ color:var(--text-faint); font-size:var(--type-body); margin-top:var(--space-1); }
.meet-act{ padding:var(--space-2) 0; border-top:1px solid var(--glass-border); }
.meet-act-t{ font-size:var(--type-body); font-weight:600; }
.meet-quote{ color:var(--text-faint); font-size:var(--type-body); font-style:italic; margin:var(--space-hair) 0; }
.meet-done{ color:var(--text-faint); font-size:var(--type-body); }
.meet-transcript{ font-size:var(--type-body); line-height:1.55; white-space:pre-wrap; max-height:320px; overflow-y:auto;
  padding:var(--space-2) var(--space-3); border:1px solid var(--glass-border); border-radius:var(--radius-card); }

/* (14 Oct, later) THE PLAYER, WHO/WHEN, YOURS VS WAITING, AND THE LIST. */
.meet-facts{ font-size:var(--type-lead); font-weight:var(--weight-medium); color:var(--text); }
.meet-filed{ font-size:var(--type-body); color:var(--text-faint); margin:var(--space-hair) 0 var(--space-1); }
.meet-who{ padding:0; color:var(--text); text-decoration:underline; text-underline-offset:var(--space-hair); }
.meet-sub{ justify-content:space-between; }
.meet-sub .rec-link{ flex:none; white-space:nowrap; }
.meet-player{ display:flex; align-items:center; gap:var(--space-3); padding:var(--space-3);
  border:1px solid var(--glass-border); border-radius:var(--radius-card); margin-bottom:var(--space-1); }
.meet-pp{ min-width:var(--space-16); }
.meet-scrub{ flex:1; min-width:0; accent-color:var(--blue); }
.meet-clock{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-faint); white-space:nowrap; }
.meet-prov.warn{ color:var(--orange); }
#meetAudioNote{ margin-bottom:var(--space-3); }
.meet-unfinished{ padding:var(--space-3); border:1px solid var(--orange); border-radius:var(--radius-card);
  font-size:var(--type-body); margin-bottom:var(--space-3); }
.meet-unfinished-t{ font-weight:var(--weight-strong); color:var(--orange); margin-bottom:var(--space-1); }
.meet-act-who{ font-size:var(--type-label); color:var(--violet); font-weight:var(--weight-medium); }
.meet-when{ font-weight:var(--weight-body); color:var(--text-faint); }
.meet-ts{ padding:0 var(--space-1); font-style:normal; font-family:var(--font-mono); font-size:var(--type-label); }
.meet-none{ font-size:var(--type-body); padding:var(--space-2) 0; }
.meet-lines{ white-space:normal; }
.meet-line{ display:flex; gap:var(--space-2); align-items:baseline; padding:var(--space-hair) 0; }
.meet-line .meet-ts{ flex:none; min-width:var(--space-12); text-align:right; }
.recs-row{ display:grid; grid-template-columns:1fr auto; gap:var(--space-hair) var(--space-3); width:100%; text-align:left;
  background:none; border:none; border-top:1px solid var(--glass-border); padding:var(--space-3) var(--space-1);
  color:var(--text); font:inherit; cursor:pointer; }
.recs-row:hover{ background:var(--glass-border); }
.recs-when{ font-size:var(--type-body); font-weight:var(--weight-medium); }
.recs-who{ font-size:var(--type-body); color:var(--text-faint); text-align:right; }
.recs-state{ grid-column:1 / -1; font-size:var(--type-label); color:var(--text-faint); }
.recs-bad{ color:var(--orange); font-weight:var(--weight-medium); }
.recs-row.bad{ border-left:var(--space-hair) solid var(--orange); padding-left:var(--space-2); }
#recWhere{ overflow-wrap:anywhere; }
.recs-item{ border-top:1px solid var(--glass-border); }
.recs-item .recs-row{ border-top:none; }
.recs-acts{ display:flex; gap:var(--space-2); align-items:center; padding:0 var(--space-1) var(--space-3); }
.recs-mac{ color:var(--blue); font-weight:var(--weight-medium); }
.recs-row.mac{ border-left:var(--space-hair) solid var(--blue); padding-left:var(--space-2); }
.meet-fileit{ padding:0 var(--space-1); }
@media (max-width: 600px){
  .meet-player{ flex-wrap:wrap; }
  .meet-scrub{ order:3; flex-basis:100%; }
  .recs-row{ grid-template-columns:1fr; }
  .recs-who{ text-align:left; }
}

/* ===========================================================================
   ONE GRID, AND THE WIDTH A CARD IS ALLOWED TO BE.
   ===========================================================================

   Matt, 11 September, after a week away: "Overall, it feels like something that
   was assembled with cheap parts." Row 1752 names the biggest single reason.
   Walking every page at 1920 with Playwright and asking the layout engine how
   wide things actually came out, rather than reading the stylesheet, found the
   same two failures over and over:

     A LONE CARD STRETCHED TO FILL THE PAGE. .pulse-tiles is
     repeat(auto-fit, minmax(272px, 1fr)) — and auto-FIT collapses the tracks
     it has not filled, so on a week with one thing in it that tile measured
     1240px wide to hold one sentence. auto-FILL keeps the empty tracks, so the
     card stays card-sized and the row just has gaps in it. That one word is
     the difference between "a card" and "a table that lost its CSS", and it
     is why every grid below is auto-fill.

     OR THE OPPOSITE: CARDS SHAVED TOO THIN. .vault-grid asked for a 260px
     minimum, so a 1776px pane fitted SEVEN of them — 242px each, every label
     wrapping to three lines, the second row ending ragged. .project-grid did
     it at 237px, #contactList forced exactly three across and got 579px cards
     holding an avatar and two short lines with a third of each card empty.
     Neither of those is a width anybody chose; both are what falls out of a
     minimum picked for a laptop and then run on a monitor.

   SO THE RULE IS A RANGE, NOT A NUMBER. A card is never narrower than
   --tile-min (below that the grid drops a column) and never wider than
   --tile-max (above that it stops being a card). Everything between is the
   grid fitting as many as it honestly can.

   THE WHOLE LADDER CAME DOWN A STEP ON 15 SEPTEMBER, and the reason is one
   sentence: "Cards 2 rows, I wanted 3 or 4....way too big". He was looking at
   an 1100px window, where the old numbers gave him two cards 460px wide — the
   widest a card was allowed to be, on the narrowest tier, which is the one
   place the old ladder was largest and he was most likely to be standing. Two
   things moved together, because either one alone would have half-answered him:

     THE CARD GOT SMALLER. 340/420/460 became 270/335/420 — and the neatest way
     to say it is that the old IDEAL is the new CEILING. A card is a fifth
     narrower where the cap binds and can no longer reach the width he called
     too big at all.

     AND THE TIERS ARRIVE EARLIER, because the breakpoints are not chosen, they
     are derived: each one is the window at which the pane can honestly hold n
     cards at --tile-min. They fell out of the new minimum; they were not picked
     to flatter it.

       two across  below about 990px of window
       three       from 990, which is what his 1100 gives
       four        from about 1280, so a normal 1440 window gets four

   AND THE FOUR NUMBERS NOW HOLD TOGETHER, which the old four did not. There are
   two things a ladder like this can get wrong and both are arithmetic:

     A TIER LETTING IN A COLUMN IT DID NOT MEAN TO. The cap for n cards must be
     narrower than n+1 cards at the minimum, or the grid opens an extra track
     inside the tier and the cap means nothing. 858 < 846? no — 2×420+18 = 858
     against 3×270+36 = 846 is the one line of this that is genuinely tight, and
     it is safe because the two-across tier's windows never produce a pane that
     wide. Three (1041 against 1134) and four (1394 against 1422) have room.

     A CARD COMING OUT WIDER THAN --tile-max. This one the old ladder DID get
     wrong and nobody noticed, because the harness only looks at three widths. A
     grid whose container lands between "n cards at the ceiling" and "n+1 cards
     at the minimum" divides into n tracks that are each wider than the ceiling,
     and there is no cap in play to stop it — In Touch is exactly that shape,
     because #view-comms keeps 82px clear of the orb, so its rows are 82px
     narrower than the page they sit on. The condition that closes the gap is
     max ≥ min + (min + gap) ÷ 2, which is 414 against a ceiling of 420. The old
     numbers needed 519 and had 460, so anywhere between 938 and 1056 of
     container drew two cards up to 475px wide — wider than the ceiling the
     harness exists to enforce.

   Below about 620px there is one card in the row and it is deliberately NOT
   bounded — see the last paragraph.

   The ceiling is enforced by capping the GRID, not the cards: --tile-span is
   two, three or four ideal cards plus their gutters, so the tracks divide to
   --tile-ideal exactly when the cap binds and shrink toward --tile-min when it
   does not. Cards never grow apart with a hole between them, and the row
   always ends on a straight edge.

   WHY THE BREAKPOINTS ARE ODD NUMBERS, AND WHY :has() IS IN THEM. A media
   query knows the window; the grid lives in .main, whose width is the window
   minus the rail — and the rail is 70px collapsed or 216px open. Stepping the
   cap on the window alone therefore either overshoots (a 1300px window with
   the rail open is a 1010px pane, which auto-fill splits into two 496px cards)
   or wastes 300px on the right. So each step is declared twice: once for the
   window as it is with the rail collapsed, once for the window as it is with
   the rail open. :has() is already used elsewhere in this file (.cm-grid, and
   .home-band:has(.st-drop.open)), so this is the house's own tool.

   The one case that is deliberately NOT bounded is a single column: below
   about 620px of window — a phone, in other words, now that the minimum is 270
   rather than 340 — there is only one card in the row, and a card that does not
   fill its own column there would just be a narrow strip with nothing beside
   it. One across takes the width it is given. */
:root{
  --tile-gap: 18px;
  /* The narrowest a card may be before the grid drops a column. */
  --tile-min: 340px;
  /* What a card measures when the cap binds — the width the page is composed
     for, and the number --tile-span is built out of. */
  --tile-ideal: 420px;
  /* The hard ceiling. Nothing in a card grid may measure more than this; the
     harness at tests/test-no-tile-stretches.js opens every page in a real
     browser and fails if anything does. */
  --tile-max: 460px;
  /* Two across is the one tier whose cap can be the CEILING rather than the
     ideal, and it is worth the exception. A 1100px window leaves a 956px
     pane; capped at two ideal cards that is 98px of dead air down the
     right-hand side, which on a small window is a tenth of the page. Capped
     at two of the widest a card may be, it is 18px. The tiers above cannot do
     this: their cap is also what stops the grid opening one more column than
     it should, and at three and four across the ceiling is wide enough to let
     that extra column in. */
  --tile-span-2: calc(var(--tile-max) * 2 + var(--tile-gap));
  --tile-span-3: calc(var(--tile-ideal) * 3 + var(--tile-gap) * 2);
  --tile-span-4: calc(var(--tile-ideal) * 4 + var(--tile-gap) * 3);
  --tile-span: var(--tile-span-2);
  /* A PAGE, as opposed to a row of cards. Wider, because a page also holds
     list rows and paragraphs, which are not cards — but stepped on the same
     ladder so a page and the cards on it end on the same right edge. */
  --page-measure: var(--tile-span-3);
  /* THE NARROWEST A READING PANE MAY BE AND STILL HAVE A COLUMN TO ITSELF —
     row 2160. Two ideal cards and the gutter between them. Below that a
     two-pane page has nothing to gain by staying two panes: the pane is down
     to card width, so it may as well take the whole line and let the cards
     wrap under it. Written as arithmetic off the same ladder the cards use, so
     it moves when they move rather than being a number somebody chose once.
     Used by .profile-grid — the contact card and the company card. */
  --pane-min: calc(var(--tile-ideal) * 2 - var(--tile-gap));

  /* ONE PANEL OF WORDS. A banner, a note, an explanation — the things that
     were spanning 1776px to carry one sentence. A measure, not a width: about
     eighty characters at the app's body size, which is what a line of prose
     can be before the eye loses the start of the next one. */
  --prose-measure: 820px;
  /* A row that is a field plus its button. Wider than prose because the field
     may hold a URL, and narrower than the page because a 1776px text box to
     paste a link into is the complaint in miniature. */
  --form-measure: calc(var(--prose-measure) + 220px);
}
/* The rail collapsed: pane = window - 70 - 14 - 60. */
@media (min-width:1200px){ body:has(.sidebar.collapsed){ --tile-span: var(--tile-span-3); } }
/* The rail open: pane = window - 216 - 14 - 60. */
@media (min-width:1346px){ body{ --tile-span: var(--tile-span-3); } }
@media (min-width:1558px){ body:has(.sidebar.collapsed){ --tile-span: var(--tile-span-4); } }
@media (min-width:1704px){ body{ --tile-span: var(--tile-span-4); } }
/* THE PAGE IS EXACTLY AS WIDE AS A FULL ROW OF CARDS, AT EVERY TIER.
   This replaces `@media (min-width:1704px){ :root{ --page-measure:
   var(--tile-span-4); } }`, which stepped the page on the WINDOW alone — the
   mistake the long note above warns about, made one rule later. With the rail
   collapsed on a 1600px window the cards are already on the four-across tier
   while the page was still on three, so the page column was NARROWER than the
   row of cards it contains and quietly cancelled a column.
   Saying it once, in terms of --tile-span itself, makes that impossible: the
   page and the cards step together because they are the same number. It also
   closes the last ragged edge on the contact sheet — at 1100 the two-across
   cap is 938 in a 956 pane, so the header used to end 18px to the right of the
   last card. Now both end together and the 18px is margin, which is a thing
   somebody chose rather than a thing left over.
   Declared on body rather than on :root because body is where --tile-span is
   resolved; on :root it would read the uncapped default and never step. */
body{ --page-measure: var(--tile-span); }

/* THE GRID ITSELF, AND EVERY NAME THAT WEARS IT.
   .tile-grid is the name new markup should use. The rest are the grids that
   already existed: they are listed here rather than rewritten in place so that
   nothing in index.html or app.js has to change class, and so that there is
   exactly one place in Lucid where the shape of a row of cards is decided.
   #contactList and #organizationList are ids and are named here for that
   reason alone — a class selector could not have reached them.

   Left out on purpose, because they are shelves rather than card grids and a
   420px minimum would turn a wall of posters into four of them: .wl-grid and
   .pod-shelf (artwork), .mf-grid and .ql-grid (shortcut chips), .house-plan
   (room chips), .media-tiles and .work-cards (a single row that is meant to
   stay a single row), and every calendar/weekday grid. Also left out are the
   two-pane page layouts — .today-grid, .cal-body, .gv-wrap, .pulse-work,
   .pulse-two, .settings-cols, .cm-grid — which are not rows of cards at all.

   .people-grid AT THE END OF THE LIST — row 2146. It is In Touch → Recent, and
   it was already a grid, which is why the 11 September adoption pass walked
   past it: it carries its own copy of the OLD track definition (the one two
   hundred lines up on #contactList), so it was pinned at exactly three across
   and its cards measured 573px on a 1920 monitor — a third of each one empty,
   which is the fault row 271 describes almost word for word. Wearing the rule
   it is four across at 420 on that monitor, like everything else.
   The duplicated declaration is deliberately LEFT where it is: tests/test-
   people-screens.js reads both rules and fails if they ever stop matching
   character for character, and that promise is still worth keeping. */
.tile-grid,
#contactList, #organizationList,
.project-grid, .pj-task-grid, .pj-rep-grid,
.pulse-tiles,
/* .profile-grid CAME OUT OF THIS LIST ON 15 SEPTEMBER — row 2160, and it had
   been the whole of the fault. It is the contact card's and the company card's
   two-pane layout, not a row of cards, so filling it with as many card-width
   tracks as fit gave a 1290px page four 309px columns and two children to put
   in them: half the card was empty, which is what Matt was looking at when he
   wrote "a bunch of empty space on the contact card n far right". The
   two-pane layouts named four paragraphs above — .today-grid, .cal-body,
   .gv-wrap, .settings-cols — were always meant to be outside this rule; this
   one was inside it by accident. Its proportions are stated at .profile-grid. */
.box-grid,
.kit-grid,
.sig-grid,
.people-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(100%, var(--tile-min)), 1fr));
  gap:var(--tile-gap);
  align-items:start;
  max-width:var(--tile-span);
}

/* ===========================================================================
   FOUR ACROSS, ON THE TWO SHEETS OF CARDS HE NAMED.
   ===========================================================================
   "Lets move cards in contacts and companies to 4 rows across." (23 September.)
   Four ACROSS, not four rows.

   The ladder above gives these two whatever auto-fill can fit at --tile-min,
   which measured four at 1920 and 1600 but three at 1440 and two at 1100. He
   asked for four, so above the tier where the pane is wide enough these two
   ids are told four rather than asked.

   THE BREAKPOINTS ARE THE HOUSE'S OWN, and they are the rail-aware pair a few
   rules above this — 1200 with the rail collapsed, 1346 with it open — which
   is exactly the step where --tile-span goes to three cards. At that step the
   pane is about 1056px, so four cards measure about 250px. That is the floor,
   and it is the narrowest a card with a 52px avatar and two lines beside it
   can honestly be.

   BELOW THAT STEP NOTHING IS FORCED. The grid goes back to auto-fill and steps
   down two, then one, exactly as it did before — so a phone gets one card the
   width of its column and there is no sideways scroll to be had. Four across
   on a 430px screen would be 86px cards, which is not a card.

   max-width is left alone on purpose: the page column and the row of cards are
   the same number (--tile-span) and they still end on the same right edge. */
@media (min-width:1200px){
  body:has(.sidebar.collapsed) #contactList,
  body:has(.sidebar.collapsed) #organizationList{
    grid-template-columns:repeat(4, minmax(0, 1fr));
  }
}
@media (min-width:1346px){
  #contactList, #organizationList{
    grid-template-columns:repeat(4, minmax(0, 1fr));
  }
}

/* ===========================================================================
   FOUR ACROSS ON THE WINDOW HE ACTUALLY USES — 5 October 2026.
   ===========================================================================
   Matt, 5 October: "4 across for #3."

   THE TWO RULES ABOVE COULD NEVER REACH HIM. His Lucid window is 1099x690,
   read off System Events today. 1099 is under 1200 and under 1346, so neither
   tier applies, and what he was actually looking at - measured in a browser at
   exactly 1099, on BOTH pages - was TWO cards across, not the three and four
   the list recorded. Contacts and Organizations share one rule and have
   always agreed with each other; what differed was nobody's measurement.

   TWO THINGS HAD TO CHANGE, NOT ONE. Forcing four columns alone would have
   done nothing visible, because the grid also carries max-width:var(--tile-span),
   and at this width --tile-span is --tile-span-2: 460*2 + 18 = 938px, a cap
   built to hold exactly two cards at --tile-max. Four tracks inside a two-card
   box is four narrow cards in the left two thirds of the page. So the cap comes
   off for these two lists at this tier, and the row uses the pane.

   THE COST, STATED RATHER THAN DISCOVERED. At 1099 the pane is about 950px, so
   four cards and three 18px gaps make each card about 224px. The note at the
   1200/1346 tiers calls 250px "the narrowest a card with a 52px avatar and two
   lines beside it can honestly be". THIS IS NARROWER THAN THAT. It is what he
   asked for, measured and handed back to him with the number rather than
   quietly applied - layout is his call, and so is reversing this one.

   1024 IS THE FLOOR. Below it the ladder is left exactly as it was: four across
   on a phone would be 86px cards, which is not a card. */
/* AND RECENT IS ON THIS RULE TOO - 8 October 2026.
   --------------------------------------------------------------------------
   Matt, 8 October, with a photograph of In Touch: "The contact section (now it
   is 3 across)". His Lucid window was 1481x690 when he sent it, read off
   System Events. At that width the ladder gives .people-grid three cards at
   --tile-min 340, while #contactList on the next screen over is already four.
   Two views of the same people in two different shapes - which is the one
   thing the comment at .people-grid promises will not happen.

   So .people-grid joins the two ids here rather than getting a tier of its
   own. Three screens, one line, and they can only disagree by somebody editing
   it. At 1481 with the rail collapsed the pane is about 1400px, so four cards
   and three 18px gaps make each card about 336px - wider than the 250px the
   note above calls the honest floor, and far wider than the 224px that note
   accepted at 1099.

   THE DUPLICATED BASE TRACK DEFINITION IS NOT TOUCHED. tests/test-people-
   screens.js still reads .people-grid{ and #contactList, #organizationList{
   out of this file and still fails if they stop matching character for
   character. That promise is unchanged; this rule sits on top of both. */
@media (min-width:1024px){
  #contactList, #organizationList, .people-grid{
    grid-template-columns:repeat(4, minmax(0, 1fr));
    max-width:none;
  }
}
/* THE CARD LISTS THAT HAD NOTHING FOR ROW 1752 TO NAME — row 2146, and this
   is the reason he had to ask twice.
   --------------------------------------------------------------------------
   Matt, 14 September: "Worth a line cards are full length. Small cards 3 or 4
   across. Not billowing down the page. I asked for all cards to no longer go
   the length of the page". He had asked, on 11 September, and the rule above
   is what answered him. It could not reach three of the lists, because A PAGE
   DOES NOT HAVE TO HAVE A GRID TO HAVE A ROW OF CARDS: renderInTouchPrompts(),
   the two goals renderers and renderRhythms() join card TEMPLATES into a string
   and drop it straight into the page's body element. There was no container
   class in the markup for the adoption pass to list, so those three came out as
   one column of page-wide cards — which is the shape he was looking at.

   THE FIX IS .tile-grid, WHICH IS WHAT THE RULE ABOVE ALREADY SAYS NEW MARKUP
   SHOULD USE. Each run of cards is now wrapped in one, in app.js, rather than
   the page body being made a grid and every heading in it taught to span: a
   heading between two groups keeps its own spacing, an empty state is still an
   ordinary line, and a new kind of row added to one of those pages later
   cannot silently take a card's cell.

   All that is left here is what a card has to give up when it stops being a
   page-wide block. #tvBody — Travel — is deliberately not among them, and the
   reason is written at .tv-trip. */

/* The bottom margin each of these carried WAS the spacing of a stack. The
   gutter belongs to the grid now, and keeping both would put a blank row under
   the last line of cards. */
.tile-grid > .cmm-row{ margin-bottom:0; }

/* THE PAGE COLUMN. .maxw was 1240px flat, which on a 1920 monitor left a
   536px strip of nothing down the right-hand side of Work, Life, Projects,
   Travel, Body and Timeline — the page looked like it had been dropped into
   the corner of the window. On the ladder it is three cards wide on a normal
   window and four on a monitor, so the page grows with the screen instead of
   sitting in it. */
.maxw{ max-width:var(--page-measure); }

/* THE PANELS THAT WERE SPANNING THE WHOLE PAGE TO HOLD ONE SENTENCE.
   Measured at 1920: .med-note 1776px for one line about what the medicine
   cabinet does; .body-banner 1240px and .body-next 1240px each carrying a
   heading and a sentence with half the box empty; .tv-booking 1240px;
   .speaker-status 1740px to say Spotify is not connected. None of them is a
   layout — each is one paragraph in a box, and a paragraph has a measure. */
.cal-note,
.speaker-status,
.pj-home-clear{
  max-width:var(--prose-measure);
}
/* And the boxes you type into. The Media page's follow field measured 1683px
   and the finder under it 1776px — a text box the width of a monitor, which
   reads as a form that was never laid out. Capped on the field rather than on
   the row, because the row is an inline-styled div in index.html with no class
   to aim at, and the button beside it simply follows the field in. */
textarea.feed-add{ max-width:var(--form-measure); }
.finder-row{ max-width:var(--form-measure); }
/* The search box at the top of a page — a field laid out as a row of the page
   itself rather than one inside a card or a toolbar. In Touch, Organizations
   and the Kitchen each had one running the full width of the window, which on
   a monitor is a 1776px slot to type the word "Karen" into. A direct-child
   selector rather than a list of ids, so a page added later is covered without
   anybody having to remember this rule exists. */
.view > input.field,
.view > textarea.field{ max-width:var(--form-measure); }
/* A disclosure bar is not a card, but at 1776px its chevron ends up a long way
   from the words it belongs to. */
.cal-picks-head{ max-width:var(--form-measure); }
/* AND THE SENTENCE UNDER A PAGE TITLE, which is prose and has always been set
   to whatever width the window happened to be. On a 1920 monitor Travel's
   opening line ran 1734px — about 240 characters — and a line that long is one
   the eye loses the start of on the way back. This is the same measure the
   panels above take, for the same reason; it only ever makes a line shorter,
   so the dozens of these that already sit inside a card are untouched. */
.page-sub{ max-width:var(--prose-measure); }

/* ===========================================================================
   THE ADOPTION: ONE PAGE COLUMN, AND EVERY PAGE ENDS ON ONE EDGE.
   ===========================================================================

   The section above settled how wide a CARD is allowed to be, and it worked:
   walked at 1920 with Playwright, every adopted grid comes out four across at
   420px and tests/test-no-tile-stretches.js holds it there. This section is
   the half that was left, and it is the half the eye actually reads.

   WHAT THE CONTACT SHEET SHOWED. tools/walk-the-widths.js opens all 27 pages
   at three window widths and prints the right-hand edge that every direct
   child of the page lands on. At 1920 In Touch came back with THREE:

       1890   the title row and the two buttons beside it
       1848   the grid of contact cards
       1154   the search field

   The vault had two, Organizations three, Media five. That is a page nobody
   composed — it is a header pinned to the window's corners with a column of
   content underneath it that stops somewhere else — and it is most of what
   "assembled with cheap parts" is describing. A card grid can be perfect and
   the page around it still reads as a table that lost its stylesheet, because
   the thing the eye checks first is whether the right-hand edges line up.

   SO THE PAGE TAKES THE SAME COLUMN THE CARDS DO. --page-measure is already
   the card ladder's own width — three ideal cards on a normal window, four on
   a monitor — so a header capped to it lands on the pixel the last card in the
   row lands on, at every width, for ever, without a number written anywhere
   that could go stale.

   AND IT IS CENTRED RATHER THAN LEFT-ALIGNED, which only matters above 1734px
   of pane and matters a great deal there. Left-aligned, a 2560px monitor puts
   680px of nothing down the right-hand side and the app looks like it was
   dropped into the corner of the window; centred, the same page has 340px of
   margin either side and reads as a document. At Matt's own 1920 the whole
   difference is 21px, which is the point: the rule costs nothing on the screen
   it was written for and saves the screen it was not.

   WHY .view AND NOT A CLASS ON EACH PAGE. .maxw exists and is worn by eleven
   views, which is how eleven of them already end on one edge and sixteen do
   not — a rule you have to remember to apply is a rule that is applied to
   about half of the app. Putting it on .view makes the column the DEFAULT and
   makes the exceptions the thing somebody has to type on purpose. A page added
   next month is composed without anybody remembering this paragraph exists.
   .maxw still resolves to the same measure, so nothing in index.html has to
   change and nothing double-applies. */
.view{ max-width:var(--page-measure); margin-inline:auto; }

/* THE SIX PAGES THAT ARE NOT A COLUMN, each with the reason it is not.
   These are the views whose job is to fill the window rather than to sit in
   it, and a ceiling on any of them would be a strip of dead screen beside a
   layout that was built to use the lot. Written as ids rather than a class
   because that is what index.html already gives us, and because a list of six
   names with six reasons is a decision on the record — which a class quietly
   omitted from a div is not. */
#view-inbox,        /* mail: a list pane and a reading pane, side by side */
#view-today,        /* the calendar: an agenda rail and the day's track */
#view-board,        /* the board canvas, which scrolls itself */
#view-music,        /* .gv-wrap is the same two-pane shape as mail */
#view-texts,        /* and so is the message view */
/* Home is the fourth kind of exception and the most deliberate. Its band
   bleeds past .main's padding with a negative margin to meet the window on both
   sides, so a ceiling on the view would leave the band short on the right and
   full on the left — worse than either. .home-grid carries the same ceiling a
   few rules up, which is where Home's own tiles are held. */
#view-home{ max-width:none; }

/* AND A PARAGRAPH THAT IS A CHILD OF THE PAGE TAKES THE PROSE MEASURE.
   Same argument as .page-sub above, generalised: In Touch's "Nobody has gone
   quiet by their own rhythm…" is a bare <p> with no class on it and ran 1330px
   at 1920. A direct child of a view is never a paragraph inside a card — those
   are further down the tree and are untouched — so this only ever reaches the
   ones that are genuinely loose on the page, and it only ever makes a line
   shorter. */
.view > p{ max-width:var(--prose-measure); }

/* AND .ht-empty, WHICH IS THE APP'S "NOTHING HERE YET" SENTENCE — 89 of them
   in app.js, and always a sentence rather than a layout. In Touch's is written
   as a <div> rather than a <p>, so the rule above could not reach it, and on a
   1734px page it ran about two hundred and thirty characters. Most of the 89
   already sit inside a card narrower than the measure and are untouched; this
   only bites on the handful that are loose on a page, which are exactly the
   ones that needed it. */
.ht-empty,
/* .wl-empty is the same sentence on Watchlist under a different name, and it
   was the last one left: a scan for leaf elements holding more than ninety
   characters in a box wider than the measure, run across all 27 pages at 1920,
   came back with exactly this one and nothing else. */
.wl-empty{ max-width:var(--prose-measure); }

/* THE ONE FIELD THAT IS NOT A FORM FIELD, AND THE LAST EDGE ON THE PAGE.
   After the column landed, four pages still came back with two right-hand
   edges — In Touch, Messages, Organizations and the Kitchen — and the second
   one was always the same thing: the search box at the top, held to
   --form-measure by the rule above and so stopping 560px short of the grid it
   filters. The rule above is right about what it was written for (a box you
   paste a URL into should not be a monitor wide) and wrong about these four,
   because a search field is not a form. It is the control bar of the list
   underneath it, and a filter that is narrower than what it filters reads as
   a thing that was dropped on the page rather than attached to it.

   Named by id rather than by a pattern on the placeholder, because these four
   are a decision and a fifth page should be added here on purpose. */
#contactSearchInput,
#orgSearchInput,
#kitSearch{ max-width:none; }

/* THE LAUNCH PAD, WHICH IS THE ONE ROW OF CARDS THE FIRST PASS DID NOT CATCH.
   Row 1728's three pads are grid-template-columns:repeat(3, minmax(0,1fr))
   with no ceiling, so on Matt's monitor they came out 582px each to hold a
   name and one line of description — a third of each pad empty, on the first
   screen the app ever shows him. The harness could not see it: 582 is under
   --prose-measure, so the per-element check passes, and .lp-big is not one of
   the adopted grids, so the grid check never looked.

   It keeps its own repeat(3) rather than joining the auto-fill rule above, and
   that is deliberate. auto-fill answers "how many fit", and the answer on a
   monitor is four — which would draw three pads and a hole where the fourth
   would go. There are exactly three pads and the note above this block says
   why (size is the ranking). So the count stays fixed and only the WIDTH is
   capped, at three ideal cards, which is the width three cards have everywhere
   else in Lucid. The two narrow-window steps below it already existed and are
   untouched. */
.lp-big{ max-width:var(--tile-span-3); }
/* And the section AROUND the pads takes the page column, so its rule and its
   row of chips end where Home's own tiles end. Home is one of the six views
   with no column of its own (the band has to bleed), so without this line the
   launch pad's hairline ran to 1890 while the grid under it stopped at 1848 —
   a 42px step that the eye reads as two pages stacked. .home-grid a few rules
   up already carries this measure; this is the same number for the band above
   it. */
.launchpad{ max-width:var(--page-measure); }

/* AND THE THREE PAGE COLUMNS THAT ARE STILL 1240px FLAT.
   .maxw was fixed a section ago and these three were missed, which is what a
   number written in four places always ends in: .pulse-work and .pulse-two are
   the two halves of the Pulse page and .media-listen is the Listening shelf on
   Media. All three are two-column LAYOUTS rather than rows of cards, so they
   are right to stay out of the grid rule above — but a layout is still a page
   column, and 1240px flat is the same 1240px flat that left a 536px strip of
   nothing down the right-hand side of every .maxw page on a monitor. Measured
   at 1920 before this line: Pulse's own header ended at 1869 and .pulse-work
   ended at 1375, which is a page with a 494px step in its right-hand edge.
   They step with the screen now, like everything else. */
.media-listen{ max-width:var(--page-measure); }
/* .feed-two is the fourth and it is written twice — 1180px stacked, 1560px
   once the embed rail appears — which is the same decision made twice with two
   numbers, neither of them on any ladder. Both become the page column; the
   rail inside it keeps its own 360px, because that is a component width and
   not a page one. */
.feed-two{ max-width:var(--page-measure); }
@media (min-width:1180px){ .feed-two{ max-width:var(--page-measure); } }

/* AND THE ONE HAND-WRITTEN PROSE MEASURE LEFT IN THE APP. #profileContent
   carried style="max-width:68ch" in index.html with a comment beside it giving
   exactly the right reason — "without it the line runs the full 1226px of the
   content column and the eye loses its place coming back to the left" — which
   is word for word why --prose-measure exists. Two measures that mean the same
   thing is the disease; the inline style is gone from index.html and this is
   where the answer lives now. */
#profileContent{ max-width:var(--prose-measure); }

/* AND THE LAST INLINE max-width IN index.html. #acctPwForm carried
   style="max-width:420px" — which is --tile-ideal, the width a card is
   composed for, written as a literal in the one file where a stylesheet cannot
   reach it. tests/test-card-widths.js has an assertion that no inline
   max-width survives in index.html and it was already red on arrival this
   wave; this is the line that was making it red. display:none stays inline
   because renderPasswordPanel() toggles it. */
#acctPwForm{ max-width:var(--tile-ideal); }

/* ===========================================================================
   THE WHITE BOX ON A DARK THEME.
   ===========================================================================

   Row 1754. Lucid never declares `color-scheme`, anywhere — so as far as the
   browser is concerned this is a light page, and every control the browser
   draws for itself is drawn in the light palette no matter which of the twelve
   themes is on. The contact cards are where it shows worst: every card carries
   a native checkbox, and on `midnight` it renders as a hard white square in
   the corner of a near-black card. .contact-pick already sets accent-color,
   which only ever colours a TICKED box; the empty one is the browser's, and
   the browser was never told.

   It is not only checkboxes. This one declaration is also what decides the
   colour of the scrollbars inside every pane, the caret in every field, the
   text-selection colour, the clear button on a search input, and — the one
   that is genuinely a white text box — the fill Chrome paints over a field it
   has autofilled.

   Two rules rather than twelve, because app.js already stamps data-mode on the
   root for exactly this kind of question: see the note above setTheme(). A
   theme added tomorrow is covered without touching this file. */
:root[data-mode="dark"]{ color-scheme: dark; }
:root[data-mode="light"]{ color-scheme: light; }

/* AUTOFILL, WHICH IGNORES `background` ENTIRELY.
   Chrome paints an autofilled field with its own colour underneath the
   element's background, so the usual fix does nothing — an inset shadow is the
   only thing that lands on top of it. The colour is the theme's own ground
   lifted a few points toward its own ink, which is the same recipe .field uses
   by eye; it has to be opaque here, because anything translucent would let
   Chrome's fill through. The absurd transition delay is the standing trick for
   stopping Chrome from animating back to its own colour a second later. */
:root{ --autofill-fill: color-mix(in srgb, var(--bg-mid) 93%, var(--text)); }
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
select:-webkit-autofill{
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--autofill-fill) inset;
          box-shadow: 0 0 0 1000px var(--autofill-fill) inset;
  caret-color: var(--text);
  transition: background-color var(--dur-never) var(--ease-out) 0s;
}
/* AND THE FILL OF THE BOX ITSELF, WHICH WAS THE SAME GREY IN ALL TWELVE.
   The harness beside this — tests/test-the-theme-owns-the-boxes.js — measures
   every field in the app on `midnight` and again on `alabaster` and fails any
   whose fill is the same number on both, because a colour that does not move
   with the theme is a literal. It found seventy-six of them, and they were all
   one rule: .field carries background:rgba(120,120,140,.08), a cold grey wash
   that has been the fill of every text box in Lucid since the first one.

   On `midnight` that is invisible enough to get away with. On `chocolate`,
   `terracotta` and `parchment` — rooms built out of warm browns and creams —
   every box you type into is a cold grey rectangle sitting in a warm room,
   which is exactly what "does not match the theme" describes.

   The theme's own ink at 7% is the same IDEA — lift the ground slightly where
   a field is — expressed in the room's colour instead of in a number: warm on
   a warm theme, cool on a cool one, dark on the light themes where a white
   wash did nothing at all. Only the boxes you type into are changed here; the
   same literal is on about thirty hovers and buttons elsewhere in this file
   and those are somebody else's row, not this one. */
:root{ --field-fill: color-mix(in srgb, var(--text) 7%, transparent); }
.field,
.chat-input,
.rec-card input.rec-search{ background:var(--field-fill); }
/* NOT .main's padding, and that is a deliberate stop. The first version of this
   rule also took four pixels off .main.main-no-topsearch — and between then and
   now row 1726 put search back on the spine, took the corner box away and
   removed that class with it, so the rule would have been a declaration aimed
   at nothing: dead CSS that reads as if it were doing something. .main is 14px
   on every page now, by somebody else's decision and with a note explaining it,
   and the top of Home does not need to be the exception. Everything above comes
   out of the band's own gaps. */

/* ===========================================================================
   THINGS ANIMATE FROM WHERE THEY CAME FROM.  Row 1823.
   ===========================================================================

   The whole of the stylesheet's share of that row is the one class below, and
   that is deliberate: the mechanism runs on the Web Animations API, in the
   block at the foot of app.js, for the reason written out at length there —
   app.css's global prefers-reduced-motion rule forces every transition in the
   app to 0.01ms with !important, which would also have silently killed the
   cross-fade the mechanism is required to fall back to.

   WHAT THIS CLASS IS FOR. Going BACK from a record needs the list underneath to
   be live and pressable immediately — a Back link that waits a third of a
   second for an animation is a worse app with a nicer animation on it — while
   the card being left still has to be on screen, on top, collapsing into its
   own row. So the destination view goes up first in the ordinary way and the
   outgoing one is held over it by this, which overrides .view's display:none
   without claiming the .active class that half the app asks the DOM about.

   pointer-events:none is the load-bearing half: for the third of a second the
   card is still visible it must not be catching presses meant for the list that
   is already live behind it.

   No colour, no size and no duration here. The geometry is computed from the
   two rectangles at the moment of the press and the timing comes from
   var(--dur-slow) and var(--ease-out), read off :root by the mechanism. */
.view.lucid-flying-out{
  display:block;
  position:absolute; top:0; left:0; right:0; height:100%;
  z-index:3;
  pointer-events:none;
  transform-origin:50% 50%;
}

/* ===========================================================================
   NOTHING SHIFTS AFTER IT LOADS — the vault head.  Row 1824.
   ===========================================================================

   Measured rather than suspected. tools/measure-the-shift.js opens every view
   in the app cold, in its own browser page, and sums what the browser itself
   reports as layout shift. Thirteen of the fourteen scored at or near zero.
   The Vault scored 0.038 on every single run, and the browser named the
   element:

     div#view-vault > div.vault-head > div   [592,0,848,93] -> [114,56,848,99]

   WHAT WAS ACTUALLY HAPPENING, from a second probe that logged the two
   children of .vault-head every 100ms:

     t+0      title block 110x33   actions at x=592, y=14
     t+7.9s   title block 467x48   actions at x=114, y=76

   #vaultSub is empty in the markup and is filled when the vault has been read.
   The sentence it is filled with — "Nothing in here yet. Start with the things
   you would have to replace…" — takes the title column from 110px wide to
   467px, the two columns no longer fit on one line of a wrapping flex row, and
   the entire toolbar drops to a second line, taking every card on the page 62
   pixels down with it. Seven seconds after the page looked finished, under
   whatever the reader's hand was already reaching for.

   The joke of it is that the sentence which moves the page is the EMPTY state.
   A vault with nothing in it is the first thing a new person sees.

   TWO RULES, AND EACH ONE CLOSES A DIFFERENT HALF.

   The first makes the toolbar's position a fact about the WINDOW instead of a
   fact about when the server answered. flex-basis:0 means the title column's
   hypothetical size is zero, so it can never be the reason the row wraps; the
   toolbar now wraps when the window is genuinely too narrow for it and at no
   other time. min-width keeps that from becoming the opposite bug — a title
   squeezed to nothing beside a row of buttons — and is written as a min() so it
   gives way rather than overflowing on a phone.

   The second reserves the height. With the toolbar staying put the head no
   longer jumps 62px, but the title column still grows from one line to two when
   the sentence lands, which is another 13. --space-16 is 64px, which is the
   toolbar's own 50px plus the room the second line needs, so the head is the
   same height before the read and after it.

   THE OTHER THIRTEEN .vault-head BLOCKS WERE DELIBERATELY NOT CHANGED, and on
   14 September they were. The sentence that stood here said a fix applied to
   fourteen headers on the strength of one measurement is a guess wearing a
   measurement's clothes, which was right about the RESERVED HEIGHT and wrong
   about the other half. Reading the markup settles it: all fourteen are the
   same six lines — a text column holding a page title and an empty .page-sub
   that JavaScript fills, beside a column of buttons, in a wrapping flex row.
   Work, Media, Fashion, Feed, Goals, Kitchen, Body, Travel, Routines,
   Watchlist, Sports, Timeline, the Vault and Medicine. Eleven of the thirteen
   fill that .page-sub from a load that has not answered yet.

   flex-basis:0 on the text column is not a guess about any of them. It does not
   change what any header looks like when the page is finished; it changes only
   WHEN the row is allowed to wrap, from "when the sentence arrives" to "when the
   window is too narrow" — and the second is the only one of those two that is a
   fact about the reader rather than about the server. The same is true of the
   six .proj-title-row pages below. Both selectors lost their `#view-…` prefix
   and nothing else changed.

   (The inline `style="min-width:0"` on all fourteen text columns came out of
   index.html at the same time, because it beat the min-width below and left the
   half of this fix that protects a phone doing nothing at all — a control that
   looks like it works and does not.)

   THE RESERVED HEIGHT IS STILL THE VAULT'S ALONE. 64px is the vault toolbar's
   own 50 plus a second line; it is a measurement of one header, and putting it
   on thirteen others would make every one of them at least 64 tall whether or
   not anything in it is that tall. That one IS a guess and it stays where it
   was measured. */
.vault-head > div:first-child{ flex:1 1 0; min-width:min(100%, 280px); }

/* THE COST ROW UNDER THE ASK BOX.

   THE COMMENT-OPENER ON THIS BLOCK WAS MISSING, AND IT COST A RULE. Everything
   from here down to the end of this note was prose with no opener in front of
   it, so the browser read it as broken CSS, threw away what it could not
   parse, and took the NEXT rule with it — the plain `.ask-allow` rule that
   gives the allowance row its layout. It had been that way since 11 September.
   The check could not see it either: lint.js steps over quoted text, and an
   apostrophe inside the un-opened prose put its scanner out of step, so it
   read the fault as ordinary words.

   AND THEN I BROKE IT A SECOND TIME FIXING IT. The first version of this note
   quoted the comment markers themselves, so the closing one INSIDE the prose
   ended the comment early and left everything below it unopened all over
   again — the same fault, now with my name on it. A note about comment markers
   does not get to contain comment markers. It does not name them at all now.

   APPENDED AT THE END OF THE FILE RATHER THAN BESIDE .chat-cost, DELIBERATELY.
   Eight agents were in this stylesheet on 11 September and one of them was
   sweeping it globally; a rule inserted into the middle of the assistant
   section is a guaranteed conflict, and a conflict in a shared stylesheet costs
   more than the four hundred bytes of distance. The rules belong with
   .chat-cost and .chat-input-row and this note is here so the next person
   moving them knows where they came from.

   NO LITERAL COLOURS. Lucid has many themes and a hex in a new rule breaks all
   but one of them. --orange and --pink are both stated per theme; there is no
   --red at :root, which is why the stop state is --pink rather than the obvious
   thing.

   THE SPACING WAS TOKENS.CSS'S NUMBERS WRITTEN OUT RATHER THAN ITS NAMES, and
   the reason given was that "tests/lint.js reads this file on its own and fails
   any var() it cannot find a definition for in it — tokens.css is a separate
   stylesheet loaded before this one, so every name from it reads to that check
   as a declaration that does nothing. Nothing else in these eighteen thousand
   lines uses a tokens.css name for that reason."

   That was true on 11 September and it stopped being true the same week:
   checkCssVars in tests/lint.js reads tokens.css before it judges app.css, with
   a note beside it saying that a lint rule which punishes the thing the codebase
   is trying to do is a lint rule that gets worked around. So the workaround
   comes out. This block was the largest single source of the fifteen values that
   tests/lint-the-scale.js could see were already the right number and still not
   the token, and two of its numbers were off the ramp entirely — which is what a
   rule spelled in literals decays into within days of being written, whatever
   the reason it started that way. 14px is now --space-4, which is also what the
   input row directly below it is padded by, so the rail and the box it belongs
   to finally start at the same place.

   THE ONE VISUAL DECISION WORTH ARGUING WITH: the track is three pixels and the
   text carries the meaning. This sits directly above the input, under the
   answer thread, and a fat progress bar there would make the assistant look
   like a download. What it has to do is be readable at a glance and get out of
   the way — so it is a hairline that fills, and every state that has no
   fraction to show simply has no track at all rather than an empty one. An
   empty track reads as "nothing spent"; an absent track reads as what it is. */
/* TOP-ALIGNED, NOT CENTRED, because the text is the only part of this row that
   can become two lines. Centred, the bar and the word "details" drifted to the
   middle of a two-line block and read as three things at three heights. Aligned
   to the top they sit on the first line where the eye starts, and the second
   line hangs below them. The offset on the bar is what puts a 3px rule on the
   optical centre of the line beside it, and it is now that subtraction rather
   than a number somebody rounded to it: the comment said 4px, the rule said
   5px, and the arithmetic — a 1.3 line box on 11px type, less the 3px rule,
   halved — says 5.65. Written as the calculation, it cannot disagree with the
   type it is centring against or with the sentence describing it. */
.ask-allow{ display:flex; align-items:flex-start; gap:var(--space-2);
  padding:var(--space-2) var(--space-4) 0; font-size:var(--type-label); line-height:1.3;
  color:var(--text-dim); cursor:pointer; user-select:none;
  transition:color var(--dur-fast) var(--ease-out); }
.ask-allow:empty{ display:none; }
.ask-allow:hover{ color:var(--text); }

/* --text-dim AND NOT --text-faint, WHICH IS WHAT .chat-cost DIRECTLY BELOW IT
   USES. It was --text-faint first, and it was measured: at 11px on the pale
   themes the rail came out at 2.25:1 on Terracotta, 2.56 on Denim and 3.27 on
   Parchment — identical to .chat-cost, because it was the same token, and well
   under what anybody can read. That is a pre-existing fault in --text-faint
   across the whole app and not this row's to fix; the note by .chat-mic already
   describes the same measurement and the same remedy, which is to re-point at
   --text-dim. So this row does that for itself rather than waiting.
   It also happens to be the right hierarchy. The rail is a live counter he is
   meant to read before he spends; the line under an answer is a receipt for
   money already gone. The one he acts on should not be the fainter of the two. */

.ask-allow:focus-visible{ outline:var(--ring-width) solid var(--ring); outline-offset:var(--ring-gap); border-radius:var(--radius-control); }
.ask-allow-bar{ flex:0 0 56px; height:3px; margin-top:calc((1.3em - 3px) / 2); border-radius:var(--radius-pill);
  background:var(--glass-border); overflow:hidden; }
.ask-allow-fill{ display:block; height:100%; border-radius:var(--radius-pill);
  background:var(--accent); transition:width var(--dur-base) var(--ease-out); }
/* IT WRAPS, AND THAT IS THE DECISION RATHER THAN AN OVERSIGHT.
   The first version was nowrap with an ellipsis, and the two states that matter
   most were the two that did not fit: "…spent. Ask Lucid has stopped …" and
   "…could not be read — the figure below is u…". An ellipsis in the middle of
   the sentence explaining that Lucid has stopped answering is the sentence not
   being read. The copy has been cut down as well, so the ordinary states are
   one line; the two alarming ones are allowed a second, which is the right way
   round — the state that needs more of his attention takes more of the room. */
.ask-allow-text{ flex:1; min-width:0; }
/* "details" rather than a chevron or an information glyph. The row is already
   one line of very small type and an icon at this size is a smudge; a word
   says what pressing it does, which a circled i never has. */
/* NOT opacity, and this is the whole reason it could never be nudged into passing.
   It was `opacity:.55`. Opacity dims the letters against WHATEVER happens to be
   behind them, so one rule failed on ELEVEN themes at once: 2.22:1 on alabaster,
   2.26 on parchment, 3.22 on regal and forest, 3.28 ink, 3.37 fathom, 3.41 on
   midnight/dark/frosted, 3.45 obsidian, 3.64 chocolate -- against a floor of 4.5.
   A label dimmed by opacity cannot be fixed by picking a better colour, because
   the colour is not what is deciding the result.
   --text-dim is the row's OWN token and the measurement this line stands on:
   .ask-allow-text beside it is --text-dim at this same 11px on this same ground,
   and it passes on all seventeen themes. --text-faint was never an option -- the
   note by .ask-allow above records it measuring 2.25-3.27 at this size, which is
   the same hole one rung down. The underline carries the "this can be pressed"
   that the dimming used to, and hover still lifts it to --text with the rest of
   the row, so the rest state is quieter than the hover exactly as before. */
.ask-allow-more{ flex:none; color:var(--text-dim); text-decoration:underline; }
.ask-allow:hover .ask-allow-more{ color:var(--text); }

/* THE FOUR STATES, AND THREE OF THEM ARE NOT GREEN.
   ok       the ordinary case, and it is deliberately the quietest thing here.
   warn     past 85% of the month.
   stop     spent, or an allowance of nothing. Ask Lucid is refusing.
   unknown  THE LEDGER COULD NOT BE READ. Amber, with no bar and no figure,
            because the one thing this must never look like is a cheap month.
            A counter reading zero over a failed read is good news that is not
            true, and it is the worst failure this feature has. */
.ask-allow-warn{ color:var(--orange); }
.ask-allow-warn .ask-allow-fill{ background:var(--orange); }
.ask-allow-stop{ color:var(--pink); }
.ask-allow-stop .ask-allow-fill{ background:var(--pink); }
.ask-allow-unknown{ color:var(--orange); }
.ask-allow-uncapped{ color:var(--orange); }
/* "Reading what this month has cost…" is a state, not a number, and it should
   not sit at the same weight as one. */
.ask-allow-loading{ opacity:.7; font-style:italic; }

/* THE FIGURE AT THE TOP OF "WHERE THE MONEY GOES".
   A month's spend is the point of that panel and it opened as a sentence in
   body type, indistinguishable from the four paragraphs of explanation under
   it. A number that is the point of the thing it sits in gets the size of one
   — 32px is tokens.css's --type-figure, and Fraunces is what every other
   headline figure in this app is set in. The ceiling beside it stays small and
   dim on purpose: what he wants at a glance is what he has spent, and "of $30"
   is the context rather than a second number competing with the first.
   AND THERE IS DELIBERATELY NO LARGE VERSION OF THIS FOR THE FAILED READ. A
   32px figure needs a figure, and the whole rule there is that there is not
   one — that state opens with a sentence in bold body type instead. */
.ask-money-figure{ font-family:var(--font-display); font-size:var(--type-figure); font-weight:600;
  line-height:1.05; margin:0 0 var(--space-hair); color:var(--text); }
.ask-money-of{ font-size:var(--type-body); font-weight:400; color:var(--text-dim); font-family:inherit; }
.ask-money-sub{ color:var(--text-dim); font-size:var(--type-body); margin:0 0 var(--space-1); }
.ask-money-figure.warn{ color:var(--orange); }
.ask-money-figure.stop{ color:var(--pink); }

/* ===========================================================================
   NOTHING SHIFTS AFTER IT LOADS — the head of All people.  Row 1824.
   ===========================================================================

   The biggest single shift measured anywhere in Lucid: 0.176, past Google's
   0.1 "good" line and two thirds of the way to its 0.25 "poor" one, on the
   page a personal CRM is most about. The browser named it:

     div#view-contacts > div.proj-title-row > div.btn-row

   It is the Vault head's bug wearing different clothes, and finding the second
   one is what makes it a pattern rather than an incident. .proj-title-row is a
   wrapping flex row: a title column on the left and the ⋯ More row on the
   right. #contactsCount is empty in the markup and is filled once the address
   book has been read — "1,284 people · 96 with no email" — the title column
   widens by a few hundred pixels, the two columns stop fitting on one line, and
   the toolbar drops to a second line taking every contact card on the page down
   with it. The bigger the address book the more cards move, which is why this
   one scores four times what the Vault did.

   THE SAME TWO RULES AS THE VAULT, for the same two reasons: flex-basis:0 so
   the title column can never be the reason the row wraps, and a reserved height
   so the count landing cannot change the head either. The height is the title's
   own line — .page-title — plus one line of .page-sub at 13px, which is what
   #contactsCount always is.

   SCOPED TO THIS ONE VIEW, like the Vault's — and un-scoped on 14 September for
   the reason written out at the vault head above. The six pages are Pulse, In
   Touch, All people, Organizations, the Projects home and Settings, and they are
   the same five lines of markup each: a text column of .page-title and .page-sub
   beside a row of buttons, wrapping. Four of the six fill that .page-sub from a
   read — #pulseWindow, #cmmSub, #contactsCount, #organizationsCount,
   #projectsCount — and Settings is the only one whose sentence is in the
   markup, where a reserved line changes nothing because the line is already
   there.

   ONE LINE OF .page-sub, WHICH IS A FLOOR AND NOT A HEIGHT. min-height cannot
   make a two-line count shorter; all it does is stop an empty <p> being zero
   tall while the answer is in the air. That is why generalising it is safe in a
   way the vault's 64px min-height is not: this one is the height of the element's
   own first line, computed from the type it is set in, and every page that wears
   it has that line. */
.proj-title-row > div:first-child{ flex:1 1 0; min-width:min(100%, 280px); }
.proj-title-row > div:first-child > .page-sub{ min-height: calc(var(--type-body) * 1.35); }


/* AND THE PAGE IT CAME FROM, HELD UNDERNEATH WHILE IT GROWS.  Row 1823.
   The sibling of .lucid-flying-out above, and it exists because a screenshot
   said so. openContactDetail swaps the view by hand rather than through
   switchView, so the people grid is display:none in the same frame the card
   starts growing out of one of its rows — and the filmstrip showed a card
   expanding out of a black rectangle. That is a panel appearing with extra
   steps. The claim of this row is that the movement tells the eye where it is,
   and it cannot do that if the place it came from has already gone.

   z-index:0 puts it under .view.active's 1, so the card grows over it. No
   opacity of its own: the mechanism only applies this to a view nobody else is
   already fading, and holding it at full strength for a third of a second is
   what makes the grid still be there when the eye goes looking for it. */
.view.lucid-flying-under{
  display:block;
  position:absolute; top:0; left:0; right:0; height:100%;
  z-index:0;
  pointer-events:none;
}

/* ===========================================================================
   NOTHING SHIFTS AFTER IT LOADS — the cross-fade itself.  Row 1824.
   ===========================================================================

   The last one, the largest single shift left in the app, and the only reason
   it took this long to find is that it is intermittent: 0.039, on roughly one
   cold boot in three, blamed by the browser on div#view-home — a page that is
   not even the one being measured. It turned up on Watchlist, on Work, on
   Travel and on All people, which between them have nothing in common. The
   detail run finally named it:

     div#view-home   [114,10,1296,235] -> [84,0,1356,900]

   That is .view.leaving being applied. The rule is `position:absolute; top:0;
   left:0; right:0` and an absolutely positioned box is placed against its
   container's PADDING box, while the view it is replacing was sitting in the
   CONTENT box. .main is padded 30px at the sides and 14 or 10 at the top, so
   the outgoing page jumps thirty pixels left and ten up — and widens by sixty —
   in the same frame it starts fading. Every navigation in the app, not only at
   boot. It scores as a shift whenever the browser happens to sample it.

   Which is the exact fault the note beside @keyframes rise was written about:
   "Was translateY(6px) scale(.99). During a cross-fade that vertical nudge
   reads as the page lurching into place. A dissolve should not also move."
   Somebody removed the nudge from the INCOMING view and the OUTGOING one had a
   bigger one all along, put there by a positioning scheme rather than by a
   keyframe, which is why reading the animation never found it.

   IT WAS .main's OWN PADDING RESTATED, AND THE RESTATEMENT DID NOT SURVIVE
   THREE DAYS. The first version of this fix copied 30px / 14px / 12px down here
   and said so out loud: "the duplication is the cost of a fix that does not
   reach into a shared rule eight other people are inside this week, and it is
   guarded rather than hoped — if .main's padding changes and these do not, the
   leaving view jumps again and the ceilings go red on the next run."

   That is exactly what happened. On 12 September the token sweep moved .main's
   sides from 30px to --space-8 and its top from 14 to --space-4, these three
   numbers stayed where they were, and the cross-fade started jumping two pixels
   left and two up on every navigation in the app — small enough to look like
   nothing in a screenshot, large enough to fail section 7b of
   tests/test-nothing-jumps.js, which it did, silently, for two days. A guard
   that only tells you after the fact is better than nothing; a number that
   cannot disagree with the thing it is copying is better than the guard.

   SO THERE IS NOTHING TO COPY NOW. .main declares --main-pad-x, --main-pad-top
   and --main-pad-bottom and then pads ITSELF with them, so these four lines and
   the padding they are compensating for are the same three values by
   construction. The phone block and the now-playing bar re-point the variables
   rather than the padding property, and both arrive here for free.

   The height is a calc rather than the 100% the original rule sets, because an
   absolutely positioned child DOES contribute to a scroll container's overflow:
   inset from the top by the padding and still a full container tall, it would
   hang that far past the bottom and .main would grow a scrollbar for a third of
   a second.

   (.main.main-no-topsearch had a line here too, setting a fourth number. Row
   1726 deleted that class from the app in September; a declaration aimed at a
   class nothing wears reads as if it were doing something, so it is gone.) */
.view.leaving{
  top:var(--main-pad-top);
  left:var(--main-pad-x);
  right:var(--main-pad-x);
  height:calc(100% - var(--main-pad-top) - var(--main-pad-bottom));
}

/* ===========================================================================
   THE THREE DOORS — 12 SEPTEMBER, ROWS 2130–2133
   ===========================================================================
   Four treatments were drawn at full size in tools/mocks/THREE-PILLARS.html
   and Matt answered "B=work c=media d=life". Three different shapes, one per
   pillar, and that is deliberate rather than untidy: Work is four readings of
   one list and wants a compact index; Media is eight separate bodies of
   content and wants a stage; Life already has a painting above it and wants
   one thing said, not seven.

   NOT ONE HARD-CODED COLOUR IN THIS BLOCK. Thirteen themes, and a literal hex
   here is a rule that is right on one of them.
   =========================================================================== */

/* ---- MEDIA · THE FLOOR OF THE ROOM ---------------------------------------
   12 September, the evening of the day the stage was built. Matt, looking at
   the live page: "Media has a giant blank space at the bottom. What can we do?
   Artwork? Ideas?" — and to the answer, "I like bringing back the art."

   THE BLANK SPACE WAS NOT MISSING CONTENT. It was a missing room. The pane
   was as tall as whatever happened to be inside it, so the Listening room —
   a head and one now-playing block — drew a four-hundred-pixel card halfway
   up a screen of black. The floor height is on .area-stage-pane in the block
   this one sits in front of; what is here is what stands on that floor.

   A SHELF, NOT A FAN, and the argument is written out in full above
   areaRoomArtHtml() in app.js. The short version: the fan's rotation exists to
   make a two-inch card read as a stack. The pane is eight hundred pixels wide.
   Blown up to that size the same gesture stops being a stack of things and
   becomes a graphic, and a graphic is decoration — which is the one thing this
   strip must not be, because every picture in it is HIS: the covers he is
   playing, the posters he saved, the crests of the teams he follows, the
   thumbnails off the sites he reads.

   SQUARE TO THE PAGE, BOTTOM-ALIGNED, IN PRIORITY ORDER LEFT TO RIGHT, on the
   pane's own hairline. Bottom-aligned rather than top, because the three
   shapes have three different heights and a row of pictures hanging from a
   common ceiling reads as a mistake where a row standing on a common floor
   reads as a shelf.

   THE SHAPE COMES FROM THE CONFIG, exactly as it does on the band: a poster is
   2:3, a sleeve and a crest are square, a feed thumbnail is 16:9, and no
   amount of looking at a URL will tell you which. One height governs all
   three — --room-art-h — so the strip is one decision rather than three, and
   the per-shape multipliers are what keep three posters and three thumbnails
   coming out to roughly the same width across the floor.

   A CONTAINED PIECE IS DRAWN BARE. `contain` is the config's way of saying
   "this is a logo on transparency"; framing one is a picture of a sticker. */
.area-stage-art{
  --room-art-h:220px;
  display:flex; flex-wrap:wrap; align-items:flex-end; gap:var(--space-3);
  padding:var(--space-4);
  border-top:1px solid var(--glass-border);
}
.area-stage-piece{
  display:block; flex:0 0 auto; overflow:hidden;
  border-radius:var(--radius-control);
  border:1px solid var(--glass-border-hi);
  background:color-mix(in srgb, var(--text) 6%, transparent);
  box-shadow:var(--elev-raised);
}
.area-stage-piece img{ display:block; width:100%; height:100%; object-fit:cover; }
.area-stage-piece.contain{
  border:0; border-radius:0; background:transparent; box-shadow:none; }
.area-stage-piece.contain img{ object-fit:contain; }
.area-stage-art[data-shape="poster"] .area-stage-piece{
  height:var(--room-art-h); aspect-ratio:2 / 3; }
.area-stage-art[data-shape="square"] .area-stage-piece{
  height:calc(var(--room-art-h) * .82); aspect-ratio:1 / 1; }
.area-stage-art[data-shape="wide"] .area-stage-piece{
  height:calc(var(--room-art-h) * .58); aspect-ratio:16 / 9; }
/* THE STRIP NEVER PUSHES THE PANE SIDEWAYS. Three 16:9 thumbnails are the
   widest the shelf ever gets, and the pane is a column of a grid rather than
   the window — so the height steps down before the width runs out, and
   flex-wrap is the floor under that arithmetic rather than the plan. */
@media (max-width: 1200px){ .area-stage-art{ --room-art-h:190px; } }
@media (max-width: 860px){ .area-stage-art{ --room-art-h:130px; padding:var(--space-3); } }

/* AND THE FLOOR COMES UP WHEN THE LIST MOVES ON TOP OF IT. Below 860 the
   stage stops being two columns: the rooms become a wrapped row of chips ABOVE
   the pane, so the pane no longer has the height of the page to itself and a
   660px floor would be most of a phone screen spent on one room's empty middle.
   Written as a descendant selector rather than as a second .area-stage-pane
   rule, so it wins on specificity and does not depend on where in this
   stylesheet it happens to sit. */
@media (max-width: 860px){
  .area-stage .area-stage-pane{ min-height:min(440px, 54vh); }
}

/* AND THE TIME AT THE END OF A ROW IS NOT CUT OFF IN A ROOM THIS WIDE.
   .ht-time is 68px wide, which is the Home tile's measurement and right for a
   tile; #mediaBody then sets those numerals a size larger, and "Sun 1:05 PM"
   has been losing its M to the pane's overflow ever since. Widened only inside
   the stage — the tiles that own the 68px keep it — and only as a MINIMUM, so
   the column still lines up when every row is a short one. A time that is
   clipped is a number the page got wrong, which is the one thing this app
   does not ship. */
#mediaBody .area-stage-body .ht-time{ width:auto; min-width:68px; }

/* A ROOM THAT CANNOT BE PREVIEWED STANDS IN THE MIDDLE OF ITS OWN FLOOR.
   Fashion is a room of this group with no channel behind it, so its pane is
   one honest sentence saying nothing here has read the desk. Left at the top
   of a pane that now has a floor height, that sentence read as a page which
   had failed to finish loading. Centred, it reads as what it is: an empty
   room with a door. The words are unchanged, and there is still no invented
   preview beside them. */
.area-stage-body:has(> .area-stage-none){ display:flex; align-items:center; }

/* ---- MEDIA · THE STAGE ---------------------------------------------------
   The rooms down the left, one of them open on the right at full size. The
   pane holds the channel's OWN body — the same markup the stack of sections
   used to draw — so there is nothing here that can drift out of step with it. */
.area-stage{
  display:grid; grid-template-columns:minmax(170px, 210px) minmax(0, 1fr);
  gap:var(--space-4); align-items:start;
  max-width:var(--tile-span); margin-bottom:var(--space-5);
}
.area-stage-list{
  display:flex; flex-direction:column; gap:var(--space-hair);
  border:1px solid var(--glass-border); border-radius:var(--radius-card);
  padding:var(--space-2); background:var(--glass-bg);
}
.area-stage-row{
  display:flex; flex-direction:column; align-items:flex-start; gap:1px;
  width:100%; text-align:left; font:inherit;
  padding:var(--space-2); border:none; border-radius:var(--radius-tight);
  background:transparent; cursor:pointer;
  transition:background var(--dur-fast) var(--ease-in-out);
}
.area-stage-row:hover{ background:color-mix(in srgb, var(--text-dim) 12%, transparent); }
.area-stage-row:focus-visible{ outline:2px solid var(--ring); outline-offset:-2px; }
.area-stage-row.on{ background:color-mix(in srgb, var(--area-media) 16%, transparent); }
.area-stage-name{ font-size:var(--type-body); font-weight:600; color:var(--text); }
.area-stage-state{ font-size:var(--type-label); color:var(--text-dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%; }
.area-stage-state.faint{ color:var(--text-faint); }
/* THE PANE HAS A FLOOR, AND THAT IS WHAT THE BLANK SPACE ACTUALLY WAS.
   Until 12 September the pane was exactly as tall as whatever was inside it,
   so the Listening room — a head and one now-playing block — drew a short
   card in the top corner of an otherwise black screen and Matt read the rest
   of the page as missing. An open room is a SURFACE: it holds its shape
   whether the evening is busy or quiet, which is also what stops the page
   jumping a hundred pixels every time he presses a different room.

   NOT 100vh, AND NOT A BARE PIXEL FLOOR EITHER. There is a banner above this
   page and a rail beside it, so a viewport-tall pane would push the whole
   stage into a scroll on every window; and a flat 540px would do the same on
   a short one. min() is the honest way to say both at once — 660 is the
   composed height at 1440 wide, and on anything shorter than about 940 the
   floor comes down with the window instead of inventing a scrollbar. 70vh is
   the figure because the banner, the page title and this stage's own bottom
   margin come to a little under three tenths of the window between them.

   A COLUMN, so the body can take the slack and the strip of his own artwork
   sits on the floor rather than halfway up it. See the block above. */
.area-stage-pane{
  min-width:0; min-height:min(660px, 70vh);
  display:flex; flex-direction:column;
  border:1px solid var(--glass-border); border-radius:var(--radius-card);
  background:var(--glass-bg); overflow:hidden;
}
.area-stage-head{
  display:flex; align-items:flex-start; justify-content:space-between;
  gap:var(--space-3); padding:var(--space-3) var(--space-4);
  border-bottom:1px solid var(--glass-border);
}
.area-stage-title{ margin:0; font-size:var(--type-title); font-weight:650; color:var(--text); }
.area-stage-line{ font-size:var(--type-body); color:var(--area-media); font-weight:600; }
.area-stage-line.faint{ color:var(--text-dim); font-weight:400; }
.area-stage-open{
  flex:0 0 auto; font:inherit; font-size:var(--type-label); font-weight:600;
  padding:var(--space-2) var(--space-4);
  border:1px solid var(--area-media); border-radius:var(--radius-control);
  background:transparent; color:var(--area-media); cursor:pointer;
  transition:background var(--dur-fast) var(--ease-in-out);
}
.area-stage-open:hover{ background:color-mix(in srgb, var(--area-media) 14%, transparent); }
.area-stage-open:focus-visible{ outline:2px solid var(--ring); outline-offset:2px; }
/* flex:1 is what puts the artwork on the FLOOR rather than immediately under
   the last line of the body. The body takes whatever height the pane has left
   over; the strip keeps its own and sits beneath it. */
.area-stage-body{ flex:1 1 auto; padding:var(--space-4); }
/* The channel bodies were written to sit inside mediaPanel(), which already
   carried the padding and the heading. Inside the stage the pane is the panel,
   so a panel drawn within one loses its own frame rather than drawing a second
   box inside the first. */
.area-stage-body > .media-panel{ border:none; background:transparent;
  padding:0; margin:0; border-radius:0; }
.area-stage-none{ font-size:var(--type-body); color:var(--text-dim);
  max-width:var(--prose-measure); }
@media (max-width: 860px){
  .area-stage{ grid-template-columns:minmax(0, 1fr); }
  .area-stage-list{ flex-direction:row; flex-wrap:wrap; }
  .area-stage-row{ width:auto; flex:0 1 auto; }
  .area-stage-state{ display:none; }
}

/* THE FIRST-RUN INVITATION, ON THE STAGE. On the band a card with nothing
   behind it stopped being a filter and became the invitation; the stage keeps
   that promise under the preview instead. Item 286's two kinds do not share a
   look either: an account invitation is the area's own colour because it is
   the one that can fail for a reason nobody here controls, and a preference is
   a quiet outline because it costs nothing and is changed as easily as it was
   made. */
.area-stage-invite{
  display:inline-block; margin-top:var(--space-3);
  font:inherit; font-size:var(--type-label); font-weight:600;
  padding:var(--space-2) var(--space-4);
  border-radius:var(--radius-control); cursor:pointer;
  border:1px solid var(--area-media); background:transparent; color:var(--area-media);
  transition:background var(--dur-fast) var(--ease-in-out);
}
.area-stage-invite.connect{
  background:color-mix(in srgb, var(--area-media) 18%, transparent);
}
.area-stage-invite:hover{ background:color-mix(in srgb, var(--area-media) 28%, transparent); }
.area-stage-invite:focus-visible{ outline:2px solid var(--ring); outline-offset:2px; }

/* ==========================================================================
   THE FOCUS RING, EVERYWHERE. Row 2232.
   ==========================================================================

   The sweep of 16 September found sixteen controls that were in the tab order
   and showed NOTHING when they were focused, and the list read like an
   accident: .btn on nineteen different pages, .field in the vault and the
   medicine cabinet, the scratch pad on Home. It was not an accident. This
   stylesheet had forty-odd :focus-visible rules and every one of them named a
   component — .lp-pad, .nav-item, .tl-run, .dup-opt — so a control belonged to
   the keyboard only if somebody had remembered it by name. Nothing underneath
   them said what a focused thing looks like, and .btn is the most-used control
   class in the file.

   So: one rule, at the bottom, where a bare `outline:none` of the same weight
   written earlier no longer wins. Everything focusable gets the ring; the
   forty components that draw their own — .mb-x's glow, .field's lit border —
   are all written with a class and outrank this, so not one of them changes.

   IT IS A TOKEN AND NOT A COLOUR, which is what makes it survive the themes.
   --ring is declared once per ground beside that ground — white at 42% on the
   dark six, near-black at 55% on alabaster, parchment and the three papers —
   so the ring is stated in the same place as the surface it has to be visible
   against, rather than being one colour hoping to clear thirteen backgrounds.
   Every one of the thirteen is measured, and five of them were failing; see the
   note at alabaster's own --ring.

   AND A FOCUSED CONTROL IS VISIBLE, FULL STOP. A handful of this app's verbs
   live at opacity:0 until the row under them is hovered, which for a mouse is
   a nicety and for a keyboard would be a control you can reach, press, and
   never see. Reaching it is what shows it. */
:focus-visible{
  outline:var(--ring-width) solid var(--ring);
  outline-offset:var(--ring-gap);
}
[data-reach]:focus-visible{ opacity:1; }

  /* ======================================================================
     THE THREAD. Lucid's front door, 21 September 2026.

     WHAT IT REPLACES AND WHY. There was a bar across the bottom of the app
     for about four hours. Matt, on seeing it live: "the bar streches the
     whole page, is buky nad the ember Lucid inside looks terrible… Right now
     it looks just like every search box ever. Nothing special, magical,
     awesome about it."

     He was right on all three counts, and the diagnosis matters more than the
     apology:

       A BAR IS FURNITURE. Border, fill, fixed height, a place it belongs.
       Furniture is the opposite of presence, and presence was the brief —
       "Maybe Lucid is an aura, or presence." There is no rectangle here at
       rest. There is a line.

       A SPHERE AT 22px IS A BEAD. The orb's own geometry — the terminator,
       the lid, the counter-mass — is what makes it read as a LIT OBJECT, and
       a lit object that small averages into mud. Below about 40px the honest
       drawing is a POINT OF LIGHT: a nearly-white core and a bloom that
       reaches past it. Light is legible at any size. That is the node.

       IT SAT IN THE MIDDLE OF HIS WORK. Bottom centre, over the content.
       This is the top right corner, out of the reading path.

     ONE ELEMENT, AND ITS WIDTH IS THE WHOLE DESIGN.
     Rest is 112px. Asking extends it to 420. An answer that needs the room
     runs it the full width and the sheet comes down. Thread and sheet were
     never two designs — they are the same gesture at two lengths, which is
     also why the big moment does not wear out: it has to be earned.

     AND THE NODE NEVER DIES. Matt's own note, and the best one he gave:
     "Maybe when thread is open the wandering thread still has the pulse of
     light." Once the panel is open the line IS the panel's top edge, so the
     node simply keeps running along it. Nothing appears, nothing is replaced;
     one element changes job. Resting, listening, thinking, answered — all of
     it carried by a single travelling point of light, with no spinner, no
     icon and no words.
     ====================================================================== */
  :root{
    --th-hot:#FFF6E4; --th-mid:#FFB347; --th-base:#FF6A1E;
    --th-len:112px;
    /* THE BAND. 24 September: "Make sure it is the same pacing on every page
       so the band is consistant." One number, declared once, and .main is the
       only thing that reads it - so no page can have a different one.
       15px of margin above the line plus the 26px row the line and the
       launchpad share. */
    /* 44px -> 14px on 3 October. That number was 15px of margin plus the 26px
       row the line and the launchpad shared. The margin is gone (the wire IS
       the edge) and the launchpad has moved to the right wall, so the only
       thing left to reserve is the wire's 10px box and a little air. This
       hands 30px back to the top of every page in the app — which is a third
       of the empty band above Home that list item 1 is about. */
    --lth-band:24px;
  }
  :root[data-lucid="aurora"]{ --th-hot:#EAFFFB; --th-mid:#7BEFD0; --th-base:#17A69A; }
  :root[data-lucid="dawn"]  { --th-hot:#FFF0E2; --th-mid:#FFB08C; --th-base:#C0407E; }
  :root[data-lucid="canopy"]{ --th-hot:#F4FFE9; --th-mid:#AEE06A; --th-base:#3E9B4F; }

  /* THE WIRE IS THE TOP EDGE — 3 October. This used to hug the top-right
     corner at 112px wide. It now spans the window, because it is not a logo in
     a corner any more, it is the line that separates Lucid from the screen.
     --th-mt goes to 0: a hairline that is the edge cannot be fifteen pixels
     below the edge. */
  .lth{ position:fixed; top:0; right:0; left:0; z-index:60;
        display:flex; flex-direction:column; align-items:stretch;
        pointer-events:none; --th-mt:0px; }
  .lth > *{ pointer-events:auto; }

  /* ======================================================================
     THE LENS, AND THE CHARGE THAT WANDERS IT.  24 September.

     Matt: "I want to switch Lucid to the charge. (#5) ... And lets go with
     the Lens (#4) ... can you have the Lucid Charge slowly wander the lens?"
     And the night before: "make it a wireframe (dull glow) with the ember
     wandering it slowly."

     THE RING IS UNLIT. That is the whole of what the charge means: the wire
     sits dim until the light reaches it. So the ring can be a wireframe and
     still be alive, because the life is the one point travelling it.

     THE TRAIL IS THE HIGHLIGHT ARC. The Lens as drawn in round one was a
     ring, a lit core and one arc of highlight. Here the arc is the charge's
     wake, so a STILL frame of this mark is exactly the mark he picked, and
     a moving one is the charge. One object, not two ideas bolted together.

     IT COSTS NOTHING. One element rotates; the wake and the point are its
     children and ride it. No layout, no repaint of the ring, no measuring -
     the same discipline the travelling bead was moved onto on 22 September
     when animating `left` was costing 379 relayouts in six seconds.

     A SMALLER FOOTPRINT, WHICH IS THE POINT. The wire was up to 150px wide
     across the top right and it was landing on page controls. This is 26px.
     ====================================================================== */

  /* THE WIREFRAME. Dim, and drawn from the ember's own base rather than from
     the text colour, so it is Lucid's line on every theme rather than a grey
     circle that happens to be there. */

  /* THE DULL GLOW. The one thing that is lit when the charge is elsewhere,
     and it is deliberately low: a mark that has to be alive to be legible is
     not a mark. At 16px this dot inside a ring is still a Lucid. */

  /* THE LAP. Slow - twenty-six seconds - and it does not run at one speed.
     It holds at the top for the first two seconds of every lap, drifts a
     hair past the bottom and settles back. Two frames of hesitation is the
     difference between something live and something animated. */
  @keyframes lthLensLap{
    0%,7%   { transform:rotate(0deg) }
    48%     { transform:rotate(184deg) }
    53%     { transform:rotate(180deg) }
    95%     { transform:rotate(357deg) }
    100%    { transform:rotate(360deg) } }

  /* THE WAKE, trailing anticlockwise behind a point that travels clockwise.
     Masked to the ring itself so it lights the wireframe rather than
     smearing a wedge across the middle of it. */

  /* THE CHARGE. Same bead the wire had - lit from above and to the left with
     a real highlight on it - and the same flicker as it lands, which here is
     at the top of every lap rather than at the far end of a line. */
  @keyframes lthLensFlick{
    0%{ opacity:1 } 1.8%{ opacity:.34 } 3.2%{ opacity:1 }
    4.6%{ opacity:.52 } 6.2%,100%{ opacity:1 } }

  /* THE LENS MEANS YOUR DAY, exactly as the line did and off the same flag.
     House rule 211 still applies at full strength: the third state - we have
     not looked yet - is the ring at its neutral pace, and a quiet day and a
     day we failed to read still do not get to look alike. */

  /* ======================================================================
     THE BAND.  24 September: "that sparkline is short, and runs into the
     buttons on many pages, as does launchpad ... Move it, extend it,
     rearrainge, do something. Not change the sparkline."

     Nothing about the sparkline changed. What changed is that it stopped
     floating over the page. The launchpad sits BESIDE it now rather than
     under it, and .main reserves this row's height at the top of every
     page, so the corner belongs to the thread and a page control can no
     longer be laid out into it. The launchpad is on the left so the line,
     and the panel that hangs off it, stay flush with the corner.
     ====================================================================== */
  /* .lth-top, NOT .lth-head, AND THAT IS THE WHOLE OF A REAL BUG.
     .lth-head was already taken - it is the small-caps heading inside the Ask
     Lucid panel, defined three hundred rows further down and therefore
     winning. My row inherited its 4px top padding, which pushed the LINE down
     four pixels while the rail carrying the travelling point stayed where it
     was, so the charge ran four pixels ABOVE the wire it travels on.
     Measured: line centre 19.75, point centre 15.75. And it worked both ways -
     every panel heading was being handed display:flex and a 10px gap.
     Invisible in every number the app collects and perfectly obvious in a
     magnified picture, which is the third time this week that has been the
     shape of it. */
  .lth-top{ display:block; }


  /* THE CHARGE. Matt, 24 September: "you still have not changed from edge on
     a wire to the charge (#5) for Lucid."

     Edge-on wire lit the whole line and slid a bead along it. The charge does
     the opposite and that is the entire idea: THE WIRE SITS DIM AND UNLIT
     UNTIL THE LIGHT REACHES IT. What you see at rest is a dark line; what
     makes it alive is one point travelling it, a wake dragging behind that
     point, and a flicker as it lands at each end - two frames of hesitation,
     which is what reads as live rather than animated.

     He chose this from mark-depth 5 on 23 September and it went onto the
     Lens instead. This is the same treatment on the line it was drawn for.

     The paint is one flat dim colour now rather than a cross-section,
     because a wire that is not lit has no highlight to shade. The
     left-to-right falloff stays as the mask. Nothing about what the line
     MEANS has changed: same three lengths, same day states. */
  /* THE OLD PAINT IS OFF. This rule used to draw a wire of its own — a
     background gradient under a left-to-right mask — and lucid-mark.js draws
     a wire on a canvas inside it. That is two wires in one place, and the
     canvas one is the drawing Matt chose on 26 September. Background and mask
     go; the box stays, because the canvas needs a host and the click needs a
     target.

     THE BOX IS 10px AND THE CANVAS IS 30px. Idle uses 7 and is 95% of the
     wire's life, so 10 is all that is reserved. The canvas hangs 20px past its
     box, over the top of the content, so listening and say can swell without
     anything reflowing and without a tall empty strip sitting there all day. */
  .lth-line{ position:relative; container-type:inline-size;
    height:10px; margin-top:var(--th-mt, 0px); margin-right:0;
    width:100%; cursor:pointer; overflow:visible;
    transition:filter .4s ease; }
  /* The reach past the ink is NOT added here. `.lth-line` already carries
     padding:8px 0 12px under (hover:hover) and (pointer:fine) — that padding
     is the door, it is what tests/test-lucid-is-the-door.js measures, and a
     second pseudo-element reach on top of it would be two answers to one
     question. The canvas rules live with the override further down, which is
     the block that actually wins. */
  .lth:hover .lth-line{ filter:brightness(1.25); }

  /* HELD BREATH, the second half of what he chose, and it is only timing.
     The wire breathes on nine seconds against the light's fifteen, so the
     pair never land the same way twice inside three quarters of a minute.
     It is a veil of the page's own ground rather than an animation on the
     line itself — partly because it then rides under the same mask, and
     partly because an endless animation on #lthLine is an animation whose
     finished promise never settles, and two checks wait on exactly that. */
  .lth-line::before{ content:''; position:absolute; inset:0; pointer-events:none;
    background:var(--bg-base); opacity:0;
    animation:lthLineBreath 9s ease-in-out infinite; }
  @keyframes lthLineBreath{ 0%,100%{ opacity:0 } 50%{ opacity:.18 } }
  /* Still when there is nothing to share (13 October) — the breath goes with
     the travelling light. See data-mark-still-idle in lucid-mark.js. */
  .lth-line[data-mark-still-idle][data-mark-sharing="0"][data-mark-state="idle"]::before{ animation:none; }

  /* THE RAIL. The light used to live INSIDE the line, which meant that on a
     quiet or a pressing day it was travelling inside a filtered box and the
     whole strip was redrawn every frame instead of the bead being slid. The
     rail is the same 1.5px box in the same place, absolutely positioned and
     carrying no filter, so the bead now moves on its own. It measures
     nothing: the container is the rail, and every keyframe is still cqw. */
  /* GONE. List item 4: "I dont love the node running the border. More
     distracting than cool." The node was motion with no meaning; the wire's
     five states are nothing but meaning. Kept as a rule rather than deleted so
     the history above it still reads, and so nothing that referenced it
     throws. */
  .lth-rail{ display:none !important; }

  /* THE CHARGE ITSELF, and it is the only lit thing on the line. It carries
     its own wake - see ::before below - and it flickers as it lands.
     background-color stays solid underneath: it is the colour everything else
     reads the light off. */
  .lth-node{ position:absolute; top:-1.75px; left:0; width:5px; height:5px; border-radius:50%;
    background-color:var(--th-hot); pointer-events:none;
    background-image:radial-gradient(circle at 36% 28%,
      var(--th-hot) 0%,
      color-mix(in srgb, var(--th-hot) 62%, var(--th-mid)) 30%,
      var(--th-mid) 60%,
      color-mix(in srgb, var(--th-base) 82%, var(--bg-base)) 100%);
    box-shadow:0 0 10px 3px color-mix(in srgb, var(--th-mid) 72%, transparent),
               0 0 22px 7px color-mix(in srgb, var(--th-base) 34%, transparent);
    animation:lthRun 19.5s ease-in-out infinite,
               lthCharge 19.5s linear infinite; }

  /* THE WAKE. A short streak behind the point, fading to nothing, drawn on
     the point itself so it is slid by the same transform and costs nothing
     extra. It has to be BEHIND the charge, which means it has to turn round
     when the charge does - hence the flip. The demo's own note for this
     treatment said it in one line: "the flicker is opacity, the wake is a
     slide and a flip." */
  .lth-node::before{ content:''; position:absolute; top:1.75px; right:2.5px;
    width:26px; height:1.5px; pointer-events:none; border-radius:1px;
    transform-origin:100% 50%;
    background:linear-gradient(270deg,
      color-mix(in srgb, var(--th-hot) 82%, transparent) 0%,
      color-mix(in srgb, var(--th-mid) 46%, transparent) 34%,
      color-mix(in srgb, var(--th-base) 18%, transparent) 66%,
      transparent 100%);
    animation:lthWake 19.5s linear infinite; }

  /* THE FLICKER AS IT LANDS. Twice at the far end, twice on the way home.
     The percentages are read off lthRun below: it arrives at the far end at
     38% and settles by 46%; it arrives back at 94%. Nothing else on the line
     moves during those frames, which is what makes them read as hesitation
     rather than as a blink. */
  @keyframes lthCharge{
    0%,37%    { opacity:1 }
    38.5%     { opacity:.4 }
    40%       { opacity:1 }
    41.5%     { opacity:.55 }
    43%,93%   { opacity:1 }
    94%       { opacity:.42 }
    95.5%     { opacity:1 }
    96.5%     { opacity:.6 }
    98%,100%  { opacity:1 } }

  /* The wake points back the way the charge came from, so it has to turn
     round when the charge does. IT IS FADED OUT BEFORE IT TURNS, for two
     reasons: a streak that swings through the point would read as a glitch,
     and at the far end a wake pointing forward would hang 26px off the end
     of the line - and the one thing the light may never do is leave it.
     A wake decaying while the point is stopped is also simply what a wake
     does.

     Read off lthRun: travelling right 6%-38%, parked at the far end 42%-62%,
     travelling left 62%-92%, parked at the start 97%-5%. */
  @keyframes lthWake{
    0%, 5%    { opacity:0; transform:scaleX(1) }
    9%, 36%   { opacity:1; transform:scaleX(1) }
    41%       { opacity:0; transform:scaleX(1) }
    /* turned while it cannot be seen */
    /* Held at nothing for the whole of the stop. Measured at 52% of the lap
       before this line: the streak was up at a quarter opacity and pointing
       RIGHT, off the end of a line the point was parked on. */
    50%, 61%  { opacity:0; transform:scaleX(-1) }
    68%, 90%  { opacity:1; transform:scaleX(-1) }
    95%       { opacity:0; transform:scaleX(-1) }
    99%, 100% { opacity:0; transform:scaleX(1) } }
  /* It hesitates before it leaves, overshoots a hair at the far end, and
     then WAITS there two and a half seconds before starting back. The
     overshoot is two pixels PAST where it settles and still flush with the
     end of the line, because the one thing the light may never do is leave
     it — the prototype went a pixel beyond the end and test-pearls is right
     about that. */
  @keyframes lthRun{
    0%,5%   { transform:translateX(1cqw) }
    9%      { transform:translateX(3cqw) }
    38%     { transform:translateX(calc(100cqw - 9px)) }
    42%     { transform:translateX(calc(100cqw - 5px)) }
    46%,62% { transform:translateX(calc(100cqw - 7px)) }
    90%     { transform:translateX(calc(1cqw + 2px)) }
    94%     { transform:translateX(0px) }
    97%,100%{ transform:translateX(1cqw) } }

  /* ----------------------------------------------------------------------
     THE LINE MEANS YOUR DAY. Matt: "I love this and the line means your day."

     THIS IS THE ONE THING ON SCREEN THAT CLAIMS TO KNOW SOMETHING, so house
     rule 211 applies at full strength: it reads askOpeningNotes()' own `hot`
     flag and NOTHING ELSE. That flag has four causes and they are all facts —
     a meeting inside half an hour, a task already late, a thread two weeks
     unanswered, something expiring this week.

     THERE IS A THIRD STATE AND IT IS THE IMPORTANT ONE. Before the day has
     loaded we do not know whether it is quiet, and a short dim line would be
     a claim we cannot support — "nothing is pressing" is not the same
     sentence as "we have not looked yet". So the thread starts at its neutral
     length and stays there until there is an answer. An empty day and a day
     we failed to read do not get to look alike.
     ---------------------------------------------------------------------- */
  :root[data-lucid-day="quiet"]    .lth{ --th-len:78px; }
  :root[data-lucid-day="pressing"] .lth{ --th-len:150px; }
  :root[data-lucid-day="quiet"]    .lth-line{ filter:brightness(.72); }
  :root[data-lucid-day="pressing"] .lth-line{ filter:brightness(1.3); }
  /* THE WAKE HAS TO SHARE THE CHARGE'S CLOCK. Every rule that changes how
     fast the point travels has to change the wake with it, or the streak
     points the wrong way for half the lap. Measured on a pressing day before
     this line existed: at 77% of the lap the point was travelling left and
     the wake was still mid-flip, because the point was on 10.4 seconds and
     the wake was on 19.5. */
  :root[data-lucid-day="pressing"] .lth-node,
  :root[data-lucid-day="pressing"] .lth-node::before{ animation-duration:10.4s; }

  /* State. Thinking runs it hot; answered sends it once and leaves it there. */
  :root[data-lucid-state="thinking"] .lth-node,
  :root[data-lucid-state="thinking"] .lth-node::before{ animation-duration:1.9s; }
  :root[data-lucid-state="settling"] .lth-node{
    animation:lthSettle 1.05s cubic-bezier(.2,.9,.25,1) forwards; }
  /* Settling replaces the lap outright, so there is no lap for a wake to
     belong to. It goes out rather than hanging off a point on a one-way
     journey pointing at nothing. */
  :root[data-lucid-state="settling"] .lth-node::before{
    animation:none; opacity:0; }
  @keyframes lthSettle{ 0%{ transform:translateX(1cqw) }
                        70%,100%{ transform:translateX(calc(100cqw - 5px)) } }
  :root[data-lucid-state="settling"] .lth-line{ filter:brightness(1.75); }

  /* Off-screen, or a person asking for reduced motion: it stops dead. A light
     animating on a window nobody is looking at is the purest waste there is.

     CHEAP MODE IS NO LONGER ONE OF THEM, 22 September. It used to be, on the
     reasoning that a slow machine should not be paying for an ornament — but
     this animation moved off `left` and onto `transform` this morning, so it
     runs on the compositor and forces no layout at all. There is nothing left
     to save by stopping it, and the thread standing still is the one way the
     app can look dead on exactly the machine that can least afford it. Cheap
     mode keeps every other saving: the blurs and the shadows still stand
     down. */
  :root[data-lucid-awake="false"] .lth-node,
  :root[data-lucid-awake="false"] .lth-node::before,
  :root[data-lucid-awake="false"] .lth-line::before{ animation-play-state:paused !important; }
  @media (prefers-reduced-motion:reduce){
    .lth-node{ animation:none !important; transform:translateX(62cqw); opacity:1; }
    /* No wake standing still: a streak behind a point that is not moving is
       a smear, not a wake. */
    .lth-node::before{ animation:none !important; opacity:0; }
    .lth-line::before{ animation:none !important; opacity:0; }
    .lth-line{ transition:none; } }

  .lth-hint{ font-size:11px; color:var(--text-dim); margin:7px 20px 0 0;
             opacity:0; transition:opacity .3s; pointer-events:none; }
  .lth:hover .lth-hint{ opacity:1; }

  /* THE PANEL HANGS OFF THE LINE, and the line is its top edge — which is the
     whole reason the node keeps running once it is open. */
  .lth-panel{ position:relative; width:var(--th-len); overflow:hidden;
    background:color-mix(in srgb, var(--bg-base) 82%, transparent);
    -webkit-backdrop-filter:blur(28px) saturate(1.7);
            backdrop-filter:blur(28px) saturate(1.7);
    border-radius:0 0 15px 15px;
    box-shadow:0 30px 66px -26px rgba(0,0,0,.88);
    height:0; max-height:0; opacity:0; pointer-events:none;
    transition:height .5s cubic-bezier(.2,.9,.25,1),
               max-height .5s cubic-bezier(.2,.9,.25,1), opacity .3s ease,
               width .52s cubic-bezier(.2,.9,.25,1); }
  .lth-pad{ padding:16px 18px 15px; transition:opacity .15s ease; }

  /* The three lengths. This is the design. */
  :root[data-lucid-state="listening"] .lth{ --th-len:230px; }
  :root[data-lucid-open="1"] .lth{ --th-len:420px; }
  :root[data-lucid-open="tray"] .lth{ --th-len:420px; }
  :root[data-lucid-open="wide"] .lth{ --th-len:min(1040px, calc(100vw - 40px)); }
  :root[data-lucid-open="1"] .lth-panel,
  :root[data-lucid-open="wide"] .lth-panel{
    height:var(--lth-h, 300px); max-height:min(52vh, 380px);
    opacity:1; pointer-events:auto; }
  :root[data-lucid-open="tray"] .lth-panel{
    height:var(--lth-h, calc(100vh - 14px)); max-height:calc(100vh - 14px);
    opacity:1; pointer-events:auto; }
  :root[data-lucid-open="1"] .lth,
  :root[data-lucid-open="tray"] .lth,
  :root[data-lucid-open="pad"] .lth,
  :root[data-lucid-open="wide"] .lth{ --th-mt:0px; }
  :root[data-lucid-open="1"] .lth-hint,
  :root[data-lucid-open="tray"] .lth-hint,
  :root[data-lucid-open="pad"] .lth-hint,
  :root[data-lucid-open="wide"] .lth-hint{ display:none; }
  /* While it is open the day-length must not fight the open-length. */
  :root[data-lucid-open="1"] .lth-line,
  :root[data-lucid-open="tray"] .lth-line,
  :root[data-lucid-open="pad"] .lth-line,
  :root[data-lucid-open="wide"] .lth-line{ --th-len:inherit; filter:none; }

  .lth-tray{ display:flex; flex-direction:column; height:0; min-height:0;
    overflow:hidden; opacity:0; pointer-events:none;
    transition:opacity .3s ease; }
  :root[data-lucid-open="tray"] .lth-tray{
    height:var(--lth-h, calc(100vh - 14px)); opacity:1; pointer-events:auto; }
  :root[data-lucid-open="tray"] .lth-pad{
    position:absolute; top:0; left:0; right:0; opacity:0; pointer-events:none; }

  .chat-card.in-tray{ position:relative; inset:auto; z-index:20;
    width:100%; max-width:none; height:auto; max-height:none;
    flex:1 1 auto; min-height:0;
    border-top:0; border-radius:0; box-shadow:none;
    background:color-mix(in srgb, var(--bg-base) 93%, transparent); }
  .chat-card.in-tray.open{ animation:none; }

  @media (prefers-reduced-motion:reduce){
    .lth-panel, .lth-tray, .lth-pad{ transition:none; } }

  .lth-field{ display:flex; align-items:center; gap:11px; }
  .lth-field input{ flex:1; min-width:0; background:transparent; border:0; outline:0;
    color:var(--text); font-family:inherit; font-size:15px; }
  .lth-field input::placeholder{ color:var(--text-dim); }
  /* TYPING HAS LIGHT. 90ms. You would not name it; you would miss it. */
  .lth-line.lth-tap{ filter:brightness(1.9); }
  .lth-dot{ width:8px; height:8px; border-radius:50%; flex:none; background:var(--th-hot);
    box-shadow:0 0 9px 2px color-mix(in srgb, var(--th-mid) 65%, transparent); }
  .lth-btn{ flex:none; width:30px; height:30px; border-radius:50%; padding:0; border:0;
    background:transparent; color:var(--text-dim); cursor:pointer; display:grid; place-items:center; }
  .lth-btn svg{ width:17px; height:17px; }
  .lth-btn:hover{ color:var(--text); background:color-mix(in srgb, var(--text) 8%, transparent); }

  .lth-rows{ margin-top:12px; border-top:1px solid color-mix(in srgb, var(--text) 10%, transparent);
             padding-top:7px; }
  .lth-head{ font:600 9.5px/1 system-ui; letter-spacing:.17em; text-transform:uppercase;
             color:var(--text-dim); padding:4px 2px 5px; }
  .lth-row{ display:flex; gap:10px; align-items:baseline; padding:6px 3px; border-radius:6px;
            width:100%; border:0; background:transparent; text-align:left;
            font-family:inherit; color:var(--text); cursor:pointer; }
  .lth-row:hover{ background:color-mix(in srgb, var(--text) 7%, transparent); }
  .lth-row-t{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis;
              white-space:nowrap; font-size:13px; }
  .lth-row-w{ flex:none; font-size:11.5px; color:var(--text-dim); }
  .lth-row.hot .lth-row-w{ color:var(--th-mid); }

  /* LISTENING. The same line, made into a level. Nothing new appears. */
  .lth-wave{ position:absolute; inset:-9px 0; display:none; align-items:center; gap:3px;
             pointer-events:none; }  /* lives on the rail, not under the mask */
  :root[data-lucid-state="listening"] .lth-wave{ display:flex; }
  :root[data-lucid-state="listening"] .lth-node{ opacity:0; }
  .lth-wave i{ flex:1; height:2px; border-radius:2px; background:var(--th-mid);
    transform-origin:center; animation:lthAmp 1.15s ease-in-out infinite; }
  @keyframes lthAmp{ 0%,100%{ transform:scaleY(.6); opacity:.45 } 50%{ transform:scaleY(7); opacity:1 } }
  @media (prefers-reduced-motion:reduce){ .lth-wave i{ animation:none; transform:scaleY(2); } }

  /* NOT BEFORE HE IS IN. */
  body:has(#loginGate.show) .lth,
  body:has(#onboardGate.show) .lth{ display:none; }

  /* ======================================================================
     WHAT THE THREAD REPLACES. All hidden, none deleted — deleting comes in
     its own pass so that a mistake here is one line to undo.
     Matt: "Make sure we remove all of the orbs, and settings for the orb.
     Only choice will be Lucid color picker."
     ====================================================================== */
  .chat-pill{ display:none !important; }   /* the 88px floating orb */
  .player-bar{ display:none !important; }  /* the player along the bottom */
  body.playing .bottom-nav{ bottom:0; }    /* the player's old shove, retired */

  /* On a phone the thread is the same object in the same corner. The app's
     own header sits below it, so it needs a little air rather than a layout. */
  @media (max-width:780px){
    .lth{ --th-mt:13px; }
    :root{ --lth-band:40px; }
    .lth-panel{ border-radius:0 0 13px 13px; }
    :root[data-lucid-open="1"] .lth{ --th-len:min(360px, calc(100vw - 26px)); }
    :root[data-lucid-open="tray"] .lth{ --th-len:min(360px, calc(100vw - 26px)); }
    :root[data-lucid-open="wide"] .lth{ --th-len:calc(100vw - 16px); }
    .lth-field input{ font-size:16px; }  /* 16px or iOS zooms on focus */

    /* ---- THE BIG ASK LUCID BUTTON --------------------------------------
       Matt: "phone app should have a big 'ask Lucid' button. Talking and
       listening will be heavily focused on phone."

       A BAR ACROSS THE BOTTOM, NOT A CIRCLE IN THE CORNER, and the reason is
       the corner's own history. The 88px sphere stood in the bottom-right and
       had to be paid for with --orb-keepout-x — a variable four rules read,
       covering four views out of forty, so on the other thirty-six it stood on
       whatever the page had put there. A bar is cleared by --bottom-nav-h's
       neighbour --ask-big-h, which .main's floor and the mail list already
       read, so it covers nothing on every screen with no per-view rule.

       IT IS THE WHOLE WIDTH LESS THE PAGE'S OWN GUTTER, which puts it in the
       same column as the content above it rather than floating free of it,
       and it sits a step above the nav so the two read as two controls and
       not one taller bar.

       NO MICROPHONE GLYPH. The mic is one tap further in, inside the panel
       this opens. A mic here would claim Lucid listens on this device. */
    .ask-big{ display:flex; position:fixed; z-index:11;
      left:var(--space-4); right:var(--space-4);
      bottom:calc(var(--bottom-nav-h) + var(--space-3));
      /* height was var(--ask-big-h), retired with the button on 5 October. */
      align-items:center; justify-content:center;
      gap:var(--space-3); padding:0 var(--space-4);
      border:1px solid var(--glass-border); border-radius:var(--radius-pill);
      cursor:pointer; -webkit-tap-highlight-color:transparent;
      font-family:var(--font-display); font-weight:600;
      font-size:var(--type-lead); color:var(--text);
      background:color-mix(in srgb, var(--bg-base) 84%, transparent);
      backdrop-filter:blur(18px);
      box-shadow:0 10px 30px rgba(0,0,0,.42); }
    /* THE BUTTON STOPS EATING THE SCROLL — list item 24, 4 October.
       Measured at 430x900: 398px wide on a 430px screen, and elementFromPoint
       at both its centre and its left edge returned the button, so every drag
       starting in that 56px band went to a control instead of to a page with
       7,467px of content in it.
       pan-y hands a vertical drag back to the browser, which pans the page
       underneath. A tap still lands on the button, and the button keeps the
       size he asked for. */
    .ask-big{ touch-action:pan-y; }
    .ask-big:active{ transform:scale(.985); }
    .ask-big:focus-visible{ outline:2px solid var(--blue); outline-offset:3px; }
    /* THE MARK CANNOT BE margin:0 auto HERE, AND IT TAKES TWO WORDS TO SAY
       WHY. .lth-mark is declared LATER in this file than this block with
       the same weight, so a rule on .ask-big-mark alone loses to it and
       the auto margin pushes the mark and the words to opposite ends of
       the button -- which is what the first simulator picture showed.
       Naming both classes outranks it wherever it sits. */
    .ask-big .lth-mark{ flex:none; margin:0; }

    /* WHILE THE PANEL IS OPEN THERE IS NO BUTTON, because the panel comes all
       the way down the screen when a question is asked (tray(), in
       lucid-thread.js) and the button would show through the bottom of its own
       answer, saying Ask Lucid underneath the thing that is answering. */
    :root[data-lucid-open] .ask-big{ display:none; }

    /* AND THE MESSAGES MOVE UP OFF IT. .toast-stack is fixed to the
       bottom-right corner at z-index 9000, and on a 390px phone that
       corner is the bottom bar -- a toast has been landing on the
       navigation labels since the bar was built. Since this button, it
       lands on the way in as well: the check in
       tests/test-the-ask-button-is-on-the-phone.js could not press the
       button at all on a run where a message happened to be up, which is
       exactly what would happen to a thumb.
       It clears both, off the same two names the pages own floor reads,
       so a taller bar or a taller button takes the messages with it. */
    .toast-stack{ bottom:calc(var(--bottom-nav-h) + var(--space-4)); }
  }

  /* ======================================================================
     THE MARK — the thread, standing still, where the orb used to be.

     The sign-in screen, the welcome screen and the Ask Lucid window's header
     each carried an orb (104px, 104px, 30px). Matt: "Make sure we remove all
     of the orbs." They still need SOMETHING — a sign-in screen with a bare
     word on it is not a brand — and the honest answer is the object the app
     already is: a line with a light travelling it.

     So the first thing you ever see of Lucid is the thing Lucid does all day,
     rather than a beautiful object you never meet again.
     ====================================================================== */
  /* THE SAME OBJECT, STANDING STILL. The sign-in screen, the welcome screen
     and the Ask Lucid header all draw the mark from the same three pieces the
     top-right one uses, so there is exactly one Lucid and --m is its diameter.
     No HTML changed here: .lth-mark is the ring and the core, .lth-mark-line
     is the lap that carries the wake, .lth-mark-node is the charge. */
  .lth-mark{ width:var(--m,132px); height:var(--m,132px); margin:0 auto;
    position:relative; border-radius:50%;
    border:max(1.25px, calc(var(--m,132px) * .014)) solid
           color-mix(in srgb, var(--th-base) 34%, transparent); }
  /* the dull glow at the centre */
  .lth-mark::after{ content:''; position:absolute; left:50%; top:50%;
    width:calc(var(--m,132px) * .30); height:calc(var(--m,132px) * .30);
    transform:translate(-50%,-50%); border-radius:50%; opacity:.46;
    background:radial-gradient(circle,
      color-mix(in srgb, var(--th-hot) 82%, transparent) 0%,
      color-mix(in srgb, var(--th-mid) 46%, transparent) 34%,
      color-mix(in srgb, var(--th-base) 16%, transparent) 62%,
      transparent 100%); }
  /* the breath, still nine seconds, still a veil of the page's own ground */
  .lth-mark::before{ content:''; position:absolute; inset:calc(-1 * max(1.25px, calc(var(--m,132px) * .014)));
    pointer-events:none; border-radius:50%;
    border:max(1.25px, calc(var(--m,132px) * .014)) solid var(--bg-base);
    opacity:0; animation:lthLineBreath 9s ease-in-out infinite; }
  :root[data-mode="light"] .lth-mark::before{ border-color:var(--text); }
  @media (prefers-reduced-motion:reduce){ .lth-mark::before{ animation:none; opacity:0; } }

  .lth-mark-line{ position:absolute; inset:calc(-1 * max(1.25px, calc(var(--m,132px) * .014)));
    display:block; border-radius:50%; will-change:transform;
    background:conic-gradient(from 0deg,
      transparent 0deg, transparent 186deg,
      color-mix(in srgb, var(--th-base) 46%, transparent) 248deg,
      color-mix(in srgb, var(--th-mid) 58%, transparent) 300deg,
      color-mix(in srgb, var(--th-mid) 88%, transparent) 338deg,
      color-mix(in srgb, var(--th-hot) 96%, transparent) 358deg,
      color-mix(in srgb, var(--th-hot) 96%, transparent) 360deg);
    -webkit-mask-image:radial-gradient(circle,
      transparent 0 calc(50% - max(1.7px, calc(var(--m,132px) * .019))),
      #000 calc(50% - max(1.7px, calc(var(--m,132px) * .019))) calc(50% - .15px),
      transparent calc(50% - .15px));
            mask-image:radial-gradient(circle,
      transparent 0 calc(50% - max(1.7px, calc(var(--m,132px) * .019))),
      #000 calc(50% - max(1.7px, calc(var(--m,132px) * .019))) calc(50% - .15px),
      transparent calc(50% - .15px));
    animation:lthLensLap 26s cubic-bezier(.45,.02,.55,.98) infinite; }

  .lth-mark-node{ position:absolute; top:calc(var(--m,132px) * -.031); left:50%;
    width:calc(var(--m,132px) * .062); height:calc(var(--m,132px) * .062);
    margin-left:calc(var(--m,132px) * -.031); border-radius:50%;
    background-color:var(--th-hot);
    background-image:radial-gradient(circle at 36% 28%,
      var(--th-hot) 0%,
      color-mix(in srgb, var(--th-hot) 62%, var(--th-mid)) 32%,
      var(--th-mid) 64%,
      color-mix(in srgb, var(--th-base) 82%, var(--bg-base)) 100%);
    box-shadow:0 0 calc(var(--m,132px) * .045) 1px color-mix(in srgb, var(--th-mid) 54%, transparent),
               0 0 calc(var(--m,132px) * .10) 2px color-mix(in srgb, var(--th-base) 22%, transparent);
    animation:lthLensFlick 26s linear infinite; }
  @media (prefers-reduced-motion:reduce){
    .lth-mark-line{ animation:none; transform:rotate(214deg); }
    .lth-mark-node{ animation:none; opacity:1; } }
  .chat-header-icon{ display:flex; align-items:center; justify-content:center; }

  /* The way back into the Ask Lucid window — see the note in index.html.
     Two words, sitting quietly under the field, weighted like a footnote
     rather than a control. */
  .lth-foot{ display:flex; gap:14px; margin-top:11px; padding-top:9px;
             border-top:1px solid color-mix(in srgb, var(--text) 8%, transparent); }
  .lth-foot-a{ border:0; background:transparent; padding:0; cursor:pointer;
    font-family:inherit; font-size:11.5px; letter-spacing:.01em;
    color:var(--text-dim); transition:color .18s ease; }
  .lth-foot-a:hover{ color:var(--text); }

  /* LUCID HAS SOMETHING TO SAY. Was a dot on the orb; the orb is gone.
     A second, slower light behind the first — the line reads as carrying
     something rather than merely being lit. No badge, no number, no dot. */
  .lth.has-word .lth-rail::after{
    content:''; position:absolute; inset:-3px -2px;
    background:radial-gradient(60% 300% at 50% 50%,
      color-mix(in srgb, var(--th-mid) 30%, transparent), transparent 70%);
    animation:lthBreath 6s ease-in-out infinite; pointer-events:none; }
  @keyframes lthBreath{ 0%,100%{ opacity:.35 } 50%{ opacity:.9 } }
  @media (prefers-reduced-motion:reduce){ .lth.has-word .lth-rail::after{ animation:none; opacity:.6; } }
  :root[data-fx="lite"] .lth.has-word .lth-rail::after{ animation:none; opacity:.6; }

  /* ======================================================================
     THE THREAD ON PAPER.

     The orb had a light path. It went out with the orb on 21 September and
     nothing took its place, so on all eight paper themes the thread was the
     dark-room object dimmed: a hairline of #FFB347 and a #FFF6E4 node on
     cream, which is the paper drawn on the paper. A glow needs a dark room.
     On cream the bloom is fog and the core has to carry it, so the same
     three stops and the same travelling node are drawn in the thread's own
     colour pulled toward the page's ink instead of toward white, and every
     halo comes down to a rim.

     Driven from data-mode, so a new paper theme inherits it rather than
     needing a line adding here.
     ====================================================================== */
  :root[data-mode="light"]{
    --th-ink:color-mix(in srgb, var(--th-base) 58%, var(--text));
    --th-ink-deep:color-mix(in srgb, var(--th-base) 40%, var(--text));
  }
  :root[data-mode="light"] .lth-line{
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--th-ink) 26%, transparent) 0%,
      color-mix(in srgb, var(--th-ink-deep) 100%, transparent) 44%,
      color-mix(in srgb, var(--th-ink) 74%, transparent) 70%,
      color-mix(in srgb, var(--th-ink) 24%, transparent) 100%); }
  /* On paper a breath cannot brighten, so it DEEPENS: the veil is ink. */
  :root[data-mode="light"] .lth-line::before{ background:var(--text); }
  :root[data-mode="light"] .lth:hover .lth-line{ filter:brightness(.82); }
  :root[data-mode="light"] .lth-line.lth-tap{ filter:brightness(.68); }
  :root[data-mode="light"] .lth-node{ background-color:var(--th-ink-deep);
    background-image:radial-gradient(circle at 36% 26%,
      color-mix(in srgb, var(--th-ink) 22%, var(--bg-base)) 0%,
      color-mix(in srgb, var(--th-ink) 72%, transparent) 26%,
      var(--th-ink-deep) 68%, var(--text) 100%);
    box-shadow:0 0 3px .5px color-mix(in srgb, var(--th-ink) 34%, transparent); }
  :root[data-mode="light"] .lth-dot{ background:var(--th-ink-deep);
    box-shadow:0 0 4px 1px color-mix(in srgb, var(--th-ink) 30%, transparent); }
  :root[data-mode="light"] .lth-wave i{ background:var(--th-ink); }
  :root[data-mode="light"] .lth-row.hot .lth-row-w{ color:var(--th-ink-deep); }
  :root[data-mode="light"] .lth-panel{
    background:color-mix(in srgb, var(--bg-base) 93%, transparent);
    box-shadow:0 26px 54px -26px color-mix(in srgb, var(--text) 44%, transparent); }
  :root[data-mode="light"] .lth.has-word .lth-rail::after{
    background:radial-gradient(60% 300% at 50% 50%,
      color-mix(in srgb, var(--th-ink) 24%, transparent), transparent 70%); }
  :root[data-mode="light"] .lth-mark{
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--th-ink) 26%, transparent) 0%,
      color-mix(in srgb, var(--th-ink-deep) 100%, transparent) 44%,
      color-mix(in srgb, var(--th-ink) 74%, transparent) 70%,
      color-mix(in srgb, var(--th-ink) 24%, transparent) 100%); }
  :root[data-mode="light"] .lth-mark-node{ background-color:var(--th-ink-deep);
    background-image:radial-gradient(circle at 36% 26%,
      color-mix(in srgb, var(--th-ink) 22%, var(--bg-base)) 0%,
      color-mix(in srgb, var(--th-ink) 72%, transparent) 26%,
      var(--th-ink-deep) 68%, var(--text) 100%);
    box-shadow:0 0 3px .5px color-mix(in srgb, var(--th-ink) 34%, transparent); }

  /* brightness() runs the other way on ink: on cream a quiet day is a line
     that lifts toward the paper, and a pressing one sinks into it. */
  :root[data-mode="light"][data-lucid-day="quiet"] .lth-line{ filter:brightness(1.4); }
  :root[data-mode="light"][data-lucid-day="pressing"] .lth-line{ filter:brightness(.78); }
  :root[data-mode="light"][data-lucid-state="settling"] .lth-line{ filter:brightness(.7); }

  /* ----------------------------------------------------------------------
     THE FOURTH LENGTH. The launchpad is not a second tray: it is this same
     panel at 264px, running to the bottom of the screen on the same curve.

     THE RUNGS SIT UNDER THE PANEL IN THE COLUMN, so the line is still the
     panel's top edge in every state and the node never stops running. They are
     .lth-rungs and not .lth-mark: .lth-mark is already the brand on the sign-in
     screen, the welcome screen and the Ask Lucid header, and a second rule
     under that name at the bottom of this file quietly took all three of them
     over — 34px wide, stacked, and no line at all.
     ---------------------------------------------------------------------- */
  /* THE RIGHT WALL — 3 October. Matt: "hate only having a shortcut to access
     the launch pad. Maybe move current hamburger menu that sits near the top
     bar and move to the right wall....maybe 1/3rd down to stay clear of Lucid
     wire."

     So it leaves the row it shared with the wire and goes to the wall, a third
     down. Stood on end: three vertical hairlines in the same 1.5px ink as the
     wire, so it reads as the wire's sibling rather than a borrowed icon. And
     the rule that makes the whole thing coherent — YOU TOUCH AN EDGE, THAT
     EDGE OPENS. The wire is the top, so Lucid comes down from it. This is the
     right wall, so rooms come out of there. */
  .lth-rungs{ position:fixed; right:0; top:33%; z-index:61;
    width:auto; height:auto; margin:0; flex:none;
    background:none; border:0; padding:16px 10px 16px 18px; cursor:pointer;
    display:flex; flex-direction:row; align-items:center; justify-content:center;
    gap:4px; transition:opacity .3s ease; }
  .lth-rungs i{ display:block; width:1.5px; border-radius:1px;
    background:color-mix(in srgb, var(--th-mid) 62%, transparent);
    transition:height .36s cubic-bezier(.2,.9,.25,1), background .36s ease,
               transform .36s cubic-bezier(.2,.9,.25,1); }
  .lth-rungs i:nth-child(1){ height:17px; }
  .lth-rungs i:nth-child(2){ height:17px; }
  .lth-rungs i:nth-child(3){ height:17px; }
  .lth-rungs:hover i{ height:21px;
    background:color-mix(in srgb, var(--th-hot) 90%, transparent); }
  .lth-rungs:hover i:nth-child(1){ transform:translateX(-2px); }
  .lth-rungs:hover i:nth-child(3){ transform:translateX(2px); }
  .lth-rungs:focus-visible{ outline:2px solid var(--th-mid); outline-offset:4px; border-radius:3px; }

  :root[data-lucid-open="1"] .lth-rungs,
  :root[data-lucid-open="tray"] .lth-rungs,
  :root[data-lucid-open="wide"] .lth-rungs{ opacity:0; pointer-events:none; }
  /* ======================================================================
     THE THREAD, EXTENDED — EVERY EDGE THAT OPENS IS LUCID.

     Matt, 23 September: "I now would like to add the 'The Thread Extended'
     to the cards. (everywhere actually) Love it." And earlier the same
     night: "the pulse line that is Lucid at top right should disappear when
     the window opens and the window edge becomes Lucid and wanders the rim",
     and "The tray that opens for events, and tasks and stuff on the agenda
     has a rim decided by theme. How about every edge that gets opened is
     Lucid, and has a slow pulse that slowly wanders the edge".

     Four keyframes of transform, against a size measured only when a panel
     changes size. A moving dashed outline or a turning conic gradient would
     repaint the whole rim every frame — the fault that cost 379 re-lay-outs
     in six seconds before the travelling light moved to transform.
     The sizes are written by lucid-rim.js onto the rim element itself, never
     onto the card, so a panel that is growing does not restyle its contents
     to move a dot.

     THE RIM IS A STICKY ANCHOR OF NO HEIGHT with the drawn box hung off it.
     It cannot be an absolute box moved against its surface's scroll: an
     offset box counts as content, and a surface that clips its overflow then
     scrolls itself to chase it — which is exactly what pulled the Ask Lucid
     window 768px off the top of the tray the first time this was built.

     ITS STICKY TOP IS 0, NOT THE PADDING. 14 October; Matt approved fixing
     this on every surface (option B). Sticky's top is measured from the
     scrolling surface's CONTENT edge, padding already taken off, and the
     anchor's own place is that content edge — so top:0 holds it exactly
     where it starts, and the box's hang of --lrim-pt lands on the border.
     It was top:var(--lrim-pt), which pushed the anchor a second padding
     down: on every padded surface that scrolls (40 of the 44 dialogs, among
     them the "Delete?" card and Settings, and the shortcut sheet; New
     Message had its own fix, 2496a01) the outline's top edge was
     drawn 24px inside, through the title, and its bottom hung 24px past the
     edge and was clipped. A surface that does not scroll itself (the
     recording card, the phone's bottom sheet) or has no padding was right
     either way and draws identically. tests/test-every-rim-on-the-edge.js.
     WebKit (the Mac app) is not checked by that test.
     ====================================================================== */
  .lrim{ display:none; position:sticky; top:0; left:0;
    flex:none; height:0; z-index:25; pointer-events:none; border-radius:inherit; }
  .lrim-b{ display:block; position:absolute; pointer-events:none; overflow:hidden;
    top:calc(-1 * var(--lrim-pt, 0px)); left:calc(-1 * var(--lrim-pl, 0px));
    width:var(--lrim-bw, 0px); height:var(--lrim-bh, 0px); border-radius:inherit;
    box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--th-base) 30%, transparent); }
  .lrim-b::before{ content:''; position:absolute; left:0; right:0; top:0; height:1.5px;
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--th-base) 42%, transparent) 0%,
      color-mix(in srgb, var(--th-hot) 96%, transparent) 40%,
      color-mix(in srgb, var(--th-mid) 66%, transparent) 64%,
      color-mix(in srgb, var(--th-base) 22%, transparent) 100%);
    -webkit-mask-image:linear-gradient(90deg, transparent 0%,
      rgba(0,0,0,.5) 20%, rgba(0,0,0,1) 62%, rgba(0,0,0,.42) 100%);
            mask-image:linear-gradient(90deg, transparent 0%,
      rgba(0,0,0,.5) 20%, rgba(0,0,0,1) 62%, rgba(0,0,0,.42) 100%);
    -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
    animation:lrimBreath 9s ease-in-out infinite; }
  @keyframes lrimBreath{ 0%,100%{ opacity:1 } 50%{ opacity:.82 } }
  @keyframes lrimBreathInk{ 0%,100%{ opacity:.86 } 50%{ opacity:1 } }
  .lrim-b::after{ content:''; position:absolute; left:0; top:0; width:5px; height:5px;
    margin:-2px 0 0 -2px; border-radius:50%;
    background-color:var(--th-hot);
    background-image:radial-gradient(circle at 36% 28%,
      var(--th-hot) 0%,
      color-mix(in srgb, var(--th-hot) 62%, var(--th-mid)) 30%,
      var(--th-mid) 60%,
      color-mix(in srgb, var(--th-base) 82%, var(--bg-base)) 100%);
    box-shadow:0 0 10px 3px color-mix(in srgb, var(--th-mid) 72%, transparent),
               0 0 22px 7px color-mix(in srgb, var(--th-base) 34%, transparent);
    animation:lrimRun 34s ease-in-out infinite; }
  @keyframes lrimRun{
    0%,2.5%   { transform:translate3d(0,0,0) }
    5%        { transform:translate3d(4px,0,0) }
    24%       { transform:translate3d(var(--lrim-w, 320px),0,0) }
    45%       { transform:translate3d(var(--lrim-w, 320px),calc(var(--lrim-h, 420px) - 4px),0) }
    47.5%     { transform:translate3d(calc(var(--lrim-w, 320px) - 4px),var(--lrim-h, 420px),0) }
    50%,57.4% { transform:translate3d(var(--lrim-w, 320px),var(--lrim-h, 420px),0) }
    76%       { transform:translate3d(0,var(--lrim-h, 420px),0) }
    97.5%,100%{ transform:translate3d(0,0,0) }
  }

  /* Every popup, tray and sheet in Lucid is a .compose-modal inside a
     .modal-backdrop, so the agenda's event tray, the task tray, every
     settings card and every confirm arrive on one line. The task drawer, the
     Ask Lucid card, the shortcut sheet and the phone's bottom sheet are the
     four that are not. .chat-card.in-tray is absent on purpose: in the tray
     it sits inside the thread's own panel, which already carries the rim. */
  .modal-backdrop.open > .compose-modal > .lrim,
  .pj-drawer.open > .lrim,
  .chat-card.open:not(.in-tray) > .lrim,
  .shortcut-card > .lrim,
  .rec-card.open > .lrim,
  .bn-sheet.open .bn-sheet-card > .lrim{ display:block; }

  /* AND NO BEAD ON A TRAY. 9 October. Matt: "Lucid bead is still wandering
     my trays. Kill it."

     WHAT WAS ACTUALLY ON SCREEN, measured with the task tray open at
     1481x690 before this rule existed: one .lrim-b::after, 5x5px, round,
     running lrimRun on a 34-second lap round a 559x690 rim. It is the only
     travelling light left in the app that has a box — #lthNode has had none
     since the mark commit on 26 September, and the Lens's own charge is the
     logo, not this.

     ONLY THE TRAYS. `as-tray` is the class trayOpen() puts on a backdrop, so
     this is exactly the eleven surfaces in TRAY_SURFACES — the task, event,
     reminder, vault, note, org and contact trays and the agenda's meeting
     tray — and nothing else. A centred confirm, a settings card, the task
     drawer, the Ask Lucid card, the shortcut sheet and the phone's bottom
     sheet all keep the bead: he named the trays, and a rim he has not
     complained about is not mine to strip.

     AND ONLY THE BEAD. The rim stays, and so does its lit top edge: the dot
     is the thing that wanders. This is the same cut the thread's own panel
     already makes six rules below — the light goes, the outline stays. */
  .modal-backdrop.as-tray > .compose-modal > .lrim > .lrim-b::after{ display:none; }

  /* THE NEW MESSAGE RIM ON ITS EDGE (14 October, 2496a01) was a rule of its
     own here, top:0 on that one dialog's anchor. Matt then approved the same
     fix everywhere, so it is now the .lrim rule itself, above. */

  /* On cream the bloom is fog, exactly as the thread already learned. */
  :root[data-mode="light"] .lrim-b{
    box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--th-ink) 34%, transparent); }
  :root[data-mode="light"] .lrim-b::before{
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--th-ink) 26%, transparent) 0%,
      color-mix(in srgb, var(--th-ink-deep) 100%, transparent) 44%,
      color-mix(in srgb, var(--th-ink) 74%, transparent) 70%,
      color-mix(in srgb, var(--th-ink) 24%, transparent) 100%);
    animation-name:lrimBreathInk; }
  :root[data-mode="light"] .lrim-b::after{
    background-color:var(--th-ink-deep);
    background-image:radial-gradient(circle at 36% 26%,
      color-mix(in srgb, var(--th-ink) 22%, var(--bg-base)) 0%,
      color-mix(in srgb, var(--th-ink) 72%, transparent) 26%,
      var(--th-ink-deep) 68%, var(--text) 100%);
    box-shadow:0 0 3px .5px color-mix(in srgb, var(--th-ink) 34%, transparent); }

  :root[data-lucid-awake="false"] .lrim-b::after,
  :root[data-lucid-awake="false"] .lrim-b::before{ animation-play-state:paused !important; }
  @media (prefers-reduced-motion:reduce){
    .lrim-b::after{ animation:none !important;
      transform:translate3d(var(--lrim-w, 320px), 0, 0); }
    .lrim-b::before{ animation:none !important; opacity:1; } }

  /* THE THREAD'S OWN PANEL gets no injected rim, because it already has the
     line: the line IS the top edge and the node has run along it since the
     panel was built. Extended means the node no longer stops at the corner —
     it carries on down the right-hand side, along the floor and back up the
     left, so line and panel are one outline with one light on it. Nothing
     about the line at rest changes. Settling keeps its own gesture: an
     answer arriving is the one moment the light means something. */
  :root[data-lucid-open="1"] .lth-panel > .lrim,
  :root[data-lucid-open="wide"] .lth-panel > .lrim,
  :root[data-lucid-open="pad"] .lth-panel > .lrim,
  :root[data-lucid-open="tray"] .lth-panel > .lrim{ display:block; }
  .lth-panel > .lrim > .lrim-b::before,
  .lth-panel > .lrim > .lrim-b::after{ display:none; }

  :root[data-lucid-open="1"]:not([data-lucid-state="settling"]) .lth-node,
  :root[data-lucid-open="wide"]:not([data-lucid-state="settling"]) .lth-node,
  :root[data-lucid-open="pad"]:not([data-lucid-state="settling"]) .lth-node,
  :root[data-lucid-open="tray"]:not([data-lucid-state="settling"]) .lth-node{
    animation:lthRim 34s ease-in-out infinite; }
  @keyframes lthRim{
    0%,2.5%   { transform:translate3d(0,0,0) }
    5%        { transform:translate3d(4px,0,0) }
    24%       { transform:translate3d(var(--lrim-w, 415px),0,0) }
    45%       { transform:translate3d(var(--lrim-w, 415px),calc(var(--lrim-h, 111px) - 4px),0) }
    47.5%     { transform:translate3d(calc(var(--lrim-w, 415px) - 4px),var(--lrim-h, 111px),0) }
    50%,57.4% { transform:translate3d(var(--lrim-w, 415px),var(--lrim-h, 111px),0) }
    76%       { transform:translate3d(0,var(--lrim-h, 111px),0) }
    97.5%,100%{ transform:translate3d(0,0,0) }
  }
  @media (prefers-reduced-motion:reduce){
    :root[data-lucid-open="1"] .lth-node,
    :root[data-lucid-open="wide"] .lth-node,
    :root[data-lucid-open="pad"] .lth-node,
    :root[data-lucid-open="tray"] .lth-node{
      animation:none !important; transform:translate3d(var(--lrim-w, 415px), 0, 0); } }

  /* =========================================================================
     GOALS — "THE WEEK IT MUST SURVIVE", and ROUTINES — "THE RUN".  24 September
     =========================================================================

     Both taken off the approved drawing in goals-routines-options.html, with
     the demo page's private tokens swapped for the app's real ones — the demo
     carried its own copy of the palette on purpose, and --th-base and friends
     do not exist here. Colour carries meaning: the pink rule down the left of
     a verdict means bad news, and nothing pulses at him.

     MOTION IS transform AND opacity ONLY. The one exception is the run ring,
     which animates stroke-dashoffset — a paint property on an SVG stroke, not
     a layout one. Animating `left` once cost this app 379 re-layouts in six
     seconds and nothing below goes near a layout property. */

/* ==========================================================================
   THE SUBJECT ENGINE — one grid, drawn once, used by Follow and by Sports.
   --------------------------------------------------------------------------
   EVERY COLOUR HERE IS A TOKEN, NOT A HEX. The app has themes and a check that
   measures whether text can be read against what is behind it on every one of
   them; a literal colour in here would pass on the theme it was written for
   and fail silently on the other eleven.

   THE FAILED TILE IS NOT THE QUIET TILE IN A DIFFERENT SHADE. It carries its
   own border and its own way out, because house rule 211 is about being able
   to TELL, and a person tells at a glance before they read a word.
   ========================================================================== */
.subj-grid{
  display:grid; gap:12px; margin-bottom:18px;
  grid-template-columns:repeat(auto-fill, minmax(190px, 1fr));
}
.subj-tile{
  display:flex; flex-direction:column; align-items:flex-start; gap:4px;
  text-align:left; padding:var(--space-3) var(--space-4); min-height:92px;
  border:1px solid var(--glass-border); border-radius:var(--radius-card);
  background:var(--glass-bg); color:var(--text);
  cursor:pointer; font:inherit; transition:border-color .12s ease, transform .12s ease;
}
.subj-tile:hover{ border-color:var(--accent); transform:translateY(-1px); }
.subj-tile:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.subj-name{ font-size:var(--type-lead); font-weight:650; color:var(--text); line-height:1.25; }
.subj-line{ font-size:var(--type-body); color:var(--text-dim); line-height:1.35; }
.subj-age{ font-size:var(--type-label); color:var(--text-dim); }
/* A tile whose feed did not answer is not pressable — there is nothing behind
   it to go to — so it does not pretend to be. */
.subj-tile.subj-failed{ cursor:default; border-color:var(--cat-red, var(--accent)); }
.subj-tile.subj-failed:hover{ transform:none; }
/* A FAILED TILE THAT STILL HAS SOMETHING BEHIND IT SAYS SO WITH THE POINTER.
   For a sport there is nothing to go to when its read failed, and the rule
   above is right. For one of his TEAMS there is — its league table, that
   league's headlines, and the way off the list — so the tile opens, and a
   control that opens must not be drawn as one that does not. Found by looking
   at the picture: the border and the words were right and the pointer said the
   opposite. The border stays red on hover on purpose: it is still a failure. */
.subj-tile.subj-failed.subj-failed-open{ cursor:pointer; }
.subj-tile.subj-failed.subj-failed-open:hover{ transform:translateY(-1px); }
.subj-tile.subj-failed .subj-line{ color:var(--text); }
.subj-tile.subj-waiting{ opacity:.72; cursor:default; }
.subj-retry{
  margin-top:var(--space-1); font-size:var(--type-label); color:var(--accent);
  text-decoration:underline; cursor:pointer;
}
.subj-whole-fail{
  border:1px solid var(--glass-border); border-radius:var(--radius-card);
  background:var(--glass-bg); padding:var(--space-4); margin-bottom:var(--space-5);
}
.subj-back{
  display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
  margin-bottom:14px;
}
.subj-title{ font-size:var(--type-title); font-weight:600; color:var(--text); }

/* ---- THE SCOREBOARD ---- */
.subj-board{ display:flex; flex-direction:column; }

/* A phone has no room for four columns, and a scoreboard squeezed into one is
   unreadable rather than small. Two lines per game instead. */
@media (max-width: 640px){
  .subj-grid{ grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:10px; }
}

/* WHICH FACE OF THE PAGE YOU CAN SEE.
   The page draws all of itself either way — see the note in renderFeed() —
   and this is the only thing that decides which half is on screen. The two
   !important rules are here because the river's own renderer writes inline
   display on those elements every time it runs, and an inline style beats a
   class; without them the river would reappear underneath the grid a moment
   after it was hidden. */
#feedSubjects{ display:none; }
#view-feed.on-subjects #feedSubjects{ display:block; }
#view-feed.on-subjects .feed-split,
#view-feed.on-subjects #feedCards{ display:none !important; }
/* SPORTS HAS ONE FACE NOW, so it needs none of this: its grid is always on and
   there is no second page to hide behind it. #sportsSubjects is therefore not
   hidden by default any more — the rule above used to hide both grids. */

/* ==========================================================================
   THE FOLLOW PAGE'S OWN ROW OF DOORS, AND THE ROOM BEHIND ONE OF THEM.
   --------------------------------------------------------------------------
   "MOve grid to the top on follow. Clean up all the shit all over it.
    Dumpster fire."                                   — Matt, 25 September.

   The grid is the first thing under the title now. Everything that used to
   stand between them is behind a button on this row, and this row has to stay
   ONE row of SMALL buttons or the grid is pushed straight back down again.
   tests/test-follow-grid-top.js measures it at 1400 wide and fails by name.
   ========================================================================== */
.follow-doors{ display:flex; gap:var(--space-2); flex-wrap:wrap; align-items:flex-start; }

/* The app-wide .btn is 12px/20px padding, which lands at 44px tall — over the
   40px this row is held to. These are the same button, smaller: same border,
   same background, same colours, so nothing about them is a new style. */
.follow-doors .btn{
  padding:var(--space-2) var(--space-3);
  font-size:var(--type-body); line-height:1.25; white-space:nowrap;
}

/* THE ADDING TOOLS. Below the grid, so opening them never moves the tiles. */
.feed-tools{
  margin:var(--space-6) 0 var(--space-5);
  padding:var(--space-5) 0 0;
  border-top:1px solid var(--glass-border);
}
/* The box is hidden with the [hidden] attribute and nothing else may argue
   with it — a door that is shut must be shut. */
.feed-tools[hidden]{ display:none !important; }

/* On a phone the row is allowed to wrap. What it may not do is become three
   rows of large buttons, which is what full-size .btn padding would give. */
@media (max-width: 640px){
  .follow-doors{ gap:6px; }
  .follow-doors .btn{ padding:6px var(--space-2); }
}

/* ==========================================================================
   FOLLOW A AND WATCHLIST A — the people, on both pages.
   --------------------------------------------------------------------------
   Every colour and size here is one of the app's own tokens, so both pages
   follow the themes instead of pretending to. The two screens share nothing
   structurally and share everything visually: a round face, one line of
   ordinary type beside it, and nothing else — no badge, no colour, no score.
   ========================================================================== */

/* ---- FOLLOW A: the sentence above the river ---- */
.feed-today{
  margin:0 0 var(--space-4); padding:var(--space-3) 0;
  border-bottom:1px solid var(--glass-border);
  font-size:var(--type-body); color:var(--text-dim); line-height:1.6;
  display:flex; flex-wrap:wrap; align-items:baseline; gap:8px;
}
.feed-today b{ color:var(--text); font-weight:650; }
.feed-today-tap{
  border:0; background:none; padding:0; margin:0; font:inherit; text-align:left;
  color:var(--text); cursor:pointer; border-bottom:1px solid var(--accent);
}
.feed-today-tap.on{ color:var(--accent); }
.feed-today-small{ font-size:var(--type-label); color:var(--text-faint); }
.feed-today-quiet{ display:block; }
.feed-today-one{ flex-direction:column; align-items:flex-start; gap:4px; }

/* ---- FOLLOW A: the one line under a card ---- */
.feed-life{
  display:flex; align-items:center; gap:10px;
  padding:10px var(--space-3); margin:0;
  border-top:1px solid var(--glass-border);
  cursor:default;
}
.feed-face{
  flex:0 0 auto; width:28px; height:28px; border-radius:50%; overflow:hidden;
  border:1px solid var(--glass-border); background:var(--glass-bg);
  color:var(--text-dim); font-size:var(--type-label); font-weight:650;
  display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0;
}
.feed-face img{ width:100%; height:100%; object-fit:cover; }
.feed-face:hover{ border-color:var(--accent); }
.feed-life-words{ min-width:0; flex:1 1 auto; }
.feed-life-1{ font-size:var(--type-label); color:var(--text-dim); line-height:1.45; }
.feed-life-1 b{ color:var(--text); font-weight:650; }
.feed-life-2{ font-size:var(--type-label); color:var(--text-faint); line-height:1.45; }
.feed-life-act{ flex:0 0 auto; font-size:var(--type-label); }


/* A TEXT ACTION MUST NOT READ AS A DISABLED CONTROL.
   <button> carries this app's filled button chrome and .mini-link resets none
   of it, so every text action on these two screens drew as a grey block. Found
   by looking at the pictures. Scoped to tonight's classes on purpose — the
   same rule would also fix the engine's "All subjects" button, and that one is
   Matt's call rather than mine. */
.feed-life .mini-link,
.feed-today .mini-link{
  background:none; border:0; padding:0; box-shadow:none; min-height:0;
  font:inherit; color:var(--blue); text-decoration:underline; cursor:pointer;
}
.feed-life .mini-link:hover,
.feed-today .mini-link:hover{ color:var(--accent); }

@media (max-width: 640px){
  /* The claim under a card is two lines on a phone and must not be cut. */
  .feed-life{ flex-wrap:wrap; }
  .feed-life-act{ margin-left:38px; }
}

/* BRING BACK ON A PHONE. The shared phone rule gives every .btn a 44px
   min-height (see "GROWN" further down), which turned this small row button
   into a big one beside an 11px name. It keeps its desk size here and gets
   the house's invisible reach instead: the ::after answers a fingertip
   across 50px, centred, while the drawn button does not move a pixel. */
@media (hover: none) and (pointer: coarse), (max-width: 480px){

  /* ---- GROWN. Buttons that were simply too small to press. -------------- */

  /* The shared button style, and the single biggest win in the file: 52 of
     the too-small controls at 430 are a .btn. They carry 12px of padding
     already, so most of them only need a few pixels; the ones that were 25
     and 27 tall needed all of it. A <button> centres its own label, so
     min-height alone is the whole change. */
  .btn{ min-height:44px; }
  /* AND THE ONE PLACE .btn DOES NOT WIN. `#profileContent .btn{ min-height:42px }`
     further up this file carries an id, so it beats the line above wherever it
     applies. Its own comment says where 42 came from: a button that measured 39
     and went red against the 40 that tests/test-mobile.js holds controls to. 40
     was never the number. It is repeated here at id strength so the phone gets
     44 on that page too, and the 42 is left alone everywhere else. */
  #profileContent .btn{ min-height:44px; }

  /* THE MAIL TOOLBAR IS THE ONE ROW WITH NO SPARE WIDTH. Mail, two icons, a
     search box, two filter pills and a chevron already fill 430, so the 9px
     that min-width added to "All" pushed "Unread" off the end - visible in
     the first set of pictures, where it read "Un". Widening the chevron to 44
     took it to "Unre". Height is free in that row and width is not, so the
     whole row keeps its natural widths: a word the user cannot read is worse
     than a control that is 35 wide and 44 tall, and the check names the row
     as owed rather than being quietly satisfied by a clipped toolbar.

     What would retire it is the row wrapping to two lines or scrolling
     sideways. Both move things, so both are Matt's to choose. */
  /* RETIRED 30 SEPTEMBER, on Matt's yes. The rule that sat here held the mail
     toolbar's buttons at their natural 32-35px width, because widening them
     pushed a word off the row. He was shown the full-size row and chose it:
     fitMailToolbar() folds the lowest-priority buttons (Mark everything read,
     then Refresh) into the chevron's menu when the row runs short, so nothing
     is lost, it moves. Every control on the row is now 44 by 44. */

  /* Filter pills and launch-pad chips. .chip is a div, so it needs to be told
     to centre what min-height has just given it room for. */
  .chip{ min-height:44px; min-width:44px; display:flex; align-items:center; justify-content:center; }
  .lp-chip{ min-height:44px; }
  .vt-btn{ min-height:44px; display:flex; align-items:center; justify-content:center; }
  .weather-chip{ min-height:44px; }
  .pw-act{ min-height:44px; }

  /* Rows that are pressed end to end. These were 42 and 43 tall — near
     enough to look right and not near enough to be right. */
  .stg-sec-head{ min-height:44px; }
  .ht-line{ min-height:44px; }
  .st-drop-btn{ min-height:44px; }

  /* Text fields. 16px font was already handled here so iOS does not zoom on
     focus; the box itself was still 33 tall. */
  .field,
  .mail-search-input,
  .cal-search input{ min-height:44px; }

  .stg-back{ min-width:44px; min-height:44px; }
  .mc-nav{ min-width:44px; min-height:44px; }

  /* TWO THAT THE INVISIBLE REACH COULD NOT CARRY, AND SO ARE GROWN.

     The sender's initial in a mail row, and the actions button on an agenda
     item. Both were given the ::after treatment first and both came back
     short at 390 — 50 x 29 and 50 x 39 — because the reach is clipped by the
     row it lives in, and a row is a short box with overflow:hidden. A reach
     that only works on the tall rows is not a fix, it is a fix that is true
     on average, so these two grow instead.

     What that costs: the initial goes from a 38px circle to a 44px one, and
     the actions button's invisible square goes from 28 to 44, which reaches
     a little further across the item it belongs to. Nothing moves; both are
     still the same drawing in the same place, larger. */
  /* TRIED GROWN, PUT BACK. These two were grown while the check was still
     measuring them where the page happened to be standing, which put some of
     them under the fixed bottom bar and reported the reach as clipped. Once
     the check centred every control before asking, the invisible reach was
     enough for both, so the drawing goes back to what it was: the sender's
     initial is a 38px circle again and the agenda actions button is 28px
     again, each with a 50px target under it. */

  

  /* THE MONTH GRID. The cells were 34 x 34 because max-height caps them and
     aspect-ratio makes the width follow. The seven columns are 1fr of a rail
     that is about 398px wide at 430, so each column already has room for 56 —
     the cells were small because they were told to be, not because they had
     to be. Raising the cap to 44 spends 10px of a column that had 22 spare.
     An invisible reach is not available here: the cells are 1px apart, so a
     44-wide target on a 34-wide cell would overlap both its neighbours and
     the date a finger got would be a lottery. */
  /* AND min-width:0, WHICH THE LINE ABOVE MADE NECESSARY AND I ALMOST
     SHIPPED WITHOUT.

     Letting the COLUMN shrink (minmax(0,1fr) on .mc-grid) was not enough on
     its own. min-height:44px makes the cell height definite, aspect-ratio:1
     then gives it a preferred width of 44 to match, and a grid item defaults
     to min-width:auto - which, with an aspect ratio in play, resolves to
     that same 44. So the columns duly shrank to 37.4 and the cells stayed
     44, and I measured the result at 115 percent before believing it:

         cells 50.6 points wide on a 44 point pitch - overlapping their
         neighbours by 6.6 points each

     Which is word for word what the comment above this one warns about: a
     target wider than its cell means the date a finger gets is a lottery.
     Nothing ran off the screen any more and the layout looked right in the
     picture; it was only wrong in the numbers. Trading 11 points of overhang
     for seven overlapping buttons would have been a worse app and a passing
     check.

     min-width:0 lets the cell be as wide as its column and no wider. It does
     nothing at all on the desktop or at 100 percent, where the column is
     already wider than 44 - it only bites when there genuinely is not room.

     MEASURED AFTER, real WebKit at 393 x 852, text size 115:

         cells 43.1 x 50.6 real screen points, no overlap, grid inside the
         phone with room to spare

     43.1 is nine tenths of a point under Apple 44 on the narrow axis, and
     that is worth saying out loud rather than rounding away. Before this it
     was 50.6 wide and the whole Saturday column was off the side of the
     phone, which is not a target at all. If Matt wants it over 44 on both
     axes the room exists - eight points of side padding on the left panel -
     but that is his call about how the page looks, not mine. */
  .mc-cell{ max-height:44px; min-height:44px; min-width:0; }

  /* ---- WIDENED. The drawing stays; the target grows underneath it. ------ */

  /* THE HOSTS. Absolute positioning needs something to be absolute inside.
     .lth-line is already relative and is left alone deliberately: it sits in
     a stack with .lth-rail and the bead that travels along it, and giving it
     a z-index here would paint the line over the bead. It does not need one —
     the rail carries pointer-events:none, so the page skips it when asked
     who owns a pixel, and the reach is clear. */
  .lth-rungs,
  .avatar.is-jump,
  .agenda-act,
  .modal-x,
  .mini-link,
  .ht-verb{ position:relative; }

  /* z-index ONLY where a later sibling would paint over the reach and take
     the press. The avatar sits before the subject and the sender in the mail
     row, so without this its extra 3px to the right lands on the subject. */
  .avatar.is-jump{ z-index:1; }

  /* THE REACH ITSELF. Centred on the control, at least as big as the control,
     and never smaller than a fingertip. 50 rather than 44 because a reach
     is measured back through whatever is drawn around it, and a few pixels go
     missing at fractional edges and under a parent's own border. At 46 the
     "Pick teams" verb on Home measured back at 43 - one pixel short, which is
     a failure and should be. 50 leaves the margin the measuring needs. */
  .lth-line::after,
  .lth-rungs::after,
  .avatar.is-jump::after,
  .agenda-act::after,
  .modal-x::after,
  .mini-link::after,
  .ht-verb::after{
    content:''; position:absolute; left:50%; top:50%;
    width:100%; height:100%; min-width:50px; min-height:50px;
    transform:translate(-50%,-50%); }

  /* THE FIRST ROW'S ⋯ SITS 5px FROM THE TOP OF THE AGENDA SCROLLER, which
     clips (overflow-y:auto clips both axes), so its reach above the button was
     cut off and a finger there landed on the panel. The scroller's box goes up
     by the reach and its padding comes down by the same amount: every row
     stays exactly where it was, the clip edge moves out of the way. */
  .agenda-scroll{ margin-top:-8px; padding-top:8px; }

  /* THE TOP BAND SHOULD ONLY CATCH WHAT IT DRAWS.

     .lth is pointer-events:none with `.lth > *` set back to auto, which makes
     the whole of .lth-top catch presses - the two controls, the 10px gap
     between them, and the empty corner around them. That was a 122 x 34
     dead patch over the top right of every screen, and the line's new height
     below would make it 122 x 48. Handing the band back to its children
     leaves only the two controls catching anything, which is what was meant.
     It does not move or resize a single drawn thing. */
  .lth-top{ pointer-events:none; }
  .lth-top > *{ pointer-events:auto; }

  /* ASK LUCID, AND WHY IT IS THE ONE THAT NEEDED A DIFFERENT ANSWER.

     The invisible reach above does not work on this control. .lth-line
     carries a mask - the left-to-right falloff that makes the wire fade out
     at its far end - and a mask clips what it is applied to, pseudo-elements
     and hit area included. So the ::after was drawn and then masked away, and
     the line measured back at 2px tall with the reach in place. The number
     said so, which is the only reason this was caught rather than shipped.

     What works is padding: the border box is what a finger meets, and
     background-origin with background-clip keep the gradient painting in the
     1.5px content box in the middle of it. The visible wire does not move -
     3px of margin plus 12px of padding still puts it 15px down, exactly where
     it has always been - and the box around it is now 45px tall. The padding
     is lopsided on purpose: .lth sits at top:0, so a centred box would put a
     third of the target above the top of the screen where no thumb reaches.

     .lth-top grows from 34px tall to about 48px as a result, which is 14 more
     pixels of the top right corner that answer to the top band rather than to
     the page under it. That is the cost and it belongs in the report. */
  .lth-line{
    margin-top:calc(var(--th-mt, 15px) - 12px);
    padding:12px 0 32px;
    background-origin:content-box; background-clip:content-box; }
  /* The breath is absolute against the padding box, so it has to be told
     where the line now is or it would breathe over all 45 pixels. */
  .lth-line::before{ inset:12px 0 32px; }

  /* ASK LUCID. It is a 1.5px line — `.lth-line`, with role="button" and an
     aria-label, on all twelve screens — and it grows to 2px on hover, which a
     phone does not have. So the headline feature of the app has been a
     hairline you were meant to hit with a fingertip. On a desk, with a mouse,
     it is a lovely piece of drawing and it is staying exactly as it is. What
     changes is that there is now a 46px band centred on it that answers to
     the same press. */

}

/* ==========================================================================
   THE CARD.                                  (Worker D, 26 September 2026)
   --------------------------------------------------------------------------
   Worker HO built the slot, the three words and the off switch on 25 September
   and said in as many words that the card itself had not been designed. Worker
   I1 filled it this afternoon with the first real suggestion. This is the
   drawing.

   WHERE IT SITS AND WHAT THAT COSTS IT. Top of Home, under the hero, in the
   page — not a popup, not a notification, not a toast. It cannot be dismissed
   by appearing somewhere he is not looking, which means it has to be quiet
   enough to live at the top of a screen he opens every morning. Clippy, Google
   Now, Cortana and Microsoft's Briefing email all died of guessing or of
   speaking too often. Not one of them died of being too quiet.

   SO: ONE CARD, ONE SENTENCE, ONE PRESS. No badge, no count, no accent bar, no
   icon, no colour it has not earned. The sentence carries the card and
   everything else recedes from it.
   ========================================================================== */
.suggest-slot {
  display: flex; flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  padding: var(--space-4);
  border-radius: var(--radius-card);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  max-width: var(--prose-measure);
}

/* THE SENTENCE. --type-lead, the same step the hero's own lead line uses, so
   the card reads as part of Home rather than as a notice pasted onto it. It is
   the one thing on the card at full --text. */
.suggest-t {
  font-size: var(--type-lead); font-weight: 600; color: var(--text);
  line-height: 1.4;
}

/* THE REASON, UNDERNEATH, IN SMALLER TYPE. --text-dim rather than an opacity,
   because opacity on a glass card dims the glass with the words.

   IT IS LONG AND IT IS MEANT TO BE. I1's sentence names the trigger, the
   number of days, where it was read and the one thing Lucid cannot see (it
   holds the first 400 characters of a Microsoft message, so this is a promise
   it found rather than the only one there is). That is the difference between a
   card that can be argued with and a card that has to be believed, so the type
   is sized to be READ — 1.65 leading at the body step — instead of trimmed to
   look tidy. */
.suggest-w {
  font-size: var(--type-body); color: var(--text-dim);
  line-height: 1.65; margin: 0;
}

/* THE PRESS. The house .btn, unmodified — there is no reason for the one button
   on Home to be a shape that exists nowhere else. The note beside it is the
   promise the button keeps, and it is on the card rather than in a tooltip
   because a tooltip is not a promise. */
.suggest-do {
  display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap; margin-top: var(--space-2);
}
.suggest-go { flex: none; }
.suggest-do-note {
  font-size: var(--type-label); color: var(--text-faint);
  line-height: 1.5; flex: 1 1 260px; min-width: 0;
}

/* ==========================================================================
   AND THE READ THAT FAILED, WHICH IS THE WHOLE REASON THIS BLOCK EXISTS.
   --------------------------------------------------------------------------
   A quiet day draws NOTHING — #lucidSuggest:empty is display:none, and quiet
   and nothing-to-say return the same empty string byte for byte. So the state
   this has to be told apart from is not another card; it is an absence. Until
   today a failed read drew ONE dim sentence at opacity .85 where an ordinary
   card's reason line sits at .72 — thirteen hundredths of an alpha, which is
   nothing at all to somebody glancing at Home on his way past.

   THE MARK IS THE HOUSE'S EXISTING ONE, NOT A NEW ONE: a 2px left rule in
   --orange on the same glass. .feed-stale, .ht-unread, .ht-stale and .one-broke
   in the Rooms column all already wear it. One meaning, one mark — giving this
   meaning a second mark is how somebody learns to read neither.

   AND THE SUGGESTION ITSELF WEARS NO RULE. That is the decision this block
   turns on. An amber edge on both cards would make the mark mean "a card" and
   nothing else, which is exactly the rule 211 collision this state is here to
   prevent. The amber edge on Home means one thing: Lucid tried to look and
   could not.

   Not a red. The scale has no --red, and this is a correction rather than an
   emergency — he is not in danger, he is being told that a silence he would
   otherwise read as a calm day is not one.
   ========================================================================== */
.suggest-slot[data-state="failed"] {
  border-left: 2px solid var(--orange);
  border-top-left-radius: 0; border-bottom-left-radius: 0;
  padding-left: calc(var(--space-4) - 1px);
}
/* The reason line on a failed read is the sentence that says WHICH silence
   this is, so it is not the dim afterthought it is on a suggestion. */
.suggest-slot[data-state="failed"] .suggest-w {
  color: var(--text); opacity: .80; font-weight: 500;
}

/* THE PHONE, AND WHY THIS BLOCK IS HERE RATHER THAN IN THE @media NEAR
   .home-band WHERE IT STARTED.
   --------------------------------------------------------------------------
   It was up there first, and every rule in it lost. A media query carries no
   extra specificity — it is the same weight as the rule it is trying to beat,
   so the LATER one in the file wins, and that block sits about a hundred lines
   ABOVE the card. Nothing about the phone layout applied at all.

   What that cost, exactly, because it is not a thing you can see by reading:
   .suggest-do-note carries flex: 1 1 260px. In a ROW that is a 260-pixel
   minimum WIDTH and it is right. The phone rule turns .suggest-do into a
   COLUMN, and in a column the flex-basis is the HEIGHT — so "Nothing sends.",
   two words, was laid out 260 pixels tall. The card measured 670px on a 390px
   screen with about 400 of them empty, and the three words sat below a chasm.

   I did not find that in the stylesheet. I found it by looking at the picture,
   and then by measuring every child of the card rather than guessing which one
   was tall. */
@media (max-width:760px){
  .suggest-slot{ padding:var(--space-3); }
  .suggest-slot[data-state="failed"]{ padding-left:calc(var(--space-3) - 1px); }
  /* THE NOTE GOES UNDER THE BUTTON. At 354px a 260px-wide note beside a
     button is two words a line. */
  .suggest-do{ flex-direction:column; align-items:flex-start; gap:var(--space-2); }
  /* flex-basis said out loud as well as the shorthand, because the basis is the
     thing that was wrong and a shorthand is easy to delete by accident. */
  .suggest-do-note{ flex:0 0 auto; flex-basis:auto; }
  .suggest-go{ align-self:stretch; }
}

/* SMALL TYPE, UNDER THE SUGGESTION. Three words and two separators, and they
   are buttons rather than links because they do something rather than go
   somewhere. */
.suggest-ctls { display: flex; align-items: center; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.suggest-sep { font-size: 12px; opacity: .35; }
.suggest-ctl {
  background: none; border: 0; padding: 4px 2px; margin: 0;
  font: inherit; font-size: 12px; color: inherit; opacity: .6;
  cursor: pointer; text-decoration: none;
}
.suggest-ctl:hover, .suggest-ctl:focus-visible { opacity: 1; text-decoration: underline; }

/* THE REVISIT LIST. Plain rows, because a list of things you turned off is a
   list to read down and not a feature to be sold back to you. */
.suggest-never-row {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 6px 0; border-bottom: 1px solid var(--line, rgba(128,128,128,.18));
}
.suggest-never-row:last-child { border-bottom: 0; }
.suggest-never-k { font-size: 13px; font-weight: 600; }
.suggest-never-d { font-size: 12px; opacity: .6; }
.suggest-never-row .suggest-ctl { margin-left: auto; }

/* WHAT IT SAYS ABOUT WHEN IT LIFTS.  (Worker HO2, 26 September.)
   One line and one button, in Settings above the revisit list. Borrows the
   never-row's shape on purpose: the two blocks answer the same question from
   two ends and they should look like they belong to each other. */
.suggest-quiet {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 8px 0 10px; border-bottom: 1px solid var(--hair, rgba(0,0,0,.08));
  margin-bottom: 6px;
}
.suggest-quiet .suggest-w { flex: 1 1 auto; min-width: 12em; }
.suggest-quiet .suggest-ctl { margin-left: auto; }
/* A FAILED READ IS NOT A QUIET DAY, and it does not whisper either. The
   sentence that says Lucid could not tell is drawn at full strength, because
   the thing it is warning about is Lucid talking after being told not to. */
.suggest-quiet[data-state="failed"] .suggest-w { opacity: .9; }
/* TX-FEED-STALE — what a feed that has stopped looks like.
   --orange rather than a red nobody has: the scale has no --red and this is not
   an emergency, it is a correction. Matt is not in danger; he is being told
   that a number he has been reading is older than it looks. */
.feed-stale-strip{ display:grid; gap:var(--space-2); margin-bottom:var(--space-3); }
.feed-stale{ padding:var(--space-3); border-radius:var(--radius-card);
  border-left:2px solid var(--orange); background:var(--glass-bg); }
.feed-stale-head{ font-weight:650; margin:0; }
.feed-stale-body{ color:var(--text-dim); margin:var(--space-1) 0 0 0; }
.feed-stale-fix{ color:var(--text-dim); margin:var(--space-2) 0 0 0; font-style:italic; }
/* THE ONE LINE THE FOUR CARDS NOW SIT BEHIND. 3 October. Same 2px orange left
   rule and same glass as .feed-stale above, on purpose — it is the same kind of
   thing, said shorter. Opening it shows exactly what was there before. */
.feed-stale-toggle{ display:flex; align-items:center; gap:var(--space-2); width:100%;
  text-align:left; cursor:pointer; font:inherit; color:var(--text);
  padding:var(--space-2) var(--space-3); border:0; border-left:2px solid var(--orange);
  border-radius:var(--radius-card); background:var(--glass-bg); }
.feed-stale-mark{ width:6px; height:6px; border-radius:50%; background:var(--orange); flex:none; }
.feed-stale-count{ font-weight:650; }
.feed-stale-more{ margin-left:auto; color:var(--text-dim); font-size:0.86em; }
.feed-stale-toggle:hover .feed-stale-more{ color:var(--text); }
.feed-stale-toggle:focus-visible{ outline:2px solid var(--orange); outline-offset:2px; }
.feed-stale-list{ display:grid; gap:var(--space-2); margin-top:var(--space-2); }
.feed-stale-list[hidden]{ display:none; }

/* The same thing said inside a contact card, where the false sentence used
   to be. */
.know-stale{ color:var(--text-dim); font-style:italic; }
/* ==========================================================================
   THE DEAD-END PICKUP — the offer that lives in the empty result.
   --------------------------------------------------------------------------
   Raycast's Fallback Commands sit IN the list that came back empty, and the
   styling has one job: to look like that empty space having more to say, not
   like a notice that arrived in it. So no card, no border, no tinted panel —
   the sentence is .page-sub like every other empty-state sentence on the page,
   and the only thing that marks the block out is the room around the button.

   The colour and the exact spacing are Matt's call, not mine; this is the
   plain version that is provably not a banner. See app.js, deadEndHtml().
   ========================================================================== */
.dead-end{ display:flex; flex-direction:column; align-items:flex-start; gap:12px;
           padding:4px 0 8px; max-width:var(--prose-measure); }
.dead-end .de-line{ margin:0; }
/* The rows the offer opens. Full width of the list rather than the measure —
   these are results, and a result list is not prose. */
.dead-end .de-rows{ align-self:stretch; max-width:none; display:flex;
                    flex-direction:column; gap:2px; margin-top:4px; }
.dead-end .de-rows .omni-row{ cursor:pointer; }

/* ONE NEXT STEP AND A WAY TO DECLINE IT, SIDE BY SIDE. The ladder shows exactly
   one yes at a time — never a menu — so this row never holds more than two
   buttons, and the No is plain because declining is not the thing being
   recommended. */
.dead-end .de-offer{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
/* THE MUTE LIST PAGE.  (Worker MU, 25 September.)

   Built on the revisit list's own rows rather than on a second set of styles,
   because they are the same object in two places and two stylesheets is how
   they end up looking like two features. Two things are added: the group
   headings, and the three or four words at the front of every row saying
   WHICH of the four kinds of silence it is. */
.mute-group + .mute-group { margin-top: 18px; }
.mute-group .section-label { margin-bottom: 4px; }
/* ONE COLUMN, NOT A RAGGED EDGE. The width is the widest of the five labels
   as a browser actually draws them — measured at 11px uppercase with this
   letter-spacing, not estimated — so every sentence on the page starts at the
   same place and the list can be read straight down. */
.mute-what {
  font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
  opacity: .55; min-width: 176px;
}
.mute-ctls { margin-top: 10px; }

/* THE FAILED READ IS NOT DRAWN LIKE THE EMPTY ONE, AND THIS IS THE HALF OF
   THAT A STYLESHEET CAN DO. The markup already differs — different
   data-state, different sentence, and a Try again button that the empty state
   does not have — and tests/test-the-mute-list.js asserts all three. This
   line only makes sure the difference is visible: the failed read carries a
   rule down its left-hand side, so the two are not one glance apart. */
.mute-list[data-state="failed"] {
  border-left: 3px solid var(--orange, #c77d2e);
  padding-left: 12px;
}
.mute-list[data-state="failed"] .suggest-w { opacity: .9; }

/* ==========================================================================
   THE ONE SURFACE — VITRINE
   ==========================================================================
 * Matt, 25 September: "I love the concept of the one surface. One surface to
 * get whatever you need from Lucid is fucking a great idea." And, of the six
 * directions he was shown: "Vitrine S3 is bad to the bone. I love it."
 *
 * So there is one floating panel now and it is both doors. Name a room and it
 * launches; ask a question and the same box answers. It is NOT attached to the
 * button that opens it — his words, "the panel is not attached to the button
 * and floats centered on the screen" — because the button will move (top bar,
 * phone thumb-bar) and the panel should not have to be redesigned when it does.
 *
 * ONE LIGHT SOURCE, UPPER-LEFT, AND IT IS NEVER IMPROVISED PER ELEMENT.
 * Everything raised takes --one-lift: a 1px rim along its top, a dark fall
 * under it, a short contact shadow and a soft drop. Everything recessed takes
 * --one-well, which is that inverted. If a new part of this panel needs a
 * shadow it takes one of these two; a third would mean a second light.
 *
 * THE GLOW SITS BEHIND THE GLASS. Each case's colour is a blurred ellipse
 * positioned ABOVE the case and clipped by it — not a border colour and not a
 * tint on the front. That is the whole difference between a lit thing and a
 * coloured-in thing, and it is why the cases read as objects in a cabinet.
 *
 * THE GROUND IS OPAQUE AND THAT IS THE POINT. See "FLOATING PANELS ARE NOT
 * WINDOWS" below.
 */
  .one-backdrop{
    position:fixed; inset:0; z-index:90; display:none;
    align-items:center; justify-content:center; padding:24px;
    background:rgba(2,3,6,.72);
    -webkit-backdrop-filter:blur(5px) saturate(.72);
    backdrop-filter:blur(5px) saturate(.72);
    animation:oneScrim 220ms var(--ease-out) both;
  }
  .one-backdrop.open{ display:flex; }
  @keyframes oneScrim{ from{ opacity:0; } to{ opacity:1; } }
  :root[data-mode="light"] .one-backdrop{ background:rgba(20,22,32,.46); }

  .one-surface{
    --one-brass:#C79A54; --one-brass-hi:#E8CB92;
    --one-lift:
      inset 0 1px 0 rgba(255,255,255,.10),
      inset 0 -1px 0 rgba(0,0,0,.45),
      0 1px 1px rgba(0,0,0,.45),
      0 10px 22px -10px rgba(0,0,0,.75);
    --one-well:
      inset 0 2px 5px rgba(0,0,0,.55),
      inset 0 -1px 0 rgba(255,255,255,.055);
    position:relative; display:flex; flex-direction:column;
    width:min(760px, 100%); height:min(660px, calc(100vh - 48px));
    border-radius:22px; overflow:hidden;
    border:1px solid var(--glass-border-hi);
    /* OPAQUE. --bg-base is a hex in every one of the app's themes, so this
       stack has no alpha left in it at the bottom. The radial above it is the
       one light, coming in over the upper-left shoulder. */
    background:
      radial-gradient(120% 90% at 22% -20%, rgba(255,255,255,.07), transparent 55%),
      linear-gradient(var(--glass-bg), var(--glass-bg)),
      var(--bg-base);
    box-shadow:
      0 1px 2px rgba(0,0,0,.5),
      0 26px 50px -18px rgba(0,0,0,.75),
      0 70px 140px -40px rgba(0,0,0,.92),
      inset 0 1px 0 rgba(255,255,255,.10);
    animation:oneRise 340ms var(--ease-spring) both;
  }
  @keyframes oneRise{
    from{ opacity:0; transform:translateY(14px) scale(.975); }
    to{ opacity:1; transform:none; }
  }
  @media (prefers-reduced-motion:reduce){
    .one-surface, .one-backdrop{ animation:none; }
  }

  /* FILM GRAIN. One 140px tile of turbulence at 16%, in overlay. Without it a
     panel this large reads as a sheet of flat plastic; with it, it reads as a
     surface with a material. It is deliberately below the threshold at which
     anyone would name it. */
  .one-grain{
    position:absolute; inset:0; pointer-events:none; z-index:0; border-radius:inherit;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='m'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='140' height='140' filter='url(%23m)' opacity='.5'/></svg>");
    opacity:.16; mix-blend-mode:overlay;
  }
  :root[data-mode="light"] .one-grain{ opacity:.07; }

  .one-crown{ display:flex; align-items:center; gap:11px; padding:15px 18px 12px;
    position:relative; z-index:2; flex:none; }
  /* THE GLOBE IS GONE AND LUCID IS HERE INSTEAD. (26 September.)

     What was here: a 19px sphere drawn out of a conic gradient and a white
     specular highlight. It looked like a planet, it never changed, and it was
     the only thing at the top of the surface -- so the panel Matt spends his
     time in showed a decoration where the one honest status light in the whole
     app should have been.

     What is here now is a HOST for the mark, not a second drawing of it.
     lucid-mark.js finds any element with data-lucid-mark and appends its own
     canvas at width:100% height:100%, which is why this box needs a real size:
     a <canvas> with no size is 300x150, and that exact bug drew the mark
     across two thirds of a demo page on 25 September.

     THE WIDTH IS THE WHOLE DRAWING, WHICH I GOT WRONG FIRST TIME.

     Matt, 26 September, looking at the shipped mark beside the demo he chose
     from: "so not really what I picked. You gave me short burst idle, and I
     wanted the opposite."

     He is right, and the drawing was never wrong. Idle A is "Long drag" and
     lucid-mark.js carries its numbers exactly as the demo has them -- 9s lap,
     0.38 trail, 0.25 rest. But the trail is 38% OF THE HOST, so the host
     decides whether 38% is a drag or a blip. The demo draws it in a card
     about 480px wide: a 182px tail crossing at 53px a second. I gave it 78px:
     a 30px tail crawling at 8.7px a second and then vanishing for 2.25s.
     Same proportions, and a completely different thing to look at.

     A tail measured as a fraction of its box is only honest if the box is the
     size the fraction was chosen for. 200px is not arbitrary either -- it is
     the width at which the 38% tail comes back to 76px, close enough to read
     as the drag he picked, and it still fits the crown beside the word LUCID
     and the three actions with room over.

     Not 19 x 19, and not square, because the mark is a WIRE: a square host
     gives it nineteen pixels to pace along and every state reads the same.
     The ink does NOT scale with the width -- same hairline weight as the
     corner, which is the third thing Matt rejected by name. */
  .one-mark{ width:200px; height:20px; flex:none; position:relative;
    border-radius:0; background:none; box-shadow:none; }
  .one-mark canvas{ display:block; width:100%; height:100%; }
  .one-nm{ font-family:var(--font-display); font-weight:600; font-size:14.5px;
    letter-spacing:.06em; text-transform:uppercase; color:var(--text); }
  .one-acts{ margin-left:auto; display:flex; gap:4px; }
  .one-act{ font-family:var(--mono, ui-monospace, monospace); font-size:10px;
    letter-spacing:.1em; text-transform:uppercase; color:var(--text-dim);
    background:none; border:1px solid transparent; border-radius:7px;
    padding:4px 8px; cursor:pointer; }
  .one-act:hover{ color:var(--text); border-color:var(--glass-border); }
  .one-act:focus-visible{ outline:2px solid var(--one-brass); outline-offset:2px; }
  .one-kb{ font-family:var(--mono, ui-monospace, monospace); font-size:9.5px;
    letter-spacing:.12em; color:var(--text-faint); border:1px solid var(--glass-border);
    border-radius:6px; padding:3px 7px; flex:none; }
  .one-x{ background:none; border:0; color:var(--text-faint); font-size:20px;
    line-height:1; cursor:pointer; padding:0 2px; flex:none; }
  .one-x:hover{ color:var(--text); }
  .one-x:focus-visible{ outline:2px solid var(--one-brass); outline-offset:2px; }

  /* THE BRASS HAIRLINE — the only metal in the object. One across, under the
     crown, and one down, between asking and going. */
  .one-rule{ height:1px; flex:none; position:relative; z-index:2;
    background:linear-gradient(90deg, transparent,
      color-mix(in srgb, var(--one-brass) 45%, transparent) 18%,
      color-mix(in srgb, var(--one-brass-hi) 70%, transparent) 50%,
      color-mix(in srgb, var(--one-brass) 45%, transparent) 82%, transparent); }

  /* THE FIELD IS RECESSED, which is the inverse of everything else here, and
     that is how you know it is the thing you type into. The row inside it is
     the app's OWN #chatInput and its own mic, moved here — not a copy. */
  .one-field{ margin:13px 18px 0; padding:8px 12px; flex:none;
    border-radius:13px; border:1px solid var(--glass-border);
    background:linear-gradient(180deg, rgba(0,0,0,.34), rgba(0,0,0,.18));
    box-shadow:var(--one-well); position:relative; z-index:2; }
  :root[data-mode="light"] .one-field{
    background:linear-gradient(180deg, rgba(0,0,0,.07), rgba(0,0,0,.03)); }
  .one-field:focus-within{ border-color:color-mix(in srgb, var(--one-brass) 55%, transparent);
    box-shadow:var(--one-well), 0 0 0 3px color-mix(in srgb, var(--one-brass) 14%, transparent); }
  .one-field .chat-input-row{ border-top:0; padding:0; gap:10px; align-items:center; }
  .one-field .chat-input{ font-size:15px; }
  .one-field .ask-allow{ margin-bottom:6px; }

  .one-duo{ display:grid; grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr);
    min-height:0; flex:1 1 auto; position:relative; z-index:2; }
  .one-duo > div{ padding:18px; min-width:0; min-height:0; display:flex;
    flex-direction:column; }
  .one-ask{ position:relative; }
  .one-ask::after{ content:""; position:absolute; top:14px; bottom:14px; right:0; width:1px;
    background:linear-gradient(180deg, transparent,
      color-mix(in srgb, var(--one-brass) 42%, transparent) 20%,
      color-mix(in srgb, var(--one-brass-hi) 58%, transparent) 50%,
      color-mix(in srgb, var(--one-brass) 42%, transparent) 80%, transparent); }

  .one-h{ font-family:var(--mono, ui-monospace, monospace); font-size:9.5px;
    letter-spacing:.2em; text-transform:uppercase; color:var(--text-faint);
    margin:0 0 12px; font-weight:500; flex:none; }
  .one-prompt{ font-family:var(--font-display); font-weight:600; font-size:27px;
    line-height:1.14; letter-spacing:-.02em; margin:0 0 14px; text-wrap:balance;
    color:var(--text); flex:none; }
  /* Outfit has no italic (14 Oct, Matt's type): the accent keeps its brass and
     is set upright at 500 rather than slanted by the browser. */
  .one-prompt i{ font-style:normal; font-weight:500; color:var(--one-brass-hi); }
  :root[data-mode="light"] .one-prompt i{ color:var(--one-brass); }
  .one-lines{ display:flex; flex-direction:column; gap:7px; }
  .one-line{ text-align:left; cursor:pointer; font-family:inherit; font-size:13.5px;
    color:var(--text-dim); border:1px solid var(--glass-border); border-radius:11px;
    padding:10px 13px;
    background:linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.012));
    box-shadow:var(--one-lift);
    transition:color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out),
               transform var(--dur-base) var(--ease-out); }
  .one-line:hover{ color:var(--text); transform:translateY(-1px);
    border-color:color-mix(in srgb, var(--one-brass) 42%, transparent); }
  .one-line:focus-visible{ outline:2px solid var(--one-brass); outline-offset:2px; }

  .one-go{ overflow:hidden; }
  .one-rooms{ overflow-y:auto; min-height:0; flex:1 1 auto; margin-right:-6px; padding-right:6px; }
  .one-rooms::-webkit-scrollbar{ width:5px; }
  .one-rooms::-webkit-scrollbar-thumb{ background:rgba(255,255,255,.12); border-radius:3px; }

  .one-lbl{ display:flex; align-items:center; gap:9px; margin:0 0 9px; }
  .one-lbl:not(:first-child){ margin-top:15px; }
  .one-lbl i{ width:16px; height:2px; border-radius:2px; background:var(--hue);
    box-shadow:0 0 9px var(--hue); flex:none; }
  .one-lbl b{ font-family:var(--mono, ui-monospace, monospace); font-size:9.5px;
    letter-spacing:.2em; text-transform:uppercase; color:var(--text-dim); font-weight:500; }
  .one-lbl s{ text-decoration:none; font-family:var(--mono, ui-monospace, monospace);
    font-size:9.5px; color:var(--text-faint); margin-left:auto; }

  .one-cases{ display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:9px; }
  .one-case{
    position:relative; cursor:pointer; border-radius:13px; padding:13px 10px 11px;
    text-align:center; border:1px solid var(--glass-border);
    background:linear-gradient(170deg, rgba(255,255,255,.09), rgba(255,255,255,.02) 46%, rgba(0,0,0,.22));
    box-shadow:var(--one-lift); overflow:hidden;
    color:var(--text-dim); font-family:inherit; font-size:12.5px;
    transition:transform var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out),
               color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out); }
  /* THE GLOW, ABOVE AND BEHIND THE GLASS. Positioned off the top of the case
     and clipped by it, so what reaches the front is spill, not paint. */
  .one-case::before{ content:""; position:absolute; left:50%; top:-24px; width:66px; height:46px;
    transform:translateX(-50%); border-radius:50%; background:var(--hue);
    filter:blur(18px); opacity:.5; transition:opacity var(--dur-base) var(--ease-out); }
  /* AND THE LINE OF ITS AREA COLOUR ALONG THE FLOOR. */
  .one-case::after{ content:""; position:absolute; left:10%; right:10%; bottom:0; height:1px;
    background:linear-gradient(90deg, transparent, var(--hue), transparent); opacity:.55; }
  .one-case b{ position:relative; display:block; font-weight:500;
    text-shadow:0 1px 3px rgba(0,0,0,.7); }
  .one-case u{ position:relative; display:block; text-decoration:none; margin-top:3px;
    font-family:var(--mono, ui-monospace, monospace); font-size:9px; letter-spacing:.08em;
    color:var(--text-faint); }
  .one-case:hover{ transform:translateY(-3px); color:var(--text); border-color:var(--hue);
    box-shadow:var(--one-lift), 0 0 34px -10px var(--hue); }
  .one-case:hover::before{ opacity:.85; }
  .one-case:focus-visible{ outline:2px solid var(--one-brass); outline-offset:2px; }
  :root[data-mode="light"] .one-case{
    background:linear-gradient(170deg, rgba(255,255,255,.92), rgba(255,255,255,.5) 46%, rgba(0,0,0,.05));
    color:var(--text); }
  :root[data-mode="light"] .one-case b{ text-shadow:none; }
  :root[data-mode="light"] .one-case::before{ opacity:.30; }

  .one-note{ font-family:var(--mono, ui-monospace, monospace); font-size:10px;
    color:var(--text-faint); margin:10px 2px 0; letter-spacing:.02em; line-height:1.5; }
  /* HOUSE RULE 211 — A FAILED READ MUST NOT LOOK LIKE AN EMPTY ONE.
     The first is a sentence saying the read failed; the second is a quieter
     one saying there is nothing by that name. They cannot be confused. */
  .one-broke{ font-size:13px; line-height:1.5; margin:6px 2px; color:var(--text);
    border-left:2px solid var(--orange, #E0A33E); padding:2px 0 2px 10px; }
  /* TWO LINES, ONE MARK. The amber rule above is the house's mark for "Lucid
     tried to look and could not" and it is already on this element, so the
     second line gets NO mark of its own -- it is weighted, not coloured. Giving
     one meaning a second mark is how somebody learns to read neither. This is
     the same treatment .suggest-slot[data-state="failed"] .suggest-w gets on
     Home, for the same sentence doing the same job. */
  .one-broke-t{ display:block; }
  .one-broke-w{ display:block; margin-top:4px; font-weight:500; opacity:.80; }
  .one-none{ font-size:13px; line-height:1.5; margin:6px 2px; color:var(--text-faint); }

  /* THE ANSWER IS THE APP'S OWN ASK WINDOW, MOVED IN — not a second one.
     #chatCard keeps its id, its thread, its history drawers and its cost line;
     it loses only the chrome it no longer needs, because the surface around it
     is now the window. */
  .one-go .chat-card{ position:static; inset:auto; z-index:auto; width:auto; height:auto;
    max-width:none; max-height:none; border:0; border-top:0; border-radius:0;
    background:none; box-shadow:none; -webkit-backdrop-filter:none; backdrop-filter:none;
    animation:none; flex:1 1 auto; min-height:0; overflow:hidden; }
  .one-go .chat-card .chat-header{ display:none; }
  .one-go .chat-thread{ padding:0 2px 2px 0; }
  .one-go:not(.answering) .chat-card.open{ display:none; }
  .one-go.answering .one-rooms{ display:none; }

  /* AT PHONE WIDTH IT IS A SHEET, AND THE MICROPHONE IS NOT AN AFTERTHOUGHT.
     "Talking and listening will be heavily focused on phone."          (Matt.)
     So the sheet comes up from the bottom where the thumb is, the field is the
     first thing under the crown, and the mic is a 40px target rather than the
     31px it can afford to be on a desk. */
  @media (max-width:700px){
    .one-backdrop{ align-items:flex-end; padding:0; }
    .one-surface{ width:100%; height:auto; max-height:92vh; border-radius:20px 20px 0 0;
      animation:oneSheet 300ms var(--ease-out) both; }
    @keyframes oneSheet{ from{ transform:translateY(26px); opacity:0; } to{ transform:none; opacity:1; } }
    .one-crown{ padding:13px 15px 10px; }
    .one-kb{ display:none; }
    .one-field{ margin:11px 15px 0; padding:9px 12px; }
    .one-field .chat-mic, .one-field .chat-voice, .one-field .chat-send{
      width:40px; height:40px; min-width:40px; flex:none; }
    .one-duo{ grid-template-columns:1fr; overflow-y:auto; }
    .one-duo > div{ padding:15px; }
    .one-ask::after{ top:auto; left:15px; right:15px; bottom:0; width:auto; height:1px;
      background:linear-gradient(90deg, transparent,
        color-mix(in srgb, var(--one-brass-hi) 55%, transparent), transparent); }
    .one-prompt{ font-size:22px; }
    .one-rooms{ overflow:visible; }
  }

/* ==========================================================================
   FLOATING PANELS ARE NOT WINDOWS
   ==========================================================================
 * app.css line 18 reads --glass-bg: rgba(28,40,86,0.44). Forty-four per cent
 * opaque — more than half see-through — and every .glass panel painted with it
 * over a backdrop-filter. That is why an email was legible THROUGH the
 * launchpad in Matt's screenshot of 25 September, and why the words STEPHANIE
 * HELLAND could be read through a column of chips.
 *
 * THE FIX IS A SPLIT, NOT A NUMBER. A card sitting ON the page background has
 * nothing behind it to bleed, so it can stay glassy and should. A panel
 * FLOATING OVER content has the page behind it, and at 44% the page wins. So
 * the floating ones get a ground with no alpha left in it: the same glass
 * colour, painted onto --bg-base, which is a hex in every theme this app ships.
 * Nothing about the colour changes. What changes is what is behind the colour.
 *
 * The blur stays. It costs nothing now and it is what the panels were reaching
 * for in the first place.
 *
 * tests/test-the-launchpad-is-not-see-through.js photographs each of these over
 * a white page and a black one and fails if a single pixel inside differs.
 */
  :root .one-surface,
  :root .lth-panel,
  :root .omni-card,
  :root .compose-modal.glass,
  :root .modal-backdrop > .glass,
  :root .from-menu,
  :root #rpActMenu,
  :root .pj-drawer,
  :root #dlgCard,
  :root .chat-card:not(.in-one){
    background:
      var(--glass-sheen),
      linear-gradient(var(--glass-bg), var(--glass-bg)),
      var(--bg-base);
  }
  /* The one surface keeps its own light on top of that ground. */
  :root .one-surface{
    background:
      radial-gradient(120% 90% at 22% -20%, rgba(255,255,255,.07), transparent 55%),
      linear-gradient(var(--glass-bg), var(--glass-bg)),
      var(--bg-base);
  }

  /* ONE OBJECT, NOT A BOX INSIDE A BOX. lucid-rim.js draws a travelling light
     around the edge of an opened card — "every edge that gets opened is Lucid",
     which is right when the card IS the panel. Inside the one surface it drew a
     second lit frame around the answer, four pixels in from the first, which is
     exactly the two-panels look the one surface was decided in order to end.
     The surface's own edge is the edge that opens now. */
  .one-go .chat-card > .lrim{ display:none; }
  .one-go .chat-thread{ background:none; }

  /* AND THE CARD'S OWN FILL HAS TO BE NAMED FROM :root TO WIN IN CHEAP MODE.
     :root[data-fx="lite"] .glass repaints every glass panel one step more
     opaque to make up for the blur that mode turns off. That is three parts of
     specificity against .one-go .chat-card's two, so in cheap mode the card
     kept a fill of its own and drew a lighter rectangle inside the surface —
     measured at 78% opaque, a box inside a box, which is exactly the look the
     one surface was decided in order to end. */
  :root .one-go .chat-card{ background:none; }

/* ==========================================================================
   THE MARK IS A DOOR ON A DESK TOO.                          (26 September)

   Matt, 26 September: "launch tray does not open and Lucid opens the old
   window", and the night before: "Nope at least I dont know how to get to it."

   The second sentence was the honest one and it took until now to believe it.

   WHAT WAS WRONG. The 45px band around the Lucid line -- the one whose own
   comment says "the headline feature of the app has been a hairline you were
   meant to hit with a fingertip" -- is written inside

       @media (hover: none) and (pointer: coarse), (max-width: 480px)

   at line 22658. So it exists on an iPhone and in a window under 480px, and
   NOWHERE ELSE. On a Mac, with a mouse, in a normal window, the whole of
   Lucid's door is the drawn line: measured in a real browser at

       2px tall, 1px of reach above it, 0 below

   The block's own note says so out loud and calls it a decision: "On a desk,
   with a mouse, it is a lovely piece of drawing and it is staying exactly as
   it is." It is a lovely piece of drawing. It is not a door. A person who
   does not already know it opens something has no way to learn that it does,
   and a person who does know has to land a mouse inside two pixels.

   WHAT THIS DOES. 8px above and 12px below, so the border box is 21.5px tall
   and a press within ten pixels of the wire opens Lucid. Under a fine pointer
   only, so the phone keeps its 45 and nothing on it changes. Lopsided for the
   reason the phone block gives -- .lth sits at top:0 and --th-mt is 15px, so
   8px up is the most that stays on the screen.

   The drawn wire DOES NOT MOVE: 15px of margin becomes 7px of margin plus 8px
   of padding, and background-origin/background-clip keep the gradient
   painting in the 1.5px content box in the middle, exactly as the phone rule
   does it. The breath is absolute against the padding box, so it is told
   where the line is or it would breathe over all 21 pixels.

   WHAT IT COSTS. About 21px of the top right corner answers to the band
   rather than to the page under it, across the line's length -- 78px when the
   day is quiet, 150px when it is pressing. .lth-top is already
   pointer-events:none with its children set back to auto, so nothing but the
   line and the button catches anything in there.

   AND IT IS STILL NOT THE EDGE. Matt, 25 September: "I like your edge in the
   demo. I touch it it glows, i click it it opens. Deliberate, simple, easy to
   find." That is a strip down the side of the screen and it is not built yet.
   This is the hairline made hittable, which is the least that was owed.

   Measured by tests/test-lucid-is-the-door.js, which asks the page what is
   under a point rather than reading min-height off a stylesheet -- the
   pseudo-element reach reports 0px here and would have passed a door that
   does not work.
   ========================================================================== */
@media (hover: hover) and (pointer: fine){
  .lth-line{
    margin-top:calc(var(--th-mt, 15px) - 8px);
    padding:8px 0 12px;
    background-origin:content-box; background-clip:content-box; }
  .lth-line::before{ inset:8px 0 12px; }
}

/* ==========================================================================
   THE EDGE. A STRIP DOWN THE SIDE THAT GLOWS WHEN TOUCHED AND OPENS WHEN
   CLICKED, AND DOES NOTHING ELSE AT ALL.                      (26 September)

   Matt, 25 September, choosing this over every alternative he was shown:

       "I dont want to flick an edge. I dont want anything gimmicky or
        potentially troublesome. (opening without desire) I like your edge in
        the demo. I touch it it glows, i click it it opens. Deliberate,
        simple, easy to find"

   WHAT IS NOT IN THIS BLOCK, AND WILL NOT BE. There is no timer, no gesture,
   no velocity test, no push-into-the-edge, no hot corner, and no rule
   anywhere that opens the surface on hover or on contact. The only thing
   hover does is change how this strip is painted. The only thing that opens
   the surface is a click, and the click is handled in lucid-thread.js by
   toggleDoor() — the same function the mark, the lens and the phone's big
   button already call, so there is one door in the code as well as on screen.
   Every one of the rejected mechanisms can fire when he did not ask, and his
   name for that failure is "opening without desire". It is the thing that
   would get this switched off.

   WHY AN EDGE IS THE EASIEST TARGET ON THE SCREEN. A pointer stops dead at
   the side of the window, so this is the one control in the app that cannot
   be overshot. Drawn all the time, it is also the one that can be learned by
   looking. Between them that is the whole of "deliberate, simple, easy to
   find" — and none of it needs a gesture.

   FINE POINTER ONLY. `display:none` is the default and `display:block`
   happens only inside the query below, so the phone and the trackpad-less
   iPad are untouched: they keep the big Ask Lucid button and gain nothing
   down their side, where an always-drawn strip under a thumb would be exactly
   the "potentially troublesome" thing.

   WHAT IT COSTS. Twelve pixels of the right-hand edge of the window answer to
   this strip rather than to the page under it, between 64px from the top and
   64px from the bottom. Measured across ten rooms before it was written: no
   button, link, field or scrollbar in the app reaches within sixteen pixels of
   the right edge of the window, in any of them, so it takes nothing away from
   anything. The full-screen scrims (#omniBackdrop and the rest) sit above it,
   so an open modal covers it and it cannot be pressed through one. The one
   surface's own backdrop is z-index 90 against this strip's 60, so while Lucid
   is open the strip is underneath it and a press out there closes the surface
   on the backdrop's mousedown — the same path the mark takes, and the reason
   this file does not need a second closing rule.

   A SIBLING OF `.lth`, NOT A CHILD, AND THAT IS ABOUT THE MARK.
   `.lth:hover .lth-line` brightens the mark and thickens it to 2px, and
   `:hover` matches every ancestor of whatever is under the pointer -- so while
   this was a child of `.lth`, resting on the strip made the mark twitch in the
   top corner with the pointer six hundred pixels below it. Two things that move
   together read as one control, and the decision doc rests on their being two:
   the mark is an INDICATOR, this strip is the BUTTON, and that separation is
   the whole reason the mark never has to grow. The full account of what moving
   out cost, and why cancelling the brighten from inside is a trap, is in the
   note beside the markup in index.html.
   ========================================================================== */
  .ledge{ display:none; }

@media (hover: hover) and (pointer: fine){
  .ledge{
    display:block; position:fixed; right:0; z-index:49;
    top:var(--ledge-top, 64px); bottom:64px; width:12px;
    cursor:pointer; -webkit-tap-highlight-color:transparent; }
  .ledge:focus{ outline:none; }

  /* THE TWO THINGS BEING A SIBLING OF `.lth` COSTS, PAID HERE.
     `.lth` is taken off screen while the front door is up, and the strip has to
     go with it or it is a door behind the door. And 49 puts it under `.lth`'s
     60, so the mark, the hint and the Ask Lucid panel paint over it -- which is
     a real fault this file already had once, when the strip covered the middle
     of the panel's microphone button -- and under `.modal-backdrop`'s 50, so an
     open dialog covers it and Lucid cannot be summoned through one. */
  body:has(#onboardGate.show) .ledge{ display:none; }

  /* THE WIRE. The mark's own paint, stood on end: one flat colour out of
     --th-base, no highlight, because an unlit wire has nothing to shade. It
     fades to nothing at both ends so it does not stop with a cut edge in the
     middle of the window, which would read as a bar rather than as a line.

     THE FADE IS IN THE PAINT AND NOT IN A MASK, and that is a bug that was
     caught by looking at a photograph rather than at a stylesheet. It was a
     mask-image, and a mask clips everything the element draws INCLUDING its
     box-shadow -- so the glow was in the computed style, the check that read
     the computed style passed, and the picture had no glow in it at all. The
     pixels either side of the lit wire measured 4.8, 7.4, 12.4, which is the
     page. Alpha stops in the gradient do the same job and clip nothing.

     AND IT IS BRIGHT ENOUGH TO BE FOUND. At 26% the wire measured 1.22:1
     against the page behind it -- a hairline you could believe in only once you
     knew it was there, which is the thing he has already said twice about the
     mark it replaces: "Nope at least I dont know how to get to it." At 56/72 it
     measures 1.6:1 at rest and 3.4:1 with the pointer on it, both read off a
     photograph by tests/test-the-edge.js rather than off this file. If it wants
     to be quieter or louder, these are the two numbers and nothing else moves. */
  .ledge-wire{
    position:absolute; top:0; bottom:0;
    right:var(--ledge-x, 5px); width:1.5px; border-radius:1px;
    background:linear-gradient(180deg,
      transparent 0%,
      color-mix(in srgb, var(--th-base) 56%, transparent) 13%,
      color-mix(in srgb, var(--th-base) 72%, transparent) 50%,
      color-mix(in srgb, var(--th-base) 56%, transparent) 87%,
      transparent 100%);
    transition:width .18s ease, right .18s ease, background .18s ease; }

  /* THE GLOW, on the strip itself rather than on the wire, because the wire is
     1.5 pixels wide and a bloom has to be wider than the thing blooming. A
     blurred bar rather than a box-shadow: a shadow would have to sit on the
     wire, and the wire is the one element here that must stay exactly 1.5px
     and exactly where it is. It carries the same fade at both ends so the
     light stops where the line stops. */
  .ledge::after{
    content:''; position:absolute; top:0; bottom:0;
    right:calc(var(--ledge-x, 5px) - 2px); width:5px;
    pointer-events:none; opacity:0; border-radius:3px;
    background:linear-gradient(180deg,
      transparent 0%,
      color-mix(in srgb, var(--th-mid) 62%, transparent) 14%,
      color-mix(in srgb, var(--th-mid) 62%, transparent) 86%,
      transparent 100%);
    filter:blur(4px);
    transition:opacity .18s ease; }

  /* TOUCHED. It glows. That is the entire behaviour of hover on this strip:
     the wire lights to the mark's own mid tone, gains half a pixel, and
     throws a short bloom either side of itself. Nothing opens, nothing
     slides out, nothing is armed, and nothing is counting.

     :focus-visible gets the same paint so the strip can be reached from the
     keyboard and still look like the thing that is about to answer — it
     carries role="button" and answers Enter and Space in lucid-thread.js,
     and a control that says it is a button and shows nothing when focused is
     HOUSE RULE 211 in the accessibility tree. */
  .ledge:hover .ledge-wire,
  .ledge:focus-visible .ledge-wire{
    width:2px; right:calc(var(--ledge-x, 5px) - 0.25px);
    background:linear-gradient(180deg,
      transparent 0%,
      color-mix(in srgb, var(--th-base) 86%, transparent) 13%,
      color-mix(in srgb, var(--th-mid)  96%, transparent) 50%,
      color-mix(in srgb, var(--th-base) 86%, transparent) 87%,
      transparent 100%); }
  .ledge:hover::after,
  .ledge:focus-visible::after{ opacity:1; }

  @media (prefers-reduced-motion:reduce){
    .ledge-wire, .ledge::after{ transition:none; } }

  /* ONE CONTINUOUS LINE, the other half of the picture in LUCID-THE-EDGE.html.
     `data-lucid-edge="joined"` on <html> moves the strip up to the height of
     the mark's own wire (7px of margin plus 8px of padding puts it at 15px)
     and slides it flush to the window's edge, so the horizontal wire turns the
     corner and runs down the side as one object. The top of the mask goes
     solid, because the top end is the corner now and a fade there would be a
     gap in the line — which in this app means "could not", and must never be
     drawn by accident. Nothing else about the strip changes: same 12px of
     reach, same glow, same click. */
  :root[data-lucid-edge="joined"] .ledge{ --ledge-top:15px; --ledge-x:0px; }
  :root[data-lucid-edge="joined"] .ledge-wire{
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--th-base) 62%, transparent) 0%,
      color-mix(in srgb, var(--th-base) 62%, transparent) 50%,
      color-mix(in srgb, var(--th-base) 46%, transparent) 87%,
      transparent 100%); }
  :root[data-lucid-edge="joined"] .ledge:hover .ledge-wire,
  :root[data-lucid-edge="joined"] .ledge:focus-visible .ledge-wire{
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--th-mid)  96%, transparent) 0%,
      color-mix(in srgb, var(--th-mid)  96%, transparent) 50%,
      color-mix(in srgb, var(--th-base) 86%, transparent) 87%,
      transparent 100%); }
  :root[data-lucid-edge="joined"] .ledge::after{
    background:linear-gradient(180deg,
      color-mix(in srgb, var(--th-mid) 62%, transparent) 0%,
      color-mix(in srgb, var(--th-mid) 62%, transparent) 86%,
      transparent 100%); }

  /* AND THERE IS NO RULE HERE HOLDING THE MARK STILL, WHICH IS THE FIX.
     There was one -- `.lth:has(.ledge:hover) .lth-line{ filter:none }` -- back
     when the strip was a child of `.lth` and resting on it fired `.lth:hover`.
     It looked right and it was wrong: `filter:none` is not the mark's resting
     value. That value is `brightness(.72)` on a quiet day, `brightness(1.3)` on
     a pressing one and different again in light mode, so the rule swapped one
     wrong brightness for another, and the check caught it reading 0.996 where
     0.72 was owed. The strip is a sibling now and there is nothing to cancel. */
}


/* ==========================================================================
   ONE MARK IN THE CORNER, AND IT IS THE ONE HE PICKED.     (26 September)

   idle A "Long drag", listening B, working C, something-to-say A, could-not A
   were built from the demo's own numbers and mounted nowhere. This hands the
   corner to them and switches off what used to draw there.

   WHAT GOES: the wire's own gradient and its left-to-right mask, the breath
   on ::before, and the whole .lth-rail -- the bead and the eighteen-bar
   listening meter. All of that is the OLDER mark. Keeping any of it means two
   marks in one corner disagreeing about what Lucid is doing, which is house
   rule 211 drawn twice.

   WHAT STAYS, untouched: the element, its id, its click handler, its role and
   label, and the 8px-above/12px-below padding that is the only reason a
   1.5px-tall door is hittable with a mouse. The canvas is painted inside that
   padding box, so the drawing sits where the wire always sat.

   AND THE LENGTH. --th-len was 112px at rest, 78 on a quiet day. A 38% tail
   on 78px is 30 pixels, which is a true long drag and still reads as a blip
   because the eye measures absolute length, not proportion -- the demo he
   judged it in is about 480px wide. So the resting lengths go up: 112 -> 240,
   quiet 78 -> 180, pressing 150 -> 300. The day states keep their meaning
   (quieter day, shorter wire) and the drag gets the room it was drawn for.
   Listening, open and wide are left exactly as they were.
   ========================================================================== */
:root .lth{ --th-len:240px; }
:root[data-lucid-day="quiet"]    .lth{ --th-len:180px; }
:root[data-lucid-day="pressing"] .lth{ --th-len:300px; }

/* box-sizing IS THE WHOLE BUG, AND IT COST A BUILD.

   With the page's border-box default, `height:20px` on an element carrying
   `padding:8px 0 12px` leaves a CONTENT box exactly 0 pixels tall. The canvas
   is width:100% height:100% of that, so it measured 0, lucid-mark.js's fit()
   returned null on every frame -- which is correct and deliberate, a canvas
   with no size is not something to draw on -- and the corner rendered nothing
   at all. Not wrong: empty. A screenshot caught it; nothing else would have.

   content-box here, so the 20px is the wire's own band and the 20px of reach
   sits outside it. The drawing is centred in that band, which puts the wire
   level with the middle of the three-bar button beside it. */
:root .lth-line{
  box-sizing:content-box;
  background:none !important;
  -webkit-mask-image:none !important; mask-image:none !important;
  filter:none !important;
  /* THE BOX IS WHAT IDLE USES — 3 October. 20px was the band the wire shared
     with the three-bar button beside it. The button has gone to the right
     wall, and idle needs 7px: the wire, its one wide faint light pass and the
     head's halo. So the band is 10, and that is all any page gives up. */
  height:10px;
  overflow:visible; }
:root .lth:hover .lth-line{ height:10px; filter:none !important; }
:root .lth-line::before{ display:none !important; content:none !important; }
/* THE CANVAS IS TALLER THAN ITS BOX, AND IS LEFT IN NORMAL FLOW.
   Listening wants +-12.6px and say's spike wants 13.8, so the drawing needs
   30. Reserving 30 would hold 23 of them empty all day for two states that
   last seconds, so the canvas overflows its 10px content box instead and hangs
   over the top of the page — nothing reflows, and when Lucid is listening the
   page has receded anyway.

   NOT position:absolute. `.lth-line` has 8px of top padding on a pointer
   device, and an absolutely positioned child resolves `top:0` against the
   PADDING box — which drew the wire eight pixels above its own band. In flow,
   the canvas starts at the content box, which is where the wire is. */
:root .lth-line canvas.lucid-mark{ display:block; width:100%;
  height:30px !important; pointer-events:none; }

/* ==========================================================================
   THE ONE SURFACE STOPS BEING A SLAB — 3 October.

   Matt, on the first version of this in the mocks: "Looks like a window slaped
   on top of the UI." He was right there and it is just as true here. Nothing is
   ADDED to the screen now; the app behind it is taken away, and the words sit
   on his own surface.

   Rooms are type. The old case was a bordered, gradient-filled button per room
   with a count under the name — twenty-odd containers in one column. The name,
   a tick in the area's colour, and nothing else.
   ========================================================================== */
.one-case{
  background:none !important; border:0 !important; box-shadow:none !important;
  padding:1px 0 !important; width:auto; text-align:left;
  font-size:16px; line-height:1.95; letter-spacing:0;
  color:color-mix(in srgb, var(--text) 82%, transparent);
  cursor:pointer;
  transition:color .14s ease, transform .14s ease; }
.one-case:hover{
  color:var(--text);
  transform:translateX(3px); }
.one-case u{ display:none; }
.one-cases{ display:flex; flex-direction:column; align-items:flex-start;
  gap:0; margin-bottom:14px; }
.one-lbl s{ display:none; }          /* the per-area count */

/* WHAT LUCID HAS TO SAY, where three hardcoded questions used to be. */
.one-say{ padding:13px 0; border-top:1px solid color-mix(in srgb, var(--text) 8%, transparent); }
.one-say:first-child{ border-top:0; }
.one-say-k{ display:flex; align-items:baseline; gap:10px;
  font-size:9px; letter-spacing:.17em; text-transform:uppercase;
  color:color-mix(in srgb, var(--th-mid) 90%, transparent); margin:0 0 4px; }
.one-say-ask .one-say-k{ color:color-mix(in srgb, var(--th-hot) 94%, transparent); }
.one-say-w{ margin-left:auto; letter-spacing:.04em; text-transform:none;
  color:color-mix(in srgb, var(--text) 34%, transparent); }
.one-say-t{ margin:0; font-size:15px; line-height:1.5;
  color:color-mix(in srgb, var(--text) 90%, transparent); }
/* A QUESTION LOOKS LIKE A QUESTION FROM ACROSS THE ROOM, before a word of it
   is read. Anything Lucid DID stays in the UI's own voice. */
.one-say-ask .one-say-t{ font-family:Georgia,"Times New Roman",serif;
  font-style:italic; font-size:17px;
  color:color-mix(in srgb, var(--th-hot) 92%, transparent); }
.one-say-d{ margin:5px 0 0; font-size:13px; line-height:1.55;
  color:color-mix(in srgb, var(--text) 52%, transparent); }
.one-say-a{ margin-top:9px; background:none; border:0; padding:0;
  font:inherit; font-size:13px; cursor:pointer;
  color:color-mix(in srgb, var(--th-mid) 96%, transparent); }
.one-say-a:hover{ color:var(--text); }

/* THE NOTCHES — the day, left to right. 3 October.
   A notch is left behind at the point on the wire where a thing happened, so
   the wire reads as a timeline rather than a count: you can see whether today
   had four things or none, and when, without opening anything. An 'ask' sits
   brighter than a 'did' because one of them is waiting on him.
   The reach around each one is a pseudo-element, because a 1px mark is not
   something anybody can be asked to hit. */
.lth-notch{ position:absolute; bottom:0; width:1px; height:6px;
  transform:translateX(-.5px); z-index:2;
  background:color-mix(in srgb, var(--th-mid) 58%, transparent);
  transition:height .2s ease, background .2s ease; }
.lth-notch::after{ content:''; position:absolute; left:-8px; right:-8px;
  top:-12px; bottom:-12px; }
.lth-notch-ask{ background:color-mix(in srgb, var(--th-hot) 80%, transparent); }
.lth-line:hover .lth-notch{ height:9px; }
.lth-notch:hover{ height:11px;
  background:color-mix(in srgb, var(--th-hot) 96%, transparent); }
@media (prefers-reduced-motion:reduce){ .lth-notch{ transition:none; } }
:root .lth-rail{ display:none !important; }


/* ===========================================================================
   THE PHONE STOPS ANNOUNCING THAT IT IS A WEB PAGE - 28 September 2026
   ===========================================================================

   Matt, holding the iPhone app for the first time: "still feels cheap".
   Measured from inside the running app on an iPhone 18 Pro Max at his 115%,
   three of the usual suspects were true and two were not:

     TRUE   body's user-select computed to `text`, so a press-and-hold
            anywhere - a tab, a card, the navigation bar - pops the blue
            selection and the magnifying loupe. No native app does that to its
            own furniture, and it is the single loudest "this is a website".
     TRUE   -webkit-tap-highlight-color computed to rgba(26,26,26,.3), so a
            grey rectangle flashes over whatever is pressed, in the shape of
            the element rather than the shape of the control.
     FALSE  a visible scrollbar. .main's offsetWidth and clientWidth are the
            same number, so no scrollbar is taking width. Ruled out.
     FALSE  content under the notch or the home indicator. env(safe-area-inset-
            top) and -bottom both compute to 0px inside the web view, because
            PhoneContentView already keeps the whole thing inside the safe area
            and WebPane-iOS sets contentInsetAdjustmentBehavior = .never so it
            is not inset twice. Ruled out - that one was already right.

   Below 780px only. The Mac has a pointer, a press there is a click and a drag
   there is a selection, and taking selection away from it would be a loss.   */
@media (max-width:780px){
  /* The app's own furniture is not text to be selected. */
  .bottom-nav, .bn-more-sheet, .bn-item, .bn-more-item, .lth-band, .seg, .chip, .btn, button{
    -webkit-user-select:none; user-select:none; }

  /* No grey rectangle over a press. A control that wants to look pressed says
     so itself with :active, in its own shape, which several already do. */
  *{ -webkit-tap-highlight-color:transparent; }

  /* AND THE EXCEPTIONS, WHICH ARE THE POINT OF SCOPING IT.
     Anything whose job is to be read or copied keeps its selection: an email
     address, a note, a message body, anything he is typing into. */
  input, textarea, [contenteditable="true"], .reading-pane, .msg-body,
  .note-body, .rp-body, .ask-msg{
    -webkit-user-select:text; user-select:text; }

  /* THE BUILD NUMBER, LAST ROW OF THE MORE SHEET.
     Quiet on purpose - it is reference material, the same as the spine's
     footer is on the Mac, and it is only ever read when he is asking "which
     one am I holding". */
  .bn-build{
    padding:var(--space-3) var(--space-4) var(--space-4);
    font-size:var(--type-body); color:var(--text-faint);
    text-align:center; letter-spacing:.01em; }
  .bn-build:empty{ display:none; }
}


/* ==========================================================================
   THE ONE SURFACE IS NOT A WINDOW — 3 October 2026.

   NOTHING IS ADDED TO THE SCREEN. That is the whole rule, and everything below
   follows from it. The slab, its border, its four shadows, its grain and its
   rise all go; the app behind blurs harder and falls back instead, and the
   words sit on the person's own surface.

   Matt first said it of the mock: "Looks like a window slaped on top of the
   UI." It was just as true of the real one, and build 256 shipped with it.

   Written at the end of the file on purpose. The rules above are the window
   this replaces, and they are left intact so the history still reads.
   ========================================================================== */

/* THE APP RECEDES. Harder blur, lower opacity, and a half-percent of scale --
   the scale is what makes it read as falling back rather than fogging up. */
.one-backdrop{
  background:color-mix(in srgb, var(--bg-deep) 62%, transparent) !important;
  -webkit-backdrop-filter:blur(26px) saturate(.6) !important;
  backdrop-filter:blur(26px) saturate(.6) !important;
  align-items:flex-start !important;
  padding:0 !important; }

/* NO SLAB. No background, no border, no shadow, no grain, no rise. The surface
   is a column of words on the app, and its only edge is where the words stop. */
.one-surface{
  width:min(1120px, 100%) !important;
  height:auto !important;
  max-height:calc(100vh - 56px) !important;
  margin:44px auto 0 !important;
  padding:0 34px 30px !important;
  border:0 !important;
  border-radius:0 !important;
  background:none !important;
  box-shadow:none !important;
  animation:none !important;
  overflow:auto !important; }
.one-grain{ display:none !important; }

/* THE CROWN GOES QUIET. Earlier, Changes and New are real doors and stay.
   The wordmark, the keyboard badge and the rule under them are the app
   introducing itself to somebody who opened it on purpose. */
.one-crown{ padding:0 0 18px !important; }
.one-nm, .one-kb{ display:none !important; }
.one-rule{ display:none !important; }
.one-crown .one-mark{ flex:1; min-width:120px; }
/* 32% WAS 2.68:1 ON HEARTH, MEASURED, AND HEARTH IS THE THEME HE LOOKS AT.
   9 October. "New" is 10px uppercase mono -- .one-surface[data-one-face="lucid"]
   .one-act pins it to 10px, 700 lines below -- and at 32% of --text it came out
   as #565960 on the sheet's #0f111a. The floor is 4.5. 52% puts the same ink at
   #8b8e96 and 5.13:1, measured the same way on the same rendered page.
   ONLY THE PERCENTAGE MOVED. Not the size, not the weight, not the tracking,
   not the position, and not the token -- it is still --text, still mixed with
   transparent, so every theme follows its own text colour as before.
   AND THIS IS THE SECOND OF TWO .one-act COLOUR RULES. The first is at line
   24489 and sets --text-dim; this block is later and carries !important, so
   this is the one on screen. Changing 24489 and declaring victory is a mistake
   already made on this file. 24489 is left alone deliberately: it never wins,
   and if this block were ever removed --text-dim is the better fallback. */
.one-act{ background:none !important; border:0 !important; box-shadow:none !important;
  font-size:11px; letter-spacing:.12em; text-transform:uppercase;
  color:color-mix(in srgb, var(--text) 52%, transparent) !important; }
.one-act:hover{ color:var(--text) !important; }
.one-x{ background:none !important; border:0 !important; box-shadow:none !important;
  color:color-mix(in srgb, var(--text) 32%, transparent) !important; }

/* NO PILL. A line, and a hairline under it. */
.one-field{
  margin:0 0 22px !important;
  padding:2px 0 8px !important;
  border:0 !important;
  border-bottom:1px solid color-mix(in srgb, var(--text) 10%, transparent) !important;
  border-radius:0 !important;
  background:none !important;
  box-shadow:none !important; }
.one-field:focus-within{
  border-bottom-color:color-mix(in srgb, var(--th-mid) 55%, transparent) !important;
  box-shadow:none !important; }
.one-field .chat-input{ font-family:Georgia,"Times New Roman",serif !important;
  font-size:21px !important; }

/* TWO COLUMNS, AND THE DIVIDER BETWEEN THEM GOES. */
.one-duo{ grid-template-columns:minmax(0,1fr) minmax(0,1.15fr) !important;
  gap:44px !important; }
.one-duo > div{ padding:0 !important; }
.one-ask::after{ display:none !important; }

/* ROOMS IN COLUMNS, not one narrow strip you scroll for ever. */
.one-go{ overflow:visible !important; }
.one-rooms{ columns:3; column-gap:30px; }
.one-lbl{ break-inside:avoid; }
.one-cases{ break-inside:avoid; }

/* THE SLOGAN ONLY SPEAKS WHEN LUCID HAS NOTHING TO. When there are messages it
   is the app talking over them about itself. :has() so no JS has to know. */
.one-ask:has(.one-say) .one-prompt{ display:none; }

@media (max-width:980px){
  .one-duo{ grid-template-columns:1fr !important; gap:28px !important; }
  .one-rooms{ columns:2; }
}
@media (max-width:640px){
  .one-surface{ padding:0 20px 24px !important; margin-top:30px !important; }
  .one-rooms{ columns:1; }
}
@media (prefers-reduced-motion:reduce){
  .one-surface{ animation:none !important; }
}


/* ==========================================================================
   WHAT THE PHOTOGRAPH CAUGHT — 3 October 2026, after the card came off.
   ========================================================================== */

/* 1 · THE BLOB BEHIND EVERY ROOM NAME. .one-case::before is the old tile's
   glow — a 66x46 radial in the area's hue, which made sense under a tile and
   reads as a smear under a word. ::after was the tile's bottom edge. */
.one-case::before,
.one-case::after{ display:none !important; content:none !important; }
.one-case:hover{ transform:none !important; }
.one-case b{ font-weight:400 !important; text-shadow:none !important; }

/* The grid of 96px minimum tracks was holding tile shapes. A list of names is
   a list. */
.one-cases{ display:block !important; }

/* 2 · A HEADING MUST NOT LEAVE ITS OWN ROOMS BEHIND. `columns` will break
   between a label and the list it names, which puts MEDIA over Life's rooms.
   The pair is wrapped against the break at both ends. */
.one-lbl{ break-inside:avoid; break-after:avoid; page-break-after:avoid; }
.one-cases{ break-before:avoid; page-break-before:avoid; }

/* 3 · THE PROMPTS ARE LINES, NOT PILLS. They only show when Lucid has nothing
   to say, and a day with nothing wrong should not look like three buttons. */
.one-line{
  border:0 !important; border-radius:0 !important;
  background:none !important; box-shadow:none !important;
  padding:7px 0 !important; font-size:15px !important;
  color:color-mix(in srgb, var(--text) 52%, transparent) !important; }
.one-line:hover{ transform:none !important;
  color:var(--text) !important; }

/* 4 · THE FIELD IS A LINE ALL THE WAY DOWN. .one-field lost its border last
   patch; the input and its two round buttons kept their own. */
.one-field .chat-input,
.one-field input[type="text"],
.one-field textarea{
  border:0 !important; border-radius:0 !important;
  background:none !important; box-shadow:none !important;
  padding-left:0 !important; padding-right:0 !important; }
/* THE SEND ARROW IS THIS RULE AND NOT THE GRADIENT PILL, which is the whole
   reason it has gone unfixed. .chat-send is declared FIVE times in this file --
   14217 (the orb's accent gradient, !important), 14231 (its light-mode twin),
   15536 (the original 36px round button), 24646 (the phone's 40px size) and
   here. Inside the Ask field this block wins: background:none, so there is no
   pill at all, and the arrow is a bare glyph on the sheet. Darkening the
   gradient or its --orb-ink would have moved nothing on screen and the number
   would not have budged -- and the pass would have been reported as a fix.
   46% measured 4.26:1 on Hearth: ink #75777e on the sheet's #0d0f19, the floor
   is 4.5, and 4.26 is a miss rather than a rounding. 52% measures 5.10:1.
   The mic and the field's other buttons share the declaration and move with
   it, which is right -- they are the same control at the same size. */
.one-field .chat-send,
.one-field .chat-mic,
.one-field button{
  border:0 !important; background:none !important; box-shadow:none !important;
  color:color-mix(in srgb, var(--text) 52%, transparent) !important; }
.one-field button:hover{ color:var(--text) !important; }
/* THE SEND ARROW TAKES THE FULL INK, 13 October. 52% measured 5.10:1 as a
   colour and 3.19:1 as pixels: the arrow is one glyph, U+2191, and its stroke is
   about a pixel and a half wide at 16px, so the antialiasing never reaches the
   colour it was given. A word at 52% has a core; this arrow does not. So the
   send arrow alone is drawn in --text, on every ground -- it is the one action
   in the field, and the mic and voice buttons beside it keep the 52% above,
   because they are drawn as icons with a real stroke. Specificity (0,2,1)
   beats the (0,2,0) and (0,1,1) rules above whichever comes first. */
.one-field button.chat-send{ color:var(--text) !important; }


/* THE NAMES RAN TOGETHER. Setting .one-cases to block left the buttons inline,
   so Projects and Board came out as ProjectsBoard. The container was changed
   and the items were not — caught in the photograph, which is the only place
   it could have been caught. */
.one-case{ display:block !important; width:100% !important; }

/* THE FIELD, FOR REAL. The last pass targeted .chat-input inside .one-field and
   the box on screen belongs to #chatInput itself, so the rectangle survived. */
.one-field #chatInput,
.one-field .chat-input-row,
#oneField input, #oneField textarea{
  border:0 !important; border-radius:0 !important;
  background:none !important; box-shadow:none !important;
  outline:none !important; }


/* ==========================================================================
   IT WAS UNREADABLE — 3 October, 10:50 PM. Matt: "cant read it".

   Taking the card away was right. Relying on backdrop-filter to push the app
   back was not: the Mac app runs in a webview where a transformed ancestor
   (#appZoomRoot) kills backdrop-filter entirely, so the scrim did nothing and
   Home came straight through the words. Two layers of text at once.

   A blur that may or may not apply cannot be the only thing separating the
   words from the page under them. The scrim is nearly opaque now and the blur
   is a bonus where it works, not the mechanism. Still no card, no border and
   no shadow — the app recedes because it is covered, not because it is fogged.
   ========================================================================== */
.one-backdrop{
  background:rgba(2,3,6,.955) !important;
  -webkit-backdrop-filter:blur(22px) saturate(.7);
  backdrop-filter:blur(22px) saturate(.7); }
:root[data-mode="light"] .one-backdrop{
  background:rgba(238,240,248,.965) !important; }


/* ==========================================================================
   TWO EDGES, TWO THINGS — 3 October 2026.

   The column the person did not ask for is folded down to its heading, which
   stays on screen and stays clickable. Folded, not deleted: a door that only
   opens one way is worse than one door, and a person who touched the wire and
   then wanted a room should not have to close anything to get there.
   ========================================================================== */
.one-surface[data-one-face="lucid"] .one-go .one-rooms,
.one-surface[data-one-face="lucid"] .one-go .one-note{ display:none; }
.one-surface[data-one-face="rooms"] .one-ask .one-prompt,
.one-surface[data-one-face="rooms"] .one-ask .one-lines{ display:none; }

/* The folded side gives its width back to the one that was asked for. */
.one-surface[data-one-face="lucid"] .one-duo{
  grid-template-columns:minmax(0,1fr) auto !important; }
.one-surface[data-one-face="rooms"] .one-duo{
  grid-template-columns:auto minmax(0,1fr) !important; }

/* A heading that is a door says so by lighting up, and nothing else. */
.one-h{ transition:color .16s ease; }
.one-h:hover{ color:color-mix(in srgb, var(--th-hot) 90%, transparent); }

/* THE WAY OUT HAS TO BE OBVIOUS. Matt, stuck in it: "cant read it" ... "or
   close it". The close was a faint 12px glyph lost among Home showing through.
   Escape and Cmd-J both already closed it and neither is visible on screen,
   which is not a way out for anybody who has not been told. */
.one-x{
  font-size:26px !important; line-height:1 !important;
  width:44px !important; height:44px !important;
  display:flex !important; align-items:center !important; justify-content:center !important;
  margin-left:6px !important;
  color:color-mix(in srgb, var(--text) 62%, transparent) !important; }
.one-x:hover{ color:var(--text) !important; }
.one-x:focus-visible{ outline:2px solid var(--th-mid); outline-offset:2px; }


/* ==========================================================================
   TWO EDGES, TWO THINGS — 3 October 2026.

   Matt, on 258: "I dont understand. The same thing opens for the side
   launcher and the lucid line on top." He was right: both doors called the
   same toggle and got the same two-column panel, so "you touch an edge, that
   edge opens" was a sentence about something that did not happen.

   The wire on the top edge leads with Lucid. The rail on the right wall leads
   with the rooms. The other column folds down to its heading, which stays on
   screen and stays clickable — folded, not deleted, because a door that only
   opens one way is worse than one door.
   ========================================================================== */
.one-surface[data-one-face="lucid"] .one-go .one-rooms,
.one-surface[data-one-face="lucid"] .one-go .one-note{ display:none !important; }
.one-surface[data-one-face="rooms"] .one-ask .one-prompt,
.one-surface[data-one-face="rooms"] .one-ask .one-lines{ display:none !important; }

.one-surface[data-one-face="lucid"] .one-duo{
  grid-template-columns:minmax(0,1fr) auto !important; }
.one-surface[data-one-face="rooms"] .one-duo{
  grid-template-columns:auto minmax(0,1fr) !important; }

/* A heading that is a door says so by lighting up, and nothing else. */
.one-h{ transition:color .16s ease; }
.one-h:hover{ color:color-mix(in srgb, var(--th-hot) 90%, transparent); }


/* ==========================================================================
   LUCID IS SMALL — 3 October 2026, late.

   Matt: "We talked about siri appearing and disappearing, and lucid is the
   giant pane. Both panels look like the same giant thing. Kinda sucks."

   He is right, and the mistake is precise: the card's DECORATION came off and
   its FOOTPRINT stayed. A 1120x478 pane behind a near-opaque scrim is still a
   thing that arrives over everything, which is the opposite of what he asked
   for. The border was never the problem. The size was.

   Siri's sheet has a background. It reads as light because it is SMALL and it
   is ATTACHED to an edge — not because it is transparent.

   So the two faces stop being one object:

     LUCID  a narrow sheet that drops out of the wire. ~620px, only as tall as
            what it has to say, hanging off the top edge it came from. The page
            behind dims a little and stays legible, because a thing this size
            is not covering anything.

     ROOMS  a drawer off the right wall. Full height, ~460px, flush to the edge
            the rungs live on. Sixty rooms genuinely need the room; a drawer
            gives it to them without pretending to be the same object.
   ========================================================================== */

/* THE SCRIM STOPS BEING A WALL. Enough to lift the sheet off the page, not
   enough to take the page away — the sheet is small now and does not need the
   app hidden to be readable. */
.one-backdrop{
  background:rgba(2,3,6,.44) !important;
  -webkit-backdrop-filter:blur(3px) saturate(.9) !important;
  backdrop-filter:blur(3px) saturate(.9) !important;
  align-items:flex-start !important;
  justify-content:flex-start !important;
  padding:0 !important; }
:root[data-mode="light"] .one-backdrop{ background:rgba(20,22,32,.30) !important; }

/* ---- LUCID: the sheet that drops out of the wire -------------------------- */
.one-surface[data-one-face="lucid"]{
  width:min(620px, calc(100vw - 32px)) !important;
  max-width:620px !important;
  height:auto !important;
  max-height:calc(100vh - 80px) !important;
  margin:14px 0 0 24px !important;
  padding:18px 22px 20px !important;
  border-radius:16px !important;
  /* IT HAS A BACKGROUND AGAIN, AND THAT IS NOT A REVERSAL. The slab was wrong
     because of its size; a 620px sheet needs its own ground to be readable and
     reads as light anyway. */
  background:
    linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.012)),
    color-mix(in srgb, var(--bg-base) 97.5%, transparent) !important;
  -webkit-backdrop-filter:blur(30px) saturate(1.1) !important;
  backdrop-filter:blur(30px) saturate(1.1) !important;
  box-shadow:0 24px 60px -20px rgba(0,0,0,.8) !important;
  animation:oneDrop 260ms cubic-bezier(.22,.9,.3,1) both !important; }

/* It comes OUT of the wire rather than fading in on top of the page. */
@keyframes oneDrop{
  from{ opacity:0; transform:translateY(-10px) scaleY(.985); transform-origin:50% 0; }
  to{ opacity:1; transform:none; } }

.one-surface[data-one-face="lucid"] .one-duo{
  display:block !important; }
.one-surface[data-one-face="lucid"] .one-field{ margin-bottom:14px !important; }
.one-surface[data-one-face="lucid"] .one-field .chat-input{ font-size:17px !important; }
.one-surface[data-one-face="lucid"] .one-h{ display:none !important; }
.one-surface[data-one-face="lucid"] .one-prompt{
  font-size:19px !important; margin-bottom:6px !important; }
.one-surface[data-one-face="lucid"] .one-crown{ padding:0 0 10px !important; }
.one-surface[data-one-face="lucid"] .one-act{ font-size:10px !important; }

/* ---- ROOMS: the drawer off the right wall -------------------------------- */
.one-surface[data-one-face="rooms"]{
  width:min(460px, calc(100vw - 24px)) !important;
  max-width:460px !important;
  height:100vh !important;
  max-height:100vh !important;
  margin:0 0 0 auto !important;
  padding:16px 26px 24px !important;
  border-radius:0 !important;
  background:
    linear-gradient(180deg, rgba(255,255,255,.035), rgba(255,255,255,.008)),
    color-mix(in srgb, var(--bg-base) 98.5%, transparent) !important;
  -webkit-backdrop-filter:blur(30px) saturate(1.1) !important;
  backdrop-filter:blur(30px) saturate(1.1) !important;
  box-shadow:-24px 0 60px -20px rgba(0,0,0,.8) !important;
  animation:oneSlide 260ms cubic-bezier(.22,.9,.3,1) both !important; }

@keyframes oneSlide{
  from{ opacity:0; transform:translateX(22px); }
  to{ opacity:1; transform:none; } }

.one-surface[data-one-face="rooms"] .one-duo{ display:block !important; }
.one-surface[data-one-face="rooms"] .one-rooms{ columns:1 !important; }

/* ROWS OF TWO. Matt, 14 Oct 22:20 EDT: "launchpad should be rows of 2 to
   shorten the sections." Each section's rooms become a two-column grid; the
   DOM order is unchanged, so they read left to right, then down, and Tab
   walks them in the same order. Only the arrangement moves: the names keep
   their font, size and colour, the headings and the drawer's width are
   untouched. Scoped to the rooms face, so the Lucid face is left as it is.

   A LONG NAME WRAPS INSIDE ITS OWN COLUMN. minmax(0,1fr) lets a track shrink
   below its longest word, and overflow-wrap:anywhere breaks a word that is
   wider than the track rather than letting it run under its neighbour. The
   row pitch is the old 1.95 line-height moved into padding, so a one-line
   name sits exactly where it did and a wrapped name's second line sits close
   under its first instead of reading as a second room. Every width, the
   phone included: at 390px each column is about 160px, and the longest room
   name measures 109px. tests/test-launchpad-rows-of-two.js measures all of it. */
.one-surface[data-one-face="rooms"] .one-cases{
  display:grid !important;
  grid-template-columns:repeat(2, minmax(0, 1fr));
  column-gap:var(--space-6); row-gap:0;
  align-items:start; }
.one-surface[data-one-face="rooms"] .one-case{
  min-width:0; white-space:normal; overflow-wrap:anywhere;
  line-height:1.3;
  padding:calc(.325em + 1px) 0 !important; }
.one-surface[data-one-face="rooms"] .one-crown{ padding:0 0 10px !important; }
.one-surface[data-one-face="rooms"] .one-act{ display:none !important; }
.one-surface[data-one-face="rooms"] .one-h{ display:none !important; }
.one-surface[data-one-face="rooms"] .one-field{ margin-bottom:14px !important; }
.one-surface[data-one-face="rooms"] .one-field .chat-input{ font-size:17px !important; }

/* THE SHEET FOLLOWS THE PAPER NOW, SO THE INK HAS TO FOLLOW IT TOO.

   9 October, measured off a rendered page. The two rules above shipped
   rgba(5,7,16,.975) and rgba(5,7,16,.985) -- a hard-coded near-black, with
   !important, where every other surface in this app uses a token. The text on
   the sheet followed the theme and the sheet did not, so on all three Serene
   papers Ask Lucid was dark ink on black: 27 of the 30 text elements on that
   screen between 1.10:1 and 3.62:1 against a 4.5 floor, on linen, mist and
   blush alike. Three quarters of the screen.
   tests/test-ask-lucid-reads-on-paper.js is the instrument and prints the
   numbers; it takes half a minute rather than the ten that a full
   tests/test-readability.js pass costs.

   The slab is a token now. That alone fixes everything on the sheet that is
   coloured with --text, --text-dim or --text-faint, because those are
   designed against the paper. It does NOT fix the three inks that are a
   PERCENTAGE of --text. Mixing a DARK ink with transparent over PALE paper
   makes it paler, so 52% of --text measures 3.11:1 on linen where the same
   52% measures 5.13:1 on hearth. Hearth is right and was measured this
   morning. It is the arithmetic that flips, not the choice.

   So the percentage is raised on LIGHT GROUNDS ONLY: 72% of --text measures
   5.53:1 on linen, 5.43:1 on mist and better than both on blush, computed,
   then confirmed in pixels. data-mode="light" is set by setTheme() for every
   light theme, so this follows the theme rather than naming the three papers
   -- a fourth paper is covered on the day it ships, with nobody editing this.
   Nothing here can reach hearth, ink or chocolate: those are
   data-mode="dark" and keep the 52% and the 32% above, unchanged, to the
   byte. THE SIZE, THE WEIGHT, THE TRACKING AND THE TOKEN ALL STAY. Only the
   percentage moves, and only on paper. */
:root[data-mode="light"] .one-surface .one-act{
  color:color-mix(in srgb, var(--text) 72%, transparent) !important; }
:root[data-mode="light"] .one-surface .one-act:hover{ color:var(--text) !important; }
:root[data-mode="light"] .one-surface .one-line{
  color:color-mix(in srgb, var(--text) 72%, transparent) !important; }
:root[data-mode="light"] .one-surface .one-line:hover{ color:var(--text) !important; }
:root[data-mode="light"] .one-surface .one-x{
  color:color-mix(in srgb, var(--text) 72%, transparent) !important; }
:root[data-mode="light"] .one-surface .one-x:hover{ color:var(--text) !important; }

/* THE BRASS ITALIC WAS THE LAST UNREADABLE THING ON THIS SHEET.

   Once the slab became a token, the one word left under the floor was the
   italic inside .one-prompt -- "Everything you own." -- at 2.18:1 on linen,
   2.19 on mist and 2.16 on blush, measured. --one-brass is #C79A54: a metal
   picked against black, 0.359 luminance, and the paper it now sits on is
   0.838. A light ink on light paper. There was already a light-mode rule
   here, 1,100 lines up, swapping --one-brass-hi for --one-brass -- the right
   instinct and one rung short, because both were chosen for a dark sheet.

   45% of the brass mixed into the paper OWN --text measures 5.60:1 on linen.
   Mixed rather than replaced, so the word is still the warm one in the
   sentence and still follows the theme: it is the same two tokens, in a
   ratio, and a new paper gets a correct brass without anybody editing this.
   Dark grounds never reach this rule and keep --one-brass-hi untouched. */
:root[data-mode="light"] .one-surface .one-prompt i{
  color:color-mix(in srgb, var(--one-brass) 45%, var(--text)) !important; }

/* TWO LABELS ON PAPER ARE DIMMED BY OPACITY RATHER THAN COLOURED, AND THAT IS
   WHY DARKER TOKENS DO NOT REACH THEM.

   .msp-group-head is --rail-ink-2 at opacity .75 and .hero-band-look is
   --text at opacity .6. Opacity dims a label against whatever happens to be
   behind it, so the colour is not what decides the result -- the same trap
   the note by .ask-allow-more already records, where one opacity:.55 failed
   on eleven themes at once. Darkening --text-dim takes .msp-group-head from
   3.26:1 only as far as 3.81:1, because three quarters of a good ink is
   still a quarter of the paper showing through.

   So the opacity goes instead. The group head is 11px uppercase on a spine
   of 13px rows and stays the quietest thing on that rail without being
   dimmed; the Quieter button keeps a little of its recession at .82, which
   is where the measurement allows it to stop.
   data-paper is set only by the Serene family, so hearth, ink and chocolate
   cannot be reached from here either. */
:root[data-paper] .msp-group-head{ opacity:1; }

/* THE AMBER ALLOWANCE ROW WAS 4.43:1 ON PAPER AND THE TOKEN IS NOT --text-dim.

   Last one on the sheet, and it took a wrong guess to find. .ask-allow-text
   measured ink 0.155 both before and after --text-dim was darkened, which is
   how it gave itself away: the sentence that reads "Could not read the
   allowance" is the UNKNOWN state and it is amber, not dim. The Serene
   palettes declare --orange as #99612D, #A55B21 and #98642F, and #99612D on
   linen paper computes to 4.43:1 -- a palette ink chosen to be legible as a
   colour, sitting a twentieth of a point under a floor of 4.5 at 11px.

   The token is left alone. --orange is also the fill on .ask-allow-fill and a
   glow elsewhere, and tests/test-serene-contrast.js checks the palettes for
   distinctness and for cast; moving a palette colour to fix one label is how
   a palette stops being a palette. 72% of the amber mixed into the paper OWN
   --text computes to 6.05:1 and is still visibly amber rather than ink.

   All three states that use this token move together, because 4.43:1 is a
   measurement of the ink at this size on this ground and not of one
   sentence. .ask-allow-stop is --pink and was not rendered by the fixture, so
   it is NOT touched here and NOT claimed: it is in the handback list. */
:root[data-mode="light"] .ask-allow-warn,
:root[data-mode="light"] .ask-allow-unknown,
:root[data-mode="light"] .ask-allow-uncapped{
  color:color-mix(in srgb, var(--orange) 72%, var(--text)); }

/* THE WAY BACK, now that the headings are gone. A short line at the foot of
   each sheet, which is the only thing either one says about itself. */

@media (max-width:640px){
  .one-surface[data-one-face="lucid"]{
    margin:10px 12px 0 12px !important; width:calc(100vw - 24px) !important; }
  .one-surface[data-one-face="rooms"]{ width:100vw !important; max-width:100vw !important; }
}
@media (prefers-reduced-motion:reduce){
  .one-surface[data-one-face="lucid"],
  .one-surface[data-one-face="rooms"]{ animation:none !important; }
}


/* ==========================================================================
   THE BIG ASK LUCID BUTTON IS REMOVED — 5 October 2026.

   The rules above are left where they are so the September decision still
   reads. Nothing renders them any more: the element is out of index.html, the
   variable they size against is retired, and this turns off anything that
   survives a merge or comes back from a stylesheet nobody re-read.
   ========================================================================== */
.ask-big{ display:none !important; }


.cd-addr-k{ flex:none; font-size:9px; letter-spacing:.14em; text-transform:uppercase;
  color:color-mix(in srgb, var(--text) 34%, transparent); }


/* ==========================================================================
   AN ICON FOR A CONTACT - item 18, 8 October 2026.
   The glyph inside an avatar disc, and the picker under the name on the card.
   Colours are the app's own tokens; nothing here chooses a hue. */
.avatar .ci-glyph{ width:52%; height:52%; display:block; }
/* The same glyph in the discs that are not .avatar (14 Oct, AVATAR 2): Home
   cards, texts, gone-quiet rows, signals, Pulse, travel, and the work, feed and
   watchlist faces. Without this an icon fills the whole disc edge to edge. */
.ht-av .ci-glyph, .sig-av .ci-glyph, .pw-av .ci-glyph, .px-av .ci-glyph,
.feed-face .ci-glyph{ width:52%; height:52%; display:block; flex:none; }
.ci-panel{ margin:0 0 12px; padding:12px; border:1px solid var(--glass-border); border-radius:var(--radius-card); }
.ci-panel[hidden]{ display:none; }
.ci-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(44px, 1fr)); gap:6px; }
.ci-opt{ display:flex; align-items:center; justify-content:center; width:100%; min-height:44px; padding:0;
  background:transparent; -webkit-appearance:none; appearance:none; cursor:pointer; color:var(--text-dim);
  border:1px solid var(--glass-border); border-radius:var(--radius-control);
  transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out),
             background var(--dur-fast) var(--ease-out); }
.ci-opt .ci-glyph{ width:22px; height:22px; }
.ci-opt:hover:not(:disabled){ color:var(--text); border-color:var(--glass-border-hi);
  background:color-mix(in srgb, var(--text) 8%, transparent); }
.ci-opt.is-on{ color:var(--text); border-color:var(--accent);
  background:color-mix(in srgb, var(--accent) 16%, transparent); }
.ci-opt:disabled, .ht-verb:disabled{ opacity:.45; cursor:not-allowed; }
.ci-opt:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.ci-foot{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:10px; }
.ci-foot-acts{ display:flex; align-items:center; gap:14px; flex-wrap:wrap; }
/* ADD PICTURE, THE FIRST CELL OF THE ICON GRID - 9 October.
   Two columns wide and labelled in words, because the twenty-nine cells beside
   it are unlabelled glyphs and the one that opens a file picker is not
   guessable from a drawing. The accent keyline is what makes it read as a
   different KIND of choice rather than a thirtieth icon, and it keeps the same
   44px minimum height as the rest so the row does not step. */
.ci-opt.ci-pic{ grid-column:span 2; gap:6px; border-color:var(--accent); color:var(--text);
  padding:0 8px; font-size:var(--type-label); font-weight:600; }
.ci-opt.ci-pic .ci-glyph{ flex:0 0 auto; }
.ci-pic-word{ white-space:nowrap; }
/* A grid narrow enough that two 44px columns do not fit (the panel on a phone)
   would otherwise push the label out of its own cell. One column there. */
@media (max-width: 420px){ .ci-opt.ci-pic{ grid-column:1 / -1; } }
.ci-note{ margin:0 0 10px; font-size:var(--type-label); color:var(--text-dim); }
.ci-warn{ color:var(--text); border-left:3px solid var(--accent); padding-left:10px; }

/* ==========================================================================
   A LOOK FOR SOMEBODY WITH NO PHOTO - 14 October 2026.
   What fills an avatar disc when he has chosen a monogram, an emoji or an
   illustrated face, and the tabs of the picker. The look is a span that fills
   whatever disc it is put in, so every disc keeps its own size, border and
   halo. Colours are Lucid's six only, through --av-c / --av-c2. */
.av-look{ width:100%; height:100%; border-radius:inherit; overflow:hidden; flex:none;
  display:flex; align-items:center; justify-content:center; line-height:1; }
.av-mono{ background-color:var(--av-c2); font-weight:700; letter-spacing:.02em;
  color:color-mix(in srgb, var(--av-c) 28%, #0E1118); }
.av-mono.av-p-dots{ background-image:radial-gradient(circle, color-mix(in srgb, var(--av-c) 42%, transparent) 22%, transparent 26%);
  background-size:22% 22%; background-position:center; }
.av-mono.av-p-stripes{ background-image:repeating-linear-gradient(135deg,
  color-mix(in srgb, var(--av-c) 30%, transparent) 0 7%, transparent 7% 16%); }
.av-mono.av-p-rings{ background-image:repeating-radial-gradient(circle at 50% 50%,
  transparent 0 10%, color-mix(in srgb, var(--av-c) 34%, transparent) 10% 13%); }
.av-mono.av-p-grid{ background-image:
  linear-gradient(90deg, color-mix(in srgb, var(--av-c) 30%, transparent) 0 6%, transparent 6%),
  linear-gradient(0deg, color-mix(in srgb, var(--av-c) 30%, transparent) 0 6%, transparent 6%);
  background-size:25% 25%; background-position:center; }
.av-emoji{ font-size:1.6em; background:color-mix(in srgb, var(--text) 7%, transparent); }
.av-artbox{ display:block; }
.av-art{ width:100%; height:100%; display:block; }
/* the timeline's text ring is drawn smaller than its font; keep the emoji in it */
.tl-avatar .av-emoji{ font-size:1.25em; }

.lk-tabs{ display:flex; flex-wrap:wrap; gap:var(--space-1); margin:0 0 var(--space-3); }
.lk-tab{ -webkit-appearance:none; appearance:none; cursor:pointer; background:transparent; color:var(--text-dim);
  border:1px solid var(--glass-border); border-radius:var(--radius-pill); padding:var(--space-1) var(--space-3);
  font:inherit; font-size:var(--type-label); font-weight:600; }
.lk-tab:hover{ color:var(--text); border-color:var(--glass-border-hi); }
.lk-tab.is-on{ color:var(--text); border-color:var(--accent); background:color-mix(in srgb, var(--accent) 16%, transparent); }
.lk-tab:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.lk-label{ font-size:var(--type-label); color:var(--text-dim); margin:0 0 var(--space-1); text-align:left; }
.lk-row{ margin:0 0 var(--space-3); grid-template-columns:repeat(auto-fill, minmax(52px, 1fr)); }
.lk-chip{ min-height:52px; }
.avatar.lk-disc{ width:40px; height:40px; font-size:var(--type-label); }
.lk-art .avatar.lk-disc{ width:44px; height:44px; }
.lk-tools{ display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap; margin:0 0 var(--space-2); }
.lk-search{ margin:0 0 var(--space-2); }
.lk-emoji-grid{ max-height:220px; overflow-y:auto; }
.lk-emoji{ font-size:var(--type-title); }
.lk-keep{ display:flex; align-items:center; gap:var(--space-1); font-size:var(--type-label); color:var(--text-dim); cursor:pointer; }
/* THE COMPANY LOGO (14 Oct). A white ground in every theme, because logos are
   drawn for white paper and a dark wordmark on a transparent PNG disappears on
   a dark disc. Contained, not cropped: a wide logo is shrunk, never cut. */
.av-logo{ background:#FFFFFF; padding:14%; box-sizing:border-box; }
.av-logo img{ width:100%; height:100%; object-fit:contain; display:block; border-radius:0; }
.lk-logo-row{ display:flex; align-items:center; gap:var(--space-3); margin:var(--space-2) 0; }
.lk-logo-row .lk-disc{ width:var(--space-12); height:var(--space-12); flex:none; }
.lk-logo-words{ display:flex; flex-direction:column; gap:var(--space-hair); min-width:0; }
.lk-logo-default{ margin-top:var(--space-2); }
.ce-look-row{ display:flex; align-items:center; gap:var(--space-3); margin:0 0 var(--space-2); }
.avatar.ce-look-av{ width:44px; height:44px; font-size:var(--type-label); }


/* ==========================================================================
   STARTER PACKS — list item 13, 5 October 2026.

   A list of sources is a LIST, so this is type and one tick of colour. The
   contact card taught the lesson: a card per address made a list of four
   addresses look like four separate things. Same here - twelve packs each
   wrapped in a panel would be twelve slabs in a dialog that is already a
   panel.

   NOTHING HERE IS !important AND NOTHING IS POSITIONED. It renders inside
   #dlgBody, which already owns its scroll and its width.
   ========================================================================== */
.pk-wrap{ display:flex; flex-direction:column; gap:18px; }
.pk{ display:flex; flex-direction:column; gap:6px; }
.pk-h{ display:flex; align-items:baseline; gap:8px; }
.pk-n{ font-size:15px; font-weight:600;
  color:color-mix(in srgb, var(--text) 96%, transparent); }
.pk-c{ font-size:12px;
  color:color-mix(in srgb, var(--text) 46%, transparent); }
.pk-note{ font-size:13px; line-height:1.45;
  color:color-mix(in srgb, var(--text) 62%, transparent); }
.pk-srcs{ display:flex; flex-direction:column; gap:1px; margin:4px 0 2px; }
/* [hidden] IS A UA STYLESHEET RULE AND display:flex BEATS IT.
   The line above set display:flex on this element, which outranks the browser's
   own [hidden]{display:none}. So every "shut" pack was still fully drawn, and
   the harness said otherwise because it counted `.pk-srcs:not([hidden])` - the
   ATTRIBUTE - instead of asking whether anything was on the screen. The markup
   was right and the picture was wrong, which is the whole reason the rule is
   to photograph it. */
.pk-srcs[hidden]{ display:none; }

/* A ROW IS A LABEL, so the whole line is the hit target and not just the
   12px box. A tick you have to aim at is a tick that gets mis-pressed. */
.pk-src{ display:flex; align-items:baseline; gap:10px; padding:5px 0;
  font-size:14px; cursor:pointer;
  color:color-mix(in srgb, var(--text) 86%, transparent); }
.pk-src input{ flex:0 0 auto; margin:0; cursor:pointer; }
.pk-src-t{ flex:0 1 auto; }
/* THE ADDRESS IS NOT TRUNCATED WITH AN ELLIPSIS. The contact card shipped
   reading "m.mur…" and the photograph was the only thing that caught it. This
   is already cut to 44 characters in the markup, so it needs no second cut
   here - it needs room and a smaller size. */
.pk-src-w{ flex:1 1 auto; text-align:right; font-size:11px;
  color:color-mix(in srgb, var(--text) 40%, transparent); }
.pk-src.is-have{ color:color-mix(in srgb, var(--text) 48%, transparent); cursor:default; }
.pk-src.is-have .pk-src-w{ color:color-mix(in srgb, var(--th-hot) 72%, transparent); }
.pk-src .x{ flex:0 0 auto; }
.pk-do{ display:flex; gap:8px; }

/* WHAT LUCID ACTUALLY READ, shown before anything can be saved. The headlines
   are the proof; the title alone could be a <title> tag on a 404 page. */
.pk-found{ display:flex; flex-direction:column; gap:4px; padding:8px 0;
  border-top:1px solid color-mix(in srgb, var(--text) 10%, transparent); }
.pk-found-t{ font-size:14px; font-weight:600;
  color:color-mix(in srgb, var(--text) 92%, transparent); }
.pk-found-i{ font-size:12px; line-height:1.55;
  color:color-mix(in srgb, var(--text) 58%, transparent); }
.pk-found button{ align-self:flex-start; margin-top:4px; }

@media (max-width:780px){
  /* The address sits under the name rather than beside it; at 390px a name
     and a 44-character address on one line is two truncations. */
  .pk-src{ flex-wrap:wrap; }
  .pk-src-w{ flex:1 1 100%; text-align:left; padding-left:24px; }
}

/* A SHUT PACK IS THE WHOLE ROW, so the thing you press is the thing you read.
   It is a <button> for the keyboard and for the screen reader, and stripped
   back to looking like the heading it replaced. */
.pk-h-btn{ appearance:none; -webkit-appearance:none; background:none; border:0;
  width:100%; padding:2px 0; margin:0; text-align:left; cursor:pointer;
  font:inherit; color:inherit; }
.pk-h-btn:hover .pk-n{ color:color-mix(in srgb, var(--th-hot) 92%, transparent); }
.pk-h-btn:focus-visible{ outline:2px solid var(--blue); outline-offset:3px; border-radius:4px; }
.pk-caret{ margin-left:auto; font-size:12px;
  color:color-mix(in srgb, var(--th-hot) 78%, transparent); }

/* NO IS A REAL ANSWER AND LOOKS LIKE ONE. It sits beside Show me, quieter but
   not hidden - a question with only a yes on it is not a question.

   AND IT NEEDS A GAP. The first version read "Show meNo thanks" as one word,
   because .one-say-a is an inline-level button with no margin and two of them
   in a row close up. Caught in the photograph; the DOM was correct and both
   buttons worked. */
.one-say-no{ opacity:.62; margin-left:16px; }
.one-say-no:hover{ opacity:1; }


/* =========================================================================
   PEOPLE ON AN EVENT - item 6. The add row under the Invitees list, and the
   way off each invitee.

   AT THE END OF THE FILE, ON PURPOSE. This stylesheet carries a :root override
   block thousands of lines below the rules it overrides, and the four-across
   fix for item 3 lost to it silently three times before being moved here on
   5 October. Last wins; the end is the only place that is reliably last.
   ========================================================================= */

/* The row itself. flex rather than a grid: the input takes what is left and the
   button keeps its own width, which is the whole of the layout. */
.ed-addp{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  margin:var(--space-2) 0 0;
}

/* Matched to .pj-input's shape rather than invented, so the one place in the
   app where you type a person's name does not look like a different app. */
.ed-addp-in{
  flex:1;
  min-width:0;                        /* or a long datalist value makes the row wider than the panel */
  padding:var(--space-2) var(--space-3);
  font:inherit;
  font-size:var(--type-body);
  color:var(--text);
  background:color-mix(in srgb, var(--text) 5%, transparent);
  border:1px solid color-mix(in srgb, var(--text) 14%, transparent);
  border-radius:var(--radius-control);
}
.ed-addp-in::placeholder{ color:var(--text-dim); }
.ed-addp-in:focus{
  outline:none;
  border-color:var(--accent);
  background:color-mix(in srgb, var(--accent) 8%, transparent);
}

.ed-addp-go{ flex:none; }

/* The sentence that stops this being a surprise. Said on screen rather than
   only in the confirm card, because the confirm card arrives AFTER he has
   decided to press, and the thing worth knowing before pressing is that
   pressing emails somebody. */
.ed-addp-note{
  margin:var(--space-2) 0 0;
  font-size:var(--type-label);
  color:var(--text-dim);
}

/* The x on an invitee row.
   OPACITY 0 UNTIL THE ROW IS HOVERED OR THE BUTTON ITSELF IS FOCUSED, and that
   second half is not decoration: a control that only appears on hover is a
   control a keyboard cannot find, and this one sends a cancellation email, so
   reaching it by accident with a stray Return would be expensive. Tab lands on
   it and it becomes visible.

   It is still a real button at opacity 0 - it takes a click and reports
   opacity 1 to anything measuring the element rather than the paint. That is
   the trap from 19 September written down where the next person will meet it:
   measure visibility from offsetParent and box size, never from this. */
.ed-inv-off{
  flex:none;
  width:22px; height:22px;
  margin-left:var(--space-1);
  padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:16px; line-height:1;
  color:var(--text-dim);
  background:none;
  border:none;
  border-radius:var(--radius-round);
  cursor:pointer;
  opacity:0;
  transition:opacity .12s ease, color .12s ease, background .12s ease;
}
.ed-invitee:hover .ed-inv-off,
.ed-inv-off:focus-visible{ opacity:1; }
.ed-inv-off:hover,
.ed-inv-off:focus-visible{
  color:var(--pink);
  background:color-mix(in srgb, var(--pink) 14%, transparent);
}

/* On a touch screen there is no hover, so hiding it means hiding it for good.
   Shown at a lower weight instead - present, reachable, not shouting. */
@media (hover:none){
  .ed-inv-off{ opacity:.55; }
}

/* PEOPLE ON A TASK - item 6, the other half. The chips in the task tray. */
.tk-people{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  gap:var(--space-2);
  margin:var(--space-2) 0 0;
}
.tk-person{
  display:inline-flex;
  align-items:center;
  gap:var(--space-2);
  padding:3px var(--space-2) 3px 3px;
  font-size:var(--type-label);
  color:var(--text);
  background:color-mix(in srgb, var(--accent) 12%, transparent);
  border:1px solid color-mix(in srgb, var(--accent) 24%, transparent);
  border-radius:var(--radius-round);
  max-width:100%;
}
.tk-person-name{
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* A door, like the invitee name is. Underlined on hover only, so a row of
   chips is not a row of links shouting at him. */
.tk-person-name.lucid-door{ cursor:pointer; }
.tk-person-name.lucid-door:hover{ text-decoration:underline; }
.tk-person-role{
  color:var(--text-dim);
  font-size:var(--type-label);
}
/* ALWAYS VISIBLE on a task chip, unlike the event one, and the difference is
   the point: taking somebody off a task tells nobody and costs nothing, so it
   does not need to be hard to reach or to ask first. */
.tk-person-off{
  flex:none;
  width:18px; height:18px;
  padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:14px; line-height:1;
  color:var(--text-dim);
  background:none; border:none;
  border-radius:var(--radius-round);
  cursor:pointer;
}
.tk-person-off:hover,
.tk-person-off:focus-visible{
  color:var(--pink);
  background:color-mix(in srgb, var(--pink) 16%, transparent);
}
/* The sentence drawn when the people could not be READ, as opposed to when
   there are none. House rule 211: those are different sentences and this is the
   one that is not an empty list. */
.tk-people-why{
  margin:var(--space-2) 0 0;
  font-size:var(--type-label);
  color:var(--orange);
}

/* ===========================================================================
   MAILSILENT, 8 OCTOBER 2026 -- NO TOAST IS EVER DOTLESS
   ===========================================================================
   .toast.ok, .toast.warn and .toast.error each give .toast-dot a background.
   Nothing else does, and the base .toast-dot rule sets none -- so a toast
   carrying any other kind drew an 8px transparent square where the dot goes
   and was pixel-for-pixel a success card with different words in it.
   reportFault() passes 'bad', which means every fault this app has reported
   to Matt since reportFault was written looked like good news.

   toastKind() in app.js now maps 'bad' onto 'error' before it reaches the DOM,
   so these two rules are the belt to that braces: they keep working if a kind
   ever reaches the markup another way, and they are at the END of the file
   because a :root override block thousands of lines above has quietly won
   three times before.

   Both colours carry a literal fallback. A var() that resolves to nothing
   kills its whole declaration, which here would mean an invisible dot again --
   the exact bug, reintroduced by the fix for it.
   =========================================================================== */
.toast.bad .toast-dot,
.toast.fail .toast-dot,
.toast.danger .toast-dot{
  background:var(--cat-red-2, #ff6b5e);
  box-shadow:0 0 10px 1px var(--cat-red-glow, rgba(255,107,94,.45));
}
/* The net under all of it: an unrecognised kind gets a visible dot in the
   theme's own dim text colour. Lower specificity (0,1,0) than .toast.ok
   .toast-dot (0,2,1), so it cannot repaint a success however late it sits in
   the file -- it only ever fills in a dot nobody else claimed. */
.toast-dot{ background:var(--text-dim, #8a8a8a); }

/* ===========================================================================
   THE WORD "MAIL" COMES OFF THE TOOLBAR AT PHONE WIDTH.   9 October 2026.

   Matt was asked at 9:45, with the alternative on the table, and chose this.

   MEASURED FIRST, on master, by tests/test-mail-toolbar-fit.js. The bar is ONE
   row at every width from 1500 down to 320 and the page has never scrolled
   sideways, so the old complaint -- that it overflows DOWNWARD into the mail --
   is fixed and stayed fixed. What is wrong now is the other direction: the last
   controls walk off the right-hand edge.

     390px   #tbSelectedCount 26px outside the bar, .tb-gutter 40px outside
     360px   #tbSelectedCount 56px outside, #tbMoreWrap 6px outside
     320px   #tbMoreWrap 46px outside

   WITH A SELECTION, 430px FAILS TOO -- the count 12px outside the bar and the
   gutter 26px -- so the query is the 430 this file already calls phone width. 

   THE PIXELS HAVE TO COME FROM SOMETHING THAT IS NOT A BUTTON. The chip strip
   used to absorb this by shrinking to nothing, and it is gone; a 44px touch
   target cannot do that, and the 12px of title margin already came off at 430.
   The heading is the only thing left on the row that is furniture rather than a
   control, and it is the one piece of it that tells a person looking at their
   mail nothing they do not already know.

   IT IS TAKEN OUT OF THE FLOW RATHER THAN OUT OF THE PAGE. display:none would
   also take the screen only h1 on this view out of the accessibility tree, and
   a phone is where that matters most. Clipped to a pixel it keeps its place in
   the document and costs the row nothing. */
@media (max-width: 430px){
  .mail-toolbar-full .tb-title{
    position:absolute; width:1px; height:1px; margin:0; padding:0;
    overflow:hidden; white-space:nowrap; clip-path:inset(50%); }
}

/* ===========================================================================
   THE TEXT BUBBLES WERE THE SIZE OF THE WHOLE COLUMN.   9 October 2026.

   Matt: "The text display is too big. Font size is fine, but the pills they
   are in are way too big."

   MEASURED AT HIS OWN WINDOW, 1481x690, with a real conversation open -- nine
   texts and two emails on a contact card, runs expanded, not an empty
   fixture. Every bubble came back IDENTICAL:

     915px wide x 51px tall   for one line of 13px text that is 20px tall
     padding 8px 0            margin-bottom 8px   gap 16px   max-width none
     inside it: a 32px monogram and an 865px body

   So the font was never the problem, exactly as he said. A twenty-pixel
   sentence was sitting in a fifty-one pixel card stretched across all nine
   hundred and fifteen pixels of the column.

   AND THE PADDING IS NOT WHERE .tl-text SAYS IT IS. .tl-text asks for
   `padding:var(--space-3) var(--space-4)` about 8,200 lines above this and
   NEVER GETS IT: .tl-row, same specificity and later in the file, sets
   `padding:var(--space-2) 0` and wins, which is why the measurement reads 8px
   0 and not 12px 16px. Editing .tl-text and declaring victory is the mistake
   this file has already had made on it. Hence .timeline-item.tl-text here --
   two classes, later still, so it beats .tl-row on specificity and on order.

   The three things he named are the three things that move. The size, the
   face and the weight of the words are untouched. */
.timeline-item.tl-text{
  padding:var(--space-1) var(--space-2);
  margin-bottom:var(--space-1);
  gap:var(--space-3);
  max-width:min(620px, 100%); }
.timeline-item.tl-text .timeline-text{ line-height:1.4; }


/* ===========================================================================
   THE FEEL UNDER HIS HAND - 9 October 2026
   ===========================================================================

   Matt: "Navigating Lucid feels cheap. Scrolls are not smooth. Buttons feel
   like they are lego bricks when pressed. Agenda calendar is still too small
   so need to take some of the agenda space which is too big. (not 50/50 but
   maybe 40/60)"

   Three complaints. TWO of them were real and are fixed below. The scroll one
   was MEASURED AND NOT REPRODUCED, and that is written down rather than
   papered over with a change that measures the same before and after.

   Headed Chrome on brokedown-palace-2, his own GPU, 1481x690, a fourteen-event
   day with eighteen tasks, sixty wheel ticks up and sixty back down:

     blurs ON  (95 live backdrop-filters)   .agenda-scroll  mean 8.5ms  p95 9.4  0 frames over 20ms of 235
     blurs OFF (data-fx="lite", 0 of them)  .agenda-scroll  mean 8.5ms  p95 9.2  2 frames over 20ms of 234
     blurs ON                               .main           mean 10.9ms p95 9.2  2 frames over 20ms of 234
     blurs OFF                              .main           mean 8.4ms  p95 9.3  0 frames over 20ms of 237

   That display runs at 120Hz, so 8.3ms IS the frame. Turning off every blur in
   the application moves the mean by nothing. So the claim higher up this file
   that backdrop-filter is "the most expensive thing on this page by a
   distance" is NOT true on this machine at this volume, and neither is layout
   thrash: onMailListScroll() reads three geometry properties and returns
   without writing any, and lucidOnScreenChanged() returns on its first line
   outside the Mac shell. Nothing here is unthrottled either.

   WHAT IS NOT RULED OUT, and is the honest next place to look: his real
   mailbox. .mail-list pages in another block of rows at 600px from the bottom
   and never drops the ones above, so the list he actually scrolls is thousands
   of rows deep, not thirty-two. This measurement does not reach that, and no
   rule below pretends to fix it.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   1. THE PRESS. "Buttons feel like they are lego bricks when pressed."
   ---------------------------------------------------------------------------
   He is right, and it is countable. app.css carried ELEVEN :active rules in
   26,032 lines, and of the 714 <button> elements in index.html and app.js only
   15 were reachable by any of them. The other 699 did nothing at all on
   mousedown: no scale, no travel, no settle. A hover tint that stays put while
   the finger is down is exactly the lego brick.

   Below 780px it is worse than nothing. The rule at row 25166 sets
   -webkit-tap-highlight-color:transparent on *, which took away the grey flash
   iOS gives for free, with the comment "a control that wants to look pressed
   says so itself with :active, in its own shape, which several already do".
   Several did. Six hundred and ninety-nine did not, so on the phone a press
   landed on a control that acknowledged nothing.

   THE RESPONSE: 2.8% smaller and half a pixel down, arriving in 60ms and
   settling back over --dur-fast (130ms) on release. Fast in, slower out, is
   what reads as a physical button; a symmetrical transition reads as a hover.
   It is deliberately smaller than the chat pill's .97 and the Ask box's .985,
   both of which are big soft targets that can afford more travel.

   ONE selector list, worn by the app's SHARED classes, so this lands
   everywhere at once instead of on the handful of buttons one worker happened
   to have open. .btn alone is on 443 of the 714. Controls that already say
   something of their own on :active keep saying it - those rules set
   background, this one sets transform, and they compose.

   The travel is a token on :root so that prefers-reduced-motion can switch it
   off in ONE place rather than repeating this selector list a third time. A
   var() that resolves to nothing kills its whole declaration, so --press-travel
   is defined unconditionally below and only ever overridden, never unset. */
/* 14 OCTOBER: ONE MECHANISM FOR EVERY PRESS, NOT A LIST OF FIFTY CLASSES.
   The list below this paragraph's predecessor reached 50 classes. The source
   has 1,539 clickable tags wearing 397 classes, and on the day this was
   written 313 of the 454 distinct shapes they make showed nothing at all under
   a forced :active (tests/test-every-press-answers.js counts them). A list
   chases that number for ever. So the press is now chosen by WHAT A THING IS,
   not by what it is called: a button, a link, a summary, a role of button /
   menuitem / tab / option / switch, anything with an onclick= (which is how
   1,684 of the app's handlers are attached), and the handful of data- hooks the
   document-level click handlers find with closest().

   TWO PARTS, both instant, neither a redesign:

   THE TINT, on every one of them. A wash of the theme's own text colour laid
   over whatever background the control already has, as a background-image so
   it composes with background-color instead of replacing it. Because it is
   --text, it darkens in light mode and lifts in dark mode with no second rule.
   Gradients do not interpolate, so it arrives on the frame the finger lands.
   A control that paints its own picture with background-image (an avatar, a
   gradient pill) keeps it: those are excluded below by name and get the
   brightness nudge instead.

   THE TRAVEL, on the compact controls only (buttons, chips, the listed
   classes). The individual `scale` property, NOT `transform`, so it composes
   with a control that is positioned by its own transform (the art window's
   arrows are translateY(-50%); the old rule set transform and made them jump
   half their height on every press). A full-width row or a tile does not
   shrink - it is tinted - because a row that scales reads as the page moving.

   prefers-reduced-motion: the travel goes; the tint stays, because a press
   must still be SEEN. A disabled control does neither. */
:root{
  --press-scale: .972;
  --press-tint: color-mix(in srgb, var(--text) 10%, transparent);
  --press-filter: brightness(1.14);
}
:root[data-mode="light"]{ --press-filter: brightness(.92); }
@media (prefers-reduced-motion: reduce){
  :root{ --press-scale: none; }
}
/* THE TINT. */
:is(button, a[href], summary, [onclick], [role="button"], [role="menuitem"],
    [role="menuitemradio"], [role="menuitemcheckbox"], [role="tab"],
    [role="option"], [role="switch"], input[type="button"], input[type="submit"],
    [data-ev], [data-open], [data-hero-span], [data-lp-nav], [data-lp-today],
    [data-lp-clear], [data-mn-mail], [data-mn-line], [data-warm],
    .mc-cell[data-iso], .chip[data-val]):active:not(:disabled, [aria-disabled="true"], .is-unavailable,
    .primary, .glass, .avatar, .mc-add, .vault-card, .mb-act-go, .pl-btn.main,
    .chat-send, .omni-ask, .one-act, .one-x, .one-line, .one-case, .lth-line){
  background-image: linear-gradient(var(--press-tint), var(--press-tint));
}
/* THE ONES THAT PAINT THEIR OWN PICTURE: a gradient fill (.primary, the
   glass cards, the chat send, the Lucid-one cases), a photo (.avatar), or a
   background held down with !important (.one-act, .one-x, .lth-line). A tint
   would either wipe the picture for as long as the finger is down or not land
   at all, so these get the same news as a brightness step instead: down in
   light mode, up in dark, the same direction the tint goes. New ones are
   found by tests/test-every-press-answers.js, which fails on any control that
   loses its own background under the finger. */
:is(button, a[href], summary, [onclick], [role="button"], [role="menuitem"],
    [role="menuitemradio"], [role="menuitemcheckbox"], [role="tab"],
    [role="option"], [role="switch"], input[type="button"], input[type="submit"],
    [data-ev], [data-open], [data-hero-span], [data-lp-nav], [data-lp-today],
    [data-lp-clear], [data-mn-mail], [data-mn-line], [data-warm],
    .mc-cell[data-iso], .chip[data-val]):is(.primary, .glass, .avatar, .mc-add, .vault-card, .mb-act-go, .pl-btn.main,
    .chat-send, .omni-ask, .one-act, .one-x, .one-line, .one-case, .lth-line):active:not(:disabled, [aria-disabled="true"], .is-unavailable){
  filter: var(--press-filter);
}

/* A disabled control is not a button and must not move under the finger. */
.is-unavailable:active{ scale: none; }
/* Four controls kept a press of their own written as transform before this
   existed (the login button's 1px drop, the chat pill, the Ask box, the
   launchpad pad). Asked for less motion, they get the tint and no travel. */
@media (prefers-reduced-motion: reduce){
  :is(.login-btn, .chat-pill, .ask-big, .lp-pad):active{ transform: none; }
}
/* WAITING ON THE SERVER. api() puts .is-busy (and aria-busy) on the control
   whose press started a write, for exactly as long as the write is out; see
   pressBusy() in app.js. Dimmed at once and the pointer says "working"; after
   half a second of waiting it starts to breathe, so a slow answer reads as
   slow rather than dead. No breathing for reduced motion: it stays dimmed. */
.is-busy{ cursor: progress; opacity: .62; }
@media (prefers-reduced-motion: no-preference){
  .is-busy{ animation: lucid-press-wait 1.1s var(--ease-out) .5s infinite alternate; }
}
@keyframes lucid-press-wait{ from{ opacity: .62; } to{ opacity: .4; } }

/* ---------------------------------------------------------------------------
   2. THE RAIL: 40 AGENDA / 60 CALENDAR. HIS NUMBER, NOT MINE.
   ---------------------------------------------------------------------------
   "Agenda calendar is still too small so need to take some of the agenda space
   which is too big. (not 50/50 but maybe 40/60)"

   #todayDayView is the only agenda screen in Lucid - the navigation calls it
   "Today / Agenda" - and it has exactly two splits that could be the one he is
   describing. Both measured at his 1481x690 before this rule:

     HORIZONTAL  left rail 319px / day timeline 992px   =  24.4 / 75.6
     VERTICAL    mini-cal 259px  / agenda-scroll 195px  =  57 calendar / 43 agenda

   Nobody looks at 24/76 and calls it 50/50, so the split he is describing is
   the VERTICAL one inside the rail, and 43/57 is what "50/50" means at a
   glance. He wants 40 agenda / 60 calendar, so the month gets the bigger half.

   WHY IT WAS 43/57, and it was a deliberate decision by somebody else:
   .mc-cell forty lines up carries `aspect-ratio:1; max-height:34px` under the
   comment "Capped so the rail stays about the agenda", and the note above
   .today-grid says in as many words "the month grid would take 252 happily,
   but the month is not the thing this rail is for". The month was capped ON
   PURPOSE to protect the agenda. He has now asked twice for the opposite, so
   the cap comes off inside this rail and the month is allowed to fill its
   share.

   flex-grow 6 and 4 against a flex-basis of 0 rather than two percentages:
   .left-panel also holds the "Agenda" section label, so basis:60%/40% would
   add up to more than the rail and overflow it by the height of that label.
   Growing 6:4 from zero splits the space that is actually free, which makes
   the two panes measure exactly 40/60 of each other whatever the label does.

   ABOVE 1100px ONLY. At the 1100px breakpoint .today-grid stacks to one
   column, the rail becomes the full width of the page and the whole view goes
   back to document flow - there the agenda measures 2956px against a 331px
   month because the PAGE scrolls, not the rail, and pinning a ratio on a pane
   with no fixed height of its own would crush the agenda into a letterbox
   inside a page that already scrolls. The phone keeps its stack. */


/* ---------------------------------------------------------------------------
   3. THE MONTH USES THE RAIL IT WAS GIVEN. 9 October.
   ---------------------------------------------------------------------------
   Matt: "So increase from 24% up to 33%. Then make the month grid use it
   better." Two jobs, and the second is the one he has now asked for twice.

   THE RAIL. clamp(276px, 24%, 360px) became clamp(276px, 33%, 600px) up at
   .today-grid, which is the only rule in this file that sets those tracks.
   600 rather than 360 because .today-grid carries max-width:1800px, so 33%
   can never resolve above 594 and the ceiling can never bite again. It WAS
   biting: measured on master, the rail is 319px at his 1481 window, which is
   the 24.4% he described -- and 360px on a 1920 monitor, which is 20.6%. The
   number he asked me to raise was only ever true on his laptop.

   AND HERE IS THE PART HE HAS BEEN WAITING TWO ROUNDS FOR. It was never the
   width. Measured on master at 1481x690:

     rail 319px  -  panel padding 40px  =  grid 277px

   87% of the rail already reached the grid, and nothing but .left-panel's own
   padding took a pixel. Two other things were eating the month:

     THE NUMERAL NEVER GREW. .mc-cell is set in --type-label -- 11px, the
     smallest type in the app. It is 11px in a 38.8px cell at his window and
     it is STILL 11px in a 44.6 x 85.1px cell on his 1920 monitor, because
     FEEL's 60% of the rail doubled the box and left the ink alone. That is
     the whole of "the calendar is too small": not a small calendar, a small
     number in a big empty square. --type-lead here, 16px, in the rail.

     A THIRD OF THE MONTH'S HEIGHT IS NOT THE MONTH. Of the 277px FEEL hands
     the mini-cal, 84px is the header, the weekday letters, the legend and
     four margins; 192px is the grid. The header and the legend both say
     something and both stay. The four margins do not, and giving them up
     hands the five week rows 18px back between them.

   THE WEEKDAY LETTERS WERE NEVER OVER THEIR COLUMNS, which nobody had cause
   to notice until the gap grew. .mc-weekdays is repeat(7,1fr) with NO gap;
   .mc-grid is seven tracks WITH one. Same total width, different pitch, so
   S M T W T F S drift off their columns -- about 2px by Saturday on master,
   and it would have been 6px once the gap went to 2. Both rows take the same
   tracks and the same gap here, so they share one pitch by construction.

   WRITTEN AS A LATER RULE ON PURPOSE, and this is the trap this file is
   famous for. Section 2 above already sets .left-panel > .mini-cal > .mc-grid
   and its children, for the 40/60 split. These are different declarations on
   some of the same selectors and they are meant to win. A grep for
   .mc-grid gives several hits; this is the block that decides the size. */


/* ===========================================================================
   THE COLLAPSED RUN OF TEXTS WAS TWICE THE HEIGHT OF THE MAIL ROW BESIDE IT.
   9 October 2026.

   Matt: "the text pills on a contact's timeline are too big." Measured at his
   window, 1481x690, between two emails:

     .timeline-item.tl-run   915 x 66     16px / 24px type
     .tlc-row                901 x 33     16px / 24.8px type

   THE SIXTY-SIX IS NOT PADDING. It is arithmetic, and every part of it was
   measured before anything here was written:

     1px border        .glass
     8px padding       .tl-row's `padding:var(--space-2) 0`
     48px content      the preview, TWO LINES of 24px
     8px padding
     1px border                                              = 66

   The two lines are the cause. The preview is a .timeline-text, and the base
   .timeline-text rule (about line 7496) clamps to TWO lines and sets a 24px
   line-height; runRowHtml hands it up to 140 characters of the newest text, so
   anything longer than about 55 characters wraps and the box is 48px tall. The
   avatar (32px, 33 with its margin) never decided the height -- it was shorter
   than the text beside it -- and no min-height is involved anywhere.

   WHY THE 9 OCT BUBBLE FIX DID NOT REACH IT. The rule above this one, "THE TEXT
   BUBBLES WERE THE SIZE OF THE WHOLE COLUMN", tightens `.timeline-item.tl-text`.
   A rolled-up run is drawn by runRowHtml as `timeline-item glass tl-row tl-run`
   -- it has no `tl-text` class, so none of that padding, margin, line-height or
   width ever applied to it. Two code paths drew "a text" and only one was fixed.

   THE FIX IS THE LEDGER'S OWN RULE: ONE LINE, CLIPPED WITH AN ELLIPSIS. That is
   exactly what .tlc-said does for the email beside it, for the same stated
   reason -- a preview that runs to a second line turns the row back into a
   card -- and the whole run is one press away (the "N more" on its right opens
   every message, in full, in its own bubble). The glance is the point.

   The ring, the rim, the monogram and the "N more" are all kept: they are what
   says this is texts, whose, and how many. The monogram comes down from 32px to
   24px because 32 + its border cannot sit inside a 33px row, and the padding is
   set so the whole pill lands on the mail row's height (3px + 1px border).

   Two or three classes deep and last in the file, so it outranks .tl-row and
   .timeline-text and the light-mode avatar rule on both specificity and order.
   Only tokens that exist: --space-*, --type-*, --radius-* (not --radius-sm or
   --type-small, which would silently kill the declaration). */
.timeline-item.tl-run{
  padding:calc(var(--space-1) - 1px) var(--space-2) calc(var(--space-1) - 1px) var(--space-1);
  margin-bottom:var(--space-1);
  gap:var(--space-2);
  max-width:min(620px, 100%); }
.timeline-item.tl-run .timeline-icon.tl-avatar{
  width:24px; height:24px; margin-top:0;
  font-size:var(--type-label);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 78%, black 12%),
    0 0 0 2px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 18%, transparent); }
:root[data-mode="light"] .timeline-item.tl-run .timeline-icon.tl-avatar{
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 82%, black 6%),
    0 0 0 2px color-mix(in srgb, var(--tl-kind, var(--text-dim)) 14%, transparent); }
.timeline-item.tl-run .timeline-text{
  display:block; -webkit-line-clamp:none;
  line-height:1.55;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* .tl-said asks for pre-wrap on its own, which would win over the nowrap on the
   line that holds it and bring the second line straight back. */
.timeline-item.tl-run .tl-said{ white-space:nowrap; }
/* PHONE. The ledger beside it drops to --type-body under 560px (see .tlc-row),
   so the pill does too, or it would stand 5px taller than the mail row and read
   larger than the email it sits between. The monogram follows to 20px so the
   row stays the ledger's height rather than the avatar's. */
@media (max-width: 560px){
  .timeline-item.tl-run .timeline-text{ font-size:var(--type-body); }
  .timeline-item.tl-run .timeline-icon.tl-avatar{ width:20px; height:20px; }
}
/* THE OFFER ON A CONTACT CARD, AND THE FOUR COUNTS IN THE SWEEP.
   9 October. "If the company is not in my companies, Lucid should offer to make
   it a new company" — so the offer has to read as a QUESTION with an editable
   answer, not as a thing that has already happened. Hence the question on its
   own line, the name in a real field rather than as text, and the Create button
   beside it. Nothing here invents a colour: the glass panel, --surface and
   --text-dim are the ones the rest of the card already uses. */
.cj-offer{ margin-top:14px; padding:14px; border-radius:12px; }
.cj-offer-q{ font-weight:600; margin-bottom:4px; }
.cj-offer-sub{ color:var(--text-dim); font-size:var(--type-body); margin-bottom:10px; }
.cj-offer-row{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.cj-offer-row .cj-offer-name{ flex:1 1 180px; min-width:0; margin:0; }
.cj-offer-row .btn{ flex:0 0 auto; }

/* FOUR NUMBERS, BEFORE A SINGLE CHECKBOX. Big figure left, the sentence it is
   true of beside it, so the answer to "was this worth it" is one glance. */
.cj-tally{ display:flex; flex-direction:column; gap:8px; margin:4px 0 0; }
.cj-tally-row{ display:flex; gap:12px; align-items:baseline; }
.cj-tally-row b:first-child{ flex:0 0 auto; min-width:3.2em; text-align:right; font-size:1.35em; font-variant-numeric:tabular-nums; }
.cj-tally-row span{ color:var(--text-dim); }
.pw-main .cj-new-name{ margin:8px 0 0; width:100%; max-width:320px; }


/* =========================================================================
   LUCID'S ACCENT — DEFAULTS ONLY.
   =========================================================================
   The live values are solved per ground by lucid-accent.js and written as an
   inline custom property on <html>, which outranks every :root rule in this
   file. These exist so the app is never mid-paint with an empty gradient:
   before the first applyAccent() they resolve to the accent the theme already
   had, which is exactly what the app looked like yesterday.

   --accent, --accent-2 and --accent-glow are NOT redefined here. They keep
   their existing definitions and lucid-accent.js overrides them inline, which
   is what let the voice reach all 425 of their uses without this stylesheet
   being swept. See HANDOFF.md, 10 October.
   ========================================================================= */
:root{
  --ac1: var(--accent); --ac2: var(--accent); --ac3: var(--accent-2); --ac4: var(--accent-2);
  /* The gradient and material tokens were here and nothing read them. They
     come back with the spine signals that consume them — see the note in
     lucid-accent.js and check 6 in tests/test-accent-contrast.js. */
}

/* ---- the voice picker's own four classes -------------------------------
   The swatches reuse .serene-sw, so they already have the right size, corner
   and ring. These add the slider row, its readout, the "default" tag, and
   nothing else.

   The slider track and thumb take --accent, so dragging a hue recolours the
   control you are dragging. That is not decoration: it is the shortest
   possible loop between the input and its effect. ---------------------- */
.accent-knob{ align-items:center; gap:var(--space-2); }
.accent-range{
  -webkit-appearance:none; appearance:none; flex:1; min-width:0;
  height:20px; background:none; cursor:pointer; margin:0;
}
.accent-range::-webkit-slider-runnable-track{
  height:4px; border-radius:999px;
  background:color-mix(in srgb, var(--text) 16%, transparent);
}
.accent-range::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none; width:15px; height:15px; margin-top:-5.5px;
  border-radius:50%; border:none; background:var(--accent);
  box-shadow:0 1px 4px rgba(0,0,0,0.4);
}
.accent-range:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
.accent-val{
  font:var(--type-label); font-variant-numeric:tabular-nums;
  color:var(--text-dim); min-width:4.5ch; text-align:right;
}
/* --text-dim, NOT --text-faint. The faint ink measured 4.48:1 on serene:linen
   in September and this is the same size of lettering in the same panel. */
.accent-default{
  font:var(--type-label); color:var(--text-dim); margin-left:var(--space-1);
  opacity:0.8;
}


/* ---- the two spine controls that moved on 10 October -------------------
   The caret left the brand block for the middle of the rail, and the Lucid
   mark became a door. Both are additive: .collapse-btn keeps every rule it
   already had, and .collapse-mid only says where it now sits.
   ---------------------------------------------------------------------- */

/* The caret was positioned by being inside .brand-block. On its own in the
   rail it needs its own box, and it is centred rather than left-aligned
   because collapsed it is the only thing on its row and an off-centre caret
   in a 58px rail reads as a mistake. */
/* --radius-sm AND --dur-1 NEVER EXISTED — found by tests/lint.js, 13 October.
   These five declarations have been doing nothing at all: the rail caret and
   the brand door have had no border-radius and no hover transition, silently,
   because both names are from a convention this codebase does not use. The
   scale names its steps (--radius-control, --radius-tight, --radius-card) and
   its durations (--dur-fast, --dur-base, --dur-slow) — see app/tokens.css.
   So this is corrected to the real names rather than by defining two aliases,
   which would have put a second name on one value and made the next person's
   guess just as likely to be wrong. --radius-control because both of these
   are controls you press; --dur-fast because 130ms is what every other state
   change in the app uses. */
.collapse-mid{
  display:flex; align-items:center; justify-content:center;
  margin:var(--space-1) 0; padding:var(--space-1) 0;
  border-radius:var(--radius-control);
  cursor:pointer; color:var(--text-faint);
  transition:background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.collapse-mid:hover{ background:color-mix(in srgb, var(--text) 7%, transparent); color:var(--text); }
.collapse-mid:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

/* THE MARK IS A BUTTON NOW, so it has to say so under the finger as well as
   to a screen reader. The lens inside already animates; this is only the
   affordance around it. */
.brand-door{ cursor:pointer; border-radius:var(--radius-control); }
.brand-door:hover{ background:color-mix(in srgb, var(--text) 6%, transparent); }
.brand-door:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }


/* ---- the ring that turns while Lucid is thinking -----------------------
   A conic gradient, not a linear one: a linear gradient run round a rounded
   rectangle bunches at the corners and thins along the long edges, which
   reads as a flicker rather than a rotation.

   It is drawn in the border box only -- padding:2px plus two masks composited
   to exclude -- so it is a ring around the card rather than a wash behind it,
   and nothing inside the card has to move to make room for it.

   It takes the accent, so it is whatever colour Lucid is speaking in today.
   -------------------------------------------------------------------- */

/* The angle has to be a real typed property or it cannot be animated: a plain
   custom property interpolates as a string and the gradient jumps from 0 to
   360 in one frame. Named --ac-ring-ang rather than --angle because this
   stylesheet is 27,000 lines and a generic name is a collision waiting. */
@property --ac-ring-ang{ syntax:'<angle>'; initial-value:0deg; inherits:false; }

.omni-card{ position:relative; }
.omni-card.ac-thinking::after{
  content:''; position:absolute; inset:0; border-radius:inherit; padding:2px;
  background:conic-gradient(from var(--ac-ring-ang),
    var(--ac1), var(--ac2), var(--ac3), var(--ac4), var(--ac1));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite:exclude;
  animation:acRingSpin 1.8s linear infinite;
  pointer-events:none;
}
@keyframes acRingSpin{ to{ --ac-ring-ang:360deg; } }

/* SOMEBODY WHO HAS ASKED FOR LESS MOTION STILL NEEDS TO KNOW IT IS WORKING,
   so the ring stops turning and holds as a still accent edge rather than
   disappearing. The state is the point; the spin is the decoration. */
@media (prefers-reduced-motion: reduce){
  .omni-card.ac-thinking::after{ animation:none; }
}


/* =========================================================================
   LUCID SPEAKS THROUGH THE SPINE.
   =========================================================================
   One grammar, five signals, applied to the rail rows that already exist.
   Nothing here adds a row, a tray or a second rail: the spine is already in
   his eye every second the app is open and already means "where things are",
   so Lucid lighting a row is a sentence he can already read.

   The tokens come back here with the rules that read them. They were written
   by lucid-accent.js and read by nothing for one commit, which is exactly the
   fault mail_vector_cursor's counters are, and tests/test-accent-contrast.js
   check 6 now fails if either half arrives without the other.
   ========================================================================= */
:root{
  --ac-prism:  linear-gradient(90deg,var(--ac1),var(--ac2),var(--ac3),var(--ac4));
  --ac-prismV: linear-gradient(180deg,var(--ac1),var(--ac2),var(--ac3),var(--ac4));
  --ac-conic:  conic-gradient(from 0deg,var(--ac1),var(--ac2),var(--ac3),var(--ac4),var(--ac1));
  /* An rgb triple rather than a colour, so each rule picks its own alpha —
     rgba(var(--ac-glow),.5). Same shape as --lq-1 and --lq-2 already use. */
  --ac-glow: 61,107,255;
  /* Material. Zero means nothing drawn at all, not something drawn faintly:
     a 2% specular is a smudge, not a finish. */
  --ac-sheen: 0;
  --ac-glass: 0;
}

/* ---- BASE · the rail light ---------------------------------------------
   :not(.active) IS LОAD-BEARING. .nav-item.active::before is that row's own
   2px marker in its area colour and has been since the rail was built. This
   rule has equal specificity and sits later in the file, so without the
   exclusion it wins and the row he is standing on loses the thing telling him
   he is standing on it. It is also the right design: a signal on the current
   row says almost nothing, and switchView clears it on arrival anyway.
   A lit sliver of the rail at that row's height. GEOMETRY, which is the whole
   reason it is the base: a colour change needs a label to land on and a swell
   needs room to grow into, and collapsed to 58px the rail has neither. This
   survives, and it keeps meaning the same thing. */
.nav-item{ position:relative; }
.nav-item.ac-lit:not(.active)::before{
  content:''; position:absolute; left:0; top:4px; bottom:4px; width:2px;
  border-radius:2px; background:var(--ac-prismV);
  box-shadow:0 0 8px rgba(var(--ac-glow),0.75);
  animation:acGrow var(--dur-1, .28s) var(--ease-out, ease-out);
}
@keyframes acGrow{ from{ transform:scaleY(0); } to{ transform:scaleY(1); } }

/* The sheen rides the sliver as a bright core rather than a second shape, so
   turning the finish off removes it completely. */
.nav-item.ac-lit:not(.active)::after{
  content:''; position:absolute; left:0; top:4px; bottom:4px; width:2px;
  border-radius:2px; opacity:var(--ac-sheen); pointer-events:none;
  background:linear-gradient(180deg, transparent, rgba(255,255,255,.9), transparent);
}

/* ---- the GLASS finish ---------------------------------------------------
   Sheen is a highlight ON the signal; Glass is a panel BEHIND it. The lit row
   gains a tinted ground in the accent's own colour, so a finding reads as
   something resting on the rail rather than something drawn on it.

   Alpha arithmetic, not a backdrop-filter: at --ac-glass: 0 the alpha is 0
   and this rule costs nothing, where blur(0px) would still promote every rail
   row to its own compositing layer for an effect that is turned off.

   This rule exists because tests/test-accent-contrast.js check 6 found
   --ac-glass being written by the app and read by nothing -- which meant the
   Glass option in Settings was a control that did nothing at all. */
.nav-item.ac-lit{
  background:rgba(var(--ac-glow), calc(0.12 * var(--ac-glass)));
}

/* ---- LAYER · ink --------------------------------------------------------
   The label itself drawn in the accent. Rides ON the rail light and never
   appears alone, which is why it is a second class rather than a sixth
   option. Expanded-only by design: it needs a word to land on, and when the
   rail is collapsed the base is still saying where. */
.nav-item.ac-lit.ac-about .nav-label{
  background:var(--ac-prism); -webkit-background-clip:text; background-clip:text;
  color:transparent; font-weight:600;
}
/* The count on the right must NOT be swallowed by the label's clip. It is the
   other meaning on this row and it keeps its own colour. */
.nav-item.ac-lit.ac-about .nav-n,
.nav-item.ac-lit.ac-about .nav-kbd{ background:none; color:var(--text-faint); -webkit-text-fill-color:currentColor; }
.nav-item.ac-lit.ac-about .nav-icon{ color:var(--accent); opacity:1; }

/* ---- STATE · breathe ----------------------------------------------------
   A slow pulse, and THE ONLY SIGNAL ALLOWED TO APPEAR WITHOUT BEING ASKED
   FOR. It means the thing expires. Reserve it: a rail that always breathes
   means nothing, which is how notification systems end up muted. */
.nav-item.ac-lit.ac-timed{ animation:acBreathe 3.4s ease-in-out infinite; }
@keyframes acBreathe{
  0%,100%{ background:transparent; }
  50%{ background:rgba(var(--ac-glow),0.16); }
}

/* ---- EVENT · swell ------------------------------------------------------
   420ms, once, at the moment a finding lands, and then it hands over to the
   rail light. NOT A STATE. Motion is legal at arrival and nowhere else, which
   is the answer to the one thing I disliked about it in round five: it moves
   its neighbours, but only while something is actually arriving. */
.nav-item.ac-arriving{
  transform:scale(1.055);
  background:color-mix(in srgb, var(--text) 8%, transparent);
  transition:transform .18s cubic-bezier(.3,1.3,.4,1);
}

/* ---- GESTURE · relay ----------------------------------------------------
   Hovering the Lucid mark brightens every lit row at once. Not a signal —
   the gesture that READS them, and the answer to "where is everything" when
   more than one row is lit or the rail is collapsed. */
.sidebar.ac-relay .nav-item.ac-lit{
  background:rgba(var(--ac-glow),0.14);
}
.sidebar.ac-relay .nav-item.ac-lit:not(.active)::before{
  box-shadow:0 0 13px rgba(var(--ac-glow),0.95);
}

/* ---- the mark carries what the rail cannot -----------------------------
   Findings belonging to no surface, and everything past the two-row cap,
   live on the mark. The halo is the only thing that says so, and it is on
   the brand block rather than the lens itself so the lens's own animation
   is left alone. */
.brand-door.ac-carrying{ animation:acHalo 2.2s ease-out infinite; border-radius:var(--radius-control); }
@keyframes acHalo{
  0%{ box-shadow:0 0 0 0 rgba(var(--ac-glow),0.5); }
  70%{ box-shadow:0 0 0 9px rgba(var(--ac-glow),0); }
  100%{ box-shadow:0 0 0 0 rgba(var(--ac-glow),0); }
}

/* Somebody who has asked for less motion still has to be able to tell that a
   row is lit. The base is geometry and stays; the three that are motion stop.
   The state is the point, the movement is the decoration. */
@media (prefers-reduced-motion: reduce){
  .nav-item.ac-lit:not(.active)::before{ animation:none; }
  .nav-item.ac-lit.ac-timed{ animation:none; background:rgba(var(--ac-glow),0.12); }
  .nav-item.ac-arriving{ transform:none; }
  .brand-door.ac-carrying{ animation:none; box-shadow:0 0 0 2px rgba(var(--ac-glow),0.45); }
}


/* The old COLLAPSED PEEK (armed only while a row was lit) is gone: every unpinned
   spine now opens on hover intent. See THE SPINE, ONE GRID at the end of this file. */


/* =========================================================================
   THE LUCID DOCK. 10 October, second attempt.
   =========================================================================
   Ask Lucid opens .one-surface -- a panel oneBuild() writes in JavaScript,
   never in index.html. Inside it oneAdopt() puts the question line (#oneField)
   ABOVE the thread, and the surface floats near the top-left at 620px wide.
   Matt, on the mock: "a rectangle on the floor of the window that grows upward
   while the question line stays still". And on 309: "Not at all what we
   discussed."

   WHY THESE DECLARATIONS CARRY !important, which is not laziness.
   Measured against the live page: `.one-surface[data-one-face="lucid"]` sets
   width: min(620px, 100vw - 32px), max-width: 620px and margin: 14px 0 0 24px,
   all three with !important. A declaration cannot beat an important one on
   specificity at any weight, so the dock answers in kind. Each override below
   names the property it is taking back. Nothing else in this block is
   important, because nothing else needs to be.

   SCOPED TO THE LUCID FACE. The same surface is used for the rooms face, which
   has its own width rules and its own shape. Scoping by the attribute means a
   dock opened by the Lucid mark cannot restyle a panel opened as rooms.
   ========================================================================= */
.one-backdrop.lucid-dock{
  /* THE SECOND SET OF !important DECLARATIONS, AND THE SAME REASON AS THE
     FIRST. Measured on the live page: .one-backdrop sets align-items,
     justify-content, padding and background-color with !important -- in two
     separate blocks. Without matching them the dock renders at the top of the
     window, full-bleed, which is exactly what the first attempt at this file
     did when I left them off.

     The floor, and the left edge of the content -- never the sidebar's width
     written down a second time. lucidDockPlace() measures .main and writes
     --dock-left; 14px is the fallback, which covers the rail and looks wrong
     rather than sitting half off screen and looking deliberate. */
  align-items: flex-end !important;      /* takes back flex-start */
  justify-content: flex-start !important;
  padding: 0 14px 14px var(--dock-left, 14px) !important;  /* takes back 0 */
  /* A dock is not a modal. The app stays lit and stays usable behind it; the
     mock had no scrim and neither does this. Escape and the close button still
     close it -- see oneClose(). */
  background: transparent !important;    /* takes back rgba(2,3,6,.96) */
  pointer-events: none;
}
.one-backdrop.lucid-dock .one-surface[data-one-face="lucid"]{
  pointer-events: auto;
  width: 100% !important;        /* takes back min(620px, 100vw - 32px) */
  max-width: none !important;    /* takes back 620px */
  margin: 0 !important;          /* takes back 14px 0 0 24px */
  /* Anchored at the bottom with the height following its content IS the
     growth. There is no animation to write: a taller box whose bottom edge is
     fixed can only grow upward. */
  height: auto;
  min-height: 68px;
  max-height: min(60vh, calc(100vh - 120px));
  border-radius: 16px;
}

/* THE RECTANGLE IS THE THREAD AND THE QUESTION LINE. NOTHING ELSE.
   The crown, the "Ask / One question. Everything you own." column and the two
   section headings are the old panel's furniture. The mock's growpanel held a
   thread and a line, and that is what Matt approved. */
.one-backdrop.lucid-dock .one-rule,
.one-backdrop.lucid-dock .one-ask,
.one-backdrop.lucid-dock .one-go > .one-h{ display: none; }

/* THE COST LINE IS GONE FROM THE DOCK. It is the owner's API spend and it has
   no business under the question line of a product people are meant to use.
   The number is not lost: diagnostics draws "What Lucid cost to run - the last
   N days" in full, which is where a bill belongs. */
.one-backdrop.lucid-dock #askAllowance{ display: none; }

/* THE CROWN COMES BACK WHEN YOU REACH FOR IT. Earlier, Changes, New, the
   shortcut hint and the close button are real controls; hiding them outright
   would make them unreachable. Collapsed to nothing at rest, restored on
   hover, so the resting rectangle is the mock's and nothing is lost. */
.one-backdrop.lucid-dock .one-crown{
  opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0;
  overflow: hidden; pointer-events: none;
  transition: opacity .18s ease, max-height .18s ease;
}
.one-backdrop.lucid-dock .one-surface:hover .one-crown,
.one-backdrop.lucid-dock .one-surface:focus-within .one-crown{
  opacity: 1; max-height: 52px; padding-top: 12px; padding-bottom: 8px;
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce){
  .one-backdrop.lucid-dock .one-crown{ transition: none; }
}

/* THE QUESTION LINE GOES LAST. .one-surface is a flex column whose children
   are grain, crown, rule, field, duo -- so the field is fourth and the thread
   fifth, which is why the box you type in sat above the answers. order moves
   it without touching oneBuild()'s markup. */
.one-backdrop.lucid-dock .one-crown{ order: 1; }
.one-backdrop.lucid-dock .one-duo{
  order: 4; grid-template-columns: 1fr; min-height: 0; overflow: auto;
  margin: 14px 18px 0;
}
.one-backdrop.lucid-dock .one-field{ order: 9; margin: 0 18px 14px; }
.one-backdrop.lucid-dock.dock-resting .one-duo{ display: none; }
.one-backdrop.lucid-dock.dock-resting .one-field{ margin: 14px 18px; }

/* THE GLOW RIM, on the rectangle, which is how Matt described it. Same conic,
   same --ac-ring-ang @property and same acRingSpin as the omnibox ring: one
   spin applied to a second element, not a second copy of the idea. */
.one-backdrop.lucid-dock .one-surface.ac-thinking::after{
  content:''; position:absolute; inset:0; border-radius:inherit; padding:2px;
  background:conic-gradient(from var(--ac-ring-ang),
    var(--ac1), var(--ac2), var(--ac3), var(--ac4), var(--ac1));
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite:exclude;
  animation:acRingSpin 1.8s linear infinite;
  pointer-events:none; z-index:9;
}
@media (prefers-reduced-motion: reduce){
  .one-backdrop.lucid-dock .one-surface.ac-thinking::after{ animation:none; }
}

/* Under 780px the app has no sidebar and a bottom bar to clear. */
@media (max-width: 780px){
  .one-backdrop.lucid-dock{ padding: 0 12px 96px 12px; }
}



/* THE THREAD FOLLOWS THE ACCENT — 14 October, decision 1. One colour picker
   (the accent: 13 presets and Adjust). lucid-thread.js stamps
   data-lucid="accent" on the root, and the thread's three stops are taken from
   the accent the theme or the picker has set. Last in the file so it outranks
   the per-theme stops above, which were each theme's own guess at the same
   thing. The hot core is the accent's emphatic stop lifted toward white. */
:root[data-lucid="accent"]{
  --th-base: var(--accent);
  --th-mid: var(--accent-2, var(--accent));
  --th-hot: color-mix(in srgb, var(--accent-2, var(--accent)) 34%, white);
}


/* ==========================================================================
   EVERY WAY TO REACH THEM — 14 October 2026.
   Every email, phone and postal address on a card, each with a label and
   Current / Previous, edited in place (renderContactReach in app.js). Built on
   the .cd-addr rows of 5 October so it reads as the same list it replaces.
   The older one-value rows are hidden only once this list has loaded.
   ========================================================================== */
.cd-reach-box.has-reach > .info-row,
.cd-reach-box.has-reach .ce-legacy-reach{ display:none; }
.cd-reach-host{ display:flex; flex-direction:column; gap:var(--space-2); }
.cd-reach-host:empty{ display:none; }
/* The verbs float over the right end of the row on hover instead of taking a
   line of their own - a list of nine values should read as nine lines, not
   eighteen. On a touch screen (no hover) they sit on their own line below. */
.cd-reach-tag{ flex:none; font-size:var(--type-label); padding:0 var(--space-2);
  border-radius:var(--radius-pill);
  border:1px solid color-mix(in srgb, var(--text) 16%, transparent);
  color:color-mix(in srgb, var(--text) 70%, transparent); }
/* The verbs are always visible on these rows on a phone (hover:none, above),
   and drop under the value rather than squeezing it. */

/* The merge screen's "what the survivor will hold" list, one line per kind. */
.dup-reach-line{ display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--space-1) var(--space-3); }
.dup-reach-kind{ flex:none; min-width:64px; font-size:var(--type-label); letter-spacing:.1em;
  text-transform:uppercase; color:color-mix(in srgb, var(--text) 50%, transparent); }
.dup-reach-item{ display:inline-flex; align-items:baseline; gap:var(--space-1); overflow-wrap:anywhere; }
.dup-reach-item.is-past{ color:color-mix(in srgb, var(--text) 50%, transparent); }
.dup-reach-state{ background:none; border:0; padding:0; font:inherit; font-size:var(--type-label);
  cursor:pointer; text-decoration:underline dotted;
  color:color-mix(in srgb, var(--th-mid) 94%, transparent); }


.cal-family .cal-chip.active{ background:color-mix(in srgb, var(--pink) 18%, transparent);
  border-color:color-mix(in srgb, var(--pink) 50%, transparent); }
.cal-family .cal-family-add{ font-weight:600; padding:var(--space-2) var(--space-3); }
.fam-form{ display:flex; flex-direction:column; gap:var(--space-2); margin-top:var(--space-2); }
.fam-form .field{ margin:0; }
.fam-row{ display:flex; gap:var(--space-2); flex-wrap:wrap; }
/* The date and the two times are replaced by Lucid's own pickers
   (lucidEnhanceScope), so the row sizes those wrappers, not the inputs. */
.fam-row > *{ flex:1 1 7rem; min-width:0; }
.fam-row > :first-child{ flex-basis:100%; }
.fam-row > .lucid-field .from-btn{ width:100%; }
.fam-check{ display:inline-flex; align-items:center; gap:var(--space-2); font-size:var(--type-body); color:var(--text-dim); }
.fam-who{ display:flex; flex-wrap:wrap; gap:var(--space-3); }
.fam-line{ display:flex; align-items:center; gap:var(--space-3); }
.fam-line > .lucid-field{ flex:1; min-width:0; }
.fam-line > .lucid-field .from-btn{ width:100%; }
.fam-members{ display:flex; flex-direction:column; gap:var(--space-2); margin:var(--space-2) 0; }
.fam-member{ display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap;
  padding:var(--space-2) var(--space-3); border:1px solid var(--glass-border); border-radius:var(--radius-card);
  background:var(--glass-bg); }
.fam-member-who{ flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--space-1); }
.fam-member-name{ flex:1; min-width:0; overflow-wrap:break-word; font-size:var(--type-body); color:var(--text); }
.fam-member-who .fam-member-name{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fam-member-role{ font-size:var(--type-label); color:var(--text-faint); }
.fam-invite{ display:flex; gap:var(--space-2); margin-top:var(--space-2); }
.fam-invite .field{ flex:1; min-width:0; margin:0; }
/* 14 October — EVERY COMPOSE MENU OPENS ON SCREEN. See placeMenuToFit in
   app.js. A .glass box whose content is already taller than the box keeps its
   own scroll while a menu is open, rather than taking the app-wide
   overflow:visible above and spilling its scrolled-away footer (More, Send,
   Discard) out under itself and off the bottom of the window. !important
   because that rule is written four different ways at four specificities. */
.glass.menu-keeps-scroll:not(.sidebar){ overflow-y:auto !important; overflow-x:hidden !important; }

.kit-fam-tag, .bc-fam-tag{ display:inline-flex; align-items:center; font-size:var(--type-label); color:var(--text-dim);
  padding:var(--space-1) var(--space-2); border-radius:var(--radius-pill);
  background:color-mix(in srgb, var(--pink) 14%, transparent); }
/* SHARE LINK PANEL — 14 Oct (OUTSIDERS). Labels, the two access choices and
   the faint email beside an answer. Nothing here is new colour. */
.sh-panel .field{ width:100%; margin-top:var(--space-1); }
.sh-panel textarea.field{ resize:vertical; }
.sh-lab{ display:block; color:var(--text-faint); font-size:var(--type-label); font-weight:700;
  text-transform:uppercase; letter-spacing:.06em; margin-top:var(--space-3); }
.sh-opt{ display:flex; align-items:flex-start; gap:var(--space-2); margin-top:var(--space-2);
  font-size:var(--type-body); color:var(--text); cursor:pointer; }
.sh-opt input{ margin-top:var(--space-1); flex:none; }
.sh-faint{ color:var(--text-faint); }
/* The layer over the panel (POLISH 3c): one above #dlgBackdrop (120), so it
   paints and takes the pointer over the Share link panel it was opened from. */
#shLayer{ z-index:121; }
.sh-when{ display:flex; flex-wrap:wrap; gap:var(--space-2); align-items:center; }
.sh-when .field{ width:auto; flex:1 1 9em; min-width:0; }

/* ==========================================================================
   THE CONVERSATION SITS ABOVE THE BOX (Matt, 14 Oct, approved).
   No conversation: the tray is unchanged -- the box, then "One question." and
   the three questions under it. Once he asks, the three questions step aside,
   the box moves to the foot of the sheet, and every question and answer stacks
   above it, oldest at the top, newest nearest the box. The sheet grows with
   the conversation, pushing the box down, until it reaches its max-height;
   from then on the box stays at the bottom and the older lines scroll up.
   Only the order and the anchoring change: the mark, the thread, the colours,
   the sheet's size and its place in the top left are untouched.
   ========================================================================== */
.one-surface[data-one-face="lucid"].one-talking{
  display:flex !important;
  flex-direction:column !important; }
.one-surface[data-one-face="lucid"].one-talking > .one-crown{ order:0; flex:none; }
.one-surface[data-one-face="lucid"].one-talking > .one-rule{ order:1; flex:none; }
.one-surface[data-one-face="lucid"].one-talking > .one-duo{
  order:2;
  flex:0 1 auto !important;
  min-height:0 !important;
  display:flex !important;
  flex-direction:column !important;
  overflow:hidden !important; }
.one-surface[data-one-face="lucid"].one-talking > .one-field{
  order:3; flex:none;
  margin-top:var(--space-3) !important;
  margin-bottom:0 !important; }
.one-surface[data-one-face="lucid"].one-talking .one-ask{ display:none !important; }
.one-surface[data-one-face="lucid"].one-talking .one-go{
  flex:0 1 auto !important;
  min-height:0 !important;
  display:flex !important;
  flex-direction:column !important;
  overflow:hidden !important; }
/* Typing a follow-up must not hide the conversation it follows. */
.one-surface[data-one-face="lucid"].one-talking .one-go .chat-card{
  display:flex !important;
  flex-direction:column !important;
  flex:0 1 auto !important;
  min-height:0 !important; }
.one-surface[data-one-face="lucid"].one-talking .one-go .chat-thread{
  flex:0 1 auto !important;
  min-height:0 !important;
  overflow-y:auto !important; }

/* ==========================================================================
   LEDGER FOR LUCID, FOLIO FOR HIM — Matt, 14 October, after two rounds of
   demos: "I like ledger for lucid text and folio for user text." … "I just
   confirmed it". The spec is the demo's `.b-pick` set.

   Wherever the Ask conversation renders (the tray, the phone Ask card, a
   conversation reopened from the history drawer) it is the one #chatThread,
   so these rules restyle the messages there and nothing else. The thread's
   order, its anchoring above the box and its scrolling are untouched.

   LUCID (Ledger): no card and no block. A "LUCID" label in the mono face with
   a brass hairline running to the right edge, list lines as ruled rows (a 1px
   rule between rows, none after the last), and the time under the answer.
   HIM (Folio): plain text with a 2px blue edge on the left, right-aligned as a
   block and left-aligned inside it, no bubble and no gold, and the time under.

   The label and the time are pseudo-elements fed by data-at (appendMsg), so
   the text of a message is still exactly what was said. The !important
   matches the older bubble rules it replaces, which use it.
   ========================================================================== */
/* The brass is the tray's own (.one-surface's --one-brass pair), restated for
   the phone Ask card and any other place the thread renders outside it. */
:root .chat-thread{ gap:var(--space-5);
  --msg-brass:var(--one-brass, #C79A54); --msg-brass-hi:var(--one-brass-hi, #E8CB92); }
:root .chat-thread .msg{ position:relative; font-family:var(--font-body); }

/* -- Lucid ------------------------------------------------------------------ */
:root .chat-thread .msg.msg-assistant{
  align-self:stretch; max-width:100%;
  background:none !important; border:0 !important; border-radius:0 !important;
  box-shadow:none !important; padding:0 !important; color:var(--text); }
:root .chat-thread .msg.msg-assistant::before{
  content:"Lucid"; display:block; margin:0 0 var(--space-2);
  font-family:var(--font-mono); font-size:var(--type-label); font-weight:500;
  line-height:1.2; letter-spacing:.14em; text-transform:uppercase; white-space:nowrap;
  color:var(--msg-brass-hi);
  /* The hairline: one background, sized to start a gap after the word and run
     to the right edge. The word is five monospace letters at .74em each. */
  background:linear-gradient(90deg,
      color-mix(in srgb, var(--msg-brass) 55%, transparent), transparent)
    no-repeat right center / calc(100% - 4.7em) 1px; }
:root .chat-thread .msg-assistant .msg-row{
  display:block; padding:var(--space-1) 0;
  border-bottom:1px solid color-mix(in srgb, var(--text) 10%, transparent); }
:root .chat-thread .msg-assistant .msg-row:not(:has(+ .msg-row)){ border-bottom:0; }
/* The bullet that started the line stays in the text, unseen: the rule is the
   bullet now. Same for the dash after a leading time. */
:root .chat-thread .msg-assistant .msg-dot,
:root .chat-thread .msg-assistant .msg-sep{
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; }
:root .chat-thread .msg-assistant .msg-t,
:root .chat-thread .msg-assistant .msg-n{
  display:inline-block; min-width:5.6em; color:var(--text-dim);
  font-variant-numeric:tabular-nums; white-space:nowrap; }
:root .chat-thread .msg-assistant .msg-n{ min-width:1.6em; }

/* -- Him -------------------------------------------------------------------- */
:root .chat-thread .msg.msg-user{
  align-self:flex-end; max-width:75%; text-align:left; font-weight:400;
  color:var(--text) !important; border:0 !important; border-radius:0 !important;
  border-left:2px solid var(--blue) !important;
  background:none !important; box-shadow:none !important;
  padding:var(--space-hair) 0 var(--space-hair) var(--space-3) !important; }

/* -- The time, under both --------------------------------------------------- */
:root .chat-thread .msg[data-at]::after{
  content:attr(data-at); display:block; margin-top:var(--space-1);
  font-family:var(--font-body); font-size:var(--type-label); font-weight:400;
  line-height:1; letter-spacing:0; text-transform:none; white-space:nowrap;
  color:var(--text-faint); }
:root .chat-thread .msg.msg-user[data-at]::after{ text-align:right; }

/* -- What sits with an answer keeps its place, without a card around it ------ */
:root .chat-thread .typing{ background:none; padding:var(--space-1) 0; }
:root .chat-thread .msg-assistant .msg-speak{ margin-left:var(--space-1); }

/* Paper. The demo's .light values say the intent: a darker brass and a darker
   blue. The blue is already the theme's own; the brass word is mixed into the
   paper's ink, the same 45% the "Everything you own." accent uses above. */
:root[data-mode="light"] .chat-thread .msg.msg-assistant::before{
  color:color-mix(in srgb, var(--msg-brass) 45%, var(--text)); }
/* At a desk the conversation reads at the demo's size (15/16px there; the
   scale's lead step here). The phone keeps the size it has: phone type sizes
   belong to another piece of work, and this change is the face and the shape. */
@media (min-width:701px){
  :root .chat-thread .msg{ font-size:var(--type-lead); line-height:1.55; }
}
/* LOOK UP IN LUCID (15 Oct) — the sheet a bookmarklet opens, in the dialog
   shell (dlgOpen), and its card in Settings › Connections. lookupSheet() in
   app.js writes this markup. A row is the same face, name and dim second
   line as a contact row; the one that is not there yet has a dashed edge. */
.lkup{ display:flex; flex-direction:column; gap:var(--space-3); white-space:normal; }
.lkup-page{ display:flex; gap:var(--space-2); align-items:baseline; min-width:0;
  font-size:var(--type-label); color:var(--text-dim); }
.lkup-host{ flex:none; font-weight:var(--weight-strong); color:var(--text); }
.lkup-title{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lkup-row{ -webkit-appearance:none; appearance:none; display:flex; align-items:center; gap:var(--space-3);
  width:100%; box-sizing:border-box; text-align:left; cursor:pointer; font:inherit; color:var(--text);
  background:transparent; border:1px solid var(--glass-border); border-radius:var(--radius-card);
  padding:var(--space-2) var(--space-3); }
.lkup-row + .lkup-row{ margin-top:calc(-1 * var(--space-1)); }
.lkup-row:hover{ border-color:var(--glass-border-hi); }
.lkup-row:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.lkup-row.is-ghost{ cursor:default; border-style:dashed; }
.lkup-row.is-ghost .lkup-av{ background:transparent; border:1px dashed var(--glass-border-hi); color:var(--text-dim); }
.avatar.lkup-av{ width:40px; height:40px; flex:none; font-size:var(--type-label); }
.lkup-words{ display:flex; flex-direction:column; gap:var(--space-hair); min-width:0; flex:1; }
.lkup-name{ font-weight:var(--weight-strong); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lkup-meta{ font-size:var(--type-label); color:var(--text-dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lkup-go{ flex:none; font-size:var(--type-label); font-weight:var(--weight-strong); color:var(--accent); }
.lkup-sub{ font-size:var(--type-label); color:var(--text-dim); font-weight:var(--weight-strong); }
.lkup-why, .lkup-read{ margin:0; font-size:var(--type-label); color:var(--text-dim); overflow-wrap:anywhere; }
.lkup-read{ color:var(--text-faint); }
.lkup-err{ margin:0; font-size:var(--type-label); color:var(--pink); }
.lkup-form{ display:grid; grid-template-columns:1fr 1fr; gap:var(--space-2) var(--space-3); }
.lkup-label{ display:flex; flex-direction:column; gap:var(--space-1); font-size:var(--type-label); color:var(--text-dim); }
.lkup-label .field{ margin:0; }
@media (max-width:600px){ .lkup-form{ grid-template-columns:1fr; } }
.lkup-bm{ cursor:grab; text-decoration:none; }
textarea.field.lkup-code{ width:100%; box-sizing:border-box; resize:none; font-family:var(--font-mono);
  font-size:var(--type-label); overflow-wrap:anywhere; }@media (max-width: 780px), (hover: none) and (pointer: coarse) and (max-height: 500px){
  :root{
    --type-label: 13px;
    --type-body: 17px;
    --type-lead: 19px;
    --type-title: 22px;
    --press-scale: .97;
    --press-filter: brightness(.86);
    /* How long a press takes to land on the phone. Between the house's 60ms
       and --dur-fast's 130: quick enough to be under the finger, long enough
       to be seen as a movement rather than a jump. */
    --dur-press: 80ms;
  }
  :root[data-mode="light"]{ --press-filter: brightness(.86); }

  /* 3. NO WAIT, AND NO RUBBER BAND. */
  html{ touch-action: manipulation; }
  :where(button, a[href], summary, label, input, select, textarea, [onclick],
         [role="button"], [role="tab"], [role="link"], [role="menuitem"], [role="switch"],
         .btn, .chip, .bn-item, .bn-more-item){ touch-action: manipulation; }
  html, body{ overscroll-behavior: none; }

  .mstrip-btn, .mstrip-icon{ height:44px; min-width:44px; }
  

  /* 5. WHAT THE BIGGER TYPE PUSHED OFF THE EDGE, put back.
     The Ask tray's header is one row of orb, title, three chips and the x.
     At 13px chips it is about 397px wide in a 326px header, so the x, the
     only way to close the tray, went off the right edge. The x is pinned to
     the header's top right corner (a 48 square, where it was drawn), and the
     chips wrap under the title, right-aligned. Nothing is hidden.
     The Ask box below it: an <input> is as wide as its own size attribute
     times the font, so at 17px it pushed the voice and send buttons off the
     card. It gives way to them now (min-width:0), as a flex child should.
     An open mail's sender: one long address no longer runs off the pane. */
  .chat-header{ position:relative; flex-wrap:wrap; row-gap:var(--space-2);
    padding-right:calc(var(--space-4) + var(--space-12)); }
  /* The title takes the rest of its line, so the three chips always go
     down together, and sit together at the right, rather than one staying
     up beside the title and two spreading out below it. */
  .chat-header .chat-title{ flex:1 0 60%; }
  .chat-header .chat-clear{ margin-left:0; }
  .chat-header .chat-title + .chat-clear{ margin-left:auto; }
  .chat-header .chat-close{ position:absolute; top:var(--space-2); right:var(--space-2);
    width:var(--space-12); height:var(--space-12); }
  .chat-input{ min-width:0; }
  .rp-name{ min-width:0; overflow-wrap:anywhere; }
}
/* THE MONTH ON A 375 PHONE. Seven 44px days and six 1px gaps need 314px, and
   the left panel's own 20px padding left the grid 301, so each day hung over
   its neighbour and a finger on the edge of the 1st got the 2nd. Below 380px
   the panel gives back 8px a side; the days keep their 44. */


/* MANROPE'S LINES ARE TALLER, SO "NORMAL" IS WRITTEN DOWN. 14 October.
   A line set at line-height:normal is as tall as the face says: 1.175 of the
   size in Public Sans, 1.366 in Manrope (both read from the font files). Every
   rule here that never set a line-height grew by a sixth when the face changed:
   measured on Mail, each row 6-12px taller and one fewer message on screen.
   Words did not wrap differently; the space between lines did. The old
   rhythm is kept by naming it once, here, instead of in a hundred rules. Form
   controls are reset to normal by the browser's own font shorthand, so they
   are told to inherit it too. */
body{ line-height:1.18; }
button, input, select, textarea{ line-height:inherit; }

/* THE TIME PICKER — key punch, three dials, a few chips. 15 October.
   Matt: "the clock picker sucks. (scroll forever)". The popover that was 96
   quarter-hour rows in a 44vh scroll (lucidEnhanceTimeInput, app.js) is now a
   typed box with its reading shown live, an hour dial, a minute dial and an
   AM/PM pair, then four one-tap chips. Built from what is already here: the
   box is .lp-time-type, the chips are .chip, Set is .btn.primary, Cancel is
   .lp-cal-action, the figures are Outfit at --type-figure. No new colours. */
.from-menu.lp-time-menu{ min-width:272px; width:272px; padding:var(--space-3); max-height:none; overflow-y:auto; }
.lp-time-menu .lp-tp-punch{ margin-bottom:var(--space-3); }
.lp-time-menu .lp-time-type{ width:100%; margin:0; }
.lp-tp-read{ min-height:16px; margin-top:var(--space-1); padding:0 var(--space-1); font-size:var(--type-label); font-weight:600; color:var(--text-faint); }
.lp-tp-read.is-good{ color:var(--text); }
.lp-tp-dials{ display:flex; align-items:center; justify-content:center; gap:var(--space-2); padding:var(--space-1) 0 var(--space-3); border-bottom:1px solid var(--glass-border); }
.lp-tp-dial{ display:flex; flex-direction:column; align-items:stretch; width:64px; }
.lp-tp-step{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:var(--space-hair); min-height:36px; padding:0; border:none; background:none; color:var(--text-faint); font-family:var(--font-display); font-size:var(--type-body); cursor:pointer; border-radius:var(--radius-control); }
.lp-tp-step:hover{ color:var(--text); background:rgba(120,120,140,.10); }
.lp-tp-ghost{ line-height:1; }
.lp-tp-val{ display:flex; align-items:center; justify-content:center; box-sizing:border-box; width:100%; height:52px; margin:var(--space-1) 0; padding:0; text-align:center; font-family:var(--font-display); font-size:var(--type-figure); font-weight:500; font-variant-numeric:tabular-nums; color:var(--text); background:rgba(120,120,140,.08); border:1px solid var(--glass-border); border-radius:var(--radius-control); cursor:ns-resize; outline:none; }
.lp-tp-val:focus{ border-color:color-mix(in srgb, var(--accent) 50%, transparent); box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
input.lp-tp-val{ cursor:text; }
.lp-tp-val.is-bad{ color:var(--text-faint); }
.lp-tp-colon{ font-family:var(--font-display); font-size:var(--type-figure); font-weight:500; color:var(--text-faint); line-height:1; padding-bottom:var(--space-1); }
.lp-tp-ampm{ display:flex; flex-direction:column; gap:var(--space-1); margin-left:var(--space-1); padding:var(--space-1); border-radius:var(--radius-control); outline:none; }
.lp-tp-ampm:focus-visible{ box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 50%, transparent); }
.lp-tp-half{ width:52px; min-height:40px; border:1px solid var(--glass-border); border-radius:var(--radius-control); background:none; color:var(--text-faint); font-family:var(--font-display); font-size:var(--type-body); font-weight:600; letter-spacing:.04em; cursor:pointer; }
.lp-tp-half:hover{ color:var(--text); }
.lp-tp-half.on{ color:var(--text); background:color-mix(in srgb, var(--accent) 14%, transparent); border-color:var(--glass-border-hi); }
.lp-tp-chips{ display:flex; flex-wrap:wrap; gap:var(--space-2); padding:var(--space-3) 0; }
.from-menu.lp-time-menu .lp-tp-chip{ display:inline-flex; align-items:center; border:1px solid var(--glass-border); border-radius:var(--radius-pill); padding:var(--space-1) var(--space-3); background:none; font-family:var(--font-body); font-size:var(--type-body); }
.from-menu.lp-time-menu .lp-tp-chip:last-child{ border-bottom:1px solid var(--glass-border); }
.lp-tp-foot{ display:flex; align-items:center; justify-content:space-between; gap:var(--space-2); padding-top:var(--space-3); border-top:1px solid var(--glass-border); }
.lp-tp-foot .lp-cal-action{ border:none; background:none; font-family:var(--font-body); }
.lp-tp-done{ padding:var(--space-2) var(--space-4); font-variant-numeric:tabular-nums; }
/* A finger, not a pointer: every target 44px. */
@media (pointer:coarse), (max-width:760px){
  .lp-tp-step{ min-height:44px; }
  .lp-tp-half{ min-height:44px; }
  .from-menu.lp-time-menu .lp-tp-chip{ min-height:44px; padding:0 var(--space-3); }
  .lp-tp-foot .lp-cal-action, .lp-tp-done{ min-height:44px; }
  .lp-time-menu .lp-time-type{ min-height:44px; font-size:var(--type-lead); }
}
/* Light mode lets every .from-menu show 3% of the page through it (the note by
   :root[data-mode="light"] .from-menu). Behind a list that is invisible; behind
   this one — big figures over a sheet full of labels — the labels read through
   the dials. Solid --bg-mid here only. */
:root[data-mode="light"] .from-menu.lp-time-menu{ background:var(--bg-mid); }

.jobs-list{ list-style:none; margin:var(--space-2) 0; padding:0; display:grid; gap:var(--space-1); }
.jobs-row{ display:grid; grid-template-columns:minmax(0, 11em) minmax(0, 1fr); gap:var(--space-3);
  padding:var(--space-2) var(--space-3); border-radius:var(--radius-tight);
  border:1px solid var(--glass-border); font-size:var(--type-body); }
.jobs-row-name{ font-weight:600; color:var(--text); }
.jobs-row-say{ color:var(--text-dim); }
.jobs-row-err{ display:block; color:var(--text); margin-top:var(--space-1); }
.jobs-row[data-state="failed"], .jobs-row[data-state="stopped"], .jobs-row[data-state="missed"]{
  border-color:color-mix(in srgb, var(--cat-red) 60%, transparent);
  background:color-mix(in srgb, var(--cat-red) 8%, transparent); }
.jobs-row[data-state="failed"] .jobs-row-name, .jobs-row[data-state="stopped"] .jobs-row-name,
.jobs-row[data-state="missed"] .jobs-row-name{ color:var(--cat-red-text, var(--cat-red)); }
.jobs-row[data-state="waiting"] .jobs-row-say{ color:var(--text); }

.help-form{ display:grid; gap:var(--space-2); margin:var(--space-3) 0; max-width:var(--form-measure, 40em); }
.help-label{ font-size:var(--type-label); font-weight:600; color:var(--text-dim); }
.help-kind{ max-width:20em; }
.help-body{ width:100%; box-sizing:border-box; resize:vertical; min-height:7em; font:inherit; }
.help-acts{ display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap; }
.help-sent, .support-list{ list-style:none; margin:var(--space-2) 0; padding:0; display:grid; gap:var(--space-1); }
.help-sent-row{ display:grid; grid-template-columns:auto minmax(0, 1fr) auto; gap:var(--space-3);
  font-size:var(--type-body); color:var(--text-dim); }
.help-sent-body{ color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.support-row{ padding:var(--space-2) var(--space-3); border:1px solid var(--glass-border);
  border-radius:var(--radius-tight); font-size:var(--type-body); }
.support-row[data-status="new"]{ border-color:color-mix(in srgb, var(--blue) 55%, transparent); }
.support-head{ display:flex; gap:var(--space-2); align-items:baseline; flex-wrap:wrap; }
.support-kind, .support-when{ color:var(--text-dim); font-size:var(--type-label); }
.support-body{ white-space:pre-wrap; margin:var(--space-1) 0 var(--space-2); color:var(--text); }
.support-acts{ display:flex; gap:var(--space-2); }

/* ---------------------------------------------------------------------------
   THE PAYWALL — 15 October (task PAY). Drawn inside the ordinary dialog card
   (dlgOpen), so it inherits its glass, fonts and colours; these two rules only
   space the body. No new colours: the offer rows reuse .fam-member.
   --------------------------------------------------------------------------- */
.pw-body{ display:flex; flex-direction:column; gap:var(--space-2); max-width:520px; }
.pw-lead{ margin:0; font-family:var(--font-display); font-size:var(--type-lead); font-weight:600; color:var(--text); }

/* =========================================================================
   THE SPINE: CALM ON HOVER, ONE GRID, A REAL PIN.   19 October.
   =========================================================================
   Matt, with a screenshot of bundle 430: "The reaction when I hover is weird
   and jerky. Feels like it flies open and shutters shut." / "It looks a mess.
   Disjointed, uneven, nothing inline." / "The tray button sucks."

   THE GRID. Every row in the spine -- nav rows, New, Needs attention, Settings,
   the Lucid door -- has the same height, the same 14px in to its icon, a 20px
   icon, the same gap to its label and the same 14px in on the right where the
   counts end. The icon column is 48px wide and the closed spine is 48px plus its
   padding, so a row's icon is at the SAME x closed and open: opening the spine
   moves nothing but the edge, and the words arrive into the room made for them.
   Groups are said with a hairline and a small-caps word, never with an indent.

   THE MOTION. Closed, the spine is an absolutely positioned panel that takes
   --spine-w-closed of the page (an .app spacer) and no more. Hover adds .peek,
   which animates ONE property -- width -- on that overlay, so the page never
   reflows. The words fade in after the panel is about 60% open and fade out
   before it starts to close, so text never squeezes or wraps. Pinned (no
   .collapsed) it is in the flow and pushes the page; that switch is instant.
   ========================================================================= */
:root{
  --spine-w-closed: calc(var(--space-12) + var(--space-3) * 2);
  --spine-w-open: calc(var(--space-16) * 3 + var(--space-10));
}
#sidebar{
  --spine-in: calc((var(--space-12) - var(--space-5)) / 2);            /* icon column: 14px in */
  --spine-label-x: calc(var(--spine-in) + var(--space-5) + var(--space-3)); /* where every label starts */
  --spine-ms: 180ms;
  --spine-ease: var(--ease-out);
  width:var(--spine-w-open); padding:var(--space-4) var(--space-3);
}
#sidebar.collapsed{
  position:absolute; top:var(--space-4); bottom:var(--space-4); left:var(--space-4);
  margin:0; z-index:60; width:var(--spine-w-closed); padding:var(--space-4) var(--space-3);
  transition:width var(--spine-ms) var(--spine-ease) 70ms;           /* closing: after the words are gone */
}
#sidebar.collapsed.peek{ width:var(--spine-w-open); box-shadow:var(--elev-floating); transition:width var(--spine-ms) var(--spine-ease); }
.app::before{ content:''; flex:none; width:calc(var(--spine-w-closed) + var(--space-4)); }
.app:has(> #sidebar:not(.collapsed))::before{ display:none; }
@media (max-width:780px){ .app::before{ display:none; } }

/* The words: never display:none, so nothing reflows. Closed they are clear and
   inert; opening, they fade in once the panel is ~60% open (108ms of 180);
   closing, they fade out first (the width waits 70ms for them). */
#sidebar :is(.nav-label, .accounts, .spine-section, .sf-text, .nav-n:not(.spine-badge), .nav-kbd, .brand-tagline){
  opacity:1; visibility:visible; transition:opacity 90ms linear 108ms, visibility 0s;
}
#sidebar.collapsed:not(.peek) :is(.nav-label, .accounts, .spine-section, .sf-text, .nav-n:not(.spine-badge), .nav-kbd, .brand-tagline){
  opacity:0; visibility:hidden; pointer-events:none; transition:opacity 60ms linear, visibility 0s linear 60ms;
}
@media (prefers-reduced-motion: reduce){
  #sidebar, #sidebar.collapsed, #sidebar.collapsed.peek,
  #sidebar :is(.nav-label, .accounts, .spine-section, .sf-text, .nav-n, .nav-kbd, .brand-tagline){ transition:none !important; }
}

/* THE HEAD: the mark on the icon column, the word on the label column, the
   tagline under the word, the pin at the far end -- and only while open. */
#sidebar .sidebar-head{ padding:0 0 var(--space-3); margin-bottom:var(--space-2); justify-content:flex-start; align-items:flex-start; }
#sidebar .brand-block{ flex:1; min-width:0; gap:0; }
#sidebar .brand-door{
  height:var(--space-10); width:100%; box-sizing:border-box; margin:0;
  padding:0 var(--spine-in) 0 calc(var(--spine-in) - var(--space-hair)); gap:calc(var(--space-3) - var(--space-hair));
  justify-content:flex-start;
}
#sidebar .brand-lens, #sidebar.collapsed .brand-lens{ --m:var(--space-6); }
#sidebar .brand-tagline{ margin:calc(var(--space-1) * -1) 0 0 var(--spine-label-x); }
.spine-pin{
  flex:none; display:flex; align-items:center; justify-content:center; cursor:pointer;
  width:var(--space-8); height:var(--space-8); margin:var(--space-1) var(--space-1) 0 0; padding:0;
  border:0; background:transparent; border-radius:var(--radius-control); color:var(--rail-ink-3);
}
.spine-pin svg{ width:var(--space-4); height:var(--space-4); }
.spine-pin:hover{ background:var(--wash-soft); color:var(--rail-ink); }
.spine-pin:focus-visible{ outline:2px solid var(--accent); outline-offset:-2px; }
.spine-pin[aria-pressed="true"]{ color:var(--accent); background:var(--wash-on); }
.spine-pin[aria-pressed="true"] svg{ fill:currentColor; fill-opacity:.25; }
#sidebar.collapsed:not(.peek) .spine-pin{ position:absolute; }   /* out of the head's flow while closed */

/* ONE ROW. */
#sidebar :is(.nav-item, .global-fab-wrap.in-spine .global-fab, .spine-attn-collapsed, .theme-toggle){
  box-sizing:border-box; display:flex; flex-direction:row; align-items:center; justify-content:flex-start;
  gap:var(--space-3); width:100%; height:var(--space-10); margin:0 0 1px;
  padding:0 var(--spine-in); border-radius:var(--radius-control);
  font-family:var(--font-body); font-size:var(--type-body); line-height:1;
}
#sidebar :is(.nav-icon, .global-fab > svg, .theme-toggle > svg, .spine-attn-collapsed > svg){
  width:var(--space-5); height:var(--space-5); flex:none; opacity:.72; stroke-width:1.6;
}
#sidebar .nav-item.active .nav-icon{ opacity:1; }
#sidebar :is(.nav-item, .global-fab, .spine-attn-collapsed, .theme-toggle) > .nav-label:not(.nav-n):not(.nav-kbd):not(.dot){
  flex:1 1 auto; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:clip; text-align:left;
}
/* THE COUNT COLUMN: the same right edge on every row, tabular, right-aligned. */
#sidebar :is(.nav-n, .nav-kbd, .nav-item .dot, .spine-badge){ margin-left:auto; flex:none; text-align:right; font-variant-numeric:tabular-nums; }
#sidebar .nav-item .dot + .nav-n, #sidebar .nav-item .nav-n + .dot{ margin-left:var(--space-2); }
/* THE NEW BUTTON keeps its brass outline, drawn inside so it costs no pixel of the grid. */
#sidebar .global-fab-wrap.in-spine{ margin:0 0 var(--space-2); }
#sidebar .global-fab-wrap.in-spine .global-fab{
  border:0; box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--brass) 34%, transparent);
}
#sidebar .global-fab-wrap.in-spine .global-fab:hover{ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--brass) 70%, transparent); }
#sidebar .global-fab-wrap.in-spine .global-fab > svg{ stroke-width:2; opacity:1; }
#sidebar .global-fab-wrap.in-spine .gf-label{ flex:1 1 auto; text-align:left; }

/* GROUP HAIRLINE + SMALL-CAPS WORD. Every small-caps heading (Areas, Accounts, Tasks) starts at the icon's left edge. */
.spine-rule{
  margin:var(--space-2) 0 var(--space-1); padding:var(--space-2) var(--spine-in) 0 var(--spine-in);
  height:var(--space-6); border-top:1px solid var(--glass-border); box-sizing:border-box;
  font-size:var(--type-label); font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--rail-ink-3);
  white-space:nowrap; line-height:1;
}
/* ACCOUNTS / TASKS sit on the label column too, and stay in the flow while closed. */
#sidebar .accounts, #sidebar .spine-section{ margin-top:var(--space-3); }
#sidebar .accounts-label{ padding-left:var(--spine-in); padding-right:var(--spine-in); }   /* headings start at the icon's left edge */
#sidebar .spine-section.grow{ display:none; }   /* its heading is the Needs attention row in the foot now */

/* THE FOOT: Needs attention (triangle, count in the count column), then
   Settings, on the same grid; the small print under them on the label column. */
#sidebar .sidebar-foot{
  margin:auto 0 0; padding:var(--space-3) 0 0; gap:0; align-items:stretch; width:100%;
  border-top:1px solid var(--glass-border);
}
#sidebar .spine-attn-collapsed{ order:1; position:relative; cursor:pointer; margin-top:0; }
#sidebar .sidebar-foot-icons{ order:2; width:100%; align-items:stretch; gap:0; }
#sidebar .sf-text{ order:3; padding-left:var(--spine-label-x); margin-top:var(--space-2); }
#sidebar .sf-build{ order:4; margin-top:0; }
#sidebar .theme-toggle{ background:transparent; color:var(--rail-ink-2); transition:background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
#sidebar .theme-toggle:hover{ background:var(--wash-soft); color:var(--rail-ink); box-shadow:none; }
#sidebar .theme-toggle:focus-visible{ outline:2px solid var(--accent); outline-offset:-2px; }
#sidebar .spine-attn-collapsed:hover{ background:var(--wash-soft); }
#sidebar .spine-attn-collapsed:focus-visible{ outline:2px solid var(--accent); outline-offset:-2px; }
/* The alerts count: a plain figure in the count column while open; while closed
   it has no room for a figure, so it is the small badge on the triangle. */
#sidebar .spine-badge{
  position:static; width:auto; height:auto; background:none; box-shadow:none; border-radius:0;
  color:var(--rail-ink-3); font-size:var(--type-body); font-weight:500;
}
#sidebar .spine-attn-collapsed.has-overdue .spine-badge{ background:none; box-shadow:none; color:var(--cat-red-2, #ff6b5e); font-weight:700; }
:root[data-mode="light"] #sidebar .spine-attn-collapsed.has-overdue .spine-badge{ background:none; color:var(--cat-red); }
#sidebar.collapsed:not(.peek) .spine-badge{
  position:absolute; top:var(--space-1); right:var(--space-1); min-width:var(--space-4); height:var(--space-4);
  display:flex; align-items:center; justify-content:center; border-radius:var(--radius-round);
  background:var(--blue); color:var(--ink); font-size:var(--type-label); font-weight:800;
}
#sidebar.collapsed:not(.peek) .spine-attn-collapsed.has-overdue .spine-badge{ background:var(--cat-red, #b8382e); color:#fff; }
#sidebar .spine-toggle{ letter-spacing:.05em; padding-right:var(--spine-in); }
#sidebar .spine-toggle .spine-count{ margin-left:var(--space-1); }
