/* =========================================================
   SHINKAI — cinematic sky weather dashboard
   Hero keeps the original 4-stop sunset gradient; everything
   below adopts the new dark editorial system.
========================================================= */

:root {
  /* ---- Safe-area insets, behind a token so they are TESTABLE ----
     `env(safe-area-inset-*)` is 0 in every desktop browser and cannot be
     overridden from CSS, so any layout rule keyed off it is invisible to a
     WebKit screenshot and only shows up on a real notch / Dynamic Island
     phone (that is exactly how the hero-compression and trends-gap bugs
     shipped). Routing every consumer through --sat / --sab keeps the real
     device behavior identical while letting a test stylesheet inject
     `--sat: 59px` and reproduce a Dynamic Island phone in a normal browser.
     Consumers MUST use var(--sat) / var(--sab), never env() directly. */
  --sat: env(safe-area-inset-top, 0px);
  --sab: env(safe-area-inset-bottom, 0px);

  /* ---- Tenki no Ko sunset gradient — brighter, more saturated.
     Goes from deep cobalt at top through cerulean to brilliant coral
     to cream-yellow at the horizon. Compared to the original muted
     plum/brown gradient, this matches the film's actual luminosity. */
  --sky-1: #1a3a5e;   /* deep cobalt (Tenki night-top) */
  --sky-2: #5fb8e8;   /* cerulean (Tenki day-mid) */
  --sky-3: #ff8c5f;   /* brilliant coral (Tenki sunset) */
  --sky-4: #fff5c8;   /* cream-yellow (Tenki horizon glow) */

  /* ---- Bright cream surfaces (was deep navy/plum — too dull).
     Cards now feel like Tenki's animated paper / Tokyo daytime tones. */
  /* Surfaces aligned to the tinytoolsstack paper tokens (tts.css --bg/--card),
     so the app, the about page and the hub read as literally the same paper.
     The shift from the old values is small (#f4f0e6 -> #f7f4ec) but it is what
     stops the seam showing when a user goes app -> about in one sitting. */
  --night-1: #f7f4ec;            /* page bg — tts warm shelf paper */
  --night-2: #ede7d6;            /* slightly darker cream */
  --card: #fffdf8;               /* card surface — tts card */
  --card-2: #ede5d0;             /* card gradient end — warm cream */
  --card-line: rgba(60, 40, 20, 0.10);
  --card-line-strong: rgba(60, 40, 20, 0.22);

  /* ---- Text on the bright cards — warm dark ink ---- */
  --cream: #1f1a14;
  --cream-muted: rgba(31, 26, 20, 0.72);
  --cream-subtle: rgba(31, 26, 20, 0.45);

  /* ---- Brighter, more saturated accents (Tenki palette) ---- */
  /* ---- THE BRAND ACCENT, and the app's real one.
     These three were the app's de-facto accent: 29 references carrying every
     emphasis point in the UI (focus rings, the active city, pins, buttons,
     the today label, link hover). Declaring an --accent token beside them
     changed nothing visible, because they were still doing the work. So the
     takeover happens HERE, in the family they already form.

     Two variants, because this app paints on two very different grounds:
       --gold      #c8790a  the brand amber. For fills, and for text on the
                            cream cards. As text on paper it is 3.1:1, so it
                            is a FILL colour on light surfaces, never a label.
       --gold-soft #f0b445  the design system's own dark-surface lift. For
                            anything sitting on the sky hero or a dark panel,
                            where the darker amber would disappear.
     Both values come from tts.css (--p-shinkai light / dark), so the app and
     the about page are literally the same amber rather than two near-misses. */
  /* Semantic tiers that were previously borrowing the brand hue. Naming them
     is what stops the next emphasis colour drifting back into the palette. */
  --danger: #c0392b;             /* destructive actions only */
  --wx-storm: #e0603c;           /* storm condition icon -- weather, not brand */
  /* THE FILL. Ann picked this brighter yellow off the dark-theme CTA, and it is
     the better fill on both grounds: ink on it is 9.6:1 where ink on #c8790a was
     5.3:1, and it needs no separate dark-theme value because it also clears
     9.0:1 on the dark card. It matches the product icon's own sun, which is
     where a subject-grounded accent should come from.
     It CANNOT be text on a light ground (1.7:1) -- but neither could #c8790a
     (3.1:1), so --gold-deep already existed for that. The structure is unchanged;
     only the fill moved. */
  --gold: #f0b445;
  --gold-deep: #a05f07;          /* the same amber, dark enough to BE text on a light tint */
  --gold-on-sky: #ffe08a;        /* the variant for text sitting ON the sky imagery,
                                    where the mid-grey cloud band is the worst case */
  --gold-soft: #f0b445;
  --gold-glow: rgba(240, 180, 69, 0.55);
  --coral: #ff6f45;              /* brilliant Tenki coral */
  --coral-soft: #ffa07e;
  --rose: #ff7ba8;               /* Hina-hair magenta-pink */
  --blue: #4aa8e8;               /* cerulean */
  --blue-soft: #87cfeb;          /* aqua */
  --plum: #9d7eff;               /* brighter periwinkle */
  --plum-soft: #c8b8ff;
  --teal: #4aa89a;

  /* ---- Skyline silhouette — kept DARK so the city edge at the bottom
     of the hero stays as a dramatic graphic moment (Tenki signature).
     If this were cream like the cards the silhouette would vanish. */
  --skyline-fill: #2a1840;

  /* ---- sunset overlay text — stays cream because hero still has dark
     bands at the top regardless of card brightening ---- */
  --sunset-ink: #fff8e8;
  --sunset-ink-muted: rgba(255, 248, 232, 0.78);
  --sunset-ink-subtle: rgba(255, 248, 232, 0.55);

  /* ---- typography ---- */
  /* ---- typography — tinytoolsstack family faces.
     Fraunces + Hanken Grotesk are the system's faces, shared with the hub,
     the about page and every sibling tool. This replaced DM Serif Display +
     DM Sans, which are on the named-cliché blocklist (the warm-editorial
     AI default) and said nothing about this product specifically.
     Self-hosted in static/fonts/tts/ — see static/tts.css. */
  --serif: "Fraunces", Georgia, serif;
  --sans: "Hanken Grotesk", -apple-system, system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;

  /* ---- BRAND ACCENT — amber, assigned to Shinkai forever.
     One accent across every Shinkai surface (app, about, privacy, popup,
     store assets) is what makes them read as one product. No other
     tinytoolsstack tool may use this hue.
     This is the BRAND layer. It deliberately does NOT govern the weather
     data-series colors below (temp / rain / wind): those are SEMANTIC —
     rain has to read blue for the chart to be legible at all. */
  --accent: #c8790a;
  --accent-lift: #f0b445;        /* the dark-surface variant, per tts */
  --accent-ink: #fffdf8;         /* text ON an accent fill */
  --accent-soft: rgba(200, 121, 10, 0.11);
  --accent-edge: rgba(200, 121, 10, 0.28);
  --accent-chip-ink: #8c5206;    /* 6.2:1 on the chip's own tint */
  --card-edge: rgba(27, 23, 16, 0.16);   /* the tts ink line, at card weight */

  /* =========================================================
     WEATHER DATA SCHEMA (single source of truth)
     =========================================================
     One CSS var per weather quantity. Both page-1 chart + page-2 trend
     charts read from these (JS reads via wxColor() helper at draw
     time). Theme overrides below this :root block can re-define each
     var so a theme switch updates all charts uniformly. */
  --wx-temp:         #d97a3a;                       /* temperature line  */
  --wx-temp-fill:    rgba(217, 122, 58, 0.15);
  --wx-wind:         #5a8fb8;                       /* wind line — warm steel-blue */
  --wx-wind-fill:    rgba(122, 168, 210, 0.22);
  --wx-gust:         #5a8fb8;                       /* gust line same hue, dashed */
  --wx-rain-line:    #2e6da4;                       /* rain prob → blue (Ann: rain should read as blue); deeper/more saturated than wind steel so the two stay distinct */
  --wx-rain-fill:    rgba(46, 109, 164, 0.30);
  --wx-rain-strip:   rgba(46, 109, 164, 0.52);
  --wx-rain-rgb:     46, 109, 164;                  /* raw RGB channels for dynamic alpha */
  --wx-sun:          #f6b94a;                       /* clear / sun */
  --wx-cloud:        #9094a4;                       /* cloud */
  --wx-sunrise:      #f6b94a;                       /* sunrise marker = warm gold */
  --wx-sunset:       #b9a8ff;                       /* sunset marker = lavender (moon) */
  --wx-humidity:     #4a82c8;                       /* humidity line stays blue */
  --wx-humidity-fill:rgba(74, 130, 200, 0.32);
  --wx-uv:           #8a5c9e;                       /* UV index — muted violet (distinct from sunset lavender) */
  --wx-uv-fill:      rgba(138, 92, 158, 0.30);
  --wx-now:          #ff8c5f;                       /* NOW vertical line + pill */
  --wx-now-glow:     rgba(255, 140, 95, 0.45);

  /* Sky-character palette (sky97) — single source of truth for the
     per-weather hero artwork. Theme overrides recolor these (painterly
     warms them, minimal hides .sky-character entirely). */
  --sc-cloud-light:  #c8c4d0;
  --sc-cloud-mid:    #9091a8;
  --sc-cloud-dark:   #4a4860;
  --sc-cloud-storm:  #2a2840;
  --sc-snow-edge:    #f5f0e8;
  --sc-fog-band:     #d8d4cc;
  --sc-rain-veil:    rgba(74, 80, 110, 0.55);
}

* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  color: var(--cream);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
html { background: var(--night-1); }
body {
  /* mobile-first: pager owns the entire viewport, no body-level grid */
  height: 100vh;
  height: 100dvh;
  margin: 0;
  overflow: hidden;
  background:
    linear-gradient(180deg,
      var(--sky-4) 0%,
      var(--sky-4) 320px,
      var(--night-1) 720px,
      var(--night-1) 100%);
}

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

/* =========================================================
   HERO — preserved 4-stop sunset gradient + layered silhouettes
========================================================= */
.hero {
  position: relative;
  height: auto;
  /* Trimmed bottom padding from 50px → 0 (Ann: "下方好像缺了内容").
     With .skyline silhouette removed earlier, the 50px bottom padding
     exposed body bg below the purple mountain band, looking like
     missing content. Now mountain silhouettes touch the hero bottom. */
  /* The min/max cap the hero's height BELOW the notch, not including it.
     Adding --sat lets the hero GROW by the safe-area amount instead of
     absorbing it: the mobile override adds `padding-top: 24px + --sat` and
     .hero-inner adds another 70px, so on a Dynamic Island phone (--sat ~59px)
     a fixed 360px cap pushed the temp + headline ~59px lower while the dark
     mountain band (.cloud-layer.near, 130px pinned to the hero bottom) stayed
     put — the temp landed inside the dark band. Growing by --sat keeps the
     content's offset from the *usable* top exactly as designed, and the extra
     height is sky filling the notch region. --sat is 0 everywhere else, so
     desktop and non-notch phones are unchanged. */
  /* 280 -> 360 (2026-07-25, Ann): the hero renders at its MIN height, not its
     max, so raising the cap alone would have been a no-op -- measured before
     changing anything. The mountain band (.cloud-layer.near) is pinned to the
     hero's bottom, so every px of extra hero height is sky inserted ABOVE it:
     the big temperature keeps its position under the topbar while the purple
     silhouette moves down, and the number stops sitting inside the band.
     Measured at --sat 59px: the temp block overlapped the band by 95px before,
     15px after. Lifting the temp instead was tried and rejected -- it just
     traded the purple band for the grey cloud layer and crowded the moon.
     Desktop is unaffected: its own .hero rule inside @media (min-width:1024px)
     restates min/max and, being later in the file, wins. */
  min-height: calc(360px + var(--sat));
  max-height: calc(440px + var(--sat));
  padding: 24px 0 0;
  overflow: hidden;
  isolation: isolate;
}

/* sky gradient — first layer, full hero */
.sky {
  position: absolute; inset: 0;
  background: linear-gradient(
    180deg,
    var(--sky-1) 0%,
    var(--sky-2) 36%,
    var(--sky-3) 74%,
    var(--sky-4) 100%
  );
  z-index: 0;
}

/* sun — clear yellow disc with multi-radial gradient + glow */
.sun {
  position: absolute;
  top: 30%;
  right: 16%;
  width: 78px;
  height: 78px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff8e7 0%, #ffcc80 45%, rgba(255,180,120,0) 70%);
  z-index: 1;
  box-shadow:
    0 0 60px rgba(255, 210, 160, 0.5),
    0 0 120px rgba(255, 180, 120, 0.32);
  animation: sun-pulse 6s ease-in-out infinite;
}
@keyframes sun-pulse {
  0%, 100% { transform: scale(1); opacity: 0.95; }
  50%      { transform: scale(1.05); opacity: 1; }
}

