/* LUCID — THE HOME ROOM'S STYLES (view-home).
 *
 * Moved out of app.css on 15 October 2026 by the room rebuild. Loaded straight
 * after app.css (see the room-css:start marker in index.html), in the same
 * order the rules had there, so the cascade is the one it was. What changed in
 * the move: rules that were stacked on the same selector with nothing between
 * them that could disagree are folded into one, rules for classes nothing
 * renders any more are gone, and the comments that explain a decision came
 * with their rules. tests/test-room-home-unchanged.js is the proof that nothing
 * on screen changed. The tile vocabulary other rooms also draw with (.ht-empty,
 * .ht-line, .ht-time, .ht-verb, .ht-why and friends) stays in app.css.
 */

/* Brass on paper is a mid-tone, lighter than the six accents, and the paper
     ink measured 4.0:1 on it straight. It is deepened toward the paper's own
     darkest neutral -- still visibly brass -- rather than given a dark letter,
     which is the 3.1:1 it had. */
:root[data-mode="light"] .hs-tog button[aria-pressed="true"]{
    background:color-mix(in srgb, var(--brass) 72%, var(--neutral-2));
    color:var(--on-accent); }

/* ==========================================================
     HOME — the front door. SECOND PASS: MONOCHROME WITH GLOW.
     ----------------------------------------------------------
     The first version shipped correct and looked like a control panel: eight
     equal rounded rectangles in a four-column grid, each carrying its own
     coloured chip. The verdict on it was four things at once — too busy, too
     many colours; the grid of equal boxes feels cheap; the typography looks
     like a web app; and, pulling the other way, it is not rich enough.

     Those are not four faults, they are one fault seen from two sides.
     "Too busy" and "not rich enough" only contradict each other if richness is
     assumed to come from HUE. It does not. The resolution is to spend
     restraint on hue and richness on LIGHT:

       · ONE accent on the whole page, not six. Nothing here uses colour to say
         what KIND of thing it is. Type weight, size and position do that job,
         and they do it without turning the page into a fruit salad.

       · DEPTH COMES FROM GLOW AND AIR, NOT FROM BORDERS. The tiles lose their
         frames completely. What separates one from the next is thirty pixels
         of nothing and a hairline that runs off the end of its label. A framed
         box says "widget"; an unframed column of type says "page".

       · THE PAGE HAS A LEAD. A full-bleed band across the top — off the edges
         of the page rather than inside a card — carries the greeting, the one
         thing most worth doing, and the count of what is outstanding set in
         the serif. Everything below it is subordinate. A grid with no lead is
         what made the first version read as a dashboard.

       · SPANS ARE TWELFTHS, NOT QUARTERS, so rows run 7/5 and 5/7 rather than
         a chessboard. The eye follows a line down the page instead of scanning
         a matrix. This is the single change that kills "grid of equal boxes".

       · URGENCY IS LUMINOSITY. A card that matters glows a little at its left
         edge. It does not turn red. In light mode a glow is invisible, so the
         same signal is carried by ink weight instead — see the guard below.

     Layout is still an ORDERED LIST with per-tile spans, not absolute x/y.
     A real 2D packing model demos better and falls apart on a phone; an
     ordered flow collapses to one column for free and can never leave a hole
     in the middle of the page.
     ========================================================== */

  /* ---- the band: the only full-bleed element in the app ---------------- */
  /* Negative margins cancel .main's padding exactly, and now they do it by
     reading the padding rather than by restating it: this said -30px while
     .main had moved to --space-8, so the one full-bleed element in the app was
     two pixels short of the window on each side. The override below still
     handles the phone, where the band is not full-bleed at all. */
  /* 160 — this used to run 38/32 top/bottom padding with a 28px gap before the
     lead and a 34px gap before the count, which put roughly 450px between the
     top of the page and the first tile on a normal window: the greeting was
     the whole opening act before anything useful began. Matt: shorten it
     without making it look amputated — so the trim is spread across every gap
     in the band rather than taken out of one (which would read as a deleted
     element) and nothing here lost a pixel of type size: the sky, the
     greeting, the lead and the count are exactly as bold as they were, just
     closer together. */
.home-band {
  position: relative;
  margin: calc(var(--space-1) * -1) calc(var(--main-pad-x) * -1) var(--space-5);
  overflow: hidden;
}

/* AND THE ONE MOMENT IT MUST NOT CLIP. The board switch rolled up into a
     chevron (item 272) and its menu drops out of a control near the top of the
     band; the band is 192px tall on a normal window and the menu wants about
     150 of them from a button that ends at 61 — so overflow:hidden cropped the
     bottom of the list. That is the "menu longer than the card it drops out of"
     bug .from-menu already had once, and it is worse here than there: a Calm
     that is painted nowhere is a control that looks like it works and doesn't.
     Same answer .glass gives, and the pools it was clipping are clipped by
     their own clip-path below instead, so nothing escapes when this lifts. */
.home-band:has(.st-drop.open){ overflow:visible; }

/* THE ONE LESSON FROM THE LIGHT-MODE READABILITY BUG, APPLIED IN ADVANCE.
     An absolutely positioned ::before with no z-index paints ABOVE every
     non-positioned child — that is exactly what made .glass unreadable. So the
     glow is pinned to z-index 0 and every child of the band is lifted to 1.
     Do not remove either half. */
