/* ==========================================================================
   SPROUT — "Viridium"
   --------------------------------------------------------------------------
   A dark, jewel-toned botanical design system. Reference: rain-beaded leaves
   in deep forest green, lit from one side, with a high-contrast display
   serif in ivory.

   Principles:
     · Crisp over cuddly. Radii stay between 2 and 6px; luxury reads sharp.
     · Hairlines, not borders. Structure comes from 1px mint at 8–18% alpha.
     · Light is a material. Radial glows and specular edges do the work that
       drop shadows do in a light UI.
     · Motion is slow and weighted. Expo-out, 340–620ms, never bouncy.
     · Type carries the luxury: PP Hatton caps for display in two weights,
       Cormorant Garamond for editorial serif, Jost for UI, Sacramento for
       one flourish.

   Two themes, both premium: "viridium" (dark, default) and "conservatory"
   (light ivory) — set via data-theme on <html>. Part D holds the light map.

   Sections: A tokens/reset/type/shell · B components · C feature blocks
             D motion, utilities, themes, responsive, a11y
   ========================================================================== */

/* ==========================================================================
   A1 · Tokens
   ========================================================================== */

:root {
  /* --- Surfaces: near-black emerald, climbing to raised glass --- */
  --bg-0: #050A08;      /* deepest — behind everything */
  --bg-1: #0A1410;      /* page */
  --bg-2: #0E1B15;      /* surface / card */
  --bg-3: #13251D;      /* raised / hover */
  --bg-4: #193024;      /* pressed / selected */

  /* --- Glass: translucent white over the emerald, plus blur --- */
  --glass:    rgba(255, 255, 255, 0.045);
  --glass-2:  rgba(255, 255, 255, 0.075);
  --glass-3:  rgba(255, 255, 255, 0.11);

  /* --- Hairlines. Mint-tinted so they never look grey. --- */
  --hair:   rgba(214, 237, 226, 0.10);
  --hair-2: rgba(214, 237, 226, 0.17);
  --hair-3: rgba(214, 237, 226, 0.28);

  /* --- Ink --- */
  --ink:   #F3F6F1;
  --ink-2: rgba(228, 240, 232, 0.72);
  --ink-3: rgba(206, 226, 214, 0.50);
  --ink-4: rgba(196, 218, 206, 0.32);

  /* --- Greens --- */
  --emerald-deep: #0F3A29;
  --emerald:      #1F6B4C;
  --emerald-lit:  #2F9A70;
  --emerald-glow: #3FBE8B;
  --mint:         #9FD3BE;
  --mint-2:       #C9E7D9;
  --mint-wash:    rgba(63, 190, 139, 0.11);

  /* --- Champagne. The one metallic; used for premium cues and overdue. --- */
  --gold:      #C8A96B;
  --gold-lit:  #DFC58F;
  --gold-wash: rgba(200, 169, 107, 0.13);

  /* --- Semantic accents --- */
  /* --sun is gone. It was a second champagne a few degrees off --gold, and
     the two contradicted each other: .sev-med took its text from one and its
     cause mark from the other, inside the same card. A two-step ladder
     expressed in colours that close is not read as a ladder. Where a finer
     step is genuinely needed the form carries it — .drop-now still breathes
     where .drop-due sits still. */
  /* There was a --sky here, a cornflower blue, used for watering and for the
     weather glyph. It is gone. Emerald, ivory, champagne and terracotta is a
     closed set that holds together; a fifth hue borrowed from utility-app
     convention was the one thing on screen that looked like it came from a
     different product. Water now wears the house colour, which is also the
     honest reading — watering is the normal business of the app, not a
     warning, and mint is what "normal" looks like here.

     There was a --rose here too, and it has gone the same way. It held the
     top step of two scales: "highly toxic" one rung above terracotta's
     "toxic", and "act quickly" in Diagnose. Two objections, and the second
     is the one that settled it. First, it made five hues out of a set the
     paragraph above had just closed at four. Second, and worse, nobody can
     tell pink from terracotta at pill size and read an ordering off it —
     they are adjacent warm hues, so the scale's most urgent step was
     signalled by a distinction the eye resolves as "some warm colour".
     A scale's top step has to be louder, not merely other. Terracotta now
     covers both rungs and the top one is filled rather than washed, which
     reads instantly and from across the room. See .pill-terra-hi. */
  --terra:      #D08A62;
  --terra-soft: rgba(208, 138, 98, 0.14);

  /* --- Oxblood. The fourth rung, and the only one that is not a degree of
     the other three: it marks a fact that cannot be taken back — an animal
     that can be seriously harmed, an action with no undo.

     Fill only, in both themes. Measured against this ground it reads 1.72:1
     as text or a hairline, which is invisible; as a fill under ivory it
     reads between 7.4 and 11.6:1, which is better than the emerald primary
     manages. That asymmetry is not a preference, it is why the rule is
     "always filled" — the colour has no other legible use here.

     It also steps *down* from the ground rather than up, which is what makes
     it read as a different class of thing rather than a fourth degree of
     urgency. The pink that used to hold this slot differed only in hue, at
     the same lightness, and nobody could order the two. --- */
  --blood:      #6E2320;
  --blood-lit:  #8A2C28;
  --blood-deep: #571B19;
  --blood-ink:  #F6EDE9;
  --blood-edge: rgba(110, 35, 32, 0.85);

  /* Pill text. These exist because .pill-sun and .pill-terra used to set
     their text with a literal hex — #EBCB8B and #E2AC8C, both pale tints
     chosen to sit on near-black. The conservatory theme dutifully overrode
     --sun and --terra and it changed nothing, because the text colour was
     baked where no stylesheet could reach it: on ivory the pills printed
     pale champagne on pale champagne and pale terracotta on pale
     terracotta, legible only if you already knew what they said. Exactly
     the mistake the plant tiles made with their gradient. If a theme can
     override the wash, it must be able to override the ink. */
  --terra-ink:  #E2AC8C;
  --terra-hi-ink: #23120A;

  /* Ink for the one *filled* button — the emerald primary. A token and not a
     literal for the same reason as the pill inks above, but with a sharper
     edge to it: the theme override used to be a rule, `[data-theme=
     "conservatory"] .btn { color: ... }`, and at (0,2,0) that outranked
     every variant's own (0,1,0) `color`. So in Conservatory the ghost, soft,
     terracotta and danger buttons all had their ink forced to near-ivory —
     which on the outline variants means ivory type on ivory paper. The
     Delete button was invisible on its own background, and it is the one
     control in the app that must never be mistaken for decoration.

     It had already been noticed once and patched one variant at a time
     (`.btn-soft` got its own override), which is the shape of a bug being
     treated as four bugs. As a token the base declaration stays at (0,1,0)
     and the variants beat it on source order, the way the cascade was
     designed to work. */
  --btn-ink: #F4FBF7;

  /* --- Compatibility aliases. The view files reference these inline; the
         names are load-bearing even though the values are all new. --- */
  --paper:      var(--bg-1);
  --paper-deep: var(--bg-0);
  --surface:    var(--bg-2);
  --surface-2:  var(--bg-3);
  --leaf:       var(--emerald-lit);
  --leaf-deep:  var(--emerald-deep);
  --leaf-soft:  var(--mint-wash);
  --leaf-wash:  var(--mint-wash);
  --line:       var(--hair);
  --line-2:     var(--hair-2);

  /* --- Type --- */
  /* PP Hatton, self-hosted. Two weights standing in for an optical size
     axis the face does not have: the display role runs from 14px on a
     section title to 86px on a hero tile letter, and a high-contrast serif
     wants more weight as it shrinks and less as it grows.

       200  at 28px and above
       500  below 28px

     Every rule that names --display therefore names a weight too. Nothing
     is left to the browser to synthesise, because with only two faces
     installed a request for 400 would be resolved by guesswork.

     The fallback is Cormorant Garamond rather than Times: it is already
     loaded for the serif role, and a missing display face landing on the
     app's own body serif is a far smaller shock than landing on a system
     face nothing else here uses. */
  --display: 'PP Hatton', 'Cormorant Garamond', Didot, 'Times New Roman', serif;
  --serif:   'Cormorant Garamond', Didot, Georgia, 'Times New Roman', serif;
  --sans:    'Jost', 'Futura', 'Century Gothic', -apple-system, BlinkMacSystemFont,
             'Segoe UI', system-ui, sans-serif;
  --script:  'Sacramento', 'Segoe Script', cursive;

  /* --- Radii. Crisp. --- */
  --r-sm: 2px;
  --r:    3px;
  --r-lg: 6px;
  --r-xl: 14px;

  /* --- Elevation. On dark, shadow is depth + a specular top edge. --- */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
  --shadow:    0 8px 24px -8px rgba(0, 0, 0, 0.60);
  --shadow-lg: 0 24px 60px -16px rgba(0, 0, 0, 0.72);
  --sheen: inset 0 1px 0 rgba(255, 255, 255, 0.055);

  /* --- Segmented control ---
     A recessed track with one raised key in it. On dark ground the track is
     a hole and the key is lit; both flip on paper, so both are tokens. */
  /* The plan's own materials. Floors carry the light reading, rung by rung;
     walls stay neutral so the floor is the only thing saying anything. */
  --plan-grid:      rgba(214, 237, 226, 0.06);
  --plan-grid-2:    rgba(214, 237, 226, 0.13);
  --plan-floor-x:   #17211C;                 /* light unknown */
  --plan-floor-0:   #16211B;
  --plan-floor-1:   #1E3026;
  --plan-floor-2:   #2C4430;
  --plan-floor-3:   #4B5A34;
  --plan-floor-4:   #6E6B3A;
  --plan-wall-a:    #1B2A22;
  --plan-wall-b:    #243529;
  --plan-wall-edge: rgba(214, 237, 226, 0.22);
  --plan-parapet:   rgba(36, 53, 41, 0.55);
  --plan-pane:      #F1E3A8;
  --plan-pane-dim:  #6F8E7E;
  --plan-pot:       #0E1B15;
  --track:    rgba(0, 0, 0, 0.22);
  --track-in: inset 0 1px 2px rgba(0, 0, 0, 0.30);
  --seg-on:   rgba(255, 255, 255, 0.14);

  /* --- Form fields ---
     The same recess as the segmented track, and tokens for the same reason:
     on dark ground a field is a well punched into the surface, on paper it
     is a shallow tint with a pressed top edge. These were hardcoded at 22%
     and 30% black, which on ivory is a muddy grey box — and since the input
     keeps `color: var(--ink)`, in Conservatory that meant near-black type on
     a grey-black fill in every form in the app. The room sheet's name,
     aspect, light and humidity values were all unreadable.

     It survived this long because a field's *value* only fills in when you
     have data to edit, and a sheet only exists after a tap, so nothing that
     renders one was ever captured. */
  --field:     rgba(0, 0, 0, 0.22);
  --field-on:  rgba(0, 0, 0, 0.30);
  --field-in:  none;

  /* --- Modal surfaces ---
     The sheet and the toast. Hardcoded before this, which put a charcoal
     slab on Conservatory's ivory paper while every control inside it went on
     resolving --ink, --hair and --glass from the light theme. Half the sheet
     was light-on-dark and half dark-on-dark. The sheet is where every form
     in this app lives, so it is the last surface that should have been left
     off the token list. */
  --sheet-bg:   #0C1712;
  --sheet-glow: rgba(63, 190, 139, 0.09);
  --toast-bg:   rgba(12, 23, 18, 0.92);

  /* Ink on the filled champagne count in the nav. Champagne is light here
     and dark in Conservatory, so a fixed near-black is legible in one theme
     and nearly invisible in the other. */
  --badge-ink: #16140C;

  /* --- Photoless plant plates ---
     Consumed by .tmark, which gets its hue from UI.plantTile. Deep, low
     luminance and narrow-gamut, so a wall of them reads as one material
     catching light from one direction rather than nine coloured squares.
     Lightnesses are unitless because .tmark multiplies them by 1% after
     adding the per-plant lift. --tile-sheen is bare channels so the same
     token can take a computed alpha. */
  /* --- Photoless plant plates ---
     Six hand-picked pairs rather than a hue computed from a name hash. The
     hash spanned 100–143° at 26% saturation and 11% lightness, which is a
     band too narrow and too dark to hold forty-four steps: two plants could
     land a degree apart and render identically, so the differentiation it
     promised was not delivered. Six is a number the eye can actually tell
     apart, and lightness varies across the set as well as hue, which is what
     does most of the work at this luminance.

     Cyan is deliberately absent. An earlier band ran to 186°, invisible here
     and a pale cornflower blue in Conservatory — half the tiles in the
     daylight theme came out the one colour this app has no use for.

     ui.js picks the index from the same name hash as before, so a plant keeps
     its identity; only the palette it draws from has changed. --- */
  --tint-0a: #1B4636;  --tint-0b: #123024;
  --tint-1a: #2F4A22;  --tint-1b: #213517;
  --tint-2a: #21403C;  --tint-2b: #162E2B;
  --tint-3a: #3E4722;  --tint-3b: #2C3317;
  --tint-4a: #163A2C;  --tint-4b: #0E281E;
  --tint-5a: #4A4529;  --tint-5b: #35311C;
  --tile-sheen: 63, 190, 139;
  --tile-sheen-a: 0.13;
  --tile-ink:   rgba(232, 240, 233, 0.82);

  /* --- Motion --- */
  --ease:     cubic-bezier(0.16, 1, 0.30, 1);      /* expo out */
  --ease-io:  cubic-bezier(0.65, 0, 0.35, 1);
  --dur-1: 180ms;
  --dur-2: 340ms;
  --dur-3: 620ms;

  /* --- Metrics --- */
  --nav-h:  78px;   /* 70 of row plus the 8 above it; the inset is added on top in .tabbar */
  --side-w: 260px;
  --pad:    22px;
  --maxw:   1080px;
}

/* ==========================================================================
   A1b · The display face
   --------------------------------------------------------------------------
   Self-hosted rather than fetched, which is the better arrangement here for
   three reasons beyond licensing: it removes a render-blocking round trip to
   two more origins, it makes the app genuinely offline on a first visit
   rather than once the font cache has filled, and it puts the one typeface
   carrying the brand under our own version control instead of a third
   party's.

   Subset to Latin, Latin-1 and Latin Extended-A. The display face is always
   uppercase, but a plant tile prints the first character of a name the user
   typed, so accented capitals have to be present.

   'swap' rather than 'block': the splash already covers first paint until
   document.fonts resolves, so there is nothing to protect against, and a
   blocked face on a slow connection would hold the whole app behind it.
   ========================================================================== */

@font-face {
  font-family: 'PP Hatton';
  src: url('fonts/hatton-ultralight.woff2') format('woff2');
  font-weight: 200;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'PP Hatton';
  src: url('fonts/hatton-medium.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* The other three faces, self-hosted too as of the store build.
   --------------------------------------------------------------------------
   They came from Google until now, which cost a second origin on every cold
   start, an IP address handed to a third party for nothing, and a native
   shell that could not render itself correctly on a first launch with no
   network — a service worker cannot precache a Google woff2 by name, because
   the URL is not knowable until the stylesheet has been parsed.

   Cormorant and Jost are the variable fonts, axis-clipped to the weights the
   app actually draws and subsetted here. That is not a detail: as eight
   static files these were 254KB, and as three variable ones they are 124KB
   for a wider range. Jost alone went from four files at 104KB to one at 24.
   Sacramento has a single weight, so there is nothing to vary and it stays
   the static subset Google was already serving.

   The weight ranges are real ranges, not a formality. Jost is declared
   300-600 because the Profile rows ask for 600 and were being given a
   synthesised fake bold; Cormorant is 300-400 because nothing in the sheet
   asks the body serif for more, and a clipped axis is a smaller file.

   latin and latin-ext stay separate for the reason Google separates them.
   Only latin is precached in sw.js — latin-ext is fetched the first time
   someone's plant is called Aloë and cached by the same-origin handler from
   then on.

   All three are SIL Open Font License; see fonts/LICENSES.md. */

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

/* ==========================================================================
   A2 · Reset
   ========================================================================== */

* { box-sizing: border-box; }

/* The `hidden` attribute is how the app shows and hides the back arrow, the
   topbar subtitle, the weather chip and the sheet backdrop. The browser's own
   `[hidden] { display: none }` is weaker than any class rule that sets a
   display, so it loses to things like `.icon-btn { display: inline-flex }`.
   This makes `el.hidden = true` mean what it says. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--sans);
  font-weight: 300;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink);
  background: var(--bg-1);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* Two offset radial glows stand in for the photograph's raking light. Fixed
   so they never scroll away, and behind everything via a negative z-index. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(1100px 680px at 12% -8%,  rgba(47, 154, 112, 0.17), transparent 62%),
    radial-gradient(900px  620px at 92% 108%, rgba(31, 107, 76, 0.20),  transparent 60%),
    radial-gradient(700px  500px at 78% 12%,  rgba(63, 190, 139, 0.07), transparent 66%),
    var(--bg-1);
}

h1, h2, h3, h4, p { margin: 0; }
button { font: inherit; color: inherit; }
img { max-width: 100%; display: block; }

::selection { background: rgba(63, 190, 139, 0.28); color: var(--ink); }

/* Slim, self-effacing scrollbars. */
* { scrollbar-width: thin; scrollbar-color: var(--hair-2) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--hair-2);
  border-radius: 99px;
  border: 2px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--hair-3); background-clip: content-box; }

/* Keyboard focus only — mint, generous, never suppressed. */
:focus-visible {
  outline: 1px solid var(--emerald-glow);
  outline-offset: 2px;
}

/* Fallback size for UI.icon() output. Container rules like `.btn svg` are
   more specific and override this; it only catches icons placed somewhere
   that has no rule of its own, which would otherwise stretch to fill. */
.ico { width: 18px; height: 18px; flex: 0 0 auto; }

/* ==========================================================================
   A3 · Typography
   ========================================================================== */

/* Display caps. Tracking tightens as size grows, and so does weight — .d-md
   is 21px and takes the 500, the two above it take the 200. */
.d-xl, .d-lg {
  font-family: var(--display);
  font-weight: 200;
  color: var(--ink);
  text-transform: uppercase;
  line-height: 1.02;
}
.d-md {
  font-family: var(--display);
  font-weight: 500;
  color: var(--ink);
  text-transform: uppercase;
  line-height: 1.02;
}
.d-xl { font-size: clamp(38px, 8vw, 66px); letter-spacing: 0.055em; }
.d-lg { font-size: clamp(28px, 5.4vw, 42px); letter-spacing: 0.075em; }
.d-md { font-size: 21px; letter-spacing: 0.12em; }

/* Editorial serif — plant names, numbers, anything that should feel written
   rather than rendered. */
.s-lg, .s-md, .s-sm {
  font-family: var(--serif);
  font-weight: 300;
  color: var(--ink);
  line-height: 1.2;
}
.s-lg { font-size: clamp(26px, 4.6vw, 36px); letter-spacing: 0.004em; }
.s-md { font-size: 21px; }
.s-sm { font-size: 17px; }

/* The flourish. Sacramento is a monoline script — it reads as a signature,
   so it appears once per screen at most and never for anything functional. */
.script {
  font-family: var(--script);
  font-weight: 400;
  color: var(--mint);
  letter-spacing: 0.01em;
  line-height: 1;
}

/* Eyebrow: tiny tracked-out caps. The workhorse of the whole system — it is
   what makes a plain stack of cards look curated. */