/* lens flare — soft warm halo behind the sun */
.flare {
  position: absolute;
  top: 30%;
  right: 16%;
  width: 220px;
  height: 220px;
  margin-top: -71px;
  margin-right: -71px;
  background: radial-gradient(circle, rgba(255, 230, 180, 0.46) 0%, rgba(255, 200, 150, 0.14) 38%, transparent 72%);
  border-radius: 50%;
  z-index: 1;
  pointer-events: none;
  mix-blend-mode: screen;
  animation: flare-rotate 12s ease-in-out infinite;
}
@keyframes flare-rotate {
  0%, 100% { transform: rotate(0deg) scale(1); opacity: 0.55; }
  50%      { transform: rotate(180deg) scale(1.15); opacity: 0.85; }
}

/* layered hill silhouettes — far / mid / near
   each layer has fixed height so SVG doesn't aspect-stretch on wide monitors */
.cloud-layer {
  position: absolute;
  left: -10%;
  width: 220%;
  z-index: 2;
  pointer-events: none;
}
.cloud-layer svg { width: 100%; height: 100%; display: block; }
/* The three bands are spaced from the hero's BOTTOM in px, not from its top in
   percent (Ann, 2026-07-25: purple and orange should sit close, with the white
   band well clear above them).

   Percent-from-top was the obvious edit and would have been wrong. The spacing
   she chose is a pixel relationship -- white 44px above orange, orange 21px
   above purple -- while a percentage is relative to a hero that is 419px on a
   notch phone, 360px in the extension popup and 281px on desktop. The mobile
   percentages (far 53.5%, mid 64%) put mid BELOW near on desktop: the orange
   band would have rendered underneath the purple one. Measured, not guessed.

   Anchored from the bottom instead, the arithmetic is height-independent,
   because .near is itself bottom-anchored:
     near top = H - 130           (bottom 0,   height 130)
     mid  top = H - 41 - 110      (bottom 41,  height 110)  -> 21 above near
     far  top = H - 105 - 90      (bottom 105, height 90)   -> 44 above mid
   so every surface gets the same rhythm instead of three accidental ones.

   `top: auto` is load-bearing: the base .cloud-layer rule has no top, but these
   rules previously set one, and top + height wins over bottom on an absolutely
   positioned box -- leaving it would silently ignore the new bottom offset. */
.cloud-layer.far {
  top: auto;
  bottom: 105px;
  height: 90px;
  opacity: 0.55;
  animation: drift-slow 120s linear infinite;
}
.cloud-layer.far svg path { fill: rgba(244, 235, 212, 0.75); }
.cloud-layer.mid {
  top: auto;
  bottom: 41px;
  height: 110px;
  opacity: 0.85;
  /* 70s -> 42s. The duration was NOT the reason this layer read as the same
     speed as the white one; the DISTANCE was. Perceived speed is distance over
     time, and drift-mid travels only 30% (-10% to -40%) where drift-slow and
     drift-fast travel 50%. So 70s against white's 120s looked like 1.7x faster
     and actually was 0.43 %/s against 0.42 %/s -- a 3% difference, invisible.
     Ann saw it before the numbers explained it: "白色和橙色一样".

     Fixing the duration rather than the distance is deliberate, and the reason
     is a hard constraint, not a preference. The layer is a single 1600-unit SVG
     stretched across 220% of the hero, not a tiling pattern, and it sits at
     left:-10%. Travelling -50% would move it -110% of the viewport, leaving it
     spanning -120%..100% -- the far and near layers do exactly this and only
     just stay covered. Mid starting at -10% would end at -60%, spanning
     -142%..78%, uncovering the right fifth of the screen. So 30% is mid's
     ceiling and speed is tuned through time alone.

     70s -> 42s -> 58s -> 65s. Every intermediate value came from reasoning
     about ratios and every one of them was wrong in the same direction: too
     fast. 42s was the geometric midpoint between white and purple and Ann read
     it as purple-speed twice. The final numbers are hers, given as ratios to
     white after seeing the whole sky in motion on the device -- which is the
     only place this is judgeable, since a still frame carries no speed.

     Current speeds -- change ONLY the durations, these ratios follow:
       white   50% / 120s = 0.417 %/s   (1.00x, the reference)
       orange  30% /  65s = 0.462 %/s   (1.11x white)
       purple  50% / 100s = 0.500 %/s   (1.20x white)

     Note what this deliberately gives up: at 1.0 / 1.1 / 1.2 the three bands
     move almost together, so the parallax depth cue is nearly gone. That is the
     point -- Ann: "看着头晕" -- but it means the old failure mode (a layer
     reading as the same speed as its neighbour) is now the INTENDED state for
     all three, and must not be "fixed" by a future session reading these
     numbers as suspiciously close. */
  animation: drift-mid 65s linear infinite;
}
.cloud-layer.mid svg path { fill: rgba(231, 150, 100, 0.9); }
.cloud-layer.near {
  /* Pinned to hero bottom edge (was 60px above) so the dark purple
     silhouette becomes the bottom of the hero rectangle. Ann: 把紫色
     挪到方形的最下方更好. */
  bottom: 0;
  height: 130px;
  opacity: 1;
  /* 40s -> 100s (Ann, 2026-07-25: "有点太快了，看着头晕"). This layer is the
     largest and closest, so its motion dominates the whole hero; at 1.25 %/s it
     was 3x the white band and read as the thing making the sky restless.

     100s over a 50% travel = 0.500 %/s = 1.20x white, the ratio she asked for.
     The keyframe name still says "fast" -- it describes the layer's role in the
     parallax stack (nearest, therefore fastest of the three), not an absolute
     speed, and renaming it would touch the far/mid pair for no gain. Speed table
     lives on .cloud-layer.mid above. */
  animation: drift-fast 100s linear infinite;
}
.cloud-layer.near svg path { fill: rgba(42, 24, 64, 0.92); }

@keyframes drift-slow { from { transform: translateX(0); }   to { transform: translateX(-50%); } }
@keyframes drift-mid  { from { transform: translateX(-10%); } to { transform: translateX(-40%); } }
@keyframes drift-fast { from { transform: translateX(0); }   to { transform: translateX(-50%); } }

/* skyline — sharp city silhouette pinned to hero bottom, blends into night */
.skyline {
  position: absolute;
  bottom: -1px; left: 0; right: 0;
  width: 100%; height: 90px;
  display: block;
  z-index: 3;
}
.skyline path { fill: var(--skyline-fill); }

/* topbar — flex row with city picker (left) + unit toggle (right).
   NO positioning here. The topbar used to be absolutely positioned INSIDE the
   hero (`position:absolute; top:var(--sat); left:36px; right:36px`), but it now
   lives ABOVE the hero in normal flow (.topbar-above-hero, which clears the
   notch with its own padding-top). Those four stale declarations kept applying
   to the new markup and had to be fought off one at a time with !important:
   `position` and `left`/`right` were caught earlier, `top` was not — so on a
   Dynamic Island phone the bar got --sat of padding AND a further --sat of
   relative offset, landing the city pill a third of the way down the hero.
   Deleting the stale rules removes the whole class of bug instead of adding a
   fourth patch. .topbar is now only the shared flex row. */
.topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}

/* unit toggle — dark pill, strong contrast vs sunset */
/* These carried rgba(15,12,31,...) -- a deep navy-plum from the palette this
   app used BEFORE its surfaces became cream. Translucent dark plum over cream
   mixes to a muddy grey-mauve that belongs to no palette, which is exactly how
   it looked. It survived every migration because the sweep everyone runs
   greps for hex literals and face names; an rgba() triple matches none of
   those patterns. */
.unit-toggle {
  display: inline-flex;
  align-items: center;
  padding: 3px;
  border: 1.5px solid var(--card-line-strong);
  background: rgba(60, 40, 20, 0.06);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-radius: 100px;
  font-family: var(--sans);
}
.unit-toggle button {
  appearance: none;
  border: none;
  background: transparent;
  /* min-height + padding = ≥44pt touch target per Apple HIG */
  min-height: 38px;
  padding: 8px 18px;
  font-size: 13px;
  font-weight: 700;
  color: var(--cream-muted);
  border-radius: 100px;
  cursor: pointer;
  font-family: var(--sans);
  letter-spacing: 0.04em;
  transition: background .15s, color .15s;
  font-variant-numeric: tabular-nums;
  /* explicit tap-feedback for touch */
  -webkit-tap-highlight-color: transparent;
}
.unit-toggle button:active { transform: scale(0.96); }
.unit-toggle button:hover { color: var(--sunset-ink); }
.unit-toggle button.active {
  background: var(--night-1);
  color: var(--gold-deep);
  box-shadow: 0 2px 6px rgba(60, 40, 20, .18);
}
.unit-toggle button:focus-visible {
  outline: 2px solid var(--gold-deep);
  outline-offset: 2px;
}

/* hero content — flow-positioned so hero sizes to content */
.hero-inner {
  position: relative;
  z-index: 4;
  max-width: 1280px;
  margin: 0 auto;
  padding: 24px 48px 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: end;
  column-gap: 56px;
  row-gap: 14px;
}
.hero-inner .temp-block { grid-column: 1; grid-row: 1; }
.hero-inner .temp-meta  { grid-column: 2; grid-row: 1; align-self: end; margin-top: 0; padding-bottom: 14px; }
.hero-inner .headline   { grid-column: 1 / -1; grid-row: 2; margin: 0; }
.temp-block {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  line-height: 0.85;
  color: var(--sunset-ink);
}
.temp-num {
  font-family: var(--serif);
  font-size: clamp(140px, 18vw, 220px);
  font-weight: 400;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 6px 32px rgba(10, 23, 56, 0.45);
}
.temp-unit {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(28px, 3vw, 38px);
  color: var(--gold-on-sky);
  /* Text laid straight onto the sky cannot be solved by picking a colour: the
     four scenes span near-black to pale blue, and each is itself a gradient. A
     soft dark scrim travels with the glyphs instead, so one colour holds
     everywhere. Cheaper and far more robust than per-scene colour tuning, which
     still left golden-hour at 1.47:1 after three passes. */
  text-shadow: 0 1px 12px rgba(20, 14, 8, 0.55), 0 0 3px rgba(20, 14, 8, 0.45);
  margin-top: 28px;
  letter-spacing: -0.01em;
}
.temp-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin-top: 12px;
  font-size: 13px;
  letter-spacing: 0.02em;
  color: var(--sunset-ink-muted);
}
/* Source-transparency caption. Sits BELOW the hero rectangle on the cream page
   background (right-aligned, like a credit line under a photo), so it labels the
   block without drawing over the sky art. Forecast variant is dimmer since it's
   the less-authoritative source. Hidden until JS fills it in. */
.temp-source-row {
  margin: 8px 20px 0;
  text-align: right;
  line-height: 1;
}
.temp-source {
  display: inline-block;
  font-size: 12px;
  letter-spacing: 0.02em;
  font-weight: 500;
  color: var(--cream-muted);
  white-space: nowrap;
}
.temp-source[data-kind="forecast"] { color: var(--cream-subtle); }
.temp-source:empty { display: none; }
/* Chart source note — sits BELOW the chart card, outside the box, right-aligned
   (mirror of .temp-source-row under the hero). The curve is a model for your
   exact location, not a station measurement. */
.chart-source-row {
  margin: 6px 20px 2px;
  text-align: right;
  line-height: 1;
}
.chart-source {
  font-size: 11.5px;
  letter-spacing: 0.02em;
  font-weight: 500;
  color: var(--cream-subtle);
  white-space: nowrap;
}
.meta-condition {
  font-style: italic;
  font-family: var(--serif);
  font-size: 16px;
  color: var(--sunset-ink);
}
.temp-meta .feels-val {
  font-family: var(--serif);
  font-style: italic;
  color: var(--gold-soft);
  font-size: 16px;
  margin-left: 4px;
}
.temp-meta-label {
  font-size: 10.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--sunset-ink-subtle);
  font-weight: 600;
}
.temp-meta-row {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}
.meta-mono {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--sunset-ink);
}
.headline {
  margin: 28px 0 0;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(21px, 2.5vw, 30px);
  line-height: 1.4;
  letter-spacing: -0.005em;
  color: #fff8e8;
  max-width: 640px;
}
.headline-marks { color: var(--sunset-ink-subtle); margin: 0 4px; }

/* .headline-band — standalone band between hero and wear card. Sits
   on the soft pastel page background so it uses plain dark ink with
   no scrim. Does NOT extend .headline (which carries scene/theme
   color overrides that assume a sky background and would wash this
   out on the pale page bg). */
.headline-band {
  display: block;
  text-align: center;
  color: #1f1a14;
  margin: 16px 16px 8px;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(18px, 2.2vw, 24px);
  line-height: 1.4;
  letter-spacing: -0.005em;
  font-weight: 500;
  max-width: 640px;
  margin-left: auto;
  margin-right: auto;
  padding: 0 16px;
}
.headline-band .band-marks { color: rgba(31, 26, 20, 0.4); margin: 0 4px; }

/* =========================================================
   MAIN — cards float up from the night horizon
========================================================= */
main {
  /* mobile-first: full viewport, pager handles all internal layout */
  position: relative;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  margin: 0;
  padding: 0;
  overflow: hidden;
  z-index: 4;
}