.home-band::before{
    /* The pools sit INSIDE the band. The first attempt hung them 80% above it
       with the band clipping at overflow:hidden, so the bright centre of every
       one of them was cropped off and all that showed was the faint tail. */
    content:''; position:absolute; left:-18%; right:-10%; top:-55%; height:205%;
    background:
      radial-gradient(38% 52% at 24% 52%, color-mix(in srgb, var(--head-glow, var(--accent-glow)) 88%, transparent), transparent 82%),
      radial-gradient(30% 42% at 58% 76%, color-mix(in srgb, var(--head-glow, var(--accent-glow)) 46%, transparent), transparent 80%),
      radial-gradient(52% 64% at 90% 14%, color-mix(in srgb, var(--head-tint, var(--accent-2, var(--accent))) 20%, transparent), transparent 80%);
    /* Masked out before the clip. Without this the pools meet overflow:hidden
       at full strength and the band ends on a ruled edge of light, which reads
       as a rectangle drawn in blue rather than as a light source. */
    -webkit-mask-image:linear-gradient(180deg, #000 0, #000 54%, transparent 92%);
    mask-image:linear-gradient(180deg, #000 0, #000 54%, transparent 92%);
    /* THE POOLS CLIP THEMSELVES, so the band does not have to.
       They hang outside it on every side — 18% left, 10% right, 55% above,
       50% below — and until now the only thing keeping them off the rest of the
       page was .home-band{overflow:hidden}, which also cropped anything else
       that dared leave the box. The board chevron's menu is now one of those
       things, so the band lifts its clip while a menu is open (rule above) and
       the pools have to hold their own edge.
       These four numbers are that box expressed in this element's own
       percentages, since its geometry is fixed percentages of the band:
         width  = 128% of the band  → the band starts at 18/128 = 14.06% in
                                      and ends 10/128 = 7.81% from the right
         height = 205% of the band  → it starts at 55/205 = 26.83% down
                                      and ends 50/205 = 24.39% from the bottom
       Which is the same rectangle overflow:hidden was cutting, to the pixel —
       so nothing about the closed state changed. */
    clip-path:inset(26.83% 7.81% 24.39% 14.06%);
    pointer-events:none; z-index:0;
  }

/* On paper, a coloured wash is a stain rather than a light source, so the
     same three pools are dropped to a hint. Light mode gets its depth from the
     hairline and the air instead — see the heat rule further down. */
:root[data-mode="light"] .home-band::before{ opacity:.30; }

/* Lite mode exists to stop paying for blur and for animation loops. A static
     radial gradient is neither — it is painted once and costs nothing to keep,
     and hiding it was removing the only thing that makes the front door look
     like anything. It stays, at half strength. */
:root[data-fx="lite"] .home-band::before{ opacity:.55; }

/* THE FRONT DOOR ON A MATTE GROUND — one lamp, off-page, in the metal.
     The three pools above are painted from --accent-glow and --accent-2, and a
     matte theme has neither: the glow token is transparent and --accent-2 is
     whatever the selected account's palette says, which on an oxblood page
     came out as a large lilac cloud in the top-right corner. It was the single
     most out-of-place object on the new grounds and it was the FIRST thing
     visible on opening the app.
     One warm pool replaces the three, well away from the middle, at a strength
     that reads as a room having a light in it rather than as a graphic. Same
     idea Regal's bloom is built on; same reason. */
:root[data-finish="matte"] .home-band::before{
    background:
      radial-gradient(50% 62% at 88% 8%, color-mix(in srgb, var(--brass) 10%, transparent), transparent 78%),
      radial-gradient(40% 54% at 16% 62%, color-mix(in srgb, var(--brass) 5%, transparent), transparent 82%);
  }

.home-band > *{ position:relative; z-index:1; }

.home-band::after{
    content:''; position:absolute; left:30px; right:30px; bottom:0; height:1px; z-index:1;
    background:linear-gradient(90deg, var(--glass-border-hi), transparent 72%);
  }

/* ==========================================================
     C14 — THE LIVING SKY
     ==========================================================
     Matt, on Home: "All of these sound good. Motion would be great. (love to
     see the weather idea) Bigger, bolder, colorful, anything to lift this from
     a board to an experience."

     WHAT IT IS. A weather-and-time layer inside the masthead, painted from the
     real reading that the Weather card and the Today page already use — see
     paintHomeSky(). Two attributes drive everything below:

       data-sky = dawn | day | dusk | night     — from the clock and sunset
       data-wx  = sunny | partly | cloudy | rain | storm   — from the reading

     WHY IT IS NOT DECORATION. Motion for its own sake is a screensaver and gets
     old in a week. This moves because the weather moves: clouds actually cross
     when it is cloudy, rain actually falls when it is raining, and the colour
     of the band at 6am is not the colour of the band at 9pm. It tells you
     something before you have read a single word, which is what separates an
     experience from an animated board.

     WHAT IT COSTS. Nothing per view and nothing per month. Every layer is a
     gradient or a blurred ellipse moved by a transform, so it is all on the
     compositor — no canvas, no timer, no request. The weather it reads was
     already being fetched.

     AND WHEN IT MUST STOP. Two switches turn all of it off, and both are
     honoured rather than dimmed: the operating system's reduced-motion
     setting, and Lucid's own Lite effects mode, which exists precisely so a
     tired machine can stop paying for animation. In both cases the colour
     stays and only the movement goes — the information is in the colour. */
.home-sky{
    position:absolute; inset:0; overflow:hidden; pointer-events:none;
    /* Above the accent pools, below every child of the band. .home-band > *
       sets z-index 1 on direct children, and this is a direct child, so it
       needs a rule of its own to sit underneath the type. */
    z-index:0;
    /* Fades out downwards so the band still ends on air rather than on a line
       of weather — the same reasoning as the mask on .home-band::before. */
    -webkit-mask-image:linear-gradient(180deg, #000 0, #000 58%, transparent 97%);
    mask-image:linear-gradient(180deg, #000 0, #000 58%, transparent 97%);
  }

/* THE WASH. Four times of day, and they are genuinely different colours
     rather than four opacities of one. Painted on the layer itself so the
     clouds and the rain sit in front of it. */
.home-sky::before{
    content:''; position:absolute; inset:0;
    background:linear-gradient(160deg,
      color-mix(in srgb, var(--sky-a, var(--accent)) 34%, transparent) 0%,
      color-mix(in srgb, var(--sky-b, var(--accent-2, var(--accent))) 20%, transparent) 46%,
      transparent 88%);
    transition:background var(--dur-ambient) var(--ease-out);
  }

.home-band[data-sky="dawn"] .home-sky{ --sky-a:#ff9e6d; --sky-b:#7f6bd0; --sky-orb:#ffcf9a; --sky-far:#3a2b4e; }

.home-band[data-sky="day"] .home-sky{ --sky-a:#57b6f5; --sky-b:#8fd8e8; --sky-orb:#ffe6a3; --sky-far:#2a3f5e; }

.home-band[data-sky="dusk"] .home-sky{ --sky-a:#ff7b54; --sky-b:#5a4a9e; --sky-orb:#ff9a6a; --sky-far:#2a1c34; }

/* On paper a full-strength coloured sky is a stain rather than a light
     source — the same call already made for the accent pools above at .30 and
     for Lite mode at .55, and made here for the same reason rather than for a
     contrast one. The readability problem the sky caused was real and is fixed
     where it belongs: see the ground under .home-boards. */
:root[data-mode="light"] .home-sky{ opacity:.55; }

:root[data-mode="light"] .home-sky::before{ opacity:.34; }

/* THE SUN, OR THE MOON. One disc; which one it is, is a colour and a
     position. It breathes rather than pulses — 14 seconds, and the amplitude is
     small enough that you notice it only if you look. */
.sky-orb{
    position:absolute; right:9%; top:-14%; width:190px; height:190px; border-radius:var(--radius-round);
    background:radial-gradient(circle at 50% 50%,
      color-mix(in srgb, var(--sky-orb, #ffd28a) 82%, transparent) 0%,
      color-mix(in srgb, var(--sky-orb, #ffd28a) 30%, transparent) 42%,
      transparent 70%);
    filter:blur(2px);
    /* TWO TERMS, MULTIPLIED, not one value set twice. --orb-wx is what the
       WEATHER says (a sun behind cloud is dimmer) and --orb-mood is how loud
       the board wants the sky to be. Setting opacity outright in either place
       means whichever rule is written last silently wins — which is exactly
       what happened when the mood block was added and the sun stopped
       dimming in the rain at all. */
    opacity:calc(var(--orb-wx, .85) * var(--orb-mood, 1));
    animation:skyBreathe 14s var(--ease-in-out) infinite;
  }

/* Overcast and worse: the sun is behind it, so it is a glow rather than a
     disc. Not hidden — a sky with no light source in it reads as a mistake. */
.home-band[data-wx="cloudy"] .sky-orb,
  .home-band[data-wx="rain"] .sky-orb,
  .home-band[data-wx="storm"] .sky-orb{ --orb-wx:.30; filter:blur(16px); }

/* TRANSFORM ONLY, AND THAT IS NOT A STYLE PREFERENCE.
     This used to breathe opacity from .85 to 1 as well, and an animated
     property beats every static rule for that property no matter how specific
     — so the overcast rule above, which drops the sun to .30 behind the cloud,
     had no effect whatsoever: the keyframes simply overwrote it sixty times a
     second. The sun stayed exactly as bright in a thunderstorm as in clear sky.
     Caught by measuring the computed opacity per condition rather than reading
     the rule and assuming. Scale is not a property anything else here sets, so
     it can be animated safely. */
@keyframes skyBreathe{
    0%,100%{ transform:scale(1); }
    50%{ transform:scale(1.06); }
  }

/* THE CLOUDS. Three, at three sizes, three speeds and three heights, because
     three identical shapes moving together is a conveyor belt rather than a
     sky. They cross the full width and are placed off-screen at both ends, so
     there is no moment where one appears out of nothing. */
.sky-cloud{
    position:absolute; border-radius:var(--radius-round); pointer-events:none; opacity:0;
    background:radial-gradient(ellipse at 50% 55%,
      color-mix(in srgb, var(--sky-cloud, #ffffff) 40%, transparent) 0%,
      color-mix(in srgb, var(--sky-cloud, #ffffff) 14%, transparent) 52%,
      transparent 76%);
    filter:blur(10px);
    transition:opacity var(--dur-ambient) var(--ease-out);
  }

.sky-cloud.c1{ width:420px; height:150px; top:6%;  left:-30%; animation:skyDrift 82s linear infinite; }

.sky-cloud.c2{ width:300px; height:110px; top:34%; left:-30%; animation:skyDrift 128s linear infinite -34s; }

.sky-cloud.c3{ width:560px; height:180px; top:-8%; left:-30%; animation:skyDrift 168s linear infinite -96s; }

/* How many are visible IS the forecast. Clear sky, no clouds; partly, one;
     overcast, all three at full weight. */
.home-band[data-wx="partly"] .sky-cloud.c1{ opacity:.55; }

.home-band[data-wx="partly"] .sky-cloud.c2{ opacity:.30; }

.home-band[data-wx="cloudy"] .sky-cloud,
  .home-band[data-wx="rain"] .sky-cloud,
  .home-band[data-wx="storm"] .sky-cloud{ opacity:.72; }

@keyframes skyDrift{ from{ transform:translateX(0); } to{ transform:translateX(190vw); } }

/* ===================================================================
     A WINDOW, NOT A COLOURED BAND.

     Asked which of three the home page should be — a mantelpiece, a window,
     or a beautiful dashboard — he answered WINDOW: weather, sky, the world
     outside.

     What was already here was the WEATHER: a gradient that changes with the
     hour, a sun that dims behind cloud, rain, lightning. All true, none of it
     a window — because a window has DISTANCE in it, and every layer above sits
     on one plane.

     Three layers put the distance in, and they are the three cheapest ways to
     do it in a strip two hundred pixels tall:

       far     something at the bottom that is a long way off
       stars   only when it is actually dark enough to see them
       birds   something alive crossing it, only when the sky is clear

     Each is one element. All three are transform and opacity — no repaint, no
     canvas — and each switches off in the weather that would hide it, which is
     the part that keeps it honest rather than decorative.
     =================================================================== */

  /* THE FAR DISTANCE. Deliberately shapeless: a ridge line, not a skyline. A
     recognisable city would be a claim about where he is, and he is in Texas,
     not wherever I drew. A soft dark horizon reads as "a long way off" without
     asserting anything. */
.sky-far{
    position:absolute; left:-4%; right:-4%; bottom:0; height:34%;
    pointer-events:none; z-index:1;
    background:
      radial-gradient(120% 100% at 18% 118%, color-mix(in srgb, var(--sky-far, #0b1024) 92%, transparent), transparent 72%),
      radial-gradient(90% 88% at 52% 126%, color-mix(in srgb, var(--sky-far, #0b1024) 86%, transparent), transparent 70%),
      radial-gradient(140% 96% at 88% 122%, color-mix(in srgb, var(--sky-far, #0b1024) 90%, transparent), transparent 74%);
    filter:blur(2px); opacity:.72;
  }

.home-band[data-sky="night"] .home-sky {
  --sky-a: #2b3a7a;
  --sky-b: #101736;
  --sky-orb: #cfd8ff;
  --sky-cloud: #8ea0d8;
  --sky-far: #080c1e;
}

/* In weather you cannot see through, the distance goes first — which is what
     actually happens, and is a better cue than any amount of extra grey. */
.home-band[data-wx="rain"] .sky-far,
  .home-band[data-wx="storm"] .sky-far{ opacity:.34; filter:blur(6px); }

/* STARS. One element, eight dots at irregular positions so it is not a
     visible grid, and a slow drift so they are not pinholes punched in card.
     ONLY AT NIGHT, and only when the sky is clear enough to have any — stars
     through overcast is the sort of detail that makes everything around it
     look invented. */
.sky-stars{
    position:absolute; inset:0; pointer-events:none; opacity:0; z-index:1;
    background-image:
      radial-gradient(1.4px 1.4px at 12% 22%, #fff, transparent 60%),
      radial-gradient(1px 1px at 31% 58%, #dfe6ff, transparent 60%),
      radial-gradient(1.6px 1.6px at 47% 14%, #fff, transparent 60%),
      radial-gradient(1px 1px at 63% 42%, #cfd8ff, transparent 60%),
      radial-gradient(1.2px 1.2px at 78% 27%, #fff, transparent 60%),
      radial-gradient(1px 1px at 88% 62%, #dfe6ff, transparent 60%),
      radial-gradient(1.3px 1.3px at 22% 74%, #fff, transparent 60%),
      radial-gradient(1px 1px at 57% 79%, #cfd8ff, transparent 60%);
    transition:opacity var(--dur-ambient) var(--ease-out);
    animation:skyTwinkle 7s var(--ease-in-out) infinite;
  }

.home-band[data-sky="night"] .sky-stars{ opacity:.85; }

.home-band[data-sky="dusk"]  .sky-stars{ opacity:.34; }

.home-band[data-wx="cloudy"] .sky-stars,
  .home-band[data-wx="rain"] .sky-stars,
  .home-band[data-wx="storm"] .sky-stars{ opacity:0; }

@keyframes skyTwinkle{
    0%,100%{ opacity:1; }
    50%    { opacity:.62; }
  }

/* BIRDS. "birds when it is clear" — his words, and the only layer here that
     is alive rather than weather. Three, at three heights and three speeds,
     because three identical marks moving together is a formation and this is
     meant to be incidental.
     A chevron drawn with one border: no image, no glyph, and it survives at
     seven pixels where anything with detail in it turns to mush. */
.sky-birds{ position:absolute; inset:0; pointer-events:none; opacity:0;
    transition:opacity var(--dur-ambient) var(--ease-out); z-index:2; }

.home-band[data-sky="day"]  .sky-birds,
  .home-band[data-sky="dawn"] .sky-birds{ opacity:.5; }

.home-band[data-wx="cloudy"] .sky-birds,
  .home-band[data-wx="rain"] .sky-birds,
  .home-band[data-wx="storm"] .sky-birds{ opacity:0; }

.sky-birds > i{
    position:absolute; width:7px; height:3px; left:-6%;
    border-top:1.5px solid color-mix(in srgb, var(--sky-far, #2a3f5e) 78%, transparent);
    border-radius:50% 50% 0 0 / 100% 100% 0 0;
    animation:skyBird linear infinite;
  }

.sky-birds > i:nth-child(1){ top:26%; animation-duration:64s; animation-delay:-8s; }

.sky-birds > i:nth-child(2){ top:34%; animation-duration:86s; animation-delay:-40s; }

.sky-birds > i:nth-child(3){ top:21%; animation-duration:73s; animation-delay:-22s; }

@keyframes skyBird{
    from{ transform:translateX(0) translateY(0) rotate(-8deg); }
    to  { transform:translateX(118vw) translateY(-14px) rotate(-8deg); }
  }

/* Cheap effects and reduced motion: the layers stay, the movement stops. A
     still sky with distance in it is still a window; an empty band is not. */
:root[data-fx="lite"] .sky-stars,
  :root[data-fx="lite"] .sky-birds > i{ animation:none; }

@media (prefers-reduced-motion: reduce) {
  .sky-stars, .sky-birds > i{ animation:none; }
}

/* WHAT IS FALLING. One repeating gradient panel, translated diagonally and
     tiled — a hundred individual raindrop elements would be a hundred layers
     for a texture that reads as one. Hidden unless it is actually raining. */
.sky-fall{
    position:absolute; inset:-30% -10% -10%; opacity:0; pointer-events:none;
    background-image:repeating-linear-gradient(74deg,
      color-mix(in srgb, var(--sky-rain, #cfe4ff) 40%, transparent) 0 1px,
      transparent 1px 9px);
    background-size:auto 120px;
    animation:skyFall 0.85s linear infinite;
    transition:opacity var(--dur-ambient) var(--ease-out);
  }

.home-band[data-wx="rain"] .sky-fall{ opacity:.22; }

.home-band[data-wx="storm"] .sky-fall{ opacity:.34; animation-duration:.6s; }

@keyframes skyFall{ from{ background-position:0 0; } to{ background-position:-34px 120px; } }

/* A STORM IS A THING THAT HAPPENS OCCASIONALLY, NOT A STROBE.
     Nine seconds apart, two frames long, and never at full white — the point
     is that you catch it out of the corner of your eye once in a while. */
.sky-flash{ position:absolute; inset:0; opacity:0; background:rgba(226,236,255,.5); pointer-events:none; }

.home-band[data-wx="storm"] .sky-flash{ animation:skyFlash 9s steps(1,end) infinite; }

/* The two brightnesses are variables so the Work setting can dim the flash
     without needing a second set of keyframes — see the mood block below. */
@keyframes skyFlash{
    0%, 96%, 100%{ opacity:0; }
    96.6%{ opacity:var(--flash-hi, .30); }
    97.2%{ opacity:0; }
    97.8%{ opacity:var(--flash-lo, .18); }
    98.4%{ opacity:0; }
  }

/* ---- HOW LOUD THE WEATHER IS -----------------------------------------
     "I like that. Subtle during work (but very noticable) and full blast on
      play."  (Matt, 26 August, on how far the weather mood should go.)

     One attribute, data-mood, set from the board — see paintHomeSky(). There
     is deliberately no second code path and no second set of layers: the sky
     is already all gradients and blurred ellipses, so the entire difference
     between the two settings is the multipliers below.

     WORK IS NOT OFF. He said noticeable, and a weather layer you have to go
     looking for is the same as not having one — the whole argument for it was
     that it tells you something before you have read a word. So Work keeps
     every layer, keeps the colour at full strength, and takes the MOTION down:
     the clouds move at half speed, the rain is lighter, the storm flashes
     once in a while rather than every nine seconds.

     PLAY IS THE ONE THAT WAS BUILT. These are the values that shipped, which
     is why the play block below mostly restates rather than raises — the
     original was already full blast and what was missing was a quieter one to
     sit beside it. */
.home-band[data-mood="work"] .sky-cloud{ animation-duration:150s; }

.home-band[data-mood="work"] .sky-cloud.c2{ animation-duration:210s; }

.home-band[data-mood="work"] .sky-cloud.c3{ animation-duration:290s; }

.home-band[data-mood="work"] .sky-orb{ animation-duration:26s; --orb-mood:.73; }

.home-band[data-mood="work"][data-wx="rain"]  .sky-fall{ opacity:.13; animation-duration:1.4s; }

.home-band[data-mood="work"][data-wx="storm"] .sky-fall{ opacity:.20; animation-duration:1.0s; }

/* Four times as long between flashes, and dimmer. Once every thirty-six
     seconds is "there is a storm out there"; once every nine is a notification. */
.home-band[data-mood="work"][data-wx="storm"] .sky-flash{ animation-duration:36s; }

.home-band[data-mood="work"] .sky-flash{ --flash-hi:.16; --flash-lo:.09; }

/* Play: the amplitude the layer was designed at, stated rather than assumed,
     so that changing the Work values later cannot silently change Play too. */
.home-band[data-mood="play"] .sky-orb{ --orb-mood:1; }

.home-band[data-mood="play"] .sky-flash{ --flash-hi:.30; --flash-lo:.18; }

/* THE TWO SWITCHES. Colour stays, movement goes — in both cases the sky is
     still telling the truth about the weather, it has just stopped moving. */

@media (prefers-reduced-motion: reduce) {
  .sky-orb, .sky-cloud, .sky-fall, .sky-flash{ animation:none !important; }
}

:root[data-fx="lite"] .sky-orb,
  :root[data-fx="lite"] .sky-cloud,
  :root[data-fx="lite"] .sky-fall,
  :root[data-fx="lite"] .sky-flash{ animation:none !important; }

:root[data-fx="lite"] .sky-cloud{ filter:blur(6px); }

.home-band-top {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
}

/* THE SECOND HALF OF THE LESSON THREE RULES UP. `.home-band > *` puts EVERY
     direct child at z-index 1, which makes each of them a stacking context of
     its own — so the board chevron's menu, at z-index 22 inside .home-band-top,
     can still only reach 1 against .home-band-main, and .home-band-main comes
     later in the document and therefore paints over it. It looked right in a
     screenshot and was not: elementFromPoint on "Play" and "Calm" returned the
     greeting row underneath, which is the definition of a control that looks
     like it works and doesn't. One number above its sibling, only while a menu
     is open, so nothing about the closed masthead changes. */
.home-band-top:has(.st-drop.open){ z-index:3; }

.home-head-acts{ display:flex; align-items:center; gap:var(--space-2); flex-wrap:wrap; }

/* TEN-PIXEL UPPERCASE IS NOT ENOUGH INK FOR --text-faint. Measured, not
     judged: the date line came back at 4.35:1 against a 4.5 floor on dark, and
     the kicker at 4.28:1. Tracked-out capitals put less ink on the page than
     their size suggests, so every label at this size on Home takes --text-dim.

     …AND --text-dim WAS STILL NOT ENOUGH, for a reason the first pass missed:
     these two sit on the HERO, and the hero is not the page background. The
     sky behind it is painted per weather and time of day, and it is lighter
     than --bg-base — so a colour chosen against the flat background loses a
     third of a point of contrast the moment it is put somewhere else. Measured
     again on 26 August: 3.45:1 on regal, 3.75 on dark, 3.85 on obsidian, 3.86
     on midnight, 3.99 on frosted. Every dark theme, none of the light ones,
     which is exactly the fingerprint of "the thing behind it is not what I
     measured against".

     So they take --text. At ten pixels, uppercase, tracked out to nearly a
     fifth of an em, that is still unmistakably a label rather than a heading —
     the recession comes from the size and the spacing, which is where it
     should have come from in the first place. */
  /* ONE LINE'S WORTH OF ROOM, HELD WHETHER THE LINE IS THERE YET OR NOT. The
     date is written by refreshGreetings() now rather than by renderHome(), so
     in practice it is in place before anything below the band has a height at
     all — but the band must not depend on that ordering to stay still. 1lh is
     this element's own line box, so it is exactly the height the date takes at
     any font scale, and there is no number here to go stale. */
.home-date{ font-size:var(--type-label); font-weight:600; letter-spacing:.17em; text-transform:uppercase;
    color:var(--text); margin:0; min-height:1lh; flex:none; }

/* ---- the lead and the count were both here --------------------------- */
  /* THE HOME REBUILD TOOK BOTH HOSTS AWAY.            (25 September.)
     .home-lead* dressed #homeLead, written by renderHomeLead(); .home-stat*
     dressed #homeStats, written by renderHomeStats(). Neither element is in
     index.html any more and neither function exists, so every one of these
     rules matched nothing — about sixty lines of type sizes, contrast
     decisions and a visibility rule that could never fire. They are deleted
     rather than left as a comment because a rule that selects nothing reads
     as if it were doing something.

     Where the two things went, and what dresses them now:
       the lead   -> the hero's suggestion, .hero-suggest / -k / -t / -w
       the count  -> the last chips on the lane strip, .lane.lane-stat
     The contrast reasoning that used to sit on .home-lead-why now belongs to
     .hero-suggest-w, and it was measured there when the hero was built. */

  /* ---- boards: Go and Calm are just the first two ---------------------- */
  /* A CONTROL OVER A MOVING BACKGROUND NEEDS A GROUND OF ITS OWN.
     These tabs are 12px at --text-dim with nothing behind them, which was
     marginal against the flat band and became unreadable once the weather sky
     went in underneath: test-readability.js measured "Calm" at 2.74:1 against a
     floor of 4.5 in dark, and at 3.62:1 in serene:mist:cool. Every theme, same
     cause.
     Dimming the sky was the first attempt and it is the wrong fix — it makes
     the number pass by making the feature weaker, and it has to be re-tuned for
     every theme separately. A ground fixes it once: at 86% of the page's own
     background the sky contributes about a seventh of what is behind the
     letters, in any theme, whatever the weather is doing. It also reads as
     what it is, which is a segmented control.

     The ground moved to the chevron button when the row rolled up — see
     .home-head-acts .st-drop-btn below. It is the same 86% mix for the same
     reason; the row itself now lives inside a solid menu, where it needs no
     ground at all. */
.home-boards{ display:flex; align-items:center; gap:var(--space-hair); flex-wrap:wrap;
    background:color-mix(in srgb, var(--bg-base) 86%, transparent);
    border:1px solid var(--glass-border); border-radius:var(--radius-card); padding:var(--space-1); }

/* EVERY RULE HERE CARRIES .home-boards, and it is not decoration.
     The tabs gained `chip` so foldChipRow() would recognise them — see
     renderHomeBoards() — and .chip is declared further up this file, so a bare
     .hb-tab would tie with it on specificity and LOSE the border, which .chip
     sets and .hb-tab does not. The ancestor is what keeps a tab a tab. It is
     the same move .cal-filter .cal-chip had to make for the same reason. */
.home-boards .hb-tab{ font-size:var(--type-body); font-weight:600; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill);
    cursor:pointer; border:none; background:none;
    color:var(--text-dim); white-space:nowrap; display:inline-flex; align-items:center;
    transition:color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out); }

.home-boards .hb-tab:hover{ color:var(--text); background:color-mix(in srgb, var(--text) 8%, transparent); }

.home-boards .hb-tab.active{ color:var(--text); background:color-mix(in srgb, var(--text) 11%, transparent); }

:root:not([data-mode="light"]) .home-boards .hb-tab.active{
    box-shadow:0 0 0 1px color-mix(in srgb, var(--accent) 38%, transparent), 0 0 18px -3px var(--accent-glow); }

.hb-del{ margin-left:var(--space-2); margin-right:-3px; font-size:var(--type-body); line-height:1; color:var(--text-faint); }

.hb-del:hover{ color:var(--cat-red-text); }

/* ---- work / life, rolled up ------------------------------------------- */
  /* "work/play/calm (on hero/home) should be chevron roll up." — 3 September,
     the same request the calendar pills got the same day.

     Same fold, so there is one dropdown in Lucid rather than four that look
     alike; all this adds is what a masthead needs that a settings pane does
     not. .st-drop is built for a full-width column control — 100% wide button,
     14px of margin under it — and here it is one item in a row of buttons over
     a moving sky.

     THE ONE THING THAT MUST NOT BE LOST is which board he is on: the closed
     button says "Work", "Play" or "Calm", never a generic word, because on Home
     that is load-bearing. stDropSync() keeps it there. */
.home-head-acts .st-drop{ margin-bottom:0; flex:none; }

.home-head-acts .st-drop-btn{ width:auto; min-width:118px; gap:var(--space-2);
    font-size:var(--type-body); font-weight:600; padding:var(--space-2) var(--space-2) var(--space-2) var(--space-4); border-radius:var(--radius-card);
    /* THE SAME GROUND THE ROW ITSELF NEEDED, AND FOR THE SAME MEASUREMENT.
       .st-drop-btn's own background is rgba(120,120,140,.08), which is nearly
       nothing, and this button sits over the weather. The 86% mix is what got
       "Calm" from 2.74:1 to a pass in every theme; moving the control must not
       quietly give that back. */
    background:color-mix(in srgb, var(--bg-base) 86%, transparent); }

.home-head-acts .st-drop-btn:hover{ border-color:var(--glass-border-hi);
    background:color-mix(in srgb, var(--bg-base) 94%, transparent); }

/* Left-aligned under the button rather than stretched to it: the button is
     only as wide as the longest board name and the menu wants room for
     "+ New board". */
.home-head-acts .st-drop-menu{ right:auto; min-width:186px; }

/* One board per line. Out in the open the row is a wrapping segmented control
     with a ground of its own, and it must be neither of those things inside a
     186px menu that already has a ground. */
.st-drop-menu .home-boards{ flex-direction:column; align-items:stretch; flex-wrap:nowrap;
    gap:var(--space-1); background:none; border:none; border-radius:0; padding:0; }

.st-drop-menu .home-boards .hb-tab{ border-radius:var(--radius-control); padding:var(--space-2) var(--space-3); }

/* The plus is the last line of a list, not a tab at the end of a row. */
.st-drop-menu .home-boards .hb-add{ margin-top:var(--space-1); padding-top:var(--space-2);
    border-top:1px solid var(--glass-border); border-radius:0 0 var(--radius-control) var(--radius-control); color:var(--text-faint); }

/* ---- the grid -------------------------------------------------------- */
  /* row dense IS THE THING THAT CLOSES THE HOLES, AND HERE IS THE ARITHMETIC.
     Measured at 1481x690 on the real default board, three tracks of 411px:
     row 3 held two one-column tiles and left column 3 empty, row 4 held one
     two-column tile and left column 3 empty - 411px of nothing in each, 822px
     of interior hole on one screen. Neither was a styling fault. With the
     default row flow the placement cursor only ever moves forward, so a
     two-column tile arriving with one column free cannot fit, gets pushed to
     the next row, and the column it skipped stays empty for good. The valid
     packings of three columns out of spans of 1 and 2 are 1+1+1, 1+2 and 2+1,
     and NOTHING made his order add up to one of those - which is exactly the
     complaint: the tiles do not tessellate and he has to find an order that
     happens to work by trial and error.
     dense lets the cursor go back and fill a skipped column with a later
     tile that fits. It is the one-line answer to "without gaps". It is also a
     LAYOUT JUDGEMENT, Matt's not mine: the cost is that a tile can appear one
     place earlier than its saved position when it is the one that fits a hole.
     Delete the word dense and the old behaviour is back, holes and all. */
/* Home's tiles are arranged by hand and span a twelve-column grid, so their
   widths are Matt's to set and are not touched here. The grid it sits in is
   given the same ceiling as every other page, so a wide monitor stops making
   every tile wider and starts leaving margin instead. */
.home-grid{ display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
    grid-auto-flow:row dense;
    gap:var(--space-8) var(--space-8); align-items:start; max-width:var(--page-measure); }

@media(max-width:1180px) {
  .home-grid {
    gap: var(--space-8);
    grid-template-columns: repeat(2, minmax(0,1fr));
  }
}

/* AND A FOURTH COLUMN WHERE THE LADDER ALREADY SAYS FOUR CARDS FIT.
     Three columns were hardcoded at every width above 1180, while
     --page-measure steps to --tile-span-4 (1734px) at 1704. Measured at
     1920x1080: three tracks of 557px, which is 97px OVER --tile-max 460 -
     every tile on the board stretched past the width a card is allowed to be.
     Four tracks of that same 1734px measure are 409px, within a pixel or two
     of the 411px he gets at 1481, so the card keeps its size and the window
     gets more of them. Breakpoints copied from the --tile-span ladder at
     rows 19527-19528 so the two can never disagree. */

@media (min-width:1704px) {
  .home-grid{ grid-template-columns:repeat(4, minmax(0,1fr)); }
}

@media (min-width:1558px) {
  body:has(.sidebar.collapsed) .home-grid{ grid-template-columns:repeat(4, minmax(0,1fr)); }
}

/* AND THE COLUMN GAP GOES, BECAUSE ON A PHONE IT IS ONLY ARITHMETIC.
     Every tile spans all twelve columns down here, so no two are ever side by
     side and not one of the eleven column gaps is ever seen. They are still
     MEASURED, though: eleven gaps at 32px are 352px of width the track list
     demands before any column gets a pixel, and a 400px phone has 368px to give
     it. That was survivable while the grid had no padding — the tracks
     collapsed to nothing and the row still fitted — and it stopped being
     survivable the moment row 2022's keep-out asked for 60 of those pixels
     back: the grid kept insisting on its 352 and the tiles hung 44px out of
     their own container and straight back under the orb. Taking the unseen gaps
     to zero is right on its own and is what makes the keep-out arrive here. */

@media(max-width:760px) {
  .home-grid .ht{ grid-column:span 1 !important; }
  .home-grid {
    grid-template-columns: minmax(0,1fr);
    column-gap: 0;
  }
}

/* THE ORB'S KEEP-OUT, TAKEN OUT OF THE PAGE AND NOT OUT OF ANY ONE TILE.
     (Row 2022.)
     Home is where the fault was found — the 'Your digest' heading's Settings
     link, twenty-four pixels of it under the sphere, reading "Settin" — and it
     is also the page with the most right-hand controls on it: a verb in every
     section heading, Open / Later / × at the end of every card, and a launch
     pad of doors under the band.
     THE FIRST ATTEMPT PUT THE KEEP-OUT ON THE TWO ACTION STRIPS — .ht-head and
     .ht-card — AND THE SCREENSHOT KILLED IT. In a heading it was free: the rule
     that runs off the end of the label simply got shorter. In a card it was
     not. 82px comes out of the TITLE there, so "Karen Whitfield has had a
     bereavement" became "Karen Whitfield has h…" and its quote went from two
     lines to three — on every card in every tile, including the left-hand ones
     the sphere can never reach.
     ONE RULE ON THE PAGE COSTS LESS AND COVERS MORE. The twelve-column track is
     82px narrower, so the cost is shared at about seven pixels a column: a
     five-wide tile gives up 34px rather than 82, the rightmost tile's edge
     lands on the orb's, and every tile, verb, card and row on this page leaves
     the corner at once rather than the two kinds of strip somebody happened to
     remember. The launch pad is in it too, so its doors stay in line with the
     tiles below them — which is the whole reason this is on the page rather
     than on .home-grid alone.
     .home-band IS THE ONE EXCEPTION AND IT IS NAMED IN THE SELECTOR. It bleeds
     off both edges on purpose — see the note beside it in index.html, and the
     negative margin it carries — and it is the one thing here the orb can never
     reach, because it is pinned to the top of the flow and scrolling only ever
     carries it further away. Pulling it in would cost the front door its full
     width for nothing: photographed at 1280 it drops the overdue count onto a
     second row and the hero grows half as tall again. */
#view-home > :not(.home-band){ padding-inline-end:var(--orb-keepout-x); }

/* A TILE IS NOT A BOX ANY MORE. No background, no border, no padding —
     it is a column of type with a label over it. Everything that used to be
     carried by the frame is carried by the space around it. */
  /* THE FLOOR UNDER A TILE IS 36px A ROW, NOT 72, AND THE NUMBER WAS MEASURED.
     (8 October. tests/measure-home-gaps.js, run on this file before and after.)

     `--ht-rows` is the tile's own height span: renderHome() writes
     `--ht-rows:${tile.h}` onto the element, so this declaration is the ONLY
     reason a tile is ever taller than what is inside it. At 72px a span of two
     claimed 144px whatever it had to say. On the thirteen-tile front door
     EIGHT tiles say a single sentence when they have nothing — 37px of type
     under a 24px heading, 61px of tile in all — so each of those eight carried
     83px of nothing. Measured on the empty board: 683px of dead space across
     ten of the thirteen tiles, on a page 1,964px long.

     At 36px the same board is 1,624px and 66px of dead space. Nothing else
     changed. Measured again on all four reachable themes (hearth, ink,
     chocolate, serene on linen and on mist): identical, floors 36/72/108,
     no page errors.

     WHY 36, AND NOT 48, AND NOT 0.
     The floor is there so a half-empty tile does not collapse and break the
     line of its row — app.js says exactly that beside --ht-rows — and the
     shortest real tile stands at 61px. Two rows of 36 is 72px: eleven pixels
     of slack over that tile. That is a floor. 144px was a quota.
       · 48 was the other candidate (96px at a span of two). It keeps more rows
         dead level — 70px of raggedness on the empty board against 131 — and
         costs 73px more page. Rejected because the alignment it buys exists
         only on the EMPTY board: on a board with content the floor is already
         inert, and the full board's rows are ALREADY out of line by up to 75px
         because every filled tile is taller than 144 anyway. 48 pays real
         pixels on every cold start for a tidiness his own data destroys.
       · 0 was rejected too. It lets a one-line tile sit at 61px beside a
         two-line one at 95px with nothing holding the pair together, and the
         72px step is what keeps six of the thirteen landing on one line.

     WHAT THIS DOES NOT CHANGE. Height still means something in Arrange:
     homeTaller() cycles 1 -> 2 -> 3 and the floors are 36 / 72 / 108, three
     distinct steps as before. A tile with enough in it is untouched either
     way, because its content already clears both floors.

     AND THE THING THIS DOES NOT DO, which is a decision rather than an
     oversight. It does not level the rows. `.home-grid` has `align-items:start`
     a few rules above, so a short tile simply ends early and leaves nothing
     under it; it does not stretch to its row. Switching that to `stretch` was
     measured too and comes out at EXACTLY the same page height — 1,624 empty
     and 2,218 full, to the pixel — with every row dead level and the air moved
     back inside the short tiles instead (197px empty, 276px full, against 66
     and 11 here). Same length, so the only question it answers is where the
     emptiness goes, and that is a taste Matt has not been asked for yet. It
     also lands on every board including Browse's eighteen tiles, which this
     does not. Left as start deliberately — do not "fix" it without asking. */
.ht{ display:flex; flex-direction:column; min-width:0; position:relative;
    min-height:calc(var(--ht-rows, 2) * 36px); }

.ht-head{ display:flex; align-items:center; gap:var(--space-3); margin-bottom:var(--space-3); flex:none; }

/* flex:0 1 auto RATHER THAN flex:none, WHICH MADE THE TWO LINES BEFORE IT
     DEAD. The label already asked for overflow:hidden and an ellipsis and could
     never use either: a flex:none item is sized to its content and never
     shrinks, so there has never been anything to hide. It has not mattered
     because the row had the whole tile to spread out in — and row 2022's
     keep-out has just taken about seven pixels a column off that, which is
     nothing on the six-wide tiles and is worth being sure about on Listening,
     the one tile that carries a verb ("Open Listening", 106px of it) at a
     three-column minimum. If a heading ever does run out of room, the label
     should lose its tail to an ellipsis rather than the verb being pushed out
     through the side of the tile. */
.ht-title {
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 70%;
  font-size:var(--type-label); font-weight:650; letter-spacing:.2em;
  color:var(--text-dim);
}

/* The rule runs off the end of the label and fades. A full-width rule boxes
     the tile back in, which is the thing being undone here. */
.ht-rule{ flex:1; height:1px; min-width:14px;
    background:linear-gradient(90deg, var(--ht-fam, var(--glass-border-hi)), transparent 88%); }

/* ---- the family hairline --------------------------------------------
     "add color accents on home page to outline cards, areas, whatever."

     One declaration per family and nothing else: the hue lands ONLY on the
     rule that already ran off the end of each tile's label, so the page gains
     five zones and not one new object. The reasoning, including which channels
     were unavailable and why, is beside HOME_FAMILY in app.js.

     TWO STEPS, BECAUSE ONE CANNOT WORK ON BOTH GROUNDS. The first attempt
     mixed the base hue into the existing border colour at 54% and it was
     invisible on `midnight`, whose default rule is ALREADY blue-grey — a
     violet line on a blue-black page beside a blue-grey line is not an accent,
     it is a rumour. The --cat-*-2 steps are the light ones and they carry on a
     dark ground; on paper they are the ones that disappear, so the light
     themes take the base step instead. Both come out of the same ramp, so a
     theme that redefines the ramp redefines this with it.

     THE HEIGHT DOES NOT CHANGE. A 2px coloured rule is a border, and a border
     is what the tiles had their frames removed to escape. */
.ht[data-fam="time"]   { --ht-fam: var(--cat-teal); }

.ht[data-fam="people"] { --ht-fam: var(--cat-blue); }

.ht[data-fam="work"]   { --ht-fam: var(--cat-violet); }

.ht[data-fam="you"]    { --ht-fam: var(--cat-green); }

.ht[data-fam="world"]  { --ht-fam: var(--cat-orange); }

:root:not([data-mode="light"]) .ht[data-fam="time"]   { --ht-fam: var(--cat-teal-2); }

:root:not([data-mode="light"]) .ht[data-fam="people"] { --ht-fam: var(--cat-blue-2); }

:root:not([data-mode="light"]) .ht[data-fam="work"]   { --ht-fam: var(--cat-violet-2); }

:root:not([data-mode="light"]) .ht[data-fam="you"]    { --ht-fam: var(--cat-green-2); }

:root:not([data-mode="light"]) .ht[data-fam="world"]  { --ht-fam: var(--cat-orange-2); }

/* EXCEPT ON REGAL, WHICH IS A THEME ABOUT NOT DOING THIS.
     Its own block says "ONE ACCENT. Brass, and only brass", and it goes to the
     trouble of re-tinting --blue toward the brass rather than letting a second
     colour into the room. Five hues down the home page is the exact thing that
     theme exists to refuse. --cat-* is not re-tinted there, so without this
     line regal would have got the loudest version of the five, not the
     quietest. The zoning is a nice-to-have; the theme is a decision. */
:root[data-theme="regal"] .ht[data-fam]{ --ht-fam: var(--glass-border-hi); }

/* AND ON THE MATTE SIX, WHICH ANSWER IT DIFFERENTLY.
     Regal refuses the five hues outright. The matte themes keep them, because
     Matt's own note on the ten grounds was "we can keep our plan for colors
     meaning something but might change those colors based on the mode" — so
     the zoning survives and the palette moves under it. The five families are
     re-pointed at the theme's own area tones, which are the muted set every
     matte ground was built around; --cat-*-2 is the bright jewel ramp and was
     never re-tinted for these rooms, so leaving it would have put five loud
     lines across an oxblood page.
     Placed HERE rather than in the matte block near the top of the file for a
     reason worth remembering: this rule and the dark-mode one above it have
     identical specificity, so the later of the two wins, and the matte block
     is eight hundred lines earlier. Position is the whole fix. */
:root[data-finish="matte"] .ht[data-fam="time"]   { --ht-fam: var(--area-media); }

:root[data-finish="matte"] .ht[data-fam="people"] { --ht-fam: var(--area-people); }

:root[data-finish="matte"] .ht[data-fam="work"]   { --ht-fam: var(--area-work); }

:root[data-finish="matte"] .ht[data-fam="you"]    { --ht-fam: var(--area-life); }

:root[data-finish="matte"] .ht[data-fam="world"]  { --ht-fam: var(--area-now); }

.ht-body{ flex:1; min-height:0; overflow-y:auto; margin:0 calc(var(--space-1) * -1); padding:0 var(--space-1); scrollbar-width:thin; }

/* A tile whose content fills its own area to the edges. The header stays on
     the page background above it, deliberately: a label sitting on top of a
     photograph is legible against this week's picture and invisible against
     next week's. */
.ht-bleed .ht-body{ position:relative; overflow:hidden; margin:0; padding:0;
    border-radius:var(--radius-card); min-height:190px; }

.ht-ctl{ cursor:pointer; color:var(--text-faint); width:22px; height:22px; border-radius:var(--radius-control);
    display:flex; align-items:center; justify-content:center; font-size:var(--type-body); flex:none; }

.ht-ctl:hover{ color:var(--text); background:color-mix(in srgb, var(--text) 9%, transparent); }

.ht-ctl.danger:hover{ color:var(--cat-red-text); background:color-mix(in srgb, var(--cat-red) 14%, transparent); }

/* Unframed tiles cannot be dragged if you cannot see where they start, so
     the frame comes back — dashed, outside the content — only while arranging. */
  /* Tiles that draw a shape rather than a list fill their row. A chart that
     stops halfway down its row leaves a hole beside whatever is taller; a list
     stretched to match one would just be a list with padding under it, which is
     why this is opt-in rather than align-items:stretch on the grid. */
.ht-fill{ align-self:stretch; }

.home-grid.editing .ht{ cursor:grab; border-radius:var(--radius-card); animation:none;
    outline:1px dashed var(--glass-border-hi); outline-offset:11px; }

.home-grid.editing .ht.dragging{ opacity:.4; cursor:grabbing; }

/* THE WHOLE TILE IS THE HANDLE.
     A picture inside a tile is a drag source in its own right, so grabbing the
     album art or a face dragged the image instead of the tile — which is most
     of the surface of the tiles worth moving. While arranging, nothing in the
     body takes the pointer, so every part of a tile picks the tile up. */
.home-grid.editing .ht-body{ pointer-events:none; user-select:none; }

.home-grid.editing .ht-body *{ -webkit-user-drag:none; }

/* WHERE IT WILL LAND - ON AN EDGE, BECAUSE A TILE IS NOT A POSITION.
     This used to turn the target tile's dashed outline solid, which says
     "this tile" and not "here". Since the move is an insert, "this tile" has
     two possible meanings and he could not tell which he was getting. The
     caret is drawn on the edge the pointer is nearest, 3px of accent in the
     11px the outline-offset already holds open, and homeDropSide() in app.js
     reads the same midpoint - so the line cannot lie about the result. */
.home-grid.editing .ht.drop-before,
  .home-grid.editing .ht.drop-after{ position:relative; }

.home-grid.editing .ht.drop-before::after,
  .home-grid.editing .ht.drop-after::after{
    content:''; position:absolute; top:-6px; bottom:-6px; width:3px;
    border-radius:var(--radius-tight); background:var(--accent);
    pointer-events:none; }

.home-grid.editing .ht.drop-before::after{ left:-13px; }

.home-grid.editing .ht.drop-after::after{ right:-13px; }

:root:not([data-mode="light"]) .home-grid.editing .ht.drop-before::after,
  :root:not([data-mode="light"]) .home-grid.editing .ht.drop-after::after{
    box-shadow:0 0 12px 0 var(--accent-glow); }

/* Staggered arrival. Fifty milliseconds apart is under the threshold where
     it reads as a queue and over the one where it reads as a jump-cut. */
@keyframes htRise{ from{ opacity:0; transform:translateY(11px); } to{ opacity:1; transform:none; } }

.home-grid .ht{ animation:htRise .5s var(--ease-out) both; animation-delay:calc(var(--i, 0) * 50ms); }

:root[data-fx="lite"] .home-grid .ht{ animation:none; }

@media (prefers-reduced-motion: reduce) {
  .home-grid .ht{ animation:none; }
}

/* A CARD IS A CLAIM, A REASON, AND SOMETHING TO DO ABOUT IT.
     The reason line is not decoration — a card that asserts something about
     his life without saying how it knows reads as surveillance rather than as
     help, and he stops trusting the page. */
.ht-card{ display:flex; align-items:flex-start; gap:var(--space-3); padding:var(--space-3) 0 var(--space-3) var(--space-4); position:relative; }

.ht-card + .ht-card{ border-top:1px solid var(--glass-border); }

/* The heat mark. Same geometry for every card so the column of them reads as
     a margin rule; only its brightness changes. z-index 0 + positioned
     children, for the reason spelled out at the top of this block. */
.ht-card::before{ content:''; position:absolute; left:0; top:15px; bottom:15px; width:2px;
    border-radius:var(--radius-tight); z-index:0; background:color-mix(in srgb, var(--text) 13%, transparent); }

.ht-card[data-heat="1"]::before{ background:var(--accent); }

:root:not([data-mode="light"]) .ht-card[data-heat="1"]::before{ box-shadow:0 0 12px 1px var(--accent-glow); }

/* Light mode gets no glow, so heat is carried by ink instead. Weight, not
     hue — the page still has one accent on it. */
:root[data-mode="light"] .ht-card[data-heat="1"] .ht-card-title{ font-weight:700; }

.ht-card-main{ flex:1; min-width:0; cursor:pointer; position:relative; z-index:1; }

/* WRAPS RATHER THAN CUTS. Matt, 30 September, shown "Tomorrow — 14 hours …"
     beside the same card with the title on two lines: two lines. A heading
     that ends in an ellipsis is a heading he cannot read. */
.ht-card-title{ font-size:var(--type-lead); font-weight:600; line-height:1.35; letter-spacing:-.004em;
    white-space:normal; overflow-wrap:anywhere; }

/* Conversation continuity: the sentence the other person actually wrote.
     Set as a quote and given the dim token rather than the faint one — it is
     the most valuable line on the card, and it is somebody's own words.

     IT WAS CALLED .ht-quote AND SO IS SOMETHING ELSE. The "line for today" tile
     (search .ht-quote-text, about three thousand lines below) uses the same
     class as a full-tile flex container: `display:flex; flex-direction:column;
     align-items:center; text-align:center; height:100%; padding:4px 10px`. Same
     name, same specificity, declared later — so it won, and item 51's quote was
     being drawn as a centred serif epigraph floating in the middle of the card
     instead of a caption sitting under the line it belongs to. Caught by
     screenshotting the tile, not by any check: tests/test-continuity.js asserts
     that a rule with this name EXISTS, which was true and beside the point.

     Renamed rather than reordered. Two components sharing a class is a trap
     whichever way round they are declared — reorder them and the next person to
     edit either one breaks the other silently. */
.ht-said{ display:block; margin-top:var(--space-1); color:var(--text-dim); font-style:italic;
            overflow:hidden; text-overflow:ellipsis; }

.ht-card-acts{ display:flex; align-items:center; gap:var(--space-2); flex:none; position:relative; z-index:1; }

.ht-x{ cursor:pointer; color:var(--text-faint); font-size:var(--type-lead); line-height:1;
    padding:var(--space-1) var(--space-2); border-radius:var(--radius-pill); }

.ht-x:hover{ color:var(--cat-red-text); background:color-mix(in srgb, var(--cat-red) 12%, transparent); }

/* Wide enough for "11:00 AM" on one line. At 54px it wrapped, and a wrapped
     time is what pushed the first event row to double height. */
  /* THE TICKET PRESS, AND THE SENTENCE UNDER THE LIST. Both are quiet: the
     press is a bordered word rather than a filled button, because the loudest
     thing in this tile should be who is playing, not the shop. */
.ht-tix{ flex:none; white-space:nowrap; cursor:pointer;
    background:none; border:1px solid var(--glass-border); border-radius:var(--radius-tight);
    color:var(--text-dim); font-size:var(--type-label); padding:var(--space-1) var(--space-2); }

.ht-tix:hover{ color:var(--text); border-color:var(--glass-border-hi); }

.ht-tix:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

.ht-tix-none{ cursor:default; color:var(--text-faint); border-style:dashed; }

.ht-tix-none:hover{ color:var(--text-faint); border-color:var(--glass-border); }

.ht-tix-note{ padding-top:var(--space-2); border-top:1px solid var(--glass-border); }

.ht-quiet-line{ font-family:var(--font-display); font-size:var(--type-title); font-weight:500;
    line-height:1.28; letter-spacing:-.015em; }

/* ---- the shapes: the tiles that are not lists ------------------------ */

  /* A year of correspondence. One series, so no legend — the tile title names
     it. Thin marks, a soft accent wash under the line, one dot on the last
     point, and no gridlines at all: at 52 points a grid is noise, and the
     question this answers is "what shape is my year", not "what was week 31". */
.ht-pulse{ display:flex; flex-direction:column; gap:var(--space-4); height:100%; }

.ht-pulse-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-4); flex-wrap:wrap; }

.ht-pulse-right{ text-align:right; }

.ht-pulse-delta{ font-size:var(--type-body); font-weight:600; color:var(--text-dim); }

/* A non-breaking space, because a trailing space in `content` is collapsed
     and the arrow ends up welded to the number. */
.ht-dir-up::before{ content:'\2191\00a0'; }

.ht-dir-down::before{ content:'\2193\00a0'; }

.ht-spark{ width:100%; flex:1; min-height:96px; display:block; overflow:visible; }

:root:not([data-mode="light"]) .ht-spark{ filter:drop-shadow(0 0 12px var(--accent-glow)); }

.ht-spark-axis{ display:flex; justify-content:space-between; font-size:var(--type-label); font-weight:600;
    letter-spacing:.14em; text-transform:uppercase; color:var(--text-dim); }

/* The people actually in his week. Sized by volume, monochrome, no chips. */
  /* A GRID, NOT A WRAPPING FLEX ROW. Twelve faces at their natural width came
     out as eleven and an orphan on a 1600px screen — which reads as a mistake,
     because it is one. Six columns is two clean rows at any width. */
.ht-circle{ display:grid; grid-template-columns:repeat(6, minmax(0, 1fr));
    gap:var(--space-5) var(--space-3); align-items:end; padding-top:var(--space-1); }

@media(max-width:1180px) {
  .ht-circle{ grid-template-columns:repeat(4, minmax(0,1fr)); }
}

@media(max-width:560px) {
  .ht-circle{ grid-template-columns:repeat(3, minmax(0,1fr)); }
}

.ht-face{ display:flex; flex-direction:column; align-items:center; gap:var(--space-2); cursor:pointer; min-width:0; }

.ht-face-d{ border-radius:var(--radius-round); overflow:hidden; display:flex; align-items:center; justify-content:center;
    font-weight:700; color:var(--text); background:color-mix(in srgb, var(--text) 9%, transparent);
    border:1px solid var(--glass-border); flex:none;
    transition:border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); }

.ht-face.lead .ht-face-d{ border-color:color-mix(in srgb, var(--accent) 55%, transparent); }

:root:not([data-mode="light"]) .ht-face.lead .ht-face-d{ box-shadow:0 0 20px -2px var(--accent-glow); }

.ht-face:hover .ht-face-d{ border-color:var(--glass-border-hi); }

:root:not([data-mode="light"]) .ht-face:hover .ht-face-d{ box-shadow:0 0 18px -3px var(--accent-glow); }

.ht-face-n{ font-size:var(--type-body); font-weight:600; max-width:100%; overflow:hidden;
    text-overflow:ellipsis; white-space:nowrap; text-align:center; }

.ht-face-c{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim); }

/* Weather as a small chart rather than a number. One shared scale across the
     five days: five independently-scaled bars are five separate charts. */
.ht-wx{ display:flex; flex-direction:column; gap:var(--space-4); height:100%; cursor:pointer; }

.ht-wx-now{ display:flex; align-items:center; gap:var(--space-4); }

.ht-wx-meta{ min-width:0; }

.ht-wx-cond{ font-size:var(--type-body); font-weight:600; }

.ht-wx-days{ display:grid; grid-template-columns:repeat(5, 1fr); gap:var(--space-2); align-items:end; }

.ht-wx-day{ display:flex; flex-direction:column; align-items:center; gap:var(--space-2); }

.ht-wx-dn{ font-size:var(--type-label); font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--text-dim); }

.ht-wx-ico{ width:17px; height:17px; color:var(--text-dim); }

.ht-wx-ico svg{ width:100%; height:100%; }

.ht-wx-track{ position:relative; width:4px; height:52px; border-radius:var(--radius-tight);
    background:color-mix(in srgb, var(--text) 8%, transparent); }

.ht-wx-track span{ position:absolute; left:0; right:0; border-radius:var(--radius-tight); background:var(--accent); }

:root:not([data-mode="light"]) .ht-wx-track span{ box-shadow:0 0 10px 0 var(--accent-glow); }

.ht-wx-hi{ font-family:var(--font-mono); font-size:var(--type-body); font-weight:500; }

.ht-wx-lo{ font-family:var(--font-mono); font-size:var(--type-label); color:var(--text-dim); }

/* One countdown, set big, with the runners-up underneath in a whisper. */
.ht-count{ cursor:pointer; }

.ht-count-n{ font-family:var(--font-display); font-size:var(--type-hero); font-weight:500;
    line-height:.95; letter-spacing:-.035em; }

.ht-count-u{ font-size:var(--type-label); font-weight:600; letter-spacing:.15em; text-transform:uppercase;
    color:var(--text-dim); margin-top:var(--space-2); }

.ht-count-t{ font-size:var(--type-lead); font-weight:600; margin-top:var(--space-3); line-height:1.3; }

.ht-count-rest{ margin-top:var(--space-4); padding-top:var(--space-2); border-top:1px solid var(--glass-border); }

.ht-sun{ display:flex; flex-direction:column; gap:var(--space-3); }

.ht-sun-pair{ display:flex; gap:var(--space-8); }

.ht-sun-l{ font-size:var(--type-label); font-weight:600; letter-spacing:.15em; text-transform:uppercase; color:var(--text-dim); }

.ht-sun-v{ font-family:var(--font-display); font-size:var(--type-title); font-weight:500; letter-spacing:-.02em; margin-top:var(--space-1); display:block; }

.ht-moon{ display:flex; align-items:center; gap:var(--space-3); padding-top:var(--space-3); border-top:1px solid var(--glass-border); }

.ht-moon-g{ font-size:var(--type-page); line-height:1; color:var(--text-dim); }

:root:not([data-mode="light"]) .ht-moon-g{ text-shadow:0 0 16px var(--accent-glow); }

/* A note and a journal are the same control wearing different labels. Borders
     only on focus: a permanently outlined box on an unframed page is the one
     thing that would put the widget back. */
.ht-scratch{ width:100%; flex:1; min-height:96px; resize:none; border:none; outline:none;
    background:transparent; color:var(--text); font-family:var(--font-body);
    font-size:var(--type-body); line-height:1.6; padding:0; }

.ht-scratch::placeholder{ color:var(--text-faint); }

.ht-scratch:focus{ box-shadow:none; }

.ht-journal{ display:flex; flex-direction:column; gap:var(--space-3); height:100%; }

/* PROMPTS DRAWN FROM THE ACTUAL DAY. Small, quiet, and gone the moment he
     starts typing — see homeTileJournal()'s own comment for why they only
     show against an empty box. A chip, not a button: tapping one fills the
     box rather than doing something irreversible. */
.ht-prompts{ display:flex; flex-direction:column; gap:var(--space-2); flex:none; }

.ht-prompt{ font-size:var(--type-body); color:var(--text-dim); border:1px solid var(--glass-border);
    border-radius:var(--radius-control); padding:var(--space-2) var(--space-3); cursor:pointer; line-height:1.4; }

.ht-prompt:hover{ color:var(--text); border-color:var(--glass-border-hi); }

/* CURATED FOR TODAY.
     Picture beside prose above 620px, stacked below it. The picture is given a
     fixed share of the width rather than a fixed size, because the works are
     every shape from a miniature to a five-metre altarpiece and a fixed box
     would letterbox half of them. */
  /* THE FRAME.
     Full bleed to the tile's own edges — a photograph inset inside a card with
     a heading above it is a thumbnail, and a thumbnail is not the thing he
     asked for. The crossfade is two stacked images rather than one whose src
     changes, because swapping a src blanks the element for a frame and the
     flicker is the whole difference between "a frame" and "a slideshow". */
.ht-frame{ position:absolute; inset:0; border-radius:inherit; overflow:hidden;
    background:color-mix(in srgb, var(--text) 5%, transparent); }

.ht-frame-img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
    opacity:0; transition:opacity var(--dur-ambient) var(--ease-out); }

.ht-frame-img.on{ opacity:1; }

.ht-frame-when{ opacity:.8; font-size:var(--type-label); }

/* The rebuild dropped max-width:34ch from this rule: a later `.page-sub` rule
   in app.css (same specificity, same element) always overrode it, so it never
   took effect and the width the screen shows is .page-sub's. */
.ht-frame-say{ margin:0; color:var(--text); }

.ht-frame-empty{ position:absolute; inset:0; display:flex; flex-direction:column;
    align-items:center; justify-content:center; gap:var(--space-3); text-align:center; padding:var(--space-5); }

.ht-frame-dots{ position:absolute; top:12px; right:14px; display:flex; gap:var(--space-2); }

/* TWO LINES ON THE TILE, ALL OF IT ON THE BIG SCREEN. The frame is about
     190px tall on Home and a four-line paragraph fills it completely — the
     photograph stops being the thing on the tile and becomes a background for
     an essay, which is the opposite of what this is for. The header's Full
     screen verb is where the rest of it lives. */
.ht-frame-cap .frame-story{ display:-webkit-box; -webkit-line-clamp:2;
    -webkit-box-orient:vertical; overflow:hidden; }

/* A caption over a photograph is unreadable against a bright sky and
     invisible against a dark one, so it gets its own gradient rather than a
     colour that happens to work on this week's picture. */
  
  /* The caption block grew from one line to a paragraph, so the gradient has to
     start higher or the last line of prose sits on the photograph itself. */
.ht-frame-cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: var(--space-8) var(--space-5) var(--space-4);
  background: linear-gradient(to top, rgba(0,0,0,.72), transparent);
  color: #fff;
  font-size: var(--type-body);
  font-weight: 500;
  pointer-events: none;
  padding-top: var(--space-12);
}

/* Controls on the tile. They live under the words rather than over the
     picture's middle, and they are only drawn when the pointer is on the tile —
     a photograph with two chevrons parked on it permanently is a carousel. */
.ht-frame-acts{ position:absolute; right:12px; top:12px; display:flex; gap:var(--space-2);
    opacity:0; transition:opacity var(--dur-base) var(--ease-out); }

.ht-frame:hover .ht-frame-acts,
  .ht-frame:focus-within .ht-frame-acts{ opacity:1; }

.frame-act{ min-width:26px; height:26px; padding:0 var(--space-2); border-radius:var(--radius-control); cursor:pointer;
    border:1px solid rgba(255,255,255,.28); background:rgba(10,12,20,.5); color:#fff;
    font-size:var(--type-lead); line-height:1; font-family:inherit;
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); }

.frame-act.wide{ font-size:var(--type-body); font-weight:600; padding:0 var(--space-3); }

.frame-act:hover{ background:rgba(10,12,20,.72); }

/* ON A TOUCHSCREEN THERE IS NO HOVER, so "appears when the pointer is over
     it" means "never appears" — and a control nobody can reach is the same
     thing as a control that does nothing. */

@media (hover: none) {
  .ht-frame-acts{ opacity:1; }
}

/* A DAY WITH NO PHOTOGRAPH. The paragraph on its own, over the day's painting
     when the art tile has already fetched one. Theme tokens here and NOT white,
     because without the painting there is nothing behind this text but the
     tile's own tinted panel — and var(--text-dim) measured 4.46:1 on blush
     paper against exactly this background, which is why every line of it is
     var(--text) at full strength. */
.frame-noPhoto{ position:absolute; inset:0; display:flex; flex-direction:column;
    align-items:flex-start; justify-content:flex-end; gap:var(--space-2); padding:var(--space-5); }

.frame-noPhoto .frame-reason,
  .frame-noPhoto .frame-story{ color:var(--text); text-shadow:none; }

.frame-noPhoto .frame-story.dim{ color:var(--text); font-style:italic; }

.frame-artcred{ color:var(--text); font-size:var(--type-body); font-weight:500; opacity:.85; }

/* With a painting behind it the same block goes back to being text over a
     picture, so it gets the scrim and the white treatment again.

     A FLAT VEIL RATHER THAN THE BOTTOM-WEIGHTED GRADIENT THE CAPTION BAND USES,
     and that is the only difference between them. A caption is two lines at the
     foot of a photograph; this block is four and fills the tile, so a gradient
     that fades to nothing at the top leaves the reason line — the most
     important sentence on the tile — sitting on whatever the top of that
     painting happens to be, and a Van Gogh sky is very pale. */
.frame-noPhoto.over{ background:linear-gradient(to top, rgba(0,0,0,.82), rgba(0,0,0,.52)); }

.frame-noPhoto.over .frame-reason,
  .frame-noPhoto.over .frame-story,
  .frame-noPhoto.over .frame-story.dim,
  .frame-noPhoto.over .frame-artcred{ color:#fff; text-shadow:0 1px 3px rgba(0,0,0,.55); }

/* AND THE BUTTON, which is the part that actually broke. `.btn` takes its
     colours from the page's theme, and on parchment that is dark ink on pale
     paper — over this veil it rendered dark on dark and could not be read at
     all. A control standing on a picture has to be lit the way the words on it
     are. Found by looking at the screenshot, not at the stylesheet. */
.frame-noPhoto.over .btn{ background:rgba(10,12,20,.55); color:#fff;
    border:1px solid rgba(255,255,255,.34);
    -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px); }

.frame-noPhoto.over .btn:hover{ background:rgba(10,12,20,.8); color:#fff; }

/* "THE ART SHOULD BE MORE THAN DOUBLE THE SIZE... NOT A FOOTNOTE... A FEATURE."
     (Matt, 26 Aug.)

     It was a footnote by construction: a 300px thumbnail pinned to the left of
     a paragraph, capped at 260px tall. Half the tile was words about the
     picture and the picture came second, which is backwards for the one tile on
     the page whose entire job is to be looked at.

     The picture is the tile now. Full width, up to 560px tall — more than
     double the old ceiling in each direction — and the caption sits underneath
     it where a caption belongs. The words are still all there; they have simply
     stopped competing with the thing they describe.

     object-fit:contain, not cover. Cover fills the frame by cropping, which is
     fine for an avatar and vandalism on a painting: it takes the sky off a
     landscape and the top of a head off a portrait. A picture is shown whole or
     it is not shown. The ground behind the letterboxing is what a gallery wall
     would be. */
  /* AND IT WAS STILL A THUMBNAIL, WHICH I ONLY FOUND BY MEASURING IT.
     Matt, 26 Aug: "the art is still a thumbnail on the homepage."

     The ELEMENT was full width — 784px — so by every number I had checked, the
     change had worked. What I had not checked was the picture inside it.
     flex:1 gave the img whatever height was left over after the caption, which
     came to 331px; object-fit:contain then shrank a 1200x1500 portrait to fit
     that height. The result is a 265px-wide picture floating in the middle of a
     784px band. A thumbnail with a lot of space around it, exactly as he said.

     THE PICTURE SIZES ITSELF NOW. Its own aspect ratio decides, capped by the
     width of the tile and by 60% of the window — so a portrait comes out tall
     and a landscape comes out wide, and neither is squeezed into whatever room
     the paragraph underneath happened to leave. The element IS the picture,
     with no band around it to letterbox into.

     The lesson, written down because I keep re-learning it: measuring the
     container is not measuring the thing. */
.ht-art{ margin:0; display:flex; flex-direction:column; gap:var(--space-3); }

.ht-art-frame{ display:flex; flex-direction:column; gap:var(--space-4); align-items:center; }

.ht-art-img{ display:block; width:auto; max-width:100%;
    height:auto; min-height:240px; max-height:min(620px, 60vh);
    border-radius:var(--radius-card); object-fit:contain; cursor:pointer;
    background:color-mix(in srgb, var(--text) 6%, transparent); }

:root:not([data-mode="light"]) .ht-art-img{ box-shadow:var(--elev-raised); }

/* The picture is a button now — it opens itself full size — so it is
     focusable, and anything focusable has to say when it has the focus. */
.ht-art-img:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }

.ht-art-text{ min-width:0; width:100%; align-self:stretch; }

.ht-art-title{ font-family:var(--font-display); font-size:var(--type-lead); font-weight:600;
    line-height:1.25; letter-spacing:-.01em; }

/* The museum's own catalogue line — artist, date, medium — never anything
     that was written for it. */
.ht-art-facts{ font-size:var(--type-body); color:var(--text-dim); line-height:1.5; margin-top:var(--space-2); }

/* And the paragraph that WAS written for it, set apart so the two are never
     mistaken for each other. */
.ht-art-note{ font-size:var(--type-body); line-height:1.6; margin:var(--space-3) 0 0; max-width:56ch;
    padding-left:var(--space-3); border-left:2px solid var(--glass-border); color:var(--text); }

/* --text-dim, not --text-faint. Ten-pixel uppercase has failed the 4.5:1
     floor every previous time it was set in the fainter token. */
.ht-art-credit{ font-size:var(--type-label); font-weight:600; letter-spacing:.09em; text-transform:uppercase;
    color:var(--text-dim); margin-top:var(--space-3); line-height:1.5; }

.ht-art-bar{ display:flex; align-items:center; gap:var(--space-3); flex:none; }

.ht-art-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; }

.ht-art-rule{ flex:1; height:1px; background:var(--glass-border); }

.ht-art-btn{ width:26px; height:26px; border-radius:var(--radius-round); border:1px solid var(--glass-border);
    background:none; color:var(--text-dim); cursor:pointer; font-size:var(--type-lead); line-height:1;
    display:grid; place-items:center; padding:0;
    transition:color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }

.ht-art-btn:hover{ color:var(--text);
    border-color:color-mix(in srgb, var(--accent) 50%, var(--glass-border)); }

/* On a phone it is still the feature — 340px is most of the fold and worth
     it, where 200px was a stamp. */

@media(max-width:620px) {
  .ht-art-img{ max-height:52vh; }
  .ht-art-note{ max-width:none; }
}

.ht-breathe{ display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:var(--space-4); height:100%; min-height:120px; cursor:pointer; }

.ht-breathe-dot{ width:34px; height:34px; border-radius:var(--radius-round);
    background:color-mix(in srgb, var(--teal) 55%, transparent); }

:root:not([data-mode="light"]) .ht-breathe-dot{ box-shadow:0 0 30px 6px color-mix(in srgb, var(--teal) 30%, transparent); }

.ht-breathe.running .ht-breathe-dot{ animation:htBreathe 11s var(--ease-in-out) infinite; }

.ht-breathe-label{ font-size:var(--type-label); color:var(--text-dim); letter-spacing:.15em; text-transform:uppercase; font-weight:600; }

@keyframes htBreathe{ 0%,100%{ transform:scale(1); } 36%{ transform:scale(1.9); } 64%{ transform:scale(1.9); } }

/* Cheap mode turns off everything decorative that loops for ever, and a
     breathing circle is the most literally decorative loop in the app. */
:root[data-fx="lite"] .ht-breathe.running .ht-breathe-dot{ animation:none; }

@media(max-width:860px) {
  .home-band{ margin:calc(var(--space-4) * -1) -14px var(--space-4); padding:var(--space-5) var(--space-4) var(--space-4); }
  .home-band::after{ left:14px; right:14px; }
}

/* The digest panel. Tabs across the top because there are up to three of
     them a day and they are different briefs, not a list. */
.dg-tabs{ display:flex; gap:var(--space-2); margin-bottom:var(--space-3); flex-wrap:wrap; }

.dg-tab{ font-family:inherit; font-size:var(--type-label); font-weight:700; text-transform:uppercase;
    letter-spacing:.08em; padding:var(--space-2) var(--space-3); border-radius:var(--radius-pill); cursor:pointer;
    border:1px solid var(--glass-border); background:rgba(120,120,140,.10); color:var(--text-dim); }

.dg-tab:hover{ color:var(--text); }

.dg-tab.on{ color:var(--text); border-color:var(--glass-border-hi); background:rgba(120,120,140,.20); }

.dg-tab:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }

.dg-lines{ display:flex; flex-direction:column; gap:var(--space-hair); }

.ht-track{ display:flex; gap:var(--space-4); align-items:center; cursor:pointer; }

.ht-track-art{ width:62px; height:62px; border-radius:var(--radius-control); object-fit:cover; flex:none; }

:root:not([data-mode="light"]) .ht-track-art{ box-shadow:var(--elev-raised); }

.ht-track-n{ font-size:var(--type-lead); font-weight:600; line-height:1.3; }

.ht-track-s{ font-size:var(--type-label); font-weight:600; letter-spacing:.14em; text-transform:uppercase;
    color:var(--text-dim); margin-top:var(--space-2); }

/* Quick launch. A grid rather than a row: a row of eight scrolls sideways,
     and sideways scroll on a dashboard tile is a thing nobody finds. */
.ql-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(104px, 1fr)); gap:var(--space-2); }

.ql-item{ position:relative; display:flex; flex-direction:column; align-items:center;
    justify-content:center; gap:var(--space-2); padding:var(--space-3) var(--space-2); min-height:76px; border-radius:var(--radius-card);
    border:1px solid var(--glass-border); text-decoration:none; color:var(--text-dim);
    cursor:pointer; transition:border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out); }

.ql-item:hover{ color:var(--text); border-color:color-mix(in srgb, var(--accent) 45%, transparent);
    background:color-mix(in srgb, var(--accent) 10%, transparent); }

.ql-mark{ width:26px; height:26px; border-radius:var(--radius-control); display:flex; align-items:center;
    justify-content:center; font-weight:700; font-size:var(--type-body); color:var(--text);
    background:color-mix(in srgb, var(--accent) 22%, transparent); }

.ql-name{ font-size:var(--type-body); font-weight:600; text-align:center; line-height:1.2;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%; }

.ql-add{ border-style:dashed; }

/* The remove cross appears on hover only. Eight permanent crosses on a
     dashboard is eight invitations to delete something by accident. */
.ql-x{ position:absolute; top:3px; right:5px; font-size:var(--type-lead); line-height:1;
    color:var(--text-faint); opacity:0; transition:opacity var(--dur-fast) var(--ease-out); }

.ql-item:hover .ql-x{ opacity:1; }

.ql-x:hover{ color:var(--cat-red); }

/* SAYS WHERE THE CLICK GOES, BEFORE THE CLICK.
     Some sites refuse to be shown inside another page and have to open in a
     tab. A tile that behaves one way for Reuters and another for Facebook,
     with nothing to tell them apart, reads as broken rather than as two
     different sites. The arrow is the difference, and the title attribute
     spells it out for anyone who hovers or uses a screen reader. */
.ql-out{ position:absolute; top:4px; left:6px; font-size:var(--type-label); line-height:1;
    color:var(--text-faint); }

/* The brain dump. The box grows to the tile and the buttons sit under it. */
.ht-dump{ display:flex; flex-direction:column; gap:var(--space-2); height:100%; }

.ht-dump .ht-scratch{ flex:1; min-height:70px; }

.ht-dump-row{ display:flex; flex-wrap:wrap; gap:var(--space-2); }

/* A line for today. Serif and centred, because it is the one thing on the
     page that is not information — it should not look like a row of data. */
.ht-quote{ display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:var(--space-2); text-align:center; height:100%; padding:var(--space-1) var(--space-3); }

.ht-quote-text{ font-family:var(--font-display); font-size:var(--type-lead); line-height:1.45;
    color:var(--text); }

.ht-quote-who{ font-size:var(--type-label); letter-spacing:.06em; text-transform:uppercase;
    color:var(--text-faint); }

/* MARKETS — his #9.
     A tile read in a glance and never studied, so: name left, price right,
     move hard right in colour. Tabular figures, because a column of numbers
     that jitters as the digits change is unreadable at this size. */
.mkt{ display:flex; flex-direction:column; gap:1px; }

.mkt-row{
    display:flex; align-items:baseline; gap:var(--space-3); padding:var(--space-2) var(--space-hair);
    border-bottom:1px solid var(--glass-border);
  }

.mkt-row:last-child{ border-bottom:none; }

.mkt-n{ flex:1; min-width:0; font-size:var(--type-body); color:var(--text-dim);
          overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

.mkt-p{ font-size:var(--type-body); font-weight:600; font-variant-numeric:tabular-nums; }

.mkt-c{ font-size:var(--type-body); font-weight:600; font-variant-numeric:tabular-nums;
          min-width:66px; text-align:right; }

.mkt-c.up{ color:var(--green); }

.mkt-c.down{ color:var(--pink); }

.mkt-c.flat{ color:var(--text-faint); }

.mkt-na{ font-size:var(--type-body); color:var(--text-faint); }

.mkt-foot{ margin-top:var(--space-2); font-size:var(--type-label); color:var(--text-faint); }

/* ==========================================================================
     THE HOME PAGE, WITH SOME LIFE IN IT
     --------------------------------------------------------------------------
     "My issue with the homepage may be the font size. Everything is small and
      nondescript. Can we bolden/bigger/fancy up this more? Give it some life?"
     
     His diagnosis is right and it is worth saying why, because "make it bigger"
     is not the fix on its own. The page had ONE typographic voice: 13.5px rows
     at weight 400 under 10.5px uppercase labels, repeated fourteen times. There
     was nothing to look at first. A page where everything is the same size is
     not calm, it is flat — the eye has nowhere to land, so it reads as
     nondescript however good the spacing is.
     
     So this is a hierarchy rather than a scale-up:
     
       THE LABEL steps back. Smaller, lighter, more tracking — it is a signpost,
         and it was competing with the content underneath it.
       THE CONTENT steps forward. 13.5px → 15px, and the first line of each tile
         carries real weight. This is the layer he actually reads.
       THE NUMBERS get the serif. Fraunces at a real size for anything that is a
         single figure — that is the "fancy" he is asking for, and a number is
         the one place it costs nothing in legibility.
       AND ONE THING PER TILE IS ALLOWED TO BE BIG. The lead row is heavier than
         the rest, so every tile has a first thing to read.
     
     Nothing here changes a layout or a colour. It is weight, size and the
     serif — which is the whole difference between a spreadsheet and a page.
     ========================================================================== */
  /* --text-faint took this to 4.48:1 in serene:linen — two hundredths under
     the 4.5 floor, on the label of every tile on the home page. The size and
     the tracking are what make it recede; the colour change was greed, and
     test-readability.js priced it. --text-dim is where it was and where it
     stays. */
/* A single figure gets the serif and real size. */
.ht-big {
  font-family: var(--font-display);
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-size: var(--type-hero);
  font-weight: 600;
  letter-spacing: -.035em;
}

@media (max-width:760px) {
  .ht-big{ font-size:var(--type-display); }
}

/* ===========================================================================
   NOTHING SHIFTS AFTER IT LOADS — the top of Home.  Row 1824.
   ===========================================================================

   The second thing the measurement found, and the one a person meets first,
   because Home is the front door. tools/measure-the-shift.js, four cold boots,
   0.028 every time:

     div#homeBand > ::before   [84,10,1356,140] -> [84,10,1356,174]
     div#launchPad             [114,118,1296,168] -> [114,198,1296,169]

   THE ELEMENT THIS RESERVED A FRAME FOR IS GONE.     (25 September.)

   #homeLead was empty in the markup until renderHomeLead() filled it, and the
   lead was the largest type on the page, so the band grew about 80 pixels
   nearly four seconds in and took the launch pad and the whole grid down with
   it. The fix was a min-height on #homeLead adding up the four pieces the lead
   is built from. The Home rebuild deleted the element, the function and the
   launch pad that was being shoved, so the rule selected nothing and is
   deleted with them.

   THE REASONING IS NOT DEAD, ONLY THIS RULE. The lead is the hero's suggestion
   now and the hero sits in the same place in the same band, so it can shift
   the page in exactly the same way. The rest of this block — the date under
   the greeting, and the lanes' own reserved two lines over at .lane-body p —
   is the same idea and is still live. If the hero is ever measured shifting,
   the frame belongs on .hero-suggest and the arithmetic above is how to size
   one: every branch of heroSuggestHtml() builds the same three parts in the
   same order, so it is arithmetic rather than a guess about content. Write it
   against the real type, not against the spacing ramp, for the reason the
   next paragraph gives. */

/* AND THE DATE UNDER THE GREETING, which is the same bug twelve pixels tall.
   #homeSub is empty in the markup and is filled with "FRIDAY, 11 SEPTEMBER"
   once Home renders. The probe caught it on one cold boot in three:

     .home-band-top  1296x46 -> 1296x58      #homeSub  204x0 -> 221x12

   Twelve pixels, and everything below the band goes down by twelve with it.
   It is smaller than the lead and it is worth fixing for the same reason: on
   the boots where it happens, the whole page moves after it looked finished.

   The reservation is one line of .home-date — 10.5px uppercase, which lays out
   at twelve — and is written against --type-label rather than against 10.5 so
   that it follows the type when the sweep collapses the five sizes at the
   bottom of the app into one. 1.2 is the ratio a normal line box comes out at
   for a face this size. The date is always exactly one line: it is built by
   toLocaleDateString and there is no branch that makes it two. */
#homeSub{ min-height: calc(var(--type-label) * 1.2); }

/* ===========================================================================
   THE TOP OF HOME.
   ===========================================================================

   Row 1750. Measured at 1440 with the page full: the first tile started 254px
   down, and only 179px of that was type — the greeting, the date and the lead.
   The other 75 was padding, in five places, none of them large enough to
   notice on its own. The band has been trimmed once before (see the note by
   .home-band) and the note there is the rule this follows too: take it out of
   every gap rather than out of one, and do not shrink a single letter. The
   greeting, the date and everything in the band are exactly the size they were.
   After this the first tile starts at 226px. */


  /* ======================================================================
     HOME — THE STATE OF THINGS, AND THE LAUNCHPAD ON THE SPINE
     ====================================================================== */
.home-band {
  padding: 10px var(--space-8) 10px;
  margin-bottom: var(--space-4);
}

.home-band-top{ align-items:baseline; padding-right:32px; gap:var(--space-4); }

.home-hero{ display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; flex:1 1 340px; }

/* Typography is the whole design here. Fraunces at a real display size, one
     step of tracking tightened, and a line height that lets it breathe — not
     a 27px bold heading that could belong to any admin panel. */
.home-greet {
  font-family: var(--font-display);
  font-weight: 500;
  letter-spacing: -.022em;
  line-height: 1.04;
  font-variation-settings: 'opsz' 96;
  font-size: var(--type-fluid-1);
  margin: 0;
}

/* FULL-STRENGTH, because it is sitting on the painted band. --text-dim over
     the sky measured 3.74:1 on three of the sixteen themes, and this is the one
     sentence on Home that is about today. */
.home-today{ color:var(--text); font-size:var(--type-lead); min-width:0; }

.home-lanes{ display:grid; grid-template-columns:repeat(2, minmax(0,1fr));
    gap:var(--space-3); margin:0 0 var(--space-6); }

.lane{ border:1px solid var(--glass-border); border-radius:var(--radius-card);
    background:rgba(255,255,255,.016); padding:var(--space-4);
    display:grid; grid-template-columns:auto minmax(0,1fr); gap:0 var(--space-4);
    align-content:start; min-height:132px; }

/* THE RULE IS THE COLOUR AND THE WORD IS NOT. Eight lanes is eight hues, and
     eleven-pixel bold in a category colour measured between 3.85:1 and 4.49:1
     across the themes — readable on midnight, not readable on three of the
     others. The hue is still there, in the 18px rule and the dot beside it,
     where it is a fill and can be as saturated as it likes. */

.lane-fig{ font-family:var(--font-display); font-size:var(--type-figure);
    font-weight:600; line-height:1; letter-spacing:-.02em; padding-right:var(--space-2);
    border-right:1px solid rgba(255,255,255,.07); align-self:start; min-width:62px;
    color:var(--text); }

.lane-fig small{ display:block; font-family:inherit; font-size:10px; font-weight:600;
    letter-spacing:.1em; text-transform:uppercase; color:var(--text-dim);
    margin-top:var(--space-1); font-family:var(--font-body); }

.lane-body{ min-width:0; }

/* TWO LINES ARE RESERVED WHETHER TWO ARE USED OR NOT, and the drift line is
     always in the markup even when it is holding nothing. A lane that has not
     been read yet is drawn at boot, before the network is asked anything; when
     the answer lands it must become the read lane WITHOUT changing height, or
     the eight of them shove the whole grid down the page. Same idea as the
     date's reserved line under the greeting, and as #homeLead's frame before
     the rebuild deleted that element — see the note by row 1824. */
.lane-body p{ margin:0 0 var(--space-2); color:var(--text-dim); font-size:var(--type-body);
    line-height:1.5; min-height:3em; }

.lane-body p b{ color:var(--text); font-weight:600; }

/* UP AND DOWN ARE THE ARROW, NOT A SECOND COLOUR. Same reason as above, and
     it is the better sign anyway: ↑ and ↓ survive being printed, dimmed, and
     looked at by somebody who does not see the difference between orange and
     teal. */

.lane .btn{ font-size:var(--type-body); padding:4px var(--space-3); }

/* A LANE THAT COULD NOT BE READ IS NOT A LANE WITH NOTHING IN IT. Rule 211,
     drawn: the figure goes to a dash, the edge turns, and the sentence says
     which of the two it is. */
.lane[data-state="empty"] .lane-fig{ color:var(--text-dim); }

.lane[data-state="waiting"], .lane[data-state="failed"]{ background:transparent; }

.lane[data-state="waiting"] .lane-fig{ color:var(--text-faint); }

/* NO WASH BEHIND THE ONE SENTENCE THAT HAS TO BE READ. A seven-per-cent
     orange tint took "That read did not come back" to 4.39:1. The edge, the
     dash and the words are the difference; the ground stays the ground. */
.lane[data-state="failed"]{ border-color:color-mix(in srgb, var(--cat-orange) 55%, transparent); }

.lane[data-state="failed"] .lane-body p{ color:var(--text); }

.lane[data-state="failed"] .lane-fig{ color:var(--cat-orange-text, var(--cat-orange));
    border-right-color:color-mix(in srgb, var(--cat-orange) 34%, transparent); }

@media (max-width:900px) {
  .home-lanes{ grid-template-columns:minmax(0,1fr); }
}

@media (max-width:700px) {
  .home-band{ padding:8px var(--space-4) 10px; margin-bottom:var(--space-3); }
  .home-band-top{ padding-right:46px; }
  .home-greet{ font-size:var(--type-title); }
  .home-today{ font-size:var(--type-body); }
  .lane{ min-height:0; }
}

/* ==========================================================================
   HOME, REBUILT — THE STRIP ACROSS THE TOP.      (25 September)

   "I like #5 — rail as a top strip."                                  (Matt.)

   The eight lanes used to be eight cards, 132 pixels tall, two to a row. They
   are the same eight lanes with the same eight numbers; what has changed is
   that each one is now a single line, number and label side by side, and all
   eight sit in one row under the hero.

   These rules are at the end of the file rather than in place because the
   originals are two thousand lines up inside another block, and a rebuild
   that can be read beside what it replaces is easier to undo than one that
   has overwritten it.
   ========================================================================== */

/* EIGHT COLUMNS WAS A COUNT OF THE LANES, NOT A WIDTH ANY LABEL FITS IN.
   8 October. Matt: the chip labels are truncated.

   MEASURED (tests/measure-home-chips.js), with every label's own requirement
   read off the rendered page rather than guessed at:

     widest sub   "renew in 30 days"      86px
     widest name  "Birthdays this month"  107px   <- a stat chip, and the
                                                     longest thing the strip
                                                     can ever be asked to say
     padding + border per chip             20px
     gap between figure and words           8px
     the figure itself, at four digits      38px  (.lane-fig is flex:none, so
                                                  every pixel it takes comes
                                                  straight out of the words)

   173px. That is what a chip needs to never cut. Eight columns gave it 136px
   at his window and 118px at 1200, so the ellipsis in `.lane-sub` was not a
   bug firing — it was the only thing left to do.

   175 is 173 with two pixels over it. Not a round number: the two numbers it
   is made of are above, and if a label is ever reworded this floor is what
   has to be re-measured.

   A COUNT, DERIVED, RATHER THAN DECLARED AT TWO WIDTHS. The right number of
   columns is whatever 175px fits into HIS window, and that changed the day he
   resized it. Measured at nine widths from 761 to 1728: nothing cut at any of
   them. 180 was also clean but drops to three columns at 761 and costs 51px of
   height there; 170 passed every width tested but sits UNDER the 173
   requirement, so a track landing exactly on the floor would clip 'Birthdays
   this month' by 3px. Both are written down here so nobody re-derives them.

   auto-FILL, NOT auto-FIT, AND THE HOUSE ALREADY PAID TO LEARN THIS. auto-fit
   collapses the tracks it has not filled, so a wide window with only the eight
   lanes on it would stretch eight chips across the whole strip — the same
   fault .pulse-tiles and .goal-dash had, written up beside both of them, and
   .goal-dash is this rule's nearest relative: a strip of figures, auto-fill,
   its own measured floor. The chip count here is not fixed either — the four
   stat chips each appear only when their figure is above zero, so the strip is
   eight chips on a quiet day and twelve on a busy one, which is exactly the
   case that collapses. auto-fill keeps the empty tracks and the chips stay
   chip-sized with the row ending in gaps.

   min(100%, 175px) so a container narrower than the floor gets a chip the
   width of the column instead of one hanging off its edge. */
#homeLanes.home-lanes{ display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(100%, 175px), 1fr));
  gap:var(--space-2); margin:0 0 var(--space-5); }

.home-lanes .lane{ position:relative; display:flex; align-items:baseline;
  gap:var(--space-2); width:100%; min-height:0; min-width:0; text-align:left;
  padding:7px 9px 7px 13px; border-radius:8px;
  border:1px solid var(--glass-border); background:rgba(255,255,255,.016);
  font:inherit; color:var(--text); cursor:pointer; }

.home-lanes .lane:hover{ background:rgba(255,255,255,.055); }

.home-lanes .lane:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; }

/* THE HUE IS A BAR DOWN THE EDGE AND NOT THE WORD. Same reasoning as the rule
   and dot it replaces: eleven-pixel bold in a category colour measured under
   4.5:1 on several of the sixteen themes. As a 2px fill it can be as saturated
   as it likes. */
.home-lanes .lane::before{ content:''; position:absolute; left:0; top:6px; bottom:6px;
  width:2px; border-radius:0 1px 1px 0; background:var(--hue); }

.home-lanes .lane-fig{ font-family:var(--font-display); font-size:var(--type-lead);
  font-weight:600; line-height:1.15; letter-spacing:-.02em; color:var(--text);
  min-width:22px; flex:none; border-right:0; padding-right:0; }

.home-lanes .lane-body{ min-width:0; display:block; }

.home-lanes .lane-name{ display:block; font-size:11px; line-height:1.2; font-weight:600;
  color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

.home-lanes .lane-sub{ display:block; font-size:var(--type-label); line-height:1.2; color:var(--text-dim);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* THE HOME LANES, 3px SHORT OF A FINGER. 41 tall at phone width; Matt,
   30 September: to 44. Here, after the rule above, because that rule's
   min-height:0 comes later than the phone block and would win. Centred, so the
   extra height sits evenly above and below. */

@media (hover: none) and (pointer: coarse), (max-width: 480px) {
  .home-lanes .lane{ min-height:44px; align-items:center; }
}

/* RULE 211, AT A FIFTH OF THE SIZE. A read that failed must never draw like a
   read that came back empty. Three marks carry it: the dash instead of a zero,
   the word under the label, and the edge. The sentence is on the title. */
.home-lanes .lane[data-state="empty"] .lane-fig{ color:var(--text-dim); }

.home-lanes .lane[data-state="waiting"]{ background:transparent; }

.home-lanes .lane[data-state="waiting"] .lane-fig{ color:var(--text-faint); }

.home-lanes .lane[data-state="failed"]{ background:transparent;
  border-color:color-mix(in srgb, var(--cat-orange) 55%, transparent); }

.home-lanes .lane[data-state="failed"] .lane-fig{ color:var(--cat-orange-text, var(--cat-orange)); }

.home-lanes .lane[data-state="failed"] .lane-sub{ color:var(--text); }

/* THE FOUR-COLUMN QUERY IS GONE, AND IT WAS THE WORST OF IT.
   It set four columns under 1180 and left eight above, so at 1181 the strip
   jumped from four columns of 237px to eight of 122px — the labels were
   INTACT at 1180 and five of them were cut at 1181. Measured: 1099 and 1150
   cut nothing, 1181 cut five, 1200 cut four, 1250 cut two, 1294 cut one. The
   whole band from 1181 up to about 1290 was broken, and nothing was wrong
   with the two widths either side of it.

   The auto-fill rule above does this job and cannot have an edge like that,
   because the column count is derived from the width instead of being declared
   at two of them. */

/* ON A PHONE THE STRIP IS ONE ROW THAT SLIDES, NOT FOUR ROWS THAT STACK.
   Eight chips two-up is four rows and 200 pixels, which is the old problem in
   a smaller coat. One sliding row costs 46 and keeps the widgets on the
   screen. Each chip is a whole snap stop, so nothing is ever half on. */

@media (max-width:760px) {
  #homeLanes.home-lanes{ display:flex; flex-wrap:nowrap; overflow-x:auto;
      scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch;
      gap:var(--space-2); padding-bottom:2px; }
  /* width:auto UNDOES THE width:100% ABOVE, WHICH WAS COSTING NINE SWIPES.
  The base .lane rule sets width:100%, and for a flex item flex-basis:auto
  reads that width — so every chip was sizing itself to the whole strip and
  min-width:134px never got a say. Measured at 760: ten chips of 728px and
  7,352px of sideways scroll, which is one chip per screen. 134 was never
  reached and was never wrong; it was overruled.
  
  175px to match the grid floor above, for the same measured reason: the
  longest name the strip can hold needs 173. It buys fewer chips per screen
  than 134 would have, and the labels on them are whole. */
  .home-lanes .lane{ flex:0 0 auto; width:auto; min-width:175px;
      scroll-snap-align:start; }
}

/* ==========================================================================
   HOME, REBUILT — THE HERO, AND ITS TWO SCENES.          (25 September)

   A window, not a dashboard. Nothing in here is boxed or bordered: it is text
   and one thin band, over the sky that was already there.
   ========================================================================== */

/* THE BAND GETS ITS HEIGHT BACK. It was squeezed to 10px of padding when the
   eight lanes below it were doing the talking; now it IS the talking. */
#homeBand.home-band{ padding:var(--space-5) var(--space-8) var(--space-5); }

/* RESERVED BEFORE IT IS FILLED. The calendar, the weather and the signals are
   three separate requests and all three land after the first paint. Without a
   floor here the whole page below drops ~200px when they do. */
.hero-scene{ display:flex; flex-direction:column; justify-content:space-between;
  gap:var(--space-3); min-height:186px; }

.hero-top{ display:flex; flex-direction:column; gap:var(--space-2); }

.hero-foot{ display:flex; flex-direction:column; gap:6px; }

.hero-next-row{ display:flex; align-items:center; gap:var(--space-4); flex-wrap:wrap; }

.hero-next-t{ font-size:min(var(--type-fluid-1), var(--type-page)); font-weight:600; letter-spacing:-.01em;
  color:var(--text); min-width:0; }

.hero-next-t b{ font-weight:600; }

.hero-next-when{ color:var(--text); opacity:.72; font-size:var(--type-body); }

.hero-quiet{ font-weight:500; opacity:.86; }

/* ONE OF THE TWO THINGS IN THE HERO THAT IS PRESSABLE. */
.hero-join{ flex:none; padding:6px var(--space-4); border-radius:var(--radius-control);
  border:1px solid color-mix(in srgb, var(--green) 55%, transparent);
  background:color-mix(in srgb, var(--green) 24%, transparent);
  color:var(--text); font-size:var(--type-body); font-weight:600; text-decoration:none; }

.hero-join:hover{ background:color-mix(in srgb, var(--green) 36%, transparent); }

.hero-join:focus-visible{ outline:2px solid var(--green); outline-offset:2px; }

/* AND A MEETING WITH NO LINK ON IT SAYS SO, rather than offering a button that
   goes nowhere. Two of Matt's mornings this week went that way. */
.hero-nojoin{ flex:none; font-size:var(--type-label); color:var(--text);
  opacity:.8; border:1px dashed var(--glass-border-hi); border-radius:var(--radius-control);
  padding:5px var(--space-3); }

/* ==========================================================================
   THE SHAPE — the hero timeline, as a silhouette.        (Worker HERO, 9 Oct)
   ==========================================================================
   Matt saw three mocks and picked this one: "I like one and the silhouttes.
   (not stretch) Apply that."

   WHAT IT REPLACES. .hero-band — a hairline rail with a lozenge per meeting,
   two drawings of it (ribbon and blocks) and a Quieter/Bolder switch to choose
   between them. Every one of those was a ROW OF CHIPS. This is not: the height
   of the curve at any point is how much is stacked on the day at that moment,
   so a packed morning and an open afternoon are a shape you read before you
   read a word.

   SEVEN SILHOUETTES, NOT ONE STRETCHED CURVE. In week mode this draws seven
   small curves side by side, each keeping its own shape. Matt rejected the
   stretched version and the mock says why in its own words: a week has seven
   data points where a day has twenty-four, so stretching blurs Wednesday into
   Thursday and makes an empty Friday read as a gentle dip instead of a clear
   day. docs/mocks/hero-week-mode.html draws both, the stretched one labelled
   as the one not to build.

   EVERY COLOUR HERE IS A TOKEN THAT EXISTS, and two of them are written with a
   fallback on purpose: --brass-2 and --glass-border-hi are stated per theme but
   are NOT in the base :root floor at the top of this file, so a theme that does
   not state them would silently kill the whole declaration they appear in.
   --brass, --area-* , --text and --text-faint ARE in the floor and are used
   bare.

   NO COLOUR CARRIES A MEANING ON ITS OWN. The curve is tinted with the colour
   of the calendar the day's weight actually came off — the same colour that
   meeting already has in the agenda and on the day grid — but nothing is only
   said in colour: a clear day is said in the word "Clear", a day that could not
   be read is said in a sentence, and every piece of type in here is the theme's
   own --text on the theme's own ground. That is what makes it survive fourteen
   themes and three papers without a contrast calculation per theme. */
.hero-shape{ position:relative; margin-top:var(--space-3);
  display:grid; grid-template-columns:auto minmax(0, 1fr); gap:var(--space-6);
  align-items:start; }

/* MORE INK, BECAUSE THE HARNESS MEASURED IT AND IT WAS NOT ENOUGH.
   ---------------------------------------------------------------------------
   Everything small in here was first written as the theme's own --text at a
   low opacity, which is the pattern the hero already used and which is right
   on Hearth and Ink. It is not right on the paper grounds. Measured off the
   actual pixels by tests/test-readability.js on 9 October:

       .hs-d3  "Mon"        11px   4.23:1 on chocolate, 3.44:1 on serene:linen
       .hs-sub "5 – 11 Oct" 11px   3.87:1 on serene:linen
       .hs-tog "Day"        11px   3.87:1 on serene:linen

   all against a floor of 4.5. The harness's own note at the top of that file
   says why darkening the token barely moves the number: at eleven pixels the
   strokes are thinner than a pixel, so every pixel is already a blend with the
   paper, and WEIGHT AND OPACITY TOGETHER are what put ink on the glass. So the
   small print in this component is heavier and less faded than the first draft
   of it. It is still the quietest type on the band; it is no longer a rumour.
   --------------------------------------------------------------------------- */
/* ---- the gauge, left, with the switch under it -------------------------
   The frame from the "B" mock. It reads FRI / 9 October in day mode and
   WK 41 / 6 – 12 Oct in week mode, so the thing that changes when you press
   the switch is the thing the switch is under. */
.hs-gauge{ min-width:104px; padding-top:2px; }

.hs-num{ font-family:var(--font-display); font-weight:400;
  font-size:var(--type-display); line-height:.9; letter-spacing:-.025em;
  color:var(--text); font-variant-numeric:tabular-nums; white-space:nowrap; }

.hs-sub{ margin-top:var(--space-2); font-size:var(--type-label); font-weight:600;
  letter-spacing:.09em; text-transform:uppercase; color:var(--text); opacity:.82;
  white-space:nowrap; }

.hs-tog{ display:inline-flex; margin-top:var(--space-3); padding:2px; gap:2px;
  border-radius:var(--radius-pill);
  background:color-mix(in srgb, var(--bg-base) 55%, transparent);
  border:1px solid color-mix(in srgb, var(--text) 12%, transparent); }

.hs-tog button{ appearance:none; border:0; cursor:pointer; font:inherit;
  font-size:var(--type-label); font-weight:600; letter-spacing:.09em;
  text-transform:uppercase; line-height:1; padding:6px var(--space-3);
  border-radius:var(--radius-pill); background:transparent;
  color:var(--text); opacity:.84; }

.hs-tog button:hover{ opacity:1; }

.hs-tog button:focus-visible{ outline:2px solid var(--text); outline-offset:2px;
  opacity:1; }

/* THE PRESSED ONE IS NOT SAID ONLY IN COLOUR. It carries the brass fill AND
   full-weight ink AND the pressed state in the markup, so it is still obvious
   on a ground where brass and the band happen to be close. --blk-ink is not
   available here, so the ink on the brass is picked by the one thing true of
   every theme's brass: it is a mid-tone metal, and near-black reads on all
   twenty-one of them. */
.hs-tog button[aria-pressed="true"]{ background:var(--brass); opacity:1;
  color:#120A04; }

/* ---- the body ---------------------------------------------------------- */
.hs-body{ min-width:0; position:relative; }

/* DAY — one silhouette, 24 hours of it, with the hours written underneath. */
.hs-one{ position:relative; height:102px; }

.hs-sv{ display:block; width:100%; height:76px; overflow:visible; }

/* The gradient stops are styled from CSS rather than written into the markup,
   so the colour arrives as one custom property on the <svg> and there is no
   second place for it to disagree with itself. --hs-c is set per curve by
   heroShapeHtml(); the fallback is the metal, which every theme states. */
.hs-sv .hs-f0{ stop-color:var(--hs-c, var(--brass)); stop-opacity:.46; }

.hs-sv .hs-f1{ stop-color:var(--hs-c, var(--brass)); stop-opacity:.12; }

.hs-sv .hs-f2{ stop-color:var(--hs-c, var(--brass)); stop-opacity:0; }

.hs-sv .hs-line{ stroke:var(--hs-c, var(--brass)); fill:none; stroke-width:1.75;
  stroke-linecap:round; stroke-linejoin:round; }

.hs-sv .hs-area{ stroke:none; }

/* The marker for now. A hairline that fades up out of nothing, with a dot on
   the baseline — the same two parts the rail's own now-marker had. */
.hs-now{ position:absolute; top:0; height:76px; width:2px; border-radius:1px;
  transform:translateX(-50%); pointer-events:none;
  background:linear-gradient(180deg, transparent, var(--text)); }

.hs-now::after{ content:''; position:absolute; left:50%; bottom:-3px;
  transform:translateX(-50%); width:7px; height:7px;
  border-radius:var(--radius-round); background:var(--text); }

/* A NAME ON THE SKY IS THE PAGE'S OWN INK ON THE PAGE'S OWN GROUND, which is
   why it needs no contrast arithmetic — the same reason the rail's ribbon
   names were moved out of their blocks and onto the band in September.
   HOW MUCH ROOM IT HAS IS WRITTEN INTO IT and it may not use more: --room is
   the distance to the next name that was kept, so two of them CANNOT print
   over one another whatever the typeface does. The bad case is a shortened
   name, never two names in one place. */
.hs-lab{ position:absolute; max-width:var(--room, 30%); min-width:0;
  white-space:nowrap; overflow:hidden; pointer-events:auto; }

.hs-lab b{ display:block; font-size:var(--type-label); font-weight:700;
  color:var(--text); overflow:hidden; text-overflow:ellipsis; }

.hs-lab i{ display:block; font-style:normal; font-size:var(--type-label);
  font-weight:600; font-variant-numeric:tabular-nums; color:var(--text);
  opacity:.84; }

.hs-lab[data-ev]{ cursor:pointer; }

.hs-lab[data-ev]:hover b{ text-decoration:underline; text-underline-offset:3px; }

.hs-lab[data-ev]:focus-visible{ outline:2px solid var(--text); outline-offset:3px;
  border-radius:var(--radius-tight); }

/* Ten pixels of air under the baseline. At four the curve's flat tail ran
   through the top of the 6A, and the two read as one smudged rule. */
.hs-hours{ position:absolute; left:0; right:0; top:86px; height:14px; }

.hs-hours span{ position:absolute; top:0; white-space:nowrap;
  font-size:var(--type-label); font-weight:600; letter-spacing:.08em;
  text-transform:uppercase;
  font-variant-numeric:tabular-nums; color:var(--text); opacity:.68; }

/* WEEK — seven of them, side by side, each on the same vertical scale.
   The shared scale is the whole point: Wednesday is only a mountain next to a
   Friday that is a plain, and two curves normalised against their own peaks
   would draw a day with one meeting and a day with six at the same height. */
.hs-seven{ display:grid; grid-template-columns:repeat(7, minmax(0, 1fr));
  gap:7px; height:96px; }

.hs-day{ position:relative; display:flex; flex-direction:column; min-width:0; }

.hs-day .hs-sv{ height:54px; }

.hs-dh{ position:relative; display:flex; align-items:baseline; gap:5px;
  margin-bottom:3px; min-width:0; }

.hs-dh span{ font-size:var(--type-label); font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; color:var(--text); opacity:.84; }

/* Mon on a window, M on a phone. Both are in the markup; this is the choice. */
.hs-dh .hs-d1{ display:none; letter-spacing:.04em; }

.hs-dh b{ font-family:var(--font-display); font-size:var(--type-body);
  font-weight:600; line-height:1; color:var(--text); opacity:.92;
  font-variant-numeric:tabular-nums; }

.hs-df{ position:relative; margin-top:2px; font-size:var(--type-label);
  font-weight:500; color:var(--text); opacity:.82; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }

/* THERE IS NO SEPARATE BASELINE RULE, and there was one for half an hour.
   A curve with nothing under it already draws its own flat tail along the
   bottom of its box, so a hairline behind it was a second ground at a slightly
   different height — on the phone the two were visibly a pixel apart and the
   seven columns stopped reading as one row. The dashed rule a clear day draws
   is now at exactly the height a flat tail sits at, so the ground is one line
   across all seven whatever is on them. */

/* TODAY KEEPS THE BRASS WASH AND THE BRASS BORDER so you never lose where you
   are in the week, and it says it a second way in the type weight below —
   because on a paper ground a brass wash at 13% is nearly nothing. */
/* THE WASH RISES OFF THE BASELINE RATHER THAN FALLING FROM THE TOP, which is
   the difference between a warm column and an empty box. Falling from the top
   put the strongest tint in the part of the column with nothing in it, and on
   a day that is clear — which today often is — the result was a tall dark
   slab with a dashed line at the bottom of it. The ink is at the bottom, so
   the warmth is at the bottom.

   AND IT IS MIXED FROM --brass-2 RATHER THAN --brass, because the metal itself
   is darker than a lit sky: photographed in the real app, twenty per cent of
   --brass over the band made today's column DARKER than the six beside it,
   which is the opposite of marking it. --brass-2 is the lighter of the pair
   and lifts. It is written with a fallback because it is stated per theme and
   is NOT in the base :root floor, and a var that resolves to nothing would
   take this whole declaration with it and leave today unmarked. The border
   stays --brass: it is a line, not a wash, and it reads on every ground. */
.hs-day.is-today::before{ content:''; position:absolute; left:-4px; right:-4px;
  top:-4px; bottom:16px; border-radius:var(--radius-control); pointer-events:none;
  background:linear-gradient(0deg,
    color-mix(in srgb, var(--brass-2, var(--brass)) 20%, transparent) 0%,
    color-mix(in srgb, var(--brass-2, var(--brass)) 7%, transparent) 46%, transparent 86%);
  border:1px solid color-mix(in srgb, var(--brass) 38%, transparent); }

.hs-day.is-today .hs-dh span{ opacity:1; }

.hs-day.is-today .hs-dh b{ opacity:1; }

.hs-day.is-today .hs-df{ opacity:1; font-weight:700; }

/* A CLEAR DAY IS A DASHED RULE AND THE WORD "CLEAR", NOT AN EMPTY BOX.
   House rule 211: an empty read and a failed read may not draw the same, and
   neither may an empty read and an empty element. The rule is drawn in the SVG
   so it sits exactly where a curve's baseline would; the word is said in the
   theme's own ink and marked a second way by the italic, NOT in brass —
   --brass-2 on linen, alabaster or parchment is a pale tan on near-white paper,
   which is the dark-ink-on-a-dark-slab fault with the lights on. */
.hs-sv .hs-clear{ stroke:var(--text); stroke-opacity:.34; stroke-width:1.5;
  stroke-linecap:round; stroke-dasharray:3 4; fill:none; }

.hs-day.is-clear .hs-df{ font-style:italic; opacity:.86; }

.hs-one.is-clear .hs-say{ position:absolute; left:0; top:28px;
  font-size:var(--type-body); font-style:italic; color:var(--text); opacity:.86; }

/* THE SUMMARY LINE, counted off the same list the curve is drawn from, so the
   words and the picture cannot disagree. */
.hs-say{ margin-top:var(--space-2); font-size:var(--type-body);
  color:var(--text); opacity:.86; }

.hs-say b{ font-weight:700; opacity:1; }

/* A READ THAT FAILED SAYS SO IN WORDS AND DRAWS NO CURVE AT ALL. It wears the
   house's existing amber rule rather than growing a second mark for one
   meaning. A flat line here would be a lie about an empty day. */
.hs-failed{ margin-top:var(--space-3); padding:var(--space-3) var(--space-4);
  border-left:2px solid var(--brass); border-radius:var(--radius-control);
  background:color-mix(in srgb, var(--brass) 9%, transparent);
  font-size:var(--type-body); color:var(--text); }

/* ---- WEEK MODE GOES FLAT NEUTRAL. ONE BLOCK, AND IT IS THE FLIP. ---------
   A week has no single hour, so painting it with one hour's dawn would be a
   picture that claims a fact the week does not have. Day mode keeps the real
   dawn/day/dusk/night sky untouched — paintHomeSky() still sets data-sky and
   nothing here changes it.
   TO PUT THE REAL SKY BACK ON THE WEEK: comment out this one block. Nothing
   else in the app reads data-hero-span. */
.home-band[data-hero-span="week"] .home-sky{
  --sky-a:var(--text-faint); --sky-b:var(--text-faint); --sky-orb:var(--text-faint); }

.home-band[data-hero-span="week"] .sky-orb{ opacity:.16; }

/* ---- end of the flip ---------------------------------------------------- */

/* ---- the narrow window ------------------------------------------------- */
/* Seven columns at 390px is 48px a column, which is enough for a curve and a
   date and not enough for a word under it. The footers go and the day letters
   stay, because the shape is the thing that survives being small. */

@media (max-width:760px) {
  .hero-shape{ grid-template-columns:minmax(0, 1fr); gap:var(--space-3); }
  .hs-num{ font-size:var(--type-figure); }
  .hs-gauge{ min-width:0; display:flex; align-items:center; gap:var(--space-2);
      flex-wrap:nowrap; }
  .hs-tog button{ padding:6px var(--space-2); }
  .hs-gauge .hs-sub{ margin-top:0; min-width:0; overflow:hidden;
      text-overflow:ellipsis; }
  .hs-gauge .hs-tog{ margin-top:0; margin-left:auto; flex:none; }
}

@media (max-width:560px) {
  /* THE COLUMNS STOP STRETCHING. A grid item fills its row by default, so
  .hs-day was 84px tall while its contents ended at 63 — and today's brass
  box, which is pinned to the bottom of its column, hung twenty pixels below
  the ground the other six stand on. */
  .hs-seven{ gap:4px; height:84px; align-items:start; }
  .hs-day.is-today::before{ bottom:-3px; }
  .hs-df{ display:none; }
  .hs-dh .hs-d3{ display:none; }
  .hs-dh .hs-d1{ display:inline; }
  .hs-dh{ gap:3px; }
  .hs-one{ height:92px; }
  /* The day's one curve and the week's seven are different heights. The
  second rule is the more specific of the two, so it wins wherever it is
  written — but it is written after anyway, because a reader should not
  have to count specificity to know which height a column gets. */
  .hs-sv{ height:66px; }
  .hs-day .hs-sv{ height:46px; }
  .hs-now{ height:66px; }
  .hs-hours{ top:76px; }
  /* Three hour marks instead of six: 6a, NOON, 10p. heroShapeHours() thins
  them in the markup as well, and this is the belt to that braces. */
  .hs-hours span[data-thin="1"]{ display:none; }
}

/* ===========================================================================
   THE HEAD OF THE HERO IS TWO COLUMNS NOW — 8 October 2026.
   ===========================================================================
   Matt: "move the weather widget from under timeline on left to above
   timeline on the right."

   The band was never the thing in the way: it is the full width of the hero
   and stays that way. What was wrong is that the weather — the one thing on
   this page he uses on the way out of the door — sat BELOW the band and hard
   left, under everything. It now shares a row with the next thing, above the
   band, hard right.

   .hero-head-r CARRIES .hero-foot AS WELL, on purpose. Every rule written for
   the weather over the last month addresses .hero-foot, including the evening
   scene's margin. Renaming it would have been the tidy change and would have
   taken the spacing with it. Only the column it sits in moved.

   auto ON THE RIGHT TRACK, not a fraction: the weather is three short lines
   and should take the room it needs and no more, so a long meeting title on
   the left is never squeezed by a column reserved for two words of weather.
   Below 760 the two stack and the right-hand one goes back to reading left,
   because a right-aligned column on a phone is a column against the wrong
   edge of the screen. */
.hero-head{ display:grid; grid-template-columns:minmax(0, 1fr) auto;
  align-items:start; gap:var(--space-3) var(--space-5); }

.hero-head-l{ min-width:0; }

.hero-head-r{ text-align:right; align-items:flex-end; }

.hero-head-r .hero-suggest{ align-items:flex-end; text-align:right; }

@media (max-width:760px) {
  .hero-head{ grid-template-columns:minmax(0, 1fr); }
  .hero-head-r{ text-align:left; align-items:flex-start; }
  .hero-head-r .hero-suggest{ align-items:flex-start; text-align:left; }
}

/* A BLOCK THAT OPENS ITS MEETING LOOKS LIKE ONE.
   --------------------------------------------------------------------------
   Matt, 8 October: "nothing clickable". Only the blocks that actually carry an
   id get this, because the cursor is a promise. */


/* THE LINE THAT SAYS WHAT THE PICTURE SAYS. Every part of it is counted off
   the same list the blocks are drawn from, in heroShapeHtml(), so it cannot
   disagree with the band underneath it. */

/* THE DAY ALREADY SPENT. Drawn from the left edge to the marker for now and
   nowhere else — it is the clock, not a mood, and it is the reason the band
   reads as a day in progress rather than a list lying on its side. */

/* THE HOURS. A mark every two hours beats 8a and 6p at the two ends, which is
   all there was: with these you can tell half past ten from half past three. */

/* NOW. A line with a cap on it, so it reads as a marker rather than as a very
   thin meeting. */

/* AN EMPTY DAY IS A STATE, NOT AN ABSENCE.   (House rule 211, drawn.)
   The band keeps its hours and keeps its marker for now, and says in words
   that there is nothing on it. A calendar that could not be READ draws no band
   at all — heroShapeHtml() returns the empty string — so the two are not the
   same picture and never can be. */
/* And in the trough it starts at the left rather than the middle, because the
   middle of the band is roughly where the marker for now stands on an
   afternoon — on a phone the line went straight through the word "today". */

/* ---- the blocks, and the names, which are two layers ------------------- */
/* THE NAMES SIT OVER THE BLOCKS RATHER THAN INSIDE THEM — see the note in
   heroShapeHtml(). Each one carries its block's left edge and width, so the
   two layers line up by construction. */
/* THE CLOCK TIME IS QUIETER THAN THE NAME, AND IT IS THE WEIGHT THAT SAYS SO,
   NOT A FADE. Fading it was the obvious way and it was the wrong one: at .72
   the time measured 4.44:1 against the sky on fathom, the brightest of the
   twelve grounds, and at .9 it still measured 4.30. Eleven-pixel type has very
   little to give away. Set at the page's own ink in a lighter weight it
   measures 4.9 there and better everywhere else, and it still reads as the
   quieter half of the caption. The numbers come out of the actual picture —
   see tests/test-the-day-band.js — and a fade put back here has to be
   measured again on all twelve. */

/* ---- RIBBON. The quiet drawing, and the one this build ships. ----------- */
/* Headroom for the row of names above the rail. */
/* THE NAME SITS ON THE SKY, ABOVE THE BLOCK, which is why it needs no contrast
   arithmetic: it is the page's own ink on the page's own ground. */
/* A one-pixel stem down to the block, so a name belongs to a meeting by sight
   and not only by being roughly above it. */
/* THE NAME SITS OVER ITS BLOCK, in whichever of near-black or white measures
   better against that block's own face — readableInk() decides, per block,
   and a block whose colour could not be resolved is never named at all rather
   than named unreadably. */
/* THE CLOCK TIME COMES OFF A NAME SET INSIDE ITS BLOCK. The hours are written
   along the band and the block is drawn at its own hour, so "3:00 PM" inside
   it is the one thing on screen the picture already says — and it was eating
   two thirds of the room, which turned every title into an ellipsis. */

/* ---- the suggestion. The other pressable thing. ------------------------- */
.hero-suggest{ display:block; min-width:0; }

.hero-suggest.is-press{ cursor:pointer; }

.hero-suggest.is-press:hover .hero-suggest-t{ text-decoration:underline; text-underline-offset:3px; }

.hero-suggest.is-press:focus-visible{ outline:2px solid var(--blue); outline-offset:3px;
  border-radius:var(--radius-control); }

.hero-suggest-k{ display:block; font-size:var(--type-label); font-weight:700;
  letter-spacing:.14em; text-transform:uppercase; color:var(--text); opacity:.66; }

.hero-suggest-t{ display:block; font-size:var(--type-lead); font-weight:600; color:var(--text); }

.hero-suggest-w{ display:block; font-size:var(--type-body); color:var(--text); opacity:.74; }

.hero-wx, .hero-tomorrow{ font-size:var(--type-body); color:var(--text); opacity:.7; }

/* ==========================================================================
   THE EVENING SCENE.  DEFAULT TREATMENT — "A", the one this build ships.

   The band goes, the suggestion takes the room it had, and tomorrow's first
   thing gets one quiet line at the bottom. The sky is a RECOLOUR of the same
   drawing rather than a second one, and the sun that sits top right in the day
   sits low and to one side. Which of those two is right is Matt's to pick —
   tools/home-options/evening-a|b|c.html draw all three at true size.
   ========================================================================== */
.hero-scene[data-scene="evening"]{ justify-content:flex-end; }

.hero-scene[data-scene="evening"] .hero-top{ margin-top:auto; }

/* CAPPED AT THE PAGE STEP. --type-fluid-1 tops out at 27px, one off the
   scale's 26, and on a wide screen that is what painted. Matt, 30 September:
   onto the scale. It still shrinks on a narrow window exactly as before. */
.hero-scene[data-scene="evening"] .hero-suggest-t{ font-size:min(var(--type-fluid-1), var(--type-page));
  letter-spacing:-.01em; line-height:1.15; }

.hero-scene[data-scene="evening"] .hero-suggest-w{ font-size:var(--type-lead); opacity:.8; }

.hero-scene[data-scene="evening"] .hero-foot{ margin-top:var(--space-4); }

/* THE SKY GOES WARMER AND LOWER. A recolour of the same layers — no second
   code path, nothing to keep in step. */
.home-band[data-scene="evening"] .home-sky{ --sky-a:#ff8a52; --sky-b:#3f2f6e; --sky-orb:#ffb070; }

.home-band[data-scene="evening"] .sky-orb{ right:auto; left:14%; top:auto; bottom:-22%; }

.home-band[data-scene="evening"] .sky-far{ opacity:.55; }

@media (max-width:760px) {
  #homeBand.home-band{ padding:var(--space-4) var(--space-4) var(--space-4); }
  .hero-scene{ min-height:150px; }
  .hero-next-t{ font-size:var(--type-lead); }
  .hero-scene[data-scene="evening"] .hero-suggest-t{ font-size:var(--type-lead); }
}

/* The four stat chips on the end of the strip. Same chip, no hue bar — they
   are counts rather than one of the eight, and the edge should say so. */
.home-lanes .lane-stat::before{ display:none; }

.home-lanes .lane-stat{ padding-left:9px; border-style:dashed; }

span.home-lanes .lane-stat, .home-lanes span.lane-stat{ cursor:default; }

/* THE SUGGESTION SLOT AND ITS THREE WORDS.   (Worker HO, 25 September 2026)

   QUIET LOOKS LIKE A QUIET DAY. There is no rule in here for a badge, a count
   or a "suggestions waiting" pill, because there is no such element: the slot
   renders the empty string in both cases and tests/test-the-off-switch.js
   compares the two. A counter of held-back things is a nag wearing a smaller
   hat, and the fastest way for one to appear is for a style to be waiting for
   it. */
#lucidSuggest:empty { display: none; }

/* ==========================================================================
   WHAT IS ON TONIGHT — the Life board's scroll.          (Matt, 26 September.)
   --------------------------------------------------------------------------
   THE CARD IS .suggest-slot, UNMODIFIED. Home has one card shape and this is
   it; a second one would make the shape mean nothing. The failed-read state
   reuses [data-state="failed"] above, so the amber left rule keeps meaning
   exactly one thing — Lucid tried to look and could not — on this card as on
   the other one.

   ONLY THE STRIP INSIDE IT IS NEW, and it is wide rather than tall on purpose:
   it is a scroll, so a night with nine games is one gesture instead of nine
   rows pushing the rest of Life off the screen.
   ========================================================================== */
.tonight-slot { max-width: none; }

.tonight-scroll {
  display: flex; flex-wrap: nowrap; gap: var(--space-2);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
  /* Room for the chips' focus ring, which is drawn outside them. Without it
     the ring is clipped by the scroller on the first and last chip and keyboard
     focus becomes invisible at exactly the two ends people tab to. */
  padding: 2px; margin: -2px;
}

.tonight-g {
  flex: 0 0 auto; scroll-snap-align: start;
  display: flex; flex-direction: column; gap: 2px;
  text-align: left; padding: 7px 11px; min-width: 132px; max-width: 260px;
  border-radius: 8px; border: 1px solid var(--glass-border);
  background: rgba(255,255,255,.016);
  font: inherit; color: var(--text); cursor: pointer;
}

.tonight-g:hover { background: rgba(255,255,255,.055); }

.tonight-g:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }

.tonight-who {
  font-size: var(--type-body); font-weight: 600; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The clock time, or the score. --text-dim at the label step, so the two names
   are what the eye lands on and the figure is read second. */
.tonight-when {
  font-size: var(--type-label); color: var(--text-dim);
  line-height: 1.3; white-space: nowrap;
}

/* A GAME IN PROGRESS, AND THE MARK IS NOT THE FAILED-READ ONE. --green, on the
   figure only, because "this is happening now" and "Lucid could not look" are
   two different facts and the amber rule already owns the second. */
.tonight-g[data-live="1"] .tonight-when { color: var(--green); font-weight: 600; }

/* TI-211 — A TILE THAT COULD NOT READ, WHICH IS NOT A TILE WITH NOTHING IN IT.
   --------------------------------------------------------------------------
   .ht-empty is the "nothing here yet" sentence, 89 of them. These two are the
   other two states, and they exist because wording alone was carrying the whole
   difference: "No reminders set." and "Lucid could not read your reminders."
   were the same dim grey sentence in the same box, and the second one is the
   only one of the two that is ever urgent.

   DELIBERATELY THE SAME MARK AS .feed-stale ABOVE — the 2px left rule and
   --orange, on --glass-bg. A feed that has stopped and a tile that could not
   read are the same kind of news to the person reading them, and giving one
   meaning two marks is how somebody learns to read neither. Not a red: the
   scale has no --red and this is a correction, not an emergency.

   The type is .ht-empty's type, because this is still a sentence inside a tile
   and not a banner across it. THE COLOUR AND THE EXACT MARK ARE MATT'S CALL;
   this is the plain version that is provably not the empty state. */
.ht-unread, .ht-stale{
  font-size:var(--type-body); color:var(--text); line-height:1.6;
  padding:var(--space-2) 0 var(--space-2) var(--space-3);
  border-left:2px solid var(--orange);
  max-width:var(--prose-measure); }

/* The reason, in the dim colour, because it is for whoever has to fix it
   rather than the first thing to read. */
.ht-unread-why{ color:var(--text-dim); margin:var(--space-1) 0 0 0; }

@media (max-width: 780px), (hover: none) and (pointer: coarse) and (max-height: 500px) {
  /* Home's Day / Week: widened, not grown. */
  .hs-tog button{ position:relative; z-index:1; }
  .hs-tog button::after{ content:''; position:absolute; left:50%; top:50%;
      width:100%; height:100%; min-width:44px; min-height:44px;
      transform:translate(-50%,-50%); }
  /* The names on the day curve: the box grows by a space-2 above and below and
  gives it straight back in margin, so the words stay where they were drawn
  and the finger gets 44. Above the curve, so the line does not take it. */
  .hs-lab[data-ev]{ z-index:1; padding-block:var(--space-2); margin-top:calc(-1 * var(--space-2)); }
  /* 1. TYPE, the one raw size on Home that skipped the scale: the lane names
  under the day curve were written as 11px and stayed 11. */
  .home-lanes .lane-name{ font-size:var(--type-label); }
}

/* ==========================================================================
   LUCID'S OWN JOBS, AND HELP & FEEDBACK — 15 October (team OPS).

   RED IS SPENT HERE AND ONLY HERE ON HOME. A failed nightly job or backup is
   the one thing on the strip that is Lucid's fault rather than the day's, and
   the brief is that Matt must find out. --cat-red is the existing category
   red; the chip keeps its shape, and the edge, the bar and the figure turn.
   Orange stays "could not read" (rule 211): a red chip and an orange chip are
   two different facts.
   ========================================================================== */
.home-lanes .lane[data-state="alarm"]{ background:color-mix(in srgb, var(--cat-red) 10%, transparent);
  border-color:color-mix(in srgb, var(--cat-red) 70%, transparent); }

.home-lanes .lane[data-state="alarm"]::before{ background:var(--cat-red); }

.home-lanes .lane[data-state="alarm"] .lane-fig{ color:var(--cat-red-text, var(--cat-red)); }

.home-lanes .lane[data-state="alarm"] .lane-sub{ color:var(--text); }

#homeJobs:empty{ display:none; }

.jobs-alarm{ display:flex; align-items:center; gap:var(--space-3);
  margin:calc(-1 * var(--space-3)) 0 var(--space-5); padding:var(--space-2) var(--space-3);
  border:1px solid color-mix(in srgb, var(--cat-red) 60%, transparent);
  border-radius:var(--radius-control);
  background:color-mix(in srgb, var(--cat-red) 8%, transparent);
  font-size:var(--type-body); color:var(--text); }

.jobs-alarm-dot{ flex:none; width:var(--space-2); height:var(--space-2); border-radius:var(--radius-round);
  background:var(--cat-red); }

.jobs-alarm-text{ flex:1; min-width:0; }

.jobs-alarm-text b{ color:var(--cat-red-text, var(--cat-red)); font-weight:700; }

.jobs-alarm-digest{ display:block; color:var(--text-dim); font-size:var(--type-label); margin-top:var(--space-1); }

.jobs-alarm-go{ flex:none; }

@media (max-width:760px) {
  .jobs-alarm{ flex-wrap:wrap; margin-top:0; }
  .jobs-alarm-go{ margin-left:auto; }
}