.eyebrow {
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.hint  { font-size: 13px; color: var(--ink-3); line-height: 1.62; }

/* The reset zeroes every p margin, so a .hint closing a card body sat 8px
   under the last line of whatever preceded it — on the watering card that
   made the species note look like the detail line belonging to the final
   interval factor. It is a closing aside about the card as a whole, so it
   needs its own air. */
.card > .hint { margin-top: 14px; }
.small { font-size: 13px; }
.tiny  { font-size: 11.5px; letter-spacing: 0.04em; }
.muted { color: var(--ink-3); }
.dim   { color: var(--ink-4); }
.nowrap { white-space: nowrap; }
.center { text-align: center; }

.link-btn {
  background: none;
  border: 0;
  padding: 0;
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--mint);
  cursor: pointer;
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.link-btn:hover { color: var(--mint-2); border-bottom-color: var(--hair-3); }

.sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ==========================================================================
   A4 · Splash
   ========================================================================== */

.splash {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  background:
    radial-gradient(760px 520px at 50% 42%, rgba(47, 154, 112, 0.22), transparent 70%),
    var(--bg-0);
  /* Slower than --dur-3, and only on the way out: the app behind it is
     already painted, so the dissolve is the one moment on this screen that
     can afford to be unhurried. */
  transition: opacity 760ms var(--ease), visibility 760ms;
}
.splash.is-out { opacity: 0; visibility: hidden; }
.splash-inner { transition: opacity 520ms var(--ease-io), transform 520ms var(--ease-io); }
.splash-inner.is-quiet { opacity: 0; transform: translateY(-12px); pointer-events: none; }

/* ---- First-run introduction, on the splash ----
   Same field, same face. A small wordmark keeps the scene continuous while
   the questions take the centre one at a time; the dots and the skip sit
   low and quiet so the question is the only thing at eye level. */
.ob {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 0 28px;
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  transition: opacity 380ms var(--ease-io);
}
.ob.is-leaving { opacity: 0; pointer-events: none; }
.ob-brand {
  position: absolute; top: calc(env(safe-area-inset-top, 0px) + 9vh);
  font-family: var(--display); font-weight: 500; font-size: 13px;
  letter-spacing: 0.34em; text-indent: 0.34em; text-transform: uppercase;
  color: var(--ink-3);
  animation: obIn 900ms var(--ease) 260ms both;
}
.ob-stage { width: 100%; max-width: 420px; }
.ob-step { display: flex; flex-direction: column; align-items: center; width: 100%; }
.ob-step > * { animation: obIn 720ms var(--ease) both; }
.ob-step > *:nth-child(2) { animation-delay: 90ms; }
.ob-step > *:nth-child(3) { animation-delay: 180ms; }
.ob-step > *:nth-child(4) { animation-delay: 270ms; }
.ob-step.is-leaving > * { animation: obOut 360ms var(--ease-io) both; }
.ob-step.is-leaving > *:nth-child(2) { animation-delay: 40ms; }
.ob-step.is-leaving > *:nth-child(3) { animation-delay: 80ms; }
.ob-step.is-leaving > *:nth-child(4) { animation-delay: 120ms; }
.ob-q {
  font-family: var(--display); font-weight: 200;
  font-size: clamp(28px, 7.4vw, 40px); line-height: 1.15; letter-spacing: 0.05em;
  color: var(--ink); text-align: center; text-wrap: balance;
  margin: 0 0 26px;
}
.ob-input {
  width: 100%; max-width: 360px;
  background: none; border: 0; border-bottom: 1px solid var(--hair-3); border-radius: 0;
  color: var(--ink); font-family: var(--serif); font-size: 28px; text-align: center;
  padding: 6px 0 12px; outline: none;
  transition: border-color var(--dur-2) var(--ease);
}
.ob-input::placeholder { color: var(--ink-4); }
.ob-input:focus { border-bottom-color: var(--mint); }
/* The rule turning mint is the focus cue; the global outline drew a box
   around a field that is meant to be a line. */
.ob-input:focus-visible { outline: none; }
.ob-hint {
  font-family: var(--sans); font-size: 13.5px; line-height: 1.55;
  color: var(--ink-3); text-align: center; text-wrap: balance;
  max-width: 36ch; margin: 16px auto 0;
}
.ob-note { min-height: 1.5em; color: var(--mint); }
.ob-actions { margin-top: 30px; display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
.ob-actions .btn { min-width: 220px; }
.ob-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.ob-chip { padding: 11px 18px; font-size: 13.5px; }
.ob-opts { display: flex; flex-direction: column; gap: 8px; width: 100%; max-width: 360px; }
.ob-opt {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  width: 100%; padding: 14px 18px;
  border: 1px solid var(--hair-2); border-radius: var(--r);
  background: var(--glass); color: var(--ink);
  font-family: var(--serif); font-size: 18px; text-align: center;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.ob-opt small { font-family: var(--sans); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-4); }
.ob-opt.is-hinted { border-color: var(--hair-3); }
.ob-opt.is-on { border-color: var(--mint); background: var(--mint-wash); }
.ob-opt.is-on small { color: var(--mint); }
.ob-opt:active { transform: scale(0.985); }
.ob-dots { position: absolute; bottom: calc(env(safe-area-inset-bottom, 0px) + 10vh); display: flex; gap: 9px; animation: obIn 900ms var(--ease) 420ms both; }
.ob-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--hair-3); transition: background var(--dur-2) var(--ease), transform var(--dur-2) var(--ease); }
.ob-dot.is-on { background: var(--mint); transform: scale(1.3); }
.ob-dot.is-done { background: var(--ink-4); }
.ob-skip {
  position: absolute; bottom: calc(env(safe-area-inset-bottom, 0px) + 4.5vh);
  background: none; border: 0; padding: 8px 12px;
  font-family: var(--sans); font-size: 10.5px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--ink-4); cursor: pointer;
  animation: obIn 900ms var(--ease) 600ms both;
}
@keyframes obIn {
  from { opacity: 0; transform: translateY(18px); filter: blur(6px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
@keyframes obOut {
  from { opacity: 1; transform: none; filter: blur(0); }
  to   { opacity: 0; transform: translateY(-14px); filter: blur(6px); }
}
@media (prefers-reduced-motion: reduce) {
  .ob-step > *, .ob-step.is-leaving > *, .ob-brand, .ob-dots, .ob-skip { animation: none; }
}

.splash-inner { text-align: center; padding: 0 28px; }

/* The wordmark arrives a letter at a time.

   It used to animate as one block, with the tracking opening from 0.34em to
   0.22em as it rose. That reads well in isolation and badly here: the whole
   word shifting width while it fades is the same motion as a font swapping
   underneath it, which is the exact ugliness this screen exists to hide.
   Per-letter, the tracking never moves — each glyph resolves in its final
   position, and what travels is focus rather than metrics. */
.splash-mark {
  font-family: var(--display);
  font-weight: 200;
  font-size: clamp(40px, 12vw, 78px);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink);
  /* Optical centring: tracking adds a trailing gap after the last letter. */
  text-indent: 0.22em;
}
.splash-mark span {
  display: inline-block;
  animation: letterIn 900ms var(--ease) both;
}
/* 70ms apart. Faster and it reads as one blurred block; slower and the eye
   starts reading the letters as a countdown rather than a word assembling. */
.splash-mark span:nth-child(1) { animation-delay: 60ms; }
.splash-mark span:nth-child(2) { animation-delay: 130ms; }
.splash-mark span:nth-child(3) { animation-delay: 200ms; }
.splash-mark span:nth-child(4) { animation-delay: 270ms; }
.splash-mark span:nth-child(5) { animation-delay: 340ms; }
.splash-mark span:nth-child(6) { animation-delay: 410ms; }

/* The script is wiped in from the left rather than faded, because that is
   how the word would actually be written. The opacity keyframe rides along
   so a browser that drops the clip-path still gets a clean fade instead of
   an instant appearance. */
.splash-script {
  font-family: var(--script);
  font-size: clamp(26px, 7vw, 40px);
  color: var(--mint);
  margin-top: -2px;
  /* Descenders in the script face sit below the line box; without the pad
     the clip would shave the tails of g and the y-descenders as it passes. */
  padding-bottom: 0.12em;
  animation: scriptIn 1000ms var(--ease-io) 780ms both;
}

/* A hairline with a specular sweep travelling along it. It draws itself out
   from the centre first — an empty rule sitting under a word that has not
   arrived yet was the one element on this screen that began finished. */
.splash-rule {
  margin: 20px auto 0;
  width: 132px;
  height: 1px;
  background: var(--hair);
  position: relative;
  overflow: hidden;
  animation: ruleIn 700ms var(--ease) 1360ms both;
}
.splash-rule i {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--mint), transparent);
  transform: translateX(-100%);
  animation: sweep 1700ms var(--ease-io) 1900ms infinite;
}

@keyframes letterIn {
  from { opacity: 0; transform: translateY(20px); filter: blur(7px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
@keyframes scriptIn {
  from { opacity: 0; clip-path: inset(0 100% 0 0); }
  to   { opacity: 1; clip-path: inset(0 0 0 0); }
}
@keyframes ruleIn {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: scaleX(1); opacity: 1; }
}
@keyframes sweep {
  to { transform: translateX(100%); }
}

/* ==========================================================================
   A5 · Grain
   ========================================================================== */

/* Fractal noise at very low opacity. Kills the flatness of large dark
   fields and gives the surface a printed quality. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 150;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ==========================================================================
   A6 · App shell
   ========================================================================== */

.app { min-height: 100vh; }

.main {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--pad) calc(var(--nav-h) + 40px + env(safe-area-inset-bottom, 0px));
}

/* --- Topbar --- */

/* The bar bleeds out to the edges of .main and pads itself back in.

   It sits inside .main, which carries the page's side gutter — so a
   background painted on the bar stopped short of each edge, and the cards
   scrolling underneath were visible through those two strips, unblurred,
   either side of a floating plate. The blur made it worse rather than
   hiding it: a sharp band of moving content beside a frosted one.

   The gutter is read from --pad rather than repeated, because .main's
   padding changes at both desktop breakpoints and a bleed that does not
   follow it reintroduces the same seam a size later.

   A 100vw layer centred on the bar was the first attempt and was wrong:
   left:50% centres on the element, and on desktop .main is pushed right by
   the sidebar, so the layer hung 130px off the right of the document and
   put a horizontal scrollbar on every wide page. Bleeding by the gutter
   cannot overhang, because the gutter is what it is cancelling. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 60;
  display: flex;
  align-items: flex-end;
  gap: 14px;
  /* The top inset is not optional here. The viewport is viewport-fit=cover
     and the shell draws edge to edge, so without it the title sits directly
     under the system clock — which is exactly what it did on the first
     device build: "TODAY" with the time printed through the O. 26px clears
     nothing on a phone whose status bar is nearer 48. The blurred gradient
     still runs up behind the bar, so the field reaches the top of the screen
     and only the text is moved down. */
  padding: calc(26px + env(safe-area-inset-top, 0px)) var(--pad) 16px;
  margin-inline: calc(var(--pad) * -1);
  background: linear-gradient(to bottom, var(--bg-1) 58%, rgba(10, 20, 16, 0));
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
/* Hairline that fades at both ends rather than hitting the edges. */
.topbar::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--hair-2) 18%, var(--hair-2) 82%, transparent);
}

.topbar-titles { min-width: 0; flex: 1; }

.topbar-title {
  font-family: var(--display);
  font-size: clamp(27px, 5.6vw, 38px);
  font-weight: 200;
  letter-spacing: 0.085em;
  text-transform: uppercase;
  line-height: 1.04;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Hatton reports a content area of about 1.15em, against a line box of
     1.04em — so the half-leading is negative and the caps' ink starts a
     fraction above the box. Measured at the 27px end: the ink top sits
     0.46px outside, and overflow:hidden shaved it, taking a slice off the
     top of every title. On a 3x screen that is a visible flat edge across
     T, O, D, A and Y.
     The overflow has to stay — it is what gives a long plant name its
     ellipsis. So the clip box grows instead: padding extends it upward, and
     the matching negative margin puts the baseline back where the layout
     had it, leaving every other measurement on this bar untouched. */
  padding-top: 0.08em;
  margin-top: -0.08em;
}

.topbar-sub {
  margin-top: 5px;
  font-size: 12.5px;
  font-weight: 300;
  letter-spacing: 0.055em;
  color: var(--ink-3);
}

.topbar-actions { display: flex; align-items: center; gap: 8px; padding-bottom: 3px; }
.topbar-back { margin-bottom: 3px; }

/* --- View --- */

.view { padding-top: 26px; animation: viewIn var(--dur-3) var(--ease) both; }

@keyframes viewIn {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

/* --- Sidebar (desktop only; Part D reveals it) --- */

.sidebar { display: none; }

.brand { display: flex; align-items: center; gap: 11px; }
.brand-glyph {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  color: var(--emerald-glow);
}
.brand-name {
  font-family: var(--display);
  font-weight: 500;
  font-size: 23px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  text-indent: 0.2em;
}
/* .brand-script is gone from the sidebar. The script is the one flourish in
   the app and it only works while it stays rare; sitting above the
   navigation for a whole session is not rare. It keeps the splash, where the
   product is introducing itself, and the mark takes its place here. */
.brand-mark-lock {
  display: flex;
  align-items: center;
  gap: 11px;
}
.brand-mark-lock svg { flex: 0 0 auto; }
/* Legacy hook — the leaf glyph was dropped when the wordmark came in. */
.brand-mark { display: none; }

.side-nav { display: flex; flex-direction: column; gap: 2px; }

.side-link {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 11px 13px;
  border: 0;
  border-radius: var(--r);
  background: none;
  color: var(--ink-3);
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  cursor: pointer;
  position: relative;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.side-link svg { width: 17px; height: 17px; flex: 0 0 auto; stroke-width: 1.3; }
.side-link:hover { color: var(--ink); background: var(--glass); }

/* Active state is a mint hairline plus a whisper of wash — restrained
   where a filled pill would look cheap. */
.side-link.is-active { color: var(--ink); background: var(--mint-wash); }
.side-link.is-active::before {
  content: '';
  position: absolute;
  left: 0; top: 50%;
  transform: translateY(-50%);
  width: 2px; height: 18px;
  background: var(--emerald-glow);
  border-radius: 99px;
}

.side-count {
  margin-left: auto;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 99px;
  background: var(--gold-wash);
  border: 1px solid rgba(200, 169, 107, 0.34);
  color: var(--gold-lit);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-align: center;
}

.sidebar-foot { margin-top: auto; display: flex; flex-direction: column; gap: 12px; }

/* --- Theme toggle --- */

.theme-toggle {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 12px;
  border: 1px solid var(--hair);
  border-radius: var(--r);
  background: var(--glass);
  color: var(--ink-3);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease);
}
.theme-toggle:hover { color: var(--ink); border-color: var(--hair-3); background: var(--glass-2); }
.theme-toggle svg { width: 15px; height: 15px; flex: 0 0 auto; stroke-width: 1.3; }

/* --- Weather chip --- */

.weather-chip {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 13px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--glass);
  box-shadow: var(--sheen);
  font-size: 11.5px;
  letter-spacing: 0.05em;
  color: var(--ink-3);
}
.weather-chip-ico { display: flex; align-items: center; flex: 0 0 auto; }
.weather-chip-ico svg { width: 20px; height: 20px; stroke-width: 1.15; color: var(--ink-3); }
.weather-chip-body { min-width: 0; }
.weather-chip-t {
  font-family: var(--serif);
  font-size: 24px;
  font-weight: 300;
  color: var(--ink);
  line-height: 1;
}
.weather-chip-l {
  margin-top: 3px;
  font-size: 10.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-4);
}

/* --- Tabbar (mobile) --- */

.tabbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 70;
  /* The inset is added to the height, not taken out of it. It was inside a
     fixed 70px with border-box sizing, so on a phone with a gesture bar the
     icons lost that space and ended up pressed against the bottom edge. The
     8px of top padding is the other half of the same complaint: the row had
     nothing between it and the hairline above. */
  height: calc(var(--nav-h) + env(safe-area-inset-bottom, 0px));
  display: grid;
  grid-auto-flow: column;
  align-items: stretch;
  padding: 8px 4px calc(env(safe-area-inset-bottom, 0px));
  background: rgba(8, 16, 12, 0.72);
  backdrop-filter: blur(22px) saturate(140%);
  -webkit-backdrop-filter: blur(22px) saturate(140%);
  border-top: 1px solid var(--hair);
}

.tab {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  border: 0;
  background: none;
  color: var(--ink-4);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-2) var(--ease);
}
.tab svg { width: 21px; height: 21px; stroke-width: 1.3; transition: transform var(--dur-2) var(--ease); }
.tab.is-active { color: var(--ink); }
.tab.is-active svg { transform: translateY(-1px); stroke-width: 1.5; }

/* A short mint rule above the active tab, drawn from the centre out. */
.tab::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 0;
  height: 1px;
  background: var(--emerald-glow);
  transform: translateX(-50%);
  transition: width var(--dur-2) var(--ease);
}
.tab.is-active::before { width: 26px; }

.tab-badge {
  position: absolute;
  top: 11px;
  left: 50%;
  margin-left: 7px;
  min-width: 16px;
  padding: 0 4px;
  border-radius: 99px;
  background: var(--gold);
  color: var(--badge-ink);
  font-family: var(--sans);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  line-height: 16px;
  text-align: center;
}

/* Champagne on these two means "due" — the urgency ladder's second rung. The
   greenhouse plus is not a due task, it is an invitation to finish setting
   up, so it takes emerald: the hue the palette gives to the primary action.
   Wearing the overdue colour for a setup prompt would spend the one signal
   that has to mean "a plant needs you today". */
.tab-badge.is-hint {
  background: var(--emerald-lit);
  color: var(--btn-ink);
}
.side-count.is-hint {
  background: var(--mint-wash);
  border-color: rgba(63, 190, 139, 0.34);
  color: var(--mint-2);
}

/* The plan card is a <button>, and a button shrinks to its content and
   centres it — so the card stopped short of the right edge and its copy
   floated in the middle, unlike every other nudge. Stretch it, left-align
   it, and give it the gap under the tabs that a div would have had. */
.plan-card { width: 100%; text-align: left; font: inherit; color: inherit; margin-top: 14px; }

/* A bare hairline for parting two blocks that are not sections. */
.rule { border: 0; border-top: 1px solid var(--hair); margin: 18px 0; }

/* ==========================================================================
   B1 · Sections
   ========================================================================== */

.section { margin-bottom: 42px; }

/* Baseline-aligned title and note over a full-width hairline. This single
   pattern gives every screen the same editorial rhythm. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  padding-bottom: 11px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--hair);
}

/* The display face, and it took a change of typeface to get here.

   This is a label rather than a heading — "NEEDS YOU TODAY" names a group
   and the eye is meant to pass over it on the way to the rows beneath — so
   it has to stay small, and small was exactly where the old display face
   failed. At 15px Italiana rendered as though half the ink had gone: the N
   lost its diagonal, the E and H crossbars fell to about fifteen percent
   grey, the D bowl broke up. The most-repeated piece of typography in the
   interface, looking on every screen like a font that had not finished
   loading. Raising it was not available either, since .s-md plant names are
   21px and a label as large as the thing it labels stops being a label.

   So it went to Jost, and the note here used to argue that the role and the
   face genuinely disagreed and the face was what gave. That was the right
   call for a face with one weight. It is not the right call now: the 500
   holds every stroke at 14px, so the label can have the display face back
   and the app's headings stop speaking in two voices.

   14px and 0.23em are kept from the sans setting rather than reverted to the
   original 15px/0.2em. The tracking is what makes this read as a label
   instead of as small body copy, and it lines the title up with the 0.2em
   .section-note sitting on the same baseline — both true for reasons that
   had nothing to do with which face was in use. */
.section-title {
  font-family: var(--display);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.23em;
  text-transform: uppercase;
  color: var(--ink);
  line-height: 1.3;
}

.section-note {
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-4);
  white-space: nowrap;
  flex: 0 0 auto;
}

/* ==========================================================================
   B2 · Cards
   ========================================================================== */

/* Glass over emerald, a hairline, and a specular top edge. The ::before
   gradient is the "wet leaf" highlight — brightest at the top-left corner,
   which is where the reference photograph is lit from. */
.card {
  position: relative;
  padding: 20px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--glass);
  box-shadow: var(--sheen), var(--shadow-sm);
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.card::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(120% 90% at 0% 0%, rgba(255, 255, 255, 0.05), transparent 58%);
}
.card > * { position: relative; }

.card-pad-0 { padding: 0; }

/* ==========================================================================
   B3 · Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 12px 20px;
  border: 1px solid transparent;
  border-radius: var(--r);
  background: linear-gradient(175deg, var(--emerald-lit), var(--emerald));
  color: var(--btn-ink);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: var(--sheen), 0 2px 12px -4px rgba(47, 154, 112, 0.5);
  transition: transform var(--dur-1) var(--ease), box-shadow var(--dur-2) var(--ease),
              filter var(--dur-1) var(--ease), background var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}
.btn svg { width: 16px; height: 16px; flex: 0 0 auto; stroke-width: 1.4; }
.btn:hover { filter: brightness(1.1); box-shadow: var(--sheen), 0 4px 20px -4px rgba(47, 154, 112, 0.62); }
.btn:active { transform: translateY(1px); filter: brightness(0.96); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; filter: none; transform: none; }

.btn-sm { padding: 8px 14px; font-size: 10px; letter-spacing: 0.16em; }
.btn-sm svg { width: 14px; height: 14px; }
.btn-lg { padding: 15px 28px; font-size: 12px; }
.btn-block { display: flex; width: 100%; }

/* Hairline only. The default for anything that is not the one primary
   action on a screen. */