.cards-row {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 16px;
  align-items: stretch;
}
.cards-row > .card { margin: 0; min-width: 0; }
.cards-row .day-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1080px) {
  .cards-row { grid-template-columns: minmax(0, 1fr); }
  .cards-row .day-strip { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  .cards-row .day-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.kicker {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--cream-subtle);
}

.card {
  background: linear-gradient(180deg, var(--card) 0%, var(--card-2) 100%);
  border: 1px solid var(--card-line);
  border-radius: 22px;
  padding: 26px 30px;
}
.card-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 18px;
  gap: 16px;
}
.head-right {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--cream-subtle);
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 12px;
}
.head-right .dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--gold);
  box-shadow: 0 0 6px var(--gold-glow);
}

/* =========================================================
   SCENARIO CHIPS
========================================================= */
.scenario-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 16px;
}
.scenario {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 10px 18px 10px 10px;
  border-radius: 100px;
  background: linear-gradient(135deg, rgba(110, 166, 216, 0.16), rgba(110, 166, 216, 0.04));
  border: 1px solid rgba(110, 166, 216, 0.26);
}
.scenario .ico {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(110, 166, 216, 0.22);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--blue-soft);
  flex-shrink: 0;
}
.scenario .ico svg { width: 18px; height: 18px; }
.scenario .body { line-height: 1.15; }
.scenario .lab {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--cream);
}
.scenario .sub {
  display: block;
  margin-top: 2px;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--cream-subtle);
  font-weight: 500;
}
.scenario.tone-warm,
.scenario.tone-positive {
  background: linear-gradient(135deg, rgba(246, 185, 74, .18), rgba(246, 185, 74, .04));
  border-color: rgba(246, 185, 74, .30);
}
.scenario.tone-warm .ico,
.scenario.tone-positive .ico {
  background: rgba(246, 185, 74, .22);
  color: var(--gold-soft);
}
.scenario.tone-coral,
.scenario.tone-warn {
  background: linear-gradient(135deg, rgba(229, 123, 91, .18), rgba(229, 123, 91, .04));
  border-color: rgba(229, 123, 91, .30);
}
.scenario.tone-coral .ico,
.scenario.tone-warn .ico {
  background: rgba(229, 123, 91, .22);
  color: var(--gold-soft);
}
.scenario.tone-plum {
  background: linear-gradient(135deg, var(--accent-soft), rgba(200, 121, 10, .04));
  border-color: var(--accent-edge);
}
.scenario.tone-plum .ico {
  background: var(--accent-soft);
  color: var(--accent);
}

/* =========================================================
   CLAUDE'S TAKE
========================================================= */
.take {
  display: grid;
  grid-template-columns: minmax(240px, 1fr) 1fr;
  gap: 22px;
  align-items: start;
}
.take-headline {
  font-family: var(--serif);
  font-style: italic;
  font-size: 24px;
  line-height: 1.22;
  color: var(--cream);
  letter-spacing: -0.01em;
  margin: 0;
}
.take-headline em { color: var(--gold-deep); font-style: italic; }
.take-sub {
  margin: 10px 0 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--cream-muted);
}
.take-sub strong { color: var(--gold-soft); font-weight: 500; }

.take-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.take-cell {
  border: 1px solid var(--card-line);
  border-radius: 14px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: rgba(60, 40, 20, 0.03);
}
.take-cell-wide { grid-column: span 2; }
.cell-head { display: flex; align-items: center; gap: 9px; }
.cell-ico {
  width: 26px; height: 26px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: rgba(110, 166, 216, 0.16);
  color: var(--blue-soft);
  flex-shrink: 0;
}
.cell-ico svg { width: 14px; height: 14px; }
.cell-ico-gold { background: var(--accent-soft); color: var(--gold); }
.cell-ico-coral { background: var(--accent-soft); color: var(--gold); }
.cell-ico-plum { background: var(--accent-soft); color: var(--gold); }
.cell-lab {
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cream-subtle);
  font-weight: 600;
}
.cell-val {
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1.15;
  color: var(--cream);
  letter-spacing: -0.01em;
}
.cell-val em { font-style: italic; color: var(--gold-deep); }
.feels-cell {
  background: linear-gradient(135deg, rgba(246, 185, 74, .10), rgba(246, 185, 74, .02));
  border-color: rgba(246, 185, 74, .28);
}
.feels-val-big em {
  font-size: 30px;
  line-height: 1;
}
.cell-detail {
  font-size: 12px;
  color: var(--cream-muted);
  line-height: 1.4;
}

/* Unified kit row (Task 8) — one chip form for clothing layers + carry
   items (umbrella, sunscreen, sunglasses). Previously split into
   .outfit-row (with a struck-through "off" state for un-picked defaults)
   and .carry-row (solid gold-filled pills). The backend's "kit" list is
   already the resolved selection — every chip shown is "on" — so there's
   no off-state to draw and nothing should read as visually more
   important than anything else. Uses the old outline-pill look
   (.outfit-pill's border treatment), not the solid-gold carry fill. */
.kit-row {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 1px;
}
.kit-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px 3px 7px;
  border-radius: 100px;
  background: var(--accent-soft);
  border: 1px solid var(--accent-edge);
  color: var(--accent-chip-ink);
  font-size: 11px;
  color: var(--cream-muted);
}
.kit-chip svg { width: 12px; height: 12px; color: var(--gold-soft); }

/* =========================================================
   24H CHART
========================================================= */
.chart-wrap {
  position: relative;
  margin: 0 -6px;
  /* touch-action: was 'pan-y' — that told the browser horizontal touch
     belongs to JS, which silently blocked the pager's scroll-snap from
     receiving horizontal swipes that started on the chart. Removed.
     The chart's JS state machine (long-press to arm scrub) handles
     scrub gestures via preventDefault inside its own touchmove listener.
     Browser is free to do horizontal page-snap for non-scrub gestures.

     Long-press text-selection / iOS callout on SVG was making the
     whole chart area highlight blue when scrub-arming. Suppress all
     three: user-select, callout, tap-highlight. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  /* Reserve vertical space at top for the condition transition icons.
     If no transitions exist for the day, the strip is empty but the
     gap stays so the temp curve doesn't shift on first vs second
     render. */
  padding-top: 22px;
}

/* Peak/trough temp markers — positioned dynamically by JS at the actual
   x,y of the temp curve's high and low points (not at fixed Y-axis). */
.chart-y-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
}
.chart-y-labels span {
  position: absolute;
  /* Warm plum pill instead of pure dark navy + harsh white outline.
     Softer pairing with the warm pastel scene; cream text still has
     ~9:1 contrast. */
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: 11px;
  color: #fff8e8;
  background: rgba(28, 20, 50, 0.78);
  padding: 2.5px 7px;
  border-radius: 5px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  letter-spacing: 0.02em;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
/* NOW label overrides — warm coral (matches the sunset palette
   --sky-3) instead of electric magenta. Same anchor as the vertical
   NOW line so a glance ties them together. */
.chart-y-labels span.now-label {
  background: var(--wx-now);
  color: #2a1410;
  box-shadow: 0 1px 4px var(--wx-now-glow);
  font-size: 9px;
  letter-spacing: 0.1em;
  padding: 1px 5px;
  font-weight: 700;
}

/* Weather condition icons at the top of the chart — one per transition */
.chart-conditions {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 22px;
  pointer-events: none;
  z-index: 2;
}
.chart-cond-icon {
  position: absolute;
  /* Nudged down so the gap above the bar matches the icon+time gap below it
     (symmetric around the day/night bar). */
  top: 6px;
  width: 18px;
  height: 18px;
  transform: translateX(-50%);
  color: var(--cream-muted);
  opacity: 0.65;
}
.chart-cond-icon svg { width: 100%; height: 100%; display: block; }
.chart-cond-icon.cond-clear { color: var(--wx-sun); opacity: 0.8; }
.chart-cond-icon.cond-rain  { color: var(--blue); opacity: 0.85; }
.chart-cond-icon.cond-snow  { color: var(--blue-soft); opacity: 0.85; }
.chart-cond-icon.cond-storm { color: var(--wx-storm); opacity: 0.9; }
.chart-cond-icon.cond-cloud { color: var(--cream-muted); opacity: 0.7; }
.chart-cond-icon.cond-fog   { color: var(--cream-subtle); opacity: 0.55; }
.chart-svg {
  width: 100%;
  height: 320px;
  display: block;
  overflow: visible;
}

.chart-hover {
  position: absolute;
  pointer-events: none;
  inset: 0;
}
.hover-line {
  position: absolute;
  top: 50px; bottom: 100px;
  width: 1px;
  background: var(--cream-subtle);
  opacity: 0;
  transition: opacity .12s;
}
/* Compact pill — single horizontal line, anchored to one side of cursor by
   JS (never centered) so the hover-line is never covered. The pill itself
   stays dark for high-contrast pop on bright cream cards. */
.hover-tip {
  position: absolute;
  top: 4px;
  background: rgba(31, 26, 20, 0.92);
  border: 1px solid rgba(255, 220, 180, 0.15);
  border-radius: 100px;
  padding: 6px 14px;
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  font-size: 11.5px;
  line-height: 1.2;
  color: #fff5d8;
  white-space: nowrap;
  box-shadow: 0 8px 30px -8px rgba(0, 0, 0, .35);
  opacity: 0;
  transition: opacity .12s;
  pointer-events: none;
}
.hover-tip .time {
  font-family: var(--serif);
  font-style: italic;
  font-size: 13px;
  color: var(--gold-soft);
  letter-spacing: 0.02em;
}
.hover-tip .val { font-family: var(--mono); color: #fff5d8; font-weight: 500; }
.hover-tip .val.gold { color: var(--gold-soft); }
.hover-tip .sep { color: rgba(255, 245, 216, 0.45); }
.chart-wrap.is-hover .hover-line,
.chart-wrap.is-hover .hover-tip { opacity: 1; }

.chart-legend {
  display: flex;
  align-items: center;
  /* Centred, to match .trend-legend. This rule simply never set
     justify-content, so it inherited flex-start and sat left while every other
     chart's legend was centred -- an omission that reads as a decision. */
  justify-content: center;
  gap: 22px;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px dashed var(--card-line-strong);
  font-size: 11.5px;
  color: var(--cream-muted);
  letter-spacing: 0.04em;
  flex-wrap: wrap;
}
.legend-item { display: inline-flex; align-items: center; gap: 8px; }
.legend-swatch { width: 22px; height: 2px; border-radius: 2px; background: var(--wx-temp); }
.legend-swatch.wind { background: var(--wx-wind); }
.legend-swatch.gust {
  background: repeating-linear-gradient(90deg, var(--wx-gust) 0 4px, transparent 4px 7px);
  height: 2px;
}
.legend-swatch.rain {
  background: linear-gradient(180deg, var(--wx-rain-fill), rgba(255,255,255,0));
  height: 10px;
  border-radius: 2px;
}
.legend-marker {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--gold-soft), var(--gold));
  box-shadow: 0 0 6px var(--gold-glow);
}
.legend-marker.set {
  background: radial-gradient(circle, var(--accent-lift), var(--accent));
  box-shadow: 0 0 6px var(--gold-glow);
}
.legend-time { color: var(--cream); font-family: var(--mono); }

/* =========================================================
   6-DAY STRIP
========================================================= */
.day-strip {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 10px;
}
.day {
  border: 1px solid var(--card-line);
  border-radius: 16px;
  padding: 16px 10px 14px;
  text-align: center;
  position: relative;
  background: rgba(60, 40, 20, 0.03);
  transition: transform .2s, border-color .2s;
}
.day:hover { transform: translateY(-2px); border-color: var(--card-line-strong); }
.day.past { opacity: 0.55; }
.day.today {
  background: linear-gradient(180deg, rgba(246, 185, 74, .15) 0%, rgba(229, 123, 91, 0.05) 100%);
  border-color: rgba(246, 185, 74, .4);
  box-shadow: 0 0 0 1px rgba(246, 185, 74, .18), 0 8px 28px -16px rgba(246, 185, 74, .45);
}
.day .d-label {
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cream-subtle);
  font-weight: 600;
}
.day.today .d-label { color: var(--gold-deep); }
.day .d-hi {
  font-family: var(--serif);
  font-size: 26px;
  line-height: 1.1;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
  margin-top: 6px;
}
.day .d-lo {
  font-size: 12px;
  color: var(--cream-subtle);
  font-variant-numeric: tabular-nums;
  margin-top: 2px;
}
.day .d-cond {
  font-size: 10.5px;
  color: var(--cream-subtle);
  letter-spacing: 0.04em;
  margin-top: 4px;
}
.day .d-bar {
  position: relative;
  height: 4px;
  margin: 10px 6px 2px;
  border-radius: 100px;
  background: linear-gradient(
    90deg,
    rgba(110, 166, 216, .35) 0%,
    rgba(246, 185, 74, .5) 60%,
    rgba(229, 123, 91, .55) 100%
  );
}
.day .d-bar::after {
  content: "";
  position: absolute;
  top: -2px;
  left: var(--cur, 50%);
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--gold);
  transform: translateX(-50%);
  box-shadow: 0 0 0 2px rgba(246, 185, 74, .18);
}