.btn-ghost {
  background: none;
  border-color: var(--hair-2);
  color: var(--ink-2);
  box-shadow: none;
}
.btn-ghost:hover {
  background: var(--glass);
  border-color: var(--hair-3);
  color: var(--ink);
  filter: none;
  box-shadow: none;
}

.btn-soft {
  background: var(--mint-wash);
  border-color: rgba(63, 190, 139, 0.26);
  color: var(--mint-2);
  box-shadow: none;
}
.btn-soft:hover { background: rgba(63, 190, 139, 0.17); border-color: rgba(63, 190, 139, 0.4); filter: none; }

/* .btn-danger and .btn-terra are gone. Both were outlined terracotta and
   both have been replaced by .btn-blood, which keeps their intent — a
   destructive control should be findable and never the brightest thing on
   the screen — and measures better at it: 2.21:1 against this ground where
   the outline read 6.69:1. The outline was the loudest of the options, not
   the quietest, which is the opposite of what the note here used to claim. */

/* Destructive actions. Declared after .btn and every other variant on
   purpose: at equal specificity the later rule wins, and a version of this
   placed earlier lost its background to .btn's emerald gradient — a delete
   button that rendered in the primary green.

   Built like the primary but without its glow. The glow is a lift, and a
   lift is an invitation; this control should be easy to find and no fun at
   all to press. The result is quieter on the page than the outline it
   replaces — 2.21:1 against the ground where terracotta read 6.69:1 — while
   being the most legible button in the app once you are actually reading it. */
.btn-blood {
  background: linear-gradient(175deg, var(--blood-lit), var(--blood-deep));
  border-color: var(--blood-edge);
  color: var(--blood-ink);
  box-shadow: var(--sheen);
}
.btn-blood:hover { filter: brightness(1.12); box-shadow: var(--sheen), var(--shadow-sm); }

/* --- Icon buttons --- */

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  border: 1px solid var(--hair);
  border-radius: var(--r);
  background: var(--glass);
  color: var(--ink-2);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.icon-btn svg { width: 17px; height: 17px; stroke-width: 1.4; }
.icon-btn:hover { color: var(--ink); border-color: var(--hair-3); background: var(--glass-2); }
.icon-btn:active { transform: scale(0.94); }

.icon-btn-bare {
  border-color: transparent;
  background: none;
  width: 34px;
  height: 34px;
}
.icon-btn-bare:hover { background: var(--glass); border-color: transparent; }

/* ==========================================================================
   B4 · Pills, chips, drops
   ========================================================================== */

/* Data pills. Hairline + wash, never solid — solid colour blocks read as
   "app", hairlines read as "editorial". */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border: 1px solid rgba(63, 190, 139, 0.24);
  border-radius: 99px;
  background: var(--mint-wash);
  color: var(--mint-2);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.07em;
  white-space: nowrap;
  line-height: 1.5;
}
.pill-lg { padding: 7px 14px; font-size: 12px; }

.pill-grey { border-color: var(--hair-2); background: var(--glass); color: var(--ink-2); }
.pill-mint { border-color: rgba(63, 190, 139, 0.3);  background: var(--mint-wash); color: var(--mint-2); }
/* The class name stays — seven call sites across the views name it, and the
   name describes the role rather than the hex. Only the colour moves. */
.pill-sun  { border-color: rgba(200, 169, 107, 0.34); background: var(--gold-wash); color: var(--gold-lit); }
.pill-terra{ border-color: rgba(208, 138, 98, 0.3);  background: var(--terra-soft);color: var(--terra-ink); }

/* The top of the severity scale — highly toxic, act quickly. There was a
   .pill-rose here doing this job in pink; see the note beside --terra for
   why an extra hue was the wrong tool. The step up is expressed as
   intensity instead: solid terracotta where .pill-terra is a wash of it.
   One glance separates them, which is the whole point of a top step.

   The ink is a token because --terra inverts between themes and a filled
   pill cannot ignore that: #D08A62 is a light clay that wants near-black
   type, #A0552C is a dark one that wants ivory. Hardcoding either would
   reproduce, on the loudest pill in the app, exactly the fault this commit
   is removing from the quiet ones.

   Given a little more weight than its siblings, too — a shade bolder and
   wider-tracked. This pill says "read the label before you let the cat in
   the room", and it should not look like a category tag. */
.pill-terra-hi {
  border-color: var(--terra);
  background: var(--terra);
  color: var(--terra-hi-ink);
  font-weight: 500;
  letter-spacing: 0.09em;
}
.pill-terra-hi svg { color: var(--terra-hi-ink); }

/* Oxblood, filled, and it carries a glyph at every call site. Reserved for a
   toxicity rating of 'very-toxic' against an animal the household actually
   keeps. One species in the catalogue qualifies; putting it on 'toxic' would
   mark 22 of 48 and the chip would stop being read. */
.pill-blood {
  border-color: var(--blood-edge);
  background: var(--blood);
  color: var(--blood-ink);
  font-weight: 500;
  letter-spacing: 0.09em;
}
.pill-blood svg { color: var(--blood-ink); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 14px;
  border: 1px solid var(--hair-2);
  border-radius: 99px;
  background: var(--glass);
  color: var(--ink-2);
  font-size: 12px;
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: all var(--dur-1) var(--ease);
}
.chip:hover { border-color: var(--hair-3); color: var(--ink); background: var(--glass-2); }
.chip svg { width: 13px; height: 13px; flex: 0 0 auto; stroke-width: 1.5; color: var(--ink-4); }
.chip:hover svg { color: var(--mint); }
.chip .mono { color: var(--ink-3); }
.chip.is-on {
  border-color: rgba(63, 190, 139, 0.45);
  background: var(--mint-wash);
  color: var(--mint-2);
}
/* Without this the selected chip's text goes mint while its icon stays the
   dim default — the mark reads as disabled inside an active control. */
.chip.is-on svg { color: var(--mint); }

/* Watering-status dot. A small bead of colour with a matching halo — the
   closest the system gets to a literal water droplet. */
.drop {
  display: inline-block;
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
  border-radius: 99px;
  background: var(--ink-4);
}
.drop-ok   { background: var(--emerald-glow); box-shadow: 0 0 0 3px rgba(63, 190, 139, 0.15); }
.drop-due  { background: var(--gold);         box-shadow: 0 0 0 3px rgba(200, 169, 107, 0.15); }
.drop-now  { background: var(--gold);         box-shadow: 0 0 0 3px rgba(200, 169, 107, 0.17); }
.drop-over { background: var(--terra);        box-shadow: 0 0 0 3px rgba(208, 138, 98, 0.17); }

/* The four beads are now emerald, champagne, champagne-metallic, terracotta —
   a single warm ramp away from the house green. It used to end in pink,
   which broke the ramp at its most important point: "you have forgotten
   this plant" is where the eye most needs to land, and a hue change reads
   as a different *kind* of state rather than a further one. */

.sev-high { color: var(--terra); }
.sev-med  { color: var(--gold); }

/* ==========================================================================
   B5 · Forms
   ========================================================================== */

/* --- Disclosure ---
   <details>/<summary>, not a div and a click handler: the open/closed state,
   the keyboard behaviour and the semantics come from the platform, which is
   the whole argument for it in a codebase with no framework to lean on.

   The pot questions sit behind one. Three controls and their defaults were
   the tallest thing between the reader and the save button, and all three
   have answers that are right often enough to leave alone — a 15cm plastic
   pot with holes. Asked as a question with a default answer, they cost one
   line until someone actually has a different pot. */
.disclosure {
  border: 1px solid var(--hair);
  border-radius: var(--r);
  background: var(--glass);
  margin-bottom: 18px;
}
.disclosure > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 15px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 300;
  color: var(--ink);
  /* Both are needed: Safari draws the triangle through a -webkit- pseudo
     element and ignores list-style, Firefox does the opposite. */
  list-style: none;
}
.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary:hover { background: var(--glass-2); }
.disclosure > summary:focus-visible { outline: 1px solid var(--emerald-glow); outline-offset: -2px; }
/* UI.icon('chevron') points right, so closed is a quarter turn clockwise to
   point down and open is a quarter turn the other way to point up — the
   same 180 degrees of travel as a purpose-drawn caret, without adding a
   second glyph for one component. */
.disclosure-chev {
  margin-left: auto;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  color: var(--ink-3);
  transform: rotate(90deg);
  transition: transform var(--dur-1) var(--ease);
}
.disclosure-chev svg { display: block; width: 16px; height: 16px; }
.disclosure[open] > summary { border-bottom: 1px solid var(--hair); }
.disclosure[open] .disclosure-chev { transform: rotate(-90deg); }
.disclosure-body { padding: 16px 15px 15px; }

/* Three controls abreast: width, material, drainage. The number wants less
   room than the two selects, hence the uneven tracks rather than three equal
   thirds. Below 380px the row would put "Glazed ceramic" in a 110px select,
   so it drops to a single column — one is a phone that cannot take it, the
   other is the request. */
/* Tracks follow the order of the controls, so they moved when material did:
   material carries the longest answers and takes the most room, the number
   needs least, drainage sits between. Sized for "Plastic" rather than
   "Glazed ceramic" — the longest option in the list still truncates, and
   widening the column enough for it would starve the other two. */
.pot-row {
  display: grid;
  gap: 9px;
  grid-template-columns: 1.15fr 0.72fr 1.05fr;
}
.pot-row .field { margin-bottom: 0; }

/* Three columns on a phone leave roughly 110px each, and .label's 0.22em
   tracking put "WIDTH (CM)" onto two lines while its neighbours stayed on
   one — three controls at two different heights, which reads as a broken
   grid rather than a tight one. Tracking is the cheapest thing to give back
   here: it is decoration on a four-character label, where the wrap is not. */
.pot-row .label {
  font-size: 9.5px;
  letter-spacing: 0.1em;
  white-space: nowrap;
}

/* The chevron's 40px of right padding is sized for a full-width select. At a
   third of the row it was eating the answer — "Has holes" came out "Has ho".
   Pulled in to match the narrower well. */
.pot-row .select {
  padding-left: 12px;
  padding-right: 30px;
  background-position: right 9px center;
  background-size: 15px;
}
.pot-row .input { padding-left: 12px; padding-right: 12px; }

/* Dimmed, not hidden, when the pot is still hypothetical. The values are
   what gets saved either way, so showing them greyed says what is being
   assumed rather than leaving the reader to wonder. */
.pot-row.is-off { opacity: 0.42; }
.pot-row.is-off .input, .pot-row.is-off .select { cursor: not-allowed; }

.pot-pending {
  margin-bottom: 16px;
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 300;
  color: var(--ink-2);
}
.pot-pending input { width: 17px; height: 17px; accent-color: var(--leaf); flex: 0 0 auto; }

@media (max-width: 379px) {
  .pot-row { grid-template-columns: minmax(0, 1fr); }
  .pot-row .label { font-size: 10px; letter-spacing: 0.22em; }
}

/* Quick start beside the room name. The select carries short answers and
   the name is what the reader is really filling in, so the name takes the
   larger share. */
.name-row {
  display: grid;
  gap: 10px;
  grid-template-columns: 0.95fr 1.05fr;
}
.name-row .field { margin-bottom: 0; }
.name-row .select { padding-left: 12px; padding-right: 30px; background-position: right 9px center; background-size: 15px; }
.name-row .input { padding-left: 12px; padding-right: 12px; }
.name-row + .field { margin-top: 18px; }
@media (max-width: 379px) {
  .name-row { grid-template-columns: minmax(0, 1fr); }
}

/* The room icon picker. A wrapping grid of equal cells rather than a strip:
   seventeen of them plus the initial would scroll sideways on a phone, and a
   set you have to drag through stops reading as a set. */
.ico-grid {
  display: grid;
  gap: 7px;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
}
.ico-cell {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  padding: 0;
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  background: var(--glass);
  color: var(--ink-3);
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease);
}
.ico-cell svg { width: 20px; height: 20px; }
.ico-cell:hover { border-color: var(--hair-3); color: var(--ink-2); }
.ico-cell:focus-visible { outline: 1px solid var(--emerald-glow); outline-offset: 2px; }
.ico-cell.is-on {
  border-color: rgba(63, 190, 139, 0.45);
  background: var(--mint-wash);
  color: var(--mint-2);
}
/* The first cell holds a letter rather than a glyph, so it carries .mono —
   whose own 46px frame and border would draw a box inside this box. */
.ico-cell .mono {
  width: auto;
  height: auto;
  border: 0;
  background: none;
  font-size: 17px;
  color: inherit;
}

/* A room plate showing its icon instead of its initial. Same frame, same
   size — only the contents differ, which is what keeps a mixed strip
   reading as one column. */
.mono-ico svg { width: 22px; height: 22px; }
.mono-sm.mono-ico svg { width: 12px; height: 12px; }
.mono-lg.mono-ico svg { width: 26px; height: 26px; }

/* Light and humidity abreast. Both are one-line selects with short answers,
   so the pair costs the same height as either alone. */
.env-row {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin-bottom: 0;
}
.env-row .field { margin-bottom: 0; }
.env-row .select { padding-left: 13px; padding-right: 34px; background-position: right 10px center; }
@media (max-width: 379px) {
  .env-row { grid-template-columns: minmax(0, 1fr); }
}

/* Acquired and last-watered abreast. "When did you get it?" wraps to two
   lines at this width and "Last watered" does not, which left the two date
   inputs on different baselines — the row looked misaligned when it was the
   labels above them that differed. Pushing each input to the bottom of its
   own column lines the controls up and lets the labels be whatever length
   the question needs. */
.date-row {
  display: grid;
  gap: 12px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.date-row .field {
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
}
.date-row .field .input { margin-top: auto; }

/* --- Diary index rows ---
   One line per entry: date, plant, kind. The date column is fixed so the
   plant names line up down the page; the kind is a small chip at the end
   because it is the least of the three things and the one that repeats
   most — a run of "Watered" reads as texture, which is right. */
.dlog {
  display: grid;
  grid-template-columns: 74px minmax(0, 1fr) auto 16px;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 13px 16px;
  border: 0;
  border-bottom: 1px solid var(--hair);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease);
}
.dlog:last-child { border-bottom: 0; }
.dlog:hover { background: var(--glass); }
.dlog:focus-visible { outline: 1px solid var(--emerald-glow); outline-offset: -2px; }
.dlog.is-orphan { cursor: default; opacity: 0.6; }
.dlog-date {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.dlog-plant {
  font-family: var(--serif);
  font-size: 17px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dlog-kind {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--sans);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-4);
  white-space: nowrap;
}
.dlog-kind svg { width: 13px; height: 13px; }
.dlog-chev { color: var(--ink-4); }
.dlog-chev svg { width: 16px; height: 16px; }
@media (max-width: 379px) {
  .dlog { grid-template-columns: 62px minmax(0, 1fr) 16px; }
  .dlog-kind { display: none; }
}

/* --- Range: the experience scale ---
   A three-stop slider rather than three chips. Chips presented the answers
   as unrelated options; the thing being asked is a single quantity with an
   order to it, and a track you drag along says that in the shape of the
   control rather than in the labels.

   The control is drawn, not styled. A native range's thumb snaps between
   steps and CSS is given no way to ease it, so the first version jumped
   between the three stops — the one place in the app where a value changed
   with no motion at all. Now the native input is still there, doing the
   gesture, the keyboard and the accessibility, but it is invisible: what the
   reader sees is a rail, a fill and a thumb positioned by --fill, and --fill
   is a registered property, so it tweens. The thumb glides to each stop and
   the fill sweeps with it, on the same easing curve as everything else. */
@property --fill {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 0%;
}

.slider-shell {
  --fill: 0%;
  --fill-ink: var(--emerald-lit);
  --thumb: 20px;
  position: relative;
  height: 22px;
  transition: --fill 380ms var(--ease);
}
.slider-shell.is-unset { --fill-ink: var(--mint-wash); }

.slider-rail {
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  height: 6px;
  transform: translateY(-50%);
  border-radius: 99px;
  border: 1px solid var(--hair);
  background: var(--track);
  box-shadow: var(--track-in);
  overflow: hidden;
}
.slider-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--fill);
  border-radius: 99px;
  background: var(--fill-ink);
  transition: background var(--dur-2) var(--ease);
}
/* The thumb rides a band inset by half its width at each end, and its
   centre is a plain percentage along that band. Putting the inset on the
   band rather than in the thumb's own left keeps the maths to one
   percentage — calc() will not divide a length by a percentage, so the
   obvious "(100% - thumb) * fill / 100%" silently fails to compute and the
   thumb sits still while the fill sweeps. The band is aria-hidden; the
   input beside it is the real control. */
.slider-travel {
  position: absolute;
  top: 0; bottom: 0;
  left: calc(var(--thumb) / 2);
  right: calc(var(--thumb) / 2);
  pointer-events: none;
}
.slider-thumb {
  position: absolute;
  top: 50%;
  left: var(--fill);
  width: var(--thumb);
  height: var(--thumb);
  transform: translate(-50%, -50%);
  border-radius: 99px;
  border: 1px solid transparent;
  background: linear-gradient(175deg, var(--emerald-lit), var(--emerald));
  box-shadow: var(--sheen), 0 2px 10px -3px rgba(47, 154, 112, 0.55);
  transition: transform var(--dur-1) var(--ease), filter var(--dur-1) var(--ease);
}
.slider-shell.is-unset .slider-thumb { filter: saturate(0.55) brightness(0.9); }
.slider-shell:active .slider-thumb { transform: translate(-50%, -50%) scale(1.08); filter: brightness(1.08); }

/* The real control, laid over the drawing. Zero opacity rather than
   display:none so it keeps its hit area, its focus ring and its role. */
.slider {
  -webkit-appearance: none;
  appearance: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; }
.slider::-moz-range-thumb { width: 20px; height: 20px; border: 0; }
.slider:focus-visible + .slider-travel .slider-thumb { outline: 1px solid var(--emerald-glow); outline-offset: 4px; }

/* The two ends of the scale, named. Tabular figures are irrelevant here but
   the ends must not drift apart as the label above them changes length, so
   they are pinned by the flex rather than centred on the track. */
.slider-ends {
  display: flex;
  justify-content: space-between;
  margin-top: 9px;
  font-family: var(--sans);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-4);
}

/* The reading of the current position, in the body serif so it is a sentence
   about the reader rather than another label on the control. It arrives a
   letter at a time, resolving from a blur — the splash wordmark's entrance,
   scaled down to a control: 14ms between letters against the splash's 70,
   and a quarter of the travel. Same shape of motion, so the two feel like
   one hand. */
.slider-value {
  font-family: var(--serif);
  font-size: 19px;
  color: var(--ink);
  margin-bottom: 14px;
  min-height: 1.4em;
}
.slider-value.is-unset { color: var(--ink-3); font-style: italic; }
.slider-value span {
  display: inline-block;
  white-space: pre;
  animation: readingIn 260ms var(--ease) both;
}
@keyframes readingIn {
  from { opacity: 0; transform: translateY(5px); filter: blur(4px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}

/* display:block is load-bearing, not tidying. Almost every .field in the app
   is a <label> wrapping its own control — which is the right markup, since it
   makes the whole caption a hit target for the input — and a <label> is
   inline by default, so `margin-bottom` on it was being discarded outright.
   Every gap in every form was therefore line-box leading rather than spacing,
   which is why the labels sat flush under the hint of the field above and
   read as captions for the *previous* control instead of headings for their
   own. The one .field that did space correctly is the Quick start row in the
   add-room sheet, because that one alone is a <div>.

   It hid behind the same blind spot as the un-themed sheet background: forms
   live inside a surface that only exists after a tap, so nothing that renders
   one had ever been captured. */
.field { display: block; margin-bottom: 18px; }

/* The reset zeroes p margins, so a hint explaining its own control had no air
   above it and touched the select it belongs to. Top only — the 18px below
   belongs to .field, and giving the hint a bottom margin too would double the
   gap on exactly the fields that carry an explanation. */
.field > .hint { margin-top: 7px; }

.label {
  display: block;
  margin-bottom: 8px;
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.input, .textarea, .select {
  width: 100%;
  padding: 13px 15px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r);
  /* background-color, never the `background` shorthand. Two rules below this
     one paint an icon into the field with background-image — the magnifier on
     .input-search and the chevron on .select — and a shorthand here resets
     background-image to none for anything it wins against. It only works at
     all because those rules happen to come later in the file, which is a
     silent dependency on source order that the next edit would break. */
  background-color: var(--field);
  box-shadow: var(--field-in);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 14.5px;
  font-weight: 300;
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--ink-4); }

.input:focus, .textarea:focus, .select:focus {
  outline: none;
  border-color: rgba(63, 190, 139, 0.5);
  /* This one was the live defect, not just a latent risk. `background:
     var(--field-on)` is a shorthand, and :focus adds a pseudo-class, so this
     rule scores 0,2,0 against .input-search's 0,1,0 — it won regardless of
     source order and reset background-image to none. The result: the
     magnifier vanished from every search field in the app the instant it was
     focused, and the chevron from every select. Since focusing a field is
     how you use it, the icon was missing for the entire time anyone was
     actually looking at the control, and present only in the resting state
     nobody interacts with.

     It hid for a long time because every screenshot catches the resting
     state — a one-shot headless capture never focuses anything — so the icon
     was there in every picture ever taken of it. It surfaced only once the
     ?fill= hook drove Discover into its no-results branch, which calls
     focus() to restore the caret after a full re-render. Two instruments had
     to exist before this was visible at all. */
  background-color: var(--field-on);
  box-shadow: var(--field-in), 0 0 0 3px rgba(63, 190, 139, 0.1);
}

.textarea { min-height: 104px; resize: vertical; line-height: 1.65; }

/* Native select arrow is unstyleable, so it is replaced with a chevron
   drawn as an inline SVG background. */
.select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239FD3BE' stroke-width='1.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 13px center;
  background-size: 17px;
  cursor: pointer;
}
.select option { background: var(--bg-2); color: var(--ink); }

/* Search field with a leading magnifier. */
.input-search {
  padding-left: 42px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239FD3BE' stroke-width='1.5'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.5-3.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left 14px center;
  background-size: 17px;
}

/* The clear button on a search field.
   Chrome ships its own, and it is a solid mid-blue X — the only blue pixel
   anywhere in an app built from emerald, champagne, terracotta and ink, and
   it appears on all four search fields the moment they hold a value. It was
   invisible to every review for the same reason the missing magnifier was:
   the button only exists once there is something to clear, so no resting
   screenshot has ever contained one.

   Drawn as a mask rather than a background-image, because a mask takes its
   colour from background-color and can therefore read a token. The two icons
   above bake `%239FD3BE` into the SVG and so render the same light mint on
   both themes; that is a compromise those rules are stuck with, since an
   input cannot be masked without masking the field itself. A pseudo-element
   is its own box, so this one can be done properly and follow the ink. */
.input-search::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  cursor: pointer;
  background-color: var(--ink-4);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M18 6L6 18M6 6l12 12'/%3E%3C/svg%3E") no-repeat center / 14px;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M18 6L6 18M6 6l12 12'/%3E%3C/svg%3E") no-repeat center / 14px;
  transition: background-color var(--dur-2) var(--ease);
}
.input-search::-webkit-search-cancel-button:hover {
  background-color: var(--ink-2);
}

/* --- Segmented control --- */

/* Segmented view switcher — Greenhouse's Rooms/All plants, and the five tabs
   on a plant. The track is a token, not a literal: it was hardcoded to
   rgba(0,0,0,0.22), which on the ivory theme laid a mid-grey slab across the
   page and buried the labels in it.

   The inner shadow is what makes it a groove rather than a swatch. A flat
   fill one shade off the page is ambiguous — it could be a recess, or a
   panel sitting on top, or just a differently-coloured rectangle — and on
   the ivory theme it resolved as the last of those: a dull grey slab laid on
   warm paper, which is the cheapest-looking thing a light UI can do. A dark
   edge tucked under the top rim removes the ambiguity in one stroke, because
   a shadow on the inside of a boundary can only mean the surface has been
   pressed in. The raised key then reads as raised without having to shout,
   since it is the only thing in the control catching light from above. */
.tabs {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--hair);
  border-radius: var(--r);
  background: var(--track);
  box-shadow: var(--track-in);
}

/* --ink-2 for the unselected labels, not --ink-3. Elsewhere in the app ink-3
   is the right weight for a quiet label, but those labels sit on the page.
   These sit on the track, which has already taken contrast away from them,
   and the two reductions compound: CARE, DIARY, PHOTOS and PROPAGATE came
   out so pale beside the lit key that they read as disabled rather than
   merely unselected — as though the plant had one tab and four greyed-out
   ones. An unselected tab is an invitation and has to look clickable.

   Relying on ink-2 rather than a contrast bump is safe because the key's
   own treatment carries the selected state on its own: a lifted plate and a
   top edge, not a colour difference the label has to win. */
.tab-btn {
  padding: 8px 16px;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-2);
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.tab-btn:hover { color: var(--ink); }

/* `.is-on`, not `.is-active`. Every one of the three render sites — the two
   Greenhouse tabs and the five on a plant — writes `is-on`, matching the
   `.chip.is-on` convention used everywhere else in this stylesheet. This
   rule was written against `.is-active` and therefore never once matched,
   which is why the switcher shipped with no selected state at all: two
   identically-styled words in a box, with nothing to say which view you
   were looking at. It is a useful reminder that a screenshot review catches
   what a grep never will — the CSS was valid, the markup was valid, and the
   two simply never met.

   The plate is deliberately stronger than a colour change alone. At 10.5px
   with 0.16em tracking, ink-3 to ink is a difference you have to go looking
   for; a lifted surface with a top edge is one you see at a glance. */
.tab-btn.is-on {
  background: var(--seg-on);
  color: var(--ink);
  box-shadow: var(--sheen), var(--shadow-sm);
}

/* A plant has five tabs, and at 10.5px with 0.16em tracking they come to
   about 437px — wider than a 414px phone. Left as an unbounded inline-flex
   the track pushed the whole document into horizontal scroll, which the
   overflow probe caught and a screenshot never would: the clipped right edge
   looks exactly like the frame.

   So the track caps at the measure and scrolls inside itself. inline-flex is
   kept deliberately rather than swapped for flex — it shrink-wraps when the
   content fits, which is what the two-tab Greenhouse switcher needs, and
   only reaches 100% when it does not. The scrollbar is suppressed because
   the track is 40px tall and a gutter inside it would eat the labels; on
   touch this is a swipe, and on a narrow desktop window the tabs still
   respond to the wheel. */
.tabs {
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
.tabs::-webkit-scrollbar { display: none; }
.tab-btn { flex: 0 0 auto; }
.tab-body { margin-top: 16px; }

/* ==========================================================================
   B6 · Layout primitives
   ========================================================================== */

.row      { display: flex; align-items: center; gap: 11px; }
.row-wrap { display: flex; flex-wrap: wrap; gap: 8px; }

/* A horizontally-scrolling row of filter chips, same reasoning as .tabs
   elsewhere in this file: the browser's own scrollbar drew as a second,
   unstyled bar sitting in the gap under the chips, reading as stray
   whitespace before the next section rather than as a hint that the row
   scrolls. Touch already makes that obvious as a swipe. */
.chip-row {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  margin-bottom: 16px;
}
.chip-row::-webkit-scrollbar { display: none; }
.chip-row .chip { flex: 0 0 auto; }

@media (max-width: 520px) {
  .tab-btn { padding: 8px 13px; letter-spacing: 0.13em; }
}

.stack    { display: flex; flex-direction: column; gap: 11px; }
.grid     { display: grid; gap: 14px; }
.grid-2   { display: grid; gap: 14px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
.w3       { display: grid; gap: 10px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.spacer   { flex: 1; }
.org      { margin-left: auto; }

.divider {
  height: 1px;
  margin: 24px 0;
  border: 0;
  background: linear-gradient(90deg, transparent, var(--hair-2) 20%, var(--hair-2) 80%, transparent);
}

/* Ornamental rule — hairline broken by a small rotated square. Separates
   movements within a view where a plain .divider would be too blunt. The
   diamond is a rotated <i>, not a glyph, so it needs no font to render. */
.ornament {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 13px;
  margin: 30px 0;
}
.ornament::before,
.ornament::after {
  content: '';
  flex: 1 1 0;
  height: 1px;
  max-width: 140px;
  background: linear-gradient(90deg, transparent, var(--hair-2));
}
.ornament::after { background: linear-gradient(90deg, var(--hair-2), transparent); }
.ornament i {
  width: 5px;
  height: 5px;
  flex: 0 0 auto;
  transform: rotate(45deg);
  background: var(--gold);
  opacity: 0.75;
}

/* ==========================================================================
   C1 · Greeting (Today)
   ========================================================================== */

/* The first thing seen each day, so it gets the most typographic weight in
   the app: script salutation, display-caps line, serif numerals. */
.greeting-card {
  position: relative;
  padding: 30px 26px 26px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background:
    radial-gradient(130% 100% at 0% 0%, rgba(63, 190, 139, 0.10), transparent 60%),
    var(--glass);
  box-shadow: var(--sheen), var(--shadow-sm);
  overflow: hidden;
  margin-bottom: 42px;
}

.greeting-hi {
  font-family: var(--script);
  font-size: clamp(30px, 8vw, 44px);
  color: var(--mint);
  line-height: 1;
  margin-bottom: 10px;
}

/* No max-width. 44ch is the right measure for a column of running prose and
   the wrong one for a single line inside a card: on a phone it wrapped to
   three lines with a third of the card empty beside it, which reads as a
   layout that has lost its right edge rather than as a comfortable measure.
   The card's own padding is the constraint, and at desktop widths the line
   is one or two lines long whatever it is given. */
.greeting-line {
  font-family: var(--serif);
  font-size: clamp(17px, 3.4vw, 21px);
  font-weight: 300;
  color: var(--ink-2);
  line-height: 1.5;
}

/* Three equal cells with a hairline between each. A grid rather than a
   wrapping flex row: as flex the two totals huddled at their natural width
   and the row read as one count and then a pair, when the three are one
   set of figures. Equal tracks give each the same share of the card, and
   the rules between them mark the cells rather than divide the row. */
.greeting-stats {
  display: grid;
  grid-template-columns: 1fr 1px 1fr 1px 1fr;
  align-items: center;
  column-gap: 14px;
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--hair);
}

.gstat-split {
  align-self: stretch;
  width: 1px;
  background: linear-gradient(to bottom, transparent, var(--hair-2) 22%, var(--hair-2) 78%, transparent);
}

/* The three cells. Two of them are buttons and one is not, so the shared
   class carries everything about how a cell looks and .gstat-go adds only
   the affordance — otherwise a <button> arrives with its own font, padding,
   background and centring and the row stops lining up. */
.gstat {
  padding: 0;
  border: 0;
  background: none;
  text-align: center;
  font: inherit;
  color: inherit;
  min-width: 0;
}
.gstat-go {
  cursor: pointer;
  border-radius: var(--r-sm);
  transition: opacity var(--dur-1) var(--ease);
}
.gstat-go:hover { opacity: 0.72; }
.gstat-go:focus-visible { outline: 1px solid var(--emerald-glow); outline-offset: 5px; }
/* The caption carries the hint that the cell is live — a chevron beside a
   34px numeral would be a second mark competing with it, and underlining a
   figure makes it look like a link in running text rather than a count. */
.gstat-go .gstat-l { color: var(--mint); }

/* A profile stat tile that is also a link. The card is the whole target and
   the caption goes mint — the same cue the greeting counts use — so the two
   places you see these numbers say "tap me" the same way. */
.stat-go {
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}
.stat-go:hover { border-color: var(--hair-3); transform: translateY(-2px); }
.stat-go:focus-visible { outline: 1px solid var(--emerald-glow); outline-offset: 3px; }
.stat-go .eyebrow { color: var(--mint); }

.gstat-n {
  font-family: var(--serif);
  font-size: 34px;
  font-weight: 300;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: lining-nums proportional-nums;
}
.gstat-l {
  margin-top: 6px;
  font-size: 9.5px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-4);
}

/* There is no .gstat-word. A rule lived here to set a word — the season —
   at 25px among 34px numerals, on the theory that matching cap heights would
   put it on the same optical line. It did not: the shorter line box lifted
   the word's own caption clear of its neighbours', so the row read as two
   stats and a visitor. The slot now holds a count like the other two and
   needs no special case, which is the tell that the markup is finally right.

   Deliberately left as a comment rather than deleted silently, because the
   temptation to put a word in this row will come back. */

/* ==========================================================================
   C2 · Task rows (Today)
   ========================================================================== */

.task {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 16px 18px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--glass);
  box-shadow: var(--sheen);
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease),
              opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.task:hover { border-color: var(--hair-2); background: var(--glass-2); }

/* Overdue is champagne, not red. It should feel like a gentle reminder from
   a concierge rather than a system alarm. */
.task.is-overdue {
  border-color: rgba(200, 169, 107, 0.3);
  background: linear-gradient(100deg, var(--gold-wash), var(--glass) 62%);
}

/* "Coming up" rows navigate instead of being ticked off. */
.task.is-link { cursor: pointer; }
.task.is-link:hover { transform: translateX(3px); }

.task.is-done { opacity: 0.4; }
.task.is-done .task-title { text-decoration: line-through; text-decoration-color: var(--ink-4); }

/* Circular hairline check that fills emerald when complete. */
.task-check {
  width: 27px;
  height: 27px;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  border: 1px solid var(--hair-3);
  border-radius: 99px;
  background: none;
  color: transparent;
  cursor: pointer;
  transition: all var(--dur-2) var(--ease);
}
.task-check svg { width: 14px; height: 14px; stroke-width: 2; }
.task-check:hover { border-color: var(--emerald-glow); transform: scale(1.08); }
.task.is-done .task-check {
  background: var(--emerald-lit);
  border-color: var(--emerald-lit);
  color: #08140E;
}

.task-body { min-width: 0; flex: 1; }
.task-title {
  font-family: var(--serif);
  font-size: 19px;
  font-weight: 400;
  color: var(--ink);
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.task-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}
/* The care-type glyph lives here rather than in the title: the title should
   be nothing but a serif line of words. */
.task-meta svg {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  stroke-width: 1.3;
  color: var(--ink-4);
}
.task.is-overdue .task-meta svg { color: var(--gold); }

.task-thumb {
  position: relative;
  overflow: hidden;
  width: 46px;
  height: 46px;
  flex: 0 0 auto;
  border-radius: var(--r);
  border: 1px solid var(--hair);
  object-fit: cover;
  background: var(--bg-3);
}

/* ---------- Photoless plant plate ----------
   Every place a plant can show a photo also has to handle not having one.
   The old fallback was the species emoji, which put a different
   illustration style in every tile. This is a deep tinted ground derived
   from the plant's own name (see UI.tintFor) carrying the plant's initial,
   lit from the same upper-left as everything else. Nine of these in a grid
   read as a considered set rather than a sticker sheet.

   It started out as a hairline leaf on every tile, which was worse than it
   sounds: six tiles in a room strip came out as six identical glyphs in six
   near-identical dark greens, reading as six empty placeholders. The
   initial distinguishes them, survives being shrunk to 34px, and speaks the
   same monogram language as the room plates. */
/* The plate carries its tint as a class rather than an inline gradient, so a
   theme switch re-tints every tile on screen without re-rendering a view —
   which an inline literal could not do, since the views are only redrawn on
   navigation. .tmark-0 is the fallback for a plate rendered before its class
   lands. */
.tmark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--mint);
  background:
    radial-gradient(120% 100% at 18% 0%, rgba(var(--tile-sheen), var(--tile-sheen-a)), transparent 62%),
    linear-gradient(152deg, var(--tint-0a), var(--tint-0b));
}
.tmark-0 { background: radial-gradient(120% 100% at 18% 0%, rgba(var(--tile-sheen), var(--tile-sheen-a)), transparent 62%), linear-gradient(152deg, var(--tint-0a), var(--tint-0b)); }
.tmark-1 { background: radial-gradient(120% 100% at 18% 0%, rgba(var(--tile-sheen), var(--tile-sheen-a)), transparent 62%), linear-gradient(152deg, var(--tint-1a), var(--tint-1b)); }
.tmark-2 { background: radial-gradient(120% 100% at 18% 0%, rgba(var(--tile-sheen), var(--tile-sheen-a)), transparent 62%), linear-gradient(152deg, var(--tint-2a), var(--tint-2b)); }
.tmark-3 { background: radial-gradient(120% 100% at 18% 0%, rgba(var(--tile-sheen), var(--tile-sheen-a)), transparent 62%), linear-gradient(152deg, var(--tint-3a), var(--tint-3b)); }
.tmark-4 { background: radial-gradient(120% 100% at 18% 0%, rgba(var(--tile-sheen), var(--tile-sheen-a)), transparent 62%), linear-gradient(152deg, var(--tint-4a), var(--tint-4b)); }
.tmark-5 { background: radial-gradient(120% 100% at 18% 0%, rgba(var(--tile-sheen), var(--tile-sheen-a)), transparent 62%), linear-gradient(152deg, var(--tint-5a), var(--tint-5b)); }
.tmark svg {
  width: 44%;
  height: 44%;
  max-width: 54px;
  max-height: 54px;
  stroke-width: 1.15;
  opacity: 0.82;
}
/* The tile letter spans 15px in a room strip to 86px on a hero, so it is the
   one place the crossover lives inside a single component. The base is the
   small case; the two large ones take the 200 below. */
.tmark-t {
  font-family: var(--display);
  font-weight: 500;
  font-size: 20px;
  line-height: 1;
  letter-spacing: 0.04em;
  color: var(--tile-ink);
  text-transform: uppercase;
  user-select: none;
}

/* Sized by the container: the same tile shows at 34px in a room strip and
   at ~200px on a lookbook card. */
.rcard-dot  .tmark-t { font-size: 15px; }
.task-thumb .tmark-t { font-size: 21px; }
.pcard-img  .tmark-t { font-size: 60px; letter-spacing: 0.06em; font-weight: 200; }
.hero-img   .tmark-t { font-size: 86px; letter-spacing: 0.06em; font-weight: 200; }

/* ==========================================================================
   C3 · Plant cards
   ========================================================================== */

.grid-plants {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
}

/* Portrait, image-forward, captioned below — a lookbook plate rather than a
   list item. The lift on hover is small; luxury does not bounce. */
.pcard {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--glass);
  box-shadow: var(--sheen);
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  padding: 0;
  transition: border-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}
.pcard:hover {
  border-color: var(--hair-3);
  background: var(--glass-2);
  transform: translateY(-3px);
  box-shadow: var(--sheen), var(--shadow);
}

.pcard-img {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  width: 100%;
  background: var(--bg-3);
  display: grid;
  place-items: center;
  border-bottom: 1px solid var(--hair);
  transition: transform var(--dur-3) var(--ease);
}
.pcard:hover .pcard-img { transform: scale(1.03); }

/* The marker on a card's plate — water countdown in the Greenhouse, "Owned"
   in Discover. Positioned and z-indexed so it sits above the plant tile,
   which is `position: absolute; inset: 0` and would otherwise paint over it.

   Glass rather than a solid fill, because it has to stay legible over two
   very different grounds: a hashed green plate today, and a photograph of
   the actual plant once the owner adds one. A tinted blur takes its contrast
   from whatever is behind it, so it works over dark foliage and pale
   ceramic without needing to know which it is. Bottom-left, diagonally
   opposite nothing in particular — but consistently so, and clear of the
   monogram in the centre of the plate. */
.pcard-drop {
  position: absolute;
  left: 10px;
  bottom: 10px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--hair-2);
  border-radius: 99px;
  background: var(--glass-3);
  backdrop-filter: blur(14px) saturate(130%);
  -webkit-backdrop-filter: blur(14px) saturate(130%);
  font-family: var(--sans);
  font-size: 9.5px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink);
  box-shadow: var(--sheen), var(--shadow-sm);
}
.pcard-drop svg { width: 11px; height: 11px; stroke-width: 1.6; color: var(--mint); }

/* The plate scales on hover; the badge must not, or the text inside it goes
   soft and the chip's border thickens with it. Counter-scaling keeps the
   label crisp while the photograph behind it still moves. */
.pcard:hover .pcard-drop { transform: scale(0.971); }

.pcard-body { padding: 14px 15px 13px; flex: 1; }

.pcard-name {
  font-family: var(--serif);
  font-size: 18px;
  font-weight: 400;
  color: var(--ink);
  line-height: 1.22;
}
.pcard-sci {
  margin-top: 3px;
  font-family: var(--serif);
  font-size: 12.5px;
  font-style: italic;
  font-weight: 300;
  color: var(--ink-4);
  line-height: 1.35;
}

/* Bled out to the card's edges. This was written as an edge-to-edge strip —
   its own 15px padding, its own border-top — but it lives inside .pcard-body,
   which already pads by 15px. The two stacked: the pill sat 30px from the
   card's left edge while the name above it sat at 15px, and the hairline
   stopped short of both sides, so the strip looked like an indented note
   rather than a footer. Negative margins put it back where it was designed
   to be; .pcard clips, so the bottom corners stay round. */