/* =========================================================
   FOOTER — US-style privacy disclosure
========================================================= */
.footer {
  width: 100%;
  max-width: 1280px;
  margin: 24px auto 36px;
  padding: 0 48px;
}
.footer-row {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--cream-subtle);
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}
.footer-row .sep { opacity: 0.4; }
.footer-row a {
  color: var(--cream-muted);
  text-decoration: none;
  transition: color .15s;
}
.footer-row a:hover { color: var(--gold-soft); }
.footer-privacy {
  margin: 14px 0 0;
  font-size: 11.5px;
  color: var(--cream-subtle);
  line-height: 1.55;
  max-width: 720px;
  letter-spacing: 0.01em;
}

/* =========================================================
   RESPONSIVE
========================================================= */
@media (max-width: 820px) {
  /* No `.topbar { top/left/right }` here — see the base .topbar rule. The
     topbar is in normal flow above the hero now; the inset offsets it needed
     as an absolutely-positioned child of .hero are what pushed the city pill
     down the hero on notch phones. .topbar-above-hero owns its own padding. */
  /* No `.hero { padding-top }` here either: the unconditional
     @media (max-width: 1023px) block below covers this whole range and sets
     padding-top together with the negative margin that slides the sky up under
     the topbar. A second value here would be dead and misleading. */
  .hero-inner { padding: 70px 22px 0; }
  /* No margin-top:-50px on `main` here — that was a pre-pager trick to pull
     cards up under the hero. With the 100dvh pager it instead pushes the
     hero+topbar 50px ABOVE the viewport, making the city pill unreachable
     unless the user rubber-bands and taps in the brief overscroll. */
  main { padding: 0 22px; }
  .card { padding: 22px 20px; }
  .take { grid-template-columns: 1fr; gap: 18px; }
  .take-grid { grid-template-columns: 1fr 1fr; }
  .day-strip { grid-template-columns: repeat(3, 1fr); }
  .chart-axis { grid-template-columns: repeat(7, 1fr); }
  .footer { padding: 0 22px; }
}
@media (max-width: 480px) {
  .take-grid { grid-template-columns: 1fr; }
  .take-cell-wide { grid-column: span 1; }
  .day-strip { grid-template-columns: repeat(2, 1fr); }
  .temp-meta { gap: 10px 16px; font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ============ skeleton placeholders ============
   Visible only when no inline data and no localStorage cache (first-ever visit
   or storage disabled). JS removes the .is-skeleton class on body after first
   successful render. */

@keyframes skeleton-shimmer {
  0%   { background-position: -200px 0; }
  100% { background-position: 200px 0; }
}

/* Hero placeholders are sized so the layout doesn't shift when real data lands. */
body.is-skeleton .temp-num         { width: 120px; }
body.is-skeleton .meta-mono        { width: 80px; }
body.is-skeleton .feels-val        { width: 60px; }

body.is-skeleton .temp-num,
body.is-skeleton .meta-mono,
body.is-skeleton .feels-val {
  /* Inherit the shimmer effect when body is in skeleton mode */
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.15) 0%,
    rgba(255, 255, 255, 0.35) 50%,
    rgba(255, 255, 255, 0.15) 100%
  );
  background-size: 200px 100%;
  animation: skeleton-shimmer 1.4s linear infinite;
  color: transparent;
  border-radius: 4px;
  display: inline-block;
}

/* =========================================================
   PAGER — horizontal swipe between two pages on mobile
   Page 1: 24-hour chart
   Page 2: hero, take, days, footer
========================================================= */
.pager {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;            /* Firefox */
  width: 100vw;
  max-width: 100%;
}
.pager::-webkit-scrollbar { display: none; }   /* Chrome/Safari */

.page {
  flex: 0 0 100%;
  min-width: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* Dot pagination — fixed at bottom, above the safe area on notched iPhones */
.pager-dots {
  position: fixed;
  bottom: calc(12px + var(--sab));
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  gap: 10px;
  z-index: 50;
  pointer-events: none;        /* container ignored, dots get pointer-events back */
}
.pager-dots .dot {
  pointer-events: auto;
  width: 8px;
  height: 8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.25);
  cursor: pointer;
  transition: background 0.2s ease, transform 0.2s ease;
}
.pager-dots .dot.active {
  background: rgba(255, 255, 255, 0.85);
  transform: scale(1.15);
}
.pager-dots .dot:hover,
.pager-dots .dot:focus-visible {
  background: rgba(255, 255, 255, 0.55);
  outline: none;
}

/* Reserve room below content so dots don't overlap the last card */
.page > :last-child { padding-bottom: 40px; }

/* =========================================================
   MOBILE CHART TIDY-UP — labels are too cramped at narrow widths
========================================================= */
@media (max-width: 768px) {
  /* Chart proportionally shorter on phones so it doesn't dominate */
  .chart-svg { height: 240px; }

  /* Legend flow-wraps cleanly on narrow widths */
  .chart-legend {
    flex-wrap: wrap;
    gap: 10px 18px;
    font-size: 11px;
  }
}

/* =========================================================
   DESKTOP / WEB LAYOUT — "Direction A" (Ann, 2026-05-28)
   The app is a mobile-first full-viewport pager. With no width
   cap, desktop stretched every card edge-to-edge — content
   floated in a sea of horizontal space. Fix: keep the sky scene
   full-bleed (the dramatic banner Ann chose) but make it taller
   so it's no longer a thin letterbox strip, and pull the cards /
   headline / trend cards into a centered reading column.
   Starts at 821px to hand off seamlessly from the mobile
   `max-width: 820px` rules — no dead zone in between.
   Mobile (<= 820px) is completely untouched.
========================================================= */
@media (min-width: 821px) {
  /* Taller hero so the full-bleed sky reads as a banner, not a strip.
     .hero-inner stays capped at 1280px (defined above) so the 60deg
     temperature keeps the open-sky placement Ann approved. */
  .hero {
    min-height: 440px;
    max-height: 480px;
  }

  /* Centered content column. min() guarantees a 24px gutter on each
     side at tablet widths (821-868px) while capping at 820px on wide
     screens. margin-inline only — preserves each element's own
     vertical margins / stacking gaps. */
  .page > .headline-band,
  .page > .card,
  .page > .trend-card,
  .trends-head {
    max-width: min(820px, 100% - 3rem);
    margin-inline: auto;
  }
}

/* Pager fills the main element (which is now 100vh / 100vw). */
.pager {
  height: 100vh;
  height: 100dvh;
}

/* =========================================================
   CITY SUGGEST DROPDOWN — live geocoder autocomplete
   (sits below the .city-add-form input in the sheet panel)
========================================================= */
.city-suggest {
  list-style: none;
  margin: 8px 0 0;
  padding: 6px;
  border: 1px solid var(--card-line);
  border-radius: 12px;
  background: rgba(60, 40, 20, 0.04);
  max-height: 240px;
  overflow-y: auto;
}
.city-suggest[hidden] { display: none; }
.city-suggest-row { margin: 0; padding: 0; }
.city-suggest-pick {
  appearance: none;
  border: none;
  background: transparent;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border-radius: 8px;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--cream);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-height: 40px;
}
.city-suggest-pick:hover,
.city-suggest-pick:active {
  background: rgba(60, 40, 20, 0.08);
}
.city-suggest-name {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.city-suggest-coords {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--cream-subtle);
  flex-shrink: 0;
}

/* =========================================================
   PAGE 2 — What-to-wear card (now at top, was a take-cell)
   + Settings card (theme picker lives here)
========================================================= */
.wear-card { /* extends .card */ }
.wear-card .wear-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.wear-headline {
  display: flex;
  align-items: center;
  gap: 12px;
}
.wear-primary {
  font-family: var(--serif);
  font-style: italic;
  font-size: 22px;
  line-height: 1.2;
  color: var(--cream);
  letter-spacing: -0.01em;
}
.wear-card .kit-row {
  /* kit chips under the headline get a small reset since the wider
     card has more horizontal room than the take-cell did. */
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 0;
}

.settings-card { /* extends .card */ }
.settings-section {
  margin-top: 4px;
}

/* --- Data-source explainer (observed vs modeled) --- */
/* Grid, not flex: with flex the body started wherever its tag happened to end,
   so PICKS (short) pushed its text left of WRITES and COVERS and the column read
   as ragged. A fixed first column makes every body start at the same x while the
   pills still hug their own text (justify-self: start). Column width is set by
   the longest tag in either section, OBSERVED. */
.src-row {
  display: grid; grid-template-columns: 86px 1fr; gap: 10px; margin-bottom: 12px;
}
.src-row:last-child { margin-bottom: 0; }
.src-tag {
  justify-self: start; font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; padding: 3px 9px; border-radius: 100px;
  height: fit-content; margin-top: 1px;
}
/* Both tags share ONE format, deliberately (Ann, 2026-07-25).

   They used to differ by weight: OBSERVED a solid amber fill, MODELED a quiet
   outline, on the reasoning that the measured value should carry the app's
   confidence colour. The reasoning was fine and the result was not: one filled
   chip beside one outlined chip is the universal look of selected-vs-unselected,
   so a pair of LABELS read as a two-option CONTROL, and the panel appeared to be
   asking the user to pick a data source. Ann: "要不然用户以为这是要我们设置 选择".
   A label that reads as a control is a product defect, not a style preference,
   which is why the older intent loses here.

   Nothing is lost by matching them: the words "Observed" and "Modeled" already
   carry the distinction, and the body text next to each explains it. Encoding it
   a second time in fill weight was never the thing doing the work.

   Historical note kept because it keeps being re-learnt: a near-white label on
   plain --gold is 3.39:1. That pairing was wrong five separate times in this
   migration, always because --gold reads like "the brand colour, use it as a
   fill". It IS the fill colour -- for DARK labels and large text. Small light
   labels need the
   deep variant underneath them. */
   Folded into the base `.src-tag` rather than kept as two identical `.obs` /
   `.mod` variants: once the formats matched, those modifiers named a difference
   that no longer existed, and a new row had to pick one of them arbitrarily. */
.src-tag { background: transparent; color: var(--cream-muted);
           box-shadow: inset 0 0 0 1.5px var(--card-line-strong); }
.src-body { font-size: 13px; line-height: 1.5; color: var(--cream); }
.src-body b { font-weight: 600; }

/* --- Observation-station picker --- */
.station-intro { font-size: 13px; line-height: 1.5; color: var(--cream-muted); margin: 0 0 14px; }
.station-intro b { color: var(--cream); font-weight: 600; }
.station-list { display: flex; flex-direction: column; gap: 8px; }
.station-opt {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  background: var(--surface, #f4f1ec); border: 1.5px solid rgba(31, 26, 20, .12);
  border-radius: 14px; padding: 13px 15px; cursor: pointer; font: inherit;
}
/* Selection is UI state, so it wears the brand accent. The green stays on the
   OBSERVED/MODELED badges above, where it carries a real distinction (measured
   vs predicted, ADR-0002) that amber would erase. Borrowing a semantic colour
   for a UI state is how the two stop being distinguishable. */
.station-opt.sel { border-color: var(--gold); background: var(--accent-soft); }
.station-radio {
  flex: 0 0 auto; width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid rgba(31, 26, 20, .3); position: relative;
}
.station-opt.sel .station-radio { border-color: var(--gold); }
.station-opt.sel .station-radio::after {
  content: ''; position: absolute; inset: 3px; border-radius: 50%; background: var(--gold);
}
.station-main { flex: 1 1 auto; min-width: 0; }
.station-name { display: block; font-size: 14.5px; font-weight: 600; color: var(--cream); }
.station-sub { display: block; font-size: 12px; color: var(--cream-subtle); margin-top: 1px; }
.station-temp {
  flex: 0 0 auto; font-family: var(--serif); font-size: 22px; color: var(--cream);
  font-variant-numeric: tabular-nums;
}
.station-saved { font-size: 12px; color: var(--cream-subtle); margin: 12px 0 0; }
.station-empty { font-size: 13px; color: var(--cream-subtle); }
.settings-card .theme-picker {
  /* override the theme-picker's old in-sheet styling — no top border
     since the card already has its own surface. */
  margin: 0;
  padding: 0;
  border-top: none;
}

/* =========================================================
   CITY PICKER — trigger pill + bottom-sheet picker
========================================================= */
/* This pill FLOATS ON THE SKY, so it needs a dark scrim: its label is cream, and
   the sky behind it runs from near-black at night to pale blue at midday. The
   scrim is what makes one label colour work across all four scenes.
   It previously carried the same rgba(15,12,31,...) value as .unit-toggle and was
   changed with it -- but .unit-toggle sits on a cream card in Settings, where a
   dark wash reads as mud, and this one sits on imagery, where a light wash leaves
   the label at 2.2:1. Same value, opposite contexts; only the value was checked.
   Restored as a warm neutral rather than the old plum. */
.city-trigger {
  appearance: none;
  border: 1.5px solid rgba(27, 23, 16, 0.30);
  background: rgba(27, 23, 16, 0.42);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  color: var(--sunset-ink);
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  min-height: 38px;
  border-radius: 100px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s, transform .07s;
  max-width: 60vw;
}
.city-trigger:hover { background: rgba(27, 23, 16, 0.54); }
.city-trigger:active { transform: scale(0.97); }
.city-trigger:focus-visible { outline: 2px solid var(--gold-soft); outline-offset: 2px; }
.city-trigger-pin { width: 14px; height: 14px; flex-shrink: 0; color: var(--gold-soft); }
.city-trigger-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}
.city-trigger-chevron { width: 12px; height: 12px; flex-shrink: 0; opacity: 0.7; }