/* flex-wrap is a guard, not a layout: every footer in the app now holds a
   single pill, so nothing wraps today. It is here because .pcard sets
   overflow to keep its bottom corners round, which means a footer that
   outgrows its card does not push or scroll — it is silently amputated. That
   is how the species grid came to display a toxicity warning reading "TO".
   A clipped word gives no hint that anything is missing, so the failure is
   invisible in review at every width you did not happen to shoot; wrapping
   is ugly for one release and then gets fixed. */
.pcard-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 13px -15px -13px;
  padding: 11px 15px;
  border-top: 1px solid var(--hair);
  font-size: 10.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* ==========================================================================
   C4 · Room cards
   ========================================================================== */

.grid-rooms {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(255px, 1fr));
}

.rcard {
  position: relative;
  padding: 20px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--glass);
  box-shadow: var(--sheen);
  cursor: pointer;
  text-align: left;
  width: 100%;
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.rcard:hover {
  border-color: var(--hair-3);
  background: var(--glass-2);
  transform: translateY(-2px);
  box-shadow: var(--sheen), var(--shadow);
}

.rcard-top { display: flex; align-items: flex-start; gap: 13px; }
/* Room monogram. The letterform is doing the work, so the square around it
   stays as quiet as possible: one hairline, a breath of emerald, nothing
   else. `.mono-sm` is the inline size used in pills and option rows. */
.mono {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  width: 46px;
  height: 46px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r-sm);
  background: radial-gradient(110% 90% at 30% 15%, rgba(63, 190, 139, 0.11), transparent 70%);
  font-family: var(--display);
  font-size: 22px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.04em;
  color: var(--ink-2);
  text-transform: uppercase;
  user-select: none;
}
.mono-sm {
  width: 18px;
  height: 18px;
  border-radius: 3px;
  font-size: 10.5px;
  letter-spacing: 0;
}
.mono-lg { width: 54px; height: 54px; font-size: 26px; }

.rcard:hover .mono { border-color: var(--hair-3); color: var(--ink); }
.rcard-name {
  font-family: var(--serif);
  font-size: 21px;
  font-weight: 400;
  color: var(--ink);
  line-height: 1.2;
}
.rcard-sub {
  margin-top: 3px;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-4);
}
/* Row of plant thumbnails along the bottom of a room card. */
.rcard-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px;
  margin-top: 18px;
  padding-top: 15px;
  border-top: 1px solid var(--hair);
}

/* Sized on the dot itself rather than on `.rcard-strip span`, which also
   caught two things it should not have: the `.tmark` photo fallback nested
   inside (a second 34px border inside the first) and `.rcard-more`, whose
   "Empty — tap to add a plant" was being squeezed into a 34px square. */
.rcard-dot {
  position: relative;
  overflow: hidden;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  background: var(--bg-3);
}
.rcard-dot img { display: block; width: 100%; height: 100%; object-fit: cover; }

.rcard-more {
  flex: 0 0 auto;
  align-self: center;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-4);
}

/* ==========================================================================
   C5 · Plant hero
   ========================================================================== */

/* Stacked by default — plate, then name. On a phone that is the only
   arrangement that works, and the plate at 3:2 across the full column is
   exactly the right amount of room to give a photo. The desktop spread
   lives in D5, not here, because it depends on the sidebar breakpoint. */
.hero { margin-bottom: 34px; }

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

/* 4:5, not 3:2. A plant is a vertical object, and the hero is the one
   placement where the photograph is the subject rather than a thumbnail —
   so it takes the portrait crop and the grid keeps the square. Three ratios
   meant whatever the owner framed, at least one placement fought it. */
.hero-img {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background:
    radial-gradient(80% 70% at 28% 18%, rgba(63, 190, 139, 0.2), transparent 70%),
    var(--bg-3);
  display: grid;
  place-items: center;
  box-shadow: var(--shadow);
}

/* The affordance over the hero plate — "Add photo" with none, "Change" with
   one. It used to carry nothing but a position, and .hero-img used to carry
   `font-size: 74px` to scale an emoji fallback that has since been replaced
   by .tmark-t, which sizes itself. The two facts met: this label inherited
   74px and printed ADD PHOTO across a third of the plate in the sans, larger
   than the plant's own name below it. Both are fixed here — the stray
   font-size is gone from the plate, and the label is now an actual control,
   a glass chip in the corner where the eye expects one. */
.hero-img-edit {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 2;
  padding: 7px 13px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r);
  background: var(--glass-3);
  backdrop-filter: blur(16px) saturate(130%);
  -webkit-backdrop-filter: blur(16px) saturate(130%);
  font-family: var(--sans);
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-2);
  box-shadow: var(--sheen);
  transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.hero-img:hover .hero-img-edit { color: var(--ink); border-color: var(--hair-3); }
/* The photograph fills the plate. Left at its own size and centred, a
   landscape shot sat in a band across the middle with the plate's wash
   showing above and below, and the two controls in the corners hung in
   that wash rather than on the picture. Cover crops instead; the plant is
   the subject and the edges are the room around it. */
.hero-img > img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; display: block; }

/* Quick update: the plate's other corner. Same glass as the photo button so
   the two read as one pair of controls on the picture, and the menu opens
   upward from it over the photograph rather than pushing the page down. */
.quick-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 13px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r);
  background: var(--glass-3);
  backdrop-filter: blur(16px) saturate(130%);
  -webkit-backdrop-filter: blur(16px) saturate(130%);
  font-family: var(--sans);
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  box-shadow: var(--sheen);
  cursor: pointer;
}
.quick-btn svg { width: 12px; height: 12px; }
.quick-btn[aria-expanded="true"] { border-color: var(--hair-3); background: var(--bg-3); }
.hero-img-quick { position: absolute; left: 12px; bottom: 12px; z-index: 2; }
/* One menu, two homes: up from the photograph's corner, down from the
   diary's button. The anchor gives the downward one something to hang from. */
.menu-anchor { position: relative; display: inline-block; }
.pop-menu {
  position: absolute;
  z-index: 3;
  min-width: 176px;
  padding: 6px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r);
  background: var(--bg-2);
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.35), var(--sheen);
}
.hero-menu { left: 12px; bottom: 54px; }
.pop-menu.is-down { left: 0; top: calc(100% + 8px); }
.pop-opt {
  display: flex; align-items: center; gap: 11px;
  width: 100%;
  padding: 11px 12px;
  border: 0; border-radius: calc(var(--r) - 2px);
  background: none;
  color: var(--ink);
  font-family: var(--sans); font-size: 13.5px; letter-spacing: 0.02em;
  text-align: left;
  cursor: pointer;
}
.pop-opt svg { width: 16px; height: 16px; color: var(--ink-3); flex: 0 0 auto; }
.pop-opt:hover, .pop-opt:focus-visible { background: var(--bg-3); outline: none; }
.pop-opt + .pop-opt { border-top: 1px solid var(--hair); border-top-left-radius: 0; border-top-right-radius: 0; }
.pop-opt:has(+ .pop-opt) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }

.hero-nick {
  font-family: var(--serif);
  font-size: clamp(32px, 7vw, 50px);
  font-weight: 300;
  line-height: 1.06;
  color: var(--ink);
  margin-top: 22px;
}
/* The name and the date it arrived, on one line. Baseline-aligned rather
   than centred, so the small text sits on the same line the 50px display
   face is standing on instead of floating at its middle; and allowed to
   wrap, because a long nickname at clamp(32px, 7vw, 50px) will take the
   whole measure on its own and the date has to go somewhere. */
.hero-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  margin-top: 22px;
}
.hero-line .hero-nick { margin-top: 0; }
.hero-since {
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.1em;
  color: var(--ink-4);
  white-space: nowrap;
}
.hero-sci {
  margin-top: 5px;
  font-family: var(--serif);
  font-size: 15px;
  font-style: italic;
  font-weight: 300;
  color: var(--ink-4);
}
/* The common name leads the botanical one on the same line, so it takes the
   upright of the pair — italic is what marks the binomial, and two italics
   either side of a dash would say they were the same kind of name. */
.hero-common {
  font-style: normal;
  color: var(--ink-2);
}

/* ==========================================================================
   C6 · Fact grid
   ========================================================================== */

/* A hairline-divided list. The 1px gap shows the container's own background,
   so the divisions cost no per-cell border bookkeeping and no double lines
   at the edges.

   Single column, deliberately. This was `repeat(auto-fit, minmax(140px,
   1fr))`, and auto-fit is the wrong tool whenever the item count is
   data-driven: any count that is not a multiple of the computed column
   count leaves empty grid areas, and on this container an empty area is a
   solid slab of --hair. The room card had exactly that — three facts, two
   columns on a phone, one grey rectangle in the corner that looked like a
   render fault. Widening the minmax does not fix it, it only changes which
   counts break.

   Multi-column fact displays now go through UI.specSheet, which lays each
   fact out as a row internally and therefore cannot orphan anything. */
.facts {
  display: grid;
  gap: 1px;
  grid-template-columns: minmax(0, 1fr);
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--hair);
  overflow: hidden;
}

/* Cells sit on a 1px background grid, so the gaps themselves are the
   hairlines — no per-cell border bookkeeping. */
.fact {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  padding: 17px 16px;
  background: var(--bg-2);
  transition: background var(--dur-2) var(--ease);
}
.fact:hover { background: var(--bg-3); }

.fact-ico { flex: 0 0 auto; display: flex; align-items: center; padding-top: 1px; }
.fact-ico svg { width: 18px; height: 18px; stroke-width: 1.15; color: var(--mint); }
.fact-k {
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.fact-v {
  margin-top: 4px;
  font-family: var(--serif);
  font-size: 16px;
  font-weight: 400;
  color: var(--ink);
  line-height: 1.35;
}

/* --- Spec sheet: one fact per full-measure row (UI.specSheet) ---
   The mosaic above is for short cells. Prose needs the measure, and a
   data-driven fact count needs a layout with no remainder, so this variant
   is a single column: label in its own narrow column so the values start on
   a common left edge and the eye can run down them, prose to the right. */
.facts-rows > .fact {
  display: grid;
  grid-template-columns: 18px 150px minmax(0, 1fr);
  align-items: start;
  gap: 0 17px;
  padding: 16px 20px;
}

/* Optical alignment, not metric: the caps label is 9.5px and the serif value
   beside it is 16px, so matching their box tops leaves the label floating.
   This drops it onto the value's cap height. */
/* One step up from --ink-4 in this variant only. Elsewhere .fact-k is a
   caption under a figure that already reads; here it is the column you scan
   down to find the fact you came for, so it has to hold at 9.5px caps. */
.facts-rows .fact-k { padding-top: 5px; color: var(--ink-3); }
.facts-rows .fact-body { min-width: 0; }
.facts-rows .fact-v { margin-top: 0; }
.facts-rows .fact-body .hint { margin: 5px 0 0; max-width: 68ch; }

/* Narrow: the label column costs more than it buys, so stack it over the
   value. Both children have to be placed explicitly — with two tracks,
   auto-placement would drop .fact-body into column 1 under the icon. */
@media (max-width: 720px) {
  .facts-rows > .fact {
    grid-template-columns: 18px minmax(0, 1fr);
    gap: 0 13px;
    padding: 15px 17px;
  }
  .facts-rows .fact-k { grid-column: 2; padding-top: 2px; }
  .facts-rows .fact-body { grid-column: 2; margin-top: 4px; }
}

/* --- Ledger: the growth measurement list ---
   A measurement is not a fact with a paragraph attached, it is a row in a
   table, and the only reason to open this section is to watch a column of
   figures climb. So the date, the figure and the change since the entry
   before it each get a track of their own, which is what lets the eye run
   straight down the numbers. As one .facts cell the figure sat in the
   leftmost 80px of a 1000px band with the rest of the row empty — the list
   held the space of a table and gave none of its legibility.

   No icon column. Every row here is the same kind of thing, so a ruler
   repeated once per measurement is decoration N times over; the section
   title already says what these are. Dropping it also lets the dates start
   on the card's own left edge, in line with the heading above them. */
.facts-ledger > .fact {
  display: grid;
  grid-template-columns: 88px minmax(88px, auto) minmax(0, 1fr) 26px;
  align-items: center;
  gap: 0 18px;
  padding: 13px 20px;
}
.facts-ledger .fact-k { color: var(--ink-3); }
.facts-ledger .fact-body { min-width: 0; }
/* Tabular figures: this is the column you scan, so it must not shift
   sideways as the digit count changes between 84 and 9. */
.facts-ledger .fact-v {
  margin-top: 0;
  font-size: 17px;
  font-variant-numeric: tabular-nums;
}

/* Mint for a gain and nothing at all otherwise. Growth going backwards is
   usually a prune or a lost leaf, not an emergency, so clay would overstate
   it — and the four hues only keep their meaning while nothing borrows
   them. */
.meas-delta { font-size: 12.5px; color: var(--ink-4); letter-spacing: 0.01em; }
.meas-delta.is-up { color: var(--mint); }
.meas-note { display: block; margin-top: 3px; font-size: 12.5px; color: var(--ink-3); }
.facts-ledger .tl-del { justify-self: end; }

/* Narrow: three tracks, and the figure moves under the date rather than
   beside it. Every child is placed explicitly — the delta has to reach the
   right edge on row 1 while the figure spans back to the left on row 2,
   which auto-placement cannot produce. */
@media (max-width: 560px) {
  .facts-ledger > .fact {
    grid-template-columns: minmax(0, 1fr) auto 26px;
    gap: 3px 12px;
    padding: 12px 16px;
  }
  .facts-ledger .fact-k     { grid-area: 1 / 1 / 2 / 2; }
  .facts-ledger .meas-delta { grid-area: 1 / 2 / 2 / 3; text-align: right; }
  .facts-ledger .fact-body  { grid-area: 2 / 1 / 3 / 3; }
  .facts-ledger .tl-del     { grid-area: 1 / 3 / 3 / 4; align-self: center; }
}

/* --- Trait list: "Loves" / "Hates" ---
   These were bare <ul>s with browser disc bullets and a padding-left — the
   last UA-styled list in the app, and a dot of solid ink is the one marker
   this design does not use anywhere else. A hairline dash instead: it sits
   in the same family as the divider, the section rule and the counter
   underline on .dx-steps.

   Deliberately colourless. Mint for loves and clay for hates would look
   obvious and be wrong: those two hues mean "you are fine" and "act now"
   throughout the app, and a species preference is neither. The eyebrow
   already says which list this is. */
/* Loves wears the leaf, Hates the terracotta: the same two meanings the
   pills and nudges already give those hues, so the pair reads before a
   word of it does. */
.card-loves { background: var(--mint-wash); border-color: rgba(63, 190, 139, 0.22); }
.card-loves .eyebrow { color: var(--emerald-glow); }
.card-hates { background: var(--terra-soft); border-color: rgba(208, 138, 98, 0.26); }
.card-hates .eyebrow { color: var(--terra-ink); }
.trait-list { margin: 10px 0 0; padding: 0; list-style: none; }
.trait-list > li {
  position: relative;
  padding-left: 19px;
  font-size: 13.5px;
  color: var(--ink-2);
  line-height: 1.6;
}
.trait-list > li + li { margin-top: 7px; }
.trait-list > li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.72em;
  width: 10px;
  height: 1px;
  background: var(--hair-2);
}

/* ==========================================================================
   C7 · Timeline / diary
   ========================================================================== */

.timeline { position: relative; padding-left: 42px; }

/* Single continuous hairline behind the dots, fading out at the bottom so
   the list does not appear truncated. Centred under the 26px marks. */
.timeline::before {
  content: '';
  position: absolute;
  left: 13px;
  top: 8px;
  bottom: 6px;
  width: 1px;
  background: linear-gradient(to bottom, var(--hair-2), var(--hair-2) 70%, transparent);
}

.tl-item { position: relative; margin-bottom: 24px; }
.tl-item:last-child { margin-bottom: 0; }
/* The entry the reader came here for, when they arrived from the diary
   list. A brief wash that fades, not a persistent mark — it answers "which
   one?" once and then gets out of the way. */
.tl-item.is-target .tl-card,
.tl-item.is-target .tl-metric { animation: targetGlow 2400ms var(--ease) both; }
@keyframes targetGlow {
  0%, 30% { box-shadow: 0 0 0 2px rgba(63, 190, 139, 0.55); }
  100%    { box-shadow: 0 0 0 2px rgba(63, 190, 139, 0); }
}

/* The mark was a bare 9px dot, which meant every entry in the diary looked
   identical — a watering, a photo and a repot all reduced to the same
   speck. At 26px it can hold the log-kind glyph, so the rail becomes
   scannable: you can find the repot without reading a single date. The
   opaque background is what lets it sit on top of the hairline.

   left: -42px, negating .timeline's padding. This said `left: 0`, and the
   containing block is .tl-item, which begins *after* that padding — so
   every mark in the diary was drawn 42px too far right, directly on top of
   its own date. The rendered result was a column of circles with "19 SEP"
   bleeding out from under them, and a hairline rail off to the left with
   nothing on it, which is the tell: a rail with no marks means the marks
   are measuring from the wrong edge. The two numbers are now the same
   number with opposite signs, so the mark cannot drift from the gutter
   again without the gutter moving too. */
.tl-dot {
  position: absolute;
  left: -42px;
  top: 0;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--bg-1);
  border: 1px solid var(--hair-2);
  transition: border-color var(--dur-2) var(--ease);
}
.tl-dot svg { width: 13px; height: 13px; stroke-width: 1.3; color: var(--ink-4); }

.tl-item:hover .tl-dot { border-color: var(--emerald-glow); }
.tl-item:hover .tl-dot svg { color: var(--mint); }

/* Date on the left, delete on the right, sharing the entry's top line. The
   rule sits between them rather than under the date so that a bodyless
   entry — a watering, say — still draws a full-width line and the diary
   reads as a ruled ledger instead of a ragged list. */
.tl-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 7px;
}

.tl-date {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 9.5px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-4);
}

/* Revealed on hover, and on focus-within so it is reachable by keyboard —
   an action you cannot tab to is not an action, it is a decoration.

   Opacity rather than display, for two reasons. It keeps the control in the
   layout, so the date line does not jump sideways as the pointer crosses
   it; and it keeps the button in the tab order at all times, so the reveal
   is purely visual and the keyboard path never depends on it. */
.tl-del {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-4);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease), color var(--dur-1) var(--ease),
              background var(--dur-1) var(--ease);
}
.tl-del svg { width: 13px; height: 13px; stroke-width: 1.4; }

/* Two hosts, because the same control appears on a diary entry and on a row
   of the growth tab's measurement list. Listed explicitly rather than using
   a bare `:hover .tl-del` so the reveal cannot be triggered by some future
   ancestor that happens to be hovered — the whole card, say, which would
   light up every trash icon in the section at once. */
.tl-item:hover .tl-del,
.tl-item:focus-within .tl-del,
.fact:hover .tl-del,
.fact:focus-within .tl-del { opacity: 1; }

.tl-del:hover { color: var(--terra); background: var(--terra-soft); }

/* A touch device has no hover to reveal anything with, so the control would
   be permanently invisible and the entry permanently undeletable. Print it
   at half strength instead: present, findable, still not shouting. */
@media (hover: none) {
  .tl-del { opacity: 0.45; }
}

.tl-card {
  padding: 15px 17px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--glass);
  box-shadow: var(--sheen);
}

/* A logged measurement. Upright serif against the diary's italic prose: a
   number is a fact, not a remark, and the two should not be in the same
   voice. Lining figures, so a column of them down the rail lines up on the
   same baseline and you can read the growth off the page without reaching
   for the chart.

   Sits bare on the page when it is the whole entry, and inside the card
   when there is also a note or a photograph — see ViewPlant.diaryTab. The
   negative top margin pulls it up under its own date line, since without a
   card there is no padding to close the gap. */
.tl-metric {
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 300;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--ink);
  line-height: 1.2;
  margin-top: -1px;
}
.tl-card .tl-metric { margin-top: 0; }
.tl-card .tl-metric + .tl-note { margin-top: 6px; }

/* Diary prose. Italic serif makes a note read as handwriting rather than
   as a log entry. */
.tl-note {
  font-family: var(--serif);
  font-size: 16px;
  font-style: italic;
  font-weight: 300;
  color: var(--ink-2);
  line-height: 1.62;
}

/* ==========================================================================
   C8 · Photos
   ========================================================================== */

.photo-grid {
  display: grid;
  gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
}

.photo-cell {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--r);
  border: 1px solid var(--hair);
  overflow: hidden;
  cursor: pointer;
  background: var(--bg-3);
  padding: 0;
  transition: border-color var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.photo-cell:hover { border-color: var(--hair-3); transform: scale(1.02); }
.photo-cell img { width: 100%; height: 100%; object-fit: cover; }

.photo-cell-date {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 14px 8px 6px;
  background: linear-gradient(to top, rgba(5, 10, 8, 0.88), transparent);
  color: var(--mint-2);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: center;
}

/* Dashed add-tile that matches the photo cells' footprint.

   Flex column, not `display: grid; place-items: center`, which is what this
   was and which does not do what it looks like it does. place-items centres
   each item inside its own track; it says nothing about the tracks. The
   tile has two items — the camera and the label — so the grid made two
   implicit rows, and since align-content defaults to stretch those rows
   each took half of a square tile. Both items were then dutifully centred:
   the camera in the middle of the top half, the label in the middle of the
   bottom half, roughly 60px apart, reading as two unrelated things in a
   dashed box rather than one button.

   `place-content: center` would also have fixed it, but a column of two
   things with a gap is what flex is for, and the fix should not depend on
   the reader knowing the difference between place-items and place-content. */
.photo-add {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  border: 1px dashed var(--hair-3);
  border-radius: var(--r);
  background: none;
  color: var(--ink-3);
  cursor: pointer;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  gap: 6px;
  transition: all var(--dur-2) var(--ease);
}
.photo-add:hover { border-color: var(--emerald-glow); color: var(--mint); background: var(--mint-wash); }
.photo-add svg { width: 19px; height: 19px; stroke-width: 1.3; }

/* ==========================================================================
   C9 · Growth chart
   ========================================================================== */

.chart { width: 100%; height: auto; max-height: 220px; display: block; overflow: visible; }
.chart-ghost { fill: none; stroke: var(--hair-3); stroke-width: 1.4; stroke-dasharray: 3 6; stroke-linecap: round; }
.chart-area { fill: url(#gGrad); }
.chart-line { fill: none; stroke: var(--emerald-glow); stroke-width: 1.6; stroke-linecap: round; }
.chart-dot  { fill: var(--bg-1); stroke: var(--emerald-glow); stroke-width: 1.6; }
.chart-grid { stroke: var(--hair-2); stroke-width: 1; stroke-dasharray: 2 5; }
.chart-lbl  {
  fill: var(--ink-4);
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.08em;
}

/* ==========================================================================
   C10 · Weather
   ========================================================================== */

/* Conditions glyph and reading sit side by side; the forecast row drops
   underneath and spans both columns. */
.weather-strip {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 18px;
  /* Bottom margin matches .section so the card keeps the page's rhythm
     instead of butting straight into the next heading, which is what it did
     when it only had a top margin. Adjacent margins collapse, so following a
     .section this is one 42px gap rather than two stacked. No divider: the
     card already carries a border, and a rule under a bordered box is a
     second line doing the first one's job. */
  margin: 14px 0 42px;
  padding: 22px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background:
    radial-gradient(120% 100% at 100% 0%, rgba(127, 182, 212, 0.1), transparent 62%),
    var(--glass);
  box-shadow: var(--sheen);
}

/* Ink, not a colour. This glyph was the one saturated blue in the whole
   interface, sitting at 34px on the busiest card of the home screen — the
   single loudest mark in an app whose entire argument is restraint. It also
   fought the 44px serif temperature beside it, which is the thing you
   actually came to read. In ink it recedes to a label and the figure wins. */
.weather-ico { display: flex; align-items: center; }
.weather-ico svg {
  width: 34px;
  height: 34px;
  stroke-width: 1.05;
  color: var(--ink-3);
}

/* Proportional figures, not tabular. Cormorant's tabular digits carry a lot
   of side bearing, which at 44px opened "16°" out into "1 6°". Tabular is
   for making columns line up; nothing lines up with this. */
.weather-temp {
  font-family: var(--serif);
  font-size: 44px;
  font-weight: 300;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: lining-nums proportional-nums;
}
/* The degree mark, set apart from the figure — see UI.deg().
   Cormorant's U+00B0 is a high-contrast lowercase o: a thin ring with two
   thick shoulders, sized for a 16px paragraph. Blown up to 44px next to a
   lining figure it stops reading as a unit and starts reading as a letter
   the number has collided with. The sans ring is an even-weight circle, so
   at 0.4em it sits quietly at cap height and the figure keeps the eye.

   `em` throughout, so the rule serves the 44px hero and the 24px topbar chip
   from one declaration. It does not scale down forever: below about 20px the
   ring shrinks to a speck floating clear of the figure and reads as a dust
   mote, so the 15px forecast tiles deliberately use the bare character and
   let Cormorant's own degree do the work. See ViewToday.weatherStrip. */
.deg {
  font-family: var(--sans);
  font-size: 0.4em;
  font-weight: 400;
  letter-spacing: 0;
  vertical-align: 0.86em;
}

.weather-now {
  margin-top: 3px;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-4);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.weather-days {
  grid-column: 1 / -1;
  display: grid;
  grid-auto-flow: column;
  gap: 4px;
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid var(--hair);
  overflow-x: auto;
}

.wday { text-align: center; min-width: 46px; }
.wday-d {
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.wday-i {
  display: flex;
  justify-content: center;
  margin: 7px 0 5px;
}
.wday-i svg {
  width: 17px;
  height: 17px;
  stroke-width: 1.1;
  color: var(--ink-3);
}
.wday-t {
  font-family: var(--serif);
  font-size: 15px;
  color: var(--ink-2);
  font-variant-numeric: lining-nums tabular-nums;
}

/* ==========================================================================
   C9b · Schedule factors
   --------------------------------------------------------------------------
   The "why 9 days and not 12?" breakdown on a plant's care tab. Each row is
   one thing about the user's setup that pushed the interval one way, and the
   direction arrow is the whole point of the row — so it gets a colour
   (emerald shortens, champagne lengthens) where the rest of the list stays
   monochrome.
   ========================================================================== */

.factor { display: flex; align-items: flex-start; gap: 10px; }
.factor-dir {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  border-radius: 50%;
  border: 1px solid var(--hair-2);
}
.factor-dir svg { width: 11px; height: 11px; stroke-width: 1.6; }
.factor-dir.is-down { border-color: rgba(63, 190, 139, 0.32); background: var(--mint-wash); color: var(--mint); }
.factor-dir.is-up   { border-color: rgba(200, 169, 107, 0.32); background: var(--gold-wash); color: var(--gold); }
.factor-k { font-size: 13px; font-weight: 500; color: var(--ink); letter-spacing: 0.01em; }

/* ==========================================================================
   C10b · All-clear panel
   --------------------------------------------------------------------------
   Shown where the task list would be when there is nothing to do. It has to
   read as a reward, not as an empty state — so it gets the ornamental
   treatment rather than the apologetic one.
   ========================================================================== */

.all-clear { padding: 34px 26px; }
.all-clear-mark {
  display: inline-grid;
  place-items: center;
  width: 54px;
  height: 54px;
  margin-bottom: 14px;
  border: 1px solid rgba(63, 190, 139, 0.26);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 30%, var(--mint-wash), transparent 70%);
}
.all-clear-mark svg { width: 22px; height: 22px; stroke-width: 1.1; color: var(--mint); }
.all-clear-t {
  margin: 0;
  font-family: var(--serif);
  font-size: 21px;
  font-weight: 300;
  letter-spacing: 0.01em;
  color: var(--ink);
}

/* ==========================================================================
   C11 · Nudges
   ========================================================================== */

/* A suggestion from the app. Emerald wash by default; `.nudge-warn` is the
   pet-toxicity variant, which used to be inline pink in the view files. */
.nudge {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 19px;
  border: 1px solid rgba(63, 190, 139, 0.24);
  border-radius: var(--r-lg);
  background: linear-gradient(105deg, var(--mint-wash), var(--glass) 70%);
  box-shadow: var(--sheen);
}
/* A hairline glyph in a small mint plate — the nudge's reason-for-being,
   stated quietly. */
.nudge-ico {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid rgba(63, 190, 139, 0.26);
  border-radius: 50%;
  background: var(--mint-wash);
}
.nudge-ico svg { width: 17px; height: 17px; stroke-width: 1.2; color: var(--mint); }
.nudge-warn .nudge-ico { border-color: rgba(200, 169, 107, 0.3); background: rgba(200, 169, 107, 0.09); }
.nudge-warn .nudge-ico svg { color: var(--gold); }
.nudge-t {
  font-family: var(--serif);
  font-size: 18px;
  font-weight: 400;
  color: var(--ink);
  line-height: 1.3;
}
.nudge-p {
  margin-top: 5px;
  font-size: 13.5px;
  color: var(--ink-2);
  line-height: 1.6;
}

/* Champagne, not rose. The overdue task row already established that this
   system raises concerns in gold; a pink card alongside it would read as a
   second, louder severity level that the app does not actually have. */
.nudge-warn {
  border-color: rgba(200, 169, 107, 0.34);
  background: linear-gradient(105deg, var(--gold-wash), var(--glass) 70%);
}

/* The one louder level, and it exists for exactly one thing: a plant that
   is poisonous to an animal the user has told us lives in the house. That
   is the only message in the app where the cost of being ignored is a vet
   bill, so it gets terracotta while everything else stays champagne. Used
   sparingly is the whole point — a second alert style would flatten it. */
.nudge-alert {
  border-color: rgba(208, 138, 98, 0.38);
  background: linear-gradient(105deg, var(--terra-soft), var(--glass) 72%);
}
.nudge-alert .nudge-ico {
  border-color: rgba(208, 138, 98, 0.34);
  background: rgba(208, 138, 98, 0.1);
}
.nudge-alert .nudge-ico svg { color: var(--terra); }
.nudge-alert .nudge-t { color: var(--ink); }

/* A nudge that navigates. */
.nudge.is-link { cursor: pointer; transition: transform var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease); }
.nudge.is-link:hover { transform: translateX(3px); }
.nudge > svg { flex: 0 0 auto; align-self: center; width: 16px; height: 16px; stroke-width: 1.4; }

/* --- Assurance: one quiet line saying nothing is wrong ---
   Deliberately not a .nudge. A nudge is a plate, a bordered box, a title and
   a paragraph, and it earns that weight by asking you to do something. Good
   news asks for nothing, so dressing it as a card would put "everything in
   here is fine" on exactly the same footing as "this plant is in the wrong
   light" — and on a room where nothing is wrong, the single most card-like
   object on the page would be the one with no action in it.

   It replaces a RIGHT LIGHT pill repeated once per plant card. Four
   identical chips down a grid is wallpaper: it reads as decoration, so the
   one card that says something different stops standing out, which is the
   only case the mark existed for. Stated once, in prose, it is a sentence
   the page says to you rather than a label stamped on every tile. */
.assure {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-3);
}
.assure svg {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  stroke-width: 1.3;
  color: var(--mint);
}

/* ==========================================================================
   C12 · Diagnose
   ========================================================================== */

.dx-opt {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 17px 18px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--glass);
  box-shadow: var(--sheen);
  color: var(--ink);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-2) var(--ease), background var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease);
}
.dx-opt:hover {
  border-color: rgba(63, 190, 139, 0.4);
  background: var(--glass-2);
  transform: translateX(3px);
}
.dx-opt.is-on {
  border-color: rgba(63, 190, 139, 0.5);
  background: var(--mint-wash);
}

/* Leading glyph on an option row. Deliberately not plated — these appear 40
   at a time in the species picker, and 40 mint circles would be a pattern
   rather than a list. */
.dx-opt-ico { flex: 0 0 auto; display: flex; align-items: center; }
.dx-opt-ico svg { width: 18px; height: 18px; stroke-width: 1.2; color: var(--ink-4); }
.dx-opt:hover .dx-opt-ico svg { color: var(--mint); }

/* Both of these are spans inside a span, so they need to be told to stack —
   otherwise the name and the species run together on one line. */
.dx-opt-t { display: block; font-family: var(--serif); font-size: 17px; font-weight: 400; }
.dx-opt-d { display: block; font-size: 12.5px; color: var(--ink-3); margin-top: 2px; line-height: 1.45; }

/* The trailing chevron. Without an explicit size an inline SVG stretches to
   fill the flex row, which blew the option card up to several hundred pixels
   tall. Scoped to direct children so it leaves .dx-opt-chk's tick alone. */
.dx-opt > svg { margin-left: auto; flex: 0 0 auto; width: 17px; height: 17px; stroke-width: 1.4; }

.dx-opt-chk {
  margin-left: auto;
  flex: 0 0 auto;
  width: 23px;
  height: 23px;
  display: grid;
  place-items: center;
  border: 1px solid var(--hair-3);
  border-radius: var(--r-sm);
  color: transparent;
  transition: all var(--dur-2) var(--ease);
}
.dx-opt-chk svg { width: 13px; height: 13px; stroke-width: 2; }
.dx-opt.is-on .dx-opt-chk {
  background: var(--emerald-lit);
  border-color: var(--emerald-lit);
  color: #08140E;
}

/* Confidence readout on a result: a short filled track and the word for it.
   The track is deliberately tiny — 44px of 2px hairline. It is a relative
   weight, not a measurement, and drawn any larger it would start to look
   like a claim to precision the scoring cannot support. */
.dx-conf-wrap {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.dx-conf {
  flex: 0 0 auto;
  width: 44px;
  height: 2px;
  border-radius: 99px;
  background: var(--hair);
  overflow: hidden;
}
/* Micro-caps, the same register as .pcard-foot and .section-note, so the
   word sits level with the severity pill beside it instead of competing
   with the cause name above. */
.dx-conf-t {
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.dx-conf > i {
  display: block;
  height: 100%;
  border-radius: 99px;
  background: linear-gradient(90deg, var(--emerald), var(--emerald-glow));
  transition: width var(--dur-3) var(--ease);
}

.dx-result {
  padding: 22px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r-lg);
  background:
    radial-gradient(120% 90% at 0% 0%, rgba(63, 190, 139, 0.09), transparent 60%),
    var(--glass);
  box-shadow: var(--sheen), var(--shadow-sm);
}

/* The cause mark. Plated, because unlike a symptom a cause belongs to a
   family — watering, feeding, light, pest, fungal — and the ring is what
   makes that reading register as a category rather than a doodle. It takes
   its colour from the card's severity so the triage is visible before you
   read a word: emerald for "not urgent", champagne for "soon", terracotta
   for "act quickly". */
.dx-cause-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border: 1px solid rgba(63, 190, 139, 0.26);
  border-radius: 50%;
  background: var(--mint-wash);
}
.dx-cause-mark svg { width: 18px; height: 18px; stroke-width: 1.15; color: var(--mint); }

.dx-result.sev-med .dx-cause-mark {
  border-color: rgba(200, 169, 107, 0.32);
  background: var(--gold-wash);
}
.dx-result.sev-med .dx-cause-mark svg { color: var(--gold); }

.dx-result.sev-high .dx-cause-mark {
  border-color: rgba(208, 138, 98, 0.34);
  background: var(--terra-soft);
}
.dx-result.sev-high .dx-cause-mark svg { color: var(--terra); }

/* Numbered treatment steps. The counter is serif and mint — it reads as a
   recipe rather than an error list. */
.dx-steps { margin: 14px 0 0; padding: 0; list-style: none; counter-reset: dx; }
.dx-step, .dx-steps > li {
  counter-increment: dx;
  position: relative;
  padding-left: 34px;
  margin-bottom: 13px;
  font-size: 14px;
  color: var(--ink-2);
  line-height: 1.65;
}
.dx-steps > li:last-child { margin-bottom: 0; }
.dx-step::before, .dx-steps > li::before {
  content: counter(dx);
  position: absolute;
  left: 0;
  top: -1px;
  width: 23px;
  text-align: center;
  font-family: var(--serif);
  font-size: 16px;
  color: var(--mint);
  border-bottom: 1px solid var(--hair-2);
}

/* A control that closes a card. .dx-steps deliberately drops the last step's
   trailing margin — right when the list ends the card, as it does in
   Diagnose — so anything placed after it butts straight into the final step.
   On Propagate a button does follow, and with no separation it read as a
   sixth step rather than as the thing you press when you have done the five.
   The rule bleeds to the card's edges (the card clips, so the radius is not
   a problem) and the hairline does the separating, no fill: a footer strip
   tinted differently from the card is indistinguishable on ivory, where
   --glass and --glass-2 are both near-white. */
.card-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 17px -20px -20px;
  padding: 14px 20px;
  border-top: 1px solid var(--hair-2);
}

/* ==========================================================================
   C13 · Sheet
   ========================================================================== */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 180;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(3, 7, 5, 0.66);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  animation: fade var(--dur-2) var(--ease) both;
}

.sheet {
  width: 100%;
  max-width: 560px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--hair-2);
  border-bottom: 0;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  background:
    radial-gradient(100% 60% at 50% 0%, var(--sheet-glow), transparent 70%),
    var(--sheet-bg);
  box-shadow: var(--shadow-lg), var(--sheen);
  animation: slideUp var(--dur-3) var(--ease) both;
}

.sheet-grip {
  width: 40px;
  height: 3px;
  margin: 11px auto 0;
  border-radius: 99px;
  background: var(--hair-3);
  flex: 0 0 auto;
}

.sheet-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px 22px 15px;
  border-bottom: 1px solid var(--hair);
  flex: 0 0 auto;
}
.sheet-title {
  flex: 1;
  font-family: var(--display);
  /* 20px was once a floor rather than a choice. Italiana had one weight, its
     thin strokes were about a ninth of its cap height, and below 20px they
     asked for roughly one pixel of ink — which antialiasing spread across
     two rows at partial coverage, leaving a pale grey line beside a
     near-black stem. "ADD TO YOUR GREENHOUSE" lost the crossbars of its T, E
     and H, so the T read as an I.

     One observation from that investigation is worth keeping, because it is
     about method rather than this face: raising the device pixel ratio does
     not help. 1x, 2x and 3x captures were equally patchy, since the ratio
     scales the stroke and the pixel grid together. A pass before it had
     concluded the opposite and filed the dropout as a 1x artifact to review
     away at dpr 3 — true of the large sizes and wrong everywhere else, which
     turned a real defect into a reason to stop looking.

     The floor is gone now: the display face has a 500 and it holds every
     stroke at 14px. 20px stays because a modal's heading wants that size,
     not because anything breaks below it. */
  font-size: 20px;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--ink);
}

/* The subject of a sheet, restated inside it. The sheet title is display
   caps and deliberately reticent; this is where the plant actually gets to
   introduce itself. */
.sheet-ident {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 22px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--hair);
}
/* The rule is a separator, so it only earns its place when there is
   something on the other side of it. Diagnose step 3 restates the symptom in
   this block inside a plain .card, and there the border drew a hairline
   across the bottom of the card with 20px of dead space beneath it — a
   divider dividing nothing, which reads as a component that lost its second
   half. Sheets are untouched, since the identity block is always followed by
   the form it introduces. Same instinct as .sheet-foot:empty: chrome whose
   whole job is to relate two things should disappear when there is one. */
.sheet-ident:last-child {
  margin-bottom: 0;
  padding-bottom: 0;
  border-bottom: 0;
}
.sheet-ident-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border: 1px solid var(--hair-2);
  border-radius: 50%;
  background: radial-gradient(circle at 50% 30%, var(--mint-wash), transparent 72%);
}
.sheet-ident-mark svg { width: 21px; height: 21px; stroke-width: 1.1; color: var(--mint); }
.sheet-ident-t {
  font-family: var(--serif);
  font-size: 20px;
  font-weight: 400;
  line-height: 1.2;
  color: var(--ink);
}