/* sheet container — fixed full-screen overlay (closed by default via [hidden]) */
.city-sheet {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: flex-end;            /* phones: bottom sheet */
  justify-content: center;
}
.city-sheet[hidden] { display: none; }

.city-sheet-backdrop {
  position: absolute; inset: 0;
  background: rgba(10, 8, 22, 0.55);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  cursor: pointer;
}

.city-sheet-panel {
  position: relative;
  background: linear-gradient(180deg, var(--card) 0%, var(--card-2) 100%);
  border: 1px solid var(--card-line);
  border-radius: 22px 22px 0 0;
  padding: 24px 22px calc(28px + var(--sab));
  width: 100%;
  max-width: 520px;
  max-height: 80vh;
  overflow-y: auto;
  /* Prevent any inner element with wide content from stretching the panel
     past the viewport on phones — Ann was scrolling horizontally to see
     the whole sheet. */
  overflow-x: hidden;
  box-sizing: border-box;
  color: var(--cream);
  box-shadow: 0 -16px 40px -8px rgba(0, 0, 0, 0.6);
  animation: city-sheet-slide-up .22s ease-out;
}
@keyframes city-sheet-slide-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

.city-sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 18px;
}
.city-sheet-head h2 {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 22px;
  margin: 0;
  color: var(--cream);
  letter-spacing: -0.01em;
}
.city-sheet-close {
  appearance: none;
  border: none;
  background: rgba(244, 236, 216, 0.08);
  color: var(--cream);
  width: 36px; height: 36px;
  border-radius: 50%;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.city-sheet-close:active { transform: scale(0.96); }

.city-list {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
}
.city-list:empty { display: none; }
.city-row {
  display: flex; align-items: center;
  border: 1px solid var(--card-line);
  border-radius: 14px;
  margin-bottom: 8px;
  background: rgba(244, 236, 216, 0.025);
  overflow: hidden;
}
.city-row.active {
  background: linear-gradient(135deg, rgba(246, 185, 74, 0.12), rgba(246, 185, 74, 0.02));
  border-color: rgba(246, 185, 74, 0.32);
}
.city-row-pick {
  appearance: none;
  border: none;
  background: transparent;
  flex: 1;
  min-width: 0;   /* allow text inside to ellipsis instead of forcing row width */
  text-align: left;
  padding: 14px 16px;
  font-family: var(--sans);
  font-size: 15px;
  color: var(--cream);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  min-height: 44px;
}
.city-row-name {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.city-row.active .city-row-pick { color: var(--gold-soft); font-weight: 500; }
.city-row-name { display: block; }
.city-row-delete {
  appearance: none;
  border: none;
  background: transparent;
  width: 44px; height: 44px;
  color: var(--cream-subtle);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  display: inline-flex; align-items: center; justify-content: center;
}
.city-row-delete:active { color: var(--danger); transform: scale(0.92); }
.city-row-delete svg { width: 16px; height: 16px; }

/* set-as-default star (filled + gold when this is the startup city) */
.city-row-default {
  appearance: none; border: none; background: transparent;
  width: 40px; height: 44px; color: var(--cream-subtle);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  display: inline-flex; align-items: center; justify-content: center;
}
.city-row-default svg { width: 16px; height: 16px; }
.city-row-default.on { color: var(--gold-soft); }
.city-row-default:active { transform: scale(0.92); }
.city-row-tag {
  margin-left: 8px; font-size: 11px; font-weight: 500;
  color: var(--cream-subtle); vertical-align: middle;
}

.city-action {
  appearance: none;
  border: 1px solid var(--card-line-strong);
  background: rgba(244, 236, 216, 0.04);
  width: 100%;
  padding: 14px 16px;
  border-radius: 14px;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--cream);
  display: inline-flex; align-items: center; gap: 10px; justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s, transform .07s;
  min-height: 48px;
  margin-bottom: 12px;
}
.city-action:hover { background: rgba(244, 236, 216, 0.08); }
.city-action:active { transform: scale(0.98); }
.city-action svg { width: 16px; height: 16px; color: var(--gold-soft); }

.city-add-form {
  display: flex; align-items: center; gap: 8px;
  border: 1px solid var(--card-line-strong);
  background: rgba(244, 236, 216, 0.04);
  border-radius: 14px;
  padding: 4px 4px 4px 14px;
}
.city-add-form > svg { width: 14px; height: 14px; color: var(--cream-subtle); flex-shrink: 0; }
.city-add-form input {
  flex: 1;
  appearance: none;
  border: none;
  background: transparent;
  padding: 12px 0;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--cream);
  outline: none;
  min-width: 0;
}
.city-add-form input::placeholder { color: var(--cream-subtle); font-style: italic; }
.city-add-form button {
  appearance: none;
  border: none;
  background: var(--gold);
  color: #1a1206;
  padding: 0 18px;
  height: 40px;
  border-radius: 100px;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  flex-shrink: 0;
}
.city-add-form button:active { transform: scale(0.96); }
.city-add-form button:disabled { opacity: 0.4; cursor: not-allowed; }

.city-sheet-hint {
  margin: 12px 0 0;
  padding: 12px 14px;
  font-size: 13.5px;
  line-height: 1.45;
  font-style: italic;
  color: var(--gold-soft);
  background: rgba(229, 123, 91, 0.10);
  border-left: 3px solid rgba(229, 123, 91, 0.5);
  border-radius: 6px;
  /* word-break so the diagnostic [env · plugin] text wraps cleanly on
     narrow phones instead of overflowing — we need this to be readable. */
  word-break: break-word;
}

@media (min-width: 720px) {
  /* desktop: center the sheet vertically rather than bottom-attached */
  .city-sheet { align-items: center; }
  .city-sheet-panel {
    border-radius: 22px;
    max-width: 460px;
  }
}

@media (max-width: 480px) {
  .city-trigger-label { max-width: 140px; }
}

/* =========================================================
   ALERT BANNER — sticky at top when severe weather active
========================================================= */
.alert-banner {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 90;
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 18px calc(14px + var(--sat) * 0.5) 18px;
  padding-top: calc(14px + var(--sat));
  background: linear-gradient(180deg, #5a1d1d 0%, #3a1313 100%);
  color: #ffe9dc;
  border-bottom: 1px solid rgba(255, 200, 180, 0.2);
  box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.6);
  font-family: var(--sans);
  animation: alert-banner-in .35s ease-out;
}
.alert-banner[hidden] { display: none; }
@keyframes alert-banner-in {
  from { transform: translateY(-100%); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}
.alert-banner-icon {
  width: 22px; height: 22px;
  color: #ffd6c3;
  flex-shrink: 0;
  margin-top: 1px;
}
.alert-banner-body {
  flex: 1;
  min-width: 0;
  display: flex; flex-direction: column; gap: 2px;
}
.alert-banner-event {
  font-family: var(--serif);
  font-style: italic;
  font-size: 16px;
  color: #fff5ee;
  line-height: 1.25;
  letter-spacing: -0.01em;
}
.alert-banner-area {
  font-size: 12px;
  color: rgba(255, 233, 220, 0.78);
  letter-spacing: 0.02em;
}
.alert-banner-close {
  appearance: none;
  border: none;
  background: rgba(255, 255, 255, 0.08);
  color: #ffe9dc;
  width: 32px; height: 32px;
  border-radius: 50%;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  flex-shrink: 0;
}
.alert-banner-close:active { transform: scale(0.94); }


/* =========================================================
   PAGE 2 TREND CHARTS — yesterday/today/tomorrow line charts
========================================================= */
.trends-head {
  text-align: center;
  /* NO safe-area inset here. It used to add --sat "so the kicker clears the
     notch", which was correct when trends was its own top-of-viewport pager
     page. Trends now continues in the SAME vertical scroll as #page-utility,
     so this header sits mid-scroll and never touches the notch — the inset was
     dead weight that opened a ~59px blank gap above the section on Dynamic
     Island phones (invisible in a WebKit shot, where --sat is 0). */
  padding: 28px 16px 8px;
}
.trends-head .kicker {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted, #6b6962);
}
.trend-card {
  margin: 10px 14px;
  padding: 14px 14px 8px;
  background: var(--card, #f4ecdf);
  border-radius: 14px;
  border: 1.5px solid var(--card-edge);
  box-shadow: 0 1px 0 rgba(27, 23, 16, 0.04);
}
.trend-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 8px;
}
.trend-label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: #1f1a14;
}
.trend-unit {
  font-family: var(--mono);
  font-size: 11px;
  color: rgba(31, 26, 20, 0.55);
}
.trend-svg-wrap {
  position: relative;
}
.trend-svg {
  width: 100%;
  height: 140px;
  display: block;
}
.trend-x-labels {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  font-family: var(--mono);
  font-size: 10px;
  color: rgba(31, 26, 20, 0.45);
  text-align: center;
  padding-top: 2px;
}

/* =========================================================
   GEAR BUTTON (page-1 footer → opens settings page)
========================================================= */
.page1-footer {
  display: flex;
  justify-content: center;
  padding: 16px 0 24px;
}
.gear-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid rgba(31, 26, 20, 0.15);
  background: rgba(255, 255, 255, 0.5);
  border-radius: 50%;
  color: rgba(31, 26, 20, 0.55);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}
.gear-btn:hover, .gear-btn:active {
  background: rgba(255, 255, 255, 0.8);
  color: #1f1a14;
}
.gear-btn:active { transform: scale(0.92); }

/* =========================================================
   PAGE 2 polish — transparent trend cards + safe-area top
========================================================= */
/* Each .page is its own 100vh / 100dvh scroll container; iOS notch
   was eating the top header on page 2. Push content down by the
   safe-area inset so the kicker is always visible. */
.page { padding-top: 0; }

/* Trend cards: drop the opaque card background — Ann wants them
   sitting on the page background so the sky-band reads as the
   continuous day/night context. Keep the structural padding so
   stacked charts breathe. */
.trend-card {
  background: transparent;
  box-shadow: none;
  padding: 8px 14px 10px;
  margin: 6px 4px;
}
.trend-svg { height: 150px; padding-top: 12px; }

/* =========================================================
   LATER-ADVICE predictive line (only shown when meaningful)
========================================================= */
/* No left accent rail here. A rail on a rounded card is one of the named
   AI-default tells, and it was carrying the "this is advice" signal alone.
   Replaced with a tinted block + a leading amber glyph: the glyph is a
   non-color cue (survives grayscale / color-vision deficiency), which the
   rail never was. */
.later-advice {
  margin: 10px 0 0;
  padding: 9px 13px 9px 11px;
  display: flex;
  align-items: baseline;
  gap: 8px;
  background: var(--accent-soft);
  border-radius: 9px;
  font-size: 13px;
  font-style: italic;
  color: rgba(31, 26, 20, 0.78);
}
/* The `hidden` attribute must win over the `display: flex` above. They tie on
   specificity, so the author flex rule wins and a "quiet" state (no upcoming
   advice) would otherwise render as an empty tinted band under the kit. */
.later-advice[hidden] { display: none; }

/* trend-chart scrub tooltip — shared per .trend-svg-wrap, hidden by default */
.trend-svg-wrap { position: relative; }
.trend-tip {
  position: absolute;
  top: 0;
  left: 0;
  padding: 4px 9px;
  background: rgba(28, 20, 50, 0.92);
  color: #fff8e8;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  border-radius: 5px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.12s ease, transform 0.12s ease;
  z-index: 5;
}
.trend-tip.visible { opacity: 1; transform: translateY(0); }

/* Suppress iOS long-press selection on trend SVGs (same protection
   the page-1 chart already has). */