.sheet-body {
  padding: 22px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* --- The pinned action bar ---
   flex: 0 0 auto so it keeps its height while the body above it shrinks and
   scrolls. The room form is the case that forced this: five fields plus two
   hints come to 682px of content in a sheet capped at 90vh, so Save sat below
   the fold and the one control the whole sheet exists to reach had to be
   found by scrolling. A modal that hides its own commit button reads as
   unfinished however well the rest of it is set.

   :empty collapses it completely — border, padding and all — so sheets that
   carry no action row (the photo viewer, the diagnose detail) are untouched
   rather than gaining a stray hairline across the bottom.

   Flat --sheet-bg rather than the plate's gradient: the gradient is a
   radial from 50% 0%, long since faded to transparent this far down, so the
   flat token is what the surrounding pixels already are. The hairline above
   is what separates the bar from content scrolling underneath it. */
.sheet-foot {
  flex: 0 0 auto;
  padding: 15px 22px;
  border-top: 1px solid var(--hair);
  background: var(--sheet-bg);
}
.sheet-foot:empty { display: none; }
/* The row arrives with its call-site flex ratios intact (1:2 on the room
   form, 1:1 on a confirm), which is the proportion the sheet asked for and
   worth preserving — so only the gap is restated here. */
.sheet-foot > .row { gap: 8px; }

/* ==========================================================================
   C14 · Toast
   ========================================================================== */

.toast-wrap {
  position: fixed;
  left: 0; right: 0;
  bottom: calc(var(--nav-h) + 18px);
  z-index: 190;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  pointer-events: none;
  padding: 0 18px;
}

.toast {
  display: flex;
  align-items: center;
  gap: 11px;
  max-width: 420px;
  padding: 13px 19px;
  border: 1px solid var(--hair-2);
  border-radius: 99px;
  background: var(--toast-bg);
  backdrop-filter: blur(18px) saturate(150%);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  box-shadow: var(--shadow-lg), var(--sheen);
  color: var(--ink);
  font-size: 13px;
  letter-spacing: 0.04em;
  animation: toastIn var(--dur-2) var(--ease) both;
}
.toast.is-out { animation: toastOut var(--dur-2) var(--ease-io) both; }

.toast-leaf { border-color: rgba(63, 190, 139, 0.36); }
.toast-leaf .toast-ico { color: var(--emerald-glow); }
.toast-warn { border-color: rgba(200, 169, 107, 0.4); }
.toast-warn .toast-ico { color: var(--gold-lit); }
.toast-ico { display: inline-flex; flex: 0 0 auto; }
.toast-ico svg { width: 16px; height: 16px; stroke-width: 1.5; }

/* ==========================================================================
   C15 · Lightbox
   ========================================================================== */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 195;
  display: grid;
  place-items: center;
  gap: 16px;
  padding: 28px;
  background: rgba(3, 7, 5, 0.94);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  cursor: zoom-out;
  animation: fade var(--dur-2) var(--ease) both;
}
.lightbox img {
  max-width: 100%;
  max-height: 78vh;
  border-radius: var(--r-lg);
  border: 1px solid var(--hair-2);
  box-shadow: var(--shadow-lg);
}
.lightbox-cap {
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-3);
  text-align: center;
}

/* ==========================================================================
   C16 · Empty states
   ========================================================================== */

/* Generous, centred, and calm. An empty screen is a first impression, so it
   gets the display face and real breathing room. */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 64px 26px;
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background:
    radial-gradient(90% 70% at 50% 0%, rgba(63, 190, 139, 0.07), transparent 65%),
    var(--glass);
}

/* The only form: a hairline glyph inside a thin ring. An emoji is a piece
   of someone else's illustration dropped into the middle of the page — it
   carries its own colour, its own weight and a different rendering on every
   platform. A 1px ring with a stroked icon belongs to the system. */
.empty-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 62px;
  height: 62px;
  margin-bottom: 22px;
  border: 1px solid var(--hair-2);
  border-radius: 50%;
  background: radial-gradient(70% 70% at 50% 25%, var(--mint-wash), transparent 70%);
  color: var(--mint);
}
.empty-mark svg { width: 24px; height: 24px; stroke-width: 1.1; }

.empty h3 {
  font-family: var(--display);
  font-size: 20px;
  font-weight: 500;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 11px;
  /* Balanced, so a heading that does have to wrap splits into even lines
     rather than filling the first and dropping one word onto the second.
     The greenhouse heading was shortened to fit on a single line, but that
     fixes one string; this fixes the class of problem, and it costs nothing.
     Every heading here is short, centred and display-set, which is exactly
     the case `balance` exists for. Browsers without it ignore the line and
     wrap the way they always did. */
  text-wrap: balance;
}

.empty p {
  font-family: var(--serif);
  font-size: 16px;
  font-weight: 300;
  color: var(--ink-3);
  line-height: 1.6;
  max-width: 42ch;
  margin-bottom: 24px;
}
.empty p:last-child { margin-bottom: 0; }

/* Legacy hook: a bare .icon wrapper appears in a couple of places. */
.icon { display: inline-flex; flex: 0 0 auto; }


/* ==========================================================================
   PART D · motion, utilities, themes, responsive
   ========================================================================== */

/* ==========================================================================
   D1 · Keyframes
   --------------------------------------------------------------------------
   Parts B and C reference these by name. `fade` and `rise` keep the names
   the old stylesheet used, because ui.js sets animation shorthand inline in
   a couple of places and those strings are not worth chasing down.
   ========================================================================== */

@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

@keyframes slideUp {
  from { opacity: 0; transform: translateY(26px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

@keyframes toastIn {
  0%   { opacity: 0; transform: translateY(16px) scale(0.96); }
  60%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}

@keyframes toastOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(-8px) scale(0.97); }
}

/* Slow drift on the background glows. Barely perceptible, but it stops a
   full-bleed dark screen from reading as flat printed matter. */
@keyframes drift {
  0%, 100% { transform: none; }
  50%      { transform: translate3d(-1.4%, 1.4%, 0) scale(1.05); }
}
body::before { animation: drift 48s var(--ease-io) infinite; }

/* The "water me now" bead breathes. Only that one state — if every status
   dot pulsed, none of them would mean anything. */
@keyframes bead {
  0%, 100% { box-shadow: 0 0 0 3px rgba(200, 169, 107, 0.17); }
  50%      { box-shadow: 0 0 0 7px rgba(200, 169, 107, 0); }
}
.drop-now { animation: bead 2.8s var(--ease-io) infinite; }

@keyframes spin { to { transform: rotate(360deg); } }


/* ==========================================================================
   D2 · Staggered reveal
   --------------------------------------------------------------------------
   app.js adds .reveal to a container and every direct child rises in
   sequence. Capped at ten steps so a long list's tail is not left waiting
   most of a second; past that everyone shares the final delay.
   ========================================================================== */

.reveal > * { animation: rise var(--dur-2) var(--ease) both; }
.reveal > *:nth-child(1)    { animation-delay: 0ms; }
.reveal > *:nth-child(2)    { animation-delay: 38ms; }
.reveal > *:nth-child(3)    { animation-delay: 76ms; }
.reveal > *:nth-child(4)    { animation-delay: 114ms; }
.reveal > *:nth-child(5)    { animation-delay: 152ms; }
.reveal > *:nth-child(6)    { animation-delay: 190ms; }
.reveal > *:nth-child(7)    { animation-delay: 228ms; }
.reveal > *:nth-child(8)    { animation-delay: 266ms; }
.reveal > *:nth-child(9)    { animation-delay: 304ms; }
.reveal > *:nth-child(n+10) { animation-delay: 342ms; }

/* Opt-in per element, for nodes whose parent is not ours to annotate. */
.rise { animation: rise var(--dur-2) var(--ease) both; }
.fade-in { animation: fade var(--dur-2) var(--ease) both; }

.spinner {
  display: inline-block;
  width: 15px; height: 15px;
  border: 1.5px solid var(--hair-2);
  border-top-color: var(--emerald-glow);
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}


/* ==========================================================================
   D3 · Utilities
   ========================================================================== */

.mt-0 { margin-top: 0 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 16px; }
.mt-3 { margin-top: 26px; }
.mb-1 { margin-bottom: 8px; }
.mb-2 { margin-bottom: 16px; }
.mb-3 { margin-bottom: 26px; }

.tc { text-align: center; }
.tr { text-align: right; }
.grow { flex: 1 1 auto; min-width: 0; }
.italic { font-style: italic; }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.num { font-variant-numeric: tabular-nums; }

.muted  { color: var(--ink-3); }
.faint  { color: var(--ink-4); }
.accent { color: var(--mint); }
.gilt   { color: var(--gold-lit); }

.t-xs { font-size: 11px; }
.t-sm { font-size: 12.5px; }
.t-md { font-size: 14px; }
.t-lg { font-size: 16px; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* Keyboard focus only — a mouse click should not leave a ring behind. */
:focus { outline: none; }
:focus-visible {
  outline: 1px solid var(--mint);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}


/* ==========================================================================
   D4 · Conservatory — the daylight theme
   --------------------------------------------------------------------------
   A plant app gets opened at a sunny window as often as it does at night,
   so shipping only the dark theme would be a liability. This is a token
   remap and nothing more: warm ivory paper, ink-dark type, the same emerald
   and champagne pulled a few shades deeper so they hold their contrast on
   light ground. Every component rule above inherits it for free.
   ========================================================================== */

[data-theme="conservatory"] {
  --plan-grid:      rgba(28, 54, 43, 0.09);
  --plan-grid-2:    rgba(28, 54, 43, 0.18);
  --plan-floor-x:   #E6E1D6;
  --plan-floor-0:   #C9CFC6;
  --plan-floor-1:   #D2D9C4;
  --plan-floor-2:   #DDE1B9;
  --plan-floor-3:   #EBE2A8;
  --plan-floor-4:   #F4E1A0;
  --plan-wall-a:    #D8D2C4;
  --plan-wall-b:    #E7E1D3;
  --plan-wall-edge: rgba(19, 35, 27, 0.28);
  --plan-parapet:   rgba(216, 210, 196, 0.55);
  --plan-pane:      #FFFBEA;
  --plan-pane-dim:  #E4ECE6;
  --plan-pot:       #FFFCF7;
  --bg-0: #EDE6DA;
  --bg-1: #F7F3EC;
  --bg-2: #FFFCF7;
  --bg-3: #FBF6EE;
  --bg-4: #F1E9DC;

  --glass:   rgba(255, 252, 247, 0.70);
  --glass-2: rgba(255, 252, 247, 0.86);
  --glass-3: rgba(250, 245, 236, 0.94);

  --hair:   rgba(28, 54, 43, 0.13);
  --hair-2: rgba(28, 54, 43, 0.22);
  --hair-3: rgba(28, 54, 43, 0.34);

  --ink:   #13231B;
  --ink-2: #32443A;
  --ink-3: #5C6F66;
  --ink-4: #92A199;

  --emerald-deep: #0B2E21;
  --emerald:      #1B6045;
  --emerald-lit:  #237A57;
  --emerald-glow: #1B6045;
  --mint:         #2C6B51;
  --mint-2:       #1D5240;
  --mint-wash:    rgba(27, 96, 69, 0.10);

  --gold:      #9C7C35;
  --gold-lit:  #7E6226;
  --gold-wash: rgba(156, 124, 53, 0.13);

  --terra: #A0552C;
  --terra-soft: rgba(160, 85, 44, 0.12);

  /* Unchanged across themes. A colour that means "cannot be undone" should
     not soften because the lights came on, and it measures 9.87:1 against
     ivory as readily as it does under it. */
  --blood:      #6E2320;
  --blood-lit:  #8A2C28;
  --blood-deep: #571B19;
  --blood-ink:  #FBF4F1;
  --blood-edge: rgba(110, 35, 32, 0.9);

  /* Pill ink, darkened past the accent itself. The wash behind it is only a
     12% tint, so there is very little contrast to work with and the text has
     to carry all of it — at 11px, --sun's own #A9761B on a champagne tint is
     readable but tiring, and the darker step is the difference between
     glancing at a pill and squinting at one. Small type on a tinted ground
     wants more contrast than large type on the same ground, which is why
     these are not simply the accent tokens reused. */
  --terra-ink: #7C3E1B;

  /* Ivory on the filled pill, since conservatory's terracotta is the dark
     end of the clay rather than the light end. */
  --terra-hi-ink: #FFF6EE;

  /* A hair warmer than the dark theme's, to match the paper. This is the
     whole of what the deleted `[data-theme="conservatory"] .btn` rule was
     ever trying to say. */
  --btn-ink: #FFFCF7;

  /* Shadow has to be re-thought, not inherited. The dark theme's shadows are
     40–72% black because they sit on near-black ground and their job is to
     read as absence of light. Dropped onto ivory, the same values print as
     hard grey rules under every card — the one thing that made the daylight
     theme look cheap. Paper shadows are wide, soft, low-alpha and tinted
     with the ambient green, and the specular top edge goes from a 5%
     whisper to near-opaque white, because on paper that edge is the paper. */
  --shadow-sm: 0 1px 2px rgba(28, 54, 43, 0.10);
  --shadow:    0 10px 30px -10px rgba(28, 54, 43, 0.17);
  --shadow-lg: 0 28px 70px -20px rgba(28, 54, 43, 0.24);
  --sheen:     inset 0 1px 0 rgba(255, 255, 255, 0.85);

  /* A recess in paper is a shadow, not a hole; the raised key is the paper
     itself, caught by its own top edge and cast shadow. So the fill gets
     lighter than the dark theme's and the inner shadow does the work — the
     groove is warmer and shallower than a hole in charcoal would be, tinted
     with the same ambient green as the drop shadows above so it reads as
     paper in a green room rather than a grey rectangle. */
  --track:    rgba(28, 54, 43, 0.055);
  --track-in: inset 0 1px 2px rgba(28, 54, 43, 0.13),
              inset 0 -1px 0 rgba(255, 255, 255, 0.7);
  --seg-on:   #FFFCF7;

  /* Fields follow the track: a shallow warm tint, and the pressed top edge
     doing the work instead of the fill. A field has to read as writable, so
     it stays lighter than the segmented groove above — a well you could put
     ink in, not a hole. */
  --field:    rgba(28, 54, 43, 0.038);
  --field-on: rgba(28, 54, 43, 0.062);
  --field-in: inset 0 1px 2px rgba(28, 54, 43, 0.10);

  /* The sheet is paper lifted off paper, so it is the lightest surface in
     the theme and leans on --shadow-lg to separate itself from the page
     behind. The glow is the same ambient green as everywhere else, pulled
     right down: at the dark theme's 9% it read as a stain across the top of
     the sheet rather than light falling on it. */
  --sheet-bg:   #FFFCF7;
  --sheet-glow: rgba(27, 96, 69, 0.045);
  --toast-bg:   rgba(255, 252, 247, 0.94);

  /* Champagne is the dark end of the gold here, so the count on it flips to
     ivory. Left at the dark theme's near-black it was charcoal on bronze —
     a badge you could see but not read, which is worse than no badge. */
  --badge-ink: #FFF8E8;

  /* Photoless plant plates, inverted. Same hue band, same per-plant lift —
     only the luminance flips, so a plant keeps its identity through a theme
     switch instead of being reassigned a colour.

     The raking highlight stays a highlight. Its job in either theme is to
     say the light comes from the top left, and since the plate's own
     gradient already runs light-to-dark in that direction on paper, the
     sheen only has to agree with it — a breath of ivory rather than the
     mint glow, which on a 90%-light plate read as fluorescence. */
  /* The same six, inverted. A plant keeps its index through a theme switch,
     so it keeps its identity rather than being reassigned a colour. */
  --tint-0a: #D9E8DF;  --tint-0b: #C6DBCF;
  --tint-1a: #E0E8D0;  --tint-1b: #CEDABB;
  --tint-2a: #D6E4E0;  --tint-2b: #C2D5CF;
  --tint-3a: #E5E8CE;  --tint-3b: #D4DAB8;
  --tint-4a: #D2E4D8;  --tint-4b: #BDD6C7;
  --tint-5a: #EAE6D2;  --tint-5b: #DBD5BC;
  --tile-sheen: 255, 252, 247;
  --tile-sheen-a: 0.5;
  --tile-ink:   rgba(19, 35, 27, 0.72);

  /* Legacy aliases, re-pointed so the views' inline styles stay coherent. */
  --paper:      #F7F3EC;
  --paper-deep: #EDE6DA;
  --surface:    #FFFCF7;
  --surface-2:  #FBF6EE;
  --leaf:       #1B6045;
  --leaf-deep:  #0B2E21;
  --leaf-soft:  #2C6B51;
  --leaf-wash:  rgba(27, 96, 69, 0.10);
  --line:       rgba(28, 54, 43, 0.13);
  --line-2:     rgba(28, 54, 43, 0.22);

  color-scheme: light;
}

/* Daylight wants cast shadow where the dark theme wanted glow. */
[data-theme="conservatory"] body::before {
  background:
    radial-gradient(1100px 760px at 8% -8%,  rgba(27, 96, 69, 0.11), transparent 62%),
    radial-gradient(880px 640px at 104% 12%, rgba(156, 124, 53, 0.09), transparent 60%),
    radial-gradient(900px 900px at 46% 116%, rgba(27, 96, 69, 0.07), transparent 66%),
    var(--bg-1);
}

[data-theme="conservatory"] .topbar {
  background: linear-gradient(to bottom, var(--bg-1) 58%, rgba(247, 243, 236, 0));
}

[data-theme="conservatory"] .tabbar { background: rgba(255, 252, 247, 0.78); }

/* The two blanket box-shadow overrides that used to sit here are gone. They
   predate --shadow-sm / --shadow / --shadow-lg / --sheen becoming theme
   tokens, and once those were themed the rules restated what the cascade
   already did — while doing active harm three ways, all of them the same
   specificity trap as the button ink above.

   `[data-theme="conservatory"] .pcard` is (0,2,0) and so is `.pcard:hover`,
   and the theme rule sits a thousand lines later, so it won: in Conservatory
   a hovered card kept its resting shadow. It still shifted up 3px and its
   border still lit, so the interaction looked *almost* right, which is why
   it survived — a card that rises without deepening its shadow reads as
   sliding on the page rather than lifting off it, and that is a thing you
   feel rather than see.

   The hardcoded values also dropped --sheen, so Conservatory's cards had no
   top-edge highlight at all — precisely the "paper caught by its own top
   edge" the theme block above sets out to build. */

/* Grain over ivory reads as dirt — dial it back and invert the blend. */
[data-theme="conservatory"] .grain {
  opacity: 0.022;
  mix-blend-mode: multiply;
}

[data-theme="conservatory"] .splash { background: var(--bg-1); }

/* The three button-ink overrides that used to live here are gone; --btn-ink
   in the token block above does the job. They read as a rule plus two
   exceptions, but they were really one bug plus two partial repairs:
   `[data-theme="conservatory"] .btn` is (0,2,0) and every variant's own
   `color` is (0,1,0), so the blanket beat all four outline variants and each
   one had to be bought back by name. Two were — ghost and soft, both
   flattened to --ink, losing the --ink-2 and --mint-2 they had asked for —
   and the other two were not, which is how Delete came to be ivory type on
   ivory paper. A token keeps the base declaration at (0,1,0) where the
   variants can outrank it on source order, as intended. */


/* ==========================================================================
   D5 · Responsive
   --------------------------------------------------------------------------
   Mobile-first: everything above assumes one column plus the tabbar. At
   900px the sidebar arrives and the tabbar retires. Past 1500px the canvas
   stops growing so the app reads as a printed spread, not a stretched page.
   ========================================================================== */

@media (min-width: 900px) {

  /* The sidebar is fixed rather than a grid column so the main column can
     keep its own centred max-width without fighting the track sizing. */
  .sidebar {
    display: flex;
    flex-direction: column;
    gap: 34px;
    position: fixed;
    left: 0; top: 0; bottom: 0;
    z-index: 80;
    width: var(--side-w);
    padding: 34px 22px 26px;
    background: linear-gradient(to bottom, var(--bg-2), var(--bg-1));
    border-right: 1px solid var(--hair);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .tabbar { display: none; }

  /* --pad rather than a literal, because .topbar bleeds by exactly this
     value to reach the edges. A number typed here and not there is how the
     bar stops short of the gutter again one breakpoint later. */
  .main {
    --pad: 34px;
    margin-left: var(--side-w);
    max-width: none;
    padding: 0 var(--pad) 60px;
  }

  .topbar-back { display: none; }

  /* Content column keeps its measure, offset inside the remaining space. */
  .topbar-titles { min-width: 0; }
  .topbar { padding: 30px 0 18px; }
  .view { padding-top: 30px; }

  .grid-plants { grid-template-columns: repeat(auto-fill, minmax(218px, 1fr)); }
  .grid-rooms  { grid-template-columns: repeat(auto-fill, minmax(262px, 1fr)); }

  /* The plant hero becomes a spread: plate left, name and status beside it.
     Stacked, at this width, the 3:2 plate claimed the full ~1000px measure
     and printed a 660px-tall rectangle above the fold — for a photoless
     plant, a near-empty slab with one letter in the middle — pushing the
     name, the species and every status pill below the first screen. A
     photograph earns that much room. A placeholder does not, and the plant's
     own name earns it more.

     42% rather than half: the plate is decoration and the text is the
     subject, so the text takes the larger share. minmax(0, ...) on both
     tracks because .hero-nick clamps to 7vw and a long binomial in
     .hero-sci will otherwise blow the column out past its share. */
  .hero {
    display: grid;
    grid-template-columns: minmax(0, 32%) minmax(0, 1fr);
    align-items: center;
    gap: 40px;
    margin-bottom: 44px;
  }

  /* The 22px gap belonged to the stack, where it separated plate from name.
     Side by side there is nothing above the name to be separated from, and
     the margin only breaks the vertical centring. */
  .hero-nick { margin-top: 0; }
}

@media (min-width: 1240px) {
  :root { --side-w: 284px; }
  .main { --pad: 52px; padding: 0 var(--pad) 64px; }
}

/* Beyond this the line length gets uncomfortable, so cap and centre. */
@media (min-width: 1560px) {
  .topbar, .view {
    max-width: var(--maxw);
    margin-left: auto;
    margin-right: auto;
  }
  .d-xl { font-size: clamp(44px, 3.2vw, 62px); }
}

/* The portrait crop cannot also run the full measure on a phone. At 430px
   that is a 386x482 plate — taller than the fold, before the plant's own
   name has appeared — and on a species page, where there is never a
   photograph, it is 482px of empty tint. Narrowing it keeps the 4:5 crop and
   gives the plate back its proportion to the page. Desktop is already
   handled by the .hero grid, which gives the plate 32% of the spread. */
@media (max-width: 720px) {
  .hero-img { max-width: 78%; margin-inline: auto; }
}

/* Landscape phones: the splash and hero otherwise eat the whole fold. */
@media (max-height: 520px) {
  .splash-mark { font-size: 34px; }
  .hero-img { aspect-ratio: 16 / 9; }
}

/* Coarse pointers get no hover transforms — they fire on tap and stick. */
@media (hover: none) {
  .card:hover, .task:hover, .pcard:hover, .rcard:hover, .btn:hover { transform: none; }
  .pcard:hover .pcard-img { transform: none; }
}


/* ==========================================================================
   D6 · Print
   --------------------------------------------------------------------------
   Someone will want a care sheet on paper. Make that not embarrassing.
   ========================================================================== */

@media print {
  body::before, .grain, .sidebar, .tabbar, .topbar-actions, .topbar-back,
  .splash, .toast-wrap, .sheet-backdrop, .photo-add { display: none !important; }

  body { background: #fff; color: #000; }
  .main { margin: 0; padding: 0; max-width: none; }
  .topbar { position: static; background: none; backdrop-filter: none; }
  .card, .task, .pcard, .rcard {
    border: 1px solid #ccc;
    box-shadow: none;
    background: #fff;
    break-inside: avoid;
  }
  .view { padding: 0; animation: none; }
}


/* ==========================================================================
   C15 · The plan
   --------------------------------------------------------------------------
   A stage the reader pans and zooms, and a panel of steps beside or below
   it. The stage is a fixed-height card on a phone so the panel can scroll
   under it in the normal flow; past the sidebar breakpoint the two sit side
   by side and the stage sticks while the panel scrolls.
   ========================================================================== */

.plan { display: grid; gap: 18px; }
.plan-stage {
  position: relative;
  height: clamp(300px, 50vh, 620px);
  border: 1px solid var(--hair);
  border-radius: var(--r);
  background: var(--bg-2);
  overflow: hidden;
  touch-action: none;
}
#pl-canvas { width: 100%; height: 100%; display: block; }
.plan-notice, .plan-overlay {
  position: absolute; z-index: 5;
  padding: 9px 12px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r);
  background: var(--bg-2);
  color: var(--ink-2);
  font-size: 12.5px; line-height: 1.5;
  pointer-events: none;
}
.plan-notice { left: 12px; right: 12px; bottom: 12px; }
.plan-notice.is-gold { border-color: rgba(200, 169, 107, 0.4); }
.plan-overlay { right: 12px; bottom: 12px; max-width: 46%; color: var(--ink-3); font-size: 11.5px; }
.plan-notice:not([hidden]) + .plan-overlay { bottom: 64px; }
.plan-stage-btn {
  position: absolute; top: 12px; right: 12px; z-index: 5;
  padding: 8px 12px;
  border: 1px solid var(--hair-2);
  border-radius: var(--r);
  background: var(--bg-2);
  color: var(--ink-2);
  font-family: var(--sans); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
  cursor: pointer;
}
.plan-stage-btn.is-left { right: auto; left: 12px; }
.plan-stage-btn.is-on { background: var(--bg-4); color: var(--ink); border-color: var(--hair-3); }

/* Full screen without the Fullscreen API, which iOS Safari grants only to
   video. The stage lifts out of the layout and covers the viewport; its own
   touch-action already swallows gestures, so the page cannot scroll
   underneath and there is no lock to remember to release. */
/* A fixed element resolves against the nearest transformed ancestor, and
   .view animates translateY on the way in. Zeroing the transform is not
   enough: an element carrying a transform animation stays a containing
   block whatever the current value is, so the animation itself has to go
   while the stage is full. Written with :has() rather than a class, so
   there is nothing to set and nothing to leak on the way out — the entrance
   simply replays once as the page comes back. */
#view:has(.plan-stage.is-full) { animation: none !important; transform: none !important; }
.plan-stage.is-full {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 120;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  max-height: none;
  border: 0;
  border-radius: 0;
}
.plan-stage.is-full .plan-tools,
.plan-stage.is-full .plan-stage-btn { top: calc(12px + env(safe-area-inset-top, 0px)); }
.plan-stage.is-full .plan-stage-btn.is-bottom { top: auto; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
.plan-stage.is-full .plan-notice { bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
.plan-tool.is-on { color: var(--ink); border-color: var(--hair-3); background: var(--bg-4); }
.plan-stage-btn.is-bottom { top: auto; bottom: 12px; display: inline-flex; align-items: center; gap: 6px; color: var(--ink); border-color: var(--hair-3); }
.plan-stage-btn.is-bottom svg { width: 12px; height: 12px; }
/* The notice spans the bottom edge; anything else down there steps up over
   it, the same way the reshape overlay does. */
.plan-notice:not([hidden]) ~ .plan-stage-btn.is-bottom { bottom: 64px; }

/* The compass and the ruler live on the stage, not in the drawing. In the
   drawing they sat at the grid's far corner and left the frame the moment
   the view centred on the rooms. */
.plan-tools { position: absolute; top: 12px; right: 12px; z-index: 5; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.plan-tool {
  width: 44px; height: 44px; padding: 0;
  border: 1px solid var(--hair-2); border-radius: 50%;
  background: var(--bg-2); color: var(--ink-2);
  display: grid; place-items: center; cursor: pointer;
}
.plan-tool svg { width: 20px; height: 20px; }
.plan-tool.is-rose { border: 0; background: none; }
.plan-tool.is-rose svg { width: 44px; height: 44px; }

/* The room's name is its heading. Same face and tracking as the title
   around it; the dotted rule is the one cue that it takes typing. */
.plan-title-edit { display: flex; align-items: center; min-width: 0; flex: 1; }
.plan-title-input {
  flex: 1; min-width: 0; width: 100%;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
  background: none; border: 0; border-bottom: 1px dotted var(--hair-3); border-radius: 0;
  padding: 2px 0 4px; outline: none;
}
.plan-title-input::placeholder { color: var(--ink-4); }
.plan-title-input:focus { border-bottom-color: var(--emerald-glow); }

/* SVG type. Names in the serif at the room's centre, readings in tracked caps. */
.plan-name   { font-family: var(--serif); font-size: 13px; fill: var(--ink-2); pointer-events: none; }
.plan-tiny   { font-family: var(--sans); font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase; fill: var(--ink-4); pointer-events: none; }
.plan-letter { font-family: var(--serif); fill: var(--ink); pointer-events: none; }
.plan-badge  { font-family: var(--sans); font-size: 9px; pointer-events: none; }
.plan-n      { font-family: var(--sans); font-size: 9px; letter-spacing: 0.1em; fill: var(--ink-3); }

/* The panel: the app's sections, plus a step eyebrow and a few small parts
   no other view has needed. */
.plan-sec { margin-bottom: 26px; }
.plan-step { display: inline-block; font-family: var(--sans); font-size: 9.5px; letter-spacing: 0.2em; color: var(--gold-lit); margin-right: 8px; vertical-align: 1px; }
.plan-toggle { cursor: pointer; user-select: none; -webkit-user-select: none; }
.plan-chev { display: inline-block; width: 8px; height: 8px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: rotate(-45deg); transition: transform var(--dur-2) var(--ease); margin-right: 2px; }
.plan-chev.is-open { transform: rotate(45deg); }
.plan-check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); cursor: pointer; }
.plan-check input { width: 17px; height: 17px; accent-color: var(--emerald-glow); margin: 0; flex: 0 0 auto; }

/* ---- The walkthrough invitation ----
   Laid over the empty grid rather than inside a dialog, and transparent to
   the pointer except at its two buttons, so a reader who would rather just
   drag a box out is never fighting it. */
.plan-invite {
  position: absolute; inset: 0; z-index: 4;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 24px; text-align: center;
  pointer-events: none;
}
.plan-invite button { pointer-events: auto; }
.plan-invite-t {
  font-family: var(--display); font-weight: 500;
  font-size: 15px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--ink-2);
}
.plan-invite .hint { max-width: 34ch; margin: 7px 0 15px; }

/* ---- The walkthrough itself ---- */
.tour {
  position: fixed; inset: 0; z-index: 190;
  display: grid; place-items: center;
  padding: 16px;
  padding-top: calc(16px + env(safe-area-inset-top, 0px));
  padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  background:
    radial-gradient(760px 520px at 50% 28%, rgba(47, 154, 112, 0.17), transparent 70%),
    var(--bg-0);
  animation: tourIn 380ms var(--ease) both;
}
.tour.is-out { opacity: 0; transition: opacity 300ms var(--ease); pointer-events: none; }
@keyframes tourIn { from { opacity: 0; } to { opacity: 1; } }

.tour-plate {
  width: 100%; max-width: 560px; max-height: 100%;
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
}
.tour-stage {
  position: relative; flex: 0 0 auto;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--hair); border-radius: var(--r);
  background: var(--bg-1); overflow: hidden;
}
@media (min-width: 620px) { .tour-stage { aspect-ratio: 16 / 11; } }
.tour-stage > svg { display: block; width: 100%; height: 100%; }
.tour-stage .plan-tool { cursor: default; }
.plan-tool.is-lit { color: var(--mint); border-color: var(--emerald-glow); box-shadow: 0 0 0 4px var(--mint-wash); }
.plan-tool.is-rose.is-lit { box-shadow: none; }

/* The panel the planner would slide up, shown only while the beat that
   uses one is running. */
.tour-mock {
  position: absolute; left: 12px; right: 12px; bottom: 12px; z-index: 6;
  padding: 13px 15px;
  border: 1px solid var(--hair-2); border-radius: var(--r);
  background: var(--bg-2); box-shadow: var(--shadow);
  opacity: 0; transform: translateY(10px); pointer-events: none;
  transition: opacity 280ms var(--ease), transform 280ms var(--ease);
}
.tour-mock.is-up { opacity: 1; transform: none; }
.tour-mock-label {
  font-family: var(--sans); font-size: 9.5px; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--ink-4); margin-bottom: 7px;
}
.tour-mock-title {
  font-family: var(--display); font-weight: 500;
  font-size: 17px; letter-spacing: 0.14em; text-transform: uppercase;
  border-bottom: 1px dotted var(--hair-3); padding-bottom: 4px;
  display: inline-block; min-width: 150px;
}
.tour-mock-title i { color: var(--emerald-glow); font-style: normal; }
.tour-field { border: 1px solid var(--hair-2); border-radius: 10px; padding: 6px 10px; background: var(--bg-3); }
.tour-field span { display: block; font-family: var(--sans); font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--ink-4); }
.tour-field b { font-family: var(--serif); font-weight: 400; font-size: 18px; }
.tour-est { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; font-family: var(--sans); font-size: 11.5px; color: var(--ink-3); }
.tour-est b { font-family: var(--serif); font-weight: 400; font-size: 14px; color: var(--ink-2); }
.tour-wall { border: 1px solid var(--hair-2); border-radius: 99px; padding: 5px 11px; font-family: var(--sans); font-size: 11px; color: var(--ink-2); }
.tour-wall.is-win { border-color: var(--gold); color: var(--gold); background: rgba(200, 169, 107, 0.1); }
.tour-wall.is-open { border-style: dashed; border-color: var(--emerald-glow); color: var(--mint); }

.tour-say { border: 1px solid var(--hair); border-radius: var(--r); background: var(--bg-2); padding: 16px 18px 14px; }
.tour-head { display: flex; align-items: baseline; gap: 11px; flex-wrap: wrap; margin-bottom: 7px; }
.tour-num { font-family: var(--sans); font-size: 9.5px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--gold); }
.tour-say h2 {
  font-family: var(--display); font-weight: 500;
  font-size: 19px; letter-spacing: 0.14em; text-transform: uppercase;
  margin: 0; line-height: 1.2;
}
/* A fixed floor under the narration, so the card does not jump a line
   taller and shorter as the beats change under it. */
.tour-say p { margin: 0; color: var(--ink-2); font-size: 16px; line-height: 1.55; min-height: 4.7em; }
.tour-meter { height: 2px; background: var(--hair); border-radius: 2px; overflow: hidden; margin: 12px 0; }
.tour-meter i { display: block; height: 100%; width: 0; background: var(--emerald-glow); }
.tour-bar { gap: 8px; flex-wrap: wrap; }

@media (prefers-reduced-motion: reduce) {
  .tour { animation: none; }
  .tour-mock { transition: none; }
}
.plan-rule { border: 0; border-top: 1px solid var(--hair); margin: 14px 0; }
.plan-back { width: 30px; height: 30px; margin-right: 10px; border: 1px solid var(--hair-2); border-radius: var(--r); background: none; color: var(--ink-2); display: inline-grid; place-items: center; cursor: pointer; vertical-align: middle; }
.plan-help { width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--hair-3); background: none; color: var(--ink-3); font-size: 11px; line-height: 1; display: inline-grid; place-items: center; cursor: pointer; padding: 0; letter-spacing: 0; }
.plan-help:hover { color: var(--ink); border-color: var(--emerald-glow); }
.plan-reading { font-family: var(--serif); font-size: 22px; font-weight: 300; line-height: 1.2; }
.plan-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; vertical-align: -1px; }
.plan-mark { flex: 0 0 auto; width: 26px; height: 6px; border-radius: 3px; margin-top: 8px; }
.plan-dial { width: 120px; height: 120px; flex: 0 0 auto; touch-action: none; cursor: grab; }
.plan-mini-rose { width: 40px; height: 40px; flex: 0 0 auto; cursor: pointer; overflow: visible; }

.plan-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; padding: 9px 11px;
  border: 1px solid var(--hair); border-radius: var(--r);
  background: var(--bg-3); color: var(--ink);
  text-align: left; cursor: pointer;
}
.plan-row-nm { display: inline-flex; align-items: center; gap: 9px; font-family: var(--serif); font-size: 15.5px; min-width: 0; }
.plan-row-nm small { font-family: var(--sans); font-size: 10.5px; letter-spacing: 0.08em; color: var(--ink-4); }
.plan-row.is-undrawn { border-style: dashed; background: none; }

.plan-wall { padding: 9px 10px; justify-content: space-between; border-radius: var(--r); text-align: left; }
.plan-wall small { color: var(--ink-4); letter-spacing: 0.08em; text-transform: uppercase; font-size: 9.5px; text-align: right; }
.plan-wall.is-open { border-color: rgba(63, 190, 139, 0.5); color: var(--ink); }

.plan-tray { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; scrollbar-width: none; }
.plan-tray::-webkit-scrollbar { display: none; }
.plan-sp {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 12px 6px 6px;
  border: 1px solid var(--hair-2); border-radius: 99px; background: var(--glass);
  cursor: grab; user-select: none; -webkit-user-select: none; touch-action: none;
}
.plan-sp:active { cursor: grabbing; }
.plan-sp-mono { width: 26px; height: 26px; border-radius: 50%; background: var(--bg-3); border: 1px solid var(--hair-2); display: grid; place-items: center; font-family: var(--serif); font-size: 14px; }
.plan-sp-nm { display: flex; flex-direction: column; font-size: 12.5px; color: var(--ink-2); white-space: nowrap; line-height: 1.2; }
.plan-sp-nm small { font-size: 10px; letter-spacing: 0.08em; color: var(--ink-4); }
.plan-ghost {
  position: fixed; z-index: 200; pointer-events: none;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--plan-pot); border: 1.5px solid var(--emerald-glow);
  display: grid; place-items: center; font-family: var(--serif); font-size: 15px; color: var(--ink);
  transform: translate(-50%, -50%);
  box-shadow: 0 6px 18px -6px rgba(0, 0, 0, 0.5);
}

@media (min-width: 900px) {
  .plan { grid-template-columns: minmax(0, 1fr) 340px; align-items: start; }
  .plan-stage { height: calc(100vh - 230px); }
  .plan-main { position: sticky; top: 104px; }
}

/* ==========================================================================
   D7 · Reduced motion
   --------------------------------------------------------------------------
   Honour the preference, but do not strip transitions to zero — instant
   state changes read as broken. Collapse them to a blink instead, and kill
   outright only the looping ambient animations and anything that moves.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 70ms !important;
    scroll-behavior: auto !important;
  }
  body::before, .drop-now { animation: none !important; }

  /* Entry animations that *move* the element are switched off rather than
     merely shortened. 1ms is the right treatment for a fade — it still
     resolves to the final state and costs nothing — but slideUp, toastIn and
     platePop each begin at a translate plus a scale, and a reader who has
     asked for reduced motion has asked for precisely that not to happen. A
     1ms slide is still a slide; it is just one they cannot follow.

     It also repairs the harness, which is how the defect surfaced. Headless
     Chrome does not reliably advance the animation clock under
     --virtual-time-budget, so a duration of 1ms never actually elapses and an
     `animation-fill-mode: both` element is captured holding its *first*
     keyframe. The sheet was measured 21px below the viewport with its action
     row apparently cut off — which is exactly translateY(26px) offset by
     scale(0.985) shrinking a 672px box, and nothing whatsoever to do with
     layout. Two hours of "the sheet does not fit on a phone" lives down that
     road. Suppressing the transform entirely means the frozen first keyframe
     and the settled last one are the same place, so the instrument reads
     true. */
  .sheet, .toast, .sheet-backdrop, .lightbox { animation: none !important; }
  /* Same reasoning as the sheet above, and the same harness trap: these
     three carry fill-mode both and start at a translate, a clip and a scale
     respectively, so a shortened duration would leave a reader — or a
     screenshot — looking at a blurred, half-wiped, collapsed splash. */
  .splash-mark span, .splash-script, .splash-rule, .splash-rule i,
  .slider-value span, .tl-item.is-target .tl-card, .tl-item.is-target .tl-metric {
    animation: none !important;
  }
  .slider-shell { transition: none !important; }
  .grain { display: none; }
  .reveal > * { animation-delay: 0ms !important; }
  .card:hover, .task:hover, .pcard:hover, .rcard:hover,
  .btn:hover, .pcard:hover .pcard-img { transform: none !important; }
}


/* ==========================================================================
   D8 · Overlay exit motion + desktop overlay geometry
   --------------------------------------------------------------------------
   A sheet that vanishes on the same frame the button is released feels
   cheap; the eye needs to see it leave. ui.js adds .is-closing, waits out
   the animation, then hides. On desktop the bottom sheet becomes a centred
   plate, because a drawer pinned to the foot of a 1400px window is a phone
   idiom stranded on a monitor.
   ========================================================================== */

@keyframes fadeOut { to { opacity: 0; } }

@keyframes slideDown {
  to { opacity: 0; transform: translateY(22px) scale(0.99); }
}

@keyframes platePop {
  from { opacity: 0; transform: translateY(14px) scale(0.975); }
  to   { opacity: 1; transform: none; }
}

.sheet-backdrop.is-closing { animation: fadeOut var(--dur-1) var(--ease-io) both; }
.sheet-backdrop.is-closing .sheet { animation: slideDown var(--dur-1) var(--ease-io) both; }

.lightbox.is-closing { animation: fadeOut var(--dur-1) var(--ease-io) both; }

@media (min-width: 900px) {
  .sheet-backdrop { align-items: center; padding: 30px; }
  .sheet {
    max-width: 540px;
    max-height: 82vh;
    border: 1px solid var(--hair-2);
    border-radius: var(--r-lg);
    animation: platePop var(--dur-2) var(--ease) both;
  }
  .sheet-grip { display: none; }
  .sheet-head { padding-top: 22px; }
  .sheet-backdrop.is-closing .sheet { animation: slideDown var(--dur-1) var(--ease-io) both; }
  .toast-wrap { bottom: 26px; }
}