.trend-svg {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

/* =========================================================
   SETTINGS SHEET — full-screen overlay (replaces page-3-in-pager)
========================================================= */
.settings-sheet {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  justify-content: center;
  align-items: stretch;
}
.settings-sheet[hidden] { display: none; }
.settings-sheet-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(27, 23, 16, 0.45);
  opacity: 0;
  transition: opacity 0.22s ease;
}
.settings-sheet.open .settings-sheet-backdrop { opacity: 1; }
.settings-sheet-panel {
  position: relative;
  background: var(--bg, #f4ecdf);
  width: 100%;
  max-width: 560px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: calc(16px + var(--sat)) 0 calc(20px + var(--sab));
  transform: translateY(100%);
  transition: transform 0.24s cubic-bezier(.2,.8,.3,1);
}
.settings-sheet.open .settings-sheet-panel { transform: translateY(0); }
.settings-sheet-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 4px 18px 14px;
}
.settings-sheet-head h2 {
  margin: 0;
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 400;
  /* --text does not exist here; this happened to equal --cream via the
     fallback. Pointing at the real token so a palette change reaches it. */
  color: var(--cream);
}
.settings-sheet-close {
  padding: 8px 16px;
  /* Was var(--text, #1f1a14). --text is not defined anywhere in this app, so
     this always fell through to the near-black fallback -- a hard black pill
     on warm paper, which is the one button colour that is never right here. */
  background: var(--gold-deep);
  color: var(--accent-fg, #fffdf8);
  border: 0;
  border-radius: 100px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.settings-sheet-close:active { transform: scale(0.96); }

/* Envelope + close, grouped so a variable-width pill and a fixed-size icon lay
   out side by side without hand-computed offsets. */
.settings-head-actions { display: flex; align-items: center; gap: 6px; }
.settings-mail {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 100px;
  color: var(--cream-muted);
  -webkit-tap-highlight-color: transparent;
}
.settings-mail svg { width: 19px; height: 19px; }
.settings-mail:hover { color: var(--cream); background: var(--card-line); }
.settings-mail:active { transform: scale(0.94); }
/* The toast confirms the clipboard fallback fired. It has to be visible even
   when the mailto: did nothing at all -- that silent no-op is the entire reason
   the fallback exists, so this is the only feedback the user gets. */
.settings-mail-toast {
  margin: -8px 18px 6px; padding: 0;
  /* --gold-soft (#f0b445) is the LIFTED variant for DARK surfaces; on this pale
     panel it measured 1.58:1, i.e. barely there -- and this toast is the only
     confirmation the user gets that the address was copied, so unreadable here
     means the fallback silently fails to communicate. Deep amber instead. */
  font-size: 12px; color: #8f5406;
  text-align: right;
  opacity: 0; transform: translateY(-3px);
  transition: opacity .18s ease, transform .18s ease;
  pointer-events: none;
}
.settings-mail-toast.show { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .settings-mail-toast { transition: none; }
}

/* Rate-us pill, above the sheet's footer. Same gold language as the rest of the
   panel's CTAs; a secondary CTA, so tinted rather than solid. */
.rate-link {
  display: block;
  margin: 6px 12px 12px;
  padding: 11px 14px;
  border-radius: 12px;
  /* Literal tints, not a var: there is no --gold-wash token, and a var() whose
     custom property was never defined falls silently to its fallback (or to the
     initial value if none is given), which is a bug that renders as "the colour
     is just slightly off" and never as an error. Two uses do not earn a token. */
  background: rgba(160, 95, 7, .10);
  /* NOT --gold-deep (#a05f07). That token is "amber dark enough to be text on a
     light tint" and it is, at 4.3:1, when the ground is the panel itself. Here
     the ground is the panel PLUS this pill's own amber wash, which is darker,
     and the text is 13px -- measured 3.41:1, under AA. Same hue, darkened only
     as far as the contrast constraint required. Measured 4.89:1. */
  color: #7f4a05;
  box-shadow: inset 0 0 0 1px var(--card-line-strong);
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.rate-link:hover { background: rgba(160, 95, 7, .17); }
.rate-link:active { transform: scale(0.99); }

/* =========================================================
   HUMIDITY contrast — Ann: hard to read against the hero
========================================================= */
/* Override the dark-navy day/dawn scene rules with a brighter cream +
   text-shadow combo so humidity reads against ANY tone in the hero
   (bright sky, dark plum mountain, coral horizon). */
body[data-time-scene="day"] .temp-meta-label,
body[data-time-scene="day"] .temp-meta,
body[data-time-scene="dawn"] .temp-meta-label,
body[data-time-scene="dawn"] .temp-meta {
  color: #fff8e8;
  text-shadow: 0 1px 4px rgba(15, 12, 28, 0.55), 0 0 8px rgba(15, 12, 28, 0.35);
}

/* Scrub reference line — vertical solid line that follows the scrub
   position on each trend chart so the user can see exactly which
   hourly data point the tooltip is reading from. Snaps to data-idx
   (not raw finger x) for clean alignment with the chart curve. */
.trend-refline {
  position: absolute;
  top: 0;
  left: 0;
  width: 1.5px;
  height: 100%;
  background: rgba(28, 20, 50, 0.65);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.1s ease;
  z-index: 4;
}
.trend-refline.visible { opacity: 1; }

/* Legend under stacked sun/cloud chart */
.trend-legend {
  display: flex;
  justify-content: center;
  gap: 14px;
  font-size: 11px;
  color: rgba(31, 26, 20, 0.7);
  padding-top: 4px;
}
.trend-legend .legend-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.swatch {
  display: inline-block;
  width: 12px;
  height: 10px;
  border-radius: 2px;
}
.swatch-sun { background: rgba(255, 196, 86, 0.7); }
.swatch-cloud { background: rgba(120, 120, 140, 0.6); }
.swatch-temp { background: var(--wx-temp); }
.swatch-uv {
  height: 0;
  border-top: 1.5px dashed var(--wx-uv);
  background: none;
  width: 16px;
}
.swatch-rain-line {
  height: 0;
  border-top: 1.5px dashed rgba(58, 110, 178, 0.85);
  background: none;
  width: 16px;
}

.gear-fab {
  position: fixed;
  top: var(--sat);
  right: 12px;
  z-index: 80;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid rgba(31, 26, 20, 0.18);
  background: rgba(255, 255, 255, 0.6);
  border-radius: 50%;
  color: rgba(31, 26, 20, 0.62);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease;
  -webkit-tap-highlight-color: transparent;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.gear-fab:hover, .gear-fab:active { background: rgba(255, 255, 255, 0.85); color: #1f1a14; }
.gear-fab:active { transform: scale(0.92); }

/* Inline gear in page-1 topbar — small circle button next to F/C. */
.gear-inline {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  margin-left: 4px;
  border: 1px solid rgba(31, 26, 20, 0.18);
  background: rgba(255, 255, 255, 0.6);
  border-radius: 50%;
  color: rgba(31, 26, 20, 0.62);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  flex-shrink: 0;
}
.gear-inline:active { transform: scale(0.92); }

/* Topbar overflow guard: when location label is long (e.g. "San
   Francisco, CA"), city-trigger must shrink/truncate so F/C + gear
   don't get pushed off-screen on narrow phones. */
.topbar .city-trigger {
  min-width: 0;
  flex: 0 1 auto;
}
.topbar .city-trigger-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 140px;
}
.topbar .unit-toggle, .topbar .gear-inline { flex-shrink: 0; }

/* hover-tip must layer above the NOW pill / peak-trough markers
   (chart-y-labels z:2 incl. NOW pill). Ann: NOW pill blocking tooltip. */
.chart-hover .hover-tip { z-index: 10; }
.chart-hover .hover-line { z-index: 9; }

/* Make sure the chart-cond-icon SVG itself preserves aspect — earlier
   width:100%/height:100% combined with a non-square parent could squash. */
.chart-cond-icon svg {
  preserveAspectRatio: xMidYMid meet;
}

/* Update the bottom legend markers to match the in-chart icons. */
.chart-legend .legend-marker {
  width: 14px;
  height: 14px;
  background: none;
  position: relative;
}

/* Sunrise / sunset markers — HTML overlay on chart-wrap (not inside the
   SVG that has preserveAspectRatio="none"), so the icons stay round. */
.chart-suntime-marker {
  position: absolute;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}
.chart-suntime-marker svg { display: block; width: 14px; height: 14px; flex-shrink: 0; }
.chart-suntime-marker.is-sunrise {
  color: #f6b94a;
  filter: drop-shadow(0 0 4px rgba(246, 185, 74, 0.6));
}
.chart-suntime-marker.is-sunset {
  color: #b9a8ff;
  filter: drop-shadow(0 0 4px rgba(185, 168, 255, 0.6));
}
/* Time label inline beside the marker icon (icon in front). The whole
   icon+time group sits below the bar; JS positions it (centered on the
   sunrise/sunset x, clamped within the bar edges). */
.suntime-label {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.05em;
  color: var(--cream-subtle);
  line-height: 1;
}

/* Topbar lives ABOVE the hero now (Ann: location/F-C/gear 往上移动到顶部),
   so the hero's sun isn't covered. Regular flow, padded to clear the notch. */
.topbar.topbar-above-hero {
  position: static;
  padding: calc(var(--sat) + 6px) 12px 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  z-index: auto;
}

/* =========================================================
   CHAT — ask anything card
   Appended here; does not touch hero/scene/cloud CSS above.
========================================================= */

.chat-log {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 16px;
  max-height: 360px;
  overflow-y: auto;
  min-height: 0;
}
.chat-log:empty::after {
  content: "Ask about tonight, tomorrow's run, or the week ahead.";
  color: var(--cream-subtle);
  font-size: 13px;
  font-style: italic;
  font-family: var(--serif);
}
.msg {
  padding: 12px 16px;
  border-radius: 14px;
  max-width: 78%;
  font-size: 14px;
  line-height: 1.55;
}
.msg.user {
  background: var(--gold);
  color: #1a1206;
  align-self: flex-end;
  border-bottom-right-radius: 4px;
  font-weight: 500;
}
.msg.assistant {
  background: rgba(60, 40, 20, 0.05);
  border: 1px solid var(--card-line);
  color: var(--cream);
  align-self: flex-start;
  border-bottom-left-radius: 4px;
}
.msg.assistant.pending { opacity: 0.62; font-style: italic; }
.msg.assistant.pending::after {
  content: "…";
  display: inline-block;
  margin-left: 2px;
  animation: pendingDot 1.2s steps(3, end) infinite;
}
@keyframes pendingDot {
  0%   { content: "."; }
  33%  { content: ".."; }
  66%  { content: "..."; }
  100% { content: "."; }
}
.msg.assistant.rate-limited {
  font-style: italic;
  color: rgba(244, 235, 212, 0.85);
}
.msg.assistant.rate-limited.scope-user {
  background: rgba(255, 200, 87, 0.09);
  border-left: 2px solid rgba(255, 200, 87, 0.55);
}
.msg.assistant.rate-limited.scope-global {
  background: rgba(230, 126, 85, 0.10);
  border-left: 2px solid rgba(230, 126, 85, 0.6);
}
.msg.assistant.error {
  background: rgba(229, 123, 91, 0.10);
  border-left: 2px solid rgba(229, 123, 91, 0.55);
  color: var(--gold-soft);
  font-style: italic;
}

.chat-form {
  display: flex;
  gap: 10px;
  align-items: center;
}
.chat-form input {
  flex: 1;
  padding: 13px 18px;
  border-radius: 100px;
  border: 1px solid var(--card-line-strong);
  background: rgba(60, 40, 20, 0.04);
  font-family: var(--sans);
  font-size: 14px;
  color: var(--cream);
  outline: none;
  transition: border-color .15s, background .15s;
}
.chat-form input::placeholder { color: var(--cream-subtle); font-style: italic; }
.chat-form input:focus {
  border-color: var(--gold);
  background: rgba(60, 40, 20, 0.07);
  box-shadow: 0 0 0 3px rgba(255, 182, 40, 0.18);
}
.chat-form button {
  width: 44px; height: 44px;
  border-radius: 50%;
  border: none;
  background: var(--gold);
  color: #1a1206;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: transform .07s, opacity .15s;
}
.chat-form button:hover { opacity: 0.9; }
.chat-form button:active { transform: scale(.96); }
.chat-form button:disabled { opacity: 0.4; cursor: not-allowed; }
.chat-form button svg { width: 16px; height: 16px; }

/* =========================================================
   DESKTOP SINGLE-SCROLL LAYOUT — "Direction B" (Ann, 2026-05-28)
   >= 1024px: dissolve the horizontal swipe-pager into one vertical
   scroll. Hero banner flush at top, then headline / cards / trend
   charts stacked below in a centered column. Dots hidden.

   Cloud-clipping fix: on mobile the hero overflow:hidden is needed
   to contain the 220%-wide cloud-layer stripes. On desktop, we give
   the hero a tall fixed height and apply a CSS mask so the clouds
   blend naturally into the page background at the bottom instead of
   being cut by a hard rectangle.

   MOBILE (<= 820px) is BYTE-FOR-BYTE untouched.
========================================================= */
@media (min-width: 1024px) {
  /* 1. Un-pager: stack pages vertically instead of side-by-side */
  .pager {
    display: block;
    height: auto;
    overflow: visible;
    scroll-snap-type: none;
  }

  .page {
    display: block;
    width: auto;
    min-width: 0;
    overflow: visible;
    flex: none;
    scroll-snap-align: none;
    scroll-snap-stop: unset;
  }

  /* 2. Hide dot nav — not meaningful in single-scroll */
  .pager-dots {
    display: none;
  }

  /* 3. Hero: full-width banner, comfortable height, flush under topbar.
     The topbar is already in static flow (.topbar-above-hero uses
     position:static) so the hero naturally starts right below it.
     overflow:hidden keeps the cloud-layer SVGs (220% wide) from
     causing horizontal scroll; the bottom mask softens the cut edge. */
  .hero {
    /* Flattened (Ann): short banner so the first card (the 24h chart) is
       fully visible in the first viewport. */
    min-height: 240px;
    max-height: 280px;
    overflow: hidden;
    /* Soft bottom edge: fade from fully opaque at 75% height to
       transparent at 100%. Clouds compositing with page background
       feels like sky fading to horizon rather than clipping at a hard line. */
    -webkit-mask-image: linear-gradient(to bottom, black 0%, black 72%, transparent 100%);
    mask-image: linear-gradient(to bottom, black 0%, black 72%, transparent 100%);
  }

  /* FIX 1 — Sun + moon: raise into upper sky on desktop so neither is
     clipped by the 72%-→-transparent mask or occluded by the mountain
     silhouette layer. The mask starts fading at 72% of 240-280px ≈ 173px
     from top; keeping sun/moon centre at ≤22% (≈53px) puts them well
     above the fade zone. z-index:1 already clears mountains (z:0).      */
  /* Sun/moon: raise high into the open sky AND lift z-index ABOVE the hill
     layers (z:2-3) so the disc is never covered by the silhouette (the night
     moon was getting its bottom eaten by the mountain band). `.hero`-prefixed
     selectors (0,2,0) are REQUIRED so these beat the base `.moon`/`.sun` rules
     in sky-themes.css, which loads AFTER this file (a plain `.moon` override
     here loses the source-order tie and silently does nothing). */
  .hero .sun,
  .hero .flare,
  .hero .moon {
    top: 6%;
    right: 14%;
  }
  .hero .sun,
  .hero .moon {
    z-index: 5;
  }
  .hero .flare {
    margin-top: -71px;
    margin-right: -71px;
  }

  /* 4. body no longer needs overflow:hidden (pager isn't the scroll root) */
  body {
    overflow: auto;
    height: auto;
    /* FIX 3 — Desktop side gutters: replace the flat --night-1 tail with
       a subtly warm, atmospheric vertical gradient. The cream→warm-honey
       variation keeps gutters alive without competing with the centered
       content column. Mobile body background is untouched (this block is
       inside @media min-width:1024px). */
    background:
      linear-gradient(180deg,
        var(--sky-4)  0%,
        var(--sky-4) 320px,
        /* gentle warm-honey tint fading into the classic warm cream */
        #ede5d2      640px,
        #e8dfc8      960px,
        /* lower half: very soft blue-grey atmosphere — less bland than
           flat cream, still quiet enough not to distract */
        #ebe5d8     1400px,
        var(--night-2) 100%);
  }

  /* 5. main loses the viewport-clip sizing so it wraps content naturally */
  main {
    width: 100%;
    height: auto;
    overflow: visible;
  }

  /* 6. Centered content column — widened slightly from the 820px default
     in the 821px block; 880px feels slightly more generous on 1024+ screens.
     Cards and charts benefit from a bit more horizontal breathing room. */
  .page > .headline-band,
  .page > .card,
  .page > .trend-card,
  .trends-head {
    max-width: min(880px, 100% - 3rem);
    margin-inline: auto;
  }

  /* 7. Remove last-child padding-bottom that was compensating for fixed dots */
  .page > :last-child {
    padding-bottom: 24px;
  }

  /* 8. trends-head: top breathing room so it reads as a section start.
     No safe-area inset — same reason as the base rule: trends is mid-scroll
     now, not at the top of the viewport. */
  .trends-head {
    padding-top: 32px;
  }

  /* 9. Chart-first card order (Ann): on desktop the 24h "TODAY" chart is the
     first card, above Today's Kit. Flex + order reorders WITHOUT touching the
     HTML, so mobile (which never enters this breakpoint) keeps its order. */
  #page-utility {
    display: flex;
    flex-direction: column;
  }
  #page-utility > .hero { order: 0; }
  #page-utility > .headline-band { order: 1; }
  #page-utility > .card[aria-labelledby="chart-heading"] { order: 2; }
  #page-utility > .wear-card { order: 3; }
  /* In a flex column, margin-inline:auto + max-width makes a text-only card
     shrink to its content width (the Today's Kit card looked much narrower
     than the chart/trend cards). Force an explicit column width so every card
     matches; the hero has no width set, so it still stretches full-bleed. */
  #page-utility > .headline-band,
  #page-utility > .card,
  #page-utility > .wear-card {
    width: min(880px, 100% - 3rem);
    margin-inline: auto;
  }

  /* FIX 2 — Unit toggle: on desktop push it to the far right, grouped
     next to the gear icon, instead of floating awkwardly in the centre.
     City pill stays left by taking the remaining space with margin-right:auto.
     Mobile topbar layout (<=820px block) is byte-for-byte unchanged because
     this rule only fires inside @media (min-width: 1024px).               */
  .topbar.topbar-above-hero {
    justify-content: flex-start;
  }
  .topbar.topbar-above-hero .city-trigger {
    margin-right: auto;
  }
}

/* =========================================================
   2026-07-19 REDESIGN — "the sky is the page"
   =========================================================
   Ann: the old warm-cream field looked 黄黄的 and 不明媚. Eight recolors of it
   all failed, which was the signal: the problem was never the hue, it was that
   `main` had a 22px side padding that matted the ENTIRE app inside a colored
   frame. The sky was a picture pasted on a page instead of being the page.

   What changed (cards' internals, chart drawing and content are UNTOUCHED):
     1. sky bleeds edge-to-edge (kill the 22px mat); cards keep their own inset
     2. sky's bottom dissolves into the field via a mask (no hard rectangle cut)
     3. the field is a continuous atmospheric gradient, not a flat color
     4. topbar floats ON the sky as glass  (needs z-index !important — the
        earlier attempt computed to `auto` and the .sky painted over it)
     5. cards breathe (20px) and cast a lift shadow; trends match the top cards
     6. the page-2 trend charts now continue in this same vertical scroll
   ========================================================= */

@media (max-width: 1023px) {
  /* 1. un-mat: the sky reaches the screen edges */
  main { padding-left: 0; padding-right: 0; }

  /* ...and the gutter moves onto the cards themselves */
  .card, .wear-card, .trend-card,
  .temp-source-row, .chart-source-row { margin-left: 16px; margin-right: 16px; }

  /* 4. topbar floats over the sky. z-index MUST be !important here: an earlier
     rule wins otherwise and the computed value falls back to `auto`, which lets
     .sky paint straight over the city pill and the gear. */
  .topbar-above-hero {
    /* !important because .topbar-above-hero's own base rule pins position:static,
       and z-index has NO effect on a static element — so z-index:20 alone silently
       did nothing and .sky painted over the city pill.
       The companion `left/right/top: auto` overrides this used to carry are gone:
       the stale absolute-positioning declarations they fought are deleted from
       the base .topbar rule now. Do NOT re-add positional offsets to .topbar —
       on a relative element they become silent nudges (that is how the city pill
       ended up a third of the way down the hero on Dynamic Island phones). */
    position: relative !important;
    z-index: 20 !important;
    background: transparent;
    padding: 12px 18px 0;
  }
  /* DARK frosted glass, not white. These pills float on the sky, and the sky
     runs from near-black at night to pale blue at midday. A 24% WHITE veil goes
     lighter as the sky goes lighter, so its white label tracked the background
     instead of separating from it: 8.5:1 at night, 1.8:1 at noon. A dark veil
     moves the other way and holds >= 4.9:1 on every scene, night included,
     while still reading as frosted because the blur is what carries that. */
  .city-trigger, .gear-inline {
    background: rgba(27, 23, 16, 0.44);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid rgba(255, 255, 255, 0.28);
    color: #fff;
  }
  .city-trigger-label, .city-trigger-pin, .city-trigger-chevron { color: #fff; }

  /* 2. sky slides up under the topbar, and its bottom edge dissolves.
     The fade must start LOW (90%) — at 76% it washed out the big number and
     the ridge line underneath it (Ann: "温度最下面有些发白"). */
  .hero {
    /* The negative margin slides the sky up BEHIND the topbar; the matching
       padding pushes hero content back down so it still starts below the bar.
       Both must also absorb --sat: the topbar's box grows by --sat (its own
       notch padding), so a fixed -56px left a bare page-background strip
       exactly where the Dynamic Island sits. With --sat in both, the sky fills
       the notch region and every below-notch position is identical to the
       zero-inset layout. */
    margin-top: calc(-56px - var(--sat));
    padding-top: calc(56px + var(--sat));
    z-index: 1;
    -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 90%, rgba(0,0,0,.7) 96%, transparent 100%);
    mask-image: linear-gradient(180deg, #000 0%, #000 90%, rgba(0,0,0,.7) 96%, transparent 100%);
  }
  .hero-inner { padding-left: 26px; }

  /* Sit the pills exactly ON the safe-area boundary instead of 6px below it
     (Ann, 2026-07-25: "顶端的 location 和设置可以再往上提一点").

     var(--sat) is the whole budget here, and going past it is the thing to
     avoid: the Dynamic Island's resting hardware is ~48px tall, but iOS
     reports a 59px inset precisely because the island EXPANDS -- music, a
     timer, a call, Face ID. Content placed in that 11px margin looks fine in
     a screenshot and gets covered the moment a Live Activity starts. So the
     inset gets the whole budget and nothing more is taken from it.

     max(), not a bare var(--sat): this block also governs every OTHER surface
     under 1024px, and those have no notch, so --sat is 0 there. A bare
     var(--sat) resolved to 0 and pinned the pills flush against the top edge
     of the Chrome extension popup and of non-notch mobile browsers -- measured,
     not guessed. max() keeps the old 6px floor wherever there is no inset to
     clear, and hands the whole budget to the inset wherever there is one.

     Scoped to this mobile block rather than edited into the base rule at
     .topbar.topbar-above-hero, because that rule also governs desktop: its
     `calc(var(--sat) + 6px)` resolves to 6px there, and the desktop block's
     own `padding: 12px 18px 0` never applies (one class vs two, so it loses
     on specificity -- verified by measuring, not by reading). Editing the
     base rule would therefore have silently moved the desktop topbar too. */
  .topbar.topbar-above-hero { padding-top: max(var(--sat), 6px); }

  /* 5. breathing room + lift */
  .card, .wear-card, .trend-card {
    margin-top: 20px;
    margin-bottom: 20px;
    box-shadow: 0 16px 38px -18px var(--lift-shadow);
  }
  .wear-card { margin-top: 14px; box-shadow: 0 20px 46px -20px var(--lift-shadow-strong); }
  .temp-source-row { margin-top: 12px; margin-bottom: 0; }
  .chart-source-row { margin-top: 8px; margin-bottom: 4px; }

  /* trends were transparent when they lived on their own page; now that they
     continue the same scroll they take the same card language as the rest. */
  .trend-card {
    background: var(--card);
    border-radius: 22px;
    padding: 18px 20px 14px;
  }
}

/* =========================================================
   TWO COLOUR SCHEMES, one per sky theme (Settings > sky theme)
   ========================================================= */

/* --- Scheme 1 · DAWN (default, pairs with the Cinematic theme) -------------
   A clean peach-coral field with a warm bloom at the horizon. Note this is warm
   but NOT the old cream: high value, clean chroma. The dull greige-yellow was
   what read 黄黄的 / 不明媚; a bright dawn peach does not. Cards stay near-white
   so the field reads as light, not as a tint over everything. */
html { background: #fff0e9; }
body {
  background:
    radial-gradient(130% 55% at 50% 2%, rgba(255, 196, 168, 0.42) 0%, rgba(255, 196, 168, 0) 60%),
    linear-gradient(180deg, #ffd9c8 0%, #ffe6da 28%, #fff0e9 58%, #fff6f2 100%);
}
:root {
  --night-1: #fff0e9;
  --night-2: #ffe6da;
  /* tts --card, not pure white. Pure white next to a warm field reads as a
     different material; the design system's card is very slightly warm so the
     app and the about page look like the same paper. Left alone in the Minimal
     scheme below, which is deliberately cool/achromatic and would fight it. */
  --card: #fffdf8;
  --card-2: #fffaf7;
  --lift-shadow: rgba(92, 44, 28, 0.24);
  --lift-shadow-strong: rgba(92, 44, 28, 0.32);
}

/* --- Scheme 2 · PAPER & INK (pairs with the Minimal / woodblock theme) -----
   Near-achromatic: a cool paper-grey field, almost no hue. The contrast with
   Dawn is a whole axis (warm/chromatic vs cool/achromatic), not a second hue,
   so switching themes reads as a real change. Fits the theme's own name.
   `html body[...]` (not `body[...]`): sky-themes.css loads AFTER shinkai.css and
   defines the same custom properties at equal specificity, so a plain
   `body[data-theme="minimal"]` block here silently loses and the cards stayed
   cream. The extra `html` outranks it without needing !important. */
html body[data-theme="minimal"] {
  background:
    radial-gradient(130% 55% at 50% 2%, rgba(210, 222, 236, 0.40) 0%, rgba(210, 222, 236, 0) 60%),
    linear-gradient(180deg, #dfe5ec 0%, #e9eef4 28%, #f1f5f9 58%, #f7fafc 100%);
  --night-1: #f1f5f9;
  --night-2: #e9eef4;
  --card: #ffffff;
  --card-2: #f8fafc;
  --lift-shadow: rgba(22, 34, 50, 0.24);
  --lift-shadow-strong: rgba(22, 34, 50, 0.32);
  /* The minimal theme owns a different CARD LANGUAGE (flat fill, 8px radius,
     hairline ink border — woodblock, not the cinematic gradient card). Keep that
     language exactly; only take the cream out of its colour. */
  --card-minimal: #ffffff;
  --card-minimal-line: rgba(22, 34, 50, 0.18);
}
html:has(body[data-theme="minimal"]) { background: #edeefb; }

@media (max-width: 1023px) {
  /* trends inherit the ACTIVE theme's card language, not a third one */
  body[data-theme="minimal"] .trend-card {
    background: var(--card-minimal);
    border: 1.5px solid var(--card-minimal-line);
    border-radius: 8px;
  }
}

/* --- City indicators (one dot per saved city; swipe or tap to move) --------
   Fixed to the bottom so they stay visible while the page scrolls through the
   chart and trends. Hidden by JS when fewer than two cities are saved. */
.city-dots {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 12px 0 calc(14px + var(--sab));
  background: linear-gradient(180deg, transparent, var(--night-1) 62%);
  pointer-events: none;   /* the strip is decoration; only the dots take taps */
}
.city-dots[hidden] { display: none; }
.city-dot {
  pointer-events: auto;
  width: 7px;
  height: 7px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--cream-subtle);
  opacity: 0.55;
  cursor: pointer;
  transition: opacity .18s ease, transform .18s ease, background-color .18s ease;
}
.city-dot:hover, .city-dot:focus-visible { opacity: 0.85; }
.city-dot.active {
  background: var(--gold);
  opacity: 1;
  transform: scale(1.25);
}
.city-dot:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

/* Desktop (Ann chose to keep the dots here too, 2026-07-19). There is no swipe
   gesture with a mouse, so on desktop the dots are the primary city control and
   must be clickable. The mobile treatment is a full-width strip with a gradient
   scrim, which would band right across a 1440px screen — so here it becomes a
   compact centred pill instead. */
@media (min-width: 1024px) {
  .city-dots {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    bottom: 20px;
    gap: 12px;
    padding: 11px 18px;
    border-radius: 100px;
    background: var(--card);
    border: 1px solid var(--card-line);
    box-shadow: 0 12px 32px -14px var(--lift-shadow);
    pointer-events: auto;   /* the pill itself is a real control here */
  }
  .city-dot { width: 8px; height: 8px; }
}

/* =========================================================
   DESKTOP FIX for the trends-merged-into-page-utility change
   =========================================================
   Merging #page-trends into #page-utility added children the desktop block
   never knew about. Two consequences, both visible only at >=1024px (which is
   why verifying at 430px alone missed them):

     a) the desktop layout assigns explicit flex `order` to hero / headline /
        chart / wear only. Every newly-moved child defaulted to order:0 and so
        rendered ABOVE the ordered ones — trends appeared before the chart.
     b) the 880px centred column is applied to an explicit selector list that
        did not include the two source-caption rows, so they spanned the full
        viewport and their right-aligned text flew to the screen edge.
   ========================================================= */
@media (min-width: 1024px) {
  /* (b) the caption rows join the centred column. `width`, not `max-width`:
     #page-utility is a flex column here, and in a flex column
     `max-width + margin-inline:auto` shrink-wraps a text-only element to its
     content width — the captions are right-aligned, so that pulled their right
     edge ~356px inside the cards'. The existing cards already solve this the
     same way (see the comment above the .headline-band/.card width rule). */
  .page > .temp-source-row,
  .page > .chart-source-row {
    width: min(880px, 100% - 3rem);
    margin-inline: auto;
  }

  /* (a) a complete order sequence. Desktop deliberately puts the 24h chart
     above Today's Kit, so that inversion is preserved; the captions sit with
     whatever they describe, and the trends close out the column. */
  #page-utility > .card { order: 8; }                    /* default: chat card */
  #page-utility > .hero { order: 0; }
  #page-utility > .temp-source-row { order: 1; }
  #page-utility > .headline-band { order: 2; }
  #page-utility > .card[aria-labelledby="chart-heading"] { order: 3; }
  #page-utility > .chart-source-row { order: 4; }
  #page-utility > .wear-card { order: 5; }
  #page-utility > .trends-head { order: 6; }
  #page-utility > .trend-card { order: 7; }
}

/* `.headline-band` sets `display:block`, which BEATS the UA stylesheet's
   `[hidden]{display:none}` — so setting .hidden in JS did nothing and the band
   kept showing the template's placeholder ("San Francisco, right now") on every
   city. An author-level rule is required to make the attribute work. */
.headline-band[hidden] { display: none !important; }

/* The trend cards' card surface must apply at EVERY width. It was written
   inside the <=1023px block, so on desktop the trends stayed transparent while
   the cards above them were white. */
.trend-card {
  background: var(--card);
  border-radius: 22px;
}
html body[data-theme="minimal"] .trend-card {
  background: var(--card-minimal);
  border: 1.5px solid var(--card-minimal-line);
  border-radius: 8px;
}

/* Same flex-column shrink-wrap trap as the caption rows: `.trend-card` and
   `.trends-head` were only ever given `max-width`, so inside the desktop flex
   column they collapsed to their content width (591px and 208px) while
   `.card`/`.wear-card` — which carry an explicit `width` — stayed at 880px.
   Every element in the column needs the explicit width, not max-width. */
@media (min-width: 1024px) {
  #page-utility > .trend-card,
  #page-utility > .trends-head {
    width: min(880px, 100% - 3rem);
    margin-inline: auto;
  }
}

/* =========================================================
   DESKTOP: a phone-width canvas on a hazy field (Ann, 2026-07-19)
   =========================================================
   Full-bleed works on a phone, but on a 2000px screen it stretched the hero to
   roughly 8:1 — the sky became a thin band and the cloud layers (fixed 90-130px
   tall) read as one small squashed patch. Rather than grow the hero to match
   the width, keep the phone's aspect: constrain the whole app to a narrow
   centred column and let the surround be an out-of-focus continuation of the
   same atmosphere.
   ========================================================= */
@media (min-width: 1024px) {
  main {
    width: min(460px, 100%);
    margin-inline: auto;
    /* the column floats on the field */
    box-shadow: 0 40px 120px -40px var(--lift-shadow-strong);
  }

  /* Inside a 460px column every child is the column's width; the 880px rules
     written for the old wide layout would now overflow it. */
  #page-utility > .card,
  #page-utility > .wear-card,
  #page-utility > .trend-card,
  #page-utility > .trends-head,
  #page-utility > .headline-band,
  .page > .temp-source-row,
  .page > .chart-source-row {
    width: auto;
    max-width: none;
    margin-left: 16px;
    margin-right: 16px;
  }

  /* The hero keeps the phone treatment: full-bleed within the column, tall
     enough for the cloud layers to read, bottom dissolving into the field. */
  .hero {
    /* --sat is 0 on desktop; carried anyway so the two breakpoints cannot
       diverge if this block ever renders on an inset-bearing device. */
    min-height: calc(280px + var(--sat));
    max-height: calc(340px + var(--sat));
    margin-top: calc(-56px - var(--sat));
    padding-top: calc(56px + var(--sat));
    -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 90%, rgba(0,0,0,.7) 96%, transparent 100%);
    mask-image: linear-gradient(180deg, #000 0%, #000 90%, rgba(0,0,0,.7) 96%, transparent 100%);
  }
  .topbar-above-hero {
    /* !important because .topbar-above-hero's base rule pins position:static and
       z-index has no effect on a static element, so `z-index:20` alone silently
       does nothing and .sky paints over it. The old `left/right: auto` companions
       are gone with the stale absolute rules on .topbar they existed to cancel. */
    position: relative !important;
    z-index: 20 !important;
    background: transparent;
    padding: 12px 18px 0;
  }
  /* DARK frosted glass, not white. These pills float on the sky, and the sky
     runs from near-black at night to pale blue at midday. A 24% WHITE veil goes
     lighter as the sky goes lighter, so its white label tracked the background
     instead of separating from it: 8.5:1 at night, 1.8:1 at noon. A dark veil
     moves the other way and holds >= 4.9:1 on every scene, night included,
     while still reading as frosted because the blur is what carries that. */
  .city-trigger, .gear-inline {
    background: rgba(27, 23, 16, 0.44);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid rgba(255, 255, 255, 0.28);
    color: #fff;
  }
  .city-trigger-label, .city-trigger-pin, .city-trigger-chevron { color: #fff; }

  /* FROSTED surround, in two layers.
     A single white veil over a smooth gradient cannot look frosted: blurring a
     smooth gradient returns the same gradient. Real frost needs (1) something
     with structure behind it and (2) a diffusing layer on top.

     ::before paints soft colour blobs drawn from the sky palette — that is the
     structure. ::after is the glass: backdrop-filter blurs those blobs, a low
     white tint gives it body, and a fine SVG-turbulence grain supplies the
     matte "sanded" texture that separates frosted from merely translucent.
     Both are fixed + pointer-events:none, and `main` sits above them (z-index 1)
     so the column itself stays perfectly sharp. */
  body::before {
    content: "";
    position: fixed;
    inset: -10%;
    z-index: 0;
    pointer-events: none;
    background:
      radial-gradient(38% 42% at 18% 22%, var(--haze-a) 0%, transparent 70%),
      radial-gradient(42% 46% at 84% 30%, var(--haze-b) 0%, transparent 72%),
      radial-gradient(46% 40% at 30% 82%, var(--haze-c) 0%, transparent 70%),
      radial-gradient(40% 44% at 76% 78%, var(--haze-a) 0%, transparent 72%);
  }
  body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    backdrop-filter: blur(64px) saturate(135%);
    -webkit-backdrop-filter: blur(64px) saturate(135%);
    background-color: rgba(255, 255, 255, 0.30);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.32'/%3E%3C/svg%3E");
    background-repeat: repeat;
  }
  main { position: relative; z-index: 1; }

  /* the city pill re-centres over the narrow column, not the whole screen */
  .city-dots { left: 50%; transform: translateX(-50%); }
}

/* Blob colours the frosted surround diffuses, one set per scheme. */
:root { --haze-a: rgba(255,168,120,.55); --haze-b: rgba(255,214,170,.50); --haze-c: rgba(255,140,120,.38); }
html body[data-theme="minimal"] { --haze-a: rgba(150,180,214,.50); --haze-b: rgba(198,214,232,.48); --haze-c: rgba(120,150,190,.34); }

/* --- Kit split: wearing now / bring along -------------------------------
   Two labelled groups instead of one undifferentiated row. The reason is not
   tidiness: a morning kit that silently contains an evening sweater reads as
   an error ("it is 22 degrees, why a sweater?"), and grouping makes the
   sweater's reason self-evident before anyone has to ask. */
.kit-split { display: flex; flex-direction: column; gap: 12px; }
.kit-group-label {
  display: block; margin-bottom: 5px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--cream-muted);
}
/* Dashed, so "not on you yet" is legible without relying on colour alone --
   the label says it too, and the pair survives grayscale. */
.kit-chip-later { background: transparent; border-style: dashed; }
.kit-when { opacity: .68; font-variant-numeric: tabular-nums; }


/* What's-new strip. A quiet, single line at the very top, not the loud red of
   the severe-weather alert-banner: this is news, not a warning. Reuses the safe
   -area top padding so it clears the notch/Dynamic Island in the iOS webview. */
.whatsnew {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 95;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 16px;
  padding-top: calc(10px + var(--sat, 0px));
  background: linear-gradient(180deg, #2b2f3a 0%, #20242e 100%);
  color: #eef1f5;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 6px 20px -10px rgba(0, 0, 0, 0.5);
  font-family: var(--sans);
  animation: whatsnew-in .35s ease-out;
}
.whatsnew[hidden] { display: none; }
@keyframes whatsnew-in {
  from { transform: translateY(-100%); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .whatsnew { animation: none; } }
.whatsnew-text {
  flex: 1; min-width: 0;
  font-size: 13.5px; line-height: 1.4;
  color: #eef1f5;
}
.whatsnew-close {
  appearance: none; border: none;
  background: rgba(255, 255, 255, 0.08);
  color: #eef1f5;
  width: 28px; height: 28px; flex-shrink: 0;
  border-radius: 50%;
  font-size: 20px; line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.whatsnew-close:hover { background: rgba(255, 255, 255, 0.16); }
.whatsnew-close:focus-visible { outline: 2px solid #7aa7d9; outline-offset: 2px; }
